Functional Dependencies
Observe the type of (+)
:
ghci> :t (+)(+) :: forall a. Num a => a -> a
This is quite different in Python:
>>> type(1 + 1)class <'int'>>>> type(1 + 1.0)class <'float'>>>> type(1.0 + 1)class <'float'>>>> type(1.0 + 1.0)class <'float'>
The +
operator in Python behaves heterogenously—when given
two int
s we get an int
; when given at least one
float
we get a float
. How would we encode this in
Haskell?
Simple! Create a multi-parameter typeclass that describes the argument types and the result type!
class (Num a, Num b, Num c) => HAdd a b c where (+#) :: a -> b -> c
Then we can write instances for the possible permutations of the desired types:
instance Num a => HAdd a a a where (+#) :: a -> a -> a (+#) = (+)
instance HAdd Int Double Double where (+#) :: Int -> Double -> Double x +# y = fromIntegral x + y
instance HAdd Double Int Double where (+#) :: Double -> Int -> Double x +# y = x + fromIntegral y
However, trying to use (+#)
is very cumbersome:
ghci> x :: Int = 1ghci> y :: Double = 2.0ghci> x +# y<interactive>:3:1: error: - No instance for (HAdd Int Double ()) arising from a use of 'it' - In the first argument of 'print', namely 'it' In a stmt of an interactive GHCi command: print itghci> x +# y :: Double3.0
This occurs because without specifying the return type c
,
Haskell has no idea what it is since it is ambiguous! As per the
definition, no one is stopping us from defining another
instance HAdd Int Double String
! On the other hand, we know
that adding an Int
and a Double
must result in
a Double
and nothing else; in other words, the types of the
arguments to (+#)
uniquely characterizes the resulting
type.
The way we introduce this dependency between these type variables by
introducing functional dependencies on typeclass declarations, which,
adding them to our declaration of HAdd
, looks something like
the following:
{-# LANGUAGE FunctionalDependencies #-}class (Num a, Num b, Num c) => HAdd a b c whereclass (Num a, Num b, Num c) => HAdd a b c | a b -> c where (+#) :: a -> b -> c
The way to read the clause a b -> c
is “a
and b
uniquely
characterizes/determines c
”, or in other words, c
is a function
of a
and b
, i.e. it is not possible that given a fixed a
and b
that we have two different inhabitants of c
. This (1) prevents the
programmer from introducing different values of c
for the same a
and
b
(which we haven’t) and (2) allows the compiler to infer the right
instance just with a
and b
alone.
ghci> x :: Int = 1ghci> y :: Double = 2.0ghci> x +# y3.0ghci> :{ghci| instance HAdd Int Double String whereghci| x +# y = show xghci| :}<interactive>:8:10: error: Functional dependencies conflict between instance declarations: instance [safe] HAdd Int Double Double -- Defined at <interactive>:17:10 instance HAdd Int Double String -- Defined at <interactive>:21:10