For the past four days I've been learning Haskell. As my first real program, I wrote a file backup utility - essentially a command line wrapper to rsync. The program was probably around 200 lines, just large enough for me to get comfortable with the language. There are many things I like about the language, and some that I don't, let's get started.
After working mainly with ruby for a while it's refreshing to go back to a statically typed language. But Haskell's type system is way better than that in Java. Now, I've used OCaml before, but that was a long time ago and I had a lot of trouble with it, so I must say I didn't "get it" at that time. This is the first time I've really gotten comfortable with this kind of type system, and I love it. When you have a type system like this, you have less need for writting tests, because the compiler catches a large class of errors for you. Like in Ruby, I write tests almost religiously sometimes because I want the luxury of the safety provided by the test suite: so that I can safely refactor my code later. Well, in haskell it's like you have that safety without having to write those tests. Granted, there are still cases when you should test for logic, but you will never have your code fail due to nil-when-you-didn't-expect-it or no-such-method(with Java it's NullPointerExceptions and ClassCastExceptions). In my short experience, it's mostly true that once you get the program to compile, "It's just works!"
The Infamous IO Monad
This works great in practice, not just in theory. All the discussions of static vs dynamic usually ignore type systems with type inference, and so are more of comparisons between old type systems and dynamic languages, and the dynamic advocates usually point out that it's easier for them to make refactorings involving changing the type of a variable. It's easier to show with an example. Let's say you have a piece of information that you need to pass to many different places in your code. At the beginning you use just a string to store it:
processFile(String filepath) ...
copyFile(String filepath, String filepath) ...
updateFile(String filepath) ...
processFiles(String filepaths) ...
Now let's say at a later time you find that a string is not enough, you need some more information to go along with each file path. you need:
Now you are screwed. Even Eclipse can't help you with this type of refactoring. You need to make probably hundreds of changes all through out your code base, including: 1) every method/constructor declaration that references filepath; 2) every instance variable that holds a filepath; 3) every local variable that holds a filepath; 4) all the places where you create a filepath; and 5) all the places where you need to actually perform operations with filepaths. In dynamic languages, you would only have to make changes for items 4 and 5, because type information is not explicitly declared at function and variable definitions. But, this is true also for type inferencing languages like haskell. Therefore, if you are using one of these languages, the refactoring argument for dynamically typed languages is a moo point. Haskell is like the best of both worlds. What's more, the compiler guides you through the refactorings, whereas with dynamic languages, you need to rely on testing to make sure your refactoring didn't break any thing.
To do any kind of IO in Haskell, you have to first grok the IO Monad. This is a scary proposition, but once you get it, you will be fine, and you'll be better for it. The way I look at it, there are 2 programming styles in Haskell: the functional style and the imperative style. The imperative style is required for any kind of IO operations, because IO operations are tainted, 'cause IO is a side effect, and functions shouldn't have side effects in the pure functional sense. To program in Haskell, you will need to be fluent in both programming styles. Now, the imperative style can be use for more than just programming IO, it's used for all monads. What's a monad, you ask? Oh brother, you'll have to ask me another day.
Weak Regex Support
Now, even though with all it's safety, you can still have runtime exceptions in Haskell. But exceptions can't happen in functional code, only imperative code. So, an example would be file-not-found. Unlike the interpreted languages though, you don't get a stacktrace when you have an exception, so this is actually a minus for Haskell.
Third Party Libraries
In Haskell, the package system is Cabal. It's closer to Python's Eggs, I would say, but slightly lower level. Because Haskell is a compiled language at heart(although there is a bytecode mode), there are more C-isms in Cabal. Many third party libraries require compiling C code, and I've had problems with many of these on cygwin.
Haskell Deserves a Great IDE
Give me my interactive shell or give me death! Thank god there's an interactive shell for Haskell. Again, best of both worlds. Although, the shell is limited - not like in ruby or python where you can pretty much type in any thing, in Haskell, you don't really have access to the full power of Haskell at the prompt. But still, it's a very very useful tool during rapid prototyping.
It's my opinion that a compiled language nowadays really need an IDE to be pleasant to work with. If for nothing else than the ease of getting error markers inside your code editor; clicking an error to jump to its line number; and having it auto-compile when you save a file. I used the haskell plugin for Eclipse briefly, but it stopped working after the first session - some Java runtime exception =( There's no reason that Haskell can't have all the features of the cool Java IDEs out there and more, other than manpower.
Haskell is not an OO system. And as such it's more vulnerable to namespace polution, because objects act like little namespaces. This shows up in some of the standard library code for example: hGetContents, hGetLine are actions that read from a file handle. Those are not pretty names. I guess h stands for handle. In an OO system, this could have been h.GetContents. The problem of namespace collison is handled at compile time. If you imported 2 modules that both have a function called map, for example, you'd have to prefix "map" with the name of the module, like Data.List.map. But needless to say, that is not nice. You could alleviate this somewhat by passing functions around as values(ah, caught myself before I said variables).
Overall, I really dig Haskell and I plan to use it for more of my work as well as play, and I am sure I will have more to say about it as I learn more. Like, I haven't even dug into testing in Haskell, and QuickTest looks really cool.