Zulip Chat Archive

Stream: lean4

Topic: how to hack around the lack of coinduction


awalterschulze (Aug 28 2024 at 20:52):

I was wondering if there is a way to hack this type to not require coinduction or unsafe

Here is the type defined in Agda using coinductive:

record Lang (P : ◇.Lang) : Set (suc ) where
  coinductive
  field
    ν : Dec (◇.ν P)
    δ : (a : A)  Lang (◇.δ P a)

Here is my attempt to redefine the type in Lean, using unsafe:

unsafe
inductive Lang {α: Type u} (R: Language.Lang α): Type (u) where
  | mk
   (null: Decidability.Dec (Calculus.null R))
   (derive: (a: α) -> Lang (Calculus.derive R a))
   : Lang R

Without unsafe, I get the following error (kernel) arg #4 of 'Automatic.Lang.mk' contains a non valid occurrence of the datatypes being declared

This is part of a project reprove the following Agda paper in Lean: "Symbolic and Automatic Differentiation of Languages - Conal Elliott"

The full project is here: https://github.com/katydid/symbolic-automatic-derivatives

The problem is also described here with slightly more detail: https://github.com/katydid/symbolic-automatic-derivatives?tab=readme-ov-file#coinduction--termination-checking

awalterschulze (Aug 29 2024 at 07:18):

We have made some progress, but now we run into termination checking issues, see
https://github.com/katydid/symbolic-automatic-derivatives/pull/2

Edward van de Meent (Aug 29 2024 at 08:12):

how should equality on a coinductive behave?

awalterschulze (Aug 29 2024 at 09:41):

I am quite the coinduction noob, so not sure

Edward van de Meent (Aug 29 2024 at 09:43):

that makes two of us, i guess :sweat_smile:

awalterschulze (Aug 29 2024 at 09:57):

So now we can define a "coinductive" type without unsafe

inductive Lang {α: Type u} : Language.Lang α -> Type (u+1) where
  | mk
    (null: Decidability.Dec (Calculus.null R))
    (derive: (a: α) -> Lang (Calculus.derive R a))
    : Lang R

But when we instantiate it, we need to use unsafe again

-- ∅ : Lang ◇.∅
unsafe
def emptyset {α: Type u}: Lang (@Language.emptyset.{u} α) := Lang.mk
  -- ν ∅ = ⊥‽
  (null := Decidability.empty)
  -- δ ∅ a = ∅
  (derive := fun _ => emptyset)

Otherwise we get the error:

fail to show termination for
  Automatic.emptyset
with errors
failed to infer structural recursion:
Not considering parameter α of Automatic.emptyset:
  it is unchanged in the recursive calls
no parameters suitable for structural recursion

well-founded recursion cannot be used, 'Automatic.emptyset' does not take any (non-fixed) arguments

Jannis Limperg (Aug 29 2024 at 10:03):

These termination issues seem to be inherent to the task. The coinductive Lang type is an infinitely branching tree of possibly infinite depth and the emptyset value has infinite depth, so its construction cannot terminate.

Maybe you can hack around this with some sort of fuel-based construction: define the type of Lang trees with depth at most n (this is inductive) and then whatever you want to do with this type can maybe be shown to only need finite depth.

awalterschulze (Aug 29 2024 at 10:09):

The Agda version also tries to get around some other termination issues using a sized type, like so

i : Size
record Lang i (P : ◇.Lang) : Set (suc ) where
   coinductive
   field
     ν : Dec (◇.ν P)
     δ :  {j : Size< i}  (a : A)  Lang j (◇.δ P a)

Note Size and Size< are defined as Agda builtins

{-# BUILTIN SIZE     Size     #-}
{-# BUILTIN SIZELT   Size<_   #-}

Is something like this, what you are referring to?

I have started to make an attempt here: https://github.com/katydid/symbolic-automatic-derivatives/blob/sized_automatic/Sadol/SizedAutomatic.lean

I don't know what I am doing, but this is not working:

inductive Lang {i: Nat} {α: Type u} : Language.Lang α -> Type (u+1) where
  | mk
    (null: Decidability.Dec (Calculus.null R))
    (derive: {j: Nat} -> (a: α) -> @Lang j α (Calculus.derive R a))
    : Lang R

It gives the following error:

(kernel) arg #5 of 'SizedAutomatic.Lang.mk' contains a non valid occurrence of the datatypes being declared

Hopefully I am just doing something stupid with how I am defining the depth and there is an easy fix?

Edward van de Meent (Aug 29 2024 at 11:18):

btw, it might be helpful if you were to post a #mwe . (things like Calculus.null throw errors in a plain project)

awalterschulze (Aug 29 2024 at 11:20):

Is Calculus.null not working for you? What error is it throwing?

Edward van de Meent (Aug 29 2024 at 11:21):

things like unknown identifier 'Calculus.derive'.

Edward van de Meent (Aug 29 2024 at 11:25):

basically, it would be helpful if you could abstract your issue so it would work in the live-lean context. (in case you didn't know about this, when you post a code block on zulip, in the top right of the block there is a an icon linking to an in-browser lean editor)

awalterschulze (Aug 29 2024 at 11:32):

Ah you mean if you do not copy the whole project from github. I think it is better to get the whole project from github (in this case). It is as small and has no dependencies. https://github.com/katydid/symbolic-automatic-derivatives

But here is a mwe for convenience:

def Lang (α : Type u) : Type (u + 1) :=
  List α -> Type u

def null {α: Type u} {β: Type v} (f: List α -> β): β :=
  f []

def derives {α: Type u} {β: Type v} (f: List α -> β) (u: List α): (List α -> β) :=
  λ v => f (u ++ v)

def derive {α: Type u} {β: Type v} (f: List α -> β) (a: α): (List α -> β) :=
  derives f [a]

class inductive Dec (P: Type u): Type u where
  | yes: P -> Dec P
  | no: (P -> PEmpty.{u + 1}) -> Dec P

inductive AutoLang {α: Type u} : Lang α -> Type (u+1) where
  | mk
    (null: Dec (null R))
    (derive: (a: α) -> AutoLang (derive R a))
    : AutoLang R

def dec_empty : Dec PEmpty := Dec.no (by intro; contradiction)

def emptyset : Lang α := fun _ => PEmpty

def auto_emptyset {α: Type u}: AutoLang (@emptyset.{u} α) := AutoLang.mk
  (null := dec_empty)
  (derive := fun _ => auto_emptyset)

This gives the following error:

fail to show termination for
  auto_emptyset
with errors
failed to infer structural recursion:
Not considering parameter α of auto_emptyset:
  it is unchanged in the recursive calls
no parameters suitable for structural recursion

well-founded recursion cannot be used, 'auto_emptyset' does not take any (non-fixed) arguments

Jannis Limperg (Aug 29 2024 at 12:07):

awalterschulze said:

The Agda version also tries to get around some other termination issues using a sized type, like so

i : Size
record Lang i (P : ◇.Lang) : Set (suc ) where
   coinductive
   field
     ν : Dec (◇.ν P)
     δ :  {j : Size< i}  (a : A)  Lang j (◇.δ P a)

Is something like this, what you are referring to?

Sized types are sort of conceptually related to the fuel approach, but are really a different thing. The Agda implementation is also pretty obviously inconsistent. Let me try to get the fuel approach to work...

Jannis Limperg (Aug 29 2024 at 12:15):

Here's a type that encodes AutoLang trees which end at some given depth with a stop constructor:

def Lang (α : Type u) : Type (u + 1) :=
  List α -> Type u

def null {α: Type u} {β: Type v} (f: List α -> β): β :=
  f []

def derives {α: Type u} {β: Type v} (f: List α -> β) (u: List α): (List α -> β) :=
  λ v => f (u ++ v)

def derive {α: Type u} {β: Type v} (f: List α -> β) (a: α): (List α -> β) :=
  derives f [a]

class inductive Dec (P: Type u): Type u where
  | yes: P -> Dec P
  | no: (P -> PEmpty.{u + 1}) -> Dec P

def emptyset (α : Type u) : Lang α := fun _ => PEmpty

inductive AutoLang {α: Type u} : Nat  Lang α -> Type (u+1) where
  | stop : AutoLang 0 L
  | mk
    (null : Dec (null R))
    (derive: (a: α) -> AutoLang i (derive R a)) :
    AutoLang (i + 1) R

def dec_empty : Dec PEmpty := Dec.no (by intro; contradiction)

def auto_emptyset {α: Type u} : (i : Nat)  AutoLang i (emptyset α)
  | 0 => .stop
  | i + 1 => .mk dec_empty fun _ => auto_emptyset i

Maybe you can do something with this.

Daniel Weber (Aug 29 2024 at 12:39):

Do you know about QPF?

Yuri (Aug 29 2024 at 12:46):

Edward van de Meent said:

how should equality on a coinductive behave?

I am worse than a noob about this, but considering that coinductions, to my knowledge, are 'observations' only, is bisimulation the best that can be done? I don't have further insights, but curious about the same.

awalterschulze (Aug 29 2024 at 12:59):

The Agda implementation is also pretty obviously inconsistent

@Jannis Limperg obviously?

awalterschulze (Aug 29 2024 at 13:07):

Yuri de Wit said:

Edward van de Meent said:

how should equality on a coinductive behave?

I am worse than a noob about this, but considering that coinductions, to my knowledge, are 'observations' only, is bisimulation the best that can be done? I don't have further insights, but curious about the same.

Ah yes, I forgot about bisimulation

At a high level a found this paper insightful on bisimulation "Automata and Coinduction (an exercise in coalgebra) - J.J.M.M. Rutten". But that might just be because I love Brzozowski's derivatives too much.

Andrés Goens (Aug 29 2024 at 13:17):

CC @Alex Keizer

Yuri (Aug 29 2024 at 13:40):

awalterschulze said:

Automata and Coinduction (an exercise in coalgebra

From the paper (thanks):

Two states that are related by a bisimulation relation are observationally indistinguishable in the sense that 1. they give rise to the same observations, and 2. performing on both states the same experiment will lead to two new states that are indistinguishable again.

And this:
image.png

Jannis Limperg (Aug 29 2024 at 13:41):

awalterschulze said:

The Agda implementation is also pretty obviously inconsistent

Jannis Limperg obviously?

Here's one issue about it: https://github.com/agda/agda/issues/2820

If you search on the Agda bug tracker for the false label, you'll find a bunch more. By 'obviously inconsistent' I mean that the sized types Agda implements are not just broken because of some bug in the type checker but because the design is wrong. I did my master on this (sort of), but didn't manage to come up with a design that is consistent and also preserves the ergonomics of Agda's design.

Jannis Limperg (Aug 29 2024 at 13:42):

Imo every paper that uses sized types should include a note about this. It's essentially like using type-in-type.

awalterschulze (Aug 29 2024 at 13:46):

Jannis Limperg said:

Here's a type that encodes AutoLang trees which end at some given depth with a stop constructor:

def Lang (α : Type u) : Type (u + 1) :=
  List α -> Type u

def null {α: Type u} {β: Type v} (f: List α -> β): β :=
  f []

def derives {α: Type u} {β: Type v} (f: List α -> β) (u: List α): (List α -> β) :=
  λ v => f (u ++ v)

def derive {α: Type u} {β: Type v} (f: List α -> β) (a: α): (List α -> β) :=
  derives f [a]

class inductive Dec (P: Type u): Type u where
  | yes: P -> Dec P
  | no: (P -> PEmpty.{u + 1}) -> Dec P

def emptyset (α : Type u) : Lang α := fun _ => PEmpty

inductive AutoLang {α: Type u} : Nat  Lang α -> Type (u+1) where
  | stop : AutoLang 0 L
  | mk
    (null : Dec (null R))
    (derive: (a: α) -> AutoLang i (derive R a)) :
    AutoLang (i + 1) R

def dec_empty : Dec PEmpty := Dec.no (by intro; contradiction)

def auto_emptyset {α: Type u} : (i : Nat)  AutoLang i (emptyset α)
  | 0 => .stop
  | i + 1 => .mk dec_empty fun _ => auto_emptyset i

Maybe you can do something with this.

This is almost working, but there might be an issue with binary operators like or

def get_null {_hi: i > 0} (l: AutoLang i R): Dec (null R) :=
  match l with
  | .mk n _ => n

def get_derive {α: Type u} {R: Lang α} (l: AutoLang (i + 1) R) (a: α): AutoLang i (derive R a) :=
  match l with
  | .mk _ d => d a

def lang_or {α: Type u} (P : Lang α) (Q : Lang α) : Lang α :=
  fun w => P w  Q w

def sum {α β: Type u} (a: Dec α) (b: Dec β): Dec (α  β) :=
  match (a, b) with
  | (Dec.no a, Dec.no b) =>
    Dec.no (fun ab =>
      match ab with
      | Sum.inl sa => a sa
      | Sum.inr sb => b sb
    )
  | (Dec.yes a, Dec.no _) =>
    Dec.yes (Sum.inl a)
  | (_, Dec.yes b) =>
    Dec.yes (Sum.inr b)

theorem hi:  (i: Nat), i + 1 > 0 := by
  simp

def auto_or {α: Type u} {P Q: Lang α} (i: Nat) (p: AutoLang i P) (q: AutoLang i Q): AutoLang i (lang_or P Q) :=
  match i with
  | 0 => .stop
  | i' + 1 =>
    .mk
      (null := sum (@get_null _ _ _ (hi i') p) (@get_null _ _ _ (hi i') q))
      (derive := fun (a: α) => auto_or i' (get_derive p a) (get_derive q a))

Here it is only reproduced with one size, but I assume I would actually need two sizes i and j
But I am double checking this now

awalterschulze (Aug 29 2024 at 13:53):

Yeah this becomes an issue with concat, which when it derives it requires two levels. But this is not a small example and would be easier to check out the repo for.

I have made as much progress as I can on the sized_automatic2 branch: https://github.com/katydid/symbolic-automatic-derivatives/tree/sized_automatic2

awalterschulze (Aug 29 2024 at 14:00):

Here's one issue about it: https://github.com/agda/agda/issues/2820

If you search on the Agda bug tracker for the false label, you'll find a bunch more. By 'obviously inconsistent' I mean that the sized types Agda implements are not just broken because of some bug in the type checker but because the design is wrong. I did my master on this (sort of), but didn't manage to come up with a design that is consistent and also preserves the ergonomics of Agda's design.

Thanks this is good to know and I added a note to the readme about this

awalterschulze (Aug 29 2024 at 14:01):

Probably easier to look at files in the pull request review, so I made a broken pull request https://github.com/katydid/symbolic-automatic-derivatives/pull/3/files

Yuri (Aug 29 2024 at 14:02):

Sorry for intruding on your thread @awalterschulze , but I want to leave this here:

Total Functional Programming by D.A. Turner

It places codata in context, which was wonderful for my own superficial understanding.

Alex Keizer (Aug 29 2024 at 14:43):

awalterschulze said:

Jannis Limperg said:

Here's a type that encodes AutoLang trees which end at some given depth with a stop constructor:

def Lang (α : Type u) : Type (u + 1) :=
  List α -> Type u

def null {α: Type u} {β: Type v} (f: List α -> β): β :=
  f []

def derives {α: Type u} {β: Type v} (f: List α -> β) (u: List α): (List α -> β) :=
  λ v => f (u ++ v)

def derive {α: Type u} {β: Type v} (f: List α -> β) (a: α): (List α -> β) :=
  derives f [a]

class inductive Dec (P: Type u): Type u where
  | yes: P -> Dec P
  | no: (P -> PEmpty.{u + 1}) -> Dec P

def emptyset (α : Type u) : Lang α := fun _ => PEmpty

inductive AutoLang {α: Type u} : Nat  Lang α -> Type (u+1) where
  | stop : AutoLang 0 L
  | mk
    (null : Dec (null R))
    (derive: (a: α) -> AutoLang i (derive R a)) :
    AutoLang (i + 1) R

def dec_empty : Dec PEmpty := Dec.no (by intro; contradiction)

def auto_emptyset {α: Type u} : (i : Nat)  AutoLang i (emptyset α)
  | 0 => .stop
  | i + 1 => .mk dec_empty fun _ => auto_emptyset i

Maybe you can do something with this.

This is almost working, but there might be an issue with binary operators like or

def get_null {_hi: i > 0} (l: AutoLang i R): Dec (null R) :=
  match l with
  | .mk n _ => n

def get_derive {α: Type u} {R: Lang α} (l: AutoLang (i + 1) R) (a: α): AutoLang i (derive R a) :=
  match l with
  | .mk _ d => d a

def lang_or {α: Type u} (P : Lang α) (Q : Lang α) : Lang α :=
  fun w => P w  Q w

def sum {α β: Type u} (a: Dec α) (b: Dec β): Dec (α  β) :=
  match (a, b) with
  | (Dec.no a, Dec.no b) =>
    Dec.no (fun ab =>
      match ab with
      | Sum.inl sa => a sa
      | Sum.inr sb => b sb
    )
  | (Dec.yes a, Dec.no _) =>
    Dec.yes (Sum.inl a)
  | (_, Dec.yes b) =>
    Dec.yes (Sum.inr b)

theorem hi:  (i: Nat), i + 1 > 0 := by
  simp

def auto_or {α: Type u} {P Q: Lang α} (i: Nat) (p: AutoLang i P) (q: AutoLang i Q): AutoLang i (lang_or P Q) :=
  match i with
  | 0 => .stop
  | i' + 1 =>
    .mk
      (null := sum (@get_null _ _ _ (hi i') p) (@get_null _ _ _ (hi i') q))
      (derive := fun (a: α) => auto_or i' (get_derive p a) (get_derive q a))

Here it is only reproduced with one size, but I assume I would actually need two sizes i and j
But I am double checking this now

This kind of construction is exactly what QpfTypes generates. The library is a bit rough still, but using it should be easier than doing the whole construction by hand. (And if it's not, please do tell me what you run into, so I can fix that)

awalterschulze (Aug 29 2024 at 15:49):

I look at the library, but I don't know if it will work with this indexed type

Alex Keizer (Aug 29 2024 at 22:35):

Ah, sorry, I missed that your type has an index. The library indeed does not support that yet


Last updated: May 02 2025 at 03:31 UTC