Fun and Functions with Gleam

Fun and Functional Programming


Learning Objectives

  • You know of functional programming and know the key characteristics of the functional programming paradigm.
  • You know how mathematical functions can be converted to Gleam.
  • You know what anonymous functions are, and why they are useful.
  • You know what higher-order functions are and know of common higher-order functions in programming languages.
  • You know how functions in Gleam can be used in mathematical proofs.

Fun and Functionality

In functional programming, functions as we are familiar with in programming, are given a stronger and finer meaning. Functional programming works with mathematical functions — which, believe us, are fun!

The core feature of mathematical functions is the property of being a ✨functional✨ relation, i.e. a mapping from an input to a unique output. This is where the name “function programming” comes from — however, it doesn’t mean that the language functions properly…

Functional programming traces its theoretical roots back to Alonzo Church’s work on the lambda calculus in the 1930s. The first practical implementation of functional programming ideas in an actual programming language was in Lisp, in late 1950s. Functional programming today is closely related to the mathematical fields of type theory and category theory.

Let’s start learning about functional programming by first seeing the least functional program the author could come up with.

Run the program to see the output

Try running the example a couple of times to see the different results.

This code breaks many of the constraints in functional programming. Here are the key takeaways why the code example is “unfunctional”:

  1. The global variable x is mutated inside the main function.

    This is not possible in functional programming, because functions have to be “self-contained”, i.e. they cannot change anything defined outside.

  2. The call to rng.nextInt is not functional because the function maps the argument (number 2) to either 0 or 1 depending on the state of the random number generator.

    In functional programming calling a function twice is guaranteed to produce the same result. The value returned by rng.nextInt(2) inside the if statement can be a different value from the previous call. This breaks referential transparency, i.e. being able to “replace equals by equals”, which makes functional programs much easier to reason about.

  3. if without else.

    In functional programming every if ends with an else, because the if-else expression must evaluate to a value.

  4. Raising an exception with throw new Exception("Fatal error"); stops the execution of the main function early.

    Functional programming doesn’t have a way to return early. Stopping early is usually only possible with a “panic” function provided by the language.

  5. Calling print("Everything is fine ${x}"); has the side effect of writing text into the output stream of the program.

    In computer science it is usually convenient to be able to manipulate the state of the computer, by reading input, producing output, sending packets, etc. Even mutating global variables in a single program is a side effect. Functional programming languages that prohibit side effects are called “pure”, and all mathematical functions are pure in this sense.

Functional programming is not only restricted to functional programming languages, in fact most programming languages support writing functional code. This is why functional programming is often called a programming paradigm, roughly meaning a high-level programming pattern or philosophy. The key is to restrict ourselves to using a subset of the programming language so that we don’t break functionality.

Further Reading

For additional information, read the excellent chapters 2.1, 2.3, 2.4, 2.5 from the book Category Theory for Programmers by Bartosz Milewski.

Also, this article about functional programming (in C++) by John Carmack is recommended fun reading.

Execution and Evalution

A mathematical function does not execute any code — it just knows the answer — Bartosz Milewski, Category Theory for Programmers

As Bartosz says in his book Category Theory for Programmers, mathematical functions aren’t executed, rather they are evaluated. Evaluation means turning an expression, or a block of code, into a final value through a sequence of evaluation rules. Evaluation rules say what the value of a certain kind of expression is, they give “meaning” to words.

For example the expression 1 + 2 evaluates to the value 3 through the following steps. But first, a word on notation: code, like 1 + 2, is used to denote expressions whereas the more mathematical looking 3 denotes values.

  1. The expression is an arithmetic add expression of syntactic shape a + b.
  2. To evaluate the expression, we first need to evaluate the sub-expressions 1 and 2:
    • evaluating the sub-expression 1 results in the value 1
    • evaluating 2 results in the value 2
  3. Now that we know the values of the sub-expressions, we can derive the value of 1 + 2, which is 1+2=3

The distinction between code expressions and values is important to keep in mind when analyzing functional programs.

Every piece of syntax/code has a similar procedure for determining its value. …Well almost every — for instance a function definition isn’t an expression, but an equation assigning functions to names. This is how in the pure functional programming language Haskell everything is laid out.

In Gleam, we can’t just return a function definition from a function, as its not a value.

Run the program to see the output

This code doesn’t compile because a named function (the fn add(a, b)) must be at module-level, i.e. outside any other functions.

Similarly imports must be made at the module-level.

Instead, we can define add at module-level and return that from f.

Run the program to see the output

But surely there is a way to write the function add inside f, right?

Well, there is, and we call that construction an anonymous function (in programming language terminology), a lambda-function, or a lambda-abstraction (in mathematical terminology).

Lambda-abstractions

A lambda-abstraction is a way to give meaning to a block of code with a free variable.

We will learn the definitions of free and bound variables in later parts of this course

The free variable in the body of the abstraction is “abstracted over” by the lambda-abstraction making the whole seem like a complete “object”.

For example, in the expression x * x, there is no knowledge of what x is, but if we create an abstraction over the x, we get the “complete” expression Abstraction(x, x * x).

In Gleam, we don’t write Abstraction, but instead use the fn syntax, just without giving the function a name (i.e. making it anonymous).

The following are valid lambda-abstractions in Gleam:

fn(x) { x }

fn(x) {
    let y = x
    y + x
}

fn(x) {
    fn(y) { x + y }
}

In addition, lambda-abstractions can “see” the variables from the outer context. For example the following function with signature IntIntInt binds the variable x which is used inside the anonymous function fn(y) ...

fn(x) {
    fn(y) { x + y }
}

To tie things up, we were discussing how to return the function add from f, and here it is using a lambda-abstraction:

Run the program to see the output

Immutability

Contrary to popular belief about functional programming, many functional languages actually do support statefulness1 and mutation of variables2 in a small context. Some languages, such as Clojure, go as far as allowing a global kind of mutation, but that breaks functionality, which is the core principle of functional programming.

Gleam doesn’t have any statefulness or global mutation.

Loading Exercise...
Loading Exercise...
Loading Exercise...

Basic Types and Type Checking

So far we have been using Gleam without using its strong type system. The reason why we can write “untyped” code is that Gleam can infer the type of most expressions we write, and we haven’t written anything out of the ordinary so far — let’s do that now.

The following code attempts to compute 5 + True, but Gleam refuses to compile the code due to a type error.

Run the program to see the output

*Argh*, now the compiler is yelling at me about an unused function

warning: Unused private function
  ┌─ /app/main/src/main.gleam:1:1

1 │ fn f() {
  │ ^^^^^^ This private function is never used

Hint: You can safely remove it.

…no wait, that’s not the solution — well it is a solution

The compiler also produced the following type mismatch error, which may be more productive to investigate:

error: Type mismatch
  ┌─ /app/main/src/main.gleam:2:7

2 │   5 + True
  │       ^^^^

The + operator expects arguments of this type:

    Int

But this argument has this type:

    Bool

What Gleam is trying to tell us is that using the + operator expects Integer expressions on either side, and True is a Boolean expression rather than an Int. This error simply means that we are trying to call the binary (i.e. takes two arguments) + function with values which have the wrong type. There is no convincing Gleam that True is an Int, Gleam doesn’t have implicit coercions, or convincing it that + takes Bool on the right as Gleam doesn’t have function overloading as we learned earlier.

*Sigh*, I guess the solution is to remove the function…

Instead of implicit coercions (and to avoid having to remove the function), we can use an explicit coercion (i.e. a regular function call) to to_int. To do this, we need to add import gleam/bool on the first lines of the file.

Run the program to see the output

This produces the expected result of 6 (and a deprecation warning about the function we just used — in Gleam stdlib 0.52.0 bool.to_int is removed, so we’ll stick with 0.51.0 for a while):

warning: Deprecated value used
  ┌─ /app/main/src/main.gleam:5:11

5 │   5 + bool.to_int(True)
  │           ^^^^^^^ This value has been deprecated

It was deprecated with this message: Please use a case expression to get
the behaviour you desire

   Compiled in 0.63s
    Running main.main
6
Boolean Anti-pattern

The authors of Gleam think that if-expressions and using booleans in general is an anti-pattern, which the author of this chapter mostly agrees with. Using Booleans as data, such as flag-like options to a function, indicate a primitive way of programming where the intention is not as clear as it would be with a custom data type for the boolean flag.

Gleam also provides innovative methods for writing conditional code using guards.

Read more in a GitHub discussion about the topic.

The return type of the function can be made explicit by adding an arrow -> followed by the type of the function’s body expression, in this case Int.

import gleam/bool

pub fn f() -> Int {
    5 + bool.to_int(True)
}

The type signature of f itself can be written as f: fn() -> Int in Gleam syntax or

𝑓:()Int

in mathematical notation.

Let’s define a function g which takes a boolean parameter and returns an integer.

import gleam/bool

pub fn g(b: Bool) -> Int {
    5 + bool.to_int(b)
}

The function has the type signature g: fn(Bool) -> Int which can be written mathematically with or without parentheses:

𝑔:(Bool)Int𝑔:BoolInt

Gleam even has enough information to infer the function’s type without us writing Bool or Int:

Run the program to see the output
Loading Exercise...
Loading Exercise...

Assigning Functions to Variables

In Gleam, functions are just ordinary data. We can assign functions to variables just like we can assign the value 5.

Run the program to see the output

Let-bindings also allow us to specify the type of a variable using a colon between the variable name and equals sign.

Run the program to see the output
The Identity Function and io.debug

The identity function can be given any of the type signatures 𝑎𝑎 where 𝑎 is any type in Gleam.

Run the program to see the output

We will go into the details on how this works in the Generics chapter.

Similarly io.debug can be given any of the type signatures 𝑎𝑎 as it prints its argument and returns it back to the caller.

Run the program to see the output

Notice that both id and io.debug have the same function signature fn(a) -> a and “mathematically” are the same function!

Try to come up with an explanation for this phenomenon.

Function Signatures — How Functions Sign Their Name

Official documentation.

So far we’ve mostly looked at functions from the functionality perspective — trying to answer “what does this function do”. In (typed) functional programming it is often useful to abstract a function to its signature. A function’s signature is a description of the type(s) that it takes as arguments and the type that it outputs. For instance, a function with the type signature IntBool maps a value of type Int to a value of type Bool. In Gleam, the IntBool type signature is written as fn(Int) -> Bool.

A common practice when developing functional programs is to first determine a function’s signature and worry about the implementation later. It is in fact so common, that there’s an entire development methodology called type-driven development that takes it above and beyond. In Gleam, stump functions can be defined by using the todo keyword. The interior functionality of todo is a bit strange, but its return type is guaranteed to be the one required by the type checker in that specific situation. Reaching a todo at runtime causes a panic.

Run the program to see the output
Hoogle – Searching functions by signature

The community of the influential functional programming language Haskell maintains a database of functions implemented by different packages. This database can be indexed with its own search engine, hoogle.

In addition to searching by name, hoogle allows one to search functions by their signature. For example, looking for a function that takes in a List(a) and returns a list of List(a) can be done with the following query: [a] -> [[a]]. This query returns functions such as inits and tails.

Searching functions by signature proves to be extremely useful in cases where naming the function is ambiguous. The database is not only useful for finding functions in Haskell, but also finding generally agreed upon names for common functions.

Multi-Parameter Function Types and Currying

The signature of functions with arity greater than one, i.e. which have multiple parameters, is usually written using parentheses with all the parameter types separated by commas.

For example, a function plus which takes two integer arguments would have the following type.

plus:(Int,Int)Int

In Gleam, this translates to the type signature: fn(Int, Int) -> Int.

Run the program to see the output

In Gleam, we can partially apply functions, i.e. pre-fill an argument to a function call, by using an underscore in place of the other argument. For example, to partially apply the value a = 2 in the call plus, we write plus(2, _). This returns us a function which takes in the missing b as an argument.

Notice that only one argument can be left out using an underscore.

Run the program to see the output

This concept of partially applying functions one argument at a time is called currying (named after the mathematician Haskell Curry).

Gleam’s authors don’t like currying. curry2 along with others were recently removed.

The function plus_alt in the example has the signature fn(Int) -> fn(Int) -> Int. This can be expressed mathematically with

plus_alt:IntIntInt

and is often used to mean exactly the same thing as (Int,Int)Int. To read chains of arrows, you can add parentheses associating to the right like so:

plus_alt:Int(IntInt)

Crucially, this is not the same as

not_plus_alt:(IntInt)Int

which belongs in the very interesting set of higher-order functions, whereas plus_alt does not.

Loading Exercise...

Higher-order Functions

Functions that take other functions as arguments are referred to as higher-order functions. They are a key feature of functional programming languages, adopted even in many not-so-functional programming languages (such as C++). The usage of higher-order functions often leans heavily on generics, which will be discussed later.

We will learn about generics in the Generics chapter.

Below is a simple higher-order function which uses concrete types (instead of generics) that takes in a function f: fn(Int) -> Int and a value Int and returns the result of the function applied to the value.

Run the program to see the output

Like apply_int, list.map takes in a function Int -> Int and applies it to each integer in the list.

Run the program to see the output

Common higher-order functions

Here is a table of some common higher-order functions which exists in almost every functional programming language. The most commonly used higher-order functions are often implemented by the language’s standard library.

NameSignatureDescriptionapply(𝑎𝑏)𝑎𝑏Applies a function to an elementmap(𝑎𝑏)[𝑎][𝑏]Applies the argument function toeach element of the argumentfilter(𝑎Bool)[𝑎][𝑎]Returns a new list consisting onlyof elements in the original list forwhich the predicate is truefold𝑏((𝑏𝑎)𝑏)[𝑎]𝑏A general operation of convertinga collection to a value byconstructing a result element byelementcurry((𝑎,𝑏)𝑐)𝑎𝑏𝑐A conversion from a functionthat takes two arguments to asingle parameter function whichreturns another single parameterfunctionuncurry(𝑎𝑏𝑐)(𝑎,𝑏)𝑐The inverse of curry

The notation [a] is Haskell syntax for Gleam’s List(a).

Isomorphism of Curried Functions

In mathematics, two structures are often considered the same if they can be shown to be isomorphic. Functions of type IntInt are structures, which describe mappings from integers to integers.

We recommend reading chapter 5.4 of Category Theory for Programmers.

One can think of functions as a set of ordered pairs, and sets clearly are a mathematical structure so functions must be too.

An isomorphism between two types 𝐴,𝐵 is called a type equivalence and denoted 𝐴𝐵.

Definition: Type Equivalence

Let 𝐴,𝐵 be types. The two types are equivalent, 𝐴𝐵, if there exist (total) functions to_fun:𝐴𝐵 and inv_fun:𝐵𝐴 such that inv_fun(to_fun(𝑎))=𝑎 and to_fun(inv_fun(𝑏))=𝑏 for all 𝑎:𝐴 and 𝑏:𝐵.

We can now prove that the functions plus and plus_alt are isomorphic using a simple theorem.

Theorem: Isomorphism of Curried Functions

Let 𝐴,𝐵,𝐶 be types. We claim that

(𝐴,𝐵)𝐶𝐴𝐵𝐶.

Proof

The isomorphism requires four pieces of information. First the to_fun which is curry:((𝐴,𝐵)𝐶)𝐴𝐵𝐶3 which transforms functions that take two arguments into a function which first takes one argument A returning a function that takes one argument B returning a C.

Because of a bug in Gleam, we will use t1, t2 and t3 for the types 𝐴, 𝐵 and 𝐶.

curry

Let’s give a name for the argument of type fn(t1, t2) -> t3 — call it f. We start writing the function by first leaving a todo hole in its definition.

Run the program to see the output
warning: Todo found
  ┌─ /app/main/src/main.gleam:2:3

2 │   todo
  │   ^^^^ This code is incomplete

This code will crash if it is run. Be sure to finish it before
running your program.

Hint: I think its type is `fn(a) -> fn(b) -> c`.

The compiler infers that the todo must have the type fn(t1) -> fn(t2) -> t3.

The compiler instead says fn(a) -> fn(b) -> c in the hint because of the aforementioned bug.

To fill this hole, we construct an anonymous function which takes a t1 as an argument and produces a function fn(t2) -> t3. Let’s refine the hole inside our function.

Run the program to see the output

The todo hole must be of type fn(t2) -> t3 which we can create by writing one more anonymous function which takes t2 as an argument.

Run the program to see the output

Finally to fill the final hole which has type t3 we can observe our context to see what we have available. On line (a), the context includes the parameters f: fn(t1, t2) -> t3 (from curry), t1: t1 and t2: t2 (from the anonymous functions). It should be quite clear that the only way to construct a t3 is to apply the function f to the elements t1 and t2.

Run the program to see the output

uncurry

The inv_fun, also known as uncurry:(𝐴𝐵𝐶)((𝐴,𝐵)𝐶), is the opposite of curry — it transfors a function A -> B -> C to one that takes both A and B as arguments in the same function call.

We again start writing the function by first leaving a hole in its definition.

Run the program to see the output

The hole has type fn(t1, t2) -> t3 which we can refine by making an anonymous function with two parameters.

Run the program to see the output

Now to fill the last hole, we can call g(t1) to get a function with signature fn (t2) -> t3, so calling g(t1)(t2) has type t3.

Run the program to see the output

The two final pieces are the proofs of uncurry(curry(𝑓))=𝑓 and curry(uncurry(𝑔))=𝑔 for all 𝑓:𝐴𝐵𝐶 and 𝑔:(𝐴,𝐵)𝐶.

Here we go:

  • Assume 𝑓:𝐴𝐵𝐶.

    To show: uncurry(curry(𝑓))=𝑓.

  • Unfold the definition of uncurry.

    To show: fn(t1: t1, t2: t2) { curry(f)(t1)(t2) } = f.

  • Unfold the definition of curry, which is curry(f) = fn(t1: t1) { fn(t2: t2) { f(t1, t2) } }.

    To show: fn(t1: t1, t2: t2) { (fn(t1: t1) { fn(t2: t2) { f(t1, t2) } })(t1)(t2) } = f.

  • Simplify/reduce the function application (fn(t1: t1) { fn(t2: t2) { f(t1, t2) } })(t1)(t2) by replacing the arguments in the function bodies.

    To show: fn(t1: t1, t2: t2) { f(t1, t2) } = f.

  • By the axiom of function extensionality, fn(t1: t1, t2: t2) { f(t1, t2) } = f, which proves the proposition.

Almost identical to the one above. Only the simplification is slightly different. The reader is encouraged to work out the details.

Summary

This proof is mathematically quite valid precisely for the reason that Gleam is a functional programming language and reasoning about programs can be reduced to reasoning about mathematical functions, which is much easier than trying to understand what is actually happening in the computer.

Taking this approach much further, we could write all four parts of the proof in a programming language using the famous Curry-Howard isomorphism! In fact, here’s a formal proof of isomorphism between curried functions in the Lean 4 proof assistant.

Loading Exercise...

Summary

In this chapter we covered the basics of functional programming (except for recursion). You learned what types of functions look like. You learned that in functional programming functions are like mathematical functions. You learned how to construct functions without names and how to use them with higher-order functions.

Finally you hopefully learned that you can do a little bit of maths in Gleam, at least construct functions between types to show their equivalence.

Footnotes

Footnotes

  1. See for example Haskell’s State monad.

  2. See for example Lean’s do notation. Lean, unlike Gleam, has early returns and for-loops while being purely functional. There is even a nice paragraph written about imperative versus functional programming in Lean. The following excerpt perfectly summarizes the usefulness of monads in functional programming.

    Monads and monad transformers allow functional versus imperative programming to be a matter of perspective — Functional Programming in Lean

  3. A more mathematical notation for the type (𝐴,𝐵)𝐶 is 𝐴×𝐵𝐶, because pairs are products. However, in Gleam, these two are distinct: a function that takes two arguments is not exactly the same as a function which takes a pair (2-tuple) as an argument.