Parts of this lesson involve things you can't do directly in ghci. I recommend starting up a text editor (Wordpad will do - anything will do as long as it can simply save things as text) and loading it in ghci by typing ":l foo.hs" (replacing foo.hs with the name of the file, of course). After making changes to the file, you can reload it in ghci by simply typing ":a" or even just ":". Specifically, datatype definitions can't be introduced in ghci at all, and pattern match candidates all have to be written on a single line, separated by semicolons.
--
Values in Haskell are built out of data constructors. Conceptually you could say every value is built that way - this isn't actually quite true, but you can pretend that it is. Types are equivalently built using type constructors. Both of them are introduced using the data syntax, also called datatype definition. Here's how it works, using a couple of examples from the Prelude:
Code: Select all
data Maybe a = Just a | Nothing
Code: Select all
data [a] = [] | a : [a] -- not actually legal Haskell, though you can pretend it is
data List a = Nil | Cons a (List a) -- the same thing, only substituting different names for the constructors. This one doesn't exist in Haskell by default.
Code: Select all
1 : (2 : (3 : [])) -- the parentheses are unnecessary but they clarify what's going on - first a list is constructed out of an empty list, then 3 is consed to it, then 2, etc..
Cons 1 (Cons 2 (Cons 3 Nil)) -- the same thing, using the differently named constructors
1 : 2 : 3 : [] -- equivalent to the first
1 : 2 : 3 -- not well-typed: 3 is not a list, so you can't cons a value to it. Try it and see the error the language gives.
Code: Select all
data Bool = True | False
Code: Select all
data Char = 'a' | 'b' | 'c' | 'd' ... -- not the way it's actually defined, but you can pretend it is
data Int = 1 | 2 | 3 | 4 ... -- again, it's not actually defined this way
Now that we know how to make values, let's see how to take them apart.
The trick is that functions don't just take arguments: They can pattern match to assert that their arguments have a certain structure. You can match on either data constructors or formal parameters. A data constructor matches itself, a formal parameter matches anything. A function definition may have any number of pattern match choices - when a function is called, they're tried one by one, top-down, until one matches.
Code: Select all
map f [] = []
map f (x:xs) = f x : map f xs
If the second argument of map is the empty list, then the first pattern is matched, and the call evaluates to [].
If the second argument is a value consed to a list, the second pattern is matched. x is bound to the value that was consed to the list, xs is bound to the rest of the list (corresponding to the two arguments given to the (:) constructor earlier).
Code: Select all
map (+1) [1,2] ==
map (+1) (1 : 2 : []) == -- second argument is a cons, so the second match is taken
(+1) 1 : map (+1) (2 : []) == -- second argument is a cons again, so second match taken again
(+1) 1 : (+1) 2 : map (+1) [] == -- second argument is the empty list, so first match is taken
(+1) 1 : (+1) 2 : []
Matching on anything but formal parameters and data constructors is impossible. If you try to match on the name of a function, what actually happens is that that name is bound to the value given as an argument. This tends to be frowned upon as it makes code confusing.
Here's a few examples of functions whose pattern match choices correspond almost directly to the appropriate data constructors:
Code: Select all
maybe n _ Nothing = n
maybe _ f (Just x) = f x
either f _ (Left x) = f x -- corresponds to data Either a b = Left a | Right b
either _ g (Right y) = g y
(++) [] ys = ys
(++) (x:xs) ys = x : xs ++ ys
foldr k z xs = go xs
where go [] = z
go (y:ys) = y `k` go ys
head (x:_) = x
head [] = undefined
[/quote]
The last two functions here use a few concepts we haven't handled yet. foldr uses where-binding, which is just a syntactic feature - it's exactly the same thing as let-binding, except that in let-binding you write the names to define first and the expression to evaluate after them (let [i]names[/i] in [i]expression[/i]) whereas with where you write the expression first and the names later ([i]expression[/i] where [i]names[/i]). head uses [b]undefined[/b], or [b]bottom[/b], which is a very strange value that will be explained in greater depth later.
A few final notes on syntax that you might have already noticed: Both type and data constructors are always written with a capital first letter (unless they're nonalphabetic such as (:), in which case they always start with a colon). Other names are written with a lowercase first letter.
Exercises:
1. Take a deep breath and meditate on the connection between data constructors and pattern matching. Play around with the functions given above if you like. Realise that in the code itself, there really is very little to the language but the lambda, data constructors to create values, pattern matching to take them apart, and names.
2. Write a datatype definition for days of the week, with one constructor corresponding to one weekday.
3. A binary tree is either a leaf containing a value, or a branch containing a value and two binary trees. Write the corresponding datatype.
4. Write a function that tells if a list is exactly 1 element long. It should have type [a] -> Bool.
5. Write a function that takes a binary tree and returns its values in a list in some consistent order (if you know what a preorder traversal is, do that). It should have type BinTree a -> [a].
As a hint for exercises 3 and 5, here's a datatype definition for Rose trees and a preorder traversal for them. A Rose tree, aka a multi-way tree, is either a leaf containing a value, or a branch containing a list of Rose trees.
data RoseTree a = Leaf a | Branch [RoseTree a]
preorder :: RoseTree a -> [a]
preorder (Leaf a) = [a]
preorder (Branch xs) = concatMap preorder xs
(concatMap is simply map, except it concatenates all of its results - which must be lists - together into a single list. You won't need it to write preorder for binary trees, but you do need (++))
As a hint for exercise 4, note that pattern matching on data constructors can be recursive. That is, it's possible to match on (x:[]), for instance. Also remember that pattern matches are tried one by one until one matches the structure of the given arguments, and that choice is then taken and evaluated.