Introduction to Functional Programming

Objects and Compatibility


Learning Objectives

  • You understand primitive types in π•Šπ•‹π•ƒβ„‚++.
  • You understand the idea of type equivalence (𝐴≃𝐡), and can justify an equivalence mathematically.
  • You can recognize common equivalences (e.g. swapping pairs, currying/uncurrying, re-ordering arguments).
  • You understand why equivalent types are not literally β€œequal” types in a programming language.
  • You can encode simple custom data types using sums and products, and define their data constructors.
  • You can distinguish general recursion from structural recursion via a recursor.
  • You know of the recursor for natural numbers, and can use it to implement primitive recursive functions.

Primitive Types

In π•Šπ•‹π•ƒβ„‚++, primitive types include the booleans, integers, characters, unit type, functions, products, sums and lists. With these types, we can build more complex types by composing them, e.g.

  • Intβ†’Int is the type of (possibly partial1) functions that map integers to integers.
  • List(ListInt) is the type which contains lists of integer lists.
  • (Int,ListInt) is the type of products, i.e. pairs, whose elements are an integer and an integer list.
  • Int+Bool is the type of coproducts, i.e. sums, whose elements are either an integer or a boolean.

Each of these types is fundamentally different in the sense that there isn’t a bijection between any distinct pair. However, this rule is not universal, there do exist types which correspond bijectively, for example all of the following:

(Bool,Bool)Bool+BoolBool→BoolUnit→(Bool,Bool)

For example, (Bool,Bool) has four elements, but so does Bool→Bool. Bool→Bool has four representatives which all behave differently, i.e. have a different function graph, compared to one another. After defining the notion of equivalence, you get to construct some equivalences including the one between pairs of booleans and functions between booleans.

Equivalence

A bijection between types is called an equivalence and denoted with the symbol ≃. An equivalence always comes with two functions, one for the forward direction and one for the reverse direction. In addition, an equivalence has two laws: the forward direction is the inverse function of the reverse direction and vice versa.

DefinitionEquivalenceAn equivalence between types 𝐴 and 𝐡 denoted 𝐴≃𝐡 consists oftwo functions 𝑓:𝐴→𝐡 and 𝑔:𝐡→𝐴 which satisfy theequivalence laws:1.𝑔 is left inverse for 𝑓:π‘”βˆ˜π‘“=id:𝐴→𝐴2.𝑓 is left inverse for 𝑔:π‘“βˆ˜π‘”=id:𝐡→𝐡

Examples

Let’s actually construct some equivalences between types in π•Šπ•‹π•ƒβ„‚++. As a first example, let’s show that elements in a pair can be swapped.

(Bool,Int)≃(Int,Bool)
No files opened Select a file to edit

The proposed functions 𝑓 and 𝑔 do appear to act correctly based on the result of computing g (f (true, 10)), but we can do better β€” let’s prove the equivalence laws!

ExampleEquivalence laws hold for 𝑓 and 𝑔, which are defined as follows:𝑓:(Bool,Int)β†’(Int,Bool)𝑓(𝑏,𝑛)=(𝑛,𝑏)𝑔:(Int,Bool)β†’(Bool,Int)𝑔(𝑛,𝑏)=(𝑏,𝑛)Proof:1.We show that 𝑔 is left inverse for 𝑓:Let 𝑝=(𝑏,𝑛):(Bool,Int). Now,𝑔(𝑓(𝑏,𝑛))=𝑔(𝑛,𝑏)=(𝑏,𝑛)and therefore π‘”βˆ˜π‘“=id.2.𝑓 is left inverse for 𝑔:Let 𝑝=(𝑛,𝑏):(Int,Bool). Now,𝑓(𝑔(𝑛,𝑏))=𝑓(𝑏,𝑛)=(𝑛,𝑏)and therefore π‘“βˆ˜π‘”=id.☐

Here’s another example which demonstrates that the order of arguments doesn’t matter, or stated using integers and booleans,

Intβ†’Boolβ†’Int≃Boolβ†’Intβ†’Int

Writing the definition for f : (Int -> Bool -> Int) -> Bool -> Int -> Int and g : (Bool -> Int -> Int) -> Int -> Bool -> Int is straight-forward:

No files opened Select a file to edit

Proving that the above 𝑓 and 𝑔 form an equivalence is left as an exercise for the reader.

We will next look at a classic example of equivalence between different styles of calling functions.

In mathematics, one typically writes 𝑓(π‘₯,𝑦) for applying a function to two arguments. The two arguments are often β€œgrouped together” as a pair 𝐴×𝐡, so the function would have type 𝐴×𝐡→… However, as you have learned by now, in functional programming one typically writes arguments by chaining arrows: 𝐴→𝐡→… β€” so it shouldn’t come as a surprise that these styles are equivalent.

Example(Bool,Int)β†’Int≃Boolβ†’Intβ†’IntProof: Let 𝑓,𝑔 as follows𝑓:((Bool,Int)β†’Int)β†’Boolβ†’Intβ†’Int𝑓 β„Ž 𝑏 𝑛)=β„Ž(𝑏,𝑛)𝑔:(Int,Bool)β†’(Bool,Int)𝑔(𝑛,𝑏)=(𝑏,𝑛)Equivalence laws hold for 𝑓 and 𝑔, which are defined as follows:1.We show that 𝑔 is left inverse for 𝑓:Let 𝑝=(𝑏,𝑛):(Bool,Int). Now,𝑔(𝑓(𝑏,𝑛))=𝑔(𝑛,𝑏)=(𝑏,𝑛)and therefore π‘”βˆ˜π‘“=id.2.𝑓 is left inverse for 𝑔:Let 𝑝=(𝑛,𝑏):(Int,Bool). Now,𝑓(𝑔(𝑛,𝑏))=𝑓(𝑏,𝑛)=(𝑛,𝑏)and therefore π‘“βˆ˜π‘”=id.☐

From now on, we use the notation 𝑓(β„Ž,𝑏,𝑛) in mathematical text to mean 𝑓(β„Ž)(𝑏)(𝑛) or f h b n in the functional programming sense. It should be clear from the context whether a function takes a pair/tuple argument or takes its arguments in a Curried way. As the variants are equivalent, making a destinction between the two explicitly should be unnecessary when reading mathematical text combined with π•Šπ•‹π•ƒβ„‚++ code.

More equivalences

Think about why the following equivalences between infinite types are valid:

Bool+Nat≃NatNat+Nat≃Nat(Nat,Nat)≃NatUnitβ†’Nat≃NatBoolβ†’Nat≃Nat

However the following is not an equivalence:

Natβ†’Bool≄Nat

Start by thinking about why Natβ†’Bool is an infinite sequence of zeros and ones. Next, you may want to explore Cantor’s diagonal argument on Wikipedia for a set-theoretic proof of the analogous claim.

Loading Exercise...

Custom Data Types

π•Šπ•‹π•ƒβ„‚++, unlike most functional programming languages, doesn’t support creating new data types, like enumerations or structures. Instead, one uses type declarations or aliases. However, many interesting data types are recursive, but type declarations in π•Šπ•‹π•ƒβ„‚++ don’t support recursion, so this approach won’t work for defining recursive types.

Let’s start with a basic example: traffic lights.

No files opened Select a file to edit

We define the type alias TrafficLight = Unit + Unit + Unit (parenthesized as (Unit + Unit) + Unit) and three constructors: red, yellow, green which return traffic lights. The repr function shows how one matches nested sums in π•Šπ•‹π•ƒβ„‚++ β€” notice that there is no fancy syntax sugar for it.

A constructor is therefore nothing special: any function that returns a TrafficLight is a constructor for it. red, yellow and green are just convenient ways to construct any and all elements of TrafficLight.

Loading Exercise...
Loading Exercise...

Enumerations With Data

Enumerations in functional languages can also carry data. The data is naturally provided when calling the constructors. Matching with case then let’s us use that data.

No files opened Select a file to edit

Nothing stops us from storing functions inside custom types, let’s do that as well.

No files opened Select a file to edit

As a reminder: sum types with more than two summands, like A + B + C are parenthesized as (A + B) + C, i.e. + associates left.

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

Algebra of Types

Product types, i.e. pairs, are intuitively understood from the perspective of cartesian products of sets. The type (Bool,Bool) has four elements corresponding with the elements of {0,1}Γ—{0,1}={(0,0),(0,1),(1,0),(1,1)}.

Like product types, sum types are a convenient method of constructing types, but why are they called β€œsum” types. If we took sum types as simply unions of sets, we would end up in trouble, as Bool+Bool would be treated as the set {0,1}βˆͺ{0,1}={0,1} and have just two elements instead of the four inhabitants of Bool+Bool: inl false, inl true, inr false and inr true.

Instead of a regular union, we take the disjoint union (⊎), i.e. we first β€œcolor” both sets with distinct colors and then take their union.

{0,1}⊎{0,1}={0,1}βˆͺ{0,1}={0,1,0,1}

The coloring can be defined by giving a unique number to each color and pairing up that color and the value.

Taking a disjoint union of two sets as the counterpart to taking a sum of two types is the natural choice.

Next, to define the algebra of types, we need so called neutral elements of products and sums. These are called 1 and 0 and correspond to the types Unit and Empty respectively.

The type Empty is defined in the π•Šπ•‹π•ƒβ„‚++ standard library as the polymorphic function forall X, X. We will cover polymorphic types in the next chapter.

DefinitionNeutral ElementLet 𝐴 be a set and 𝑓:𝐴→𝐴→𝐴 be an associative function, i.e.𝑓(π‘₯,𝑓(𝑦,𝑧))=𝑓(𝑓(π‘₯,𝑦),𝑧) for all π‘₯,𝑦,π‘§βˆˆπ΄.If 𝑒:𝐴 is an element such that 𝑓(π‘₯,𝑒)=𝑓(𝑒,π‘₯)=π‘₯, then it’s calledthe neutral element of 𝑓.

We would like to take 𝐴 to be the β€œtype of all types” and 𝑓 to be the product of types, however we end up in trouble. First, the notion of a β€œtype of all types” is problematic, as its interpretation would have to be a set leading to impredicativity issues. Secondly, the notion that the types in 𝑓(π‘₯,𝑓(𝑦,𝑧))=𝑓(𝑓(π‘₯,𝑦),𝑧) are equal is also problematic as we have learned already. To rectify these issues, we replace β€œset” by β€œtype” and = by equivalence ≃ when talking about neutral elements in the algebra of types.

Next, we show associativity of product types, where 𝑓(𝑋,π‘Œ)=(𝑋,π‘Œ) i.e. (𝑋,(π‘Œ,𝑍))≃((𝑋,π‘Œ),𝑍). We start with concrete types for now to make things easier: 𝑋=Int,π‘Œ=Bool,𝑍=Char. As usual, we construct functions going both ways.

No files opened Select a file to edit

Exercise for the reader: f and g satisfy the equivalence laws.

As Unit is claimed to be the neutral element of products, we construct the equivalences (𝑋,Unit)≃(Unit,𝑋)≃𝑋 using the following functions

swapswap(_,π‘₯)↦π‘₯π‘₯↦((),π‘₯)(𝑋,Unit)(Unit,𝑋)𝑋

Next, your task is to prove the distributivity laws that tie product and sum types together.

There is a lot more interesting mathematics regarding the algebra of types that is not covered here. For example function types have the property of exponentiation: 𝐴→𝐡 can be viewed as 𝐡𝐴 and it’s not hard to prove that 𝐢𝐴+𝐡≃𝐢𝐴×𝐢𝐡. To learn more about exponential types, check chapter 9.5 in Category Theory for Programmers.

Loading Exercise...

Natural Numbers

Natural numbers are perhaps the most simple yet interesting object in mathematics. They form a set of infinite cardinality (size) and are characterized by just three simple functions:

  1. The constant (function) zero.
  2. The successor function, which adds 1.
  3. The recursor, which let’s us match a natural number with two branches.

π•Šπ•‹π•ƒβ„‚++ doesn’t have natural numbers as a primitive, but we can encode them in a silly but straight-forward way: using lists.

In short, we use a list of units to represent a natural number. It’s horribly inefficient but it works for small numbers (< 1000). For example, the number zero is an empty list, one is the list [()], two is [(), ()] and so on.

No files opened Select a file to edit

To encode an integer as a Nat, i.e. a list of units, we map all negative integers to zero and positive integers to corresponding natural numbers.

No files opened Select a file to edit
Loading Exercise...

Recursors

Recursors are functions that work like match-expressions. However, what makes recursors more general is that they let the branches use β€œrecursive results”. For example, fold is the recursor of β€” you guessed it: lists! It takes in two β€œbranches”: the empty list case, and the non-empty recursive case. The accumulator in the cons branch is the recursive result of the recursor.

Similarly, the recursor for natural numbers takes in two functions: the zero function, and the recursive step function. We can define the recursor in π•Šπ•‹π•ƒβ„‚++ for the silly natural number type as follows:

No files opened Select a file to edit

Don’t worry about the β€œforall” for now, it’s explained in the next chapter on polymorphism.

As we can see, natural numbers can be implemented as lists, and the recursor by recursively β€œfolding” over the list. The key property of the recursor is that unlike general recursion that we learned in the previous chapters, the recursor always terminates. The variable name ih (induction hypothesis) in the code refers to the recursive result for the predecessor. The recursor is in fact a kind of induction principle.

Some programming languages (such as Agda, Idris 2, Lean and Rocq), where functions in the core calculus must be total, attempt to convert general recursive definitions to a more general version of the recursor, which implements structural recursion. In these languages the recursor can be derived for every algebraic data type that is accepted2!

Functions that can be implemented using only the recursor (and constructors, such as zero and succ) are called primitive recursive, and this class of (total) functions is large but limited compared to general recursion. One well-known example of a non-primitive recursive3 total function is the Ackermann-PΓ©ter function written as follows:

import nat

iszero : Nat -> Bool
iszero = fun n : Nat, rec Bool n true (fun xs : Nat, fun ih : Bool, false)

pred : Nat -> Nat
pred = fun n : Nat, rec Nat n zero (fun xs : Nat, fun ih : Nat, xs)

ack : Nat -> Nat -> Nat
ack = fun a : Nat, fun b : Nat,
    if iszero a then
        succ b
    else if iszero b then
        ack (pred a) one
    else
        ack (pred a) (ack a (pred b))
Loading Exercise...

Footnotes

Footnotes

  1. π•Šπ•‹π•ƒβ„‚++ supports general recursion, so it is easy to accidentally define a function that never returns β€” which makes it partial. This means that the type arrow in π•Šπ•‹π•ƒβ„‚++ is not the same as what we have in mathematics. However, we aim to write total functions to avoid unwanted infinite loops, or to make it easier to handle errors. ↩

  2. One of the requirements for a valid data type is called β€œstrict positivity”. Strict positivity ensures that the recursor is a total function. Here’s an example of a non strictly-positive data type in Haskell. It has a non-positive constructor Bad which takes in Bad -> Bad where, because the data type being defined occurs in a β€œnegative position”

    data Bad = Bad (Bad -> Bad)
    
    instance Show Bad where
      show (Bad _) = "BAD"
    
    getFun :: Bad -> (Bad -> Bad)
    getFun (Bad f) = f
    
    omega :: Bad -> Bad
    omega f = (getFun f) f
    
    loop :: Bad
    loop = omega (Bad omega)
    
    -- We have an infinite unfolding
    -- loop = omega (Bad omega) = (getFun (Bad omega)) (Bad omega) = omega (Bad omega) = loop

    You can read more in the Agda Wiki or in Rocq Docs. ↩

  3. Source ↩