fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

Allow further expressions as active pattern arguments

Open Happypig375 opened this issue 4 years ago • 11 comments

Allow arbitrary expressions as active pattern arguments

I propose we allow:

  • Property access Already allowed:
let (|Eq|_|) x = (=) x >> function true -> Some Eq | false -> None
let l = "123".Length
match 3 with
| Eq l -> printfn "A"
| _ -> printfn "B"

Should allow:

let (|Eq|_|) x = (=) x >> function true -> Some Eq | false -> None
match 3 with
| Eq "123".Length -> printfn "A"
(*
error FS0010: Unexpected symbol '.' in pattern matching. Expected '->' or other token.
error FS0010: Unexpected symbol '.' in pattern matching. Expected '->' or other token.
error FS0001: This expression was expected to have type
    'int'    
but here has type
    'string'    
*)
| _ -> printfn "B"
  • typeof (https://github.com/fsharp/fslang-suggestions/issues/758) Already allowed:
let (|Eq|_|) x = (=) x >> function true -> Some Eq | false -> None
let t = typeof<int>
match typeof<int> with
| Eq t -> printfn "A"
| _ -> printfn "B"

Should allow:

let (|Eq|_|) x = (=) x >> function true -> Some Eq | false -> None
match typeof<int> with
| Eq typeof<int> -> printfn "A" // error FS0010: Unexpected type application  in pattern matching. Expected '->' or other token.
// error FS0010: Unexpected type application  in pattern matching. Expected '->' or other token.
// error FS0685: The generic function 'typeof' must be given explicit type argument(s)
| _ -> printfn "B"
  • Lambdas (fun and function) Currently allowed:
let (|Member|) f x = f x
let f (x:System.Type) = x.BaseType
match typeof<int> with
| Member f (Member f null) -> printfn "A"
| _ -> printfn "B"

Should allow:

let (|Member|) f x = f x
match typeof<int> with
| Member (fun x -> x.BaseType) (Member (fun x -> x.BaseType) null) -> printfn "A"
(*
error FS0010: Unexpected keyword 'fun' in pattern. Expected ')' or other token.
error FS0583: Unmatched '('
error FS0010: Unexpected symbol ')' in implementation file
error FS0010: Unexpected keyword 'fun' in pattern. Expected ')' or other token.
error FS0583: Unmatched '('
error FS0010: Unexpected symbol ')' in implementation file
error FS0001: Type mismatch. Expecting a
    'System.Type -> 'a'    
but given a
    '('b -> 'c) -> 'b -> 'c'    
The type 'System.Type' does not match the type ''a -> 'b'
*)
| _ -> printfn "B"

(Imagine this with #506 :wink:)

let (|Member|) f x = f x
match typeof<int> with
| Member _.BaseType (Member _.BaseType null) -> printfn "A"
| _ -> printfn "B"

(Partially applied operators are already ok too!)

let (|Apply|) f x = f x
match 1 with
| Apply ((+) 1) (Apply ((+) 1) 3) -> printfn "A"
| _ -> printfn "B"
  • Computation expressions Currently allowed:
let (|SeqEqual|_|) a = Seq.map2 (=) a >> Seq.fold (&&) true >> function true -> Some SeqEqual | false -> None
let otherSeq = seq { 1; 2; 3 }
match seq { 1; 2; 3 } with
| SeqEqual otherSeq -> printfn "A"
| _ -> printfn "B"

Should allow:

let (|SeqEqual|_|) a = Seq.map2 (=) a >> Seq.fold (&&) true >> function true -> Some SeqEqual | false -> None
match seq { 1; 2; 3 } with
| SeqEqual (seq { 1; 2; 3 }) -> printfn "A"
(*
error FS0010: Unexpected integer literal in pattern. Expected identifier, 'global' or other token.
error FS0583: Unmatched '('
error FS0010: Unexpected integer literal in pattern. Expected identifier, 'global' or other token.
error FS0583: Unmatched '('
error FS0723: Invalid argument to parameterized pattern label
*)
| _ -> printfn "B"
  • Records Already allowed:
let (|Eq|_|) x = (=) x >> function true -> Some Eq | false -> None
type X = { x : int }
let x = { x = 3 }
match { x = 3 } with
| Eq x -> printfn "A"
| _ -> printfn "B"

Should allow:

let (|Eq|_|) x = (=) x >> function true -> Some Eq | false -> None
type X = { x : int }
match { x = 3 } with
| Eq { x = 3 } -> printfn "A"
// error FS0723: Invalid argument to parameterized pattern label
| _ -> printfn "B"
  • Anonymous records Already allowed:
let (|Eq|_|) x = (=) x >> function true -> Some Eq | false -> None
let x = {| x = 3 |}
match {| x = 3 |} with
| Eq x -> printfn "A"
| _ -> printfn "B"

Should allow:

let (|Eq|_|) x = (=) x >> function true -> Some Eq | false -> None
match {| x = 3 |} with
| Eq {| x = 3 |} -> printfn "A"
// error FS0010: Unexpected symbol '{|' in pattern
// error FS0010: Unexpected symbol '{|' in pattern
| _ -> printfn "B"

The existing way of approaching this problem in F# is to annoyingly put the argument in a let binding elsewhere!

Pros and Cons

The advantages of making this adjustment to F# are

  1. Consistency with the rest of F#
  2. Convenience when using active patterns
  3. Concise matching with active patterns

The disadvantages of making this adjustment to F# are (none).

Extra information

Estimated cost (XS, S, M, L, XL, XXL): S

Related suggestions: (put links to related suggestions here) https://github.com/fsharp/fslang-suggestions/issues/758 - This while only being scoped to typeof.

Affidavit (please submit!)

Please tick this 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] I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • [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.

Please tick all that apply:

  • [x] This is not a breaking change to the F# language design
  • [x] 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.

Happypig375 avatar Jun 03 '21 17:06 Happypig375

Also for context: https://github.com/dotnet/fsharp/issues/870 https://github.com/dotnet/fsharp/issues/4828 https://github.com/dotnet/fsharp/issues/5500

This is a missing case in let rec convSynPatToSynExpr x =

The F# language spec should really over which syntactic forms are converted.

But more importantly, F# pattern syntax doesn't actually have any syntax for type applications today, which is the core problem here. I think it is easy enough to add one but it would involve adding a case to the parser.

So Id be very happy to see this addressed but we should use a short RFC for it (consider it pre-approved)

Why not all expressions??

Happypig375 avatar Jun 03 '21 17:06 Happypig375

Why not all expressions??

At a high level it would be ideal to accept all expressions. However this would not be backwards compatible. The F# pattern syntax is a different part of the parser grammar to the expression syntax. However they have a convertible subset.

For example

  • 1 is both a pattern and an expression.
  • 1 & 1 is a valid pattern but not a valid expression
  • for x in 1 .. 2 do printfn "4" is a valid expression but not a valid pattern

Now, for better or worse the thing that is an active pattern argument is, syntactically, a pattern, which must lie in the subset that is convertible. That's what convSynPatToSynExpr does. So only convertible patterns are admitted.

dsyme avatar Jun 03 '21 17:06 dsyme

@dsyme I don't think you can put patterns when expressions are expected (active pattern inputs), nor put expressions where patterns are expected (active pattern outputs).

Happypig375 avatar Jun 03 '21 17:06 Happypig375

@dsyme I don't think you can put patterns when expressions are expected (active pattern inputs)

At the parser level, this isn't the case - almost, but not quite. This is because we allow the pattern to be dropped for a unit return type, e.g.

For example:

let (|A|_|) (n: int) (x:int) = if n = x then Some() else None;;

let f x =
    match x with 
    | A 3 -> 1
    | A 4 -> 2
    | _ -> 3

Here 3 and 4 look like pattern syntax but end up as active pattern arguments (expressions). This is the origin of convSynPatToSynExpr

Note I'm 100% ok with augmenting the pattern syntax to cover as many of the cases you mention above as possible, subject to backwards compatibility constraints.

dsyme avatar Jun 03 '21 17:06 dsyme

Are pattern syntaxes and expression syntaxes mutually exclusive? Can't a union of syntaxes be parsed and then error later?

Happypig375 avatar Jun 03 '21 17:06 Happypig375

Are pattern syntaxes and expression syntaxes mutually exclusive?

Yes, there are enough small differences that's not possible.

In retrospect I may have unified these syntaxes originally. However there are also good reasons not to do that, as this is the only corner-case point where this matters, and it may have prevented other work

dsyme avatar Jun 03 '21 17:06 dsyme

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. */

Happypig375 avatar Jun 09 '21 17:06 Happypig375

What about interpolated strings?

let (|Eq|_|) comp value = 
    if comp = value then Some() else None
    
let number = 22
let str = "Catch-22"
match str with
| Eq $"Catch-%d{number}" -> // error FS0010: Unexpected interpolated string (first part) in pattern
    printfn "Same"
| _ -> 
    printfn "Not same"

gsomix avatar Oct 02 '21 11:10 gsomix

What about interpolated strings?

Yes, they should be added to the available expressions.

dsyme avatar Oct 03 '21 12:10 dsyme

Marking as approved-in-principle because there are definite cases that should be allowed, as described above.

dsyme avatar Jun 14 '22 15:06 dsyme

Matching patterns of interpolated string and use the matched pattern's interpolation for stuffs can be a really easy way to write parser for things :

match string with
| ($"<div>{innerHtml}</div>") ->
    printfn (innerHtml)
| ($"<img src = "{url}"/>") ->
    printfn (url)
| _ -> ()
let rec parser string depth number_of_div =
    match string with
    | ($"<div>{innerHtml}</div>") ->
        parser innerHtml (depth + 1) (number_of_div + 1)
    | ($"<{_}>{innerHtml}</{_}>") ->
        parser innerHtml (depth + 1) (number_of_div)
    | _ -> struct {|depth = depth; div_count = number_of_div|}

If logics behind can process this pattern really efficiently this would be powerful I would be feeling so good about writing codes and building things and the satisfaction is incomparable for me coding using C#

Xyncgas avatar Feb 16 '25 01:02 Xyncgas