
PragmaticBookshelf
Spotlight: Rebecca Skinner (Author) Interview and AMA!
Author Spotlight
Rebecca Skinner
@RebeccaSkinner
Welcome to our latest author spotlight, where we sit down with Rebecca Skinner, author of Effective Haskell. This book puts the power of Haskell to work in your programs, learning from an engineer who uses Haskell daily to get practical work done efficiently. Discover how to realize the benefits of a pure functional language, like protecting your code from side effects. Learn to employ advanced programming concepts to solve real-world problems. Don’t just learn the syntax, but dive deeply into Haskell as you build efficient, well-tested programs.
This is also an AMA. Everyone commenting or asking a question will automatically be entered into our draw to win a copy of Rebecca’s book!
Most Liked

RebeccaSkinner
Please introduce yourself
I’m Rebecca Skinner, and I’m the author of Effective Haskell. I’ve been writing Haskell for over ten years now. For the last two and a half years or so I’ve been volunteering on the Haskell.org committee, which is the group that helps manage the Haskell.org website and some of the Haskell infrastructure. I suppose you could say that I’m really enthusiastic about Haskell.
For people who are not familiar with the Haskell language, how would you introduce it to them?
Haskell is a pure functional programming language that puts a strong emphasis on letting programmers build precise abstractions to represent particular problem domains. Haskell has a lightweight and flexible syntax. Combined with lazy evaluation and a strong type system, Haskell is particularly well suited for building APIs and designing systems that need to capture the complexities of different technical or business domains and present them to users in a way that is both easy to use and hard to misuse.
You started off by saying that Haskell is a pure functional language. So could you get into that a little bit and talk about how that affects coding?
I think most programmers these days have exposure to functional programming. Newer languages like Rust, Swift, and Kotlin have taken a lot of inspiration from functional languages, including Haskell. There’s also a pretty big contingent of people in the JavaScript and TypeScript ecosystem who are enthusiastic about functional programming. Even C++ and Java have started to get more features inspired by functional programming. I think the most surprising thing when you move from an object-oriented or multi-paradigm language to a functional programming language is how much working in a functional language affects the way that you think about your program at the architectural level.
Although functional programming has a reputation for being a very high-level abstract way of thinking about programs, in a lot of ways it leads us to a more concrete and direct way of thinking about architecture and how we solve problems. In an object-oriented language, programming can be very indirect. If you want some specific output, you need to call a method on one object that sends a message to another object and so on. Abstraction in an object-oriented language generally involves adding more layers of indirection through objects and interfaces. Functional programming frequently makes the abstraction more direct because there’s a focus on composing smaller pieces of functionality on the fly. You can start to see this when you use functional features in non-functional languages. For example, using higher-order functions like map encourages you to directly write a lot of small functions that do one particular transformation on an element and then pass them to the map function. In a more traditional OO language, where composition is harder, you’re often faced with the choice to predict all of the different operations you might want to do on your data ahead of time, or else you need to add design patterns like factories and adapters that work around the lack of composition. Those design patterns can make it hard to follow what your program is doing.
While most programmers have had some exposure to the idea of functional programming in their languages thanks to the new features that are getting added, people are often a bit less comfortable with the idea of pure functional programming. In a pure functional language, all of our values are immutable and we avoid any arbitrary external side effects, like printing things to the screen or talking to the network. At first this can seem really limiting, because it focuses on all of the things that we can’t do, but in practice it turns out to be extraordinarily useful when we are working in a functional language because it means that we can combine code freely without ever having to worry about accidental or unintended side effects. We’re also not totally prohibited from having side effects when we need them, they just aren’t the default. In Haskell we already have a lot of really nice ways to combine smaller parts of our program into a larger program, and we reuse these same capabilities to let us manage side effects. In Haskell, if you want to do something like open a file or make an HTTP request, you create an IO action that will do that thing for you when the program runs. You can combine all of these little IO actions into one big one, and that big IO action that you created is what does all of the work when you actually run your program.
Could you discuss how Haskell APIs promote safety?
A Haskell API really is just describing and presenting a surface for someone to interact with our code. The difference is that Haskell gives us a lot of flexibility so that we can be very precise about how we describe that interface, and it lets us limit the interactions so that people can’t use our program incorrectly. Pure functional programming is a great tool for this, because we can design an API where we can very precisely control what inputs a user needs to accept and what outputs they need to return. When we know that code the user is passing in won’t have any side effects, it makes it easier to have confidence in how our code will be used. For example, if I’m building an API to interact with a system that requires authentication and has security policies in place, I can ask the user to pass me pure functions, and I know that any private information I pass into those functions won’t leak out, because the function can’t have any side effects.
How can Haskell help you with asynchronous code?
There are a couple of different ways that we deal with asynchronous programming In Haskell. The async library is one way we can do asynchronous programming, and it’s a great example of the point I was making earlier about how Haskell’s purity and type system can help us design APIs that make it easier to do resource management. The async library lets us write asynchronous code without worrying about leaking resources and forgetting to wait on threads, and it does it by treating the threads as the resource that the library is managing. When we use the library, we can say, “Hey, run this code asynchronously while you also run this other code,” and the library will create a new thread and pass it in as an argument to your function. You can do anything you might need to do, like sending a signal to the thread or canceling it, but it’s only in scope while your function is running and it will be cleaned up for you automatically.
The STM library is another example of a really great library that makes it easy for us to deal with asynchronous code in a way that feels familiar to anyone who has done async programming in other languages, but we get some really nice extra properties from STM compared to the usual synchronization primitives. STM stands for “software-transactional memory” and it makes it easy to write small individual parts of a program that each deal with some concurrency, and we can compose those effectively into a larger functions without any individual part of the application needing to know how the other parts of the program are handling their own concurrency.
Really, being able to compose functions is where I think Haskell shines whether those functions are directly needing to think about concurrency or not. If we think about a language like JavaScript that handles asynchronous code with promises, you see a pattern that comes up repeatedly where you need to combine promises into a bigger promise so that you can run more of the code asynchronously. In practice you can end up with a pretty large asynchronous program that gets run in the end and it’s just going to, for example, call one API and then wait for the response and pass that response to the next API, wait for that response, and so on. It turns out that that is almost exactly the same problem you have when you think, “Okay, well, I’m reading a file from disk, and then I’m writing some of the file to the screen, and then I’m going to wait for user input.” There are other examples that aren’t quite as obviously the same until you think about it: I have this function that might fail, but if it doesn’t, I want to take its output and pass it into this next function that might also fail, or I have this list of values and I want to take each one and pass it to a function that will return its own list.
When I talk about the ways that Haskell’s type system can help us write nicer APIs, this is another great example. Thanks to higher-kinded types, we can define these really broadly useful libraries that work with all different sorts of things that we might want to combine like this. We call them Applicatives and Monads. Haskell itself also has some special syntax called “do notation” that makes it easier to work with Monads. The underlying features that Haskell has for dealing with asynchronous programming are nice, but the fact that we can reuse so many useful libraries when we’re working with asynchronous code is an even bigger benefit.
Is this similar to the notion of generics or protocols in other languages?
Yeah, exactly. If we’re talking about generics in a language like Java, we have generics that let us write a value like “a list of type T
” where we can make T a string or number or another list. We can do that in Haskell too, but we can go a bit further and instead of being specific about having a list, we can make that generic as well. You can kind of get there with interfaces, but the higher-kinded types give you a lot of expressive power that you just don’t have in other languages.
How do you interact from Haskell with, for example, a C library or Java libraries, or just external libraries, so that you can take advantage of that code? Either directly, or by building some interop bridge?
Haskell has very good support for interoperating with the C ABIs. It’s pretty straightforward to use C libraries from Haskell, and a lot of other languages support the C calling convention, so we can generally interoperate with most other natively compiled languages like C++, Rust, and Go either directly or with only some minor modifications or a small shim. We can go the other direction too, and create native libraries from Haskell that can be used in other languages. There are also tools like C2HS that let you automatically generate language bindings for Haskell from larger C applications, and there are a number of projects that let you compile Haskell code into other languages.
How can people keep track of what you’re doing and follow you?
My website is https://rebeccaskinner.net, and I’m often active on Twitter at cercerilla. For anyone who is reading Effective Haskell, I’m also active on the DevTalk forum, where I love getting feedback and answering questions about the book.
Thank you for taking the time to speak with us. This has been great!

RebeccaSkinner
That’s a great question. There’s a lot of overlap, between the two, but there are some things you can do with a switch statement that you can’t do with pattern matching, and vice versa.
Let’s look at an example of where they overlap. One common way that people use switch
statements is to pick a choice from a set of potential options. For example, if you were writing a program to display a list of files, you might want to match specific file extensions and show a more human readable name for the file type. In a language with switch
statements you might say (in C-like pseudo-code):
string get_file_type_description(string file_name) {
string extension = get_file_extension(file_name);
switch (extension) {
case "png":
return "Image Fiile";
case "txt":
return "Plain text file";
default:
return (extension + " file");
In this example, we’re matching each branch of the switch
statement with an exact value, and returning a value based on that. In Haskell we can do the same thing with pattern matching. The usual way would be using a case
expression, which looks pretty similar to switch
:
getFileDescription :: FilePath -> String
getFileDescription fileName =
case getFileExtension fileName of
"png" -> "Image File"
"txt" -> "Plain text file"
extension -> extension ++ " file"
The most notable difference in this example is that when we’re using pattern matching, we don’t have keywords like case
and default
. Instead, each branch starts with the pattern we want to match against (in this case, the exact string we want to match on). Instead of a default
case, we just provide a variable that will end up holding whatever value is there if we didn’t match one of the other branches.
It’s a little surprising that the very common use case for switch statements and pattern matching with case
overlap so much, because in a lot of other ways they offer really different features. One of the main selling points of pattern matching is that it allows you to quickly access data that has a fairly complicated “shape”. Let’s look at another common example of pattern matching with case
: handling command line arguments.
In a larger application you might use a library to deal with command line arguments, but for short programs with only a couple of options, you might want to do it manually. Let’s say that you’re building a program to copy a file. You want to support a couple of different options for copying:
First, if the user passes in the --help
flag, you want to display a help message. Otherwise, they should pass in a source file, a destination file, and an option number of bytes to copy We can use pattern matching to make the process a lot easier:
getProgramOptions :: [String] -> Either String ProgramOptions
getProgramOptions commandLineArgs =
case commandLineArgs of
[inputFile, outputFile] ->
makeProgramOptions inputFile outputFile CopyAllData
[inputFile, outputFile, amount] ->
makeProgramOptions inputFile outputFile (CopySomeData amount)
_ -> showHelpText
In this example we’re using pattern matching to identify how many arguments were passed in, and to bind the value of those arguments to particular variables like inputFile
and outputFile
(the _
at the end says to match anything and ignore the values, like default
). This is really convenient because it lets us combine the common concerns of “check how many arguments were passed in” and “assign the arguments to a variable” into a single expression.
Another benefit of pattern matching is that you can use it in more places than you would want to use an case
expression. For example, you can commonly see pattern matching being used with functions. This function to recursively add up the numbers in a list is one example:
addNumbers [] = 0
addNumbers (thisNumber:rest) = thisNumber + addNumbers rest
In the first function definition, we match on the case where we have an empty list and return a value of 0. The second definition matches the first element of the list and the rest of the list into two parts. It’s safe, because we’ve already check for empty lists in the earlier version of the function.
Another case where pattern matching is useful is when you are defining values. For example, we can get each of the elements out of a tuple with pattern matching in a way that looks awfully similar to multi-return in languages like Go:
returnTwoNumbers = (5,7)
sumTwoNumbers =
let (x,y) = returnTwoNumbers
in x + y
Here, we’re using a pattern to make it easier for us to get at each of the values inside of the tuple. Without pattern matching we’d have to say:
sumTwoNumbers =
let numbers = returnTwoNumbers
in fst numbers + snd numbers
Of course, there are some situations where switch
can do things that pattern matching doesn’t do. For example, some languages let you embed an arbitrary expression in your switch statements:
string file_size_units(int bytes) {
switch(bytes)
case (bytes > 1024):
return "kb"
case (bytes > 1024 * 1024)
return "mb"
case (bytes > 1024 * 1024 * 1024)
return "gb"
default:
return "bytes"
}
There’s not a good way to use pattern matching for this in Haskell, but we can use a different feature, guard clauses to get the same ability:
fileSizeUnits bytes
| bytes > 1024 = "kb"
| bytes > 1024 * 1024 = "mb"
| bytes > 1024 * 1024 * 1024 = "gb"
| otherwise = "bytes"
Anyway, this response has gotten pretty long, but I hope it helps!

RebeccaSkinner
That’s a great question, and the answer is pretty nuanced I think.
At a high level, I’d say that today Haskell is probably more widely used for data engineering than for implementing models, Haskell’s data modeling capabilities and strong support for streaming data make it a great fit for a lot of data engineering work, and if I were trying to look for the best way to introduce Haskell to an ML team, I’d probably start there.
When it comes to implementing models, I think the choice is a bit more nuanced. There’s a lot Haskell brings to the table. Being able to model your data accurately and working with the type system to help you write correct code is a great benefit to building models since it makes it much easier to reason about how your models are being built. Even more importantly, being a pure functional language Haskell can give you some really amazing guarantees when you want to be able to understand and describe your system. Being able to understand how your models produce a decision, and being able to reproduce those results or test new models against historic data are huge problems that come up in any sort of data team, and Haskell allows us to model these constraints and guarantee we’re clearly capturing our inputs and outputs.
There are some challenges that come with using Haskell for writing models too. Library support is a consideration, but I often remind people to not worry about all of the libraries you don’t have, and just look to see if there are the specific libraries you need. If you’re comparing Haskell to something built largely by gluing together pieces of SciKit or Pandas or something then the library support will probably be a bit disappointing, but if you’re implementing most of your model yourself then Haskell’s going to compare a lot more favorably. Another challenge I see people talk about when using Haskell to build models is the fact that it’s often not as ergonomic to work with Haskell early in the process of developing a model. Optics give us the tools we need to arbitrarily traverse deeply nested data structures in an ad-hoc way, and more advanced type system features like GADTs can help us write code that we might use dataframes for in python, but getting to the point where you can use those things comfortably can involve a bit of an upfront investment, and it’s sometimes hard to convince people who are comfortable with Python that it’s going to be worth it.
On the whole, I think Haskell can be a great choice, but it’s also a particularly hard sell to data scientists in my experience. I’d certainly encourage you to consider trying Haskell for your own problems, and certainly look at it for data engineering problems, but for building models it’s probably best to work with your team and help them get comfortable with Haskell before you start trying to build significant work with it.
Popular In The Spotlight topics










Other popular topics










Latest in In The Spotlight
Latest (all)
Categories:
Popular Portals
- /elixir
- /rust
- /wasm
- /ruby
- /erlang
- /phoenix
- /keyboards
- /js
- /rails
- /python
- /security
- /go
- /swift
- /vim
- /clojure
- /java
- /haskell
- /emacs
- /svelte
- /onivim
- /typescript
- /crystal
- /c-plus-plus
- /tailwind
- /kotlin
- /gleam
- /react
- /flutter
- /elm
- /ocaml
- /vscode
- /opensuse
- /ash
- /centos
- /php
- /deepseek
- /scala
- /zig
- /html
- /debian
- /nixos
- /lisp
- /agda
- /textmate
- /react-native
- /sublime-text
- /kubuntu
- /arch-linux
- /ubuntu
- /revery
- /manjaro
- /django
- /spring
- /diversity
- /nodejs
- /lua
- /slackware
- /c
- /julia
- /neovim