Closed vasily-kirichenko closed 9 months ago
I'm thinking of match?
.
match? x with Some x -> x // no need for None, implicitly returns ()
match?
is a good shorthand, because what we want is a slight variant on match
.match? x with pattern -> action // | pattern2 -> action2 ...
// equivalent to:
match x with pattern -> action | _ -> () // | pattern2 -> action2 ...
This is pattern matching, including both routing and binding of variables, so a very similar syntax makes sense.
if let
is not the right syntax.if let pattern = x then action // elif let pattern2 = x then action2 ...
let pattern = x in action
and if x = expr then action
) and merges them together without regard for the grammar of the originals. if
is something very simple normally, taking a boolean condition, but nothing that begins with "let" here corresponds to a boolean.pattern
and x
mixed up.elif
is too flexible, requiring = x
to be typed in repeatedly.If let makes sense to me if the expression evaluated to Some or true. Match? I would expect the expression to also be an option. Swift has these with also guard for the opposite pattern so you can choose the most logical syntax.
On Sun, 13 Oct 2019 at 15:39, Charles Roddie notifications@github.com wrote:
match? is a good shorthand, because what we want here is a slight variant on match.
match x with pattern -> action | _ -> () // | pattern2 -> action2 ...match? x with pattern -> action // | pattern2 -> action2 ...
This is pattern matching, including both routing and binding of variables, so a very similar syntax makes sense. if let is not the right syntax.
if let pattern = x then action // elif let pattern2 = x then action2 ...
- Really this just takes two forms (let pattern = x in action and if x = expr then action) and merges them together without regard for the grammar of the originals. if is something very simple normally, taking a boolean condition, but nothing that begins with "let" here corresponds to a boolean.
- Users who are coding too quickly may get the order of pattern and x mixed up.
- The natural extension elif is too flexible, requiring = x to be typed in repeatedly.
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/fsharp/fslang-suggestions/issues/705?email_source=notifications&email_token=AAEPXSWWQHCDMMLQOCUIUHDQOMXILA5CNFSM4GCI6LC2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEBCXQ5A#issuecomment-541423732, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEPXSWHDNHO3RYDOFLUY3TQOMXILANCNFSM4GCI6LCQ .
If the expression is limited to an option, then it would duplicate Option.iter
.
match?
certainly is an interesting idea, but if it's going to mean 'a match that automatically inserts catch all four missing matches', then the return type of this catch-all should be the same, which is problematic.
Unless it's going to be only allowed in yield
or yield!
expressions, but this greatly limits the applicability.
Unless it's only allowed in if
expressions and a successful match means true, a non-match false.
Another way of dealing with this is to allow active patterns to return a specific instance of an interface, let's say IMatch
. This could then yield a property that is used as return type, and using match or match? can use this active pattern without ->...
, which then becomes implicit. Such approach could be generic enough to simplify the use case of the OP, and to apply to many other use cases.
let x = if (match? foo with MyMatch) then...
Here, MyMatch
implements IMatch
, which gets treated specially (details TBD).
(just trying to think out of the box, not sure this is feasible, but the discussion here suggests that some improvements to matching are warranted)
I would like match?
to be able to accept arbitrary patterns.
let x = match? foo with MyMatch x -> x // like if without else, this will infer the type of x as unit
The IMatch
becomes too much like a regular old if
. It isn't needed.
@7sharp9 If let makes sense to me if the expression evaluated to Some or true. Match? I would expect the expression to also be an option.
Neither if let
nor match?
would restrict patterns to options. Options were just an initial motivating example.
@abelbraaksma then the return type of this catch-all should be the same
Yes it's unit
(for both if let
and match?
).
@abelbraaksma
let x = if (match? foo with MyMatch) then... else ...
This is a separate shorthand suggestion, where foo matches MyMatch
is equivalent to match foo with MyMatch -> true | _ -> false
. I think it should go in a separate suggestion (with a better example because your example reduces to let x = match foo with MyMatch -> ... | _ -> ...
). It's slightly related to https://github.com/fsharp/fslang-suggestions/issues/239.
Well ? Is synonymous with try, and try with option, so if that wasn’t the case then it would be confusing.
On Sun, 13 Oct 2019 at 16:26, Charles Roddie notifications@github.com wrote:
@7sharp9 https://github.com/7sharp9 If let makes sense to me if the expression evaluated to Some or true. Match? I would expect the expression to also be an option.
Neither if let nor match? would restrict patterns to options. Options were just an initial motivating example.
@abelbraaksma https://github.com/abelbraaksma then the return type of this catch-all should be the same
Yes it's unit.
@abelbraaksma https://github.com/abelbraaksma let x = if (match? foo with MyMatch) then...
This is a separate shorthand suggestion, where foo matches MyMatch is equivalent to match foo with MyMatch -> true | -> false. I think it should go in a separate suggestion - with a better example because your example reduces to let x = match foo with MyMatch -> ... | -> .... It's slightly related to #239 https://github.com/fsharp/fslang-suggestions/issues/239.
— You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub https://github.com/fsharp/fslang-suggestions/issues/705?email_source=notifications&email_token=AAEPXSVMS7Q4F6QTQTXQIXDQOM4Y3A5CNFSM4GCI6LC2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEBCYT7A#issuecomment-541428220, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEPXSV65RWR4G7YTT2SWOLQOM4Y3ANCNFSM4GCI6LCQ .
What about ifmatch
e.g.
ifmatch opts.Id with Some id then yield Props.Id id
elif ...
elifmatch ...
else ....
What about ifmatch e.g.
Could work and is unambiguous, but doesn't that imply introducing a formerly non-reserved keyword as new keyword?
What's wrong with
#nowarn "25"
match opts.Id with Some id -> yield Props.Id id :> IHTMLProp
At least it's clear that some warning (completeness checking, in this case) is being ignored.
@FunctionalFirst "The effect of disabling a warning applies to the entire file" (source).
@charlesroddie Hmm, I didn't expect that. I also figured on something like #restore "25"
being available, but it doesn't seem to be.
Hmm, I didn't expect that. I also figured on something like #restore "25" being available, but it doesn't seem to be.
There's an open suggestion somewhere that intends to solve this. It's already approved in principle, I believe, it just needs an RFC and an implementation.
Could work and is unambiguous, but doesn't that imply introducing a formerly non-reserved keyword as new keyword?
Yes, though likely we could somehow turn off the feature if the user has defined a value/function called ifmatch
What's wrong with
#nowarn "25" match opts.Id with Some id -> yield Props.Id id :> IHTMLProp
Do you really think requiring a preprocessor directive for a code pattern is a good idea? IMO their intended purpose is definitely not to allow code to be more ergonomic. Imagine a whole code base polluted with this directive everywhere.
The suggestions are confusing to follow because there's no definite statement of the functionality which should be achieved. Here's one attempt to explain my understanding structurally:
if let
will bind identifiers to a matched value, but only if the pattern, in fact, matches. It solves the problem of not needing to "unsafely" rely on previous assertions, because doing so leads to error-prone code when accessing the value(s) which went through the assertion. For instance, in the following code:
if Option.isSome opts.Id then yield opts.Id.Value
the yield part assumes Value
is present because Option.isSome
has been evaluated to true
; however, a programmer could make the mistake of writing
if Option.isNone opts.Id then yield opts.Id.Value
which isn't invalid code, but it's wrong.
By the description above, we're interested in defining a binding (singular) if the pattern (singular) matched. It's not relevant to wonder about what would be returned in the other "branches" (this is not match
), by that description, because we're only interested in the bound values from this single pattern. In other words, if let
does not produce an expression with the matched value; it's a true/false statement which introduces new bindings, as far as evaluation is concerned. The same way you can't do:
let foo = let bar
It would not be possible to do
let foo = if let (Some id) = opts.Id
This rephrasing is more in-line with https://github.com/fsharp/fslang-suggestions/issues/705#issuecomment-436901840. Lots of other different ideas have been brought up, but from my perspective they're tied to this notion of if let
behaving like match
. While that would be more "powerful", if it's inconsistent or can't be made to work due to caveats, then can the proposal proceed in a more restricted form? I personally think an implementation with the intentions mentioned above would be useful as it is.
I propose the original issue text be updated (@vasily-kirichenko), I agree that it's difficult to understand what this proposal entails. Even with your comment, @resolritter, I still had to go over relevant bits in the comments.
The key part here is the binding, just like in match
syntax, to prevent if x then y
fail. I'm not sure it should be a requirement that the result be unit
, it feels to me too limiting. The way I understand it, you can now write:
let f x =
match x with
| Ok ok -> ok
| _ -> // raise or return default value
With if let
, let's call it ifmatch
per @dsyme's comment, we can rewrite this as follows:
let f x = ifmatch x with Ok ok then ok else ... (* raise or return default *)
In the left-hand bound syntax proposal from @vasily-kirichenko, this would instead be:
let f x = if let (Ok ok) = x then ok else ... (* raise or return default *)
Which is essentially the following using existing syntax, but without the default raising on non-match:
// gives warning
let f x = let (Ok ok) = x in ok
In the absence of else
, just like normal if
, the return type must be unit
.
The advantage of this syntax is that you can bind a value in the match syntax. The disadvantage is that it gives yet another way to do the same thing.
As an aside, this essentially reminds me of Perl syntax, that allows you to write $x = foo() or die()
, which executes or
-branch if the first is false. Which in and off itself suggests yet another, simpler, alternative syntax:
// using `or` cancels the completeness checking and executes the or-branch on non-match
let f x = let (Ok ok) = x in ok or raise "failed"
// same with the unit-example from the OP
let f x = let (Ok ok) = x in log (ok) or ()
Thinking of this a bit, it kinda feels more natural and more importantly, better self-documenting the intention of the code.
@abelbraaksma I appreciate the intention, but I think you arrived at the same "misinterpretation" I pointed out as the main problem in this discussion; or, rather, this different interpretation you have could be seen as another implementation take for the feature, but one that, in my view, expands on the scope intended from the motivating examples in the OP.
I'll reiterate from the previous comment
In other words,
if let
does not produce an expression with the matched value; it's a true/false statement which introduces new bindings, as far as evaluation is concerned.
Followed by
The same way you can't do:
let foo = let bar
It would not be possible to do
let foo = if let (Some id) = opts.Id
The point is not to make this new feature behave like a lesser version of match
. if let
(or ifmatch
, or what have you) will not be assignable.
If you scroll all the way up to the first examples, the usage pattern proposed by @vasily-kirichenko does not imply any sort of RHS construct; while that might the case for Rust and possibly other languages, since we already have match
in F#, there's not a good reason to introduce a lesser version of it through this feature.
Choosing between the two goes like this: if you want a RHS value decided from a pattern, use match
. On the other hand, if you want to use a binding which matched during the evaluation of an if
expression, use if let
. Instead of thinking of "if let" as a couple, think of it as a let
which happened during an if
; that'll hopefully make this more explicit.
Going by this interpretation, it would not be relevant to discuss return values, or elif
, or else
, or anything else in that vein. And would it be useful enough like this? The previous post motivates some usages where it would make the code less error-prone. If it were to be implemented according to this explanation, I believe it'll also work for the motivating code in the first few posts on this thread.
https://github.com/fsharp/fslang-suggestions/issues/705#issue-378244682 https://github.com/fsharp/fslang-suggestions/issues/705#issuecomment-436823250 https://github.com/fsharp/fslang-suggestions/issues/705#issuecomment-436901840
@resolritter, I understand you cannot write let foo = let bar
, as the second let
misses a statement. It should be let foo = let bar = 1 in bar + 1
. Likewise, let foo = if let (Some id) = opts.Id
should be let foo = if let (Some id) = opts.Id then id else 42
, or something like that.
In other words, if let does not produce an expression with the matched value; it's a true/false statement which introduces new bindings, as far as evaluation is concerned.
and
The point is not to make this new feature behave like a lesser version of match. if let (or ifmatch, or what have you) will not be assignable.
But assignment doesn't exist in F# and is not what I proposed, and that if let
introduces a scoped binding is exactly what I showed (at least I tried).
Then you link to posts that appear to follow my suggestion of syntax. In your post, I don't see a suggestion of syntax or other examples, only things that you say are not possible. Something tells me that you think there should be no =
-sign in this proposal, though all comments have the =
-sign except for https://github.com/fsharp/fslang-suggestions/issues/705#issuecomment-436823250 by @cartermp, but I think that was just an error in writing it up as he talks about Swift and the like, which also have the =
-sign in the syntax.
In my write-up and n your summary, we both talk about a new binding. Like in Swift, this new binding should be in scope of the if
-block. And like current existing syntax, the lh-side contains the pattern. The binding would not be in scope of the else
-block. As @vasily-kirichenko suggests, this is most useful for types that have two cases, like Result
or Option
.
Since any existing if
block can currently be used at the rh-side of a binding, I would choose to keep that, as opposed to limiting this proposal to always returning unit
. That's just my take, seeing the discussion above it is clearly not the take of the OP. The jury is still out on this ;).
I.e., currently this is legal:
let x = if foo = 42 then y else z
And likewise, principle of least surprise, this should be legal if this proposal were accepted:
let x = if let (Some foo) = myOpt then foo else 42
The previous post motivates some usages where it would make the code less error-prone.
In that/your post there are no examples of if let
, or at least I'm not seeing them, I'm confused to what you mean here. There is an example on a regular if
that could go haywire if coders use direct accessors to optional values like myOpt.Value
, which should virtually never be used, whether or not this syntax is going to be accepted.
So, to summarize, I still don't quite get what you're aiming at, in fact, it seems to me that you are saying the same as I do, just using different terminology, and somehow we misunderstand each other. ;) Perhaps you can give some non-amgibuous examples that illustrate what you mean and examples with the new syntax that illustrate what you consider illegal.
PS: I still would like to ask @vasily-kirichenko to rewrite the original proposal with examples that don't require domain knowledge, preferably as concise as possible. Plus a summary of the alternatives discussed in the thread. Considering the input from @dsyme and @cartermp, it feels like this syntax may get accepted, but it would be nice if we have a clear summary to continue discussion on.
I have misread https://github.com/fsharp/fslang-suggestions/issues/705#issuecomment-699485229 in the sense that I thought it was disagreeing with the explanation.
What honestly prompted my participation in this thread was reading those sorts of comments
https://github.com/fsharp/fslang-suggestions/issues/705#issuecomment-437048389 https://github.com/fsharp/fslang-suggestions/issues/705#issuecomment-542385848 https://github.com/fsharp/fslang-suggestions/issues/705#issuecomment-436906276
where match
's functionality is being compared to what's expected out of this new construct in its simplest form, if we are to consider the motivating examples. I argue that it's not a welcome comparison because match
expressions produce a useful value, whereas in order to solve the needs raised in the OP, if let
can be limited to a true/false evaluation which has the side-effect of defining bindings. In other words, if let
being "limited" to this use-case would still be enough useful; perhaps more importantly, it would not overlap with match
, while still filling a gap which can't be solved elegantly currently (closest I found was https://github.com/fsharp/fslang-suggestions/issues/705#issuecomment-542385848, but it requires a pragma and it's also clunkier).
I would choose to keep that, as opposed to limiting this proposal to always returning unit
In contrast to this, I am insisting on the premise that "limiting" the functionality sounds like a good idea. Having if let
fit a very specific purpose has the upside of not introducing two ways of expressing the same thing, thus it would not be used anywhere match
could be used, vice versa.
Finally,
rewrite the original proposal with examples that don't require domain knowledge, preferably as concise as possible
This would be a good move for progressing the proposal. Lots of suggestions have been raised already; if they can be broadly separated into two (or N) camps
if let
should behave somewhat like a variant of match
if let
should be a true
or [false|unit|...]
statement which binds from a single patternEach different proposition can be motivated with examples of where/how they would be used, and then hopefully inform a decision of what's worth pursuing. It might be the case that e.g. (1) has too many caveats and is infeasible, (2) is not useful enough, etc.
Compelling case for both if let
and elif let
:
Not that I would encourage writing both styles, but helps with non-uniform expressions.
let someDumbFn () =
let a = None
let b = None
let c = None
let d = None
let e = None
let f = None
let g = None
let h = None
let i = None
let j = None
let k = None
let l = None
match (a, b, c, d, e, f, g, h, i, j, k, l) with
| (Some a, _, _, _, _, _, _, _, _, _, _, _) -> printfn "a: %A" a
| (None, Some b, _, _, _, _, _, _, _, _, _, _) -> printfn "b: %A" b
| (None, None, Some c, Some d, Some e, Some f, Some g, Some h, Some i, Some j, Some k, Some l) -> printfn "WAH..."
| _ -> printfn "Bad luck son."
if let (Some a) = a then
printfn "a: %A" a
elif let (Some b) = b then
printfn "b: %A" b
elif let (Some c) = c
&& let (Some d) = d
&& let (Some e) = e
&& let (Some f) = f
&& let (Some g) = g
&& let (Some h) = h
&& let (Some i) = i
&& let (Some j) = j
&& let (Some k) = k
&& let (Some l) = l then
printfn "WAH..."
else
printfn "Bad luck son."
@Swoorup, not sure I'd consider that "compelling", the match
variant is easier to read to me, and would benefit from splitting in two nested matches perhaps, for clarity. Or better, an improved domain model. I know it's just an example, but still.
FYI, Idris language has a little bit similar feature.
let Just id = opts.Id | Nothing => () in
...
Above code is same as the following code:
case opts.Id of
Nothing => ()
Just id => ...
Regarding.
match? opts.Id with Some id -> yield Props.Id id :> IHTMLProp
This example would be perfect, only sometimes I need to disable exhaustiveness on match. Also, I strive really hard to not have "if"s in my code, pattern match all the things ! More data-flow and less control-flow.
( I wish there was a warnings to keep you out from using if
, my entire 20Kloc backend has only 10 or so ifs
, but ifs are sometimes useful, but I digress )
More examples, this would also work this proposed change of match?
, so it disables the exhaustiveness check for unit or any DU when the last case is trivial like None of unit
, I think this would be the most useful feature to make code more terse in some cases.
let x = match? opts.Id with Some id -> yield Props.Id id :> IHTMLProp
And if you really want an if
, just by making if
work automatically on options and introducing a bit of syntax, (stolen from the match itself), to introduce the binding, its possible to achieve a very simple syntax with what we already have.
So I propose two changes to make it work:
The first one would be accepting options in the if
and automatically doing Option.isSome
on them, and the second change would be using the as
keyword to introduce the result of Option.get
as a 'let' binding if the Option.isSome
is true.
Examples for the proposed usage would be:
if opts.Id as it then yield Props.Id it :> IHTMLProp // when opts.Id is of type option<_>
if (match? opts with SomeActivePattern id -> Some id) as someVar then yield someVar |> someMapping // *1
if (Some 1) as v then v.GetType() |> Dump // "System.Int32"
The syntax on *1
almost already work, except mostly for the binding, I guess that it would be just some syntactic sugar for:
let someVar = (match opts with SomeActivePattern id -> Some id | _ -> None ) in if Option.isSome someVar then yield Option.get someVar |> someMapping
Or even without parenthesis, but I don't know how ambiguous that would be to parse, I don't mind paying the parenthesis in this case.
if match? opts.Id with Some id -> id as it then yield Props.Id it :> IHTMLProp
The syntax would be very similar to the let syntax, except we are introducing a variable binding not a value with the as
.
Its also very similar to what match
already does, except as
introduce it to the block-expression of the if
, this way the syntax wouldn't look bizarrely different and would fit better with the rest of the language.
Also, no new keywords would need to be introduced. Except for the new variant to match
, but I'm not sure if this is a token or keyword or what, as we have match!
already its already reserved, so its not a problem.
//things that are already possible
let id = 0 in let y = 1 in 2
match o with | :? 'T as res -> res
It's strange, but I literally never feel the need for this feature in this form
if let <pat> = <expr> else <expr>
just feels like a bad way of writing a match.
if let <pat> = <expr>
- the aim is to have a match with an implicit -> ()
. If so, then why not add something which keeps the same flow as match
, e.g.
match? opts.Id with Some id -> Props.Id id
match? opts.Value with Some value-> Props.Id value
match? opts.DefaultValue with Some defValue-> Props.Id defValue
...
This is consistent enough with if .. then
allowing the else
branch to be elided and ()
implied.
It also generalises to allow multiple pattern rules with still an implicit _ -> ()
though those use cases aren't that interesting (why not make the | _ -> ()
explicit? It's just one line)
match? input with
| Some 0 -> print "hello"
| Some 2 -> print "goodbye"
I'm trying to work out why I prefer this. I think I want the order to be
match?
rather than
if let
which seems back-to-front to me.
Also I'm attached to the principle that let doesn't fail
in F# (unless a warning is given for an incomplete match).
Also I like that converting between the two forms doesn't rearrange my code, it jsut removes ?
and adds | _ -> ()
I'm not attached to match?
- and there are problems with ?
which normally implies optionality or nullable. I just can't particularly think of anything better.
Thinking more about this proposal, I realize that C# essentially added this functionality when pattern matching was introduced by expanding the existing is
keyword. Prior it was a simple type checking operator, but was expanded to take pattern, match it against an expression, and, if the expression matches the pattern, propagate a true
expression, and, if the pattern creates variables, allow access to them from code that will only be executed along the true path.
I like the idea of adding a new operation which can return a boolean value to an instruction which can branch based off of a boolean, such as if ... then
and while ... do
, making the bound values from the pattern available in the appropriate branches.
If is
can be brought over as is, then that would allow the following.
if opts.Id is Some id then Props.Id id
if opts.Value is Some value then Props.Id value
if opts.DefaultValue is Some defValue then Props.Id defValue
...
If is
is too much of a breaking change, then, perhaps something else, such as match
could be used.
if opts.Id match Some id then Props.Id id
if opts.Value match Some value then Props.Id value
if opts.DefaultValue match Some defValue then Props.Id defValue
...
If is is too much of a breaking change
It probably is, because is
is not a reserved keyword. You can have let is x = x
, and since space has higher precedence, opts.Is is Some id
would then have the compiler complain that opts.Id is
is of the wrong type (i.e., is
being a function and all).
The other 'breaking change" thingy is that we are quite strict about what characters are allowed for infix operators. And operators containing letters are not among them.
This is probably not
, as discussed with Don. Closing it for now @dsyme.
Yes, I assessed this with Vlad, and this is not something that we plan to add to F#.
If a flow-typing system were added to F# it may be different. But for now, an explicit use of match
is the way to approach this kind of elimination.
match x with
| pat -> ...
| _ ->
...
Sad to see this been discarded. I really use and like the if let
in Swift
https://github.com/MangelMaxime/Fulma/blob/1cbbc35e046007029e4b382d502cd9b301443a69/src/Fulma/Elements/Form/Textarea.fs#L144-L151
I propose we add
if let <pattern> then <expression>
expression, so the above code would be rewritten as following:It's inspired by the same Rust's feature, see https://doc.rust-lang.org/book/second-edition/ch06-03-if-let.html, and the same Swift's feature, see https://medium.com/@abhimuralidharan/if-let-if-var-guard-let-and-defer-statements-in-swift-4f87fe857eb6
The existing way of approaching this problem in F# is (see the first snippet, or
match ... with Some x -> ... | None -> ()
, which is even more verbose, but safer).Pros and Cons
The advantages of making this adjustment to F# are:
The disadvantages of making this adjustment to F# are:
Extra information
Estimated cost (XS, S, M, L, XL, XXL): S-M
Affidavit (please submit!)
Please tick this by placing a cross in the box:
Please tick all that apply: