Why does this expression have a valid type?

231 views Asked by At

Banging around in ghci, I happened to notice that the expression (*) 1 [1..5] apparently has a valid type.

:t (*) 1 [1..5]
(*) 1 [1..5] :: (Enum t, Num [t], Num t) => [t]

Apparently it is a list with several type constraints, including Num [t] which looks impossible to me, like it should give an error.

How is this the type of the expression? Why does ghci's :t command not give an error here?

3

There are 3 answers

0
Dan Robertson On BEST ANSWER

Let's look at how these constraints come to be to explain the type.

Numbers

In Haskell a literal number is replaced with a call to fromInteger (or fromRational if it has a decimal point or an 'e' in it). This way one can write '1' and have it be float or a double or an int or whatever. The type of fromInteger is

fromInteger :: Num a => a

So 1 gets desugared to fromInteger (1::Integer) which has type Num t => t

Ranges

In Haskell the syntax [a..b] is converted into the call enumFromTo a b and the type is enumFromTo :: Enum a => a -> a -> [a]. Putting these together we get

[1..5] == enumFromTo (fromInteger 1) (fromInteger 5) :: (Enum a, Num a) => [a]

Putting it all together

Now the type of (*) is Num b => b -> b -> b so we combine these all together to get:

(Num t,
Num a,
Enum a,
Num b,
t~b,
[a]~b) => b

Note that a~b means the types a and b are the same. Combining these gives the type

(Num a, Enum a, Num [a]) => [a]
8
Daniel Wagner On

Num [t] is not just possible, it's easy:

import Control.Applicative
liftA0 = pure -- hobgoblins, simple minds, etc.
liftA1 = fmap
instance Num t => Num [t] where
    (+) = liftA2 (+)
    (-) = liftA2 (-)
    (*) = liftA2 (*)
    negate = liftA1 negate
    abs    = liftA1 abs
    signum = liftA1 signum
    fromInteger n = liftA0 (fromInteger n)

So it would be awful for GHC to produce an error instead of inferring that your expression could be well typed with an appropriate instance.

Of course, it would also be awful to ever write this instance in real code, but GHC shouldn't be in the business of passing judgment on code like we humans do.

2
Iceland_jack On

This pattern of idiomatic/applicative lifting exists as Data.Monoid.Ap where Ap [] a specifies lifting operations with pure, fmap and liftA2: (+) = liftA2 (+):

>> :set -XDerivingVia
>> :set -XStandaloneDeriving
>>
>> import Data.Monoid (Ap(..))
>>
>> deriving via Ap [] a instance Num a => Num [a]
>>
>> 1 * [1..5]
[1,2,3,4,5]
>> [100,200] * [1..5]
[100,200,300,400,500,200,400,600,800,1000]

The behaviour for lists is derived via Ap [] a. You get different applicative behaviour through ZipList

>> import Control.Applicative (ZipList(..))
>> 
>> deriving via Ap ZipList a instance Num a => Num [a]
>>
>> 1 * [1..5]
[1,2,3,4,5]
>> [100,200] * [1..5]
[100,400]