haskelltypespolymorphismparametric-polymorphismrank-n-types

Is there a way to pass an operator with unknown type in haskell?


I have a function f op = (op 1 2, op 1.0 2.0), which is required to work like this:

f (+)
(3, 3.0)

But without declaring the type of f it works like this:

f (+)
(3.0, 3.0)

And I'm struggling with declaring the type of f. It should take an operator which works with all instances of Num. Is it even possible in Haskell?


Solution

  • The problem is that you forced the operator to work on Fractional types by applying it to fractional numbers such as 1.0 and 2.0. Your code typechecks because Fractional is a subtype of Num (meaning that each instance of Fractional is also an instance of Num).

    Following experiment in GHCi should make it clear:

    Prelude> :t 0
    0 :: Num p => p
    Prelude> :t 0.0
    0.0 :: Fractional p => p
    Prelude> :t 0 + 0.0  -- Fractional taking advantage!
    0 + 0.0 :: Fractional a => a
    

    So, if you want to make it work on Nums entirely, you just need to get rid of those ".0"s:

    Prelude> f op = (op 1 2, op 1 2)
    Prelude> f (+)
    (3, 3)
    

    However

    If you really need the behavior where the second element of returned tuple is Fractional and the first is some more general Num, the things get a bit more complicated.

    The operator (or actually, function) you pass to f has to appear as with two different types at the same time. This is normally not possible in plain Haskell, as each type variable gets a fixed assignment with each application – that means that (+) needs to decide if it is Float or Int or what.

    This can be, however, changed. The thing you will need to do is to turn on the Rank2Types extension by writing :set -XRank2Types in GHCi or adding {-# LANGUAGE Rank2Types #-} at the very top of the .hs file. This will allow you to write f in a manner in which the type of its argument is more dynamic:

    f :: (Num t1, Fractional t2)
      => (forall a. Num a => a -> a -> a) -> (t1, t2)
    f op = (op 1 2, op 1.0 2.0)
    

    Now the typechecker won't assign any fixed type to op, but instead leave it polymorphic for further specializations. Therefore it may be applied to both 1 :: Int and 1.0 :: Float in the same context.

    Indeed,

    Prelude> f (+)
    (3,3.0)
    

    Protip from comments: the type constraint may be relaxed to make the function more general:

    f :: (Num t1, Num t2)
      => (forall a. Num a => a -> a -> a) -> (t1, t2)
    f op = (op 1 2, op 1 2)
    

    It will work fine in all cases the previous version of f would do plus some more where the snd of the returned pair could be for instance an Int:

    Prelude> f (+)
    (3,3)
    Prelude> f (+) :: (Int, Float)
    (3,3.0)