ITrees
The Free monad:
Inductive FFree (E : Type → Type) (R : Type) : Type :=
| Ret (x : R)
| Do {X} (op : E X) (k : X → FFree E R).
CoInductive itree (E : Type → Type) (R : Type) :=
| Ret (r:R)
| Tau (t : itree E R)
| Vis {X:Type} (e: E X) (k : X → itree E R).
| Ret (r:R)
| Tau (t : itree E R)
| Vis {X:Type} (e: E X) (k : X → itree E R).
Represents computations that can:
- Terminate by returning a value of type R
- Perform an internal step of computation, before continuing to compute
- Trigger an event e expecting a certain answer of type X. It then continues to compute, but must be ready to do so no matter what is the environment's answer: it therefore takes a continuation indexed by X
(ASIDE) The "real" Interaction Trees data-structure
Context {E : Type → Type} {R : Type}.
Variant itreeF (itree : Type) :=
| RetF (r : R)
| TauF (t : itree)
| VisF {X : Type} (e : E X) (k : X → itree)
.
Variant itreeF (itree : Type) :=
| RetF (r : R)
| TauF (t : itree)
| VisF {X : Type} (e : E X) (k : X → itree)
.
Note: this is a coinductive definition
CoInductive itree : Type := go
{ _observe : itreeF itree }.
{ _observe : itreeF itree }.
And we introduce some notation to make it look like the previous definition.
We first define the Input/Output events informally described above
Variant IOE : Type → Type :=
| Output (n : nat) : IOE unit
| Input : IOE nat.
| Output (n : nat) : IOE unit
| Input : IOE nat.
Here is a very boring computation that simply returns 1789
Definition aux_armes : itree IOE nat := Ret 1789.
And one that requests a value from the user and doubles it
Definition double : itree IOE nat :=
Vis Input (fun n ⇒ Ret (2 × n)).
Vis Input (fun n ⇒ Ret (2 × n)).
A program that repeatedly does I/O:
CoFixpoint echo : itree IOE void :=
Vis Input (fun n ⇒ Vis (Output n) (fun _ ⇒ echo)).
Vis Input (fun n ⇒ Vis (Output n) (fun _ ⇒ echo)).
CoFixpoint spin_spec : itree IOE void :=
Tau spin_spec.
Tau spin_spec.
spin can have any type at all.
CoFixpoint spin {E R} : itree E R := Tau spin.
Variant Empty : Type → Type :=
| empty : Empty void.
| empty : Empty void.
The environment cannot respond to this event
Definition fail {R} : itree Empty R :=
Vis empty (fun x : void ⇒ match x with end).
Vis empty (fun x : void ⇒ match x with end).
Substitution:
Definition subst {E : Type → Type} {T U : Type} (k : T → itree E U)
: itree E T → itree E U :=
cofix _subst (u : itree E T) : itree E U :=
match u with
| Ret r ⇒ k r
| Tau t ⇒ Tau (_subst t)
| Vis e h ⇒ Vis e (fun x ⇒ _subst (h x))
end.
Monad operations
Definition bind {E : Type → Type} {T U : Type} (u : itree E T) (k : T → itree E U)
: itree E U := subst k u.
: itree E U := subst k u.
An elementary itree computation emits an event:
Definition trigger {E : Type → Type} {A : Type} (e : E A) : itree E A :=
Vis e (fun x ⇒ Ret x).
Vis e (fun x ⇒ Ret x).
Definition iter {E : Type → Type} {R I: Type}
(step : I → itree E (I + R)) : I → itree E R :=
cofix iter_ i :=
lr <- step i;; (* one step of the loop *)
match lr with
| inl l ⇒ Tau (iter_ l) (* got a new acc. => jump back into loop *)
| inr r ⇒ Ret r (* got a final result => terminate *)
end.
(step : I → itree E (I + R)) : I → itree E R :=
cofix iter_ i :=
lr <- step i;; (* one step of the loop *)
match lr with
| inl l ⇒ Tau (iter_ l) (* got a new acc. => jump back into loop *)
| inr r ⇒ Ret r (* got a final result => terminate *)
end.
Note: the backwards jump is guarded by Tau
Define the factorial fact n using the iter combinator.
Example:
Definition fact (n : nat) : itree voidE nat :=
iter (fun '(acc,n) ⇒
match n with
| O ⇒ Ret (inr acc)
| S m ⇒ Ret (inl (n × acc, m))
end) (1,n).
iter (fun '(acc,n) ⇒
match n with
| O ⇒ Ret (inr acc)
| S m ⇒ Ret (inl (n × acc, m))
end) (1,n).
"Running" an itrees computation
Fixpoint burn (n : nat) {E R} (t : itree E R) :=
match n with
| O ⇒ t
| S n ⇒
match observe t with
| RetF r ⇒ Ret r
| VisF e k ⇒ Vis e k
| TauF t' ⇒ burn n t'
end
end.
match n with
| O ⇒ t
| S n ⇒
match observe t with
| RetF r ⇒ Ret r
| VisF e k ⇒ Vis e k
| TauF t' ⇒ burn n t'
end
end.
You can this way test that your factorial computes what it should
Eval compute in burn 10 (fact 6).
==> Ret 720 : itree voidE nat
Imperative loops are particular patterns of recursion: they do not
carry any notion of accumulator. We can capture this pattern with a repeat
combinator.
A useful specialization for Imp Semantics
Definition repeat {E} (step : itree E (unit + unit)) : itree E unit :=
iter (fun _ ⇒ step) tt.
iter (fun _ ⇒ step) tt.