How and why Haskell is better (than your favorite $LANGUAGE)

Categories
Haskell logo

tl;dr - Haskell’s worth checking out if you haven’t yet, you’ll find some features you may want to bring back to your beloved $LANGUAGE.

Rah-Rah Haskell posts in 2021? You betcha.

Note that all examples are written in simple Haskell where possible. No eta reductions for example, the code should be very easy to understand. Most of the code in here will sail past the compiler (you could use it for your own projects!) but some if it might not, but is very close to code that would compile.

Beautiful syntax for functions and composition

Haskell’s syntax takes some getting used to like most other languages, but it is beautiful. It encourages writing small composable functions that look like this:

someFunction :: String -> String
someFunction s = s ++ " foo"

As you might guess someFunction is the name of the function, String is the type of the first argument the function takes, and String denotes the type of the output. I’ve chosen to call the first argument s in the example, and ++ denotes string concatenation though it’s not too important here. A function with two inputs looks like this:

anotherFunction :: String -> String -> String
anotherFunction first second = first ++ " foo " ++ second

You can call the arguments whatever you want (first, f, foo) – it’s not the naming that matter, it’s the types that matter. This is a boon to code readability and encourages people to write small well-defined functions that do one thing well.

Another thing that haskell is really fantastic at is composition of functions. Let’s combine two functions that operate similarly:

addFoo :: String -> String
addFoo s = "foo " ++ s

addBar :: String -> String
addBar s = s ++ " bar"

addFooAndBar :: String -> String
addFooAndBar = addFoo . addBar

So what’s happening there? Well we’ve made a new function addFooAndBar that is the composition of . function composition operator. addBar will be called first, then the resulting value will be passed directly to addFoo so that can be run as well. A String goes in one end, gets passed through addBar and comes out, immediately that string is fed to addFoo and it comes ouf of there – the composition of these functions produces a string as well so the type signature of the composed function addFooAndBar is the very reasonable String -> String.

When you execute addFooAndBar, it’ll look just like any other function call in Haskell:

addFooAndBar "string" -- = "foo string bar"

Most other languages will look something like this for composition:

add_foo(add_bar(value))

That’s not bad, but gets a bit tedious – imagine a composition 3-5 functions long?

Non-nullable types

Friends don’t let friends use nullable types when they have a choice. If you’ve ever used Optional<T> in Java and reached the realization that every single object you’ve been dealing with your whole Java life is secretly an Optional<T>, then you realize the usefulness of non-nullable types.

An example:

-- Int will never be null, and you don't have to worry about it ever being null
plusOne :: Int -> Int
plusOne n = n + 1

-- If you *want* to work on values that may or may not be there
plusOne :: Maybe Int -> Maybe Int
plusOne (Just n) = Just (n + 1)
plusOne Nothing  = Nothing

Being forced to be always be strict about the differences here is worth applauding in a language.

Errors as values

Not every error is exceptional. Somewhat similar to the non-nullable types quality-of-life improvement, when things can fail in an unexceptional manner, you can properly represent that in the same thread of computation as everything else. For example a web request failure should not be unexpected to developers familiar with the fallacies of distributed computing). Haskell has a nice useful Either type:

An example:

-- Either is in the standard library (Prelude) but
-- if we were to define it ourselves:
-- data Either err value = Left err
--                       | Right value
--

-- Process a response to extract a value, *if* it worked
processReponse :: Either SomeError Response -> Maybe String
processReponse (Left err)   = Nothing
processReponse (Right resp) = doSomeProcessing resp

Haskell will force you to handle both cases – where something has gone wrong, and when it’s all gone right.

Let the compiler take the wheel

Go ahead, make a change that absolutely breaks everything in your codebase – in Haskell that’s a great way to refactor, because the compiler won’t let you get away with it, 99% of the time! That 1% of the time is usually when you’ve enabled an extension, doing some useful-but-unstable type magic, or dealing with fundamentally incorrect business/logic requirements of one form or another.

It’s hard to give a good example for this so I’ll just let you think back on any really bad refactoring experiences (particularly in dynamic languages) you have an imagine a better one – one where you could write the version of the function that’s right, then work your way back correcting the types to make sure everything slots into place, easily.

Stronger types means less unit tests

I have to say, I don’t enjoy writing unit tests. They’re unquestionably useful (I’d be an idiot not to) but I find I can get much farther not writing them in strongly typed languages. Being excited that you wrote a test to check if -1 got submitted to a function that expects only natural numbers is a bit silly to me these days.

Not that defensive coding and trying to think like a “red team”/how your function can break is useless, but if a certain state should be impossible, make it impossible at the type system level!

-- This function works on natural numbers, which are guaranteed to be
-- 0 or greater!
addOneToNat :: Natural -> Natural
addOneToNat n = plusNatural 1 n

But wait you might ask – what if you make a minusOneOnNat, what protects you from minusing one from zero? Well we’ve got two options with GHC.Natural

minusNatural :: Natural -> Natural -> Natural
minusNaturalMaybe :: Natural -> Natural -> Maybe Natural

How could the first one possibly protect us? Well the answer is it doesn’t, it throws. This is the dirty anything-can-explode world that the other lesser languages live in, so obviously avoid using that one at all costs! Use the minusNaturalMaybe and now you can protect your operations (and surface the fact that you’ve done something potentially unsafe so other downstream functions can react).

Structural Pattern matching

Yeah a bunch of other languages are catching on, but Haskell is already there (and likely inspired the other languages or designers). It’s all over the language:

-- Nice and obvious, get a one, spit out a True
isAOne :: Int -> Bool
isAOne 1 = True
isAOne _ = False

-- What if you didn't know you had an int or not?
IsAOneMaybe :: Maybe Int -> Bool
IsAOneMaybe (Just 1) = True
IsAOneMaybe (Just _) = False
IsAOneMaybe Nothing  = False

Types as you thought they would be: Algebraic Data Types (over Classes)

OK, now we’re going to get just a little type-y, but don’t worry, it’s pretty simple. Try and write something like this in your favorite untyped/typed $LANGUAGE:

-- An alias (called "newtype") in Haskell
-- to functions, UserID and String are the same thing
-- now you can write functions like (getUserID :: User -> UserID)
type UserID = String

-- A "newtype" container type, with a constructor function to do validation as you like
-- newtypes act as the original type but can be used to imbue value/add constraints (see: https://wiki.haskell.org/Newtype)
newtype UserID2 = UserID2 { _inner :: String }

-- A constructor function (if you share only this and not the UserID2 value constructor, you've cot control over how people can make the value)
mkUserID :: String -> Maybe UserID2
mkUserID s = case trimmedString of
               "" -> Nothing
               "not-this" -> Nothing
               s -> Just (UserID2 s)

Look how nice and concise that is!

Speaking of IDs, have you ever run into that really weird situation whenf you have multiple things that are basically the same thing, but made different ways and you want them to be the same type but your language won’t let you? Generalized Algebraic Data Types (BEWARE that link gets intense VERY quickly) sound scary but they actually solve this problem with relative ease. Let’s use a problem that’s more common than it should be to illustrate:

-- You can create a CrossServiceID
data CrossServiceID k where
    UUIDID    :: UUID -> CrossServiceID UUID
    StringID  :: String -> CrossServiceID String
    IntID     :: Int -> CrossServiceID Int

Unfortunately CrossServiceID won’t save you from having to deal the architecture you’ve created but at the very least it will force you to find out the “right” every time you work with a CrossServiceID, while giving you a nice combined type to use. Said a different way – at runtime when you receive a CrossServiceID, you can use haskell’s destructuring to figure out which kind of ID you have, while the rest of the world happily in CrossServiceIDs not knowing the difference.

Typeclasses over Inheritance (interfaces/abstract classes) and even composition

The secret is out, you probably don’t want to mix structure and functionality in your Types/Classes. Go got it right with their method implementation and their interfaces, Rust has it right with Traits and before both of those Haskell had it right with Typeclasses. Your type and what it can do should be separate:

-- | Some class
data SomeType = SomeType { name    :: String
                         , counter :: Int
                         }

-- | A (type)class that deals with things which have counters
class Counter a where
    increment :: a -> Int -> a
    decrement :: a -> Int -> a

-- | An typeclass instance that implements Counter for SomeType
instance Counter SomeType where
    increment :: SomeType -> Int -> SomeType
    increment t@SomeType{counter} n =  t{counter=counter + 1}

    decrement :: SomeType -> Int -> SomeType
    decrement t@SomeType{counter} n =  t{counter=counter + 1}
    decrement t@SomeType{counter} n =  t{counter=counter - 1}

Nice and separate! And when you want to layer typeclasses, no need for dirty-no-good class inheritance – use Haskell’s Constraint system:

-- | Typeclass for a Database Backend
class (HasLogger b, MonadIO m) => DatabaseBackend b m where
    -- | Connect the backend
    connect :: b -> m b
    -- | Disconnect the backend
    disconnect :: b -> m b
    -- | Get the current backend version
    getCurrentVersion :: b -> m BackendVersion
    -- | Migrate to a given backend version
    migrateToVersion :: BackendVersion -> b -> m (Maybe b)

-- | Entity stores must be capable of performing various functions on entities
class DatabaseBackend b m => EntityStore b m where
    -- | Create some entity that has been deemed Insertable
    create :: InsertableDBEntity entity => b -> entity -> m (Either DBError (ModelWithID e))
    -- .... elided ...

class EntityStore b m => UserStore b m where
    -- | Add a new user
    addUser :: b -> User -> Password -> m (Either DBError (ModelWithID User))
    -- ... elided ...

The real code is a bit more complicated but in general this code tells us:

  • To run addUser we want to be sure that we have a some kind of UserStore b m
    • b is a polymorphic variable that represents the database,
    • m is the flavor of monad world we’re operating in
    • we know that we must also have an EntityStore, which lets us know we can create, and of course to create we must be able to connect to the databse, etc. The
  • To run create we must be dealing with some sort of EntityStore b m
    • b and m are as before
    • we know that we must also have some sort of DatabaseBackend b m
  • To run connect and the other functions we msut be dealing with some sort of DatabsaeBackend b m
    • whatever type that b is needs to satisfy HasLogger
    • whatever type that m is needs to have MonadIO

Downstream, people only need to think in terms of UserStores that they’ve been given and they can be assured the rest of the Constraints have been met.

A few more breaths of fresh air in this space:

Concurrency, Parallelism and the runtime system

Haskell has a fantastic Runtime System (RTS) which has great support for the following:

  • System threads (pthread.h)
  • Green Threads (“fibers”) - lightweight threads, usually multiplexed over system threads on a provided runtime
  • Software Transactional Memory - primitives for working with shared memory regions, like compare and set

Other languages often have the first (System threads) by way of underlying OS interoperability (pthread.h), and sometimes have the second multiplexed on top (ex. Golang’s Goroutines), but they rarely have the third – extensive constructs to robustly interact with shared memory. While Haskell doesn’t focus on completely preventing unsafe things like memory races in the same way Rust does, using STM (even naively) generally keeps you very safe.

Unfortunately there are a few things that can go wrong in Haskell here:

  • Asynchronous exceptions are tricky
  • Laziness can make asynchronous behavior a little mor complicated than normal

Haskell isn’t perfect here but outside of these issues it’s pretty great ride. You’re likely going to have other problems before you hit these.

Monads and effects for contextual computation

A lot of ink has been spilled over Monads – what they are, how to use them, etc. I won’t do more than repeat the usual “Monads are monoids in the category of endofunctors”. If I were to put that in somewhat more useful terms, Monads are a way to contain (usually effectful) computations and the state that goes with them. That’s not all you can use Monads for but it’s not a terrible way to think about them. Let’s not get into it too deeply, because what I want to show you is a somewhat advanced feature anyway, selective capability exposure with monads (“classy mtl” for those in the know):

findAddressByID :: (HasDBBackend m db, MonadError ServerError m) => AddressID -> m (EnvelopedResponse (ModelWithID Address))
findAddressByID cid = getDBBackend
                      >>= liftIO . flip getAddressByID cid
                      >>= ifNothingThrowIOError Err.failedToRetrieveResource
                      >>= pure . EnvelopedResponse "success" "Successfully retrieved address"

Without thinking too deeply about the code or what it means, this code takes a m (Monad) that adheres to the HasDBBackend type class, and the MonadError ServerError typeclass. As you might imagine, that all we know about the m is that it “has” a DB backend, and can throw errors!. It’s not absolutely impossible to perform unknown behavior (I’ll spare you the how), but it’s drastically less likely now, and we have a function that takes an AddressID (whatever that is) and returns a Monad that when executed will return a EnvelopedResponse (ModelWithID Address). It’s a bit of a mouthful but the important part here is that

Another pattern rapidly gaining steam in Haskell communities is Free/FreeR Monads and Effect systems (libraries like freer-simple, polysemy and others) which takes this approach even further. You can define an “effect model”:

-- | The type of an key used to restrict an object
-- You might want to create a constructor that enforces some invariants
newtype ObjectKey = ObjectKey { _key :: String }

-- | The Effect Model of an object store
data ObjectStore s where
  GetObject :: ObjectKey -> ObjectStore ()
  PutObject :: ObjectKey -> ByteString -> ObjectStore ByteString
  DeleteObject :: ObjectKey -> ObjectStore ()

getObject' :: Member ObjectStore effs => Eff effs String
getObject' = send GetObject

putObject' :: Member ObjectStore effs => Eff effs String
putObject' = send PutObject

deleteObject' :: Member ObjectStore effs => Eff effs String
deleteObject' = send DeleteObject

Well awesome, but you need to actually write the implementation right? Well here’s the cool part – you can write the interpreters (which contain the implementation) any way you want:

runObjectStoreS3 :: Eff '[ObjectStore, IO] a -> IO a
runObjectStoreS3 = runM . interpretM (\case
  PutObject key bytes -> saveToS3 region bucket accessKey secretKey key bytes
  GetObject key -> retrieveFromS3 region bucket accessKey secretKey key
  DeleteObject key -> deleteFromS3 region bucket accessKey secretKey key
  )

Obviously the key saveToS3 function and retrieveFromS3 functions there are black boxes, and only having the key be variable is a bit silly, but you get the idea. The bucket can easily be specified on in the Effect model just like the Key is, or you could choose to switch out whole interpreters to accomplish this, which might make calling code simpler at the expensive of the setup code (maybe for different environments).

And when you want to run this effect in testing:

runObjectStoreHashMap :: Eff '[ObjectStore, IO] a -> IO a
runObjectStoreHashMap = runM . interpretM (\case
  PutObject key bytes -> insert key bytes yourMap
  GetObject key -> retrieveFromS3 region bucket accessKey secretKey key
  DeleteObject key -> deleteFromS3 region bucket accessKey secretKey key
  )

And now you have a guaranteed hot-swappable implementation which will never require any upstream code changes. Your upstream code is free to live at the level of the ObjectStore effect model, without worrying about what’s underneath.

There’s a bit of a learning curve to both Monads and Effect Systems, but they’re incredibly clean and powerful concepts for use in creating and managing abstractions.

Or… you could use Rust

Rust is basically the closest non-ML langauge to Haskell (AFAIK), they’ve taken so many of the good points that maybe you should just use that. It’s got some different pitfalls and a steep learning curve (essentially trade monads for manual memory management + borrowing), but it is useful at two different scopes where haskell is not – low level development (places where large runtimes don’t go – micro controllers, etc), and the web (WASM).

There also seems to be quite a few Haskellers kicking the tires on Rust (and a lot who are just plain into it at this point, since it’s been a while).

For those who love semantics, check out the under-construction (AFAIK) Haskell in plain English (Google Doc).

Wrapup

Hopefully this article was a litle provocative and challenges you to look at the parts of your language that could be a little more Haskell-like (the good parts and not the bad of course!). Popularly used languages have come a long way and there’s more to go – copying is encouraged.

Like what you're reading? Get it in your inbox