fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
344 stars 21 forks source link

Allow pattern matching for exceptions in match expressions #895

Open ebresafegaga opened 4 years ago

ebresafegaga commented 4 years ago

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:

Please tick all that apply:

abelbraaksma commented 4 years ago

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 commented 4 years ago

It should be noted that OCaml has this exact feature.

abelbraaksma commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

I marked this approved in principle, subject to this comment https://github.com/fsharp/fslang-suggestions/issues/895#issuecomment-670927823

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 1 year ago

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 commented 8 months ago

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 commented 8 months ago

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: https://github.com/fsharp/fslang-suggestions/issues/830.

smoothdeveloper commented 8 months ago

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 commented 8 months ago

@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 commented 8 months ago

@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 https://github.com/fsharp/fslang-suggestions/issues/731 as well.

Aside, what about match!?