What is the Haskell response to Node.js?
Solution 1:
Ok, so having watched a little of the node.js presentation that @gawi pointed me at, I can say a bit more about how Haskell compares to node.js. In the presentation, Ryan describes some of the benefits of Green Threads, but then goes on to say that he doesn't find the lack of a thread abstraction to be a disadvantage. I'd disagree with his position, particularly in the context of Haskell: I think the abstractions that threads provide are essential for making server code easier to get right, and more robust. In particular:
using one thread per connection lets you write code that expresses the communication with a single client, rather that writing code that deals with all the clients at the same time. Think of it like this: a server that handles multiple clients with threads looks almost the same as one that handles a single client; the main difference is there's a
fork
somewhere in the former. If the protocol you're implementing is at all complex, managing the state machine for multiple clients simultaneously gets quite tricky, whereas threads let you just script the communication with a single client. The code is easier to get right, and easier to understand and maintain.callbacks on a single OS thread is cooperative multitasking, as opposed to preemptive multitasking, which is what you get with threads. The main disadvantage with cooperative multitasking is that the programmer is responsible for making sure that there's no starvation. It loses modularity: make a mistake in one place, and it can screw up the whole system. This is really something you don't want to have to worry about, and preemption is the simple solution. Moreover, communication between callbacks isn't possible (it would deadlock).
concurrency isn't hard in Haskell, because most code is pure and so is thread-safe by construction. There are simple communication primitives. It's much harder to shoot yourself in the foot with concurrency in Haskell than in a language with unrestricted side effects.
Solution 2:
Can Haskell provide some of the benefits of Node.js, namely a clean solution to avoid blocking I/O without having recourse to multi-thread programming?
Yes, in fact events and threads are unified in Haskell.
- You can program in explicit lightweight threads (e.g. millions of threads on a single laptop).
- Or; you can program in an async event-driven style, based on scalable event notification.
Threads are actually implemented in terms of events, and run across multiple cores, with seamless thread migration, with documented performance, and applications.
E.g. for
- massively concurrent job orchestration
- concurrent collections scaling on 32 or 48 cores
- tool support for profiling and debugging multi-threaded/multi-event programs.
- high performance event-driven web servers.
- interesting users: such as high-frequency trading.
Concurrent collections nbody on 32 cores
In Haskell you have both events and threads, and as it is all events under the hood.
Read the paper describing the implementation.
Solution 3:
First up, I don't hold your view that node.js is doing the right thing exposing all of those callbacks. You end up writing your program in CPS (continuation passing style) and I think it should be the compiler's job to do that transformation.
Events: No thread manipulation, the programmer only provides callbacks (as in Snap framework)
So with this in mind, you can write using a asynchronous style if you so wish, but by doing so you'd miss out on writing in an efficient synchronous style, with one thread per request. Haskell is ludicrously efficient at synchronous code, especially when compared to other languages. It's all events underneath.
Callbacks are guaranteed to be run in a single thread: no race condition possible.
You could still have a race condition in node.js, but it's more difficult.
Every request is in it's own thread. When you write code that has to communicate with other threads, it's very simple to make it threadsafe thanks to haskell's concurrency primitives.
Nice and simple UNIX-friendly API. Bonus: Excellent HTTP support. DNS also available.
Take a look in hackage and see for yourself.
Every I/O is by default asynchronous (this can be annoying sometimes, though). This makes it easier to avoid locks. However, too much CPU processing in a callback will impact other connections (in this case, the task should split into smaller sub-tasks and re-scheduled).
You have no such problems, ghc will distribute your work amongst real OS threads.
Same language for client-side and server-side. (I don't see too much value in this one, however. JQuery and Node.js share the event programming model but the rest is very different. I just can't see how sharing code between server-side and client-side could be useful in practice.)
Haskell can't possibly win here... right? Think again, http://www.haskell.org/haskellwiki/Haskell_in_web_browser .
All this packaged in a single product.
Download ghc, fire up cabal. There's a package for every need.
Solution 4:
I personally see Node.js and programming with callbacks as unnecessarily low-level and a bit unnatural thing. Why program with callbacks when a good runtime such as the one found in GHC may handle callbacks for you and do so pretty efficiently?
In the meantime, GHC runtime has improved greatly: it now features a "new new IO manager" called MIO where "M" stands for multicore I believe. It builds on foundation of existing IO manager and its main goal is to overcome the cause of 4+ cores performance degradation. Performance numbers provided in this paper are pretty impressive. See yourself:
With Mio, realistic HTTP servers in Haskell scale to 20 CPU cores, achieving peak performance up to factor of 6.5x compared to the same servers using previous versions of GHC. The latency of Haskell servers is also improved: [...] under a moderate load, reduces expected response time by 5.7x when compared with previous versions of GHC
And:
We also show that with Mio, McNettle (an SDN controller written in Haskell) can scale effectively to 40+ cores, reach a thoroughput of over 20 million new requests per second on a single machine, and hence become the fastest of all existing SDN controllers.
Mio has made it into GHC 7.8.1 release. I personally see this as a major step forward in Haskell performance. It would be very interesting to compare existing web applications performance compiled by the previous GHC version and 7.8.1.
Solution 5:
IMHO events are good, but programming by means of callbacks is not.
Most of the problems that makes special the coding and debugging of web applications comes from what makes them scalable and flexible. The most important, the stateless nature of HTTP. This enhances navigability, but this imposes an inversion of control where the IO element (the web server in this case) call different handlers in the application code. This event model -or callback model, more accurately said- is a nightmare, since callbacks do not share variable scopes, and an intuitive view of the navigation is lost. It is very difficult to prevent all the possible state changes when the user navigate back and forth, among other problems.
It may be said that the problems are similar to GUI programming where the event model works fine, but GUIs have no navigation and no back button. That multiplies the state transitions possible in web applications. The result of the attempt to solve these problem are heavy frameworks with complicated configurations plenty of pervasive magic identifiers without questioning the root of the problem: the callback model and its inherent lack of sharing of variable scopes, and no sequencing, so the sequence has to be constructed by linking identifiers.
There are sequential based frameworks like ocsigen (ocaml) seaside (smalltalk) WASH (discontinued, Haskell) and mflow (Haskell) that solve the problem of state management while maintaining navigability and REST-fulness. within these frameworks, the programmer can express the navigation as a imperative sequence where the program send pages and wait for responses in a single thread, variables are in scope and the back button works automatically. This inherently produces shorter, more safe, more readable code where the navigation is clearly visible to the programmer. (fair warning: I´m the developer of mflow)