Polymorphism
Sometimes, we want to express the fact that a certain function will work for any input type at all. For example, consider a function to find the length of a list of natural numbers:
lengthN : List(N) -> N
lengthN([]) = 0
lengthN(_ :: xs) = 1 + lengthN(xs)
Or how about a function to find the length of a list of rational numbers:
lengthQ : List(Q) -> Q
lengthQ([]) = 0
lengthQ(_ :: xs) = 1 + lengthQ(xs)
It is easy to see that lengthN and lengthQ are identical
except for their types, and that it is going to be very tedious if we
have to write a different version of this function for every possible
element type. The length of a list does not depend on
the elements at all, so we would like to be able to define it once and
for all, in a way that will work for any type of list.
Indeed, we can do exactly that, by using a type variable (any name that starts with a lowercase letter) in place of the concrete element type:
length : List(a) -> N
length([]) = 0
length(_ :: xs) = 1 + length(xs)
Here, the variable a can stand for any type. This expresses both
a requirement that the definition of length does not care what type
a is (and disco will actually check to make sure that is the
case), and a promise that length can be used on any particular
list. For example,
Disco> length [1,2,3]
3
Disco> length [True, False]
2
On the other hand, suppose we tried to define this function, which adds one to every element of a list:
incr : List(a) -> List(a)
incr([]) = []
incr(x :: xs) = (x + 1) :: incr(xs)
Disco does not accept this definition. The problem is that although
it promises that it will work on lists of any type, it doesn’t: it
tries to add one to the elements, and adding 1 is only something that
works for some types. For example, it doesn’t make sense to add 1 to
every element in a list of Booleans. incr will work fine if we
give it a more specific type, such as List(N) -> List(N).
Another good example is function composition, which takes two functions and connects the output of one function to the input of the other, creating a new function representing the “pipeline” of doing one function then the other. The input and output types of the functions don’t matter at all — other than the fact that the output type of the one function has to match the input type of the other. We can write it as follows:
compose : (b -> c) * (a -> b) -> (a -> c)
compose(f,g) = \x. f(g(x))
a, b, and c can all stand for different types (although
they are not required to be different). Notice, however, that the
input type of the first function is b, and the output type of the
second function is also b—hence no matter what type b
represents, they must be the same. The function that results takes an
input of type a and ultimately produces an output of type c
after running the input through both functions.
Note that type definitions can also be polymorphic; see that page for more information.