-
Notifications
You must be signed in to change notification settings - Fork 460
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for ML-style or-patterns #371
Comments
Basic support can be implemented as a macro, i.e., a def fib : Nat → Nat
| 0 | 1 => 1
| (n+2) => fib n + fib (n+1) into def fib : Nat → Nat
| 0 => 1
| 1 => 1
| (n+2) => fib n + fib (n+1) If we use macros, then we can also support inductive strOrNum
| S (s : String)
| I (i : Int)
open strOrNum
def ex1 :=
match S "test" with
| I a | S a => toString a The RHS would be elaborated twice. Once in a context where def ex1 :=
match S "test" with
| I a | S a => not a We would get the type mismatch message twice. Regarding pretty-printing, we can accomplish it by adding annotations as we do for inaccessible terms and
If we decide to enforce these rules, then the macro approach is not sufficient, and we would have to modify the
To be able to support this feature, we would have to enforce the rules you sketched above.
I am wondering whether ML languages put additional "bells and whistles" on top of this feature. |
ML languages typically do enforce the same types across variants, double checked with F# and Haskell prior to making a proposal and it's also consistent with my memory. And when I was thinking about it as a macro with double elaboration, and different types, then the IDE problem immediately shows up. Whilst it's technically strictly more expressive as a pure macro expansion, I'm fairly confident it's less useful. Because your proof state would get jumbled or you would have to pick one. At that point it's a more succinct syntax, yes, but you can't use it for proofs naturally. Additionally, I conjecture that case where an RHS is syntactically the same term but with different types doesn't occur much if at all in practise. The only times where the RHS terms would be the same is applications of generic functions and compositions thereof, like your
I wasn't thinking about this feature in terms of a macro precisely because of enforcing some additional rules as well as the IDE. And also I'm thinking of it as a means to make proofs more powerful. One big problem we have if we go down the macro-only route is that we cannot easily fix it with a more elaborate strategy further down the line. That is to say, as we all know, removing features from a language is a very unpopular move. And pattern matching in particular is such a central component of the language that we should take it slow and get it right before releasing. We can add the macro to the documentation as an example for something that can be done using macros but I don't think it's a great candidate to build the language feature. I was thinking of the feature more in terms of downcompilation of the following kind: given the generated matcher with major premise let matcherFunc : S -> T
| A a b => f a b
| B a b => f a b
| C c => g c And given a match expression with or patterns match x with
| A a b | B a b => f a b
| C c => g c translates to: match x with
| A a b => matcherFunc (A a b)
| B a b => matcherFunc (B a b)
| C c => matcherFunc (C c) this way, we get type checking due to the type checking already done in the I am not perfectly sure, if we get the type compatibility (def eq) between the various
Extensions to basic pattern matching include:
this list is taken from F#, similar extensions exist for haskell. Of the aforementioned my guess as for the rough order of usefulness / prevalence is something like:
And finally, if doing an extension like this is something that there's at least interest in, I'd be very happy to spend (even considerable) time on it. I'd probably need a bit of support getting started, though. What I don't want to do, is spend time on it, make some choices that are not compatible with your goals, make a pr and have it be rejected. That's why I'm reaching out ahead-of-time. |
Thanks! Your message is very useful. I agree with the points you made. I have only one technical issue that is not clear to me. I will describe it below.
It makes sense. I am going to create a "roadmap" topic at the lean4-dev channel. It will be useful to decide when it is a good time to add new features.
This is great. My main concern is that I think implementing this feature (as an elaborator) is a tough project for new contributors. It would require a considerable amount of synchronization with us. The current implementation is already very complicated, and as you know, we have recently fixed a few bugs there. It is also incomplete. We are not generating equations nor the elimination principles for writing proofs.
Makes sense.
I think this one is tricky. Lean currently generates an auxiliary function for a sequence of | [a, b, c] => ...
| a::as => ...
| _ => ... we generate a function with type {α : Type u_1} →
(motive : List α → Sort u_2) →
(x : List α) →
((a b c : α) → motive [a, b, c]) →
((a : α) → (as : List α) → motive (a :: as)) →
((x : List α) → motive x)
→ motive x := The function always has 4 different sections. We are using this design in Lean4 because we can isolate the dependent pattern matching compilation from the RHS code. It prevents accidental code explosion, allows us to pretty print For example, if we use the command set_option pp.all true
#print fib we obtain ...
fib.match_1.{1} (fun (x : Nat) => @Nat.below.{1} (fun (x : Nat) => Nat) x → Nat) x
... The I think it would be quite hard to reflect the I agree with you that the macro approach has shortcomings (e.g., IDE integration) and that enforcing the rules you have described is the ideal long-term solution. Regarding the Feasibility/Timing
|
Thanks for the detailed reply. And I agree also regarding the feasibility and timing. In light of that, I think it best to park the issue until the equations and induction principles are complete. Just one question of understanding. When I said to make it useful for proofs, I meant this: let matchFunc
| A a b => f a b
| B a b => f a b
| C c => g c dependent or not gives rise to several lemmas: def lemma1 : ∀ x a b, x = A a b → matchFunc x = f a b
def lemma2 : ∀ x a b, (∀ a b, x ≠ A a b) → x = B a b → matchFunc x = f a b
def lemma3 : ∀ x c, (∀ a b, x ≠ A a b) → (∀ a b, x ≠ B a b) → x = C c → matchFunc x = g c this should be provable by a bunch of distinction by cases on the structure of the RHS term. There isn't much clever going on, just raw unfolding. What I was merely proposing was adding a further lemma for the or pattern: def lemma4 : ∀ x a b, (x = A a b) ∨ ((∀ a b, x ≠ A a b) ∧ x = B a b) → matchFunc x = f a b this last one holds purely by propositional reasoning without caring about the RHS at all. Of course this lemma is useless in general, but it allows us to massage the RHS of this lemma, i.e. all RHS's uniformly at once. I was not suggesting that it lowers the number of cases in general, but it still might occasionally be useful and it's cheap to prove if we already have the lemmas 1-3 proven. I hope that makes sense. The kind of case where I see this as useful is when doing an |
There's another feature (perhaps two) present in Coq and OCaml not mentioned here: the ability to put alternatives as subpatterns. For example, I can write match foo with
| bar (baz a | qux a) => ...
end This can be combined with match foo with
| bar (baz a | _ as a) => ...
| qux (succ (succ n | zero as n)) => ...
end |
see #371 This commit does not implement all features discussed in this issue. It has implemented it as a macro expansion. Thus, the following is accepted ```lean inductive StrOrNum where | S (s : String) | I (i : Int) def StrOrNum.asString (x : StrOrNum) := match x with | I a | S a => toString a ``` It may confuse the Lean LSP server. The `a` on `toString` shows the information for the first alternative after expansion (i.e., `a` is an `Int`). After expansion, we have ``` def StrOrNum.asString (x : StrOrNum) := match x with | I a => toString a | S a => toString a ```
After leanprover#137, `length_tail` and `length_drop` live in `Init/Data/List/Lemmas`.
The I think as far as exhaustiveness goes, treating them as not contributing to exhaustivity checks would be fine. I would personally not expect the compiler to be able to analyze arbitrary predicates, and don't write code this way. (Though I've never written substantial OCaml. does OCaml do this?) I think the syntax could look something like this. I would imagine inductive Foo where
| bar (a b c : Nat)
let foo := Foo.bar 1 2 3
match foo with
| .bar a b c if a == b * c => ... -- guard here means this .bar match does not help exhaustiveness
| .bar a b c => ... |
I think that they way Views let you apply a function to a discriminant (or sub-discriminant) and match pattern on the result of that function. {-# LANGUAGE ViewPatterns #-}
data Ints = Ints Int Int
add :: Ints -> Int
add (Ints a b) = a + b
foo :: Ints -> Bool
foo (add -> 5) = True
foo _ = False
-- |
-- >>> foo (Ints 2 3)
-- True
-- >>> foo (Ints 4 1)
-- True
-- >>> foo (Ints 2 2)
-- False I don't think it is currently possible in lean to match on a sum of numbers in a pair like this. Of course, in this particular example I could just matched on a sum, views become more useful when combined with pattern synonyms. Also and-patterns and guards could be potentially implemented using views. And-patterns could be implemented with a view And the other hand, views can be implemented using guards by combining them with |
Currently Lean's pattern matching doesn't have ML-style or-patterns leading to somewhat repetitive of this kind:
You'll notice that the RHS of the first two cases are exactly identical. I propose, we support ML-style or-patterns to allow more compact code like this:
The following is a proposed spec for the feature:
Currently, the Lean parser always expects a
=>
after the LHS of a pattern match. As such there would be no ambiguity adding or-patterns by leaving out the RHS for certain patterns.I propose, we allow either the
=>
or a further pattern after a LHS has been completed, i.e. the correct number of arguments has been matched.Several patterns are compatible if the following rules hold:
This will allow us to write the following:
on the other hand the following is forbidden:
It is forbidden, because the types of the variables
a
are not definitionally equal. On the other hand the following is allowed:Semantically, the left out pattern takes on exactly the term that's the RHS of the last merged pattern:
is the same as:
For pretty-printing purposes, I propose sticking to whatever was written in the source code, i.e. merge the patterns that are merged in source code and keep the ones separate that were separate.
For proof and computational purposes, the two syntaxes should be interchangeable:
as such the aforementioned equality should ideally hold definitionally. This could be achieved by the former reducing to the latter.
For equation lemmas that we are going to have anyway for the match expressions, the or-patterns can have additional lemmas:
Whilst the normal match expression has lemmas for each case asserting the previous cases weren't taken. The or-patterns have theses premises combined in a disjunction.
This new equation lemma is potentially useful in proofs where one might want to treat the RHS uniformly.
I feel fairly strongly about this one, since it may well allow us to do a bunch of operations on an RHS without having to do a
cases
followed by applying the same exact chain of tactics many times. Consider this example:if we have some kind of lemma that is useful for massaging the RHS, we can uniformly rewrite the RHS for all the cases in one go and only use the premises later in the proof. At which point, we can possibly weaken the premises and unify them into fewer cases that we have to do actual work about.
Feel free to poke holes at the initial spec, I'm happy to do more homework and extend it as needed.
The text was updated successfully, but these errors were encountered: