Zulip Chat Archive
Stream: lean4
Topic: Constrain function input to one constructor
Yuri de Wit (Mar 13 2022 at 18:01):
Say I have the following inductive type:
inductive Term where
| Var (name : String)
| Dup (nam0 : String) (name1 : String) (expr : Term) (body : Term)
| Let (name : String) (expr : Term) (body : Term)
| Lam (name : String) (body : Term)
| App (func : Term) (argm : Term)
| Ctr (name : String) (args: List Term)
| U32 (numb : UInt64)
| Op2 (oper : Operation) (val0 : Term) (val1 : Term)
I would like to define a function that only accepts Ctr
terms as an input.
I know this can be done with something like:
def flatten : Rule -> Rule
| Ctr name args => sorry
| _ => fatal! ...
But instead of handling this in the function at runtime, I would like to make sure this function is never called with anything other than with a Ctr
. Is there a way to achieve that at compile time?
The first thing that comes to mind is to use a phantom type to discriminate the cases I am interested, but I am wondering if there are other ways.
Yuri de Wit (Mar 13 2022 at 18:02):
I also understand that depending on the application, this may just shift the burden of pattern matching to the client and that may be counter productive.
Yuri de Wit (Mar 13 2022 at 18:10):
In my case, I believe this question is coming from the fact that the left-hand-side of Rule can only be a constructor in:
structure Rule where
lhs : Term
rhs : Term
Maybe lhs
should be a separate type.
Simon Hudon (Mar 13 2022 at 18:15):
A typical piece of advice here is to accept that flatten
can be called on arbitrary expressions and write your theorems with the side condition that the term is a Ctr
.
You can also create a Type just for representing a Ctr
and provide functions Term -> Option Ctr
and Ctr -> Term
. Then you can make flatten
take a Ctr
as a parameter.
Horatiu Cheval (Mar 13 2022 at 18:16):
The function flatten
seems to have type Term -> Term
in this example? In any case, you could define a predicate isCtr
only satisfied by those Term
which are applications of Ctr
, and require a proof of that in the type of flatten
. (Is this what you mean by a phantom type?)
Yuri de Wit (Mar 13 2022 at 18:19):
Horatiu Cheval said:
The function
flatten
seems to have typeTerm -> Term
in this example?
Sorry, I tried to simplify the example and made it incorrect.
The real flatten function takes a List Rule
and needs to group them by LHS Ctr name (note this is a work in progress and does not compile ...)
abbrev RuleGroup := Std.HashMap String (Array Rule)
def groupBy (rules: List Rule) : RuleGroup :=
return rules.foldl groupByName Inhabited.default
where
groupByName : RuleGroup -> Rule -> RuleGroup
| rg, ⟨ (Term.Ctr name args), rhs ⟩ => match rg.find? name with
| none => rg.insert name [rule]
| some rs => rs.push rule
(btw, it turns out List.groupBy has an unexpected type/impl to me, at least)
Yuri de Wit (Mar 13 2022 at 18:26):
Horatiu Cheval said:
(Is this what you mean by a phantom type?)
I mean something along the lines of (does not compile):
inductive IsCtr where
| yes
| no
inductive Term (a : IsCtr) where
...
| Ctr (name : String) (args: List a) : Term IsCtr.yes
So that I could, for instance:
def test : Term IsCtr.yes -> Term
Yuri de Wit (Mar 13 2022 at 18:27):
Simon Hudon said:
A typical piece of advice here is to accept that
flatten
can be called on arbitrary expressions and write your theorems with the side condition that the term is aCtr
.
Could you provide a simple example of how this could be done?
Simon Hudon (Mar 13 2022 at 18:29):
Your definition with panic!
is the definition I was thinking of. For a theorem, I'd need an example of the kind of theorem you're working on
Simon Hudon (Mar 13 2022 at 18:40):
If you're trying to statically know which constructor you're working with, you can also go this way:
inductive Term where
| Var (name : String)
| Lam (name : String) (body : Term)
| App (func : Term) (argm : Term)
| Ctr (name : String) (args: List Term)
inductive TermCase where
| Var
| Lam
| App
| Ctr
open Term
inductive IsTermCase : Term → TermCase → Prop where
| Var : IsTermCase (Var v) TermCase.Var
| Lam : IsTermCase (Lam v t) TermCase.Lam
| App : IsTermCase (App t₀ t₁) TermCase.App
| Ctr : IsTermCase (Ctr c ls) TermCase.Ctr
def SpecificTerm (c : TermCase) := { t // IsTermCase t c }
def Term.check : Term → Option (SpecificTerm c) := sorry
def Term.ofSpecific : SpecificTerm c → Term := Subtype.val
def useVar : SpecificTerm TermCase.Var → Nat := sorry
def specialize (t : Term) (h : IsTermCase t c := by constructor) :
SpecificTerm c :=
⟨t, h⟩
def useVar? : Term → Option Nat
| Var v => some <| useVar (specialize (Var v))
| _ => none
def varName : SpecificTerm TermCase.Var → String
| ⟨ Var s, _ ⟩ => s
useVar?
shows how easy it is (using specialize
) to create a SpecificTerm
when you have local information about which constructor you're faced with. varName
shows you can the equation compiler can dismiss all the impossible cases.
Simon Hudon (Mar 13 2022 at 18:41):
The boilerplate seems easy enough to generate automatically.
Simon Hudon (Mar 13 2022 at 19:03):
The design can also work by replacing IsTermCase
with a function Term -> TermCase
and that can make it easier to derive a Decidable
instance for the corresponding propositions, if that matters
Chris B (Mar 13 2022 at 19:13):
In the past I've used this:
def flatten (r : Rule) (h : ∃ name args, r = Ctr name args := by decide) : Rule := ...
You'll have to write the decidable instance, but with the optional/auto arg by decide, you can usually pretend it's not there. If you use it a lot you can extract the existential hypothesis into a predicate isCtr
or whatever.
Simon Hudon (Mar 13 2022 at 19:25):
The downside of not wrapping that proof into a subtype @Chris B is that now, when writing a theorem about flatten, you have proof term floating around making rewriting more awkward. By moving that proof term to a function that done nothing but wrap the subtype, you can write separate theorems about the wrapping and the flatten
function.
Simon Hudon (Mar 13 2022 at 19:32):
This formulation for h
also seems hard to use by the equation compiler when trying to find unreachable cases
Chris B (Mar 13 2022 at 19:33):
Touche, I agree that the explicit existential is more ad-hoc. My use case has been just "this function should only work for this constructor" without the need for a lot of subsequent proving.
Simon Hudon (Mar 13 2022 at 19:40):
That's fair. I think we can generalize both approaches btw. For generally decidable predicates, we can define decidable subtypes:
variable {T : Type} {p : T → Bool}
def DecSubtype (p : T → Bool) := { t // p t = true }
def Term.caseOf : Term → TermCase
| Var s => TermCase.Var
| App _ _ => TermCase.App
| Lam _ _ => TermCase.Lam
| Ctr _ _ => TermCase.Ctr
def VarTerm :=
DecSubtype (λ t => decide <| Term.caseOf t = TermCase.Var)
def VarOrLamTerm :=
DecSubtype (λ t => decide <| Term.caseOf t = TermCase.Var ∨ Term.caseOf t = TermCase.Lam)
def specialize (t : T) (h : p t = true := by rfl) :
DecSubtype p :=
⟨t, h⟩
def varName : VarTerm → String
| ⟨ Var s, _ ⟩ => s
def boundVarName : VarOrLamTerm → String
| ⟨ Var s, _ ⟩ => s
| ⟨ Lam s _, _ ⟩ => s
Simon Hudon (Mar 13 2022 at 19:44):
(This is a really cool problem btw. Thanks @Yuri de Wit! You've convinced me I want that in my library)
Yuri de Wit (Mar 14 2022 at 00:46):
Simon Hudon said:
(This is a really cool problem btw. Thanks Yuri de Wit! You've convinced me I want that in my library)
I am glad my ignorance was useful! Now I need to digest all this!
Mario Carneiro (Mar 14 2022 at 02:32):
No one mentioned the simplest solution, which is to not accept Term
at all and instead pass arguments (name : String) (args: List Term)
to the function
Mario Carneiro (Mar 14 2022 at 02:33):
it's kind of an #xy solution but it is usually easier to use than the other suggestions
Yuri de Wit (Mar 14 2022 at 04:18):
True, except that the real flatten (my fault, since I did not provided it in its full glory) is def flatten : List Rule -> RuleGroup
(RuleGroup is an HashMap String (Array Rule)
).
So I need to process a list of Rules (structure Rule (lhs: Term) (rhs: Term)
) containing Term's, instead of a single Term.
Yuri de Wit (Mar 14 2022 at 04:21):
Another solution would be to simply create a dedicated type for the left hand side at the expense of some duplication of the Ctr definition, as far as I can see.
Mario Carneiro (Mar 14 2022 at 04:25):
If you are just writing lean 4 code and are not fussed about proofs, you can just use if let
and skip the elements that are not of the required form
Mario Carneiro (Mar 14 2022 at 04:26):
or panic!
if you find them
Last updated: Dec 20 2023 at 11:08 UTC