Library FreeSpec.Core.Interface
Definition
Definition interface := Type → Type.
Declare Scope interface_scope.
Bind Scope interface_scope with interface.
Given i : interface, a term of type i α identifies a primitive of i
expected to produce a result of type α.
The simpler interface is the empty interface, which provides no primitives
whatsoever.
Another example of general-purpose interface we can define is the STORE s
interface, where s is a type for a state, and STORE s allows for
manipulating a global, mutable variable of type s within an impure
computation.
Inductive STORE (s : Type) : interface :=
| Get : STORE s s
| Put (x : s) : STORE s unit.
Arguments Get {s}.
Arguments Put [s] (x).
According to the definition of STORE s, an impure computation can use two
primitives. The term Get : STORE s s describes a primitive expected to
produce a result of type s, that is the current value of the mutable
variable. Terms of the form Put x : STORE s unit describe a primitive
which does not produce any meaningful result, but is expected to update the
current value of the mutable variable.
The use of the word “expected” to describe the primitive of STORE s is
voluntary. The definition of an interface does not attach any particular
semantics to the primitives it describes. This will come later, and in
fact, one interface may have many legitimate semantics.
Impure computations are likely to use more than one interface, but the
impure monad takes only one argument. We introduce iplus (denoted by
<+> or ⊕) to compose interfaces together. An impure computation
parameterized by i ⊕ j can therefore leverage the primitives of both i
and j.
When defining general-purpose impure computations that we expect to reuse in
different context, we want to leave the interface as a parameter, and rather
express the constraints in terms of interface availability. We tackle this
challenge by means of interface composites.
We encode this mechanics using two type classes: MayProvide, and
Provide.
Polymorphic Interface Composites
- We say an interface composite ix provides a concrete interface i when there exists a function inj_p : ∀ α, i α → ix α.
- Conversely, we can determine if a primitive of an interface composite ix is forwarded to a concrete interface i when there exists a function proj_p : ∀ α, ix α → option (i a).
Class MayProvide (ix i : interface) : Type :=
{ proj_p {α} (e : ix α) : option (i α)
}.
Class Provide (ix i : interface) `{MayProvide ix i} : Type :=
{ inj_p {α} (e : i α) : ix α
; proj_inj_p_equ {α} (e : i α) : proj_p (inj_p e) = Some e
}.
We provide a default instance for MayProvide in the form of a function
proj_p which always return None. We give to this default instance a
ridiculously high priority number to ensure it is selected only if no other
instances are found.
Instance default_MayProvide (i j : interface) : MayProvide i j|1000 :=
{ proj_p := fun _ _ ⇒ None
}.
It is expected that, for an interface composite ix which provides i and
may provide j, inj_p and proj_p do not mix up i and j
primitives. That is, injecting a primitive e of i inside ix, then
projecting the resulting primitive into j returns None as long as i
and j are two different interfaces.
Class Distinguish (ix i j : interface) `{Provide ix i, MayProvide ix j} : Prop :=
{ distinguish : ∀ {α} (e : i α), proj_p (i := j) (inj_p (ix := ix) e) = None
}.
Manipulating MayProvide, Provide and Distinguish manually quickly
becomes verbose, especially due to the combinatorial nature of
Distinguish. In an attempt to make things easier for FreeSpec users, we
provide ProvideN and StrictProvideN (with N from 2 to
5). ProvideN can be used to define impure computations which use N
interfaces, while StrictProvideN can be used to reason about said
computations (StrictProvideN requires the N interfaces to be different,
thanks to the Distinguish type class).
Class Provide2 ix i1 i2 `{Provide ix i1, Provide ix i2}.
#[program]
Instance Make_Provide2 `(Provide ix i1, Provide ix i2)
: Provide2 ix i1 i2.
Class Provide3 ix i1 i2 i3 `{Provide ix i1, Provide ix i2, Provide ix i3}.
#[program]
Instance Make_Provide3 `(Provide ix i1, Provide ix i2, Provide ix i3)
: Provide3 ix i1 i2 i3.
Class Provide4 ix i1 i2 i3 i4
`{Provide ix i1, Provide ix i2, Provide ix i3, Provide ix i4}.
#[program]
Instance Make_Provide4 `{Provide ix i1, Provide ix i2, Provide ix i3, Provide ix i4}
: Provide4 ix i1 i2 i3 i4.
Class Provide5 ix i1 i2 i3 i4 i5
`{Provide ix i1, Provide ix i2, Provide ix i3, Provide ix i4, Provide ix i5}.
#[program]
Instance Make_Provide5
`{Provide ix i1 , Provide ix i2, Provide ix i3, Provide ix i4, Provide ix i5}
: Provide5 ix i1 i2 i3 i4 i5.
Class StrictProvide2 ix i1 i2
`{Provide ix i1, Provide ix i2, ! Distinguish ix i1 i2, ! Distinguish ix i2 i1}.
#[program]
Instance Make_StrictProvide2
`(Provide ix i1, Provide ix i2, ! Distinguish ix i1 i2, ! Distinguish ix i2 i1)
: StrictProvide2 ix i1 i2.
Class StrictProvide3 (ix i1 i2 i3 : interface)
`{Provide ix i1, Provide ix i2, Provide ix i3, ! Distinguish ix i1 i2,
! Distinguish ix i1 i3, ! Distinguish ix i2 i1, ! Distinguish ix i2 i3,
! Distinguish ix i3 i1, ! Distinguish ix i3 i2}.
#[program]
Instance Make_StrictProvide3
`(Provide ix i1, Provide ix i2, Provide ix i3, ! Distinguish ix i1 i2,
! Distinguish ix i1 i3, ! Distinguish ix i2 i1, ! Distinguish ix i2 i3,
! Distinguish ix i3 i1, ! Distinguish ix i3 i2)
: StrictProvide3 ix i1 i2 i3.
Class StrictProvide4 (ix i1 i2 i3 i4 : interface)
`{Provide ix i1, Provide ix i2, Provide ix i3, Provide ix i4,
! Distinguish ix i1 i2, ! Distinguish ix i1 i3, ! Distinguish ix i1 i4,
! Distinguish ix i2 i1, ! Distinguish ix i2 i3, ! Distinguish ix i2 i4,
! Distinguish ix i3 i1, ! Distinguish ix i3 i2, ! Distinguish ix i3 i4,
! Distinguish ix i4 i1, ! Distinguish ix i4 i2, ! Distinguish ix i4 i3}.
#[program]
Instance Make_StrictProvide4
`(Provide ix i1, Provide ix i2, Provide ix i3, Provide ix i4,
! Distinguish ix i1 i2, ! Distinguish ix i1 i3, ! Distinguish ix i1 i4,
! Distinguish ix i2 i1, ! Distinguish ix i2 i3, ! Distinguish ix i2 i4,
! Distinguish ix i3 i1, ! Distinguish ix i3 i2, ! Distinguish ix i3 i4,
! Distinguish ix i4 i1, ! Distinguish ix i4 i2, ! Distinguish ix i4 i3)
: StrictProvide4 ix i1 i2 i3 i4.
Class StrictProvide5 (ix i1 i2 i3 i4 i5 : interface)
`{Provide ix i1, Provide ix i2, Provide ix i3, Provide ix i4, Provide ix i5,
! Distinguish ix i1 i2, ! Distinguish ix i1 i3, ! Distinguish ix i1 i4,
! Distinguish ix i1 i5, ! Distinguish ix i2 i1, ! Distinguish ix i2 i3,
! Distinguish ix i2 i4, ! Distinguish ix i2 i5, ! Distinguish ix i3 i1,
! Distinguish ix i3 i2, ! Distinguish ix i3 i4, ! Distinguish ix i3 i5,
! Distinguish ix i4 i1, ! Distinguish ix i4 i2, ! Distinguish ix i4 i3,
! Distinguish ix i4 i5, ! Distinguish ix i5 i1, ! Distinguish ix i5 i2,
! Distinguish ix i5 i3, ! Distinguish ix i5 i4}.
#[program]
Instance Make_StrictProvide5 (ix i1 i2 i3 i4 i5 : interface)
`(Provide ix i1, Provide ix i2, Provide ix i3, Provide ix i4, Provide ix i5,
! Distinguish ix i1 i2, ! Distinguish ix i1 i3, ! Distinguish ix i1 i4,
! Distinguish ix i1 i5, ! Distinguish ix i2 i1, ! Distinguish ix i2 i3,
! Distinguish ix i2 i4, ! Distinguish ix i2 i5, ! Distinguish ix i3 i1,
! Distinguish ix i3 i2, ! Distinguish ix i3 i4, ! Distinguish ix i3 i5,
! Distinguish ix i4 i1, ! Distinguish ix i4 i2, ! Distinguish ix i4 i3,
! Distinguish ix i4 i5, ! Distinguish ix i5 i1, ! Distinguish ix i5 i2,
! Distinguish ix i5 i3, ! Distinguish ix i5 i4)
: StrictProvide5 ix i1 i2 i3 i4 i5.
Composing Interfaces
Inductive iplus (i j : interface) (α : Type) :=
| in_left (e : i α) : iplus i j α
| in_right (e : j α) : iplus i j α.
Arguments in_left [i j α] (e).
Arguments in_right [i j α] (e).
Register iplus as freespec.core.iplus.type.
Register in_left as freespec.core.iplus.in_left.
Register in_right as freespec.core.iplus.in_right.
Infix "+" := iplus : interface_scope.
For iplus to be used seamlessly as a concrete interface composite, we
provide the necessary instances for the MayProvide, Provide and
Distinguish type classes. Note that these instances always prefer the
left operand of iplus. For instance, considering a situation where
there is an instance for Provide ix i and an instance for Provide jx i,
the instance of Provide (ix + jx) i will rely on ix.
The main use case for iplus is to locally provide an additional
interface. For instance, we can consider a with_state function which would
locally give access to the STORE interface, that is with_state : ∀
ix s α, s → impure (ix + STORE s) α → impure ix α. In such a case, the
interface made locally available shall be the right operand of iplus. This
way, functions such as with_state are reentrant. If we take an example,
the following impure computation:
will return false (that is, the variable in the inner store).
with_state true (with_state false get)
Instance refl_MayProvide (i : interface) : MayProvide i i :=
{ proj_p := fun _ e ⇒ Some e
}.
#[program]
Instance refl_Provide (i : interface) : @Provide i i (refl_MayProvide i) :=
{ inj_p := fun (a : Type) (e : i a) ⇒ e
}.
Instance iplus_left_MayProvide (ix i j : interface) `{MayProvide ix i}
: MayProvide (ix + j) i :=
{ proj_p := fun _ e ⇒
match e with
| in_left e ⇒ proj_p e
| _ ⇒ None
end
}.
#[program]
Instance iplus_left_Provide (ix i j : interface) `{Provide ix i}
: @Provide (ix + j) i (iplus_left_MayProvide ix i j) :=
{ inj_p := fun (a : Type) (e : i a) ⇒ in_left (inj_p e)
}.
Next Obligation.
now rewrite proj_inj_p_equ.
Qed.
Instance iplus_right_MayProvide (i jx j : interface) `{MayProvide jx j}
: MayProvide (i + jx) j :=
{ proj_p := fun _ e ⇒
match e with
| in_right e ⇒ proj_p e
| _ ⇒ None
end
}.
#[program]
Instance iplus_right_Provide (i jx j : interface) `{Provide jx j}
: @Provide (i + jx) j (iplus_right_MayProvide i jx j) :=
{ inj_p := fun _ e ⇒ in_right (inj_p e)
}.
Next Obligation.
now rewrite proj_inj_p_equ.
Qed.
By default, Coq's inference algorithm for type classe instances inference is
a depth-first search. This is not without consequence in our case. For
instance, if we consider the search of an instance for MayProvide (i + j)
j, Coq will first try iplus_right_MayProvide (as explained previously),
meaning he now search for MayProvide i j. It turns out such an instance
exists: default_MayProvide.
To circumvent this issue, we write a dedicated tactic find_may_provide
which attempts to find an instance for MayProvide (?ix + ?jx) ?i with
refl_MayProvide, iplus_left_MayProvide and iplus_right_MayProvide.
Ltac find_may_provide :=
eapply refl_MayProvide +
(eapply iplus_left_MayProvide; find_may_provide) +
(eapply iplus_right_MayProvide; find_may_provide).
Hint Extern 1 (MayProvide (iplus _ _) _) ⇒ find_may_provide : typeclass_instances.
#[program]
Instance refl_Distinguish (i j : interface)
: @Distinguish i i j (@refl_MayProvide i) (@refl_Provide i) (@default_MayProvide i j).
#[program]
Instance iplus_left_default_Distinguish (ix jx i j : interface)
`{M1 : MayProvide ix i} `{P1 : @Provide ix i M1}
: @Distinguish (ix + jx) i j
(@iplus_left_MayProvide ix i jx M1)
(@iplus_left_Provide ix i jx M1 P1)
(@default_MayProvide _ j).
#[program]
Instance iplus_right_default_Distinguish (ix jx i j : interface)
`{M1 : MayProvide jx i} `{P1 : @Provide jx i M1}
: @Distinguish (ix + jx) i j
(@iplus_right_MayProvide ix jx i M1)
(@iplus_right_Provide ix jx i M1 P1)
(@default_MayProvide _ j).
#[program]
Instance iplus_left_may_right_Distinguish (ix jx i j : interface)
`{M1 : MayProvide ix i} `{P1 : @Provide ix i M1} `{M2 : MayProvide jx j}
: @Distinguish (ix + jx) i j
(@iplus_left_MayProvide ix i jx M1)
(@iplus_left_Provide ix i jx M1 P1)
(@iplus_right_MayProvide ix jx j M2).
#[program]
Instance iplus_right_may_left_Distinguish (ix jx i j : interface)
`{M1 : MayProvide jx i} `{P1 : @Provide jx i M1} `{M2 : MayProvide ix j}
: @Distinguish (ix + jx) i j
(@iplus_right_MayProvide ix jx i M1)
(@iplus_right_Provide ix jx i M1 P1)
(@iplus_left_MayProvide ix j jx M2).
#[program]
Instance iplus_left_distinguish_left_Distinguish (ix jx i j : interface)
`{M1 : MayProvide ix i} `{P1 : @Provide ix i M1} `{M2 : MayProvide ix j}
`{@Distinguish ix i j M1 P1 M2}
: @Distinguish (ix + jx) i j
(@iplus_left_MayProvide ix i jx M1)
(@iplus_left_Provide ix i jx M1 P1)
(@iplus_left_MayProvide ix j jx M2).
Next Obligation.
apply distinguish.
Defined.
#[program]
Instance iplus_right_distinguish_right_Distinguish (ix jx i j : interface)
`{M1 : MayProvide jx i} `{P1 : @Provide jx i M1} `{M2 : MayProvide jx j}
`{@Distinguish jx i j M1 P1 M2}
: @Distinguish (ix + jx) i j
(@iplus_right_MayProvide ix jx i M1)
(@iplus_right_Provide ix jx i M1 P1)
(@iplus_right_MayProvide ix jx j M2).
Next Obligation.
apply distinguish.
Defined.