pozorvlak: (Default)
pozorvlak ([personal profile] pozorvlak) wrote2006-12-04 09:02 pm

On Haskell

As I've mentioned a few times, I'm (re)-learning Haskell. Here are my current thoughts on the language. No doubt, when I have drunk of the Kool-aid in fullness, I shall look back on them and laugh/cringe at my naiveté. Please try not to get upset about them: if it helps, think of them as having big <at-my-current-state-of-knowledge> tags around them.
  • Haskell's syntax is nice. Clear and concise. As part of my Haskell-learning regime, I've been going through Paul Graham's paean to the Lisp macro, On Lisp1, re-writing the example code in Haskell. Thus far (ie, in the early, functional chapters), the Haskell code is generally shorter and clearer than the Lisp code it replaces, sometimes dramatically so. And recall that the Lisp code was written by an expert who's fanatical about brevity and writing for expositional purposes.
  • In particular, pattern-matching is a Good Thing, as is the (+3) notation for operators. I note that Paul Graham wants to add something like the latter to Arc, in the form of [_ + 3], but of course you could implement it fairly trivially in Common Lisp with read-macros.
  • On the other hand, Haskell's implementation of syntactic whitespace is needlessly complicated and unintuitive. Do you know the exact rules Python uses for syntactic whitespace? No, neither do I. That's because you don't have to.
  • Pervasive lazy evaluation is a major win (but see also...). A lot of the On Lisp macro examples can be seen as workarounds for the lack of lazy evaluation.
  • Pervasive partial application allows for some very nice effects, and could thus be construed as a win. However, it disallows optional and keyword arguments, prevents the existence of functions with the same name and different numbers of arguments, and in general rules out variadic functions (unless all the arguments are of the same type). All of these are Good Things. I haven't made up my mind which I'd rather have yet.
  • Haskell's type system is not a major win. It may in fact be a win, but so far I have seen little evidence to support this. So far, it's caused me nothing but pain while writing code, and it prevents you from expressing lots of useful and well-defined ideas as Haskell code. Yes, it helped me find a bug the other day, but probably not as quickly as I could have found it in a weakly-typed language. It's less expressive than the type system Ada had in 1987 - in particular, the lack of types parametrised by integers is a huge sucking chest wound disfiguring the language. It's possible Data.Generic may help here - I haven't got my head around that yet.
    On the other hand, hackers whose opinions I respect assure me that some day I will come to see the type system as a friend and ally, so I'm trying to keep an open mind about this point in particular.
  • On a similar note, type classes are an ugly crock.
  • Haskell's documentation is inadequate. I've written about this before: then, I'd had a particularly Bad Documentation Day and was ranting, but I stand by the substance of my comments. It occurs to me that this may reflect a cultural difference. I come from Perl, which (intentionally) makes very few guarantees, and encourages the wide use of downloaded libraries. Some Perl libraries are well-known, but most are very specialised, and you might well download a library from CPAN for one specific project, hack with it for a day or two, and never use it again. In this environment, good documentation is extremely important, and thus community standards for docs are very high. Occasionally, they're even satisfied :-)
  • Debugging tools are not luxury items, guys. I had a useful chat with Duncan over the weekend, and now intend to check out Buddha and Hat, but really, this stuff should come with the distribution.
  • Hugs and GHCi are the two least useful toplevels I've ever had the misfortune to use.
  • Referential transparency is a Seriously Good Thing, but then I thought that anyway. I'm not entirely convinced that the language should be enforcing it: it's often possible to write a side-effect free function that works by manipulating some state. Compiler optimisations, I suppose, and it's good to have "Here Be Dragons" signs around the non-transparent bits, in the form of the rather ugly monad syntax.
  • I miss variable interpolation. I tried to add it using Template Haskell, but ran into some problems: I'll post about that some time. I also miss having hashes2 at my fingertips. They're wonderfully useful things, and having them on a par with lists allows for some extremely cool/simple solutions to some problems. Syntax is not trivial.

By the way, I've fixed the juggling program. But then I was reading Burkard Polster's The Mathematics of Juggling on Saturday, and came across a much simpler algorithm: a siteswap [a_0,...,a_n-1] is juggleable iff the function (i |-> i + a_i mod n) is a permutation of [0..n-1] (by finiteness, iff it's a surjection). In Haskell (untested),

isJuggleable ss = and (map (`elem` test) [0 .. n])
        where test = map (`mod` n) $ zipWith (+) [0 .. n-1] ss
              n    = length ss
Or in Perl:
sub is_juggleable {
        $hit{$_ + $i++ % ($#_+1)}++ foreach @_;
        return scalar(keys %hit) > $#_;
}
See what I mean about the hashes? :-) Tests: er, that has a bug in it, but my girlfriend has been wanting to go home for a while now. I'll fix it tomorrow.

1I strongly recommend this exercise to everyone, actually - writing code helps you to absorb the ideas, and thinking about why the code's different in the two languages helps you to see the tradeoffs between their design decisions. And On Lisp is a great book, with rather more than its fair share of Keanu moments - those moments when you sit back and say "Whoa." The chapter on anaphoric macros is particularly cool.
2By "hashes", I mean associative arrays/dictionaries/etc, I'm not bothered about the implementation. Data.Map qualifies, but it's a lot less convenient than just being able to say $foo{$bar} = $fred, or $foo{bar} = $fred (barewords are treated as strings in hash indexes). Consider how ugly most Haskell code would be if there were no syntactic sugar for lists...

[identity profile] pozorvlak.livejournal.com 2006-12-05 06:15 pm (UTC)(link)
Your example of converting a GTK window into an IRC command is suggestive. It suggests that my mental model is mostly in terms of code that manipulates basic datatypes like strings, integers, floats, and closures, and lists/hashes/sets/etc of them, whereas you're thinking of code that manipulates more complex opaque datatypes representing higher-level constructs. It is of course possible to implement complex data in terms of simple data, but are you then doing work in your head that the compiler could be doing for you? When I'm coding in Perl, I'm thinking about what the variable contains, rather than how it's represented. "1" contains 1, albeit as a string. $cmd might contain an IRC command, hopefully represented with some nice OO interface provided by Net::IRC::Command or whatever. Passing it to a function which expects a GTK window would be fine at the time, but would cause a runtime error when the code tries to invoke an undefined method on it. Does this count as a type error? I suppose it counts as a "Does not walk like a duck" error...

When I read your comment, my first reaction would be "why would anyone try to do that?". But now I'm assuming a programmer who knows when a variable represents an IRC command (or, more generally, which implements the bit of the interface that $_ wants to use), which is effectively one who has some sort of type-checking subroutine running somewhere in $_'s head.

From man perltoot:
Some languages provide a formal syntactic interface to a class's methods, but Perl does not. It relies on you to read the documentation of each class. If you try to call an undefined method on an object, Perl won't complain, but the program will trigger an exception while it's running. Likewise, if you call a method expecting a prime number as its argument with a non-prime one instead, you can't expect the compiler to catch this. (Well, you can expect it all you like, but it's not going to happen.)

C++ functions, by the way, if handed arguments of the wrong type, will attempt to invoke constructors on their arguments to coerce them into the right types. IIRC, this only works with classes, not basic types. And I don't think it does this recursively, though that would be pretty cool :-)