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

negation patterns #793

Closed
3 of 5 tasks
Happypig375 opened this issue Oct 3, 2019 · 34 comments
Closed
3 of 5 tasks

negation patterns #793

Happypig375 opened this issue Oct 3, 2019 · 34 comments

Comments

@Happypig375
Copy link
Contributor

Happypig375 commented Oct 3, 2019

I propose we add negation patterns, that is patterns that say "this pattern match must fail", e.g.

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

These interact with completeness checking, for example. Variables cannot be bound inside the negation pattern.

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 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:

  • 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
Copy link
Contributor Author

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
Copy link
Member

cartermp 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
Copy link

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
Copy link
Contributor Author

Happypig375 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
Copy link
Member

cartermp 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
Copy link
Contributor Author

! 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
Copy link
Member

cartermp 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
Copy link
Member

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
Copy link

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
Copy link

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
Copy link

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
Copy link

yatli commented Oct 7, 2019

shall we also consider pattern matching in if conditions?

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

@ReedCopsey
Copy link

ReedCopsey 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
Copy link

yatli 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
Copy link
Member

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
Copy link

yatli 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
Copy link
Member

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
Copy link
Collaborator

dsyme 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
Copy link
Collaborator

dsyme 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
Copy link

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
Copy link
Collaborator

dsyme 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
Copy link

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

@charlesroddie
Copy link

charlesroddie 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

@dsyme dsyme changed the title Pattern matching: Parity with C# 9 negation patterns Sep 9, 2021
@dsyme
Copy link
Collaborator

dsyme commented Sep 9, 2021

I've renamed this to negation patterns.

I've never used a language with negation patterns and while I appreciate their declarative nature I'm curious if they actually make pattern matching simpler to read and understand. Or does the person reading the code have to invert the negation in their head?

@dsyme
Copy link
Collaborator

dsyme commented Sep 9, 2021

I mean it feels like something really explicit may be better:

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

Also, we have to be aware negation of matching is pretty corrosive w.r.t. software engineering - the equivalent of partial _ wildcard patterns. Is it even a good thing in balance?

@Happypig375
Copy link
Contributor Author

It's useful in branch reordering and indentation elimination.

match i with
| 1 ->
   match j with
   | 2 -> ...
   | _ -> ()
| _ -> ()
match i with
| not 1 -> ()
| 1 ->
match j with
| not 2 -> ()
| 2 -> ...

@dsyme
Copy link
Collaborator

dsyme commented Sep 9, 2021

It's useful in branch reordering and indentation elimination.

Yes, it's useful, but is it easy to read and understand? I'm not at all sure

@Happypig375
Copy link
Contributor Author

What about eliminating the need to define inverse active patterns for existing active patterns?

@dsyme
Copy link
Collaborator

dsyme commented Sep 9, 2021

@Happypig375 I'm happy for us to list out the advantages. The question I'm raising is the different - that is, is it simpler to read and understand for the non-code author, the beginner? It might be convenient for the code author, and hard to understand for everyone else.

Pattern matching already contorts the mind - it makes people read code as "decomposition of information" rather than "production of information" (expressions). That's intrinsic, though I'm very aware of this when I look at pattern matching in C#, for example.

Negations also contort the mind - they make you look at program text "the opposite way around". I find reading negations of boolean logic really difficult.

Putting these together really concerns me - we'd be adding a second level of mind contortion and people might use it because it's cute.

I'd really like to understand how we can get evidence about that. And it would aboslutely motivate something really explicit lile not match (which is how I say the construct when reading it aloud - a useful sign).

@charlesroddie
Copy link

charlesroddie commented Sep 9, 2021

@Happypig375 your 2 examples aren't equivalent. An example of equivalent code in 2 syntaxes where there is an advantage of using not would be useful.

Edit... unless you meant that putting match j in the second example actually is only reached in the 1 match for i, in which case the second example should really give an error for incorrect indentation.

@Happypig375
Copy link
Contributor Author

Hmm, I'll come back when I see that not-patterns really add readability.

@charlesroddie
https://github.com/dotnet/fsharp/blob/699291aab8a3b5e877d9482cdf1c83808bdd8a9f/src/fsharp/LexFilter.fs#L932
Match clauses can have their contents exactly aligning.

@charlesroddie
Copy link

You can currently do this without a warning:

match 0 with
| 1 -> 2
| _ ->
3

But I think it's a bug and should be changed to give an indentation warning on 3. I don't see how your link is relevant.

@dsyme
Copy link
Collaborator

dsyme commented Sep 9, 2021

But I think it's a bug and should be changed to give an indentation warning on 3. I don't see how your link is relevant.

This is by design - several constructs allow linear non-indentation by choice, e.g.

// non-indenting conditional "early return"
if e1 then e2 else
e3

// non-indenting matching 
match e1 with
| p1 -> e2
| _ ->
e3

// non-indenting SQL-like comprehensions for query syntax
for x in foo do
for y in goo do
something

@dsyme
Copy link
Collaborator

dsyme commented Apr 12, 2023

My proposed resolution was here: #793 (comment)

However, in retrospect I still am deeply concerned that negation patterns "flip the mind" in awkward ways that make code much harder to read and understand, even if theoretically more maintainable. I don't know what it is, but as soon as I read a negation pattern my heart rate jumps and I feel stressed, like my brain needs to start thinking the other way around, negatively instead of forwardly.

Given this, and the very large implementation cost, I think I'm just going to close this whole issue. There are some other suggestions to enforce exhaustive matching on particular types and those seem better I think

@dsyme dsyme closed this as completed Apr 12, 2023
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