I’ve spent the past several years working with functional programming languages in my free time – primarily Haskell and OCaml. I love both languages but also find aspects of each frustrating.

Haskell is terse and elegant with type classes providing a powerful mechanism for ad-hoc polymorphism. However, it can also be confusingly implicit and I personally find lazy evaluation to have more downsides than upsides.

OCaml is explicit and powerful with a best-in-class module system. However, I believe it is often exhaustingly explicit, especially when dealing with custom data types in generic containers.

Over the past few months I’ve been experimenting with the Gleam programming language and I’ve been very impressed. I believe it makes three very interesting design choices that provide the best of Haskell and OCaml, with relatively few downsides.

  1. use expressions, which are syntactic sugar for callback-style APIs
  2. Structural equality for all types, including user-defined types
  3. No ad-hoc polymorphism

I’ll explore the implications of these design decisions in this post, comparing Gleam with Haskell and OCaml. I’ll be using only the standard libraries of each language.

Brief Gleam Overview

Gleam is a strongly-typed functional programming language. It targets both the BEAM (Erlang’s virtual machine) and JavaScript. It is impure (allowing untracked side effects) and all data structures provided by the standard library are immutable.

Here’s an example Gleam program that adds up the numbers in a list and prints the result:

import gleam/int
import gleam/io
import gleam/list

fn add(sum: Int, n: Int) -> Int {
  sum + n
}

pub fn main() {
  let items = [1, 2, 3, 4, 5]

  items
  |> list.fold(0, add)
  |> int.to_string
  |> io.println
}

// 15

This simple program demonstrates some important parts of Gleam’s design:

  • Optional type annotations (encouraged on top-level functions) with type inference
  • First-class functions
  • Explicit imports
  • Visibility modifiers for functions (pub for public, default is private)
  • List literal syntax
  • The |> operator for function pipelines (a |> f(b) is equivalent to f(a, b))

Here’s a slightly more complicated program:

import gleam/int
import gleam/io
import gleam/list
import gleam/string

pub type Color {
  Brown
  White
  Other
}

pub type Pet {
  Dog(color: Color, name: String)
  Cat(age: Int, name: String)
}

fn color_to_string(color: Color) -> String {
  case color {
    Brown -> "brown"
    White -> "white"
    Other -> "other"
  }
}

fn pet_to_string(pet: Pet) -> String {
  case pet {
    Dog(color, name) -> name <> " is " <> color_to_string(color)
    Cat(age, name) -> name <> " is " <> int.to_string(age)
  }
}

pub fn main() {
  let pets = [Dog(color: Brown, name: "Hobbes"), Cat(age: 5, name: "Garfield")]

  pets
  |> list.map(pet_to_string)
  |> string.join(", ")
  |> io.println
}

// Hobbes is brown, Garfield is 5

In this example, we see even more cool features:

  • Custom types, with multiple constructors
  • Exhaustive pattern matching
  • Polymorphic functions (list.map works with any type)
  • A string concatenation operator (<>)

That’s enough Gleam to get into the rest of the post.

Monadic-style APIs

I won’t fall into the trap of trying to define Monads in this post. Instead, let’s talk about monadic-style APIs – that is, APIs that allow you to do a bunch of things one after another, with the ability to use the result of a previous computation in the next computation, and also allows some logic to happen between steps.

A subset of these of APIs are often called “railway oriented programming” – you do a bunch of steps, each which may fail, and if they all succeed you do something with the results. We’ll focus on this use case for the rest of this section.

Let’s built a program that fetches several values from a key/value data structure and, if all the values are present, adds them up. The second value we fetch will depend on the first value we fetch.

Here’s an example in Haskell:

module Main (main) where

import Data.Map (Map)
import Data.Map qualified as Map

doStuff :: Map String Int -> Maybe Int
doStuff items = do
  a <- Map.lookup "a" items
  b <- Map.lookup (show a) items

  return (a + b)

main :: IO ()
main = do
  let items = Map.fromList [("a", 1), ("1", 2)]
  let result = doStuff items

  putStrLn (show result)

-- Just 3

The Map.lookup function has the following type:

ghci> :t Map.lookup
Map.lookup :: Ord k => k -> Map k a -> Maybe a

That is, given an key that is ordered and a map, it possibly returns a value. The Maybe type represents a value that may or may not be present in Haskell.

There’s a few important things to notice about this program:

  • Haskell’s Map type “just works” with String keys because String is ordered.
  • Haskell’s Maybe type “just works” with the do-notation. If a function returns Nothing, the do-notation short-circuits and the whole function returns Nothing. However, it’s not obvious to the programmer how this works or what function specifically is being called.
  • The return function “just works” to return a Just value. Again, this is non-obvious to the programmer.
  • The show function automatically knows how to turn a Maybe Int into a String.

Here’s a roughly equivalent example in OCaml:

module StringMap = Map.Make (String)

module Syntax = struct
  let ( let* ) = Option.bind
  let return x = Some x
end

let do_stuff items =
  let open Syntax in
  let* a = StringMap.find_opt "a" items in
  let* b = StringMap.find_opt (string_of_int a) items in
  return (a + b)
;;

let option_to_string = function
  | None -> "None"
  | Some n -> Printf.sprintf "Some(%i)" n
;;

let () =
  let items = StringMap.of_list [ "a", 1; "1", 2 ] in
  items |> do_stuff |> option_to_string |> print_endline
;;

(* Some(3) *)

The StringMap.find_opt function has the following type:

val find_opt : key -> 'a t -> 'a option

Some important things to notice here:

  • You must explicitly create a Map with your chosen key type (in this case String).
  • You must explicit create a module that represents the let* and return functions for the Option type.
  • This Syntax module must be locally in-scope to use the appropriate implementation of let* in the body of do_stuff. This means an explicit local open of the Syntax module.
  • It’s very obvious to the programmer what implementation of let* and return are being used in a given context.
  • You need to manually tell OCaml how to print a int option.

Now, let’s look at two examples of the same program in Gleam. Here’s the first one:

import gleam/dict.{type Dict}
import gleam/int
import gleam/io
import gleam/result
import gleam/string

fn do_stuff(items: Dict(String, Int)) -> Result(Int, Nil) {
  items
  |> dict.get("a")
  |> result.try(fn(a) {
    items
    |> dict.get(int.to_string(a))
    |> result.try(fn(b) { Ok(a + b) })
  })
}

pub fn main() {
  let items = dict.from_list([#("a", 1), #("1", 2)])

  items
  |> do_stuff
  |> string.inspect
  |> io.println
}
// Ok(3)

There are two important types in the above program. First, dict.get:

pub fn get(from: Dict(a, b), get: a) -> Result(b, Nil)

And second, result.try:

pub fn try(
  result: Result(a, b),
  fun: fn(a) -> Result(c, b),
) -> Result(c, b)

Things to note:

  • Dict “just works” with String keys.
  • All function calls are explicit, along with their namespaces.
  • The nesting of result.try calls is a bit of a pain.
  • string.inspect can automatically turn a Result(Int, Nil) into a String.

This example has a bunch of the good features from Haskell and OCaml – generic collections “just work”, it’s obvious what functions are being called, and string conversion is convenient via string.inspect.

However, the “callback hell” pyramid of doom is not great. This is where use comes to the rescue!

In Gleam, use is a mechanism for rewriting callback-based APIs in a flat style. The following two blocks of code are equivalent:

// standard callback
f(a, fn(b) { g(b) })

// with use
use b <- f(a)
g(b)

We can employ use to remove the nesting from our previous example:

import gleam/dict.{type Dict}
import gleam/int
import gleam/io
import gleam/result
import gleam/string

fn do_stuff(items: Dict(String, Int)) -> Result(Int, Nil) {
  use a <- result.try(dict.get(items, "a"))
  use b <- result.try(dict.get(items, int.to_string(a)))

  Ok(a + b)
}

pub fn main() {
  let items = dict.from_list([#("a", 1), #("1", 2)])

  items
  |> do_stuff
  |> string.inspect
  |> io.println
}
// Ok(3)

This example retains all of the previous advantages and removes the nested callbacks. Very nice.

Custom Types

Let’s say we want to rewrite our above examples, but this time our keys will be a custom type rather than Strings. Haskell first:

module Main (main) where

import Data.Map (Map)
import Data.Map qualified as Map

data Person = Person
  { name :: String,
    age :: Int
  }
  deriving (Eq, Ord)

drew :: Person
drew = Person {name = "Drew", age = 42}

jane :: Person
jane = Person {name = "Jane", age = 61}

doStuff :: Map Person Int -> Maybe Int
doStuff items = do
  a <- Map.lookup drew items
  b <- Map.lookup jane items

  return (a + b)

main :: IO ()
main = do
  let items = Map.fromList [(drew, 1), (jane, 2)]
  let result = doStuff items

  putStrLn (show result)

-- Just 3

This works pretty much the same as our previous example, except we need to tell Haskell that our Person can be compared for equality and is ordered (deriving (Eq, Ord)).

Next, here’s OCaml:

module Person = struct
  type t =
    { name : string
    ; age : int
    }

  let compare p1 p2 =
    match compare p1.name p2.name with
    | 0 -> compare p1.age p2.age
    | other -> other
  ;;
end

let drew = Person.{ name = "Drew"; age = 42 }
let jane = Person.{ name = "Jane"; age = 61 }

module PersonMap = Map.Make (Person)

module Syntax = struct
  let ( let* ) = Option.bind
  let return x = Some x
end

let do_stuff items =
  let open Syntax in
  let* a = PersonMap.find_opt drew items in
  let* b = PersonMap.find_opt jane items in
  return (a + b)
;;

let option_to_string = function
  | None -> "None"
  | Some n -> Printf.sprintf "Some(%i)" n
;;

let () =
  let items = PersonMap.of_list [ drew, 1; jane, 3 ] in
  items |> do_stuff |> option_to_string |> print_endline
;;

(* Some(3) *)

This has quite a bit more boilerplate than our previous OCaml example. When we want a custom type to be the key of a Map, we must manually implement the compare function (as noted earlier, I’m sticking to standard libraries, so no ppx_deriving).

Finally, here’s Gleam:

import gleam/dict.{type Dict}
import gleam/io
import gleam/result
import gleam/string

type Person {
  Person(name: String, age: Int)
}

const drew = Person(name: "Drew", age: 42)

const jane = Person(name: "Jane", age: 61)

fn do_stuff(items: Dict(Person, Int)) -> Result(Int, Nil) {
  use a <- result.try(dict.get(items, drew))
  use b <- result.try(dict.get(items, jane))

  Ok(a + b)
}

pub fn main() {
  let items = dict.from_list([#(drew, 1), #(jane, 2)])

  items
  |> do_stuff
  |> string.inspect
  |> io.println
}
// Ok(3)

Note that I don’t need to do anything to allow my custom Person type to be a key in a Dict.

Conclusion

Gleam has made some very interesting decisions in the functional programming design space. I believe it has pragmatically taken the best of Haskell and OCaml with very few downsides.

I’m surprised at how versatile use can be, especially given Gleam’s lack of ad-hoc polymorphism or a module system. Folks have already made lovely parser combinators, decoders, and more. I even wrote a CLI option parsing library myself.

I’m excited to continue working with Gleam and believe it has a bright future for functional programming newcomers and old timers alike.