3

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?

1
  • 1
    Have you tried using :type in GHCi to inspect the inferred type of f? Commented Dec 23, 2020 at 11:33

1 Answer 1

6

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)
Sign up to request clarification or add additional context in comments.

4 Comments

You can relax the Fractional constraint to Num. It'll be tightened by a fractional argument.
Yeah, I just wanted to replicate the behavior in the example. But good catch! Will include it
+1, though I'd add that the original poster may have been looking for a more specialized rank-2 type, like f :: (forall a. Num a => a -> a -> a) -> (Int, Double) or similar, with concrete types in the tuple.
You can add Ord to the rank-2 Num constraint, or even bump it up to Real, while still letting it with with Integer, Int, Double, etc.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.