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

Allow pattern matching for exceptions in match expressions #895

Open
5 tasks done
ebresafegaga opened this issue Jul 20, 2020 · 18 comments
Open
5 tasks done

Allow pattern matching for exceptions in match expressions #895

ebresafegaga opened this issue Jul 20, 2020 · 18 comments

Comments

@ebresafegaga
Copy link

ebresafegaga commented Jul 20, 2020

I propose we allow pattern matching for exceptions in match expressions when the expression to be matched is a function call (or indexer/ property)
For example,

let l = [1;2;3]

let printer i = 
    match l.[i] with 
    | n -> printfn "Item: %d" n
    | exception ArgumentException -> printfn "Invalid index"
    | exception e -> printfn "An error unknown occurred: %s" e.Message

printer 10

The existing way of approaching this problem in F# is :

     let l = [1;2;3]

let printer i = 
    try 
        match l.[i] with 
        | n -> printfn "Item: %d" n
    with 
    | :? ArgumentException as e -> printfn "Invalid index"
    | e ->  printfn "An error occurred: %s" e.Message

Even though this isn't bad I think the former is clearer, more succinct and even easier to read (say, a new team member)

Pros and Cons

The advantages of making this adjustment to F# are
1. Expressiveness
2. Clear and concise code (favoring readers over writers)
3. It Improves pattern matching in general
4. It's a nice and beautiful syntax
5. It's intuitive
6. Efficient code via tail recursion

The disadvantages of making this adjustment to F# are
1. Implementation costs in the compiler
2. Multiple ways on doing the same thing
3. Might not be familiar to developers from other ecosystems (e.g C#) just learning F#

Extra information

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

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

I like this idea, but it would be even nicer if the completeness checker could use information of the function to help telling if we miss an exception (something like Java has had for a long time). That would require adding raising exceptions to the meta data, or tagging functions with something like RaisesAttribute (in case it cannot be determined statically easily).

The same completeness check for exceptions could be added to try/with.

Maybe the rule ought to be that all exception cases should come last. I wonder how the same syntax should be used in conjunction with F# exceptions (the ones you can already match over).

@Tarmil
Copy link

Tarmil commented Jul 20, 2020

It should be noted that OCaml has this exact feature.

@abelbraaksma
Copy link
Member

abelbraaksma commented Jul 20, 2020

The argument in that post that it helps with tail recursion inside a try is a good one. 👍

@ebresafegaga, you may want to add that to the pros, it's a strong argument, I think.

@ebresafegaga
Copy link
Author

The argument in that post that it helps with tail recursion inside a try is a good one.

@ebresafegaga, you may want to add that to the pros, it's a strong argument, I think.

Thanks, done!

@dsyme
Copy link
Collaborator

dsyme commented Aug 8, 2020

A more orthogonal (and simpler to imlement) variation is that what follows exception is a standard exception pattern, e.g.

let printer i = 
    match l.[i] with 
    | n -> printfn "Item: %d" n
    | exception (:? ArgumentException as e) -> printfn "Invalid index" e.ArgumentName
    | exception e -> printfn "An error unknown occurred: %s" e.Message

This is important for two reasons

  1. It doesn't introduce a new category of patterns like "ArgumentException" (though that could conceivably be a separate, orthogonal suggestion, applicable to try/with as well)

  2. It is often necessary to bind exceptions at their more specific types. Note that in F# exceptions are actually types, whereas in OCaml the are a different category of tags (you can't use an exception tag as a type).

BTW I'm sold on this feature solely because it helps people write more accurate code where the exception handling doesn't cover too many things. I don't think it's particularly beautiful nor for beginners - it's for more advanced users

@dsyme
Copy link
Collaborator

dsyme commented Aug 8, 2020

BTW those interested in programming language archeology may want to know that AFAIK the first people to propose this kind of construct were Nick Benton and Andrew Kennedy at Microsoft Research (though they integrated with let not match). I'm not totally sure the OCaml people are even aware of that :)

https://www.cs.tufts.edu/~nr/cs257/archive/nick-benton/exceptional-syntax.pdf

@dsyme
Copy link
Collaborator

dsyme commented Aug 8, 2020

I marked this approved in principle, subject to this comment #895 (comment)

I'm not planning to work on it myself though may be able to give some guidance. I don't think it would be too hard to implement

@ebresafegaga
Copy link
Author

A more orthogonal (and simpler to imlement) variation is that what follows exception is a standard exception pattern, e.g.

let printer i = 
    match l.[i] with 
    | n -> printfn "Item: %d" n
    | exception (:? ArgumentException as e) -> printfn "Invalid index" e.ArgumentName
    | exception e -> printfn "An error unknown occurred: %s" e.Message

This is important for two reasons

1. It doesn't introduce a new category of patterns like "ArgumentException"  (though that could conceivably be a separate, orthogonal suggestion, applicable to try/with as well)

2. It is often necessary to bind exceptions  at their more specific types.  Note that in F# exceptions are actually types, whereas in OCaml the are a different category of tags (you can't use an exception tag as a type).

BTW I'm sold on this feature solely because it helps people write more accurate code where the exception handling doesn't cover too many things. I don't think it's particularly beautiful nor for beginners - it's for more advanced users

I totally agree with you.

@ebresafegaga
Copy link
Author

BTW those interested in programming language archeology may want to know that AFAIK the first people to propose this kind of construct were Nick Benton and Andrew Kennedy at Microsoft Research (though they integrated with let not match). I'm not totally sure the OCaml people are even aware of that :)

https://www.cs.tufts.edu/~nr/cs257/archive/nick-benton/exceptional-syntax.pdf

Awesome! A lot of great ideas have come from MSR. I'll definitely check this out.

@ebresafegaga
Copy link
Author

I'd very much like to help with implementing this. For a while now I've been trying to hack on the F# compiler, but it seemed so daunting. So I think this would be a good opportunity for me to do that and I would definitely need some guidance. Thanks!.

@cartermp
Copy link
Member

@ebresafegaga The next step would be an RFC here: https://github.com/fsharp/fslang-design

Although starting off with some coding isn't a bad idea either, it's often a good idea to flesh out a preliminary design before diving into a trial implementation. You can look at RFCs like https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1090-Generic-struct-type-whose-fields-are-all-unmanaged-types-is-unmanaged.md as an example of the level of detail.

@kerams
Copy link

kerams commented Aug 21, 2023

There's an important difference between the 2 snippets. The second one catches exceptions in when guards, clause expressions and active patterns, whereas the first one, supposedly, doesn't.

@Thorium
Copy link

Thorium commented Mar 10, 2024

Exception handling shouldn't be part of your typical program flow.

Would this encourage writing manual exception handling more, when the business logic program flow should be handled without rising exceptions?

Typically F# attitude has been failwith, fail fast and hard, don't try to recover.

I know there are libraries raising exceptions like business-as-usual, but that will not justify this, just like C# libraries returning nulls don't justify C# becoming language optimized for null-handling: more and more keywords to make anti-patterns easier.

Not only performance implications, exception-handling is like goto-clauses causing random not-clear paths between nodes of your program. Besides that they make debugging .NET harder, when you can't just break on each CLR exception.

@smoothdeveloper
Copy link
Contributor

I think the feature should mention that it works the same for the function keyword, just in case this would not be obvious.

@Thorium while I think vanilla f# we love doesn't lean strongly on exception declaration nor finegrained handling via try/with; in context of larger integration with .net idioms, the richness and expressivity of f#, for this type of interop (many f# usage also happens in mixed dotnet codebases), doesn't remove to the soundness of the core of the language, when it stands mostly on its own.

There are trade-offs, but f# and I assume ocaml earlier on, endorsed usage of exceptions, unlike go, rust, some dialect of c++.

Having this supported, doesn't preclude to, one day, having an f# checker / attribute, that would enable an "pure exceptionless f#" ecosystem (for soundness or performance reasons), but keeping in mind that f# is foremost used on runtimes that endorse usage of exceptions, and that there are wins in expressivity with this feature, while still making exception keyword very very visible, for this suggestion.

You may be interest in my point about exceptions, in this feature suggestion: #830.

@smoothdeveloper
Copy link
Contributor

Side note, maybe f# semantic colouring should have something similar to mutable, around all constructs related to exceptions (failwith functions, exception types, try/with/finally)?

@realparadyne
Copy link

@smoothdeveloper

I think the feature should mention that it works the same for the function keyword, just in case this would not be obvious.

Is that correct? In the case of function isn't it too late at that point? e.g. if I pipe something into a function the compiler can't at that point wrap a try..with around whatever generated the value. But in the case of match expr with it can transform it into a try..(match expr with)..with and split the exception and non-exception cases between the two with sections.

Unless what you're piping into the function is a Result type that has a value or an exception and then you could have it unwrap everything from that.

@smoothdeveloper
Copy link
Contributor

@realparadyne you are right for the match input! That said, @kerams comment also gave me some food for thoughts, I think the exception occuring on when beside the match input, should also be covered; in my understanding, this would apply to function.

Those nuances make me a bit uncomfortable along same line as @Thorium, but still think, the construct for total match, including any exception that occurs in evaluation, from top to bottom, in a single scope has value for expressivity, enough to not down vote the issue.

I think the RFC should put emphasis on aspects pertaining to how introducing exception patterns interacts with�"total match" checks that are in the current version of the language, and maybe considering #731 as well.

Aside, what about match!?

@brianrourkeboll
Copy link

This could be interesting for working with BCL/.NET-style exceptions when combined with the object/property patterns from #968, which would address (1) in Don's comment above #895 (comment).

match f x with
| y -> Ok y
| exception SomeException (InnerException = null) as e
| exception SomeException (InnerException = e)
| exception e -> Error e
match f x y with
| z ->| exception ArgumentException (Message = m, ParamName = "x") ->| exception ArgumentException (Message = m, ParamName = "y") ->| exception e ->

Ideally we might also want to try to do #957 at the same time, so that we could avoid a mismatch in pattern syntax between OCaml-style exceptions (which currently require semicolons if you want to use named patterns to deconstruct their arguments) and BCL/.NET/class-style exceptions.

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

10 participants