How to translate recursive code to Dhall
Embed recursive data types and functions in a non-recursive language
The Dhall configuration language provides built-in support only for one
recursive data structure: List
. The language does not provide built-in
support for user-defined recursive types, recursive values, or recursive
functions.
For example, a definition of a list with integer values in Haskell could look like this:
data ListInt = Nil | Cons Int ListInt
This code is not supported in Dhall because it is a recursive definition:
the type ListInt
is being defined using the type ListInt
itself. A corresponding definition in Dhall syntax is rejected by Dhall:
⊢ let ListInt : Type = < Nil | Cons : { head: Integer, tail : ListInt } > in ListInt
Error: Unbound variable: ListInt
A simple recursive function can be defined in Haskell like this:
step :: Int -> Int
step n = if n == 0 then 0 else step (n - 1)
A corresponding definition in Dhall syntax would be:
⊢ let step = \(n: Natural) -> if Natural/isZero n then 0 else step (Natural/subtract 1 n) in step
Error: Unbound variable: step
This code is rejected by Dhall because step
may not be used inside its own definition.
Why is recursion not supported directly?
Prohibiting direct recursion is one of the core design decisions in Dhall. It ensures that any well-typed Dhall program will always evaluate to a final value (called the “normal form”) within a finite time. Because of that limitation, it is simply not possible to write a Dhall program that type-checks but enters an infinite loop while evaluating. This is a valuable property for a configuration language.
Nevertheless, one can still work with a wide range of recursive types and functions in Dhall. Recursion must be encoded in a special way, ensuring statically (before evaluation) that all recursive data structures are finite and all recursive functions terminate. This guide explains how to do encode recursion in Dhall, walking through examples of progressively increasing difficulty.
How to implement recursive types: a general recipe
The main idea is to replace a recursive type definition by a more detailed description (called a “recursion scheme”) showing how a value of the recursive type may be constructed from simpler parts or from primitive values. That description will be itself non-recursive, and so it will be accepted by Dhall. Then one needs to use a trick (known as the “Church encoding”) that involves universally quantified types. That trick replaces a recursive type definition with an equivalent but more complicated type whose definition is not recursive.
We will now explain the Church encoding technique step by step.
Step 1: from a recursive type definition to a recursion scheme
First, we rewrite a recursive type definition in the form data T = F T
where F
is a new type constructor called a “recursion scheme”. We will need
to define F
appropriately each time.
For example, the integer list type has this Haskell definition:
data ListInt = Nil | Cons Int ListInt
We need to rewrite this definition as data ListInt = F ListInt
, where F
must be a suitable new type constructor. In this case, it is clear what F
should
be:
data F r = Nil | Cons Int r
We find F r
by replacing all recursive instances of ListInt
by the type parameter r
. (Here the letter r
reminds us of “recursion”.)
If we substitute ListInt
for r
in F r
, we obtain Nil | Cons Int ListInt
, which is exactly the right-hand side of the original recursive definition. In
this way, data ListInt = F ListInt
is the same as data ListInt = Nil | Cons Int ListInt
. It shows that our choice of F
is correct for ListInt
.
The recursion scheme (F
) describes all the possible ways of constructing a value of a recursive type. In this example, there are only two ways of constructing
a value of type ListInt
: first, Nil
is a value of type ListInt
. Second, if we somehow already have a value r
of type ListInt
, we can use Cons
to
construct new values of type ListInt
, for instance, Cons -123 r
.
Because F
is itself not recursive, Dhall will accept its definition. The Dhall code for F
is:
let F = λ(r : Type) → < Nil | Cons : { head : Integer, tail : r } >
As another example, take a binary tree with integer leaf values:
data TreeInt = Leaf Int | Branch TreeInt TreeInt
The corresponding recursion scheme F
is defined by:
data F r = Leaf Int | Branch r r
This F
is a non-recursive type constructor defined in Dhall by:
let F = λ(r : Type) → < Leaf: Integer | Branch : { left : r, right : r } >
If a recursive data type has itself some type parameters, those type parameters will have to be added to F
in addition to the type parameter r
.
For example, a list with a type parameter can be defined in Haskell as:
data List a = Nil | Cons a (List a)
The corresponding recursion scheme is:
data F a r = Nil | Cons a r
The corresponding Dhall code is:
let F = λ(a : Type) → λ(r : Type) →
< Nil | Cons : { head : a, tail : r } >
A binary tree with a type parameter in Haskell:
data Tree a = Leaf a | Branch (Tree a) (Tree a)
The corresponding recursion scheme F
is:
data F a r = Leaf a | Branch r r
The Dhall code is:
let F = λ(a : Type) → λ(r : Type) →
< Leaf : a | Branch : { left : r, right : r } >
As another example, consider a binary tree with two type parameters:
data TreeAB a b = LeafA a | LeafB b | Branch (TreeAB a b) (TreeAB a b)
The corresponding recursion scheme is:
data F a b r = LeafA a | LeafB b | Branch r r
The Dhall code is:
let F = λ(a : Type) → λ(b : Type) → λ(r : Type) →
< LeafA : a | LeafB : b | Branch : { left : r, right : r } >
A somewhat more complicated example is a “mutually recursive” type definition where two types are defined in terms of each other. In Haskell:
data Layer = Name String | OneLayer Layer | TwoLayers Layer2 Layer2
data Layer2 = Name2 String | ManyLayers [ Layer ]
The type Layer
is defined via itself and Layer2
, while Layer2
is defined via Layer
.
We need two recursion schemes (F
and F2
) to describe this definition. In terms of the recursion schemes, the type definitions should look like this:
data Layer = Layer (F Layer Layer2)
data Layer2 = Layer2 (F2 Layer Layer2)
We will achieve this formulation if we define F
and F2
by:
data F a b = Name String | OneLayer a | TwoLayers b b
data F2 a b = Name2 String | ManyLayers [ a ]
The recursion schemes F
and F2
are non-recursive type constructors with two type parameters each. The Dhall code for this example is:
let F = λ(a : Type) → λ(b : Type) → < Name : Text | OneLayer : b | TwoLayers: { left : b, right : b } >
let F2 = λ(a : Type) → λ(b : Type) → < Name2 : Text | ManyLayers : List a >
These examples show how to convert recursive type definitions into the corresponding recursion schemes. In this tutorial, we will often denote recursion schemes
by F
. (For every example, there will be a different F
.)
The definitions of F
s are non-recursive and will be accepted by Dhall.
Step 2: the Church encoding
Now we show the trick known as the “Church encoding”. This trick uses a given recursion scheme F
to define a special type that contains a universal
quantifier. In Dhall, that type is written as:
let C = ∀(r : Type) → (F r → r) → r
It turns out that C
is equivalent to a type T
defined recursively by T = F T
.
The type C
may be called a “Church encoding of T
”.
Because the definition of C
is not recursive, it will be accepted by Dhall.
With this trick, we have achieved the goal of defining the type T
without recursion.
Let us now discuss this trick in more detail.
The meaning of universal quantifiers
Carefully note that the Church encoding uses the universal quantifier (∀
) and not the λ
symbol. Let us explain the difference in some detail using an
example.
We will compare the Dhall expressions λ(r : Type) → Optional r
and ∀(r : Type) → Optional r
.
The expression λ(r : Type) → Optional r
is equivalent to just the type constructor Optional
. It is a function operating on types: it needs to be applied to
a particular type in
order to produce a result type. The expression λ(r : Type) → Optional r
is itself of type Type → Type
. This is not what we need for the Church encoding.
The expression ∀(r : Type) → Optional r
has type Type
. It is the type of functions having a type parameter r
and returning a value of type Optional r
.
So, a value of type ∀(r : Type) → P r
must be a function expression of the form λ(r : Type) → ...
. The code of such a function must work for all types r
and
produce some value of type Optional r
, no matter what the type r
might be. The Church encoding uses functions of this kind.
Additional type parameters and mutual recursion
If the recursion scheme F
has additional type parameters a
, b
, etc., we need to write the Church encoding with all those parameters next to F
:
let C = λ(a : Type) → λ(b : Type) → ∀(r : Type) → (F a b r → r) → r
For mutually recursive definitions with several recursion schemes, we write a Church-encoded type for each mutually recursive type separately. For instance, if
we have two mutually recursive types with recursion schemes F
and F2
, we write:
let C = ∀(a : Type) → ∀(b : Type) → (F a b → a) → (F2 a b → b) → a
for the first type and:
let C2 = ∀(a : Type) → ∀(b : Type) → (F a b → a) → (F2 a b → b) → b
for the second type.
The result is a pair of types that solve the “system of type equations” C = F C C2
, C2 = F2 C C2
.
All these type definitions (C
, C2
) are not recursive because they just use previously defined (and also non-recursive) type constructors (F
, F2
, etc.)
in some type expressions with
quantifiers. However, it turns out that those definitions are equivalent to recursive data types, with the additional guarantee that the resulting data
structures are always finite.
It is far from obvious why a type of the form ∀(r : Type) → (F r → r) → r
is equivalent to a recursively defined type T = F T
.
A mathematical proof of that property is given in the
paper “Recursive types for free” by P. Wadler.
In this tutorial, we will focus on the practical use of Church encoding.
Let us write the Dhall code for the examples shown in the previous section.
The type ListInt
(a list with integer values):
let F = λ(r : Type) → < Nil | Cons : { head : Integer, tail : r } >
let ListInt = ∀(r : Type) → (F r → r) → r
A binary tree with integer leaf values:
let F = λ(r : Type) → < Leaf: Integer | Branch : { left : r, right : r } >
let TreeInt = ∀(r : Type) → (F r → r) → r
A list with values of type a
:
let F = λ(a : Type) → λ(r : Type) →
< Nil | Cons : { head : a, tail : r } >
let ListA = λ(a : Type) → ∀(r : Type) → (F r → r) → r
This ListA
is actually equivalent to Dhall’s built-in List
.
A binary tree with leaf values of type a
:
let F = λ(a : Type) → λ(r : Type) →
< Leaf : a | Branch : { left : r, right : r } >
let TreeA = λ(a : Type) → ∀(r : Type) → (F a r → r) → r
A binary tree with two type parameters:
let F = λ(a : Type) → λ(b : Type) → λ(r : Type) →
< LeafA : a | LeafB : b | Branch : { left : r, right : r } >
let TreeAB = λ(a : Type) → λ(b : Type) → ∀(r : Type) → (F a b r → r) → r
Two mutually recursive types Layer
and Layer2
:
let F = λ(a : Type) → λ(b : Type) → < Name : Text | OneLayer : b | TwoLayers: { left : b, right : b } >
let F2 = λ(a : Type) → λ(b : Type) → < Name2 : Text | ManyLayers : List a >
let Layer = ∀(a : Type) → ∀(b : Type) → (F a b → a) → (F2 a b → b) → a
let Layer2 = ∀(a : Type) → ∀(b : Type) → (F a b → a) → (F2 a b → b) → b
Step 3: Working with recursive types
We have shown a recipe for converting any recursive type definition into a recursion scheme and finally into non-recursive (but more complicated) Church-encoded type.
It takes some work to figure out how to write values of those types and to develop techniques for programming with Church encodings more conveniently.
Example: ListInt
To learn those techniques, we begin by looking at the Church-encoded type ListInt
whose Dhall code is:
let F = λ(r : Type) → < Nil | Cons : { head : Integer, tail : r } >
let ListInt = ∀(r : Type) → (F r → r) → r
How can we implement a value x
of this type? We need to write a function that takes an arbitrary type r
and an arbitrary function frr
of type F r → r
and then returns a value of type r
.
let x : ListInt = λ(r : Type) → λ(frr : F r → r) → ... -- Some code here.
The code of this function must work for any type r
whatsoever. How can we produce a value of type r
if we do not even know what type it is?
We can do that only by applying the function frr
to some argument of type F r
. So, now we need to see how we could produce a value of type F r
.
We notice that F r
is a union type with one of the possibilities being just Nil
.
So, we can give that value Nil
as an argument to frr
. This will give us a value of type r
that we can return. So, one possibility of implementing a value
of type ListInt
is x0
defined like this:
let x0 : ListInt = λ(r : Type) → λ(frr : F r → r) →
let fr0 = < Nil | Cons : { head : Integer, tail : r } >.Nil
let r0 = frr fr0
in r0
in x0
The other variant of the union type F r
is Cons
containing a record with an Integer
and a previously known value of type r
. But where will we get that
previous value? So far, we could only get the value x0
defined just above. Let us build upon that code. Choosing the integer value arbitrarily as -123
, we
write:
let x1 : ListInt = λ(r : Type) → λ(frr : F r → r) →
let fr0 = < Nil | Cons : { head : Integer, tail : r } >.Nil
let r0 = frr fr0
let fr1 = < Nil | Cons : { head : Integer, tail : r } >.Cons {head = -123, tail = r0 }
let r1 = frr fr1
in r1
in x1
We have written the code in a verbose way in order to show the pattern of how to build up more values of type r
(denoted r0
, r1
, …). We can continue in
the same way to encode more complicated values of type ListInt
:
let x2 : ListInt = λ(r : Type) → λ(frr : F r → r) →
let fr0 = < Nil | Cons : { head : Integer, tail : r } >.Nil
let r0 = frr fr0
let fr1 = < Nil | Cons : { head : Integer, tail : r } >.Cons {head = -123, tail = r0 }
let r1 = frr fr1
let fr2 = < Nil | Cons : { head : Integer, tail : r } >.Cons {head = +456, tail = r1 }
let r2 = frr fr2
in r2
in x2
We find that we can implement values of type ListInt
if we just choose a list of zero or more integers (-123
, +456
, etc.) and write code as shown above
with zero or more similar-looking steps. Each step contains an arbitrary integer value and computes a new value of type r
out of a previous value. In this
way, the type ListInt
represents (possibly empty) lists of integer values. There is no other way of constructing a value of type ListInt
.
Could we hide the verbose boilerplate and make working with ListInt
easier? Let us introduce “constructors” nil
and cons
so that the code for x2
will be
just cons +456 (cons -123 nil)
.
The nil
constructor is the same as x0
shown above. The cons
constructor encapsulates one step of the boilerplate code:
let F = λ(r : Type) → < Nil | Cons : { head : Integer, tail : r } >
let ListInt = ∀(r : Type) → (F r → r) → r
let nil : ListInt = λ(r : Type) → λ(frr : F r → r) →
frr (F r).Nil
let cons: Integer → ListInt → ListInt = λ(head : Integer) → λ(tail : ListInt) →
λ(r : Type) → λ(frr : F r → r) →
let fr = (F r).Cons { head = head, tail = tail r frr }
in frr fr
in cons +456 (cons -123 nil)
In this code, it is important that we are allowed to write tail r frr
while computing fr
. The value tail : ListInt
is a function whose first argument is a
type. We are using that function with the type r
that we have received in the body of cons
. We are allowed to do this because tail
, being a value of
type ListInt
, is a function that can work with arbitrary types r
.
Let us also implement a foldRight
function for ListInt
. That function serves as a general “aggregation” algorithm, converting a list of integers into an
aggregated value of some type. The type signature of foldRight
is:
foldRight : ∀(r : Type) → ∀(init : r) → ∀(update : Integer → r → r) → ListInt → r
The arguments of foldRight
are an arbitrary result type r
, an initial value of type r
, and an “updater” function of type r → Integer → r
. The arguments
of the updater function are the currently aggregated value of type r
and a next integer from the list. The result is the next aggregated value (of type r
).
The function foldRight
should iterate over all integers in the list and keep updating the current result value (of type r
) until the list is finished and a
final value is obtained.
It is perhaps surprising that the code of foldRight
is non-recursive:
let foldRight : ∀(r : Type) → ∀(init : r) → ∀(update : Integer → r → r) → ListInt → r =
λ(r : Type) → λ(init : r) → λ(update : Integer → r → r) → λ(list : ListInt) →
let consR : { head : Integer, tail : r } → r = λ(fr : { head : Integer, tail : r }) → update fr.head fr.tail
let frr : F r → r = λ(fr : F r) → merge { Nil = init, Cons = consR } fr
in list r frr
in foldRight
This code merely calls the given value list : ListInt
on a certain function fr : F r → r
. That function is constructed out of the given arguments init
and update
.
Because foldRight
is non-recursive, Dhall accepts that function.
In this way, Dhall is able to construct integer lists and also to run loops over them, computing an aggregated value using foldRight
.
If we take, for example, the list l = cons +456 (cons -123 nil)
,
which corresponds to the ordinary list [+456, -123]
, then foldRight r init update l
starts
from the initial value init : r
. Then it takes the right-most element of the list (-123
) and computes update -123 init
. Then it
computes update +456 (update -123)
. This fits with the name “fold right”: the computation starts from the right-most element of the list and iterates to the
left.
As an example, we use foldRight
to implement a function that converts a ListInt
value into the built-in list type List Integer
:
let toList : ListInt → List Integer = λ(list : ListInt) →
foldRight (List Integer) ([]: List Integer) (λ(x: Integer) → λ(r : List Integer) → r # [ x ]) list
in toList (cons +456 (cons -123 nil))
The result is computed as [ +456, -123 ]
.
Where did the recursion go?
The technique of Church encoding may be perplexing. If we are actually working with recursive types and recursive functions, why do we no longer
see any recursion in the code? In foldRight
, why is there no code that iterates over a list of integers in a loop?
An answer is found by comparing the codes for the values x0
, x1
, and x2
shown in the previous section when working with ListInt
. The values x0
, x1
,
and x2
are functions whose second argument is a function frr : F r → r
. The code for x0
calls that function only once; the code for x1
calls that
function twice; and the code for x2
calls that function three times.
This explains why foldRight
is non-recursive. The code of foldRight
merely prepares a function frr
and passes it to the given value of type ListInt
. If
we
run foldRight
on x2
, it is the code of x2
that will call the function frr
three times. But there is actually no loop in x2
. It is just hard-coded in
the
function x2
to apply frr
three times in a row.
A list of 1000 integers will be represented by a function that takes an argument frr : F r → r
and applies frr
a thousand times
to some arguments.
This is because the only way of creating a list of 1000 integers is to create an expression such as cons 1 (cons 2 (cons 3 (... (cons 1000 nil)))...)
.
It is hard-coded in that expression to call the cons
function 1000 times.
In this way, the Church encoding avoids loops and allows us to represent iterative computations without recursion.
Data structures that contain 1000 integers are replaced by functions that are hard-coded to call their argument 1000 times. In this way, the Church encoding guarantees that all recursive structures will be finite and all operations on those structures will terminate. It is for that reason that Dhall is able to accept Church encodings of recursive types without compromising any safety guarantees.
Church encoding and fold
types are equivalent
How to generalize foldRight
from ListInt
to arbitrary recursive types? That is done via an equivalence relationship between a Church-encoded type, such
as ListInt
, and the type of the corresponding foldRight
function.
Looking at the type of foldRight
for ListInt
, we note that the type of functions F r → r
is equivalent to a pair of a value of type r
and a function
with the type signature Integer → r → r
.
To see why, consider how we can implement any function of type F r → r
(equivalently, < Nil | Cons : { head : Integer, tail : r } > → r
). That function must
handle two cases: when the argument is Nil
and when the argument is Cons
. When the argument is Nil
, the function must return a value of type r
. That
value must be hard-coded within the function body (because Nil
contains no data from which we could create a value of type r
). When the argument
is Cons { head = h, tail = t }
then the function must somehow create a value of type r
out of that data. This ability is equivalent to having a function of
type Integer → r → r
.
So, the data required to implement a function of type F r → r
is a pair of a value of type r
and a function of type Integer → r → r
.
This is the same as the data contained in the arguments of foldRight
(that is, init
and update
).
So, the type of foldRight
can be rewritten equivalently as:
∀(r : Type) → (F r → r) → ListInt → r
We can then swap the order of curried arguments to obtain another equivalent type expression:
ListInt → ∀(r : Type) → (F r → r) → r
This is exactly the same as the type ListInt → ListInt
. The code of foldRight
is equivalent to just an identity function of that type. No wonder it is
non-recursive!
Keeping this in mind, we may say that the Church encoding trick consists of encoding recursive types via the types of their fold
functions.
The same argument will hold for any recursive types, including recursive types with extra type parameters. Given a recursion scheme F
and the corresponding
Church-encoded type C = ∀(r : Type) → (F r → r) → r
, a fold
function can be implemented in general as an identity function of type C → C
adapted to a
typical type signature of fold
-like functions:
∀(r : Type) → ∀(frr : F r → r) → C → r
In practice, it is easier to use a value of type C
directly as a fold
function. We will see an example of that usage shortly.
The build
function
In the previous section we showed the constructors nil
and cons
for the ListInt
type. What is the corresponding technique for an arbitrary Church-encoded
type with a given recursion scheme F
?
Both the constructors nil
and cons
have types of the form “something that returns a ListInt
value”:
let nil : ListInt = ...
let cons: Integer → ListInt → ListInt = ...
To make this pattern more apparent, we can rewrite those types equivalently as functions from some record types to ListInt
:
let nil1 : {} -> ListInt = ...
let cons1: { x : Integer, l : ListInt } → ListInt = ...
The two constructors nil1
and cons1
are equivalent to a single function nil1cons1 : < n : { } | c : { x : Integer, l : ListInt } > → ListInt
.
The argument of that function has type < n : { } | c : { x : Integer, l : ListInt } >
, which is equivalent to F ListInt
because F ListInt
means < Nil | Cons : { head : Integer, tail : ListInt } >
.
So, we find that the two constructors nil
and cons
may be replaced by a single value of type F ListInt → ListInt
.
Of course, practical programming is more convenient with the pair of constructors nil
and cons
.
But the equivalence of that pair (nil
and cons
) to a single value of type F ListInt → ListInt
shows us how to generalize this technique from ListInt
to
an arbitrary recursive type.
Namely, the full set of constructors for an arbitrary recursion scheme F
and the corresponding Church-encoded type C = ∀(r : Type) → (F r → r) → r
is just a
specific value of type F C → C
.
It turns out that there is a unique value of type F C → C
that satisfies certain required properties (which are beyond the scope of this tutorial). We will
denote that value by build: F C → C
. It is that build
function that encapsulates all the basic constructors that are used for building values the type C
.
A general implementation of build
depends on having the standard fmapF
function for the type constructor F
. So, this technique only works when F
is a
covariant type constructor. But this is always true in all practical cases.
The Dhall code for build
is:
let F = ∀(r : Type) → ... -- Define it here. This is non-recursive.
let fmapF : ∀(a : Type) → ∀(b : Type) → (a → b) → F a → F b = ... -- Define it here. This is non-recursive.
let C = ∀(r : Type) → (F r → r) → r
let build : F C → C = λ(fc : F C) → λ(r : Type) → λ(frr : F r → r) →
let c2r : C → r = λ(c : C) → c r frr
let fr : F r = fmapF C r c2r fc
in frr fr
in build
In Dhall, the only built-in recursive data type is List
. The corresponding fold
and build
functions are the built-in symbols List/fold
and List/build
.
Example of using fold
and build
: pretty-printing a binary tree
We have shown how to define the fold
and build
functions for any Church-encoded type, given the recursion scheme F
and the corresponding fmapF
function.
When working with a specific type, such as a binary tree, it is often convenient to implement specific constructors for that type instead of using the
generic build
function.
As an example, we will now implement a function that converts a TreeInt
value into a string in a LISP-like format, such as "(+1 (+2 +3))"
.
As we have seen before, TreeInt
is Church-encoded via the recursion scheme F
defined by:
let F = λ(r : Type) → < Leaf: Integer | Branch : { left : r, right : r } >
let TreeInt = ∀(r : Type) → (F r → r) → r
The (non-recursive!) fmapF
function for F
is:
let fmapF : ∀(a : Type) → ∀(b : Type) → (a → b) → F a → F b =
λ(a : Type) → λ(b : Type) → λ(f : a → b) → λ(fa : F a) → merge {
Leaf = (F b).Leaf,
Branch = λ(branch : { left : a, right : a }) → (F b).Branch { left = f branch.left, right = f branch.right }
} fa
To create TreeInt
values, we need a build
function. The general code for build
is reduced to:
let build : F TreeInt → TreeInt =
λ(fc : F TreeInt) → λ(r : Type) → λ(frr : F r → r) →
frr (fmapF TreeInt r (λ(c : TreeInt) → c r frr) fc)
or equivalently:
let build : F TreeInt → TreeInt =
λ(fc : F TreeInt) → λ(r : Type) → λ(frr : F r → r) →
frr (merge {
Leaf = (F r).Leaf,
Branch = λ(branch : { left : TreeInt, right : TreeInt }) → (F r).Branch { left = branch.left r frr, right = branch.right r frr }
} fc)
A function of type F TreeInt → TreeInt
is equivalent to a pair of functions of types Integer → TreeInt
and TreeInt → TreeInt → TreeInt
respectively. Let
us denote these two functions by leaf
and branch
. The code of those functions can be read off the above code of build
if we apply build
to arguments of
type (F TreeInt).Leaf
and (F TreeInt).Branch
:
let leaf : Integer → TreeInt = λ(x : Integer) → λ(r : Type) → λ(frr : F r → r) → frr ((F r).Leaf x)
let branch : TreeInt → TreeInt → TreeInt = λ(left : TreeInt) → λ(right : TreeInt) → λ(r : Type) → λ(frr : F r → r) → frr ((F r).Branch { left = left r frr, right = right r frr })
Now we can construct values of type TreeInt
:
let example1 = branch (leaf +1) (branch (leaf +2) (leaf +3))
To implement a pretty-printer for TreeInt
, we use values of type TreeInt
as fold
functions. The type signature of the Church encoding takes care of
recursion for us. We only need to implement a non-recursive function frr : F Text → Text
that describes how to create the text representation for a larger
tree — either from a leaf or from the text representations of the two subtrees. We enclose values in parentheses and add a space between two subtrees:
let print : TreeInt → Text = λ(tree: ∀(r : Type) → (F r → r) → r) →
let frr : F Text → Text = λ(fr : F Text) →
merge {
Leaf = λ(t : Integer) → Integer/show t,
Branch = λ(b : { left : Text, right : Text }) → "(${b.left} ${b.right})" } fr
in tree Text frr
let test = assert : print example1 === "(+1 (+2 +3))"
Similarly to this example, one can use fold
to implement a wide range of recursive functions on trees.
Pattern matching on Church-encoded values
When working with recursive types in ordinary functional languages, one often uses pattern matching. For example, here is a simple function that detects whether a given tree is a single leaf:
data TreeInt = Leaf Int | Branch TreeInt TreeInt
isSingleLeaf: TreeInt -> Bool
isSingleLeaf t = case t of
Leaf _ -> true
Branch _ _ -> false
Another example is a function that checks whether the first element of a list exists:
headMaybe :: [a] -> Maybe a
headMaybe [] = Nothing
headMaybe (x:xs) = Just x
The Dhall translation of TreeInt
and ListInt
are Church-encoded types:
let F = λ(r : Type) → < Leaf: Integer | Branch : { left : r, right : r } >
let TreeInt = ∀(r : Type) → (F r → r) → r
and
let F = λ(r : Type) → < Nil | Cons : { head : Integer, tail : r } >
let ListInt = ∀(r : Type) → (F r → r) → r
Values of type TreeInt
and ListInt
are functions, so we cannot perform pattern matching on such values. How can we implement functions like isSingleLeaf
and headMaybe
in Dhall?
The general method for translating pattern matching into Church-encoded types C
consists of two steps. The first step is to define a function we will
call unroll
, of type C → F C
. This function is the inverse of the function build : F C → C
from the previous subsection.
The Dhall code for unroll
is:
let F = λ(r : Type) → ... -- Define it here. This is non-recursive.
let fmapF : ∀(a : Type) → ∀(b : Type) → (a → b) → F a → F b = ... -- Define it here. This is non-recursive.
let C = ∀(r : Type) → (F r → r) → r
let unroll : C → F C =
let fmapBuild : F (F C) → F C = fmapF (F C) C build -- Use the definition of `build` above.
in λ(c : C) → c (F C) fmapBuild
in unroll
A rigorous proof that unroll
and build
are inverse functions is shown in the
paper “Recursive types for free”, which is beyond the scope of this tutorial.
Because of those functions, the types C
and F C
are equivalent (isomorphic) to each other. Any data of type C
can be mapped
into data of type F C
and back, without loss of information. In that sense, the type C
satisfies the “type equation” C = F C
. This is one way of defining
rigorously the meaning of recursive types written in Haskell as data T = F T
.
The second step is to apply unroll
to the value on which we need to use pattern matching. The result will be a value of type F C
, which will be typically a
union type. With values of that type, we can just use the ordinary pattern matching (merge
in Dhall).
This technique allows us to translate isSingleLeaf
and headMaybe
to Dhall. Let us look at some examples.
For C = TreeInt
, the type F C
is the union type < Leaf: Integer | Branch : { left : TreeInt, right : TreeInt } >
. The function isSingleLeaf
is
implemented via pattern matching on that type:
let F = λ(r : Type) → < Leaf: Integer | Branch : { left : r, right : r } >
let TreeInt = ∀(r : Type) → (F r → r) → r
let fmapF : ∀(a : Type) → ∀(b : Type) → (a → b) → F a → F b =
λ(a : Type) → λ(b : Type) → λ(f : a → b) → λ(fa : F a) → merge {
Leaf = (F b).Leaf,
Branch = λ(branch : { left : a, right : a }) → (F b).Branch { left = f branch.left, right = f branch.right }
} fa
-- Assume the definition of `unroll` as shown above.
let isSingleLeaf : TreeInt → Bool = λ(c : TreeInt) →
merge {
Leaf = λ(_ : Integer) → true,
Branch = λ(_ : { left : TreeInt, right : TreeInt }) → false
} (unroll c)
in isSingleLeaf
For C = ListInt
, the type F C
is the union type < Nil | Cons : { head : Integer, tail : ListInt } >
. The function headOptional
that replaces
Haskell’s headMaybe
is written in Dhall like this:
let F = λ(r : Type) → < Nil | Cons : { head : Integer, tail : r } >
let ListInt = ∀(r : Type) → (F r → r) → r
let fmapF : ∀(a : Type) → ∀(b : Type) → (a → b) → F a → F b =
λ(a : Type) → λ(b : Type) → λ(f : a → b) → λ(fa : F a) → merge {
Nil = (F b).Nil,
Cons = λ(pair : { head : Integer, tail : a }) → (F b).Cons (pair // { tail = f pair.tail })
} fa
-- Assume the definition of `unroll` as shown above.
let headOptional : ListInt → Optional Integer = λ(c : ListInt) →
merge {
Cons = λ(list : { head : Integer, tail : ListInt }) → Some (list.head),
Nil = None Integer
} (unroll c)
in headOptional (cons -456 (cons +123 nil))
The result is computed as Some -456
.
Other operations on Church-encoded data types
Recursive data types such as lists and trees support certain useful operations such as concat
, filter
, or traverse
. Normally, those operations are
implemented via recursive code. To use those operations in Dhall, we need to avoid using recursion and instead use the Church-encoded data (that is, a fold-like
function) as the provider of iteration. Let us show some examples of how this can be done.
Concatenating and reversing non-empty lists
We will implement concat
and reverse
functions for non-empty lists using a Church encoding.
Non-empty lists (NEL: Type → Type
) can be defined recursively as:
data NEL a = One a | Cons a (NEL a)
The recursion scheme corresponding to this definition is:
data F a r = One a | Cons a r
Convert this definition to Dhall and write the corresponding Church encoding:
let F = ∀(a : Type) → ∀(r : Type) → < One : a | Cons : { head : a, tail: r } >
let NEL = ∀(a : Type) → ∀(r : Type) → (F a r → r) → r
It will be more convenient to rewrite the type NEL
without using union or record types. An equivalent definition is:
let NEL = λ(a : Type) → ∀(r : Type) → (a → r) → (a → r → r) → r
The standard constructors for NEL
are:
a function (
one
) that creates a list of one elementa function (
cons
) that prepends a given value of typea
to a list of typeNEL a
Non-empty list values can be now built as cons Natural 1 (cons Natural 2 (one Natural 3))
and so on.
let one : ∀(a : Type) → a → NEL a =
λ(a : Type) → λ(x : a) → λ(r : Type) → λ(ar : a → r) → λ(_ : a → r → r) → ar x
let cons : ∀(a : Type) → a → NEL a → NEL a =
λ(a : Type) → λ(x : a) → λ(prev : NEL a) → λ(r : Type) → λ(ar : a → r) → λ(arr : a → r → r) → arr x (prev r ar arr)
let example1 : NEL Natural = cons Natural 1 (cons Natural 2 (one Natural 3))
let example2 : NEL Natural = cons Natural 3 (cons Natural 2 (one Natural 1))
The folding function is just an identity function:
let foldNEL : ∀(a : Type) → NEL a → ∀(r : Type) → (a → r) → (a → r → r) → r =
λ(a : Type) → λ(nel : NEL a) → nel
To see that this is a “right fold”, apply foldNEL
to some functions ar : a → r
and arr : a → r → r
and a three-element list such as example1
. The result
will be arr 1 (arr 2 (ar 3))
; the first function evaluation is at the right-most element of the list.
Folding with one
and cons
gives again the initial list:
assert : example1 === foldNEL Natural example1 (NEL Natural) (one Natural) (cons Natural)
To concatenate two lists, we right-fold the first list and substitute the second list instead of the right-most element:
let concatNEL: ∀(a : Type) → NEL a → NEL a → NEL a =
λ(a : Type) → λ(nel1 : NEL a) → λ(nel2 : NEL a) →
foldNEL a nel1 (NEL a) (λ(x : a) → cons a x nel2) (cons a)
let test = assert : concatNEL Natural example1 example2 === cons Natural 1 (cons Natural 2 (cons Natural 3 (cons Natural 3 (cons Natural 2 (one Natural 1)))))
To reverse a list, we right-fold over it and accumulate a new list by appending elements to it.
So, we will need a new constructor (snoc
) that appends a given value of type a
to a list of type NEL a
, rather than prepending as cons
does.
let snoc : ∀(a : Type) → a → NEL a → NEL a =
λ(a : Type) → λ(x : a) → λ(prev : NEL a) →
foldNEL a prev (NEL a) (λ(y : a) → cons a y (one a x)) (cons a)
let test = assert example1 === snoc Natural 3 (snoc Natural 2 (one Natural 1))
Now we can write the reversing function:
let reverseNEL : ∀(a : Type) → NEL a → NEL a =
λ(a : Type) → λ(nel : NEL a) → foldNEL a nel (NEL a) (one a) (snoc a)
let test = assert : reverseNEL Natural example1 === example2
let test = assert : reverseNEL Natural example2 === example1
Sizing a Church-encoded type constructor
The functions concatNEL
and reverseNEL
shown in the previous section are specific to list-like sequences and cannot be straightforwardly generalized to
other recursive types, such as trees.
However, a number of useful functions (such as filter
, join
, traverse
) can be implemented generally for any Church-encoded data type.
Within this tutorial’s limited scope, we just look at some functions that compute the total size and the maximum depth of a data structure.
Suppose we are given an arbitrary recursion scheme F
with two type parameters. It defines a type constructor C
via Church encoding as:
let F = ∀(a : Type) → ∀(r : Type) → ...
let C = ∀(a : Type) → ∀(r : Type) → (F a r → r) → r
We imagine that a value p : C a
is a data structure that stores zero or more values of type a
.
The “total size” of p
is the number of the values of type a
that it stores. For example, if p
is a list of 5 elements then the size of p
is 5. The size
of a TreeInt
value branch (branch (leaf +10) (leaf +20)) (leaf +30)
is 3 because it stores three numbers.
The “maximum depth” of p
is the depth of nested recursion required to obtain that value. For example, if p
is a TreeInt
value branch (branch (leaf +10) (leaf +20)) (leaf +30)
then the depth of p
is 2. The depth of a single-leaf tree (such as leaf +10
) is 0.
The goal is to implement these functions generically, for all Church-encoded data structures at once.
Both of those functions need to traverse the entire data structure and to accumulate a Natural
value. Let us begin with size
:
let size : ∀(a : Type) → ∀(ca : C a) → Natural =
λ(a : Type) → λ(ca : C a) →
let sizeF : F a Natural → Natural = ???
in ca Natural sizeF
The function sizeF
should count the number of data items stored in F a Natural
. The values of type Natural
inside F
represent the sizes of nested
instances of C a
; those sizes have been already computed.
It is clear that the function sizeF
will need to be different for each recursion scheme F
.
For a given value fa : f a Natural
, the result of sizeF fa
will be equal to the number of values of type a
stored in fa
plus the sum of all natural
numbers stored in fa
.
For example, non-empty lists are described by F a r = < One : a | Cons : { head : a, tail: r } >
.
The corresponding sizeF
function is:
let sizeF : < One : a | Cons : { head : a, tail: Natural } > → Natural = λ(fa : < One : a | Cons : { head : a, tail: Natural } >) → merge {
One = λ(x : a) → 1,
Cons = λ(x : { head : a, tail: Natural }) → 1 + x.tail,
} fa
Binary trees are described by F a r = < Leaf : a | Branch : { left : r, right: r } >
.
The corresponding sizeF
function is:
let sizeF : < Leaf : a | Branch : { left : Natural, right: Natural } > → Natural = λ(fa : < Leaf : a | Branch : { left : Natural, right: Natural } >) → merge {
Leaf = λ(x : a) → 1,
Branch = λ(x : { left : Natural, right: Natural }) → x.left + x.right,
} fa
Having realized that sizeF
needs to be supplied for each recursion scheme F
, we can implement size
like this:
let size : ∀(a : Type) → ∀(sizeF : ∀(b : Type) → F b Natural → Natural) → ∀(ca : C a) → Natural =
λ(a : Type) → λ(ca : C a) → λ(sizeF : ∀(b : Type) → F b Natural → Natural) →
ca Natural (sizeF a)
Turning now to the depth
function, we proceed similarly and realize that the only difference is in the sizeF
function.
Instead of sizeF
described above, we need depthF
with the same type signature ∀(b : Type) → F b Natural → Natural
.
For the depth calculation, depthF
should return 1 plus the maximum of all values of type Natural
that are present. If no such values are present, it just
returns 1.
For non-empty lists (and also for empty lists), the depthF
function is the same as sizeF
(because the recursion depth is the same as the list size).
For binary trees, the corresponding depthF
function is:
let depthF : < Leaf : a | Branch : { left : Natural, right: Natural } > → Natural = λ(fa : < Leaf : a | Branch : { left : Natural, right: Natural } >) → Natural/subtract 1 (merge {
Leaf = λ(x : a) → 1,
Branch = λ(x : { left : Natural, right: Natural }) → 1 + Natural/max x.left x.right,
} fa)
Here the functions Natural/max
and Natural/subtract
come from the standard Dhall prelude.
Performance
The performance of Church-encoded values is slower than that of directly implemented data structures. Although fold
is a non-recursive function, its execution
time is linear in the data size because fold
needs to traverse the entire data.
Another source of slowness is that a value c
of a Church-encoded type C
is a function that may call its parameter frr : F r → r
many times. For example,
a ListInt
value representing a list of 1000 integers is a function that calls its parameter frr
1000 times. So, evaluating any code that applies c
to
some frr
will have to perform 1000 evaluations of some function. The evaluation time is linear in the data size.
In particular, this applies to the code of unroll
. Since we use unroll
for pattern matching, any single pattern matching will be linear in the data size.
For instance, evaluating headOptional
takes time that is linear in the length of the list, even though we may have expected that headOptional
only needs to
examine the first element of the list. If headOptional
is called repeatedly, evaluation may become quite slow.
The functions concatNEL
and reverseNEL
shown previously are quadratic in the length of the list because they use foldNEL
twice nested.
Examples
We will now illustrate the general recipe on more examples, showing how to translate recursive Haskell code into non-recursive Dhall definitions.
Recursive record
Consider the following recursive Haskell code:
-- Example0.hs
data Person = MakePerson
{ name :: String
, children :: [Person]
}
example :: Person
example =
MakePerson
{ name = "John"
, children =
[ MakePerson { name = "Mary", children = [] }
, MakePerson { name = "Jane", children = [] }
]
}
everybody :: Person -> [String]
everybody p = name p : concatMap everybody (children p)
result :: [String]
result = everybody example
… which evaluates to:
$ ghci Example0.hs
*Main> result
["John","Mary","Jane"]
To convert this to non-recursive Dhall code, we use the Church encoding recipe.
The first step is to define the recursion scheme. We name the type parameter _Person
to avoid confusion, as we will want to define the type Person
later.
let F = λ(_Person : Type) → { name: String, children: List _Person }
This definition is non-recursive and will be accepted by Dhall.
We do not need to Church-encode List
as it is natively supported in Dhall.
The next step is to define the Church-encoded type Person
:
let Person = ∀(_Person : Type) → (F _Person → _Person) → _Person
This replaces the Haskell definition of the type Person
.
Next, we create the example value of type Person
. We will name the function parameter MakePerson
for clearer comparison with the Haskell code.
let example : Person = λ(_Person : Type) → λ(MakePerson : F _Person → _Person) →
MakePerson {
name = "John",
children =
[ MakePerson { name = "Mary", children = [] : List _Person ]
, MakePerson { name = "Jane", children = [] : List _Person ]
]
}
Now we need to translate the recursive function everybody
to Dhall.
Actually, one cannot mechanically translate arbitrary recursive code to Church-encoded types.
This is possible only for certain classes of recursive functions.
One such class comprises “fold-like” functions: that is, functions whose recursive calls are performed directly on substructures of the recursive data type.
It turns out that everybody
is a fold-like function. To see why, let us visualize how that function works on a given recursive data structure in Haskell:
everybody (MakePerson { name = "John", children = [ child1, child2, ... ] })
== "John" : concat [(everybody child1), (everybody child2), ...]
Here child1
and child2
are substructures of the recursive data type Person
. Each of them is again of type Person
.
Once the recursive calls everybody child1
, everybody child2
, etc., are finished, we obtain a list of values of type List String
. The remaining computation
is equivalent to a function of type String → List (List String) → List String
.
That type is equivalent to just F (List String) → List String
and is exactly the same type as the argument of the Church-encoded type Person
if we use the
type List String
instead of the type argument _Person
.
So, the recursive pattern can be replaced by a “fold” where we just need to supply a “folding function” of type F (List String) → List String
as its argument.
The Dhall code is:
let everybody : Person → List Text =
let concat = http://prelude.dhall-lang.org/List/concat
let foldingFunction : F (List Text) → List Text = λ(p : F (List Text)) →
[ p.name ] # concat Text p.children
in
λ(x : Person) → x (List Text) foldingFunction
Put together the entire Dhall code and run it:
-- example0.dhall
let Person
: Type
= ∀(_Person : Type) →
∀(MakePerson : { children : List _Person, name : Text } → _Person) →
_Person
let example
: Person
= λ(_Person : Type) →
λ(MakePerson : { children : List _Person, name : Text } → _Person) →
MakePerson
{ children =
[ MakePerson { children = [] : List _Person, name = "Mary" }
, MakePerson { children = [] : List _Person, name = "Jane" }
]
, name = "John"
}
let everybody
: Person → List Text
= let concat = http://prelude.dhall-lang.org/List/concat
in λ(x : Person) →
x
(List Text)
( λ(p : { children : List (List Text), name : Text }) →
[ p.name ] # concat Text p.children
)
let result
: List Text
= everybody example
in result
… which evaluates to the same result as the Haskell code:
$ dhall <<< './example0.dhall'
List Text
[ "John", "Mary", "Jane" ]
Recursive union types
Recursive union types are covered by the same Church encoding recipe, except that their recursion schemes F
will be union types.
Instead of writing lots of merge
expressions, it is more convenient to replace the type of functions F r → r
by a curried function type.
For example, consider the natural number type implemented recursively with two constructors Zero
and Succ
via this Haskell code:
-- Example1.hs
import Numeric.Natural (Natural)
data Nat = Zero | Succ Nat
example :: Nat
example = Succ (Succ (Succ Zero))
toNatural :: Nat -> Natural
toNatural Zero = 0
toNatural (Succ n) = 1 + toNatural n
result :: Natural
result = toNatural example
… which produces this result
:
$ ghci Example1.hs
*Main> result
3
The Church encoding can be written without union types as:
let Nat : Type = ∀(_Nat : Type) → ∀(Zero : _Nat) → ∀(Succ : _Nat → _Nat) → _Nat
So, the Haskell code corresponds to this Dhall code (we rename _Nat
to just Nat
for brevity):
-- example1.dhall
let Nat
: Type
= ∀(Nat : Type) → ∀(Zero : Nat) → ∀(Succ : Nat → Nat) → Nat
let example
: Nat
= λ(Nat : Type) →
λ(Zero : Nat) →
λ(Succ : Nat → Nat) →
Succ (Succ (Succ Zero))
let toNatural
: Nat → Natural
= λ(x : Nat) → x Natural 0 (λ(n : Natural) → 1 + n)
let result
: Natural
= toNatural example
in result
… which produces the same result
:
$ dhall <<< './example1.dhall'
Natural
3
Mutually recursive types
The above pattern generalizes to mutually recursive types, too. For example, this Haskell code:
-- Example2.hs
import Numeric.Natural
data Even = Zero | SuccEven Odd
data Odd = SuccOdd Even
example :: Odd
example = SuccOdd (SuccEven (SuccOdd Zero))
oddToNatural :: Odd -> Natural
oddToNatural (SuccOdd e) = 1 + evenToNatural e
evenToNatural :: Even -> Natural
evenToNatural Zero = 0
evenToNatural (SuccEven o) = 1 + oddToNatural o
result :: Natural
result = oddToNatural example
… which produces this result
:
$ ghci Example2.hs
*Main> result
3
… corresponds to this Dhall code:
let Odd
: Type
= ∀(Even : Type) →
∀(Odd : Type) →
∀(Zero : Even) →
∀(SuccEven : Odd → Even) →
∀(SuccOdd : Even → Odd) →
Odd
let example
: Odd
= λ(Even : Type) →
λ(Odd : Type) →
λ(Zero : Even) →
λ(SuccEven : Odd → Even) →
λ(SuccOdd : Even → Odd) →
SuccOdd (SuccEven (SuccOdd Zero))
let oddToNatural
: Odd → Natural
= λ(o : Odd) →
o Natural Natural 0 (λ(n : Natural) → 1 + n) (λ(n : Natural) → 1 + n)
let result = oddToNatural example
in result
… which produces the same result
:
$ dhall <<< './example2.dhall'
Natural
3
The trick here is that the Dhall’s Odd
type combines both of the Haskell
Even
and Odd
types. Similarly, Dhall’s oddToNatural
function combines
both of the Haskell evenToNatural
and oddToNatural
functions. You can
define a separate Even
and evenToNatural
in Dhall, too, but they would not
reuse any of the logic from Odd
or oddToNatural
.
Smart constructors
The build
function shown earlier is, in principle, equivalent to all possible constructors that one may need when building values of a recursive type.
When working with recursive union types, however, using the build
function requires writing lots of merge
expressions.
It is more convenient to create several “smart constructors” that replace the need for merge
expressions.
The following examples implement the same logic as the prior examples, except defining convenient intermediate constructors along the way.
For example, we can define a MakePerson
smart constructor and then use that
smart constructor to create the same example
Person
:
-- example0.dhall
let List/map = https://prelude.dhall-lang.org/v16.0.0/List/map
let Person
: Type
= ∀(Person : Type) →
∀(MakePerson : { children : List Person, name : Text } → Person) →
Person
let MakePerson
: { children : List Person, name : Text } → Person
= λ(x : { children : List Person, name : Text }) →
λ(_Person : Type) →
λ(MakePerson : { children : List _Person, name : Text } → _Person) →
let adapt
: Person → _Person
= λ(y : Person) → y _Person MakePerson
in MakePerson
(x with children = List/map Person _Person adapt x.children)
let example
: Person
= MakePerson
{ children =
[ MakePerson { children = [] : List Person, name = "Mary" }
, MakePerson { children = [] : List Person, name = "Jane" }
]
, name = "John"
}
let everybody
: Person → List Text
= let concat = http://prelude.dhall-lang.org/List/concat
in λ(x : Person) →
x
(List Text)
( λ(p : { children : List (List Text), name : Text }) →
[ p.name ] # concat Text p.children
)
let result
: List Text
= everybody example
in result
Carefully notice the difference in how we create an example
Person
, which
changed from this:
let example
: Person
= λ(Person : Type) →
λ(MakePerson : { children : List Person, name : Text } → Person) →
MakePerson
{ children =
[ MakePerson { children = [] : List Person, name = "Mary" }
, MakePerson { children = [] : List Person, name = "Jane" }
]
, name = "John"
}
… to this:
let example
: Person
= MakePerson
{ children =
[ MakePerson { children = [] : List Person, name = "Mary" }
, MakePerson { children = [] : List Person, name = "Jane" }
]
, name = "John"
}
They are both the same type and they both have the same normal form, but the
latter is more ergonomic to create from the end user’s perspective due to
using the MakePerson
smart constructor we defined.
We can also rework the Nat
example in the same way:
-- example1.dhall
let Nat
: Type
= ∀(Nat : Type) → ∀(Zero : Nat) → ∀(Succ : Nat → Nat) → Nat
let Zero
: Nat
= λ(Nat : Type) → λ(Zero : Nat) → λ(Succ : Nat → Nat) → Zero
let Succ
: Nat → Nat
= λ(x : Nat) →
λ(Nat : Type) →
λ(Zero : Nat) →
λ(Succ : Nat → Nat) →
Succ (x Nat Zero Succ)
let example
: Nat
= Succ (Succ (Succ Zero))
let toNatural
: Nat → Natural
= λ(x : Nat) → x Natural 0 (λ(n : Natural) → 1 + n)
let result
: Natural
= toNatural example
in result
… and the Even
/Odd
example:
let Even
: Type
= ∀(Even : Type) →
∀(Odd : Type) →
∀(Zero : Even) →
∀(SuccEven : Odd → Even) →
∀(SuccOdd : Even → Odd) →
Even
let Odd
: Type
= ∀(Even : Type) →
∀(Odd : Type) →
∀(Zero : Even) →
∀(SuccEven : Odd → Even) →
∀(SuccOdd : Even → Odd) →
Odd
let Zero
: Even
= λ(Even : Type) →
λ(Odd : Type) →
λ(Zero : Even) →
λ(SuccEven : Odd → Even) →
λ(SuccOdd : Even → Odd) →
Zero
let SuccEven
: Odd → Even
= λ(x : Odd) →
λ(Even : Type) →
λ(Odd : Type) →
λ(Zero : Even) →
λ(SuccEven : Odd → Even) →
λ(SuccOdd : Even → Odd) →
SuccEven (x Even Odd Zero SuccEven SuccOdd)
let SuccOdd
: Even → Odd
= λ(x : Even) →
λ(Even : Type) →
λ(Odd : Type) →
λ(Zero : Even) →
λ(SuccEven : Odd → Even) →
λ(SuccOdd : Even → Odd) →
SuccOdd (x Even Odd Zero SuccEven SuccOdd)
let example
: Odd
= SuccOdd (SuccEven (SuccOdd Zero))
let oddToNatural
: Odd → Natural
= λ(o : Odd) →
o Natural Natural 0 (λ(n : Natural) → 1 + n) (λ(n : Natural) → 1 + n)
let result = oddToNatural example
in result
In each case, the general pattern for building the “smart constructors” is the same: any time we reach a recursive occurrence of the type, we apply the recursive occurrence to all the variables we brought into scope, in the same order.
For example, when building the smart constructor for MakePerson
, each
recursive occurrence is bound to a variable named y
, which is applied to
the two bound variables we brought into scope (Person
and MakePerson
):
let MakePerson
: { children : List Person, name : Text } → Person
= λ(x : { children : List Person, name : Text }) →
λ(_Person : Type) →
λ(MakePerson : { children : List _Person, name : Text } → _Person) →
let adapt
: Person → _Person
= λ(y : Person) → y _Person MakePerson
-- See here: ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
in MakePerson
(x with children = List/map Person _Person adapt x.children)
… and you see the same pattern here in the Nat
example:
let Succ
: Nat → Nat
= λ(x : Nat) →
λ(Nat : Type) →
λ(Zero : Nat) →
λ(Succ : Nat → Nat) →
Succ (x Nat Zero Succ)
-- Here: ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
… and in the Even
/Odd
example:
let SuccEven
: Odd → Even
= λ(x : Odd) →
λ(Even : Type) →
λ(Odd : Type) →
λ(Zero : Even) →
λ(SuccEven : Odd → Even) →
λ(SuccOdd : Even → Odd) →
SuccEven (x Even Odd Zero SuccEven SuccOdd)
-- Here: ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
JSON
You can see a real example of this pattern in the Prelude’s support for
JSON
Conclusion
The general algorithm for translating recursive code to non-recursive code is known as the Boehm-Berarducci encoding and is shown in this paper:
This guide doesn’t explain the full algorithm due to the amount of detail involved, but if you are interested you can read the above paper.
Also, if the above examples were not sufficient, feel free to open an issue to request another example to add to this guide.