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.
use
expressions, which are syntactic sugar for callback-style APIs- Structural equality for all types, including user-defined types
- 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 tof(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” withString
keys becauseString
is ordered. - Haskell’s
Maybe
type “just works” with the do-notation. If a function returnsNothing
, the do-notation short-circuits and the whole function returnsNothing
. 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 aJust
value. Again, this is non-obvious to the programmer. - The
show
function automatically knows how to turn aMaybe Int
into aString
.
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 caseString
). - You must explicit create a module that represents the
let*
andreturn
functions for theOption
type. - This
Syntax
module must be locally in-scope to use the appropriate implementation oflet*
in the body ofdo_stuff
. This means an explicit localopen
of theSyntax
module. - It’s very obvious to the programmer what implementation of
let*
andreturn
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” withString
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 aResult(Int, Nil)
into aString
.
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 String
s. 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.