I started learning Haskell and found a nice exercise. It's the following:
grouping: Int -> [Student]->[(Team, Student)]
grouping teamNumber = zip ys
where ...
So, the exercise wants that i try to fill the rest. The function should do the following:
Example : grouping 2 ['Mark','Hanna','Robert','Mike','Jimmy'] = [(1,'Mark'),(2,'Hanna'),(1,'Robert'),(2,'Mike'),(1,'Jimmy')]
.
So, we are building teams which consists of two Students, and the last Student 'Jimmy' has no teammates.
Then, I also look up what the predefined function zip
does. It gets two list arguments and connects each element of the lists to a tuple to build a list of tuples.
My idea: 1) I try to build two functions "grab" and "infinite". They are looking as following:
grap :: Int -> [a] -> [a]
grab _ [] = []
grab n (x:xs) = if n <= 0 then [] else x : grab (n-1) xs
infinite :: Num a => a -> [a]
infinite x = x : infinite(x+1)
So, what they do is: With infinite
I want to create an infinite list. And grap
should take n
elements of that. Example grap 2 (infinite 1) = [1,2]
.
I use these two in the first line of my where-declaration to fulfill the given function from above. So, I have:
grouping: Int -> [Student]->[(Team, Student)]
grouping teamNumber = zip ys
where
xs = grap teamNumber (infinite 1)
So, xs
is now my first list of zip
, especially the integer-list.
But now my question: zip
as predefined function requires also a second list, especially the list of the names of the students, but in the given function they give zip only one argument, namely the ys
as a list. How can I understand that?
Currying can be a bit confusing when you first encounter it. Here's the deal, pretty much (there are some technicalities I'm going to ignore).
The basic concept is this: in Haskell, every function takes just one argument. If you want to simulate a function that takes two arguments, there are two ways to do this:
Tuples
You can write a function that takes a tuple. This is the conventional approach in Standard ML, but is typically only used in Haskell in cases where it is very particularly the most sensible thing to do:
Currying
You can write a curried function. The concept behind currying is that when you apply a function to an argument you can get another function back that takes a second argument. I'll write this out very explicitly to begin with, using lambda notation:
Suppose I start with
(product 3) 4
. I can reduce(product 3)
first to getThen I can finish off the job, getting 12.
Haskell offers a bit of syntax to help with this sort of thing. First off, it lets me write
to mean
Secondly, it makes function application left-associative, so I can write
product 3 4
to mean(product 3) 4
.Finally, it makes the
->
type constructor right-associative, so I can writeproduct :: Double -> Double -> Double
instead ofproduct :: Double -> (Double -> Double)
.In Haskell, the following is equivalent:
(
(\x -> ...)
is of course Haskell's notation for anonymous, so called "lambda" functions (\
being a reminder for the Greek letterλ
.)In Haskell, functions are just like other values, so there's no special syntax for function calls, or "function pointers" etc.. As for the types, the above naturally entails
Stare at it for a moment.
So that's that about currying. Function calls are denoted just by juxtaposition in Haskell, and so it associates to the left (
f x y
is really((f x) y)
). And because Haskell definitions are automatically curried, the arrows in types associate to the right (a->b->c
is reallya->(b->c)
).Type of `grouping teamNumber`
Look carefully at the type of
grouping :: Int -> [Student]->[(Team, Student)]
, and the arguments that are being declared for its declarationWhat is the return type (the type on the right-hand side of the equals sign) if
grouping
is provided with all the arguments listed on the left-hand side of the equals sign?Answer
The type on the right-hand side of the equals sign is
[Student]->[(Team, Student)]
. In Haskell, a function that takes two arguments and returns a result can be equivalently seen or defined as a function that takes the first argument and returns a (function that takes the second argument and returns the result). So we could say, for example, that the expression(grouping 3)
is a function that takes a list of students and returns a list of those students, labeled in 3 groups. Presumably, if(grouping 3)
were applied to the list of students from your example, we would haveType of `zip ys`
What does currying have to do with the following type and expression?
What would the type of
zip ys
be if, for example,ys :: [Bool]
?What does this have to do with your question?
When you consider this together with the type of
grouping teamNumber
, how does this tell you what the type ofys
needs to be in your exercise?Putting it all together
From the exercise code (ignoring the types and the
where
clause) we have:Two things can only be
=
in Haskell if their types will unify. In this case, the type ofgrouping teamNumber
must unify with the type ofzip ys
.From the first part, we know that the type of
grouping teamNumber
is[Student]->[(Team,Student)]
.From the second part, we know that
zip ys
has the type[b] -> [(a, b)]
, wherea
is a type such thatys
has the type[a]
.Therefore, we know that (
~
is type equality in Haskell)These will unify if we substitute the following for the type variables
b
anda
Now, we know the type of
ys
is[a]
, which, if we make the same substitution, is[Team]
.Therefore, the types will be correct if
ys :: [Team]
.Conclusion
If you can provide a
ys :: [Team]
, you can produce a function from students to students tagged with their team ([Student]->[(Team,Student)]
) by passing theys
as the first argument tozip
. Such a function is exactly whatgrouping
needs to return when it has been applied to the single argument,teamNumber :: Int
.