[Agda] Coinductive families
Andreas Abel
andreas.abel at ifi.lmu.de
Mon Oct 4 23:26:07 CEST 2010
Thorsten, I mostly agree with your exposition.
But we also need to distinguish:
1) Internal representation. (sum, products, (co)induction)
2) External syntax. (data and record)
In particular, I do not think that Agda's record syntax is a very good
idea and think that Haskell's syntax is better. In my practical
experience, it is not an uncommon refactoring to turn a "record" into
a data type by adding a field:
data D = C1 { f1 :: A1, ..., fn :: An } -- "record"
later add:
| C2 ...
In Agda you have to rewrite record syntax into data syntax if you want
to do this change.
Also, being a record is not necessary tied to having exactly one
constructor, and you put forward the Vec example, which is an indexed
record.
In my opinion, a record is more extensionally given as a data types
whose complete set of projections is definable. Lists are not records
because their constructors are overlapping (both produce a List A),
but Vec's constructor's are not overlapping (one produces a Vec A
zero, the other a Vec A (suc n), which cannot be confused).
In MiniAgda you can define the Vec record by
data Vec (A : Set) : Nat -> Set
{ vnil : Vec A zero
; vcons : [n : Nat] -> (vhead : A) -> (vtail : Vec A n) -> Vec A
(suc n)
} fields vhead, vtail
which creates the two destructors vhead and vtail.
The big question is: which indexed records give rise to modules?
Encoding indexed records in the way you describe below works of
course, but is it a practical solution?
Cheers,
Andreas
On Oct 4, 2010, at 7:49 PM, Thorsten Altenkirch wrote:
> I think it is a good idea if we distinguish two issues:
>
> 1) The definitions of datatypes by constructors or destructors
> corresponding to labelled sums (data) and labelled products
> (codata)
> 2) The definition of recursive datatypes as either inductive or
> coinductive
> corresponding to initial algebras or terminal coalgebras.
>
> E.g. sometimes it is more natural to represent coinductive datatypes
> in their constructor representations, eg. for colists. Also
> decoupling the two allows a flexible syntax for mixed inductive /
> coinductive datatypes (see Nisse's and main recent papers on this).
>
> In any case, I think it is a good idea to dualize our current way to
> define indexed datatypes by indexed constructors to indexed records
> defined by destructors as Dan and Andreas suggest. To add another
> example, we can define Vec using destructors:
>
> record Vec (A : Set) : ℕ → Set where
> destructor
> hd : ∀ {n} → Vec A (suc n) → A
> tl : ∀ {n} → Vec A (suc n) → Vec A n
>
> Dually to the fact that we can encode indexed datatypes using
> equality and sigma-types, we can can encode these indexed records
> using eqaulity and pi-types, namely
>
> record Vec (A : Set)(n : ℕ) : Set where
> field
> hd : ∀ {m} → n ≡ suc m → A
> tl : ∀ {m} → n ≡ suc m → Vec A m
>
> And we can recover the constructors:
>
> nil : ∀ {A} → Vec A 0
> nil = record {hd = λ () ; tl = λ ()}
>
> cons : ∀ {A n} → A → Vec A n → Vec A (suc n)
> cons {A}{n} a as = record {hd = λ _ → a;
> tl = λ eq → tl' eq}
> where
> tl' : ∀ {m} → suc n ≡ suc m → Vec A m
> tl' refl = as
>
> Cheers,
> Thorsten
>
>
>
> On 4 Oct 2010, at 09:04, Andreas Abel wrote:
>
>> Hi Dan,
>>
>> thanks for your post. It's a pity you were not at the last Agda
>> meeting in Nottingham where Anton Setzer gave a talk on this. We
>> have been discussing coinduction for two years on the Agda meetings
>> now, an the conclusions we reached were quite similar to what you
>> present here.
>>
>> This is my own digest of the discussion. Basically, when you write
>> (in fantasy Agda)
>>
>> codata Stream (A : Set) : Set where
>> cons : (head : A) -> (tail : Stream A) -> Stream A
>>
>> this means that you are defining a corecursive record
>>
>> record Stream (A : Set) : Set where
>> destructor
>> head : Stream A -> A
>> tail : Stream A -> Stream A
>>
>> I am using a keyword "destructor" here instead of "field", because
>> field does not allow you to mention the record you are destructing
>> in the type. But this is important for coinductive families, as
>> you also point out, or, we could also call them "indexed records".
>>
>> It comes natural then to extend pattern matching to destructor
>> patterns, so the constructor is creating an object defined by its
>> observations.
>>
>> cons : .{A : Set} -> A -> Stream A -> Stream A
>> head (cons a as) = a
>> tail (cons a as) = as
>>
>> Of course, cons and the destructors could be created automatically
>> from the above "codata" definition. If you go on and define map you
>> get
>>
>> map : .{A B : Set} -> (A -> B) -> Stream A -> Stream B
>> head (map f s) = f (head s)
>> tail (map f s) = map f (tail s)
>>
>> map does not reduce by itself, only if its head or tail is
>> demanded. Thus, there is no special care needed to prevent looping
>> of the type checker in the presence of corecursion anymore. Also,
>> the subject reduction problem discussed on the Coq and Agda lists
>> (posted by Nicolas Oury) is gone.
>>
>> The above definition of map could be generated from the constructor-
>> based one automatically, I suppose, and should be the internal
>> representation (part of the deal has been realized by Nisse and me
>> already as "record pattern translation").
>>
>> An interesting special case is the Unit type (empty record)
>>
>> codata Unit : Set
>> destructors
>>
>> We get
>>
>> unit : Unit
>> () -- no destructors
>>
>> and this makes, as you observed below, Unit the terminal type
>>
>> terminal : .{A : Set} -> A -> Unit
>> ()
>>
>> (We probably need a better syntax for "no destructors", maybe
>>
>> terminal a = ()
>>
>> to indicate that "terminal a" is of a type which has no destructors.)
>>
>> Then we can go on and have indexed records, as you also observe.
>>
>>> data Fin : ℕ → Set where
>>> mk : ∀{n} → Maybe (Fin n) → Fin (suc n)
>>> empty : ⊥ → Fin 0
>>
>> This could be represented as
>>
>> record Fin : Nat -> Set where
>> destructors
>> fromSuc : .{n : Nat} -> Fin (suc n) -> Maybe (Fin n)
>> fromZero : Fin 0 -> Bot
>>
>> and then we get the constructors
>>
>> mk : .{n : Nat} -> Maybe (Fin n) -> Fin (suc n)
>> fromFin (mk m) = m
>> -- fromZero does not match
>>
>> empty : Bot -> Fin 0
>> empty ()
>>
>> or
>>
>> fromZero (empty a) = a
>>
>> Cheers,
>> Andreas
>>
>> On Oct 4, 2010, at 4:47 AM, Dan Doel wrote:
>>
>>> On Sunday 03 October 2010 6:10:34 pm Nils Anders Danielsson wrote:
>>>> On 2010-10-01 15:09, Iain Lane wrote:
>>>>> record Tailspin : Set where
>>>>>
>>>>> field
>>>>>
>>>>> Kaboom : Tailspin
>>>>>
>>>>> test : Tailspin
>>>>> test = ?
>>>>
>>>> I think the problem here is that you are using a recursive record
>>>> type.
>>>> Such types are not really supported, but for some reason they are
>>>> not
>>>> disallowed, maybe to allow experiments with coinductive records:
>>>>
>>>> record Stream (A : Set) : Set where
>>>> constructor _∷_
>>>> field
>>>> head : A
>>>> tail : ∞ (Stream A)
>>>
>>> I posted this in response to an issue on the bug tracker* (so
>>> sorry if you're
>>> seeing this twice), but there may be some eyes here that it
>>> wouldn't reach
>>> otherwise, and it's related to the above.
>>>
>>> I was thinking not long ago about notations for defining codata.
>>> One nice
>>> special case is when the functor in question is a product. Since the
>>> observation is then T -> FT*GT*..., you can write it like:
>>>
>>> codata Stream A = Head :: A & Tail :: Stream A
>>>
>>> But, I was thinking about a more GADT/inductive family-like
>>> syntax, and the
>>> obvious choice is:
>>>
>>> codata Stream (A : Set) : Set where
>>> head :: Stream A -> A
>>> tail :: Stream A -> Stream A
>>>
>>> where the list specifies the destructors, and the type being
>>> defined is a
>>> product thereof, instead of a sum as in the inductive case (and
>>> this is
>>> probably not surprising; inductives and sums are colimits, and
>>> coinductives
>>> and products are limits). Charity handles type definitions in this
>>> way.
>>>
>>> But, in the ordinary coinductive case, the first argument is
>>> always the same,
>>> so we could think about eliding it, which of course produces the
>>> suggestive:
>>>
>>> codata Stream (A : Set) : Set where
>>> head :: A
>>> tail :: Stream A
>>>
>>> and this looks exactly like records. And, in fact, it doesn't just
>>> look like
>>> records, it *behaves* a lot like (Agda) records in several ways.
>>>
>>> 1) Both records and codata are defined by the observations that
>>> can be
>>> performed on them, rather than how they're constructed, as
>>> inductives are.
>>>
>>> 2) The eta rule for records is very much like proper extensional
>>> equality for
>>> coinductive types: two values are equal if all observations/
>>> projections of the
>>> two are equal. For instance, for the empty record:
>>>
>>> record ⊤ : Set where
>>>
>>> with no projections, as a codata, we'd expect to, for any type not
>>> needing to
>>> support any observations, there to be a canonical injection into
>>> the empty
>>> record:
>>>
>>> ana : ∀{A : Set} → A → ⊤
>>> ana _ = _
>>>
>>> term : ∀{A} → (f : A → ⊤) → f ≡ ana
>>> term f = refl
>>>
>>> And lo and behold, there is. Not that this isn't equally justified
>>> by
>>> induction for non-recursive records (inductive and coinductive
>>> records should
>>> coincide when non-recursive), but the focus on equality of
>>> projections/observations seems like a coinductive take.
>>>
>>> 3) In the bug in question, there was a problem with the
>>> termination checker
>>> not accepting code involving something like:
>>>
>>> foo ... (x , y) = ... (foo ... x) ... (foo ... y) ...
>>>
>>> because the match on (x , y) is translated to projections from the
>>> record.
>>> However, Nils also pointed out that the following code should not
>>> check:
>>>
>>> record R : Set where
>>> constructor c
>>> field
>>> p : R
>>>
>>> foo : R -> R
>>> foo (c r) = foo r
>>>
>>> because it translates into:
>>>
>>> foo : R -> R
>>> foo r = foo (p r)
>>>
>>> which will not normalize (foo r => foo (p r) => foo (p (p r))
>>> => ...). The
>>> solution found was to make 'p r <= r', so that the checker is
>>> aware that the
>>> argument isn't getting *bigger*, but it isn't necessarily getting
>>> smaller,
>>> either (the latter part invalidates foo). However, is this
>>> situation not
>>> rather strange?
>>>
>>> If R were indeed inductive, we'd expect it to be empty. For
>>> instance:
>>>
>>> data R : Set where
>>> c : R -> R
>>>
>>> ¬R : R → ⊥
>>> ¬R (c r) = ¬R r
>>>
>>> but this is exactly the function we've just ruled out, because it
>>> won't
>>> normalize with records. And in the inductive case, p r must be
>>> smaller than r,
>>> but we are treating it as if it isn't.
>>>
>>> However, if records are coinductive, the above makes perfect sense:
>>>
>>> a) p r <= r, because coinductive types admit circular definitions,
>>> and so
>>> projecting out of one may yield something of equal size, but not
>>> greater.
>>> b) foo is obviously not a productive corecursive definition, so
>>> it's natural
>>> to rule it out. There's no longer a strange situation where we
>>> must rule
>>> out an ostensibly inductively justified definition because it
>>> would fail
>>> to normalize.
>>> c) It's okay that we can't prove that R is uninhabited, because it
>>> *is*
>>> inhabited by c (c (c (c ...))).
>>>
>>> 4) I haven't bothered to think this through much yet, but:
>>> coinductives had
>>> problems (and still do in Coq, I think) with subject reduction
>>> when they were
>>> initially conceived of as being built out of constructors. Taking
>>> the view
>>> that values of coinductive type are opaque things with observation
>>> functions
>>> seems important to a well-working theory.
>>>
>>> When constructors and matching thereon were added for records, I
>>> believe there
>>> were similar issues with subject reduction, which is why the
>>> matches are now
>>> translated to projections.
>>>
>>> I suspect these two situations are related.
>>>
>>> So, how about it? Is it possible that Agda has had non-recursive
>>> coinductive
>>> types all along, and that codata would have been better added by
>>> fixing the
>>> way that recursive records work?
>>>
>>> On the topic specified in the subject line, I've been thinking
>>> about what
>>> coinductive families might look like. Inductive families are
>>> initial algebras
>>> in a slice category (which makes them a way of specifying how an
>>> entire family
>>> T i is constructed at once), so a coinductive family should be a
>>> final
>>> coalgebra in the same. I'm not sure I have the chops to specify
>>> what that is,
>>> exactly, *but*, I can speculate wildly.
>>>
>>> We allow the specification of inductive families by allowing
>>> constructors to
>>> target arbitrary values of the index:
>>>
>>> data Fin : ℕ → Set where
>>> zero : ∀{n} → Fin (suc n)
>>> suc : ∀{n} → Fin n → Fin (suc n)
>>>
>>> So, it stands to reason that we should be able to specify
>>> coinductive families
>>> by allowing projections to specify arbitrary starting indices:
>>>
>>> codata CoFin : ℕ → Set where
>>> pred : Fin (suc n) → Maybe (Fin n)
>>> empty : Fin 0 → ⊥
>>>
>>> Of course, where the 'n' comes from in the pred destructor is
>>> something I
>>> haven't figured out yet. :) Perhaps the presence of the n at all
>>> is an
>>> illusion in both cases. Edwin Brady et al. tell us that inductive
>>> families
>>> need not store their indices**. Perhaps this is not merely an
>>> optimization,
>>> but a recognition that quantification over indices is part of a
>>> schema for
>>> defining many constructors, not actually a field of the
>>> constructor, and:
>>>
>>> pred : ∀{n} → Fin (suc n) → Maybe (Fin n)
>>>
>>> is a similarly sensible schema (apologies if the paper actually
>>> makes this
>>> observation; it's been a while since I read it).
>>>
>>> If this definition is a little surprising, note that we could also
>>> write Fin
>>> as:
>>>
>>> data Fin : ℕ → Set where
>>> mk : ∀{n} → Maybe (Fin n) → Fin (suc n)
>>> empty : ⊥ → Fin 0
>>>
>>> which makes it a little clearer that we're dealing with the same
>>> functors
>>> here.
>>>
>>> Thoughts?
>>>
>>> -- Dan
>>>
>>> [*] http://code.google.com/p/agda/issues/detail?id=334
>>>
>>>
>>> [**] http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.3849
>>>
>>> Also, here's a fancier proof than the top one: the anamorphism
>>> into the
>>> product record is unique. Of course, I don't expect eta rules to
>>> extend nicely
>>> into providing extensional equality of recursive records-as-codata.
>>>
>>> module Prod where
>>> record _×_ (A B : Set) : Set where
>>> field
>>> fst : A
>>> snd : B
>>>
>>> open _×_
>>>
>>> ana : ∀{A B C : Set} → (C → A) → (C → B) → C → A × B
>>> ana f g x = record { fst = f x ; snd = g x }
>>>
>>> comm₁ : ∀{A B C} → (f : C → A) (g : C → B) → (fst ∘
>>> ana f g) ≡ f
>>> comm₁ f g = refl
>>>
>>> comm₂ : ∀{A B C} → (f : C → A) (g : C → B) → (snd ∘
>>> ana f g) ≡ g
>>> comm₂ f g = refl
>>>
>>> open ≡-Reasoning
>>>
>>> univ : ∀{A B C : Set} → (f : C → A) (g : C → B)
>>> → (h : C → A × B)
>>> → (fst ∘ h) ≡ f
>>> → (snd ∘ h) ≡ g
>>> → h ≡ ana f g
>>> univ f g h eq₁ eq₂ = begin
>>> (λ x → h x)
>>> ≡⟨ refl ⟩
>>> (λ x → record { fst = (fst ∘ h) x
>>> ; snd = (snd ∘ h) x })
>>> ≡⟨ cong (λ k →
>>> λ x → record { fst = k x
>>> ; snd = (snd ∘ h) x })
>>> eq₁ ⟩
>>> (λ x → record { fst = f x
>>> ; snd = (snd ∘ h) x })
>>> ≡⟨ cong (λ k → λ x → record { fst = f
>>> x
>>> ; snd = k x })
>>> eq₂ ⟩
>>> (λ x → record { fst = f x ; snd = g x })
>>> ≡⟨ refl ⟩
>>> (λ x → ana f g x)
>>> ∎
>>> _______________________________________________
>>> Agda mailing list
>>> Agda at lists.chalmers.se
>>> https://lists.chalmers.se/mailman/listinfo/agda
>>>
>>
>> Andreas Abel <>< Du bist der geliebte Mensch.
>>
>> Theoretical Computer Science, University of Munich
>> Oettingenstr. 67, D-80538 Munich, GERMANY
>>
>> andreas.abel at ifi.lmu.de
>> http://www2.tcs.ifi.lmu.de/~abel/
>>
>> _______________________________________________
>> Agda mailing list
>> Agda at lists.chalmers.se
>> https://lists.chalmers.se/mailman/listinfo/agda
>
>
Andreas Abel <>< Du bist der geliebte Mensch.
Theoretical Computer Science, University of Munich
Oettingenstr. 67, D-80538 Munich, GERMANY
andreas.abel at ifi.lmu.de
http://www2.tcs.ifi.lmu.de/~abel/
More information about the Agda
mailing list