Zulip Chat Archive
Stream: new members
Topic: Chaining coercions
Kevin Sullivan (Feb 11 2021 at 02:50):
Hi Folks, A question about chaining of coercions. I start with three types, X, Y, and Z, and define pairwise coercions in both directions: from X to Y and Y to X and from Y to Z and Z to Y. Coercions of X-valued objects to Z then works fine, and from Z to X as well. However, if I now add a "branch" in the coercion graph, say from X to Y' and back, the automatic chaining of coercions (at least from X-Z and Z-X) now fails. What are the constraints on how complex a graph of coercions can be, in which automatic chaining will continue to work, and why? Thanks! --Kevin S
Mario Carneiro (Feb 11 2021 at 02:51):
X to Y and back is bad, that's a typeclass cycle and will make all typeclass inference problems fail
Mario Carneiro (Feb 11 2021 at 02:52):
You can have X to Y and back if you inhabit has_coe_t
instead since it won't try to chain them
Kevin Sullivan (Feb 11 2021 at 02:55):
Mario Carneiro said:
X to Y and back is bad, that's a typeclass cycle and will make all typeclass inference problems fail
I see. Interesting. The use case is that I have a tree of isomorphic types, and I'd like to have automated coercions between any two types in the tree. Any thoughts on how we might obtain such a capability?
Mario Carneiro (Feb 11 2021 at 02:56):
You could have a coe A -> X
and a coe_t X -> A
for all A and some chosen X
Kevin Sullivan (Feb 11 2021 at 02:57):
Mario Carneiro said:
You could have a coe
A -> X
and a coe_tX -> A
for all A and some chosen X
I will let you know how that works out. Thanks a ton as always, Mario.
Mario Carneiro (Feb 11 2021 at 03:00):
structure foo := (out : nat)
structure bar := (out : nat)
structure baz := (out : nat)
instance foo_nat : has_coe foo nat := ⟨foo.out⟩
instance bar_nat : has_coe bar nat := ⟨bar.out⟩
instance baz_nat : has_coe baz nat := ⟨baz.out⟩
instance nat_foo : has_coe_t nat foo := ⟨foo.mk⟩
instance nat_bar : has_coe_t nat bar := ⟨bar.mk⟩
instance nat_baz : has_coe_t nat baz := ⟨baz.mk⟩
variables (a : foo) (b : bar) (c : baz)
#check (a : foo)
#check (a : bar)
#check (a : baz)
#check (b : foo)
#check (b : bar)
#check (b : baz)
#check (c : foo)
#check (c : bar)
#check (c : baz)
Kevin Sullivan (Feb 11 2021 at 03:06):
Basic types for tree of linear spaces .
abbreviation coord := ℕ
mutual inductive space, frame
with space : Type
| base
| der : frame → space
with frame : Type
| std
| new : space → ℕ → frame
-- open space frame
def getf : space → frame
| space.base := frame.std
| (space.der f) := f
def gets : frame → space
| frame.std := space.base
| (frame.new s n) := s
-- get coord of new frame relative to old frame
def getc : frame → coord
| frame.std := 1
| (frame.new f n) := n
-- get old frame on which which frame is built
-- std frame is built on itself
def geto : frame → frame
| frame.std := frame.std
| (frame.new s n) := getf s
inductive point (s : space) : Type
| mk : ℕ → point
def getps : ∀ {s : space}, point s → space
| s p := s
def getpf : ∀ {s : space}, point s → frame
| s p := getf s
A simple demo/test case.
import .space_tree
--
def s0 : space := space.base -- implicit std frame
def f0 := getf s0 -- standard frame
def f1 := frame.new s0 2 -- new basis coord 2
def s1 := space.der f1
def f2 := frame.new s1 3 -- new basis coord 3
def s2 := space.der f2
def f1' := frame.new s0 5
def s1' := space.der f1'
-- forward and backward transforms
def pt_0_1 : point s0 → point s1
| (point.mk n) :=
let f := (getf s1) in
let c := (getc f) in
(point.mk (n/c))
def pt_1_0 : point s1 → point s0
| (point.mk n) :=
let f := (getf s1) in
let c := (getc f) in
(point.mk (n*c))
--
def pt_0_1' : point s0 → point s1'
| (point.mk n) :=
let f := (getf s1) in
let c := (getc f) in
(point.mk (n/c))
def pt_1'_0 : point s1' → point s0
| (point.mk n) :=
let f := (getf s1) in
let c := (getc f) in
(point.mk (n*c))
--
def pt_1_2 : point s1 → point s2
| (point.mk n) :=
let f := (getf s2) in
let c := (getc f) in
(point.mk (n/c))
def pt_2_1 : point s2 → point s1
| (point.mk n) :=
let f := (getf s2) in
let c := (getc f) in
(point.mk (n*c))
--
instance pt_1_0_coe :
has_coe (point s1) (point s0) :=
⟨pt_1_0⟩
instance pt_0_1_coe :
has_coe_t (point s0) (point s1) :=
⟨pt_0_1⟩
--
instance pt_1'_0_coe :
has_coe (point s1') (point s0) :=
⟨pt_1'_0 ⟩
instance pt_0_1'_coe :
has_coe_t (point s0) (point s1') :=
⟨pt_0_1' ⟩
--
instance pt_2_1_coe :
has_coe (point s2) (point s1) :=
⟨pt_2_1⟩
instance pt_1_2_coe :
has_coe_t (point s1) (point s2) :=
⟨pt_1_2⟩
--
/-
Hmm: Need an explicit has_coe_t from
point s0 to point s2.
-/
--
def p0 : point s0 := point.mk 12
def p2 : point s2 := point.mk 2
def p01 : point s1 := p0 -- coerce
def p12 : point s2 := p01 -- coerce
def p02 : point s2 := p0 -- coerce chain
#reduce p01 -- expect 6
#reduce p12 -- expect 2
#reduce p02 -- expect 2 (chained)
def p21 : point s1 := p2 -- coerce
def p10 : point s0 := p21 -- coerce
def p20 : point s0 := p2 -- coerce chain
#reduce p21 -- expect 6
#reduce p10 -- expect 12
#reduce p20 -- expect 12 (chained)
Kevin Sullivan (Feb 11 2021 at 03:48):
Mario Carneiro said:
structure foo := (out : nat) structure bar := (out : nat) structure baz := (out : nat) instance foo_nat : has_coe foo nat := ⟨foo.out⟩ instance bar_nat : has_coe bar nat := ⟨bar.out⟩ instance baz_nat : has_coe baz nat := ⟨baz.out⟩ instance nat_foo : has_coe_t nat foo := ⟨foo.mk⟩ instance nat_bar : has_coe_t nat bar := ⟨bar.mk⟩ instance nat_baz : has_coe_t nat baz := ⟨baz.mk⟩ variables (a : foo) (b : bar) (c : baz) #check (a : foo) #check (a : bar) #check (a : baz) #check (b : foo) #check (b : bar) #check (b : baz) #check (c : foo) #check (c : bar) #check (c : baz)
Here's more what I'm looking for. This example subsumes yours and illustrates the problem. I'm not a typeclass inferencing person, but might not an inferencer just stop when it detects a cycle, and move on if necessary? (Yeah, that's why I post in beginners.)
structure foo := (out : nat) structure bar := (out : nat) structure baz := (out : nat) structure bam := (out : nat) /- nat ^ ^ | | v v foo bar ^ ^ | | v v bam baz -/ -- ^ arrows instance foo_nat : has_coe foo nat := ⟨foo.out⟩ instance bam_foo : has_coe bam foo := ⟨λ m, foo.mk m.out⟩ instance bar_nat : has_coe bar nat := ⟨bar.out⟩ instance baz_bar : has_coe baz bar := ⟨λ bz, bar.mk bz.out⟩ -- v arrows instance nat_foo : has_coe_t nat foo := ⟨foo.mk⟩ instance foo_bam : has_coe_t foo bam := ⟨λ f, bam.mk f.out⟩ instance nat_bar : has_coe_t nat bar := ⟨bar.mk⟩ instance bar_baz : has_coe_t bar baz := ⟨λ r, baz.mk r.out⟩ -- what works and what breaks? variables (f : foo) (r : bar) (z : baz) (m : bam) #check ((1 : nat) : foo) #check ((1 : nat) : bar) #check ((1 : nat) : bam) -- no: no downward chaining #check ((1 : nat) : baz) -- no: no downward chaining #check (f : nat) #check (m : nat) #check (r : nat) #check (z : nat) #check (m : bar) -- one coe_t at end of chain ok #check (z : foo) -- works symmetrically #check (m : baz) -- no: at most one coe_t at end #check (z : bam) -- no: symmetric /- Looks like I need to write an explicit has_coe_t for each link in the transitive closure of the down-pointing arrows minus the down-pointing arrows already written. Better than nothing but not great. Best we can do? -/
Mario Carneiro (Feb 11 2021 at 03:52):
I'm not a typeclass inferencing person, but might not an inferencer just stop when it detects a cycle, and move on if necessary?
Yes. Lean 3's algorithm is bad, Lean 4's is better and mostly solves this problem
Mario Carneiro (Feb 11 2021 at 03:55):
Like I said, you need a coe from A -> X and coe_t from X -> A for varying A and fixed (privileged) X; this is a star graph of coercions. A general graph isn't going to cut it because you need at most one edge in every path to be a coe_t edge (and in fact the coe_t edge has to be the last in the path)
Kevin Sullivan (Feb 11 2021 at 03:59):
Mario Carneiro said:
I'm not a typeclass inferencing person, but might not an inferencer just stop when it detects a cycle, and move on if necessary?
Yes. Lean 3's algorithm is bad, Lean 4's is better and mostly solves this problem
I see. Do you happen to know where I can read more about the new approach?
Here's a more general form of the problem, I suppose:
Given (statically) a collection, C = { T }, of isomorphic types, and a connected tree (dag, graph, etc), S, of pairwise bijections between types in C, generate coercions for each edge in the complete directed graph over C.
Mario Carneiro (Feb 11 2021 at 04:02):
https://arxiv.org/abs/2001.04301
It's probably not especially useful to you though
Mario Carneiro (Feb 11 2021 at 04:03):
The problem is that typeclass resolution is far more general than your statement
Mario Carneiro (Feb 11 2021 at 04:03):
so we need an algorithm that solves the general case but also does the smart thing in this restricted setting
Mario Carneiro (Feb 11 2021 at 04:04):
The problem as posed is easy; you can use e.g. tarjan's algorithm for finding SCC's in a directed graph
Mario Carneiro (Feb 11 2021 at 04:05):
but it's a challenge to make that work within the constraints of lean 3 coercions
Mario Carneiro (Feb 11 2021 at 04:05):
if you are fine with a tactic doing the coercion you have a lot more flexibility
Kevin Sullivan (Feb 11 2021 at 04:05):
Mario Carneiro said:
https://arxiv.org/abs/2001.04301
It's probably not especially useful to you though
Yes. What I'm wondering, very pragmatically, is whether Lean 4's approach solves my problem?
Mario Carneiro (Feb 11 2021 at 04:06):
I'm not entirely sure, you could try it
Kevin Sullivan (Feb 11 2021 at 04:06):
Also, yes, I could write a tactic. But I'm lazy. And yes I'll give it a try in Lean 4 and report back. Thank you for the conversation. It's clear to me now where things stand with Lean 3. Love this community.
Mario Carneiro (Feb 11 2021 at 04:07):
The problem with a tactic is that it's not invisible
Kevin Sullivan (Feb 11 2021 at 04:07):
Mario Carneiro said:
The problem with a tactic is that it's not invisible
Aye
Mario Carneiro (Feb 11 2021 at 04:07):
coercion does not require any syntax, tactics don't get that luxury
Mario Carneiro (Feb 11 2021 at 04:08):
mathlib has lots of by exactI
in it which ideally you wouldn't have to see at all
Kevin Sullivan (Feb 11 2021 at 04:09):
Mario Carneiro said:
coercion does not require any syntax, tactics don't get that luxury
Very interesting.
Last updated: Dec 20 2023 at 11:08 UTC