3

For example, with types, you can implement AdHoc polymorphism using Typeable to define a different behavior depending on what the input type is:

foo :: forall a. Typeable a => a -> a
foo | Just HRefl <- typeRep @a `eqTypeRep` typeRep @Bool = not
    | otherwise = id

-- >>> foo 3
3
-- >>> foo True
False

I would like to do something similar to that but instead check if a type has a curtain instance of a Constraint, and implement a different behavior on that.

So far I have looked at two packages, Data.Constraint and Data.Exists. I think the solution lies with one of them but I have not been able to understand how I would go about doing this.

For context, this came about when I was thinking about what a polymorphic safe integer division function would look like

polymorphicSafeDiv :: Integral a => a -> a -> Maybe a
polymorphicSafeDiv x y = undefined

If I want this function to work for both bounded and unbounded Integrals I need to consider both, division by zero, and the overflow scenario for minBound @Int / (-1). I would imagine something like

polymorphicSafeDiv :: Integral a => a -> a -> Maybe a
polymorphicSafeDiv x y
 | y == 0    = Nothing
 | y == (-1) = case (???) of
                 Just Relf -> Nothing
                 _         -> Just $ x `div` y
 | otherwise = Just $ x `div` y
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Mathias Sven
  • 349
  • 3
  • 7
  • 2
    https://github.com/mikeizbicki/ifcxt – Noughtmare Feb 18 '23 at 14:24
  • 2
    Better to use [`if-instance`](https://github.com/sheaf/if-instance) which doesn't require TemplateHaskell or creating an instance for each class. – Iceland_jack Feb 18 '23 at 14:36
  • 2
    Just be careful, because it is unsafe. Discussion [here](https://www.reddit.com/r/haskell/comments/x9k5fl/branching_on_constraints_ifinstance_applications/). – Iceland_jack Feb 18 '23 at 14:37
  • 1
    https://stackoverflow.com/questions/75182250/take-action-based-on-a-type-parameters-typeclass – Dmytro Mitin Feb 18 '23 at 14:43
  • I realized as I was writing my answer that your proposed solution isn't correct even if it were sanely possible. `minBound / (-1)` overflows for `Int`, but not for `Word`. – Daniel Wagner Feb 19 '23 at 18:12

2 Answers2

2

Using the if-instance as suggested in the comments leads to a very ergonomic way to express what I wanted, although any calling code will need the extension turned on as well

{-# OPTIONS_GHC -fplugin=IfSat.Plugin #-}

module Main where

import Data.Constraint.If ( IfSat, ifSat )

polymorphicSafeDiv :: forall a. (IfSat (Bounded a), Integral a) => a -> a -> Maybe a
polymorphicSafeDiv x y 
  | y == 0    = Nothing
  | otherwise = ifSat @(Bounded a) boundedSafeDiv optmisticDiv
  where
    optmisticDiv = Just $ x `div` y 

    boundedSafeDiv :: (Bounded a, Integral a) => Maybe a
    boundedSafeDiv
      | y == minBound `quot` maxBound = if x == minBound then Nothing else optmisticDiv
      | otherwise                     = optmisticDiv

main :: IO ()
main = do 
  print (polymorphicSafeDiv (minBound @Int ) (-1))
  print (polymorphicSafeDiv (minBound @Int ) (-2))
  print (polymorphicSafeDiv (minBound @Int ) 0   )
  print (polymorphicSafeDiv (minBound @Word) 0   )
  print (polymorphicSafeDiv (minBound @Word) maxBound)
  print (polymorphicSafeDiv (2^128)          (-1))
  print (polymorphicSafeDiv (2^128)          0   )

{-
Nothing
Just 4611686018427387904
Nothing
Nothing
Just 0
Just (-340282366920938463463374607431768211456)
Nothing
-}
Mathias Sven
  • 349
  • 3
  • 7
  • 1
    Again, this is not correct. The correct answer to `polymorphicSafeDiv (minBound @Word) (-1)`, a test you have not included here, should be `Just 0`, not `Nothing`. (Or, if you object that `(-1)` isn't a good `Word`, then the correct answer to `polymorphicSafeDiv (minBound @Word) maxBound` has the same problem -- it should be `Just 0`, not `Nothing`.) – Daniel Wagner Feb 23 '23 at 16:10
  • @DanielWagner Thanks for pointing it out! I have made changes to the answer to accommodate `Word`, the fact that `(-1)` matched against `maxBound @Word` completely escaped me – Mathias Sven Feb 23 '23 at 19:00
  • 1
    It's worth noting that `if-instance` is not entirely safe; there's a reason that the Haskell language itself provides no way to depend on a constraint **not** matching. The [github readme for `if-instance`](https://github.com/sheaf/if-instance#a-type-family-too) mentions that you can implement `unsafeCoerce` with its `IsSat` type family, but even the [earlier examples](https://github.com/sheaf/if-instance#when-does-branch-selection-occur) show different behaviour for `ifSat` in different modules, on the same type. It's useful, just be aware that ensuring it makes sense is your problem. – Ben Feb 23 '23 at 21:46
  • I think the corrected code given here is an *excellent* argument for making a new class. Checking ```y == minBound `quot` maxBound``` is an incredibly arcane way to get the effect you're going for. I don't even know how to think about it carefully enough to be sure whether it's right or not. By comparison, with a new `SafeDivide` class, you simply write what you mean in the implementation -- `Int` and friends check for `-1` while `Word` and friends don't. – Daniel Wagner Feb 24 '23 at 03:40
1

The idiomatic way to do this is to make a new class.

class    LowerBound a       where lowerBound :: Maybe a
instance LowerBound Int     where lowerBound = Just minBound
instance LowerBound Integer where lowerBound = Nothing

polymorphicSafeDiv :: (LowerBound a, Integral a) => a -> a -> Maybe a
polymorphicSafeDiv x = \case
    0  -> Nothing
    -1 -> case lowerBound of
        Just lb | x == lb -> Nothing
        _ -> Just (x `div` y)
    y  -> Just (x `div` y)

Actually, I'm not sure LowerBound is a great name for this. For example, Word has a lower bound, but dividing it by other things shouldn't produce Nothing. Perhaps you should make a division class directly.

class Integral a => SafeDivide a where
    safeDiv :: a -> a -> Maybe a
    default safeDiv :: Bounded a => a -> a -> Maybe a
    safeDiv x = \case
        -1 | x == minBound -> Nothing
        y -> testZeroDiv x y

testZeroDiv :: Integral a => a -> a -> Maybe a
testZeroDiv x = \case
    0 -> Nothing
    y -> Just (x `div` y)

instance SafeDivide Int
instance SafeDivide Integer where safeDiv = testZeroDiv
instance SafeDivide Word    where safeDiv = testZeroDiv
Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • I agree that doing runtime constraint-checking magic would not be the right way to implement this, It's just what made me think about whether that was possible! Since this is essentially what is done in Core (I think, the type and its instance dict get passed as args). Funnily enough, this function accidentally works for `Word` because its lower bound is `0`, so dividing by it should also produce `Nothing`, but it would be bogus for a Nat+ type – Mathias Sven Feb 19 '23 at 20:49
  • @MathiasSven But we are checking the *numerator* for equality with `minBound`. So no, it does not accidentally work for `Word`. Your proposal would incorrectly give ```0 `safeDiv` (-1) = Nothing``` instead of ```0 `safeDiv` (-1) = Just 0```. – Daniel Wagner Feb 20 '23 at 06:41
  • @MathiasSven I'm not sure what "this is what is done in Core" means. There's no Core term that dispatches on whether it was given a dictionary or not -- it wouldn't be possible to write a sensible term with a type that both did and did not have a dictionary argument first. (If you were writing Core by hand, you could pass a `Maybe (Dictionary c)`, say, but there is no corresponding surface Haskell that would produce that.) – Daniel Wagner Feb 20 '23 at 06:51
  • Apologies, I was thinking of another function I had implemented that only worked on bounded `Num`s, so `Int32`, `Int64`, `Word`... And in that case `minBound` would be zero so it would be fine... Indeed what I want to do is not really ergonomically since these types would pollute the codebase. `if-instance` looks interesting but I haven't been able to check it out yet, but it seems to be what I want. – Mathias Sven Feb 23 '23 at 04:29