Gidti Sample
Gidti Sample
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean
Publishing process. Lean Publishing is the act of publishing an in-progress ebook
using lightweight tools and many iterations to get reader feedback, pivot until you
have the right book and build traction once you do.
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1. Formal systems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.1. MU puzzle example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4. Programming in Idris . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
4.1. Basic syntax and definitions . . . . . . . . . . . . . . . . . . . . . . . . . . 8
4.1.1. Defining functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
4.1.2. Defining and inferring types . . . . . . . . . . . . . . . . . . . . . . 10
4.1.3. Anonymous lambda functions . . . . . . . . . . . . . . . . . . . . 15
4.1.4. Recursive functions . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
4.1.5. Recursive data types . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
4.1.6. Total and partial functions . . . . . . . . . . . . . . . . . . . . . . . 22
4.1.7. Higher-order functions . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.1.8. Dependent types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.1.9. Implicit parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.1.10. Pattern matching expressions . . . . . . . . . . . . . . . . . . . . 29
4.1.11. Interfaces and implementations . . . . . . . . . . . . . . . . . . . 30
4.2. Curry-Howard isomorphism . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.3. Quantitative Type Theory . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Thanks to:
Thanks to my family, coworkers, and friends for all the support they give to me.
Finally, thank you for purchasing this book! I hope that you will learn new techniques
in reading this book and that it will spark up some more interest in logic, dependent
types, and type theory.
Introduction
Writing correct code in software engineering is a complex and expensive task, and too
often our written code produces inaccurate or unexpected results. There are several
ways to deal with this problem. In practice, the most common approach is to write
tests, which means that we are writing more code to test our original code. However,
these tests can only ever detect problems in specific cases. As Edsger Dijkstra noted,
“testing shows the presence, not the absence of bugs”. A less common approach is to
find a proof of correctness for our code. A software proof of correctness is a logical
proof that the software is functioning according to given specifications. With valid
proofs, we can cover all possible cases and be more confident that the code does
exactly what it is intended to do.
Idris is a general-purpose functional¹ programming language that supports depen-
dent types. The features of Idris are influenced by Haskell, another general-purpose
functional programming language. Thus, Idris shares many features with Haskell,
especially in the part of syntax and types, where Idris has a more advanced type
system. There are several other programming languages that support dependent
types², however, I chose Idris for its readable syntax.
The first version of Idris was released in 2009. It is developed by The Idris Community
and led by Dr. Edwin Brady. Seen as a programming language, it is a functional
programming language implemented with dependent types. Seen as a logical system,
it implements intuitionistic type theory, which we will cover later. Finally, we will
show how these two views relate to each other in section 4.2.
Idris allows us to express mathematical statements. By mechanically examining these
statements, it helps us find a formal proof of a program’s formal specification.
To fully understand how proofs in Idris work we will start with the foundations by
defining: formal systems, classical mathematical logic, lambda calculus, intuitionistic
logic, and type theory (which is a more “up-to-date” version of classical mathematical
logic).
¹The core concept of functional programming languages is a mathematical function.
²Several other languages with dependent types support are Coq, Agda, Lean.
1. Formal systems
Before we can construct proofs of correctness for software we need to understand
what a proof is and what it means for a proof to be valid. This is the role of formal
systems. The purpose of formal systems is to let us reason about reasoning – to
manipulate logical proofs in terms of their form, rather than their content. This level
of abstraction makes formal systems powerful tools.
Definition 1
A formal system is a model of abstract reasoning. A formal system consists
of:
Inside a given formal system the grammar determines which formulas are syntac-
tically sensible, while the inference rules govern which formulas are semantically
sensible. The difference between these two is important. For example, thinking of
the English language as a (very complicated!) formal system, the sentence “Colorless
green ideas sleep furiously” is syntactically valid (since different parts of speech are
used in the right places), but is semantically nonsense.
After a formal system is defined, other formal systems can extend it. For example,
set theory is based on first-order logic, which is based on propositional logic which
represents a formal system. We’ll discuss this theory briefly in the next chapter.
1. Formal systems 5
Definition 2
For a given formal system, the system is incomplete if there are statements
that are true but which cannot be proved to be true inside that system.
Conversely, the system is complete if all true statements can be proved.
The statement “This statement is not provable” can either be true or false. In the
case it is true, then it is not provable. Alternatively, in the case it is false then it is
provable, but we’re trying to prove something false. Thus the system is incomplete
because some truths are unprovable.
Definition 3
For a given formal system, the system is inconsistent if there is a theorem
in that system that is contradictory. Conversely, the system is consistent
if there are no contradictory theorems.
A simple example is the statement “This statement is false”. This statement is true if
and only if³ it is false, and therefore it is neither true nor false.
In general, we often put our focus on which parts of mathematics can be formalized
in concrete formal systems, rather than trying to find a theory in which all of
mathematics can be developed. The reason for that is Gödel’s incompleteness
theorem. This theorem states that there doesn’t exist⁴ a formal system that is both
complete and consistent. As a result, it is better to reason about a formal system
outside of the system (at the metalanguage level), since the object level (rules within
the system) can be limiting. As the famous saying goes to “think outside of the box”,
and similarly to how we sometimes do meta-thinking to improve ourselves.
In conclusion, formal systems are our attempt to abstract models, whenever we
reverse engineer nature in an attempt to understand it better. They may be imperfect
but are nevertheless useful tools for reasoning.
³The word iff is an abbreviation for “If and only if” and means that two statements are logically equivalent.
⁴Note that this theorem only holds for systems that allow expressing arithmetic of natural numbers (e.g. Peano,
set theory, but first-order logic also has some paradoxes if we allow self-referential statements). This is so because the
incompleteness theorem relies on the Gödel numbering concept, which allows a formal system to reason about itself by
using symbols in the system to map expressions in that same system. For example, 0 is mapped to 1, S is 2, = is 3, so
0 = S0 ⇐⇒ (1, 3, 2, 1). Using this we can express statements about the system within the system - self-referential
statements. We will look into these systems in the next chapter.
1. Formal systems 6
Definition 4
We’re given a starting string MI, combined with a few inference rules, or
transformation rules:
No. Rule Description Example
1 xI → xIU Append U at a string ending in MI to MIU
I
2 Mx → Mxx Double the string after M MIU to MIUIU
3 xIIIy → xUy Replace III inside a string MUIIIU to MUUU
with U
4 xUUy → xy Remove UU from inside a string MUUU to MU
In the inference rules the symbols M, I, and U are part of the system, while x is a
variable that stands for any symbol(s). For example, MI matches rule 2 for x = I, and
it can also match rule 1 for x = M. Another example is MII that matches rule 2 for x
= II and rule 1 for x = MI.
We will show (or prove) how we can get from MI to MIIU using the inference rules:
1. MI (axiom)
2. MII (rule 2, x = I)
3. MIIII (rule 2, x = II)
4. MIIIIIIII (rule 2, x = IIII)
5. MUIIIII (rule 3, x = M, y = IIIII)
6. MUUII (rule 3, x = MU, y = II)
7. MII (rule 4, x = M, y = II)
8. MIIU (rule 1, x = MI)
1. Formal language
1. Formal systems 7
Note that, to be able to apply rule 3, we need to have the number of subsequent I’s
to be divisible by 3. Let’s have our invariant say that “There is no sequence of I’s in
the string that with length divisible by 3”:
We’ve shown that with the starting axiom MI it is not possible to get to MU because no
sequence of steps can turn a string with one I into a string with no Is. But if we look
carefully, we’ve used a different formal system to reason about MU (i.e. divisibility by
3, which is not part of the MU system). This is because the puzzle cannot be solved in
its own system. Otherwise, an algorithm would keep trying different inference rules
of MU indefinitely (not knowing that MU is impossible).
Every useful formal system has this limitation. As we’ve seen, Gödel’s theorem
shows that there’s no formal system that can contain all possible truths, because
it cannot prove some truths about its own structure. Thus, having experience with
different formal systems and combining them as needed can be useful.
⁵An invariant is a property that holds whenever we apply any of the inference rules.
⁶After having introduced ourselves to proofs, you will be given an exercise to prove this fact.
4. Programming in Idris
In this chapter, we will introduce Idris’ syntax, by defining functions and types.
Depending on what level of abstraction we are working on, types and proofs can
give us a kind of security based on some truths we take for granted (axioms). In fact,
this is how we develop code on a daily basis, as software engineers. We have a list of
axioms, for example, a foreach loop in a programming language, and starting from
it we build abstractions, expecting this foreach to behave in a certain way. However,
this security is not always easy to achieve. For example, consider a scenario where
we have a button that is supposed to download a PDF document. In order to prove
that it works as expected, we must first pick the abstraction level we will be working
on, and then proceed by defining software requirements (what is a PDF, what is a
download). So, we first have to define our specifications, and then we can proceed
with proving correctness.
Idris, even though a research language, still has its own uses. It is a Turing complete
language, which means that it has the same expressive power as other programming
languages, for example, Java, or C++.
To start with, Idris can be downloaded and installed via www.idris-lang.org/download.
With the code above we’re defining a function f (x) = x+1, where x is natural number
and f (x) (the result) is also a natural number⁷. The first line add_1 : Nat -> Nat is
called a type signature, and specifies the type of our function; in this case a function
that takes a natural number and returns a natural number. The second line add_1 x
= x + 1 is the definition of the function, which states that if add_1 is called with a
number x, the result would be x + 1. As can be seen by the example, every function
has to be provided a type definition. We can interact as follows in the REPL mode:
1 Idris> add_1 5
2 6 : Nat
Idris responds to us that as a result, we get 6, which is of type Nat. Constants are
defined similarly; we can think of them as functions with no arguments.
1 number_1 : Nat
2 number_1 = 1
As in Haskell, we can use pattern matching. What happens during this phase is Idris
does a check (match) against the definitions of the function and uses the definition
of the function that matches the value.
⁷It is worth noting that in Haskell we have types and kinds. Kinds are similar to types, that is, they are defined as
one level above types in simply-typed lambda calculus. For example, types such as Nat have a kind Nat :: * and it’s
stated that Nat is of kind *. Types such as Nat -> Nat have a kind of * -> *. Since in Idris the types are first-class citizens,
there is no distinction between types and kinds.
4. Programming in Idris 10
We’ve just defined a function that accepts a natural number and returns a boolean
value (True or False). On the first line, we specify its type. On the second line, we
pattern match against the input Z and return True in that case. On the third line,
we pattern match against the input x (which is all remaining inputs except Z). In
this case, we return False. The code above, depending on the input, will branch the
computation to the corresponding definition. Note that Z corresponds to 0 for the
type Nat.
Exercise 1
Write a function my_identity which accepts a natural number and returns
the same number.
Exercise 2
Write a function five_if_zero which accepts a natural number and returns
5 when called with an argument of 0, otherwise returns the same number.
For example, five_if_zero 0 should return 5. five_if_zero 123 should
return 123.
⁸A polymorphic type can accept additional types as arguments, which are either defined by the programmer or
primitive ones.
4. Programming in Idris 11
1 data A a b = A_inst a b
This will create a polymorphic type that accepts two type arguments, a and b. Valid
constructed types are A Nat Bool, A Nat Nat, etc. A is a type constructor (a function
that returns a type) and A_inst⁹ is a value constructor (a function that returns a value
of type A a b).
Note how the type changes depending on what values we pass to the constructor,
due to polymorphism.
Here’s an equivalent definition by using the GADT (Generalized Algebraic Data Types)
syntax:
With the postulate keyword we can define axioms which are functions that satisfy
the types without giving an actual argument for construction. With the command :t
we can check the type of an expression, so as a result, we get:
⁹The value and type constructors must be named differently since types and values are at the same level in Idris.
4. Programming in Idris 12
1 Idris> :t A
2 A : Type -> Type -> Type
3 Idris> :t A_inst
4 A_inst : a -> b -> A a b
That is, we show the type definitions for both the newly-defined type and its value
constructor. Note how we created a product type here. Idris has a built-in¹⁰ notion of
pairs, which is a data type that can be defined in terms of products. For example, (1,
2) is one pair. We can also define tuples with (1, "Hi", True), which is equivalent
to (1, ("Hi", True)), i.e. a pair where the first element is a number, and the second
element is a pair. Note that the types (a, b) -> c (curried) and a -> b -> c
(uncurried) represent the same thing.
Analogously, if we want to create a sum type, we could do the following:
1 Idris> :t B
2 B : Type -> Type -> Type
3 Idris> :t B_inst_left
4 B_inst_left : a -> B a b
5 Idris> :t B_inst_right
6 B_inst_right : b -> B a b
The functions B_inst_left and B_inst_right are also known as introduction rules.
For extracting values from data types such as B a b, also known as elimination rules,
we can use pattern matching¹¹. As an example, to extract a from B a b, we can use
¹⁰By built-in, we usually mean it’s part of Idris’ library. We can always implement it ourselves if we need to.
¹¹Although product and sum types are very general, due to polymorphism, we can say something very specific about
the structure of their values. For instance, suppose we’ve defined a type like so: data C a b c = C_left a | C_right
(b,c). A value of type C can only come into existence in one of two ways: as a value of the form C_left x for a value x
: a, or as a value of the form C_right (y,z) for values y : b and z : c.
4. Programming in Idris 13
1 f : B a b -> a
2 f (B_inst_left a) = a
Note how we used the data type at the function type level and the value constructor
in the function definition to pattern match against.
Natural numbers can be defined as data Nat = Z | S Nat, where we either have a
zero or a successor of a natural number. Note how this type is not polymorphic (it
doesn’t accept any variables after the type name). Natural numbers are built-in as a
type in Idris.
We can use the operator == to compare two numbers. Note that == is still a function,
but it’s an infix one. This means that unlike other functions that we define which are
prefix, i.e. start at the beginning of the expression, == needs to appear between the
parameters. For example:
1 Idris> 1 == 1
2 True : Bool
Idris has several built-in primitive types, including: Bool, Char, List, String (list of
characters), Integer, etc. The only value constructors for the type Bool are True and
False. The difference between a Nat and an Integer is that Integer can also contain
negative values. Here are some examples of interacting with these types:
1 Idris> "Test"
2 "Test" : String
3 Idris> '!'
4 '!' : Char
5 Idris> 1
6 1 : Integer
7 Idris> '1'
8 '1' : Char
9 Idris> [1, 2, 3]
10 [1, 2, 3] : List Integer
By using the :doc command, we can get detailed information about a data type:
4. Programming in Idris 14
In order to make Idris infer the necessary type of the function that needs to be built,
we can take advantage of holes. A hole is any variable that starts with a question
mark. For example, if we have the following definition:
We can now ask Idris to tell us the type of hole1, that is, with :t hole1 we can
see that Idris inferred that the specific result is expected to be of type Bool. This is
useful because it allows us to write programs incrementally (piece by piece) instead
of constructing the program all at once.
Exercise 3
Define your own custom type. One example is data Person = PersonInst
String Nat, where String represents the Person’s name and Nat represents
the Person’s age. Use the constructor to generate some objects of that type.
Afterward, use :t to check the types of the type and value constructors.
Exercise 4
Come up with a function that works with your custom type (for example,
it extracts some value) by pattern matching against its value constructor(s).
4. Programming in Idris 15
Exercise 5
Repeat Exercise 4 and use holes to check the types of the expressions used
in your function definition.
1 Idris> let f = 1 in f
2 1 : Integer
Alternatively, the REPL has a command :let that allows us to set a variable without
evaluating it right away, that can be used at a later point:
1 Idris> :let f = 1
2 Idris> f
3 1 : Integer
Lambda (anonymous) functions are defined with the syntax \a, b, ..., n => ....
For example:
4. Programming in Idris 16
With the example above, we defined a function addThree that accepts three param-
eters and as a result it sums them. However, if we do not pass all parameters to a
function, it will result in:
We can see (from the type) that as a result, we get another function.
Definition 1
Currying is a concept that allows us to evaluate a function with multiple
parameters as a sequence of functions, each having a single parameter.
Exercise 6
Write a lambda function that returns True if the parameter passed to it is
42 and False otherwise.
The definition states that 0 is an even number, and that n + 1 is even or not depending
on the parity (evenness) of n. As a result, we get:
1 Idris> even 3
2 False : Bool
3 Idris> even 4
4 True : Bool
5 Idris> even 5
6 False : Bool
1 even 5 =
2 not (even 4) =
3 not (not (even 3)) =
4 not (not (not (even 2))) =
5 not (not (not (not (even 1)))) =
6 not (not (not (not (not (even 0))))) =
7 not (not (not (not (not True)))) =
8 not (not (not (not False))) =
9 not (not (not True)) =
10 not (not False) =
11 not True =
12 False
We see how this exhibits a recursive behavior since the recursive cases were reduced
to the base case in an attempt to get a result. With this example, we can see the power
of recursion and how it allows us to process values in a repeating manner.
A recursive function can generate an iterative or a recursive process:
1. An iterative process¹² (tail recursion) is a process where the return value at any
point in computation is captured completely by its parameters
2. A recursive one, in contrast, is one where the return value is not captured
at any point in computation by the parameters, and so it relies on postponed
evaluations
In the example above, even generates a recursive process since it needs to go down
to the base case, and then build its way back up to do the calculations that were
postponed. Alternatively, we can rewrite even so that it captures the return value by
introducing another variable, as such:
¹²The key idea is not that a tail-recursive function is an iterative loop, but that a smart enough compiler can pretend
that it is and evaluate it using constant function stack space.
4. Programming in Idris 19
In this case, we can refer to the return value of the function (second parameter) at
any point in the computation. As a consequence, this function generates an iterative
process, since the results are captured in the parameters. Note how we brought down
the base case to refer to the parameter, instead of a constant True. Here is how Idris
evaluates even 5 True:
1 even 5 True =
2 even 4 False =
3 even 3 True =
4 even 2 False =
5 even 1 True =
6 even 0 False =
7 False
To conclude, iterative processes take fewer calculation steps and are usually more
performant than recursive processes. Recursive functions combined with pattern
matching are one of the most powerful tools in Idris since they allow for computation.
They are also useful for proving mathematical theorems with induction, as we will
see in the examples later.
Exercise 7
{
1, if n = 0
Factorial is defined as: f act(n) =
n · f act(n − 1), otherwise
Exercise 8
Re-write the factorial function to generate an iterative process.
Hint: The type is fact_iter : Nat -> Nat -> Nat and you should
pattern match against Z acc (number zero, and accumulator) and (S n)
acc (successor, and accumulator).
Definition 2
A recursive data type is a data type where some of its constructors have a
reference to the same data type.
We will start by defining a recursive data type, which is a data type that in the
constructor refers to itself. In fact, earlier in this book we already gave a recursive
definition - Nat. As a motivating example, we will try to define the representation of
lists. For this data type, we’ll use a combination of sum and product types. A list is
defined as either End (end of the list) or Cons (construct), which is a value appended
to another MyList:
This means that the type MyList has two constructors, End and Cons. If it’s End, then
it’s the end of the list (and does not accept any more values). However, if it’s Cons,
then we need to append another value (e.g. Cons 3), but afterward, we have to specify
another value of type MyList a (which can be End or another Cons). This definition
allows us to define a list. As an example, this is how we would represent (1, 2) using
our Cons End representation:
4. Programming in Idris 21
Note how Idris automatically infers the polymorphic type to MyList Integer and
MyList Char. In particular, these lists are homogeneous; all the items in a MyList a
must have type a.
Here is one way of implementing the concatenation function, which given two lists,
should produce a list with the elements appended:
The first line of the code says that add' is a function that accepts two polymorphic
lists (MyList Nat, MyList Char, etc), and produces the same list as a result. The
second line of the code pattern matches against the first list and when it’s empty we
just return the second list. The third line of the code also pattern matches against the
first list, but this time it covers the Cons case. So whenever there is a Cons in the first
list, as a result, we return this element Cons x appended recursively to add' xs ys,
where xs is the remainder of the first list and ys is the second list. Example usage:
Exercise 9
Unfold add' (Cons 1 (Cons 2 (Cons 3 End))) (Cons 4 End) on paper to
get a better understanding of how this definition appends two lists.
Exercise 10
Come up with a definition for length', which should return the number of
elements, given a list.
Hint: The type is length' : MyList a -> Nat
4. Programming in Idris 22
Exercise 11
Come up with a definition for even_members, which should return a new
list with even natural numbers only.
Hint: The type is even_members : MyList Nat -> MyList Nat. You can
re-use the definition of even we’ve discussed earlier.
Exercise 12
Come up with a definition for sum', which should return a number that
will be the sum of all elements in a list of natural numbers.
Hint: The type is sum' : MyList Nat -> Nat.
Definition 3
A total function is a function that terminates (or returns a value) for all
possible inputs.
A partial function is the opposite of a total function. If a function is total, its type
can be understood as a precise description of what that function can do. Idris
differentiates total from partial functions but allows defining both¹³. As an example,
if we assume that we have a function that returns a String, then:
1 partial
2 test : Nat -> String
3 test Z = "Hi"
4
5 total
6 test2 : Nat -> String
7 test2 Z = "Hi"
8 test2 _ = "Hello"
1 Idris> test 0
2 "Hi" : String
3 Idris> test 1
4 test 1 : String
5 Idris> test2 0
6 "Hi" : String
7 Idris> test2 1
8 "Hello" : String
We can note that the evaluation of test 1 does not produce a computed value as
a result. Note that at compile-time, Idris will evaluate the types only for total
functions.
Exercise 13
Try to define a function to be total, and at the same time make sure you
are not covering all input cases. Note what errors will Idris return in this
case.
Definition 4
A higher-order function is a function that takes one or more functions as
parameters or returns a function as a result.
4. Programming in Idris 24
There are three built-in higher-order functions that are generally useful: map, filter,
fold (left and right). Here’s the description of each:
1. map is a function that takes as input a function with a single parameter and a
list and returns a list where all members of the list have this function applied
to them
2. filter is a function that takes as input a function (predicate) with a single
parameter (that returns a Bool) and a list and only returns those members in
the list whose predicate evaluates to True
3. fold is a function that takes as input a combining function that accepts two
parameters (current value and accumulator), an initial value and a list and
returns a value combined with this function. There are two types of folds, a
left and a right one, which combines from the left and the right respectively
As an example usage:
Note that :: is used by the built-in List type, and is equivalent to Cons we’ve used
earlier. However, since :: is an infix operator, it has to go between the two arguments.
The value [] represents the empty list and is equivalent to End. In addition, the built-
in List type is polymorphic.
4. Programming in Idris 25
Exercise 14
Do a few different calculations with mymap in order to get a deeper
understanding of how it works.
Exercise 15
Implement a function myfilter that acts just like the filter function.
Hint: Use :t filter to get its type.
Exercise 16
Given foldl (\x, y => [y] ++ x) [] [1, 2, 3] and foldr (\x, y => y
++ [x]) [] [1, 2, 3]:
We created a new type called MyVect which accepts a natural number n and returns
a Type, that is joined with two value constructors:
1 x : MyVect 2
2 x = Cons 1 (Cons 2 Empty)
1 x : MyVect 3
2 x = Cons 1 (Cons 2 Empty)
Which is a way of Idris telling us that our types do not match and that it cannot
verify the “proof” provided.
4. Programming in Idris 27
In this example, we implemented a dependent type that puts the length of a list at
the type level. In other programming languages that do not support dependent types,
this is usually checked at the code level (run-time) and compile-time checks are not
able to verify this.
One example where such a guarantee might be useful is in preventing buffer
overflows. We could encode the dimension of an array at the type level, and statically
guarantee that array reads and writes only happen within bounds.
Exercise 17
Come up with a function isSingleton that accepts a Bool and returns a
Type. This function should return an object of type Nat in the True case,
and MyVect Nat otherwise. Further, implement a function mkSingle that
accepts a Bool, and returns isSingleton True or isSingleton False, and
as a computed value will either return 0 or Empty.
Hint: The data definitions are isSingleton : Bool -> Type and mkSingle
: (x : Bool) -> isSingleton x respectively.
In this case, we defined a function lengthMyVect that takes a MyVect and returns a
natural number. The value n in the definition of the function will be the same as the
value of n at the type level. They are called implicit parameters because the caller
of this function needn’t pass these parameters. In the function definition, we define
implicit parameters with curly braces and we also need to specify the list parameter
which is of type MyVect n to pattern match against it. But, note how we don’t refer
to the list parameter in the computation part of this function and instead, we can use
an underscore (which represents an unused parameter) to get to:
4. Programming in Idris 28
We can also have implicit parameters at the type level. As a matter of fact, an
equivalent type definition of that function is:
If we ask Idris to give us the type of this function, we will get the following for either
of the type definitions above:
1 Idris> :t lengthMyVect
2 lengthMyVect : MyVect n -> Nat
However, we can use the command :set showimplicits which will show the
implicits on the type level. If we do that, we will get the following for either of the
type definitions above:
To pass values for implicit arguments, we can use the following syntax:
Exercise 18
Try to evaluate the following code and observe the results:
The function above is a recursive one, and depending on the value of even x it will
branch the recursion. Since pattern matching works against value constructors, and
even x is a function call, we can’t easily pattern match against it. We used even x in
the function body to do the check. Idris provides another additional keyword with
that allows us to pattern match a value of some expression. The keyword with has
the following syntax:
Note how we have to specify new pattern matching clauses after the line that uses
the with keyword. This is so because we won’t have the original pattern match in
context. Given this, an alternative definition of the function above is:
In this function, we defined two new pattern matches after the line that uses the
with keyword. Since we don’t have x and l' in this new pattern matching context,
we have to rewrite them on the left side of the pipe, and on the right side of the
pipe we pattern match against the value of even x, and then branch the recursion
(computation).
1 interface Eq a where
2 (==) : a -> a -> Bool
3 (/=) : a -> a -> Bool
4 -- Minimal complete definition:
5 -- (==) or (/=)
6 x /= y = not (x == y)
7 x == y = not (x /= y)
Note how we can specify comments in the code by using two dashes. Comments are
ignored by the Idris compiler and are only useful to the reader of the code.
The definition says that for a type to implement the Eq interface, there must be
an implementation of the functions == and /= for that specific type. Additionally,
the interface also contains definitions for the functions, but this is optional. Since
the definition of == depends on /= (and vice-versa), it will be sufficient to provide
only one of them in the implementation, and the other one will be automatically
generated.
As an example, let’s assume that we have a data type:
¹⁴They are equivalent to Haskell’s class keyword. Interfaces in Idris are very similar to OOP’s interfaces.
4. Programming in Idris 31
We use == for Nat and String since this is already defined in Idris itself. With this,
we can easily use == and /= on Fooinst:
Nats implement the built-in Num interface, which is what allows us to use 0 and Z
interchangeably.
Exercise 19
Implement your own data type Person that accepts a person’s name and
age, and implement an interface for comparing Persons.
Hint: One valid data type is:
named after the mathematician Haskell Curry and logician William Howard. In other
words, a mathematical proof is represented by a computer program and the formula
that we’re proving is the type of that program. As an example, we can take a look at
the function swap that is defined as follows:
The isomorphism says that this function has an equivalent form of mathematical
proof. Although it may not be immediately obvious, let’s consider the following
proof: Given P ∧ Q, prove that Q ∧ P . In order to prove it, we have to use the inference
rules and-introduction and and-elimination, which are defined as follows:
As we’ve discussed, we can use product types to encode pairs. We can note the
following similarities with our earlier definition of swap:
As long as Idris’ type checker terminates, we can be certain that the program provides
a mathematical proof of its type. This is why Idris’ type checker only evaluates total
functions, to keep the type checking decidable.
4. Programming in Idris 33
Consider the lengthMyVect example from before. If we change its definition from k
to k + k, the type checker will not complain:
Then the type checker will throw an error for k + k, but not for k.
Further reading
Morris, D. W., Morris, J., Proofs and Concepts, 2016
Velleman, J. D., How to Prove It: A Structured Approach, 1994
Megill, N., Metamath, 1997
Halmos, P., Naive Set Theory, 1960
Lipovaca, M., Learn You a Haskell for Great Good, 2011
The Idris Community, Programming in Idris: A Tutorial, 2015
Wadler, P., Propositions as Types, 2014
Pierce, B., Logical Foundations, 2011
Pierce, B., Types and Programming Languages, 2002
Löh, A., McBride C., Swierstra W., A tutorial implementation of a dependently typed
lambda calculus, 2010
Martin-Löf, P., Intuitionistic Type Theory, 1984
Hofstadter, D., Gödel, Escher, Bach, 1979
Sitnikovski, B., Tutorial implementation of Hoare logic in Haskell, 2021
Smullyan, R., The Gödelian Puzzle Book, 2013
About the author
Boro Sitnikovski has over 10 years of experience working professionally as a Software
Engineer. He started programming using the Assembly programming language on an
Intel x86 at the age of 10. While in high school, he won several prizes for competitive
programming, varying from fourth, third, and first place.
He is an Informatics graduate; his Bachelor’s thesis was titled “Programming in
Haskell using algebraic data structures”, and his Master’s thesis was titled “Formal
verification of Instruction Sets in Virtual Machines”. He has also published papers
on software verification. Other research interests include programming languages,
mathematics, logic, algorithms, and writing correct software.
He is a strong believer in the open-source philosophy and contributes to various
open-source projects.
In his spare time, he enjoys time with his family.