Zulip Chat Archive

Stream: new members

Topic: Basic confusion about type inference in exercise


mars0i (Sep 29 2024 at 17:52):

Given this definition:

inductive Vect (α : Type u) : Nat  Type u where
   | nil : Vect α 0
   | cons : α  Vect α n  Vect α (n + 1)
deriving Repr

I should be able to define a reverse function using some version of this algorithm (which works, for example, using List):

def Vect.revHelper (xs : Vect a n) (acc : Vect a m) : Vect a (m + n) :=
  match xs with
  | .nil => acc
  | .cons y ys => revHelper ys (.cons y acc)

def Vect.reverse (xs : Vect a n) : Vect a n :=
    Vect.revHelper xs .nil

def ns :=  Vect.cons 2 (Vect.cons 3 Vect.nil)

The context and error on | .cons y ys => revHelper ys (.cons y acc) is:

 expected type (38:19-38:45)
a : Type u_1
n m : Nat
xs : Vect a n
acc : Vect a m
n : Nat
y : a
ys : Vect a n
 Vect a (m + (n + 1))

 38:29-38:31: error:
application type mismatch
  revHelper ys
argument
  ys
has type
  Vect a n : Type ?u.2269
but is expected to have type
  Vect a (n + 1) : Type ?u.2269

Why does Lean give ys a new length variable n✝ : Nat? Since xs matched on .cons y ys, I would think that Lean would infer that the type of ys is Vect a (n - 1). Why is ys expected to have type Vect a (n✝ + 1) in revHelper ys (.cons y acc)?

Kyle Miller (Sep 29 2024 at 18:00):

Lean is automatically including n in the match discriminants, like this

def Vect.revHelper (xs : Vect a n) (acc : Vect a m) : Vect a (m + n) :=
  match n, xs with
  | 0, .nil => acc
  | n' + 1, .cons y ys => revHelper ys (.cons y acc)

Kyle Miller (Sep 29 2024 at 18:00):

It's necessary because xs depends on n, and the value of n needs to change depending on the constructor.

Kyle Miller (Sep 29 2024 at 18:02):

The error message is a bit confused, since revHelper is using the expected type when elaborating that case.

This shows the issue more clearly:

def Vect.revHelper (xs : Vect a n) (acc : Vect a m) : Vect a (m + n) :=
  match xs with
  | .nil => acc
  | .cons y ys =>
    let res := revHelper ys (.cons y acc)
    res

Kyle Miller (Sep 29 2024 at 18:03):

type mismatch
  res
has type
  Vect a (m + 1 + n) : Type ?u.7554
but is expected to have type
  Vect a (m + (n + 1)) : Type ?u.7554

Kyle Miller (Sep 29 2024 at 18:03):

The problem is that m + 1 + n✝ and m + (n✝ + 1) are not definitionally equal.

Kyle Miller (Sep 29 2024 at 18:05):

It's a classic problem working with dependent types. Here's one possibility for working around the issue:

def Vect.cast {m n : Nat} (h : m = n) (v : Vect α m) : Vect α n :=
  h  v

def Vect.revHelper (xs : Vect a n) (acc : Vect a m) : Vect a (m + n) :=
  match xs with
  | .nil => acc
  | .cons y ys =>
    (revHelper ys (.cons y acc)).cast (by rw [Nat.succ_add]; rfl)

Kyle Miller (Sep 29 2024 at 18:09):

In practice, I think it's much easier working with a subtype instead of a new inductive type.

structure Vect (α : Type u) (n : Nat) where
  elts : List α
  length_eq : elts.length = n

def Vect.reverse (xs : Vect α n) : Vect α n where
  elts := xs.elts.reverse
  length_eq := (List.length_reverse _).trans xs.length_eq

mars0i (Sep 30 2024 at 03:33):

Thank you @Kyle Miller for such a thorough explanation! This is very helpful.

There are a couple of points I'm not sure I understand, but I'll do some investigation--your answer will help me learn some things I wanted to learn about--and come back here if I can't figure it out.

mars0i (Oct 05 2024 at 04:53):

After some study, I figured some things out, but I'm still confused about something. Any insight or tips or readings that anyone can suggest would be very welcome.

The h argument to Vect.cast should be a proposition stating an equality. But instead of passing a proposition we pass the result of two tactics, rw and rfl. I'm confused about this role for by, and about how a rewriting tactic has an effect outside of by. It's likely that quite a bit of what I say here won't make sense--because I am confused. :smile: I kind of understand the simple tactic examples using by that I've studied in FPIL and TPIL, although there are more complex tactic examples that I haven't studied.

rw has to act on something--it has to have something to rewrite--correct? But there is nothing to rewrite until it's passed to cast? I'm confused about the fact that the equality proposition that, I guess, is apparently passed to cast, is generated inside a by context, where there is nothing given to rewrite.

I can see that what the lhs of Nat.succ_add evaluates to would match (m + 1) + n✝ in Vect a (m + 1 + n✝) . Then the rhs of Nat.succ_add, with variable substitutions due to matching , would be (m + n✝) + 1. The latter is definitionally equal to m + (n✝ + 1) because of the way that Nat.add is defined. So at a purely intuitive level, I see that succ_add and rfl provide information that could allow one to substitute m + (n✝ + 1) for (m + 1 + n✝), which is the transformation needed.

What's confusing is that cast expects to be passed an equality, and what is passed to it is (by ...), where the ... consists of a rewrite tactic rw [Nat.succ_add], rewriting by substituting one side of succ_add for the other, followed by an equality proof tactic (rfl). I think I'm confused about about what by and tactics do. Or maybe I'm confused by rw. (I did read the rewrite documentation to which rw refers, but that didn't answer these questions.)

Henrik Böving (Oct 05 2024 at 08:14):

cast expects a proof of some form x = y and you can use a tactic block with by to do that proof, just like any other proof in the Lean world

mars0i (Oct 05 2024 at 18:59):

Thanks @Henrik Böving. That's a very helpful clarification. Once you state it, it seems obvious, but it wasn't, for me.

So a by block is in some sense a function--not in the way that it evaluates its arguments--but but it accepts arguments and returns a proof.

I'm still confused though about what is happening inside of (by rw [Nat.succ_add]; rfl).

Is there some way to see what proof (by ...) is returning? When I try to run #check on a by block by itself, or try to assign the value to a variable, the error messages I get suggest to me that a by block is only meaningful within the usual contexts in which it is used.

mars0i (Oct 05 2024 at 19:05):

I see that I can assign the value of a by block to a local variable. This type checks:

def Vect.revHelper (xs : Vect a n) (acc : Vect a m) : Vect a (m + n) :=
  match xs with
  | .nil => acc
  | .cons y ys =>
     let prf := (by rw [Nat.succ_add]; rfl)
     (revHelper ys (.cons y acc)).cast prf

Or I can use have instead of let. But I don't know how to see the value of prf.

mars0i (Oct 05 2024 at 19:25):

Oh--I see it now. I can see the value of prf in the Infoview window when the cursor is on the last line. (Yayy!) And what's displayed is different with let and have. I'll experiment.

mars0i (Oct 05 2024 at 19:37):

I understand now that have shows prf's type--what cast needs; let shows its value as well. (Lean is amazing.)

Kyle Miller (Oct 06 2024 at 01:47):

mars0i said:

Is there some way to see what proof (by ...) is returning?

(by? ...)

mars0i (Oct 06 2024 at 04:08):

I didn't know about by?. It doesn't seem to do what I wanted, but it does other things that look useful. Thanks @Kyle Miller as well as @Henrik Böving.

Kyle Miller (Oct 06 2024 at 16:02):

Could you say what you wanted? The message printed by by? shows exactly what proof the by returns.

mars0i (Oct 06 2024 at 17:57):

If I use

     have prf := (by rw [Nat.succ_add]; rfl)
     (revHelper ys (.cons y acc)).cast prf

then if I place the cursor on prf I see (among other things)

prf : m + 1 + n = m + (n + 1)

which is the equality needed so that cast can perform the needed substitution.

If I use by? rather than by in your original (extremely helpful) suggestion, this code is

def Vect.revHelper (xs : Vect a n) (acc : Vect a m) : Vect a (m + n) :=
  match xs with
  | .nil => acc
  | .cons y ys =>
    (revHelper ys (.cons y acc)).cast (by? rw [Nat.succ_add]; rfl)

If I place the cursor on by?, the Infoview window shows

a : Type ?u.31378
n m : Nat
xs : Vect a n
acc : Vect a m
n : Nat
y : a
ys : Vect a n
 ?m.31630 = m + (n + 1)

 151:49-151:61: error:
tactic 'rewrite' failed, did not find instance of the pattern in the target expression
  ?n.succ + ?m
a : Type ?u.31378
n m : Nat
xs : Vect a n
acc : Vect a m
n : Nat
y : a
ys : Vect a n
 ?m.31630 = m + (n + 1)

 151:41-151:44: information:
Try this: sorryAx (?m.31630 = m + (n + 1)) true

 suggestions:
sorryAx (?_ = m + (n + 1)) true

The sorryAx lines show ... = m + (n✝ + 1), but the lhs of the expression is just a variable in each case, so I don't learn that (by? ...) is returning an equality proof m + 1 + n✝ = m + (n✝ + 1).

Am I using by? the wrong way, or is there some other place I should put the cursor? Maybe there's a configuration option I need to set. I'm using lean.nvim, so maybe there's something turned on by default in VSCode but not in nvim. I can probably turn it on. Or maybe there's another buffer--not Infoview--that I should be looking at?

Kyle Miller (Oct 06 2024 at 18:01):

The sorryAx means that things are not working correctly with by?. Notice that switching to by? causes the tactic proof to have errors (this is worth reporting).

mars0i (Oct 07 2024 at 00:52):

OK--I see. Done: #5625


Last updated: May 02 2025 at 03:31 UTC