Skip to content
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

Allow "if let" syntax #6685

Closed
vicuna opened this issue Nov 30, 2014 · 27 comments
Closed

Allow "if let" syntax #6685

vicuna opened this issue Nov 30, 2014 · 27 comments

Comments

@vicuna
Copy link

vicuna commented Nov 30, 2014

Original bug ID: 6685
Reporter: @whitequark
Status: acknowledged (set by @damiendoligez on 2015-01-08T17:26:23Z)
Resolution: open
Priority: normal
Severity: feature
Category: language features
Related to: #6357
Monitored by: mdelahaye @Drup @gasche @ygrek @yallop @hcarty @yakobowski

Bug description

"if let p = e then x" would be equivalent to "match e with p -> x | _ -> ()", and "if let p = e then x else x'" to "match e with p -> x | _ -> x'".

This is inspired by Rust and provided rather significant readability improvement for them: https://github.com/rust-lang/rust/pull/19405/files

I think this feature falls in the same category as "match with exception", syntax-wise. Notably it rearranges the pattern and expression.

I've found that the two match statemenst above is quite common in my code, and "if let" would nicely improve it.

@vicuna
Copy link
Author

vicuna commented Nov 30, 2014

Comment author: @lpw25

For the record "if let" pre-dates Rust, appearing in both Swift and Clojure.

It would also be possible to support the related "while let" feature.

@vicuna
Copy link
Author

vicuna commented Nov 30, 2014

Comment author: @sliquister

A readable version of the diff (because whitespace changes are not shown) can be seen at https://github.com/rust-lang/rust/pull/19405/files?w=1 .

@vicuna
Copy link
Author

vicuna commented Dec 1, 2014

Comment author: @yallop

The idea goes back even further than Clojure. For example, Paul Graham's "On Lisp" (1993) defines a macro if-match which is essentially the same as "if let" (http://www.paulgraham.com/onlisptext.html).

@vicuna
Copy link
Author

vicuna commented Dec 1, 2014

Comment author: @johnwhitington

Why not re-use another keyword to make it read better? How about:

if e as p then x
if e as p then x else x

or

if e when p then x
if e when p then x else x

A more radical way would be to allow = to be used when exactly one side is a pattern.

if e = L (_, Y ) then x
if e = L (
, Y _) then x else x

But this seems like an abuse of equals! I like 'when' above.

@vicuna
Copy link
Author

vicuna commented Dec 1, 2014

Comment author: @yallop

Overloading = would clash with existing syntax. For example,

if Some x = Some y then x

is already meaningful.

@vicuna
Copy link
Author

vicuna commented Dec 1, 2014

Comment author: @johnwhitington

Yallop: Quite. Brain not in gear today.

I do find the syntax in the original patch hard to read, though, so it would be nice to find something better, if others have the same impression. I think it's seeing 'let' without 'in'.

@vicuna
Copy link
Author

vicuna commented Dec 9, 2014

Comment author: @damiendoligez

"if let" seems really strange to me.

What about this:

match e with p then x
match e with p then x else y

@vicuna
Copy link
Author

vicuna commented Dec 9, 2014

Comment author: @whitequark

The then-else form is only confusing with normal match, bringing no improvement at all. The else-less form is marginally better. But I dislike both.

My original idea was that "if" is essentially a modifier that makes "let" refutable. You would write the code similar to what you would usually write with an if, e.g.:

if Some 1 = e then
do_x ()

and refactor it to use a pattern match:

if let Some x = e then
do_x x

There have been many cases where I wished that let was refutable, but disliked the heaviness of match, which is the motivation for this report.

@vicuna
Copy link
Author

vicuna commented Dec 9, 2014

Comment author: @lpw25

"if let" seems really strange to me.

Whilst it is a little strange, it does have the benefit of existing in a few other languages, so people are more likely to have seen it before.

@vicuna
Copy link
Author

vicuna commented Dec 9, 2014

Comment author: @yallop

It does seem preferable to extend match rather than let here. Currently let is more typically used for destructuring products and match is more typically used for destructuring sums: we say

let (x,{l = (y,z)}) = e1 in e2

rather than

match e1 with (x,{l = (y,z)}) in e2

but prefer match where there are multiple branches:

match e1 with A x -> e2 | B (x, y) -> e3

Using 'let' for branching seems inharmonious with the existing style, and is rather arbitrarily limited to a single branch. I agree with Damien that it seems better to extend match than let, but I'm not sure that it's wise to encourage partial/fragile pattern matching at all. Wouldn't every use of this feature trigger warning 4?

Warning 4: this pattern-matching is fragile.
It will remain exhaustive when constructors are added to type t.

On the other hand, if we do end up including it then we should certainly also extend match guards to support 'when let':

match e with
Var x when let Some v = lookup x env -> e'
| ...

@vicuna
Copy link
Author

vicuna commented Dec 10, 2014

Comment author: mdelahaye

I like the "if let" myself.

Concerning the warning, I think a different warning should be raise iff the number of constructors is greater than two.

It reasonates with the definition of the if-then-else construct in Coq for instance, where the if-then-else is a special case of match where the matched expression's type has two constructors (https://coq.inria.fr/refman/Reference-Manual004.html#sec64)

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @yallop

Another possibility is to add a form for explicitly-partial matches. For example, we might add a 'match?' form that allows us to write

match? x with Some y ->
match? f y with [z] ->
y + z

which behaves like the more verbose:

match x with
| Some y ->
(match f y with
[z] -> y + z
| _ -> ())
| _ -> ()

We could do the same for let, I suppose, giving something very much like 'if let' in the original proposal:

let? Some y = x in
let? [z] = f y in
y + z

There's some precedent for this kind of postfix modifier; for example, we already have 'open!' and 'method!'.

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @lpw25

match? x with Some y ->
match? f y with [z] ->
y + z

which behaves like the more verbose:

match x with
| Some y ->
(match f y with
[z] -> y + z
| _ -> ())
| _ -> ()

I think that the "?" postfix would more naturally expand to:

match x with
| Some y ->
(match f y with
[z] -> y + z
| _ -> raise Match_failure("filename", loc1, loc2))
| _ -> raise Match_failure("filename", loc1, loc2)

Which I think this is an improvement on the current practise of writing:

match x with
| Some y -> ..
| _ -> assert false

because it indicates that the failure is from a match failure, rather than any other kind of assertion.

However, this version of the expansion is obviously not a replacement for "if let".

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @yallop

lpw25: the intention is to ask "does e match p?" without needing to explicitly handle the case where it doesn't. Selectively disabling warnings is useful, but it's not the goal here.

We can already achieve the behaviour you propose with

match[@ocaml.warning "-8"] x with Some y ->
match[@ocaml.warning "-8"] f y with [z] ->
y + z

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @lpw25

Selectively disabling warnings is useful, but it's not the goal here.

Sure, but previous postfix things have been about disabling warnings.

If the thing you dislike about if let is its attachment to let rather than match, then what about:

if match x with Some y then print_string y;

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @yallop

One drawback of both 'if match' and 'if let' is that the reader has to scan arbitrarily far to the right to disambiguate it from existing valid syntax. The following look rather similar but behave quite differently:

if match x with Some y -> y then e
if match x with Some y then e

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @lpw25

I think I prefer if let to if match and match? because it will also work with when (as Jeremy suggested earlier). This seems like an important use of these "binding non-exhaustive matches" -- more important than if let in my opinion because it allows you to express things which can't really be expressed at the moment.

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @yallop

I'm also strongly in favour of extending "when" to support pattern matching. However, there are two significant objections to "when let".

First, it has the same problem as "if let" and "if match" that the human reader needs to scan ahead arbitrarily to disambiguate two possible parses. There's evidence that this kind of ambiguity causes difficulties in practice from so-called "garden path" sentences in English; when we read things like

The old man the boat.
The complex houses married and single soldiers and their families.

then it can take considerable mental effort to find the correct parse.

Second, it would be a real shame to limit "pattern-matching when" to a single branch.

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @lpw25

First, it has the same problem as "if let" and "if match" that the human reader needs to scan ahead arbitrarily to disambiguate two possible parses. There's evidence that this kind of ambiguity causes difficulties in practice from so-called "garden path" sentences in English; when we read things like

The old man the boat.
The complex houses married and single soldiers and their families.

I agree that it is not ideal, but I don't think that lets directly within if, while, or when are that common, so it shouldn't come up that often.

I also don't think your English examples are analogous, because they could not be parsed by an LALR parser. The key difference is that in the case of if let you know for each sub-expression what kind of thing it is, regardless of which version is being used (e.g. the thing after let is always a pattern, the thing after = is always an expression). In your English examples, this is not true, so you must go back and re-parse the sub-expressions after you have determined which syntax is being used (e.g. man is usually a noun but is here used as a verb, so first you treat it as a noun and once that does not parse you must go back and try again treating it as a verb).

Second, it would be a real shame to limit "pattern-matching when" to a single branch.

I agree, but I can not think of a good syntax for doing so. Also note that the single branch case is expressive enough to handle the multiple branch case:

let f = function
| _ :: _ :: _ -> Some true
| _ :: _ -> Some false
| _ -> None
in
match foo with
| true when let Some x = f bar -> x
| _ -> raise Not_found

so it would still be a great improvement over the current situation.

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @lpw25

I also don't think that if let requires you to scan all the way to the right to disambiguate in practise, because in one case the pattern should be irrefutable, and in the other it should be refutable. So

if let Some x ...

is almost certainly an if let, whilst

if let x ...

is almost certainly a let within an if expression.

In both cases, if the assumption is not true then a warning should be issued (non-exhaustive match in one case and unused case in the other).

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @yallop

The point of the analogy with English is that syntax that forces the reader to backtrack increases the cognitive load.

When you see

if let Some x = e ...

you need to know whether the condition is that the pattern matches or that the result of the expression is true. LALR doesn't really have anything to do with it: there are all sorts of things that LALR parsers can parse that humans can't and vice versa.

Relying on the reader's refutability checking is likely to make things even slower!

Also note that the single branch case is expressive enough to handle the multiple branch case

I don't think that's true at all.

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @lpw25

The point of the analogy with English is that syntax that forces the reader to backtrack increases the cognitive load.

Yes, and I'm saying that this syntax does not require backtracking from the reader.

When you see

if let Some x = e ...

you need to know whether the condition is that the pattern matches or that the result of the expression is true.

Whether this is an if let or a let in an ifcondition does not change what this code will do, the expression e will be evaluated and its result bound to x if it is a Some. By reading further, the reader learns where this x will be bound and what happens in the failure case, but the reader does not need to return to this part of the code.

Relying on the reader's refutability checking is likely to make things even slower!

Except that in 99% of cases this check is trivial (is the pattern a variable). I imagine that it is quite difficult (if not impossible) to find a real world piece of code that currently reads if let Foo x = ....

I don't think that's true at all.

In what way is the example I gave not an illustration that the case of multiple branches can be handled using when let?

The need for a local function, and wrapping the result in an option is not ideal, but the point is that you can bind variables in a pattern, match on an expression containing those variables and then if the match succeeds return an expression based on those variables, and if the match fails continue with the original pattern match. This is not currently possible, and would be with the addition of when let.

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @yallop

Yes, and I'm saying that this syntax does not require backtracking from the reader.

Only if readers can defer determining the scope of bound variables and what happens when matching fails until they're halfway through the code. I think human people trying to understand human intentions like to know these kinds of details as soon as possible.

In what way is the example I gave not an illustration that the case of multiple branches can be handled using when let?

I think I was mistaken.

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @lpw25

Only if readers can defer determining the scope of bound variables and what happens when matching fails until they're halfway through the code. I think human people trying to understand human intentions like to know these kinds of details as soon as possible.

Sure. I'm not really disagreeing with you that it is awkward to read, I just think that your English examples require backtracking in a much stronger way than this syntax does, and that the kind of double-take that those examples force you to do does not really apply here.

I think the lack of people actually writing if let Foo x = ... in practise is a much stronger argument that the syntax isn't that bad. (Ironically it is the fact that let is not normally used for variants which makes it easier to read).

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @yallop

I think the truth is somewhere in between. Sure, the ambiguity introduced by 'if let' doesn't correspond perfectly in every respect to the difficulties with English garden path sentences. The difficulties are analogous, not identical.

The problem is a bit more than the new construct being "awkward to read", though: it's not possible to decide whether you're in an 'if let' or an 'if' until you've read arbitrarily far to the right. You can call this "learning how the code behaves later" rather than "backtracking" if you prefer, but it's still a cognitive burden. As you say, the problem is mitigated by the fact that 'if let' is currently rare, especially with variant types, but it's not eliminated.

There's also still the problem that let is a strange choice for a branching construct, since match, not let, is used for matching with branching elsewhere.

And there's still the problem that let limits you to a single branch, which seems less than optimal.

For what it's worth, I don't really understand what you have against 'match?' and 'let?'. Is it simply the fact that '!' is used to disable warnings? It seems a shame to give up a whole syntactic category for that purpose now that we can accomplish the same thing with attributes .

@vicuna
Copy link
Author

vicuna commented Dec 18, 2014

Comment author: @lpw25

For what it's worth, I don't really understand what you have against 'match?' and 'let?'. Is it simply the fact that '!' is used to disable warnings? It seems a shame to give up a whole syntactic category for that purpose now that we can accomplish the same thing with attributes .

It's not just that '!' elsewhere is used to remove warnings, it's that there is an obvious remove warnings behaviour for match? which is very similar to what you are proposing it to do. This seems likely to confuse (for example I originally thought that there was just a typo in your expansion).

I would also prefer a solution that worked consistently for all of if, while and when since the same issue arises in all three places.

@github-actions
Copy link

This issue has been open one year with no activity. Consequently, it is being marked with the "stale" label. What this means is that the issue will be automatically closed in 30 days unless more comments are added or the "stale" label is removed. Comments that provide new information on the issue are especially welcome: is it still reproducible? did it appear in other contexts? how critical is it? etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant