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

Comparison pattern #661

Closed
5 tasks done
jwosty opened this issue Apr 20, 2018 · 17 comments
Closed
5 tasks done

Comparison pattern #661

jwosty opened this issue Apr 20, 2018 · 17 comments
Labels

Comments

@jwosty
Copy link
Contributor

jwosty commented Apr 20, 2018

Title of Suggestion

I propose we add a new first-class pattern that tests comparison on appropriate objects. For example, I'd love to be able to write code like the following, without using getting an incomplete match warning or resorting to wildcards:

let (|Negative|Zero|Positive|) = function
    | 0 -> Zero
    | x when x > 0 -> Positive
    | x when x < 0 -> Negative

I'd love if there were a built-in pattern that accomplishes this. Simply using | (x > 0) -> ... would not be possible because it is too restrictive*. So, I present a few possible syntaxes:

| x where x > 0 -> ...
| x if x > 0 -> ...
| x foreach x > 0 -> ...
| x forall x > 0 -> ...
| (x >? x > 0) -> ...

Whatever the syntax, the feature should be able to be used with more complex patterns of course. Ideally, if possible, it should support comparing bound values to each other, not just literals, as in:

| (a, b) where (a > b) -> ...

This pattern should support any IComparable, and each of the associated operations -- >, <, and '=' (even though the last one is just equivalent to a direct pattern). Perhaps it should also support AND and OR, like so:

| (a, b) where (a > 0 && a > b) -> ...

Though this may make it too complicated for the compiler to check (maybe just AND? or neither of these?)

* It would make it impossible to support comparing two names to each other, so it really has to be a different thing altogether

Pros and Cons

This would be really nice because it adds more compile time safety in these circumstances.

The disadvantages are that this would be yet another pattern for people to keep track of.

Extra information

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

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • 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
@abelbraaksma
Copy link
Member

While I appreciate the suggestion and I've more than once been stymied by the fact that the compiler cannot check for completeness in trivial cases, it's less obvious than it seems. The first thing that comes to mind is transitivity. It is natural to think that if A <= B is true then A > B must be false.

But it isn't. While for integers there are corner cases at or around overflow situations (think A - 1 > B, where A = Int32.MinValue), but they could be reasoned with. It becomes much harder when you need to consider that A <= B and A > B are both false if one of them is NaN. And when types are converted, rounding errors and the effect of the smallest possible increment of doubles and floats may yield hard-to-detect (though stable) non-transitivity issues.

And then there's the situation of overloading operators, or redefining existing operators, in these cases, transitivity bets are totally off.

Another area to look at is that two values can be binary equal, but unequal to each other. Or they can be binary unequal, but equal for the equality operator. And then there's the question of the implementations of IEqualityComparer and IComparable and the likes.

I'm not saying it's impossible what you are suggesting, but it really requires rigorous thinking as to what sets of rules are considered complete. And there's one thing you can already do, as you show, and that is to help the compiler by creating complete active patterns (as opposed to partial ones).

@0x53A
Copy link
Contributor

0x53A commented Apr 21, 2018

  1. I think this feature would be useful enough for primitive types where the compiler knows the exact semantics (int, float, ...) without extending it to custom overloads.

  2. It becomes much harder when you need to consider that A <= B and A > B are both false if one of them is NaN. And that is why I would love it if the compiler told me Match is incomplete, consider NaN.

@abelbraaksma
Copy link
Member

@0x53A actually, that would be the inverse. Perhaps "matches are complete but no special test for NaN detected". Which is precisely why this discussion is worthwhile, even if it's only to find out what we, as programmers want to be warned about, and how to reach that deterministically, but also, that we have to be careful what to wish for ;)

@jwosty
Copy link
Contributor Author

jwosty commented Apr 21, 2018

Interesting points, I didn't even think about NaN. That's why I asked this question -- I knew there would be some things I'm missing and some good ideas from the community! :) I agree though in it would probably be best for the compiler to remind you of this case. A minor question comes to mind, though: should the warning be different than a normal "incomplete pattern match exception", so that it is easy to selectively not care about this case if you want to accept the responsibilities?

And @abelbraaksma mentions active patterns (though not in the way I was originally thinking -- good stuff). This can pretty much already be implemented in user code, just without infix syntax:

// just a quick PoC as this can definitely be improved; i.e. we probably want to know _which_ number the NaN is
let (|LT|EQ|GT|NaN|) (a, b) =
    if System.Double.IsNaN a || System.Double.IsNaN b then NaN (a, b)
    else if a < b then LT (a, b)
    else if a = b then EQ (a, b)
    else GT (a, b)

// sample usage: 
let printCmp a b =
    match a, b with
    | LT (a, b) -> sprintf "%f < %f" a b
    | EQ (a, b) -> sprintf "%f = %f" a b
    | GT (a, b) -> sprintf "%f > %f" a b
    | NaN (a, b) -> sprintf "one of (%f, %f) is NaN" a b

Perhaps a good option (and would also be much larger in scope) would be to invent infix operator active patterns. That way, these rules wouldn't be hardcoded into and dictated by the compiler, and could be switched out for different rules at will (like how the only thing special about ?, which doesn't even come with an implementation by default, is its syntactic sugar). An implementation of this would appear in library code and could look similar to this:

let (|op_LessThan|op_Equality|op_GreaterThan|NaN|) (a, b) =
    if System.Double.IsNaN a || System.Double.IsNaN b then NaN (a, b)
    else if a < b then op_LessThan (a, b)
    else if a = b then op_Equality (a, b)
    else op_GreaterThan (a, b)

@dsyme
Copy link
Collaborator

dsyme commented May 29, 2018

Just to mention that the original suggestion has been made before and declined, so we won't be doing it. But I'll leave this open for further discussion

@dsyme dsyme added the declined label May 29, 2018
@pavelbraginskiy
Copy link

The language Prolog is pretty much entirely based around this issue.

@cartermp
Copy link
Member

@dsyme do you recall the rationale for declining this in the past?

@dsyme
Copy link
Collaborator

dsyme commented Oct 29, 2019

.net has a standard in this area and we use that. Also, ocaml used integers too

@Happypig375
Copy link
Contributor

A standard in this area? Can you explain?

@abelbraaksma
Copy link
Member

abelbraaksma commented Oct 30, 2019

No idea what he means, but there are so many variations possible for each type... , see my comment.

Though in your own code it's trivial to create an active pattern that operates on comparable types, so that's perhaps another reason why they don't want to increase the surface area of core functions. But I'm guessing ;).

@dsyme
Copy link
Collaborator

dsyme commented Oct 30, 2019

.net has a standard in this area and we use that. Also, ocaml used integers too

Oh I see, the proposal is to add an active pattern, rather than a new type with cases LT|EQ|GT. I was referring to the fact that .NET uses integers for comparison function return types

In any case, I don't think we'll add this to the standard library. The user can define such a pattern if needed

@jwosty
Copy link
Contributor Author

jwosty commented Oct 30, 2019

@dsyme To clarify, this proposal is not just to add an active pattern, but a new language-level pattern: as it stands, match expr with | (a, b) where (a > b) -> ... cannot be valid syntax. Basically I would really like to be able to use that syntax one way or another.

If the objection is that language doesn't want to take an opinion on the nuances of the behavior of such a pattern, perhaps a sane modification would be to make this pattern syntactic sugar for a user-definable active pattern, like how the language recognizes the sugar for the ? operator but delegates implementation to the user or a library.

@dsyme
Copy link
Collaborator

dsyme commented Oct 31, 2019

Oh I see. Sorry for not reading properly again - too hasty.

This isn't something we're likely to do - it's a feature that has been considered since pattern matching was invented, and borders on the whole field of constraint solving for tools like Z3 - there is basically no end to the amount of logic power we could pour into checking these kind of constraints. This also means there is is no end to the subtlety the programming team may need to be aware of about how this logic checking power is being used.

@abelbraaksma
Copy link
Member

as it stands, match expr with | (a, b) where (a > b) -> ... cannot be valid syntax

@jwosty If you simply replace where with when it's valid. I assume you knew that already, so the suggestion you make would be more about adding completeness checking is the compiler encounters boolean comparisons that cover the whole set of inputs.

So, the downside currently is that you don't have completeness checking. But you can put the three (or four) cases in an active pattern, this will infer that the types must be comparable, and you have what you want to have: completeness checking and generics. You can even create the type to support multiple arguments per case.

@jwosty
Copy link
Contributor Author

jwosty commented Oct 31, 2019

@abelbraaksma yes, that about sums it up.

@abelbraaksma
Copy link
Member

abelbraaksma commented Oct 31, 2019

Perhaps as an improvement on your code from Apr 12, 2018, you can make it a tad more readable and/or usable with this:

let (|LT|EQ|GT|NaN|) (a, b) =
    match a, b with
    | _ when a > b -> GT
    | _ when a = b -> EQ
    | _ when a < b -> LT
    | _ -> NaN

let f a b = 
    let print x = printfn "Value %A is %s %A" a x b
    match a, b with
    | LT -> print "less than"
    | EQ -> print "equal to"
    | GT -> print "greater than"
    | NaN -> print "NaN"

This approach has the advantage that the type is inferred to support anything that is comparable, and treats NaN correctly. It also has the advantage that you don't need to capture on each match branch, as I had the feeling that doesn't aid readability. This way (without the arguments) it is much clearer what is going on.

The op_GreaterThan won't compile because cases must start with an uppercase letter. And we cannot turn it into a function (as in let (|LT|EQ|GT|NaN|) a b = ...), though that would be cool, as then we could write | LT 12.0 or something (but of course, even if it where allowed, it is impossible to completeness-check again).

Oh, and your original code could become as generic as possible given the requirement by doing this (granted, this isn't obvious):

let inline (|Negative|Zero|Positive|NaN|)< ^t  when ^t: (static member Zero: ^t) and ^t : comparison> (x: ^t)  = 
    match x with
    | x when x = LanguagePrimitives.GenericZero -> Zero
    | x when x > LanguagePrimitives.GenericZero -> Positive
    | x when x < LanguagePrimitives.GenericZero -> Negative
    | _ -> NaN

Just my 2p ;)

@cartermp
Copy link
Member

cartermp commented Dec 8, 2019

Closing as declined

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants