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
Enable deconstruction syntax in :? patterns #830
Comments
Similar to #828: I think it's bad, outside of the compiler, to make type deconstruction more convenient. If you are doing this it's clear that the type system has failed. Or, in the case of the compiler, is under construction. @smoothdeveloper can you think of any example outside the compiler when it would be good to write code using this syntax? The one case you suggested (elmish) had a "use with caution" warning and I would strengthen that to "do not use". |
@charlesroddie elmish and also actor code are valid use cases, I wrote "use with caution" in sense of "don't expect exhaustivity check from compiler". I don't think it should be forbidden as the syntax makes it clear ( Without the feature, it is practically impossible to have a pattern match that would still fall through the remaining cases but to resort on active patterns that aren't going to be reused elsewhere in the code and may lead to more obfuscated code for the purpose at hand. An alternative syntax would be to force using / reusing let f =
function
| :? DU when A(1) -> "a1"
| :? DU when A(b) -> sprintf "other a%i" b
| :? Rec when {a=1;b=b} -> "rec1"
| :? Ex when Ex(1) -> "ex1"
| _ -> "?" Without supporting this, a whole class of code that exist and is expressible in languages supporting more flexible pattern matching won't be translatable to F#, people will try to adopt F# coming from such languages and wonder why there is no escape hatch beside active pattern contortions (deviating those from their intent of handling non destructurable types). |
What's wrong with matching on the type with I'd argue in general, that when you need to match over types, and it isn't to match polymorphism, that there seems to be something wrong with the design of your code, as you essentially write code where you say 'this part is not type safe, let's try to find something we can use here'. Though admittedly, there are certainly cases where type tests are a necessary evil. I do see (some) benefit in your proposal, as to less typing, but your post suggests that certain scenarios are now impossible or very hard to do. However, I fail to understand what is currently impossible (you say it's now restricted to exception types, I don't see that either). You write "it is practically impossible to have a pattern match that would still fall through", but if a type test fails, it won't fall through, it'll try the next type test,until it reaches the end. I may be missing the obvious here, just trying to get a firm grasp of your proposal. |
The same could be said for a lot of features in F#. People coming from C# are used to implicit conversions. In F#, all of a sudden these don't exist anymore. It takes time to adopt to the new coding style. When I see students attempting F# they use casts everywhere, and complain about it being so cumbersome, until they start embracing DU's for a better fit, and finally enjoying the benefits of both type safety and better inference. I'm not saying your suggestion is bad (I haven't understood it yet), but mimicking another language may not be the proper argument here ;) |
The suggestion is not really about mimicking the other languages, but I wanted to put the notice that languages have more flexible/lenient pattern matching handling now, or will in nearish future; in few years, a majority of developers will have gained exposure to pattern matching and some constructs won't be translatable. In my sample, say I'm only interested in In those scenarios, nesting F# already has the constructs of the suggestion baked in, just restricted to Here is the same sample without the suggestion: let inline fallback () = "?"
function
| :? DU as du ->
match du with
| A(1) -> "a1"
| A(b) -> sprintf "other a%i" b
| _ -> fallback ()
| :? Rec as record ->
match record with
| {a=1;b=b} -> "rec1"
| _ -> fallback ()
| :? Ex as ex ->
match ex with
| Ex(1) -> "ex1"
| _ -> fallback ()
| _ -> fallback () Hope that explains the crux of it. Regarding casts, no early returns, etc. there are idiomatic escape hatches. |
Added a potential warning/error that could happen when type tests among a same non polymorphic deconstructible type happen to be interleaved instead of done in a straight sequence. Thanks for the feedback so far, I hope my explanations and samples clarify a bit. |
Making |
Note that As for the original suggestion, I think I do like it. Since the use of |
Isn't I'd say It seems better to have upcast and downcast be real functions that could be used like /offtopic |
Wouldn't a function for the :? operator be named typeOf? At least, that's how I read it. |
@heronbpv it's more or less the equivalent of the |
If the pattern(s) you want to match against allow the relevant types to be inferred, you can use a relatively generic active pattern. Here's the example function:
|
@glchapman, that's a pretty nifty approach! (I deleted my prev comment, it had the wrong assumptions). I just checked how it actually compiled. And the code is not too bad, it actually translates mostly to how matches translate. But since it is a partial active pattern, the compiler will not combine tests. I.e.: If you had let f = function
| {a = 1; b = b } -> printfn "Record 1, b = %s" b
| {a = 2; b = b } -> printfn "Record 2, b = %s" b
| {a = 3; b = b } -> printfn "Record 3, b = %s" b that'll end up as a single let f = function
| Is {a = 1; b = b } -> printfn "Record 1, b = %s" b
| Is {a = 2; b = b } -> printfn "Record 2, b = %s" b
| Is {a = 3; b = b } -> printfn "Record 3, b = %s" b will end up as three calls to I'm not saying it is a bad idea, it isn't, I like it and never thought of using active patterns in that way. But still there's room for adding this to the |
Yeah, it would be nice if the compiler was a little smarter with active patterns. Here's another possibility which seems to compile better, but requires some boilerplate UnionXX types:
FWIW, the proposed change would not be a high priority for me, but if something like it were to be made, I think it would be better to extend the & pattern so that patterns on the right could take advantage of an extended environment established by a successful match on the left. E.g.:
The A(1) on the right would be allowed because the type test on the left appropriately narrowed the target type. |
Currently, I'm also having this issue! I post it on stack overlow as "inline Type Testing Pattern with Record Pattern": I'll be more involved here than I was there, as you're looking for a more valid scenario: In my case, I have an SDK that is wrapping an API, to avoid a humongous tree of DUs to represent each possible request, what I did was creating an interface type IGERequestV2 =
{ From: Agent
ExternalID: System.Guid
Props: ISDKRequest
} The SDK also returns a response, which again is a generic: type IGEResponseV2 =
{ Request: IGERequestV2
Response: HttpResponse } And my sagas can decide what to do when toy see an SDKResponse: let choose =
function
| { Response = { StatusCode = statusCode; Body = body };
Request = { Props = (:? UserNode.Media.GET.Props as props)) } }
when statusCode = 200 ->
match props with
.... This way I make choices based on the request, as the response is just a generic HTTP Response. Of course, today I nest another |
This syntax would be amazing, I asked the other day why the I tried to do this a lot in the cases of nested options until I discovered that you can just match tuples. However, one of the answers I got stated:
seeing the nested pattern syntax, and the tuple workaround for, and the "expression oriented" nature of the language, I say that I'd prefer the option of: let f =
function
| :? Rec ({a=1;b=b}) -> "rec1"
| _ -> "?" This way the Type Test Pattern is indistinct of the normal Identifier Pattern which is what I'm looking for, treating subtypes as Identifiers (like it would do in a DU), and be able to match on the right expression |
A related comment #751, in general, is very related |
Now that we have Anonymous typed-tagged union RFC, this suggestion would be a complement to that feature. |
That would reduce a lot of boilerplate and make use of true DU wider. As now you usually have to create DU with cases of a single record 😥 |
We added patterns on the right of type DU = A of int | B of string
type Rec = {a: int; b: string }
exception Ex of int
let f (x: obj) =
match x with
| :? DU as A(1) -> "a1"
| :? DU as A(b) -> sprintf "other a%i" b
| :? Rec as {a=1;b=b} -> "rec1"
| :? exn as Ex(1) -> "ex1"
| _ -> "?" So this is good enough |
I propose we enable usual deconstruction syntax in
:?
matches:There is no exhaustivity check, but the existing requirement for a fallback case as a warning.
A warning (or an error) could be added if type tests among several cases of same DU or record types are separated with other non related cases.
Approximate ways of approaching this problem in F#:
exception
types, as done in the compiler itself https://github.com/dotnet/fsharp/blob/70883fb00a867f6e81aa9c7cecdd9212c4e78d93/src/fsharp/ErrorLogger.fs#L400-L407WrappedError
to a single case DU in the compiler code and reimplement the match with the same cases evaluated in same orderthe equivalent code right now:
Pros and Cons
The advantages of making this adjustment to F# are:
exception
types to achieve similar aimupdate
function scenarios when handling heterogeneous commands instead of nesting DUs (use with caution)The disadvantages of making this adjustment to F# are:
Extra information
Estimated cost (XS, S, M, L, XL, XXL): M
Affidavit
Please tick this by placing a cross in the box:
Please tick all that apply:
The text was updated successfully, but these errors were encountered: