Jean-Louis Giordano

Jean-Louis Giordano

Passionate Developer

Idris is a one of those programming languages that empowers its users with the possibility to express very strong guarantees about the code they write before running it. It does this by using something called “Dependent Typing”, which is a very expressive kind of type system.

One of the guarantees you can get is to have safe arithmetic with natural numbers (0, 1, 2, 3…). The interesting bit with Natural numbers is that they cannot be negative, so the substraction a - b only works if b ≤ a. Idris actually forces you to make sure of that before trying to run your code.

Playing around with Nat

Let’s look at some code:

foo : Nat
foo = 4 - 3

Idris will gladly compile, then you can ask the value of foo at the REPL to evaluate the result:

λΠ> foo
1 : Nat

Now if we try another case where a is smaller than b, then idris will not be happy:

foo : Nat
foo = 2 - 3

- + Errors (1)
 `-- fun.idr line 17 col 8:
     When checking right hand side of foo with expected type
             Nat

     When checking argument smaller to function Prelude.Nat.-:
             Can't find a value of type
                     LTE 3 2

So it fails to compile, and tells you it cannot prove that 3 is smaller than 2.

That’s pretty neat! Now how smart is idris then? Let’s try!

foo : Nat
foo = (2 + 2) - 3

Yup! That works! Neat, so Idris can figure out that 2 + 2 ≥ 3 at compile time. Let’s try something harder: can we abstract over one of the parameters?

foo : Nat -> Nat
foo n = n - 3

- + Errors (1)
 `-- fun.idr line 17 col 10:
     When checking right hand side of foo with expected type
             Nat

     When checking argument smaller to function Prelude.Nat.-:
             Can't find a value of type
                     LTE 3 n

Ok so that makes sense: Idris is not happy because it cannot prove that given any natural number n, n is smaller than 3. (for instance, if n = 2 then that’s not true).

But wait…

foo : Nat -> Nat
foo n = (3 + n) - 3

OMG! Yep that compiles! So Idris manages to figure out that given any natural number n, 3 + n ≥ 3! That’s really cool!

It does that just by itself, without me having to prove anything.

How does that work?

Let’s look a bit closer at the - function:

λΠ> :doc (-)
Prelude.Nat.(-) : (m : Nat) -> (n : Nat) -> {auto smaller : LTE n m} -> Nat

    infixl 8

    The function is Total

The first line is the type of the function, and wow, it’s a bit more complicated than I expected… Let’s look at it closer:

(m : Nat) and (n : Nat) are the two arguments to the functions, so we can read (m : Nat) -> (n : Nat) -> ... as “given any natural number m and any natural number n, …”.

The next part is this big thingy: {auto smaller : LTE n m}. Let’s unpack this a bit.

The curly braces {} in the type means it’s an implicit parameter: it’s not passed explicitely, and should be derivable from context. auto is a keyword that tells Idris to automatically derive that parameter from context. So if we removed all that fluff, we could rewrite the type like this:

Prelude.Nat.(-) : (m : Nat) -> (n : Nat) -> (smaller : LTE n m) -> Nat

Which would read like:

Given a natural number m, a natural number n, and a proof “smaller” that n is less than m, you’ll get a natural number out.

The magic happens when adding the curly-braces and the auto keyword, as this proof can be found by Idris automatically.

OK maybe that’s a bit too much… Perhaps the best way is to try to re-implement all of that ourselves from scratch.

Let’s build numbers from scratch

Let’s first define natural numbers. We can define them recursively, by saying it’s either 0 or the successor of another natural number:

data MyNat : Type where
  MZ : MyNat
  MS : MyNat -> MyNat

So 0 is MZ, 1 is MS MZ, 2 is MS (MS MZ)… That’s actually how natural numbers are implemented in idris (using the digits representation is just sugar).

So we can implement addition like this. First we declare the type of addition:

mplus : MyNat -> MyNat -> MyNat

It says “given a natural number and another natural number I’ll give you another natural number”. From that and from your text editor, you can ask Idris to add a placeholder body for the function definition (in emacs, C-c C-s)

mplus : MyNat -> MyNat -> MyNat
mplus x y = ?mplus_rhs

The part with a question mark is what needs to be filled. If you evaluate that code in idris (C-c C-l), Idris will tell you something like this:

Holes

This buffer displays the unsolved holes from the currently-loaded code. Press
the [P] buttons to solve the holes interactively in the prover.

- + Main.mplus_rhs [P]
 `--               x : MyNat
                   y : MyNat
     ------------------------
      Main.mplus_rhs : MyNat

What this says is that there is a “hole” (mplus_rhs) in your program of type MyNat, and that to fill that hole you have 2 parameters at your disposition (x and y) with a similar type. But this doesn’t allow us to define addition, so we can ask Idris to do a case split on the first argument to see if we can get more things at our disposal.

In emacs you do that by putting your cursor on the first argument and pressing C-c C-c, resulting in this:

mplus : MyNat -> MyNat -> MyNat
mplus MZ y = ?mplus_rhs_1
mplus (MS x) y = ?mplus_rhs_2

Which after you load (C-c C-l) you get:

Holes

This buffer displays the unsolved holes from the currently-loaded code. Press
the [P] buttons to solve the holes interactively in the prover.

- + Main.mplus_rhs_1 [P]
 `--                 y : MyNat
     --------------------------
      Main.mplus_rhs_1 : MyNat

- + Main.mplus_rhs_2 [P]
 `--                 x : MyNat
                     y : MyNat
     --------------------------
      Main.mplus_rhs_2 : MyNat

If we look at the first case, the first argument is zero. In that case we just need to return the second argument (0 + y = y). Since nothing is ambiguous here, we can even ask idris to fill that for us by putting the cursor on ?mplus_rhs_1 and performing a “proof search” (pressing C-c C-a):

mplus : MyNat -> MyNat -> MyNat
mplus MZ y = y
mplus (MS x) y = ?mplus_rhs_2

This went like this: when performing the proof search, Idris looked at the type of the hole, and whatever was available from context that matched that type (in this case, only y was available), and used that to fill the hole!

Pretty neat eh?

Now the second bit if we do the same we get the same result:

mplus : MyNat -> MyNat -> MyNat
mplus MZ y = y
mplus (MS x) y = y

Obviously Idris cannot read our mind. If we ask Idris to fill a hole with a given type, it just provides the simple possible answer for that, in this case y.

Fine, let’s just use our human brains. We can use the property that (1 + x) + y = 1 + (x + y):

mplus : MyNat -> MyNat -> MyNat
mplus MZ y = y
mplus (MS x) y = MS (mplus x y)

We can try it out at the REPL:

λΠ> mplus (MS (MS MZ)) (MS (MS MZ))
MS (MS (MS (MS MZ))) : MyNat

λΠ> mplus (MS (MS MZ)) MZ
MS (MS MZ) : MyNat

OK it hurts the eye a little but we can see that 2 + 2 = 4 and 2 + 0 = 2.

Now let’s move to substraction. First the type:

mminus : MyNat -> MyNat -> MyNat

Then add the body automatically:

mminus : MyNat -> MyNat -> MyNat
mminus x y = ?mminus_rhs

Case split on the second argument:

mminus : MyNat -> MyNat -> MyNat
mminus x MZ = ?mminus_rhs_1
mminus x (MS y) = ?mminus_rhs_2

OK x - 0 = x so we can fill the first case:

mminus : MyNat -> MyNat -> MyNat
mminus x MZ = x
mminus x (MS y) = ?mminus_rhs_2

For the second case we need to split again on x actually:

mminus : MyNat -> MyNat -> MyNat
mminus x MZ = x
mminus MZ (MS y) = ?mminus_rhs_1
mminus (MS x) (MS y) = ?mminus_rhs_3

OK last case we can use the fact that (x + 1) - (y + 1) = x - y:

mminus : MyNat -> MyNat -> MyNat
mminus x MZ = x
mminus MZ (MS y) = ?mminus_rhs_1
mminus (MS x) (MS y) = mminus x y

But the middle case is actually annoying: what is the answer to 0 - x for natural numbers? That’s annoying because natural numbers do not contain negative numbers!

So we need to somehow constrain the input in such a way that this case is impossible, i.e. we need to have a proposition that given x and y, y is smaller than x.

Building math from scratch

How do we do that? Actually similarly to how we defined natural numbers:

  • Any natural number is larger than zero
  • If n ≤ m, then (n + 1) ≤ (m + 1)

In Idris that translates to something like this:

data MyLTE  : (m : MyNat) -> (n : MyNat) -> Type where
  MyLTEZero : MyLTE MZ right
  MyLTESucc : MyLTE left right -> MyLTE (MS left) (MS right)

So if you want to write 0 ≤ 2, you would write:

zeroLessThanTwo : MyLTE MZ (MS (MS MZ))
zeroLessThanTwo = MyLTEZero

If you want to write 2 ≤ 3, you would write:

twoLessThanThree : MyLTE (MS (MS MZ)) (MS (MS (MS MZ)))
twoLessThanThree = (MyLTESucc (MyLTESucc MyLTEZero))

So MyLTE (MS (MS MZ)) (MS (MS (MS MZ))) is the proposition that 2 ≤ 3, and (MyLTESucc (MyLTESucc MyLTEZero)) is the proof that it’s true.

Given this, let’s start over our definition of minus, this time requiring a proof that m ≤ n:

mminus : (m : MyNat) -> (n : MyNat) -> MyLTE n m -> MyNat

Add body:

mminus : (m : MyNat) -> (n : MyNat) -> MyLTE n m -> MyNat
mminus m n x = ?mminus'_rhs

This time we split on the proof (x):

mminus : (m : MyNat) -> (n : MyNat) -> MyLTE n m -> MyNat
mminus m MZ MyLTEZero = ?mminus'_rhs_1
mminus (MS right) (MS left) (MyLTESucc x) = ?mminus'_rhs_2

Cool! This is where the “dependent type” magic happens: case splitting on the proof has an effect on the arguments m and n: if the proof you provide is 0 ≤ m, then clearly n = 0, and if the proof you provide looks like (1 + x) ≤ (1 + y), then clearly m and n are not zero.

So let’s fill in some of the logic:

mminus : (m : MyNat) -> (n : MyNat) -> MyLTE n m -> MyNat
mminus m MZ MyLTEZero = m
mminus (MS right) (MS left) (MyLTESucc x) = mminus right left ?prf

If we look at the hole left for the proof in the recursive part of this function, we see this:

- + Main.prf [P]
 `--     right : MyNat
          left : MyNat
             x : MyLTE left right
     -----------------------------
      Main.prf : MyLTE left right

There’s a hole of type MyLTE left right and a parameter x of type MyLTE left right! Cool, perhaps we can just ask Idris to fill that up for us! (C-c C-a)

mminus : (m : MyNat) -> (n : MyNat) -> MyLTE n m -> MyNat
mminus m MZ MyLTEZero = m
mminus (MS right) (MS left) (MyLTESucc x) = mminus right left x

Neat!! Now let’s try to use that method a bit:

foo : MyNat
foo = mminus (MS (MS (MS MZ))) (MS MZ) ?prf

This is 3 - 1 with a hole for the proof.

The hole looks like this:

- + Main.prf [P]
 `-- MyLTE (MS MZ) (MS (MS (MS MZ)))

OK… So how do we proceed? Let’s just ask Idris to see if it can figure out something (C-c C-a):

foo : MyNat
foo = mminus (MS (MS (MS MZ))) (MS MZ) (MyLTESucc MyLTEZero)

AAAH! That worked! How is that possible? Actually Idris can try to find proof by looking a the constructors available for the type of the hole (in our case, MyLTEZero and MyLTESucc x) and try to build a value using those constructors.

Neat!

Hide the math!

So can we make that process automatic then? Yes if we introduce the proof as an implicit parameter and add the auto keyword! Let’s try:

mminus : (m : MyNat) -> (n : MyNat) -> {auto smaller : MyLTE n m} -> MyNat

Add the body:

mminus : (m : MyNat) -> (n : MyNat) -> {auto smaller : MyLTE n m} -> MyNat
mminus m n = ?mminus_rhs

Wait… Where’s the proof in the body? OK actually with implict parameters, you have to explicitely say you need them:

mminus : (m : MyNat) -> (n : MyNat) -> {auto smaller : MyLTE n m} -> MyNat
mminus {smaller} m n = ?mminus_rhs

Case split on the proof:

mminus : (m : MyNat) -> (n : MyNat) -> {auto smaller : MyLTE n m} -> MyNat
mminus {smaller = MyLTEZero} m MZ = ?mminus_rhs_1
mminus {smaller = (MyLTESucc x)} (MS right) (MS left) = ?mminus_rhs_2

And fill the rest:

mminus : (m : MyNat) -> (n : MyNat) -> {auto smaller : MyLTE n m} -> MyNat
mminus {smaller = MyLTEZero} m MZ = m
mminus {smaller = (MyLTESucc x)} (MS right) (MS left) = mminus right left

And now we can try again our substraction:

foo : MyNat
foo = mminus (MS (MS (MS MZ))) (MS MZ)

Sweet! And if we do something more funky:

foo' : MyNat -> MyNat
foo' n = mminus (mplus (MS MZ) n) (MS MZ)

Yep that still works! Neato. Wait so how about this:

foo'' : MyNat -> MyNat
foo'' n = mminus (mplus n (MS MZ)) (MS MZ)

- + Errors (1)
 `-- fun.idr line 48 col 17:
     When checking right hand side of foo'' with expected type
             MyNat

     When checking argument smaller to function Main.mminus:
             Can't find a value of type
                     MyLTE (MS MZ) (mplus n (MS MZ))

Ah noes! That didn’t work… The reason for that is that Idris has no idea that 3 + n = n + 3, and we’ve defined addition by case splitting on the first argument…

Well, that’s for another time!

Conclusion

I hope this gave you some idea of how powerful and the kind of guarantees that can be expressed in Idris and gave some insights on how things can be made practical (not having to provide or carry proofs when those proofs can be derived from context).

Jean-Louis Giordano

Jean-Louis Giordano

Passionate Developer