Part 1: function, currying, pattern matching and function composition
It makes sense that if some pattern/idea is used a lot, then it should be denoted with minial syntax. It follows that Haskell, famous for being functional, doesn’t even have a keyword for defining functions. To define a function, start with function name (add
for example), followed by argument list separated by spaces, followed by =
and then the function body:
add x y = x + y
print $ add 1 2
## 3
Note that the $
sign seperate print
and add
so the compiler knows add 1 2
should be treated/evaluated as one group and then the result goes into print
.
Functions are automatically curried, meaning that we can pass in fewer arguments and get back a more specific function.
For example, if we want to define a function that adds 2 to a number, we can do: add2 = add 2
. That is, the type of add
is Int -> Int -> Int
, or in English, add
takes an Int
and returns a function that takes an Int
and returns an Int
. I use Int
as a concrete example, actually the add
we defined is polymophic in that it can also operate on Double
, Float
etc.
add x y = x + y
add2 = add 2
print $ add2 3
## 5
Functions support pattern matching. For example, to calculate the distance to origin of a point, (3, 4), we can do
point = (3, 4) -- a tuple
distToOrigin (x, y) = sqrt $ x^2 + y^2 -- pattern match the elements of tuple
distToOrigin point
## 5.0
Function composition is denoted by .
(pronouned as “after”). For example, we can compose sqrt
and distToOriginSquared
to compute distance to origin:
point = (6, 8)
distToOriginSquared (x, y) = x^2 + y^2
distToOrigin = sqrt . distToOriginSquared
distToOrigin point
## 10.0
Part 2: data types
We can define a type with data
keyword, followed by the type constructor (the name of which is your choice).
For example, we can defined a list type that always has at least one element:
data NonEmptyList a = SingletonList a | Cons a (NonEmptyList a)
Here, NonEmptyList
on the left hand side (as well as right hand side) of the type definiton is an example of type constructor (as it’s name suggests, it constructs a new type), where as SingletonList
and Cons
on the right hand side of the type definition are data constructors, they are used to construct/instantiate data of that perticular type.
I guess if you’re familiar with java, python or other OOP languages, it helps to think of the type constructor as class definition, e.g.:
public class NonEmptyList<A> {
...
}
whereas data constructors are equivalent to the constructor methods, except that in java constructors all have to have the same name as the class. e.g.:
public class NonEmptyList<A> {
public NonEmptyList(A a) {
// constructor to construct a "Singleton" NonEmptyList
...
}
public NonEmptyList(A a, NonEmptyList<A> nonEmptyList) {
// constructor to construct a "Cons" NonEmptyList
...
}
}
Another similarity between data constructors in haskell and constructor methods in other languages (java) is that they are both functions: constructor in java is just a function that takes some arguments and returns/instantiates a concrete instance of the class, data constructors in haskell also return a value of different type.
Back to the haskell NonEmptyList
definition, |
means the NonEmptylist
is either a SingletonList
that contains a single element of type a
, or a Cons
that contains an element of type a
and another element of type NonEmptyList a
(note that NonEmptyList a
is a type constructed by the type constructor NonEmptyList
).
With this new data type, we can safely take the head of the nonempty list, without worry about any runtime exception being throw, by pattern matching against the two cases with the data constructor SingletonList
and Cons
:
:{
head (SingletonList x) = x
head (Cons x _) = x -- `_` means we don't care the value of the second parameter of the Cons
:}
aSingletonList = SingletonList 1
head aSingletonList
aComplicatedList = Cons 10 (Cons 20 (SingletonList 30))
head aComplicatedList
## 1
## 10
Note that because I’m using ghc -e
to compile the snippets (e
stands for expression), I need to use :{ ... :}
to tell ghc
that the two cases of my function definition should be evaluated together, otherwise ghc -e
will evaluate expressions line by line and throw a Non-exhaustive patterns
exception after it evaluates the first case.