What Makes Haskell Unique, Part 1
A look at what separates Haskell from other languages commonly used in web development, and how to perform async I/O and concurrency in the Haskell language.
Join the DZone community and get the full member experience.Join For Free
I gave a talk this winter at the F(by) 2017 conference in Minsk, Belarus. The conference was great, I would definitely recommend it in the future. Thank you very much to the organizers for the opportunity to present on Haskell.
I prepared for this talk differently than I've prepared for other talks in the past. I'm very comfortable writing up blog posts but have always found slide preparation difficult. This time around, I wrote up the content in mostly-blog-post form first, and only created the slides after that was complete. Overall, this worked very well for me, and I'll try it again in the future (if others want to share their approaches to preparing talks, I'd definitely be happy to hear them).
As a result: I'm able to share the original write-up I did as well. For those who saw the live talk (or the video): you may want to skip towards the end, which covers some material that there wasn't time for in the talk itself.
If you'd like to follow with the slides, they're also available.
My name is Michael Snoyman. I work at a company called FP Complete. One of the things we do is help individuals and companies adopt Haskell and functional programming in general. And that leads right into the topic of my talk today:
What Makes Haskell Unique
Programmers today have a large number of languages to choose from when deciding what they will learn and use in their day-to-day coding. In order to make intelligent decisions about which languages to pursue, people need to be able to quickly learn and understand what distinguishes one language from another.
Given that this is a functional programming conference, it's probably no surprise to you that Haskell can be called a functional programming language. But there are lots of languages out there that can be called functional. Definitions vary, but let's take a particularly lax version of functional programming: first-class functions, and higher-order functions. Well, by this definition, even a language like C counts! You may want to limit the definition further to include syntactic support for closures, or some other features. Regardless, the same point remains:
Haskell May Be Functional, but That Doesn't Make it Unique
In fact, there's a long list of features I could rattle off that could be used to describe Haskell.
- Statically typed
- Strongly typed
- Green threads
- Native executables
- Garbage collected
Some of these features, like being pure and lazy, are relatively rare in mainstream languages. Others, however, are commonplace. What I'm going to claim is that not one of these features is enough to motivate new people to Haskell—including people in this audience—to start using it. Instead:
It's the Combination of These Features That Makes Haskell Unique
As an example: the intersection of purity, strong typing, and functional programming style, for instance, lends itself to a high-level form of expression which is simultaneously easy to write, easy to read, easy to modify, and efficient. I want to share some examples of some code in Haskell that demonstrate how the language encourages you to write code differently from other languages. And I'm going to try to claim that this "different" style is awesome, though it also has some downsides.
Async I/O and Concurrency
Let's start off with a use case that's pretty popular today. Look at this pseudocode and tell me what's wrong with it:
json1 := httpGet(url1) json2 := httpGet(url2) useJsonBodies(json1, json2)
Given the heading of this slide, you may have guessed it: this is blocking code. It will tie up an entire thread waiting for the response body from each of these requests to come back. Instead, we should be using asynchronous I/O calls to allow for the more efficient use of system resources. One common approach is to use callbacks:
httpGetA(url1, |json1| => httpGetA(url2, |json2| => useJsonBodies(json1, json2) ) )
json1 <- httpGet url1 json2 <- httpGet url2 useJsonBodies json1 json2
This may surprise you since this looks exactly like the blocking pseudocode I showed above. It turns out that Haskell has a powerful runtime system. It will automatically convert your blocking-style code into asynchronous system calls, and automatically handle all of the work of scheduling threads and waking them up when data is available.
This is pretty great, but it's hardly unique to Haskell. Erlang and Go, as two popular examples, both have this as well. If we want to see what makes Haskell different, we have to go deeper.
It's pretty lame that we need to wait for our first HTTP request to complete before even starting our second. What we'd like to do is kick off both requests at the same time. You may be imagining some really hairy APIs with threads, and mutable variables, and locks. But here's how you do this in Haskell:
(json1, json2) <- concurrently (httpGet url1) (httpGet url2) useJsonBodies json1 json2
Haskell has a green thread implementation which makes forking threads cheap. The
async library provides a powerful, high-level interface performing actions in parallel without bothering with the low-level aspects of locking primitives and mutable variables. And this builds naturally on top of the async I/O system already described to be cheap about system resource usage.
What we've seen already is elegant in Haskell, but it's not terribly difficult to achieve in other languages. Let's take it to the next level. Instead of needing both JSON response bodies, we only need one: whichever one comes back first. In pseudocode, this might look like:
promise1 := httpGet(url1) promise2 := httpGet(url2) result := newMutex() promise1.andThen(|json1| => result.set(json1) promise2.cancel()) promise2.andThen(|json2| => result.set(json2) promise1.cancel()) useJsonBody(result.get())
This code is tedious and error-prone, but it gets the job done. As you can probably guess, there's a simple API for this in Haskell:
eitherJson <- race (httpGet url1) (httpGet url2) case eitherJson of Left json1 -> useJsonBody1 json1 Right json2 -> useJsonBody2 json2
At first, this may seem like it's just a well-designed API. But there's quite a bit more going on under the surface. The Haskell runtime system itself supports the idea of an asynchronous exception, which allows us to cancel any other running thread. This feature is vital to making
And here's the final piece in the puzzle. All of the thread scheduling and canceling logic I've described doesn't just apply to async I/O calls. It works for CPU-intensive tasks as well. That means you can fork thousands of threads, and even if one of them is busy performing computation, other threads will not be starved. Plus, you can interrupt these long-running computations:
let tenSeconds = 10 * 1000 * 1000 timeout tenSeconds expensiveComputation
Summary: Concurrency and Async I/O
- Cheap threads
- Simple API
- Highly responsive
- Complicated runtime system
- Need to be aware of async exceptions when writing code
That's all for Part 1. Tune in tomorrow when we'll cover immutability and purity, reasoning about code, data races, how to know when to use mutability, and software transactional memory!
Published at DZone with permission of Michael Snoyman, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.