Zulip Chat Archive
Stream: lean4
Topic: Confusion about coercions
Ioannis Konstantoulas (Jul 12 2023 at 09:14):
In the Functional Programming book, we encounter a coercion from NonEmptyList to List:
instance : Coe (NonEmptyList α) (List α) where
coe
| { head := x, tail := xs } => x :: xs
and a claim that 'This allows non-empty lists to be used with the entire List API.' I am not sure what this entails; for instance, I am trying to append two NonEmptyLists and it fails:
def myNeList : NonEmptyList String := ["hello", "my"]
def mySnList : NonEmptyList String := List.append myNeList ["world"]
results in
type mismatch
List.append ?m.1468 ["world"]
has type
List String : Type
but is expected to have type
NonEmptyList String : Type
What am I doing wrong here? Ultimately, I just want to be able to do
def concList : NonEmptyList String := myNeList ++ mySnList
Ruben Van de Velde (Jul 12 2023 at 09:24):
Well, you're successfully calling List.append and this creates a new List. At this point, lean says "hold on, is that new List really nonempty?" - and this is indeed something you need to prove
Kyle Miller (Jul 12 2023 at 09:27):
Yeah, it means you can use NonEmptyList wherever a List is expected, but it doesn't mean you can use a List wherever a NonEmptyList is expected.
Arthur Adjedj (Jul 12 2023 at 09:29):
Coercions allow you to use NonEmptyLists where a List would be expected, but it only goes one way. For example, this definition doesn't give an error:
def myNeList : List String := ({head := "hello", tail := ["my"]} : NonEmptyList String)
If you wanted to do things the other way around, you would need to have a coercion from List to NonEmptyList. This, however, is not possible since you'd also need to coerce empty lists into a nonempty one. If you want to go deeper, there is a way to have some form of such coercion using a dependent coe, called CoeDep:
instance : CoeDep (List α) (x::l) (NonEmptyList α) := ⟨{head := x, tail := l}⟩
Using this, any list that has a term on the head will be able to coerce to a NonEmptyList. This makes your first definition myNeList work. However, it's not so obvious to Lean that your second list mySnList has such a shape, and as such, you'll still get an error on it.
Kyle Miller (Jul 12 2023 at 09:33):
You could define your own constructor that tries to prove nonemptiness automatically using simp:
structure NonEmptyList (α : Type) : Type where
head : α
tail : List α
instance : Coe (NonEmptyList α) (List α) where
coe
| { head := x, tail := xs } => x :: xs
def NonEmptyList.ofList (xs : List α) (nonempty : xs ≠ [] := by simp [*]) : NonEmptyList α where
head := xs.head nonempty
tail := xs.tail
@[simp]
def NonEmptyList.coe_ofList (xs : List α) (nonempty: xs ≠ []) :
(NonEmptyList.ofList xs : List α) = xs := by
cases xs
· simp at nonempty
· simp [ofList]
def myNeList : NonEmptyList String := .ofList ["hello", "my"]
def mySnList : NonEmptyList String := .ofList <| List.append myNeList ["world"]
def myNeListOfNeList (neList : NonEmptyList α) : NonEmptyList α := .ofList neList
Ioannis Konstantoulas (Jul 12 2023 at 11:25):
My problem with all this is that the Functional Programming book is using the above code in the Applicative section:
inductive Validate (ε α : Type) : Type where
| ok : α → Validate ε α
| errors : NonEmptyList ε → Validate ε α
instance : Coe (NonEmptyList α) (List α) where
coe
| { head := x, tail := xs } => x :: xs
instance : CoeDep (List α) (x :: xs) (NonEmptyList α) where
coe := { head := x, tail := xs }
instance : Applicative (Validate ε) where
pure := .ok
seq f x :=
match f with
| .ok g => g <$> (x ())
| .errors e =>
match x () with
| .ok _ => .errors e
| .errors e' =>
.errors (e ++ e') -- NonEmptyList coercion fails to concatenate
This code is directly from that book and it fails. I am not sure what I am missing, since the definitions and instances regarding NonEmptyList are scattered throughout the text.
Ioannis Konstantoulas (Jul 12 2023 at 21:03):
I wrote an implementation of HAppend for non-empty lists, but the error in the above code persists :(
Arthur Adjedj (Jul 12 2023 at 21:16):
This works for me:
structure NonEmptyList (α : Type) : Type where
head : α
tail : List α
inductive Validate (ε α : Type) : Type where
| ok : α → Validate ε α
| errors : NonEmptyList ε → Validate ε α
instance : Functor (Validate α):= sorry
instance : Coe (NonEmptyList α) (List α) where
coe
| { head := x, tail := xs } => x :: xs
instance : CoeDep (List α) (x :: xs) (NonEmptyList α) where
coe := { head := x, tail := xs }
instance : Append (NonEmptyList α) where
append := λ ⟨x,a⟩ b => {head := x, tail := a ++ b}
instance : Applicative (Validate ε) where
pure := .ok
seq f x :=
match f with
| .ok g => g <$> (x ())
| .errors e =>
match x () with
| .ok _ => .errors e
| .errors e' =>
.errors (e ++ e')
Arthur Adjedj (Jul 12 2023 at 21:22):
but you're right, it's unfortunate that code coming straigth from the book doesn't work
Arthur Adjedj (Jul 13 2023 at 11:43):
Looking back at this, I'm having a hard time accepting that this shouldn't work:
structure NonEmptyList (α : Type _) : Type _ where
head : α
tail : List α
inductive Validate (ε α : Type _) : Type _ where
| ok : α → Validate ε α
| errors : NonEmptyList ε → Validate ε α
instance : Functor (Validate α):= sorry
instance : Coe (NonEmptyList α) (List α) where
coe
| { head := x, tail := xs } => x :: xs
instance : HAppend (List α) (NonEmptyList α) (NonEmptyList α) where
hAppend := λ a b => match a with
| [] => b
| x::a => {head := x, tail := a ++ b}
instance : Applicative (Validate ε) where
pure := .ok
seq f x :=
match f with
| .ok g => g <$> (x ())
| .errors e =>
match x () with
| .ok _ => .errors e
| .errors e' =>
.errors ((e : List ε) ++ e')
^^
/-has type
List ε : outParam (Type ?u.1962)
but is expected to have type
NonEmptyList ε : Type ?u.1962-/
Looking into the synthInstances, it looks to me like the outParam of HAppend is decided only upon the first argument (of type List ε), and thus decides this is HAppend (List ε) (List ε) (List ε), before trying to coerce e' into a List. I would naively expect the outParam to take all "non-out" parameters into account before being decided upon. Is this an intended behaviour ? Marking foo as a default_instance doesn't solve the issue either.
Sebastian Ullrich (Jul 13 2023 at 12:05):
This kind of ambiguous input regarding order of coercions and heterogeneous operations is fundamentally problematic. The heuristic ++ uses to insert coercions fails in this case. https://github.com/leanprover/lean4/blob/a3ebfe29ea516e45dba97dfd303cfc11cc47ff04/src/Lean/Elab/Extra.lean#L91-L96
Kyle Miller (Jul 13 2023 at 12:27):
I wouldn't suggest to turn off the binop% elaborator here, but in case it's illuminating here's how you do it, and then Lean finds the HAppend instance:
macro_rules | `($x ++ $y) => `(HAppend.hAppend $x $y)
instance : Applicative (Validate ε) where
pure := .ok
seq f x :=
match f with
| .ok g => g <$> (x ())
| .errors e =>
match x () with
| .ok _ => .errors e
| .errors e' =>
.errors ((e : List ε) ++ e')
David Thrane Christiansen (Jul 13 2023 at 12:30):
Ioannis Konstantoulas said:
This code is directly from that book and it fails. I am not sure what I am missing, since the definitions and instances regarding
NonEmptyListare scattered throughout the text.
This is definitely a drawback of the style!
The code examples that the book's build process draws from are available in its repository. The Append instance is here: https://github.com/leanprover/fp-lean/blob/56422d4a279d6a74dcf6dc1f9eaa84227ea81f0c/examples/Examples/Classes.lean#L1024-L1026
All the book declaration and expect info lines can be ignored - they're part of how the code was extracted.
It would be a nice addition to the book to write a program to extract more readable source to all examples, eliminating the ones with errors.
Arthur Adjedj (Jul 13 2023 at 12:40):
David Thrane Christiansen said:
Ioannis Konstantoulas said:
This code is directly from that book and it fails. I am not sure what I am missing, since the definitions and instances regarding
NonEmptyListare scattered throughout the text.This is definitely a drawback of the style!
The code examples that the book's build process draws from are available in its repository. The
Appendinstance is here: https://github.com/leanprover/fp-lean/blob/56422d4a279d6a74dcf6dc1f9eaa84227ea81f0c/examples/Examples/Classes.lean#L1024-L1026All the
book declarationandexpect infolines can be ignored - they're part of how the code was extracted.It would be a nice addition to the book to write a program to extract more readable source to all examples, eliminating the ones with errors.
I believe @Ioannis Konstantoulas may have been lead by the exercise given at chapter 4.5 into believing it would make things work, seeing this message:
Ioannis Konstantoulas said:
I wrote an implementation of
HAppendfor non-empty lists, but the error in the above code persists :(
Perhaps it may help to have one other exercise before the HAppend one to simply prove Append (NonEmptyList α) ? This would be enough to make the code from the book "work"
David Thrane Christiansen (Jul 16 2023 at 08:47):
Good idea! I'm away from the computer for a bit - would you mind making an issue here? https://github.com/leanprover/fp-lean/issues
Last updated: May 02 2025 at 03:31 UTC