Allow multiple guard clauses for a single arm in pattern matchings
I propose we allow multiple guard clauses for a single arm in pattern matchings, as in Haskell.
Consider the following code snippet:
let fileType: string option -> string = function
| Some s when s.EndsWith ".png" -> "image"
| Some s when s.EndsWith ".mp4" -> "video"
| _ -> "unknown"
In the matching above pattern Some s appeared twice only for different guards. Of course in this scenerio Some s is short and does feel bothering but things can get really complicated in real-world projects. In that case, writing the same pattern over and over again can be cumbersome.
In some languages, most noticeably Haskell, there can be multiple guard clauses for a single arm. Following is the Haskell equivalent of fileType function:
fileType (Just s)
| ".png" `isSuffixOf` s = "image"
| ".mp4" `isSuffixOf` s = "video"
fileType _ = "unknown"
I think the same can be extended to F#. It would be desirable if one can write something like:
let fileType: string option -> string = function
| Some s
when s.EndsWith ".png" -> "image"
when s.EndsWith ".mp4" -> "video"
| _ -> "unknown"
The existing way of approaching this problem in F# is replicating the pattern, as shown above; or moving the guards into the body part of the arm:
let fileType: string option -> string = function
| Some s ->
if s.EndsWith ".png" then "image"
else if s.EndsWith ".mp4" then "video"
else "unknown"
| _ -> "unknown"
This solved the problem of replicative patterns, but lost the ability to "fallthrough" to the next clause if all guards fails, leading to duplicate "unknown".
Other solutions might include active patterns or deliberate reordering, but they are all not ideal in terms of conciseness.
Pros and Cons
The advantages of making this adjustment to F# are more powerful pattern matching mechanism and less redundant code.
The disadvantages of making this adjustment to F# are encouragement for unstructured boolean tests and potential abuse of fallthrough, perhaps. Also, the layout rule may be subtle.
Extra information
Estimated cost (XS, S, M, L, XL, XXL): M
Related suggestions: None
Affidavit (please submit!)
Please tick these items by placing a cross in the box:
- [x] This is not a question (e.g. like one you might ask on StackOverflow) and I have searched StackOverflow for discussions of this issue
- [x] This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to the compiler and tooling repository
- [x] This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it
- [x] I have searched both open and closed suggestions on this site and believe this is not a duplicate
Please tick all that apply:
- [x] This is not a breaking change to the F# language design
- [ ] I or my company would be willing to help implement and/or test this
For Readers
If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.
The existing way of approaching this problem in F# is replicating the pattern, as shown above; or moving the guards into the body part of the arm:
let fileType: string option -> string = function | Some s -> if s.EndsWith ".png" then "image" else if s.EndsWith ".mp4" then "video" else "unknown" | _ -> "unknown"This solved the problem of replicative patterns, but lost the ability to "fallthrough" to the next clause if all guards fails, leading to duplicate
"unknown".Other solutions might include active patterns or deliberate reordering, but they are all not ideal in terms of conciseness.
I think active patterns would indeed be the F# solution here:
let (|EndsWith|_|) (suffix : string) (s : string) = s.EndsWith suffix
let fileType = function
| Some (EndsWith ".png") -> "image"
| Some (EndsWith ".mp4") -> "video"
| _ -> "unknown"
Or, since None can't end with anything:
let (|EndsWith|_|) (suffix : string) (s : string option) = s |> Option.exists _.EndsWith(suffix)
let fileType = function
| EndsWith ".png" -> "image"
| EndsWith ".mp4" -> "video"
| _ -> "unknown"
Those seem pretty close to the conciseness of your Haskell example, no?
fileType (Just s)
| ".png" `isSuffixOf` s = "image"
| ".mp4" `isSuffixOf` s = "video"
fileType _ = "unknown"
Active patterns are fantastic when you can reuse them. It's not hard to imagine that you could end up with a multitude of variations of EndsWith, because the underlying pattern might be slight different. Having APs that are only ever used in one match is somewhat counterproductive when the goal is reducing verbosity/repetition.
Personally I think moving the guard into an if inside the clause body, as suggested, is the way to go (as a bonus you are not giving up exhaustiveness checking at the top level).
Another proposal is to allow non-exhaustive nested matching
let fileType: string option -> string = function
| Some s with
| _ when s.EndsWith ".png" -> "image"
| _ when s.EndsWith ".mp4" -> "video"
| _ -> "unknown"
Although it's not clear what _ will bind to.
To reduce noise it's possible to implement guard-only pattern case
match x with
| somePattern when someCondition -> ()
| when anotherCondition -> ()
And together these 2 features will allow to write nice
let fileType: string option -> string = function
| Some s with
| when s.EndsWith ".png" -> "image"
| when s.EndsWith ".mp4" -> "video"
| _ -> "unknown"
Like in many previous suggestions about extending the pattern matching language, active patterns are the recommended answer. Are they considered difficult to author, or perhaps a non-standard tool in one's toolbox?
For the motivating example here, I could also imagine a new library/module offering built-in APs for commonly encountered scenarios of matching built-in types ( StringPatterns , ArrayPatterns, DateTimePatterns, ... ?). Since APs do compose well, they could then be used to match on parts of complex types as well.
Related: https://github.com/fsharp/fslang-suggestions/issues/715#issuecomment-1505402112
I'm not in favour of making additions/changes around
whenin pattern matching.The situation today is stable and well-documented. It causes some confusion as
whenapplies over an entire set of disjunctive matches, but that is fairly well known.I think adding further things in this area won't be productive in balance
Maybe we can allow arbitrary boolean/option-returning functions to be used in place of active patterns like in Haskell:
#r "nuget:FSharpPlus" // String.endsWith from FSharpPlus
open FSharpPlus
let fileType: string option -> string = function
| Some (`String.endsWith` ".png") -> "image" // function wrapped in backticks
| Some (`String.endsWith` ".mp4") -> "video"
| _ -> "unknown"
let fileType': string option -> string = function
| Some (`_.EndsWith` ".png") -> "image" // using the shorthand lambda notation for instance methods
| Some (`_.EndsWith` ".mp4") -> "video"
| _ -> "unknown"
Related: https://github.com/fsharp/fslang-suggestions/issues/968 (@dsyme ruled that property patterns cannot resolve to methods and active patterns should be used - but defining a use-once active pattern is boilerplate)
Maybe the backticks can even be extended to support expression syntax in patterns (eliminating the infinite expansion of syntax at https://github.com/fsharp/fslang-suggestions/issues/1018):
let fileType: string option -> string = function
| Some (`(fun (x:string) -> x.EndsWith) ".png"`) -> "image" // arbitrary expression syntax wrapped in backticks
| Some (`(fun (x:string) -> x.EndsWith) ".mp4"`) -> "video"
| _ -> "unknown"
Active patterns still have their place for total active patterns. But the partial active pattern is just a limited form of boolean/option-returning functions?
Maybe we can allow arbitrary boolean/option-returning functions to be used in place of active patterns like in Haskell:
This is already doable with APs, we could just add this to the standard library:
let (|Is|_|) f x = f x
so that you can then do:
let fileType: string option -> string = function
| Some (Is (String.endsWith ".png")) -> "image"
| Some (Is (String.endsWith ".mp4")) -> "video"
| _ -> "unknown"
@Tarmil Pattern syntax cannot cover all expression syntax - see https://github.com/fsharp/fslang-suggestions/issues/1018#issuecomment-857885378
For reference: https://github.com/dotnet/fsharp/blob/a70f3beacfe46bcd653cc6d525bd79497e6dd58e/src/fsharp/pars.fsy#L3238-L3241
/* Also, it is unexpected that '(a, b : t)' in a pattern binds differently to */ /* '(a, b : t)' in an expression. It's not that easy to solve that without */ /* duplicating the entire expression grammar, or making a fairly severe breaking change */ /* to the language. */
Also (|Is|_|) doesn't generalize over option voption and bool returning functions - you'd need 3 versions
let (|Is|_|) f x = f x
module String = let endsWith (x: string) (s:string) = s.EndsWith x
let fileType: string option -> string = function
| Some (Is (String.endsWith ".png")) -> "image"
| Some (Is (String.endsWith ".mp4")) -> "video"
| _ -> "unknown"
3tvik2tx..fsx(4,17): error FS0001: Type mismatch. Expecting a
'string -> 'a option'
but given a
'string -> bool'
The type ''a option' does not match the type 'bool'
@Tarmil Pattern syntax cannot cover all expression syntax - see #1018 (comment)
Sure, but that should be a separate issue. A solution that adds special syntax for full function expressions, while leaving AP arguments with their current limitations, would feel pretty half-baked IMO.
Also
(|Is|_|)doesn't generalize overoptionvoptionandboolreturning functions - you'd need 3 versions
I think limiting it to bool would be fine.
A special syntax like the backticks can be the idiomatic way to embed arbitrary expressions as patterns - and option/voption/bool-returning functions can be implicitly partial active patterns. The proposed expression embed syntax can also be AP arguments.
I am marking this as probably not since many existing solutions have been shown and the suggestion did not get traction afterwards. That being said, a separate suggestion to bringing in active patterns (which have been used in F# code) to FSharp.Core is possible and could simplify a lot of beginner scenarios 👍 .