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

Pattern matching: Parity with C# 9 #793

Open
Happypig375 opened this issue Oct 3, 2019 · 23 comments

Comments

@Happypig375
Copy link
Contributor

commented Oct 3, 2019

Pattern matching: Parity with C# 9

I propose we have pattern matching that is at least on par with C# 9.

Here is the list:

  • Parenthesised patterns (we already have it)
  • And patterns (we already have it)
  • Or patterns (we already have it)
  • Not patterns (should add)
  • Relational patterns (should add)

The existing way of approaching this problem in F# is using active patterns. However,

  • Not patterns are complimentary to And and Or patterns and should be added for parity;
  • Relational patterns are complimentary to the equality-based constant patterns and should be added. Moreover, VB.NET already has it via Case Is, and C# is planning to add it despite when clauses for switch expressions are supported. Not having it in F# by default means that F# the functional language somehow has missing pieces for functional constructs compared to the other two languages. This is not friendly to C# migrants to F# who will discover the asymmetry.

If possible, I'd like to reinitiate the request for range patterns despite not being included in C# 9: it is complimentary to comparison patterns and is nicely symmetrical to range constructors for lists and for loops. Most built-in syntax for constructors havd their respective deconstructor, e.g. lists, arrays, tuples, struct tuples, constants, etc. This is missing for ranges.

Pros and Cons

The advantages of making this adjustment to F# are

  1. Parity with C#
  2. Shorter and more concise code.
  3. Making F#'s built-in pattern matching moee complete, compared to the upcoming C# 9.

The disadvantages of making this adjustment to F# are

  1. Multiple ways to do the same thing. This is offset by parity with C#, where not implementing this will cause more harm than good for people from C#.
  2. Revisiting declined suggestions (comparison patterns, range patterns). This is negated by the significant environmental change: C# deciding to implement comparison patterns. This poses a challenge for F# if not following suit.

Extra information

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

Related suggestions: (put links to related suggestions here)
#661 Comparison pattern - Declined
#264 Range pattern - Declined

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is a partial duplicate of #661 and #264.
  • 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.
  • This requests to reconsider #661 and #264.

Please tick all that apply:

  • 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
@Happypig375

This comment has been minimized.

Copy link
Contributor Author

commented Oct 3, 2019

This is how it will look:

  • Not patterns: The ! operator will be overloaded to mean not in a pattern context.
match 5 with
| !(4 | 5) -> true
| 4 -> false
| 5 -> bool (Console.ReadLine())
  • Comparison patterns: Following C#, the naked operators will be used. They will not be parenthesised to avoid confusion with function application.
let a = (<) 3 // 3 < x
let b =
  function
  | < 3 -> true // x < 3
  | _ -> false

Range patterns: Same as how you would construct a range.

match x with
| 0..13 -> 13
| 14 -> 14
| 15..System.Int32.MaxValue -> 15
@cartermp

This comment has been minimized.

Copy link
Member

commented Oct 3, 2019

Given the existence of ! for dereferencing ref cells, use as you propose would presuppose the deprecation of ! for that purpose, which is quite a breaking change. I think not could be used there instead.

Out of the suggestions, I would think that not patterns are the most likely to be accepted here. Relitigating past decisions doesn't typically work, and the general ethos of F# to have a more general mechanism instead of specific ones bumps into this. Still, I see the value in built-in comparison and range patterns, so I can't say I'm personally opposed. I just don't think they'd be too high of a priority.

@auduchinok

This comment has been minimized.

Copy link

commented Oct 3, 2019

Given the existence of ! for dereferencing ref cells, use as you propose would presuppose the deprecation of ! for that purpose, which is quite a breaking change.

It's not used this way in patterns, is it?
not on the other hand is a valid identifier and using it would break things.

@Happypig375

This comment has been minimized.

Copy link
Contributor Author

commented Oct 4, 2019

The existing & and | are already overloaded to mean boolean operations outside of patterns (albeit with warnings) and pattern combinators inside patterns. ! can do the same too.

@cartermp

This comment has been minimized.

Copy link
Member

commented Oct 4, 2019

Sure, ! could be assigned to mean that only within a match expression. Though I think we could get away with not expr as a pattern, since the liklihood that someone is using not to mean a variable pattern is really low, and you can't specify something like not expr today:

image

@Happypig375

This comment has been minimized.

Copy link
Contributor Author

commented Oct 4, 2019

! is in line with the other symbol operators - & and |. Moreover, ! will eventually gain the "not" boolean operation, deprecating its use as a dereferencing operator - #569, so the symmetry with not shouldn't be a consideration.

@cartermp

This comment has been minimized.

Copy link
Member

commented Oct 4, 2019

#569 isn't really realistic and is not on the docket for F# 5.0. That issue was formulated based on a misunderstanding of language defaults with the LangVersion flag.

@abelbraaksma

This comment has been minimized.

Copy link

commented Oct 4, 2019

I would agree with staying with known semantics. It is typically a bad idea to change semantics of previously chosen operators. Having ! meaning one thing here and a totally other thing there is not going to look well on the language and would be another barrier for learning the syntax.

Though there's the small chance someone has not declared as an active pattern (even though this should, by convention, start with a capital, so chances this breaks existing code is slim). Moreover, typically new operators and functions that are added to the Core library can be simply overridden. So unless someone would have, say, use System after their own declarations, the rare case where not as an active pattern is already defined will remain functioning as it was.

Though I do believe there is a case to be made for is as well. Not just for parity, but also because it currently requires a when-clause, I think, to match against an existing reference type (or did I misunderstand this parity thing with VB.NET?).

@isaacabraham

This comment has been minimized.

Copy link

commented Oct 5, 2019

I can understand some of the extra flexibility introduced by this (especially the not pattern), but I really don't buy into the idea that we should do this because "c# has it". Language features should be judged on merits of whether they are suitable for F# alone - I don't want F# to be a clone of C# (or vice versa), and a goal of adding features to F# shouldn't be (IMHO) that it will make migrating from C# easier / it will be less work to learn the language.

@realparadyne

This comment has been minimized.

Copy link

commented Oct 7, 2019

I find that using 'not' instead of '!' is s strength of F# as it is more clear when reading and easier to spot when scanning for the source of a bug, '!' is too easy to miss.

@colinbull

This comment has been minimized.

Copy link

commented Oct 7, 2019

You can sort of already do this...

let (|Not|_) a = if a then Some () else None

Then

match x with | Not _ -> printfn “Not %A” x | _ -> printfn “Is %A” x

@yatli

This comment has been minimized.

Copy link

commented Oct 7, 2019

shall we also consider pattern matching in if conditions?

if (o is T t) {
    // do something with t
}
@ReedCopsey

This comment has been minimized.

Copy link

commented Oct 7, 2019

@yatli Personally, I find type test patterns to frequently be signs of a poor underlying design (too much OO emphasis). I'd rather that pattern not end up in F# (esp. in a form that seems to encourage its use).

Do you see this as useful for any other patterns?

@yatli

This comment has been minimized.

Copy link

commented Oct 7, 2019

For example, a shortcut equivalent for:

match o with
| T(t) when t > 3 -> // do something with t
    ()
| _ -> ()

//==========

if o is T(t) && t > 3 then
    ()

//if type test is required, cast our usual spell:
if o is :? T as t then
    // do something with t

// edit: active pattern + if could be cool too
if kv is KeyValuePair(k, v) && k = 1 then ...

Pros:

  • quick tenary ops
  • explicit branch conditions, you can do if a is ... then ... elif b is ... then ... else ... with confidence knowing that b won't be matched against at the beginning.
  • active patterns in if could be beneficial for writing DSLs

Cons:

  • new keyword is
@abelbraaksma

This comment has been minimized.

Copy link

commented Oct 8, 2019

quick tenary ops

I don't spot ternary operators in your proposal, but other proposals with actual ternary ops (i.e., similar to cond ? trueval : falseval) have been discussed and considered a no-go, so it would fail the third affidavit ("it has already been decided").

with confidence knowing that b won't be matched against at the beginning.

I think you mean "again"? If so, I'm afraid the inverse is rather true. F# checks if your matches are complete, but cannot do so for if-statements. You could write if a is T(x) then ... elif a is T(x). With match expressions that would yield a warning.

active patterns in if could be beneficial for writing DSLs

Not sure what you mean here. I don't see why normal patterns, with exhaustion testing, isn't good for DSL's.

As an aside, there's an age-old proposal that has recently been resurrected (see discussion here: #222 and RFC draft here: fsharp/fslang-design#402). These follow your idea to some extend, but instead of turning a is into an operator, it follows the already existing, but hidden syntax that any DU inherently has (i.e., IsSome etc). It is possible that said proposal would cover your use cases already.

@yatli

This comment has been minimized.

Copy link

commented Oct 9, 2019

I don't spot ternary operators

if a is T then 1 else 2 vs. match a with | T -> 1 | _ -> 2, the former feels more fluent for me. Not that I'd like to introduce other tenary ops.

I think you mean "again"?

No :) I mean for if ... else ... it is very clear that the branch conditions will be checked sequentially, so if you want to check an active pattern twice because of concurrency/other reasons, you can do it.

Also, I'm always not very confident about how the compiler would arrange multiple match conditions.
In the most straightforward version, the matched expressions are first evaluated, and then the patterns are matched against them one-by-one. So if we have two match expressions (e.g. match f x, g y with ... ), first f x and g y will be computed, and then the patterns apply, even if the patterns contain wildcard. With if patterns, if we feel g y is too expensive to compute, we can first evaluate f x and proceed conditionally. Does that make sense to you?

Not sure what you mean here. I don't see why normal patterns, with exhaustion testing, isn't good for DSL's.

That's not my point. What I mean is that if we allow pattern matching in if conditions, we can reuse active patterns in if expressions:

let (|GOOD|BAD|) x = ...

if x is GOOD then ...

IsSome etc

could be convenient, but I think pattern matching in if has wider application (for example, active patterns).

Sorry if I'm off-topic, I can move this into a new proposal.

@abelbraaksma

This comment has been minimized.

Copy link

commented Oct 9, 2019

Sorry if I'm off-topic, I can move this into a new proposal.

That may be better, I agree.

Does that make sense to you?

It does, but nothing stops you from writing match f x with Foo -> match g y with Bar -> 1, which has the same benefit.

Don't forget that matching against DU's is (typically) a table-lookup in IL. Which means that it requires only one evaluation. With if, that would not be the case.

let (|GOOD|BAD|) x = ...
if x is GOOD then ...

And how would this work when the active pattern has multiple arguments?

But you may be onto an interesting idea, even though I am not convinced it is worth the effort, or that it solves enough use-cases to get traction. But I must admit that I too sometimes would have liked writing if [match expr matches] then, which I currently write as if (x |> function Foo -> true | _ -> false) then 42 else 0. Perhaps not as clear as (something similar to) if x is Foo.

But I digress, let's set up a different topic for this.

@dsyme

This comment has been minimized.

Copy link
Collaborator

commented Oct 10, 2019

My proposed resolution:

  1. Yes in principle for 'not' patterns (they were not really considered for F# 1.0/2.0 but really should have been)

  2. No to 'range' patterns (they were considered but F# 1.0/2.0 rejected, I don't see any real reason to change this)

  3. No to 'relational' patterns (they were considered for F# 1.0/20 but rejected, I don't see any real reason to change this)

  4. No to 'ternary operators', this kind of thing isn't particularly helpful for keeping F# simple

@dsyme

This comment has been minimized.

Copy link
Collaborator

commented Oct 10, 2019

#569 isn't really realistic and is not on the docket for F# 5.0. That issue was formulated based on a misunderstanding of language defaults with the LangVersion flag.

Nah, it was a misapplication of the defaults for the feature in the Visual Studio toolchain 😛

More seriously, it's really a shame we still don't have a way to deprecate anything even over 5 or 10 year timeframe, for freshly written code....

@pblasucci

This comment has been minimized.

Copy link

commented Oct 10, 2019

I’m mostly in agreement (as usual) with Don’s ... let’s call “taste” — especially with regards to ternary expressions. However, I’m curious what benefits are imparted from a specific syntax for negation in matching (as opposed to just defining a partial active pattern)? Is there some optimization the compiler couldn’t otherwise perform?

@dsyme

This comment has been minimized.

Copy link
Collaborator

commented Oct 10, 2019

I’m mostly in agreement (as usual) with Don’s ... let’s call “taste” — especially with regards to ternary expressions. However, I’m curious what benefits are imparted from a specific syntax for negation in matching (as opposed to just defining a partial active pattern)? Is there some optimization the compiler couldn’t otherwise perform?

IIUC not patterns are not boolean negation, but rather they say "this pattern match must fail", e.g.

match inp with
| A x -> x
| B x -> x
| !(A _ | B _) -> 0

Thus they interact with completeness checking, for example. I first saw them in "Alice ML" though I'm not sure where they existed before that

@pblasucci

This comment has been minimized.

Copy link

commented Oct 10, 2019

Ahh... thanks, @dsyme. That actually makes perfect sense. Cheers!

@charlesroddie

This comment has been minimized.

Copy link

commented Oct 11, 2019

@yatli shall we also consider pattern matching in if conditions?
if (o is T t) {... }
Sorry if I'm off-topic, I can move this into a new proposal.

Already exists here: #705

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.