Image by Josué Nunes from Pixabay

The Unreasonable Effectiveness of Haskell

Reliability, Resource Management, and DSLs

Rebecca Skinner
The Pragmatic Programmers
8 min readFeb 17, 2022

--

https://pragprog.com/newsletter/

I picked up Haskell for the first time back in 2008. At the time, I was working mostly in C and C++, and dabbling in common lisp and OCaml on the side. When I decided to drop WindowMaker and try out one of the fashionable-at-the-time tiling window managers, I picked up XMonad without having ever heard of Haskell. At first, I learned just enough Haskell to set up my environment the way I wanted it. After getting introduced to Haskell, I found it hard to not pick it up more and more often. Here was this kind of weird little niche language that I’d only heard of because I needed to configure a window manager, but it had captured my imagination utterly.

Over a decade later, Haskell still has a hold on me. I do still work with other languages, including C and C++ along with newer and newly popular languages like Python, Rust, and Go, but I keep coming back to Haskell. The reason is simple.

Once you learn how to use Haskell, you start to realize that it’s an amazingly effective tool for building software.

There are a few commonly quoted technical reasons Haskell is a good language:

  • Static types. An expressive static type system helps you prevent bugs at compile time
  • Memory and null safety. Both memory and null safety mean you’re protected from the two most common causes of runtime error
  • Native applications. Haskell code can be compiled to efficient native applications, making it suitable for even performance-sensitive problems

These are all great features, but they fall short of explaining why Haskell is unusually good at solving such a wide range of problems.

A Language For Building Languages

Image generated by Picture to People

I think Haskell manages to be unreasonably effective across a huge variety of problem spaces thanks to the ease with which it allows you to create domain-specific languages (DSLs). By handing you the tools to create incredibly effective languages for each problem you need to solve, Haskell manages to adapt to one problem domain after another. You can, of course, build DSLs in just about any language, but Haskell takes something that might be a painful chore in other languages and turns it into something joyous. To program in Haskell is to manifest the act of creation in the form of language.

It isn’t surprising that Haskell would be great at building languages. If you look into the history of its creation, you can see that Haskell shares a lot of its DNA with ML, the original language designed for writing languages. Today people use Haskell for implementing academic research languages and fully-fledged general-purpose programming languages, including:

  • GHC. The most popular Haskell compiler is itself written in Haskell.
  • Elm. A programming language designed for the web that is written in, and heavily influenced by, Haskell.
  • Dhall. A powerful configuration language for everything.
  • Agda. A programming language and proof assistant.

Building general-purpose languages isn’t that common, though. Haskell’s benefits shine in building small micro-languages for each problem domain or area of business logic needed when building programs. It’s true that Haskell has features like records and type classes and first-class functions that can serve some of the same mechanical functions that objects and interfaces serve in other languages. However, in practice, little languages are one of the most common units of abstraction when writing Haskell programs.

Haskell breathes languages in the same way that Java breathes classes.

Better Resource Management

Photo by Aaron Burden on Unsplash

Haskell helps us build better languages by providing tools to effectively handle resource management. As a pure functional language, all functions we write in Haskell, and by extension, our embedded DSLs, have to be pure functions. That means every function in our DSL must:

  • Accept a value, and return a value.
  • Be free of any external state and always return the same output when given the same input.
  • Avoid any side effects, including reading in any data other than its input, or writing out any information other than its return value.

Building A Small Reporting DSL

Let’s imagine a scenario to illustrate the value of purity for building our DSL. We want to build a small language for analysis and generating reports on internal data about users and how they use our service. All functions in our DSL will be pure functions that accept data like user information or business metrics and return information from their analysis — a report or data to be stored in a database.

Pure Functions For Effective DSLs

Thanks to pure functions, the code written using our DSL can never do anything to fetch any data other than the input it’s being given. We can be sure that all output is safely derived from only the known data put into the system. We also give ourselves flexibility in how we get that data. Each function gets data only as a parameter and does not have any state that we need to be concerned with. We’re free to build a runtime that gets data from real or fake data sources, from a production database or a test file, or even to generate random data for testing purposes. We can also enforce a degree of privacy by writing our runtime so that certain fields are redacted before they are ever passed to any code that might use the data. Since users of our DSL are writing pure functions, they can’t subvert any of our resource management — they can’t get data that we don’t want them to have access to, and they can’t do anything with the data except return it to our runtime.

Types For DSLs

Haskell’s expressive type system gives us even more tools for adding resource management to our DSLs. Using features like higher-kinded types we can easily modularize our runtime and separate out parts of the code that manage resources from the type of resources being managed. Thanks to easy and pervasive polymorphism in Haskell, we’re also able to reuse most code, making it easier to build more small languages as we work on our application.

In Haskell, all output is safely derived from only known data.

DSLs for a Distributed World

Photo by Umberto on Unsplash

Another way that Haskell helps us write better languages is by giving us better tools to control how we run programs in our DSLs. When all code is made up of pure functions, there’s no worry about initializing complex state or preserving state across runs. We are free to run as much or as little of the code in parallel as we want, to ship the code to other machines to execute, or even to avoid running the code altogether, using a cached result if we’ve already called it with a particular input before. This sort of flexibility is also enormously valuable when we are testing our language since the lack of state means that we’re able to test any given small piece of it completely in isolation.

Automating Parallelism

Control over how we run our applications is even easier using Haskell’s type system to implement techniques like defunctionalization. Defunctionalization makes it easy to turn code written in our embedded DSLs into data that we can send to processes on other machines. In a world where data engineering is becoming a key tool for running businesses effectively, Haskell is a clear winner. We can use Haskell to build type-safe pure functional DSLs that can be natively parallelized and built to run in highly-distributed big data environments — an astounding force multiplier for getting things done.

Haskell is a clear winner in distributed, big data environments.

Reliability

Image generated by Picture to People

Building small languages doesn’t help us if those languages aren’t reliable. Thanks to Haskell’s type system and its purity, we can not only get guarantees that our own code will work well, but we can build DSLs that prevent users from writing bugs in the first place.

When building a DSL, one of the most important tools for preventing bugs is to make sure users always have the data they need and can combine data only in ways that make sense. In many languages, these two requirements are at odds. Combining different types of data can be done, but often requires relaxing constraints on the type of data. For example, we can combine two objects by making the resulting value an instance of a shared superclass, but as languages in our embedded DSL get larger, we lose more and more type information over time. As we lose type information, we lose constraints that ensure we have all data needed and that it’s well-formed.

In some languages, we lose information as we combine types, but Haskell lets us preserve it

Thanks to features of Haskell’s type system like type families and GADTs we can build systems that let users combine different kinds of data without losing information. We can build up new types at compile time that represent ways users are combining values in our DSL, ensuring that we never lose information. Programs can grow larger while remaining safe and expressive.

Reliability Means Scalability

When we combine the type safety from using Haskell for our embedded language with the benefits of purity, then we can start to build unreasonably effective and safe languages. Combining stateless, pure functions without losing type information gives us the ability to safely grow the size of our programs. Choosing between local and distributed evaluation of our code means that we can scale down to individual workstations, or up to the size of an entire cluster as needed. Finally, fully managing access to resources lets us build safer, more secure, and less error-prone programs in our embedded language. When we combine all of these benefits we are able to build programs of truly exceptional quality.

Haskell’s type safety and purity allows us to build unreasonably effective and safe languages.

Ready To Get Started?

If you’re ready to get started building the languages you need to build the programs of your dreams, head over to the Haskell downloads page and give it a try.

Be sure to check out Rebecca Skinner’s book from The Pragmatic Bookshelf, now in beta. You’ll find a code that saves you 35% on the book’s forum page on DevTalk. Promo codes are not valid on prior purchases.

--

--