I read a Haskell book and now I can’t stop having crazy thoughts

Pete Gleeson
6 min readJul 8, 2018

--

For the last year, I have had a little blue elephant staring wishfully at me from my book shelf. Recently, it got to a point where I couldn’t let those beautiful eyes stare any longer. I decided to read Learn You a Haskell for Great Good.

This post is going to cover a few takeaways I got from the book. Heads up — these takeaways are going to be very much through the lens of a Javascript developer.

Thought #1: Haskell’s type system and Flow are similar

When I picked up this book, I thought this was going to be like comparing apples to oranges. To my surprise, Haskell’s type system and Flow are similar, in that they both focus on type inference. This means you write code mostly without type annotations and the type system can figure out the types of what you are writing.

Take some example code like three = 1 + 2. Both type systems will know that three is a number without the developer having to say so.

Thought #2: Generic types are functions for types

In all programming languages, functions get passed data and return new data. Haskell’s type system makes it possible to write functions that operate on types. These Type Constructors accept types and return new types.

// type constructor in Haskelldata Either a b = Left a | Right btype StringOrNumber = Either String Num

Many of the things you can do with functions you can do with Type Constructors. This includes partial execution of these types.

type StringOrSomething = Either String

Above we have partially executed Either with String. The result StringOrSomething is a Type Constructor that accepts only one type argument.

Thinking about other type systems, a lightbulb moment for me was realising that Generics can be thought of as functions that operate on types.

// type with generics in Flowtype Either<L, R> = { left: L, right: R }type StringOrNumber = Either<String, number>

Now when I read types with generics I kind of blur my eyes and replace type with function and <> with (). Doing so makes Either read like a function that accepts two types. The type ErrorOrNumber is the result of calling Either with Error and number as arguments. This realisation was a really nice simplification of how I think about types with generics.

Thought #3: Map, filter, reduce everything

In addition to defining your types, Haskell makes you think about how your types are used. Can they be compared? Ordered? Converted to a string? Or more interestingly — can they be mapped, filtered or reduced?

In Haskell, a type that is “mappable” is called a Functor. Examples of built-in types that are Functors include List, Maybe and IO (side-effect). What these types have in common is that they are container types. List is a container of elements, Maybe is a container for a value or nothing and IO is a container for a side-effect result. So long as a type is a container type and follows a couple of laws, it can be made a Functor. This means if you have your own type that makes sense to be a Functor, it can be mapped the same way as all other Functors.

The book describes mapping as opening up a container, applying a function to what is inside and boxing the result up into another container. In the context of Javascript, the most obvious example of a type that supports mapping is Array. However, if we stick with that fairly abstract definition of mapping, we find Promise fits nicely as well. Calling then on a Promise applies a function to the value inside the Promise container and returns a new Promise with the returned value.

The usage of Promise#then and Array#map is everywhere in modern JS. Working with something that is mappable is super convenient. Unfortunately, there is no language support for making anything mappable. Iterators and Generators provide language support for making anything iterable. Iterables can be used in for...of loops. Is it much more of a stretch to think we could have something similar for mapping?

I have been messing around with how this could be possible. While exploring different options I had two goals in mind:

  1. Everything is mapped in the same way
  2. Anything can be mapped

Where I landed was a standalone map function (a-la lodash.map) of the shape map = fn => subject => result. The shape of this function was designed for usage with the proposed Pipeline Operator. Let’s have a look at mapping Promises and Arrays with this function.

fetch("/users?id=1")
|> map(r => r.json())
["hi", "there"]
|> map(s => s.toUpperCase())
|> map(s => s + "!")

Cool, both Arrays and Promises are mapped in the same way. By currying map, we are able to call it once with our mapping function and pass that to the pipeline operator. The operator will execute the function again with the argument and pass along the result to the next pipeline.

What about other container types? For each type we want to be mappable, we need to define a Functor. The Functor describes how to map a certain type. Let’s think about how we could make Objects mappable. If you have been programming in JS for a reasonable amount of time, I bet you wished for this before.

const objectFunctor = fn => obj =>
Object.keys(obj)
.map(k => ({ [k]: fn(obj[k]) }))
.reduce((acc, curr) => ({ ...acc, ...curr }), {});

The above implementation is really descriptive of the container metaphor we have been talking about. We open the container with Object.keys, we apply fn to what is inside (in this case every value), and finally we pack the return values into a new container with reduce.

The implementation of map figures out the type it is mapping and calls the appropriate functor.

export const map = fn => subject => {
// ...
if (subject instanceof Array) {
return arrayFunctor(fn)(subject);
} else if (subject instanceof Object) {
return objectFunctor(fn)(subject);
}
// ...
}

So with those two things in place we can now map an Object.

({ hi: "there" }) |> map(value => `${value}!`)

A built-in Haskell type that is mappable is Function. Mapping one function with another might sound strange because it doesn’t fit the container idea we have talked about so far. In Haskell, the behaviour of mapping one function onto another is the same as composing the two functions together. Using that definition for mapping functions, we can write a Functor and support Functions in map.

// map uses this functor when the subject is a function
const functionFunctor = fx => gx => x => fx(gx(x))
const plusOneTimesTwo = (x => x + 1) |> map(x => x * 2)

What about mapping our own types? Let’s use a tree structure as an example of a container type that you might write yourself.

type Tree = {
value: number,
children: Tree[]
}

We can decide that mapping a Tree means visiting each node, applying a function to the value and returning a new tree containing those values. What we need to do now is write a Functor that matches that description.

const treeFunctor = fn => tree => {
const { value, children } = tree;
return {
value: fn(value),
children: children |> map(treeFunctor(fn))
};
};

The last piece is working out how to tell map to use this treeFunctor when mapping a Tree.

someTree 
|> map(i => i + 1, treeFunctor)
|> map(i => i * 5, treeFunctor)

I went back and forth on the nicest way to specify a custom mapping behaviour. The part I don’t like about this approach is it introduces some inconsistency between mapping in-built types and custom types. The part I do like about this approach is that it’s predictable.

While this was a pretty fun exercise, I think that providing a consistent way of mapping anything needs to be supported by the language. Fingers crossed that one day I’ll type map into my editor and watch it turn a beautiful reserved-word-purple. 🤞

Thanks for reading! If this has peaked your interest, Learn You A Haskell is free to read online.

If you want to play around with the map function here is a link to the repo.

Come talk to me on Twitter https://twitter.com/pete_gleeson 👋

More where this came from

This story is published in Noteworthy, where thousands come every day to learn about the people & ideas shaping the products we love.

Follow our publication to see more product & design stories featured by the Journal team.

--

--