I have to admit that, nowadays, I'm not a huge fan of Haskell. It may be that I learned it when "Real World Haskell" did not exist. I learned on excellent Hudak's book, and although it is a very nice book (as implied by the adjective "excellent") I think that the focus is quite too academical (which is an awkward criticism from me). When learning a language I like books which give me the pragmatic of the language (that is to say "how I'm I expected to write the code") but also practical examples. Something to hack myself in order to dig into the language.
Unfortunately back then most of the "nicer" example could be run easily on Windows or Linux but not OS X (some graphic library, IIRC). So... I grow tired of Haskell rather soon (it's not nice to code continuously referring to library indexes to see if what you are doing has already been done and trying to understand the logic of some very idiomatic piece of code you have not been introduced to).
RWH does a very nice job in teaching Haskell (and perhaps you can read Hudak as a second book, if you need). However, it came somewhat too late. Perhaps another time I will start with Haskell again.
Moreover, while code in RWH is idiomatic and readable, I think that many programming styles popular in the Haskell community (judging from pieces of code I stumbled upon surfing the web) tend to be so clever that "I'm not able to debug them". Not unless I have decomposed them...
For example, I quite like the idea that everything is curried. I got a BSc in maths before turning to computer science and I have seen plenty of complex function. I'm used to functions that take other functions as argument, return functions. Functions (sometimes called operators) which bridge spaces of functions and similar stuff.
Consequently I quite like the idea that + is a unary function which returns a unary function (that is to say an "adder").
def add(x): def adder(y): return x + y return adder
This is a bit awkward in Python, however you can use stuff in the functools module to do the trick. However, Haskell is built around this very concept. If you just write (+ 1) you have the "increment" function. While I think this is nice (after all I don't really like to write a full lambda to do such a thing), abuses are easy to spot.
Another thing I rather like is the fact that you can omit "unused" parameters.
E.g., it is perfectly ok to write:
inc = (+ 1)
Which makes sense: (+ 1) is the function which adds 1 and we just give it the name inc. So far so good. If you are not very happy with operators, the classical example is:
sum = foldr (+) 0
instead of
sum' xs = foldr (+) 0 xs
(these examples come from the point-free Haskell guide). And I agree (to a point). In fact I still believe that there should be just one way to do things; however, Haskell guides make it very clear that the first version is preferred. Moreover, you can always write down the second variant and then remove dummy variables.
What I especially like is how it stresses the functional nature of the language: you don't "define" (defun?) functions, you just give them names. Functions just are. Ok, to much lambda calculus lately.
The point is that Haskell programmers tell you that:
fn = f . g . his clearer than
fn x = f (g (h x))
On that I strongly disagree. Since I have been introduced to the function composition operator, I have found that most people get it wrong. I have no idea (afterall you just substitute the composition operator with an open parentheses and everything is fine). Now, that is not necessarily a problem: not everyone is destined to become a programmer (and an Haskell programmer).
On this I found some support in the lisp/scheme/clojure community:
Avoid functional combinators, or, worse, the point-free (or `point-less') style of code that is popular in the Haskell world. At most, use function composition only where the composition of functions is the crux of the idea being expressed, rather than simply a procedure that happens to be a composition of two others. Rationale: Tempting as it may be to recognize patterns that can be structured as combinations of functional combinators -- say, `compose this procedure with the projection of the second argument of that other one', or (COMPOSE FOO (PROJECT 2 BAR)) --, the reader of the code must subsequently examine the elaborate structure that has been built up to obscure the underlying purpose. The previous fragment could have been written (LAMBDA (A B) (FOO (BAR B))), which is in fact shorter, and which tells the reader directly what argument is being passed on to what, and what argument is being ignored, without forcing the reader to search for the definitions of FOO and BAR or the call site of the final composition. The explicit fragment contains substantially more information when intermediate values are named, which is very helpful for understanding it and especially for modifying it later on.
which comes from here. Of course this can be an instance of "Lisp syntax is not very good at doing this kind of things, so you should not even try them". And it is not hard to see that (compose foo bar) could not be used extensively as function composition in Haskell, as it carries more syntactic noise. Clojure actually has builtin comp and partial functions:
clojure.core/comp([f] [f g] [f g h] [f1 f2 f3 & fs]) Takes a set of functions and returns a fn that is the composition of those fns. The returned fn takes a variable number of args, applies the rightmost of fns to the args, the next fn (right-to-left) to the result, etc.clojure.core/partial([f arg1] [f arg1 arg2] [f arg1 arg2 arg3] [f arg1 arg2 arg3 & more]) Takes a function f and fewer than the normal arguments to f, and returns a fn that takes a variable number of additional args. When called, the returned function calls f with args + additional args.
Which is useful, are used sparingly.
However, when you start freely mixing the various features of Haskell syntax you have got a real mess. At least in my opinion. Functions taking multiple parameters in Haskell are really just unary functions returning functions.
E.g., (A x B) -> C in fact is just A->(B->C).
From the point of view of a programmer coming from another language, you can "bind" only a subset of the arguments of a function. Which, in Haskell terms means that you simply get a function instead of just a value.
For example, (map (*2)) is a function that doubles all the value of a list. It's just like that! And if you don't have the list in a variable: you can compute it on the fly (e.g. filter odd [1..10], which means take only the odd values from the range [1..10] -> 1, 3, 5, 7, 9).
The first tentative fails:
Prelude% map (*2) filter odd [1..10] %interactive%:1:9: Couldn't match expected type `[a]' against inferred type `(a1 -> Bool) -> [a1] -> [a1]' In the second argument of `map', namely `filter' In the expression: map (* 2) filter odd [1 .. 10] In the definition of `it': it = map (* 2) filter odd [1 .. 10]
Here the problem is that the "second parameter" of map should be a list. However the compiler finds "filter". Filter is a perfectly legal value for a function, only it has type (a1 -> Bool) -> [a1] -> [a1] and not [a]. In other words, we are passing a function where a list is expected.
Essentially the compiler just reads the arguments and tries to "fill" the parameters. In this case, however, it does not work. We should have written map (*2) (filter odd [1..10]).
See the parentheses? They do the right thing. Now the compiler knows that it is supposed to pass map the return value of the filter function.
However, now we have a "moral" problem: we have parentheses. Point free... does not work! We would have to write
f lst = map (*2) (filter odd lst)
instead of
f = map (*2) (filter odd)
In fact the latter is not valid. (filter odd) has type [a]->[a], not [a]. Still, I the use of parentheses is very readable in my opinion. But we want to use point free, remember? "Luckily" map (*2) has type [a]->[a] and (filter odd) also has type [a]->[a]. They can be composed! How?
map (*2) . filter odd
Now, if you are a real Haskell programmer, you are going to like it. I somewhat like it... it has some perverse elegance, I have to admit. Then imagine that you can compose this way any function. Haskell programs are highly composible.
In fact it is not hard to write very very complex functions just chaining other functions with . (and $). E.g., which is the function that computes the sum of all the perfect squares of a sequence of numbers, excluding the ones smaller than 50?
sum . dropWhile (< 50) . map (** 2)
The point is that there is no limit to the nonsense you can do this way.
No comments:
Post a Comment