Using replace in Idris


Rewriting types

This is a fairly technical post on Idris: a dependently-typed functional language. It will likely be of interest only to other Idris learners. I'm working on a longer post on dependent types, Idris, and proof that will (hopefully) be of more general interest.

The use of replace & rewrite in Idris has been the subject of some confusion. I think I've gotten comfortable with replace, at least, and I wanted write it down. Like Martin John Baker (in the first link) "for some reason I don't feel I am going to understand rewrite fully unless I can do it using replace."

If, in the Idris REPL, we say :doc replace, we are told:

Builtin.replace : {0 p : _ -> Type} -> (0 _ : x = y) -> (1 _ : p x) -> p y
  Perform substitution in a term according to some equality.
  Totality: total
  Visibility: public export

So: given a predicate \(p\) on some (un-named) type, and given a proof term for the claim that \(x\) is propositionally equal to \(y\), and given an element of type \(p \enspace x\), then we get an element in \(p \enspace y\). I get the premise: if \(x = y\), then for all predicates \(p\) we have \(p \enspace x = p \enspace y\). What confused me was how to actually make use of this.

Let's take a simple example from Functional Programming in Idris 2: we wish to write the following function:

replaceVect : Vect (n + 0) a -> Vect n a

You see the problem: to the type checker, Vect (n + 0) a and Vect n a are different types. We proceed, using replace:

replaceVect : Vect (n + 0) a -> Vect n a
replaceVect xs = replace {p = {- pred -}} {- proof: x = y -} xs

Nb that this won't type-check; I couldn't even setup holes for this. It's just a skeleton of my intended implementation using comments. The problem as I see it is that the problem is under-determined: we need to pick \(x\), \(y\) and \(p\) such that:

  1. \(p \enspace x = Vect \enspace (n + 0) \enspace a\)
  2. \(p \enspace y = Vect \enspace n \enspace a\)

and such that we can prove to the type-checker that \(x = y\).

If we notice that picking \(p\) to be \(k \rightarrow Vect \enspace k \enspace a\), then \(x\) & \(y\) fall right out: \(x = n + 0\) and \(y = n\). This leaves us with:

replaceVect : Vect (n + 0) a -> Vect n a
replaceVect xs = replace {p = \k => Vect k a} {- proof: n + 0 = n -} xs

Now we need, for any \(n\), a proof that \(n + 0 = n\). OK, well, that would look like a function from \(n:\mathbb{N}\) into the proposition \(n + 0 = n\):

addZeroRight : (n : Nat) -> n + 0 = n

A little clause adding, case splitting and proof search gives us:

addZeroRight : (n : Nat) -> n + 0 = n
addZeroRight 0 = Refl
addZeroRight (S k) = ?addZeroRight_rhs_1

Thinking from the logic side of this, we're writing a proof by induction: we've got the base case (i.e. zero) and now we need the inductive case. Except we're on the computational side, writing a function. The computational analogue of induction is recursion: what if we called ourselves on \(k\)?

addZeroRight : (n : Nat) -> n + 0 = n
addZeroRight 0 = Refl
addZeroRight (S k) = ?addZeroRight_rhs_1 (addZeroRight k)

Idris now tells us what we have & what we need to complete the function:

   k : Nat
------------------------------
addZeroRight_rhs_1 : plus k 0 = k -> S (plus k 0) = S k

This is a job for cong. If, again in the REPL, we say :doc cong, Idris tells us

Prelude.cong : (0 f : (t -> u)) -> (0 _ : a = b) -> f a = f b
  Equality is a congruence.
  Totality: total
  Visibility: public export

So: if we have a proof that \(k + 0 = k\) (which we do: that's just \((addZeroRight \enspace k)\)), then \(cong \enspace S\) will give us \(S \enspace (k + 0) = S \enspace k\)– just what we need:

addZeroRight : (n : Nat) -> n + 0 = n
addZeroRight 0 = Refl
addZeroRight (S k) = cong S (addZeroRight k)

That's it! Now, for any \(n\), \(addToZeroRight \enspace n\) produces a proof term for the proposition \(n + 0 = n\); just what we needed to make replace work:

replaceVect : Vect (n + 0) a -> Vect n a
replaceVect xs = replace {p = \k => Vect k a} (addZeroRight n) xs

So, for now, my mental model of replace is this: compare the type you have to the type you want. They're likely structurally very similar. Construct a predicate \(p\) that expresses that structure– the argument to \(p\) should pick-out the difference between the type you have & the type you want. \(x\) & \(y\) will be the corresponding arguments to \(p\). Then, if you can find yourself a proof that \(x = y\), you're good to go.

10/14/24 18:36