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

Anonymous DUs #728

Open
cmeeren opened this Issue Mar 28, 2019 · 5 comments

Comments

Projects
None yet
3 participants
@cmeeren
Copy link

cmeeren commented Mar 28, 2019

Anonymous DUs

I propose we add anonymous discriminated unions to F#. This could, among other things, allow for more simple error handling.

Example (I have no strong opinion on the syntax, the below is inspired by the anonymous record syntax):

/// Divides two positive numbers or returns an error.
// int -> int -> Result<int, {| DivByZero | NeitherPositive of int * int | DividendNotPositive of int | DivisorNotPositive of int |}>
let dividePositive dividend divisor =
  if divisor = 0 then Error {| DivByZero |}
  elif dividend <= 0 && divisor <= 0 then Error {| NeitherPositive (dividend, divisor) |}
  elif dividend <= 0 then Error {| DividendNotPositive dividend |}
  elif divisor <= 0 then Error {| DivisorNotPositive divisor |}
  else Ok (dividend / divisor)

/// Divides two positive numbers or returns a more simple error than dividePositive.
// int -> int -> Result<int, {| DivByZero | ArgsNotPositive |}>
let simpleDividePositive dividend divisor =
  match dividePositive dividend divisor with
  | Ok x -> Ok x
  | Error {| DivByZero |} -> Error {| DivByZero |}
  | Error {| DividendNotPositive _ | DivisorNotPositive _ | NeitherPositive _ |} ->
      Error {| ArgsNotPositive |}

/// Logs the result of dividing two positive numbers
let tryDividePositiveAndLog dividend divisor =
  match simpleDividePositive with
  | Ok x -> printfn "Result: %i" x
  | Error {| ArgsNotPositive |} -> printfn "At least one argument was negative"
  | Error {| DivByZero |} -> printfn "Proper error handling just saved the universe

The existing way of approaching this problem in F# is by declaring the types:

type DividePositiveError =
  | DivByZero
  | NeitherPositive of int * int
  | DividendNotPositive of int
  | DivisorNotPositive of int

let dividePositive dividend divisor =
  // ...

type SimpleDividePositiveError =
  | DivByZero
  | ArgsNotPositive

let simpleDividePositive dividend divisor =
  // ...
  // here we'll have to qualify DividePositiveError.DivByZero when matching
  // due to name collision

let tryDividePositiveAndLog dividend divisor =
  // ...

Since the cases are not defined in a single place (unlike anonymous record fields), I don't know how it would work for arbitrary expressions and sub-expressions. One possibility is that it could work only at a function level, by merging all anonymous cases in "corresponding places in an expression tree" (unsure if that makes sense) into a single anonymous DU type. This is effectively what is demonstrated above. Alternatively one could require an explicit definition of all cases in an expression's type signature, but that removes some of the benefit (not all though; we still get rid of having to define separate types that only one function returns, and we get rid of name clashes).

Pros and Cons

The advantages of making this adjustment to F# are:

  • Avoids proliferation of error types that are only used in one function (and matched in one or a few others)
  • Decreases resistance of returning custom, descriptive error cases. Personally I have a lot of functions in my code that return Error () that means a specific thing, because they only have one failure condition and I don't want to create a single-case DU for every function that can fail. Having anonymous DUs would make it easier to return a descriptive anonymous DU case.
  • Avoids name clashes due to several functions (possibly from different layers, calling functions in layers below) needing to return similarly named errors.

The disadvantages of making this adjustment to F# are:

  • More to learn
  • As this suggestion stands, it is unclear how it can generalize to arbitrary expressions (though that could be just a limitation of my knowledge)

Extra information

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

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
@kevmal

This comment has been minimized.

Copy link

kevmal commented Mar 28, 2019

Maybe a better comparison for an existing approach would be ChoiceXOfY:

let dividePositive dividend divisor =
  if divisor = 0 then Error(Choice1Of4())
  elif dividend <= 0 && divisor <= 0 then Error (Choice2Of4(dividend, divisor))
  elif dividend <= 0 then Error(Choice3Of4 dividend)
  elif divisor <= 0 then Error(Choice4Of4 divisor)
  else Ok (dividend / divisor)
let (|DivByZero|NeitherPositive|DividendNotPositive|DivisorNotPositive|) x = x

/// Divides two positive numbers or returns a more simple error than dividePositive.
// int -> int -> Result<int, {| DivByZero | ArgsNotPositive |}>
let simpleDividePositive dividend divisor =
  match dividePositive dividend divisor with
  | Ok x -> Ok x
  | Error DivByZero -> Error (Choice1Of2())
  | Error(DividendNotPositive _ | DivisorNotPositive _ | NeitherPositive _ ) ->
      Error (Choice2Of2())
let (|DivByZero|ArgsNotPositive|) x = x

/// Logs the result of dividing two positive numbers
let tryDividePositiveAndLog dividend divisor =
  match simpleDividePositive dividend divisor with
  | Ok x -> printfn "Result: %i" x
  | Error ArgsNotPositive -> printfn "At least one argument was negative"
  | Error DivByZero  -> printfn "Proper error handling just saved the universe"
@cmeeren

This comment has been minimized.

Copy link
Author

cmeeren commented Mar 28, 2019

I disagree, as far as idiomatic error handling goes (which is what I intended the example to show). That approach decouples the error identifier from the container. First of all, that means that you can use any 4-case pattern to match any Choice<_,_,_,_>, so you can match on the wrong errors. Secondly, in order to understand the errors when reading the function, you have to manually map back and forth by "index" between the active pattern cases and the Choice cases. Also, I've never seen that approach in blogs or code, so it doesn't seem to me to be a very common way of doing error handling.

Explicit error cases are safer, clearer and AFAIK a more common and idiomatic way of doing error handling.

@kevmal

This comment has been minimized.

Copy link

kevmal commented Mar 28, 2019

Not trying to give an equivalent to the suggested "Anonymous DUs", just saying if you want a DU in the moment you'd probably use Choice. Similar to how prior to anonymous records you might consider tuples as an alternative (even though you lose naming, you could mess up the ordering or whatever).

@cmeeren

This comment has been minimized.

Copy link
Author

cmeeren commented Mar 29, 2019

Ah, OK. Yes, then your example is more similar to the anonymous DU suggestion, and some drawbacks of that method are provided in my reply. :)

@theprash

This comment has been minimized.

Copy link

theprash commented Mar 29, 2019

OCaml has a similar feature called Polymorphic Variants (variants are equivalent to F# discriminated unions). See Real World OCaml for a detailed description (search for the Polymorphic Variants section), which also has a good discussion of the pros and cons.

I'm very interested in this feature for a scripting perspective but would also like to see what a larger app that makes heavy use of this would look like. Error handling sounds like a good use case.

In OCaml the syntax is to prefix the case name with a backtick, e.g. `CaseName. This makes more sense than using curly braces. Anonymous records use {||} because records use {} and then they are modified by adding |.

In F#, would this work like anonymous records, where a function receiving an instance needs to know all of the fields in the record? Could it be like the OCaml polymorphic variants where a function can take in a value and pattern match it on `Case1, `Case2 and `Case3, and then a value can be passed in from another function that only produces `Case1 or `Case2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.