2

Let's say I want to write a function to compute factorials of nonnegative integers. I could write something like this:

fact :: Num a => Int -> a
fact n = fromInteger(product [1..n])

(Let's not concern ourselves with negative values of n at the moment, as that isn't what my question is about.) If I write the function this way, it means that if I want to turn around and use it in an expression like

myExp x :: Double -> Double
myExp x = sum(map (\k -> x^k/fact(k)) [0..9])

directly, without bothering with converting the integer to a double.

Is there any reason not to do this? Am I losing something desirable that the type system would do for me?

I suspect that the only reason the arithmetic operators have restrictive types (e.g.

(/) :: Fractional a => a -> a -> a

so that 3.0 / (1 :: Int) is an error) is that the type system wouldn't be able to express things like "use the type of either operand, whichever is bigger." Am I correct?

2 Answers 2

2

In your example, calling fromInteger is redundant. You might as well have written:

fact :: (Num a, Enum a) => a -> a
fact n = product [1..n]

because product already has type Num a => [a] -> a (it's now also polymorphic on Foldable types instead of being specific for lists but let us forget it for the moment), and the built-in [1..n] syntax uses the Enum typeclass under the hood .

What your invocation of fromInteger does is to monomorphise the result of product to Integer, since that's the type of the argument of fromInteger, which is a Num instance as well so it works. Then, fromInteger returns again an arbitrary Num. So this is a redundant round-trip where you could have as well just used the generic Num value returned by product.

Then, in the other function, everything works out since Double is also a Num instance.

So in this case you are not loosing nor gaining nothing. What's the point of the hierachy of type classes of numeric types, then?

If you wrote your example in a language like C, that would have been an automatic promotion, which is fine (in this case, not always, as C programmers know). Instead, in Haskell, you write code as if you were exploiting promotions, without ever having promotions at all, since the code is monomorphised at the invocation site and operations are performed on the desired type from the start: the fact function (without the fromInteger redundant call) works on Doubles if its called to return a Double, it does not work on Integers just to promote the Integer result back to Double.

On the other hand, conversions on the other direction, i.e. coercions, have to be forbidden. This happens if you have a function returning a Fractional (e.g. a Double) and you pass it to a function that wants a Num. You would obtain a type mismatch and this avoids unexpected information loss.

6
  • The definition of fact you give above doesn't compile for me, which I expected, since at least in my environment \n -> product [1..n] has type a -> a (with constraints on a), which could only specialize to Int -> Int, not Int -> a, Are you using a different version of Haskell? I'm using GHC 8. Commented Feb 27, 2018 at 15:05
  • Yes, it was a typo in the type signature. I've updated it, also with the missing Enum a constraint that comes from the use of the [1..n] list syntax. Commented Mar 1, 2018 at 5:26
  • If I understand correctly, I think the middle of your answer is based on the premise that Double satisfies the constraints for the argument (and hence the result) of fact, but with the Enum constraint that's not true. Commented Mar 1, 2018 at 13:50
  • If the above comment wasn't clear, my problem is that the example in my question won't compile using your version of fact; it needs a fromIntegral somewhere or perhaps some other method of making the types match so the arithmetic can be done. Commented Mar 13, 2018 at 12:42
  • Yes you need fromIntegral to fit my version of fact in your example Commented Mar 22, 2018 at 2:26
2

First off, the code you've given:

fact :: Num a => Int -> a
fact n = fromInteger (product [1..n])

doesn't type check. So, that's a pretty good reason right there not to write the function that way. The problem is that n is of type Int, so product [1..n] is also of type Int, but fromInteger expects an Integer, not an Int.

You could replace fromInteger with fromIntegral, but you shouldn't. Observe:

fact1 :: Num a => Int -> a
fact1 n = fromIntegral (product [1..n])

and in GHCi:

> fact1 10
3628800
> fact1 100
0
> fact1 100 :: Double
0.0
> fact1 100 :: Integer
0
>

In comparison, a sensible implementation of a polymorphic fact might look like:

fact2 :: (Integral n, Num a, Enum a) => n -> a
-- OR:  fact2 :: (Num a, Enum a) => Int -> a
fact2 n = product [1..fromIntegral n]

and:

> fact2 100 :: Int
0    -- correct value in light of `Int` overflow
> fact2 100 :: Integer
9332...long number here...000  -- correct for infinite-precision integer
> fact2 100 :: Double
9.33262154439441e157   -- right again, when working with `Double`s
>

So, to answer your question, yes, you are losing something desirable that the type system does for you.

Specifically, the type system makes it more difficult to thoughtlessly convert between different numeric types which can result in the attributes of one numeric type (e.g., Int) unexpectedly leaking into apparent uses of other numeric types (e.g., Integer). If you throw fromIntegral and other polymorphic conversions into mathematical functions without carefully considering the implementation, you are likely to introduce similar sorts of errors.

So, how should you write mathematical functions in Haskell?

Well, as a general approach, polymorphic Haskell mathematical functions are typed and written to work within a specific numeric type to the extent possible. So, the signature of (+) is:

(+) :: (Num a) => a -> a -> a

meaning that the arguments each have the same type, and that will also be the type of the result. If I'm using an Int, I know that overflow may be an issue. If I'm using an Integer, I know that it won't. If I'm using a Double, I know that adding one to a huge number might still be equal to the huge number, but at least it won't be negative (as it might with Int).

If I combine a whole bunch of computations:

myRoot :: Float -> Float -> Float
myRoot a b c = (-b + sqrt (b*b - 4*a*c)) / (2*a)

I know that the entire computation is taking place with operations on Floats and not secretly involving Float-to-Double-to-Float conversions.

You should try to write your own mathematical functions this way, and leave conversions to the user of your function, where they will be clear and explicit, and the user will have (hopefully) demonstrated that they know what they're doing.

Now, fact is actually a bit of a special case. If you were going to follow the advice above (i.e., write functions that work within a single numeric type), then you might favor a signature more like:

fact3 :: (Num a, Enum a) => a -> a
fact3 n = product [1..n]

This is what @gigabytes suggested. This is probably okay, but it can lead to some odd results:

> fact3 2.5
6.0
>

The problem is that fact only makes sense as a factorial function when applied to integers, and thoughtless application to a non-integer probably represents a programming error. Better to let the type system help out by using one of the alternative type signatures above:

> fact2 2.5
...type error...
>

If the programmer really wanted to take a factorial of a non-integer, he or she should make the desired behavior explicit:

> fact2 (floor 2.5)
2
> fact2 (ceiling 2.5)
6
>

Similar reasoning leads to types like:

round :: (RealFrac a, Integral b) => a -> b

and the multiple power operators:

(**) :: Floating a => a -> a -> a
(^) :: (Integral b, Num a) => a -> b -> a
(^^) :: (Fractional a, Integral b) => a -> b -> a

which -- except for (**) -- don't follow the "work in one type" pattern.

All of this speaks to your last question. The reason that operations like:

(/) :: (Fractional a) => a -> a -> a

have restrictive types isn't that the type system can't express "use whichever type is bigger", it's that Haskell programmers don't WANT the type system to express that concept, at least in the context of different numeric types. They value the fact that (/) and div are different operators, and that the type of (/) makes it clear that the operation will be taking place within a particular numeric type without implicit coercions behind the programmer's back.

The drawback is that hundreds of new Haskell programmers have been stymied by:

mean xs = sum xs / length xs

but it's generally been considered a small price to pay.

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.