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.
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”:
-
The global variable
x
is mutated inside themain
function.This is not possible in functional programming, because functions have to be “self-contained”, i.e. they cannot change anything defined outside.
-
The call to
rng.nextInt
is not functional because the function maps the argument (number2
) to either0
or1
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 theif
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. -
if
withoutelse
.In functional programming every
if
ends with anelse
, because theif
-else
expression must evaluate to a value. -
Raising an exception with
throw new Exception("Fatal error");
stops the execution of themain
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.
-
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.
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 through the following steps. But first, a word on notation: code, like 1 + 2
, is used to denote expressions whereas the more mathematical looking denotes values.
- The expression is an arithmetic add expression of syntactic shape
a + b
. - To evaluate the expression, we first need to evaluate the sub-expressions
1
and2
:- evaluating the sub-expression
1
results in the value - evaluating
2
results in the value
- evaluating the sub-expression
- Now that we know the values of the sub-expressions, we can derive the value of
1 + 2
, which is
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.
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
.
But surely there is a way to write the function
add
insidef
, 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).
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 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:
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.
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.
*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 Int
eger expressions on either side, and True
is a Bool
ean 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.
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
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:
Gleam even has enough information to infer the function’s type without us writing Bool
or Int
:
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.
Let-bindings also allow us to specify the type of a variable using a colon between the variable name and equals sign.
The Identity Function and io.debug
The identity function can be given any of the type signatures where is any type in Gleam.
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.
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
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 maps a value of type to a value of type .
In Gleam, the 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.
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.
In Gleam, this translates to the type signature: fn(Int, Int) -> Int
.
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.
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
and is often used to mean exactly the same thing as . To read chains of arrows, you can add parentheses associating to the right like so:
Crucially, this is not the same as
which belongs in the very interesting set of higher-order functions, whereas plus_alt
does not.
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.
Like apply_int
, list.map
takes in a function Int -> Int
and applies it to each integer in the list.
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.
The notation
[a]
is Haskell syntax for Gleam’sList(a)
.
In mathematics, two structures are often considered the same if they can be shown to be isomorphic. Functions of type 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 and such that and 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 which is 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.
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.
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.
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
.
uncurry
The , also known as , is the opposite of — 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.
The hole has type fn(t1, t2) -> t3
which we can refine by making an anonymous function with two parameters.
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
.
Proof of for all
The two final pieces are the proofs of and for all and .
Here we go:
-
Assume .
To show: .
-
Unfold the definition of .
To show:
fn(t1: t1, t2: t2) { curry(f)(t1)(t2) }
=f
. -
Unfold the definition of , 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.
Proof of for all
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.
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
-
See for example Haskell’s State monad. ↩
-
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
-
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. ↩