fsharp / fslang-design

RFCs and docs related to the F# language design process, see https://github.com/fsharp/fslang-suggestions to submit ideas
518 stars 144 forks source link

[RFC FS-1004 Discussion] Result type #49

Closed enricosada closed 7 years ago

enricosada commented 8 years ago

This issue is used to track discussions of F# RFC FS-1004 - "Result type". Please discuss in thread below (if necessary)

forki commented 8 years ago

In https://github.com/Microsoft/visualfsharp/pull/964 @wallymathieu is proposing to add:

/// <summary>Helper type for error handling without exceptions.</summary>
[<StructuralEquality; StructuralComparison>]
[<CompiledName("FSharpResult`2")>]
type Result<'T1,'T2> = 
| Success of 'T1 
| Error of 'T2

Is that really the type we want for Results? Chessie is using:

/// Represents the result of a computation.
type Result<'TSuccess, 'TMessage> = 
/// Represents the result of a successful computation.
| Ok of 'TSuccess * 'TMessage list
/// Represents the result of a failed computation.
| Bad of 'TMessage list

If we add a specialized type to FSharp.Core then we should really think about it's usage. the proposed type is maybe too generic and basically only an alias for Choice.

enricosada commented 8 years ago

I think it's good to have a shared (in whole f# ecosystem) Success/Failure type. But I think is wrong to add more functions in FSharp.Core. External libraries like Chessie can add additional functions using the Result type. Maybe the only function i'd like to add is Result.ofChoice: Choice1Of2 -> Result but maybe it's not needed

@swlaschin do you have a feedback to add? because of Railway Oriented Programming

wallymathieu commented 8 years ago

@forki Compared to Chessie you could either create a specialised type or an alias : RopResult<'TSuccess, 'TMessage> = Result<'TSuccess*'TMessage list,'TMessage list> The difference with Choice is that this type tells you that one of the states is a failure and the other is a success. Choice1Of2 does not read like Success.

forki commented 8 years ago

Yes, but IMHO this alone is not useful enough to justify this type in the FSharp.Core

wallymathieu commented 8 years ago

@swlaschin has provided an example of railway oriented programming, if you use an alias for the type in Railway-Oriented-Programming-Example you need to add a few more type signatures.

haf commented 8 years ago

The success case should not contain a list of messages.

forki commented 8 years ago

Yes that is something we discussed in chessie and I'm not convinced that it is a good design. It came from the idea to trace the origin of a calculation (which is useful in some cases - and yes there are probably better ways to do that). But this shows that it is not that easy to find the right type.

mexx commented 8 years ago

IMHO technically Choice is as good as Result would be besides the mentioned addition of the semantic. However this semantic can be added by providing an alias with some supporting functions and active patterns. For now I would vote against this addition.

haf commented 8 years ago

It can't be in an outside library, because that library is an extra dependency. Choice is quite bad as a name, so I agree with the suggestion in general. Users of Suave get confused when we return Choice<string, string>.

wallymathieu commented 8 years ago

Semantics are important. As a developer reading Choice<string, string> I don't see anything indicating that there is an error (in the same way option string * option string does not tell you that you expect exactly one to have a value). However, using Result<string, string> the type system tells me that the second type parameter is the type of the potential error.

forki commented 8 years ago

Yes we all agree on this, but it doesn't mean that this one way to encapsulate results is the best. "the best" is probably not needed, but for Fsharp.core we should be really careful to select one that is useful in many scenarios.

If you look at rust then you see a community that embraced the Result in the whole standard crates, but somehow some people are now discussing if this was a good idea.

I personally still think that Result is a good idea. But just adding the base type is not that useful for me. I always need methods to wrap and unwrap. FSharpx and EntCore have these for Choice and Chessie has these for its own Result type. If we really add result type, then we should also look which standard methods we can add to return proper results instead of exceptions.

forki commented 8 years ago

(Sorry if I sound too negative. I'm not against this, but working on Chessie made it very clear to me that it's not that easy to make that idea work well in practice.)

wallymathieu commented 8 years ago

What kind of problems did you have in Chessie?

forki commented 8 years ago

Chessie was extracted for the Rop usage in Paket and then refactored a couple of times in the hope to make it useful in more situations. So we changed designs a couple of times, but I think we are still not happy. So what i want to say is: I have no solution, but we should not rush it. Can you come up with a list of pros and cons that you see with the current design?

wallymathieu commented 8 years ago

Pro and cons of the different designs.

Lets start by analysing the Railroad oriented programming approach

When we look at the type defined in both Chessie and in the example by @swlaschin we see that it's an approach that mixes Log and Result.

How do we infer this?

If we look at 'TMessage list as Log<'TMessage>:

type RopResult<'TSuccess, 'TMessage> = 
| Ok of 'TSuccess * Log<'TMessage>
| Bad of Log<'TMessage>

So in reality this is equivalent to: Log<'TMessage> * ('TSuccess option) Since you don't know if a "message" is from a previous successful computation or not (just by looking at the type).

From a railway oriented programming-perspective this makes sense. The downside of this is that you don't have any specific message-type associated with failure. The nice thing about this approach is that you can combine messages and recover from failure and then fail again (and avoid losing any message along the way).

What if we analyse the Choice-like approach

type Result<'TSuccess, 'TError> = 
| Success of 'TSuccess 
| Failure of 'TError

In the case that when we have a failure we will get some enumeration that describes the failure, then a simple Choice<CustomerState, CustomerValidationFailure list> is quite sufficient.

What about the case when we potentially have a customer as the result of our operation, or we have gotten string (say from a web service) that explains why things have failed, then we might want to model it as Result<Customer, string>.

For instance in Suave we have the case that a value is found or a failure description is returned. This we would like to model as Result<string, string>. Since the failure does not have any type information other than string, wrapping it in Result helps explain what we are returning.

A mix of ROP and Choice-like approach

It would be interesting to look at Chessie but with the following type declared: RopResult<'TMessage,'TSuccess,'TError> = Log<'TMessage> * Result<'TSuccess, 'TError>

mexx commented 8 years ago

My thoughts on Chessie way of ROP

As @forki mentioned Chessie got multiple refactoring trying to tide the design of Result. And as also mentioned not all are happy and so do I. In my understandings the concept of Trace should be made visible somehow. @wallymathieu also mentioned this flaw in his analysis. I think the mix of ROP and Choise-like approach would fit my conceptual model at best. In it the trace message can be of different type as the error one. I can even imagine to have a separate library to handle the tracing aspect, as it's orthogonal to the result and can be seen as Trace<'Message, 'Result> = Log<'Message> * 'Result. By this definition even a non Result typed function can be traced.

Extra dependency

It can't be in an outside library, because that library is an extra dependency.

@haf can you elaborate why an extra dependency is bad, we always have to depend on some library. Why is to depend on one more is bad? A separate library has advantages especially with the current release cycle of FSharp.Core. IMHO the only advantage to have it defined in FSharp.Core would be when it would be eagerly used as the result of functions provided by FSharp.Core.

wallymathieu commented 8 years ago

Extra dependency: usually the problem is that using a library with many dependencies in .net is still kind of flaky. Having a library like suave without other dependencies makes things easier for the consumers of that library.

eiriktsarpalis commented 8 years ago

My feeling on this is that there are as many possible result types as there are monad implementations with some support for error handling baked in. The problem with any result type beyond choice is that it is necessarily ad-hoc and it really depends on the type of effect we wish manage on the specific monad implementation.

radekm commented 8 years ago
    | Bad of 'TMessage list

IMO list is not suitable since it has slow concatenation.

neoeinstein commented 8 years ago

How is this any better than declaring type Result<'a,'b> = Choice<'a,'b> and providing active patterns that match "Success" or "Failure" to Choice1Of2 and Choice2Of2?

One issue that I have with the Chessie types is that I have to re-implement many of my Choice utility functions to also work with Result. In some cases, I would have some functions that were using the Chessie types and some dependencies that used the more portable Choice model. Then I would have to convert between Choice and Result back and forth. It got quite irritating, and so I switched to a type alias: type Result<'a,'b> = Choice<'a,'b list> which has worked much better for me.

@haf I think that returning a Choice<string, 'TFailure> with an appropriate supporting failure type would be better. Couldn't Suave also just do a similar type alias?

@radekm How big are you expecting your error listing to be? And how often are you expecting that the "Bad" path chains multiple failures together? I haven't seen any cases for myself where performance on the failure path has been a problem.

smoothdeveloper commented 8 years ago

Choice<'a, 'b> doesn't convey so well which case is the result (apparent with the type I guess but Choice1Of2 doesn't read like Result to me) so the fact it is isomorphic with proposed type isn't much a concern.

https://hackage.haskell.org/package/base-4.8.2.0/docs/Data-Either.html

I don't think Result should embed a list of messages (can be either added by wrapping the Result type, or as type argument if only relevant to one of the cases).

Which function of FSharp.Core relies on Choice?

radekm commented 8 years ago

How big are you expecting your error listing to be?

You can write functions which process millions of records, so the Result type should be able to handle similar number of errors.

And how often are you expecting that the "Bad" path chains multiple failures together?

I use applicative functor for validation quite often - it's more user friendly to gather as many errors as possible instead of stopping after the first error.


An interesting article related to this topic is How to fail – introducing Or_error.t.

radekm commented 8 years ago

Choice<'a, 'b> doesn't convey so well which case is the result (apparent with the type I guess but Choice1Of2 doesn't read like Result to me) so the fact it is isomorphic with proposed type isn't much a concern.

@smoothdeveloper Good point, I agree.

dsyme commented 8 years ago

Meta point - I'm very glad to see the lengthy discussion (also based on real-world experience) - just what I was hoping for from our first forays into using RFCs!

enricosada commented 8 years ago

Our cousin, ocaml 4.03 added a result type

type ('a,'b) result = Ok of 'a | Error of 'b

I think Ok instead of Success is better because it's short. It's something we are going to write a lot.

Otherwise i think Success/Failure is better pair than Success/Error , because Failure is more an antonym than Error. And Failure doesnt mean an Error, only the bad choice happened

So :+1 Ok/Error because it's already used (ocaml/rust/elixir) so it's better to use a standard if possibile. Another good option is Ok/Failure.

I'll write a table with proposed naming, existing implementations with links to lang design discussions (if possibile).

Another possibility is like rust, Ok/Err. Bonus point Err has less problems if a type Error is already defined in user code (compatibility).

About type, i think it must be generic on both sides, so Ok of 'a | Error of 'b . Because only the application/library know what is the info it need for Error path (it's easier to map the error info if needed)

dsyme commented 8 years ago

BTW the ExtCore Choice<'T,'Error> functions are definitely relevant here: https://github.com/jack-pappas/ExtCore/blob/180138b6ccf77b5e1d4e411b6c1de2c428839a1f/ExtCore/Pervasive.fs#L1015

huwman commented 8 years ago

Hi all, Excited about the possibility of having the Result type in F# core. I have been using the following result type and useful functions inspired by elm for a while:

type Result<'Err, 'Value> = 
    | Error of 'Err
    | Success of 'Value

See the snippet with the related functions.

It is important to me that the Result type be usable not only from F#, in my opinion compiling the type as FSharpResult makes it less usable and feel foreign in C#. I also feel that for it to be worth including in the core the related functions for wrapping, unwrapping and chaining be included also.

haf commented 8 years ago

+1 for Ok, since it's short and sweet.

vasily-kirichenko commented 8 years ago
  1. type Result<'a, 'e> = Choice<'a, 'e>
  2. Use https://github.com/jack-pappas/ExtCore/blob/180138b6ccf77b5e1d4e411b6c1de2c428839a1f/ExtCore/Pervasive.fs#L1015 functions over it, as Don said.
  3. Use the following computation expressions:

    • Choice
    • ReaderChoice
    • ProtectedState
    • ReaderProtectedState
    • StatefulChoice
    • AsyncChoice
    • AsyncReaderChoice
    • AsyncProtectedState
    • AsyncStatefulChoice

    see here https://github.com/jack-pappas/ExtCore/blob/180138b6ccf77b5e1d4e411b6c1de2c428839a1f/ExtCore/Control.fs#L786

It's possible to define Result as a new type, then port all this above stuff into FSharpCore, but we will end up with two incompatible types. I understand that F# community is still don't use Either monad widely, but our team, for example, have used Choice + ExtCore for about 3 years now and it would be quite painful to move to Result (and it would be possible only if all the CEs and functions above were ported).

smoothdeveloper commented 8 years ago

Can type alias be defined for the DU cases themselves?

I really think the naming of choice types cases was the main obstacle from their more widespread adoption.

With the isomorphism standpoint, byte is isomorphic to char, I think what the naming conveys (and functions consuming/exporting the type) is also important.

Speaking of migrating code bases relying on Choice to another type, why migrate it, maybe new code can rely on new one and have a functions to convert from/to in the code at boundary? It is not elegant but does the job until changes get propagated little by little.

Nice to share your experience BTW!

vasily-kirichenko commented 8 years ago

@smoothdeveloper You cannot define aliases for DU cases, but you can define an active pattern and type aliases which make new type illusion:

let inline (|Ok|Err|) x =
    match x with
    | Choice1Of2 x -> Ok x
    | Choice2Of2 e -> Err e

let inline Ok x = Choice1Of2 x
let inline Err e = Choice2Of2 e
let inline (>>=) m f = match m with Ok x -> f x | Err e -> Err e

type ResultBuilder() =
    member inline __.Bind(m, f) = m >>= f
    member inline __.Return x = Ok x

let result = ResultBuilder()

let r =
    result {
        let! a = 
            match Ok 1 with
            | Ok x -> Ok (x + 1)
            | Err _ -> Ok 0

        let! b = Ok 2
        let! c = Err 3
        return a + b       
    }

open ExtCore.Control

let r' =
    // works with ExtCore CEs as well!
    choice {
        let! a = 
            match Ok 1 with
            | Ok x -> Ok (x + 1)
            | Err _ -> Ok 0

        let! b = Ok 2
        return a + b        
    }
mexx commented 8 years ago

I can see only one thing from the above discussion, we need an easy way to provide aliases for DU cases.

Sidenote: @smoothdeveloper byte is not isomorphic to char. Character has a much wider range than just 256 available byte values.

smoothdeveloper commented 8 years ago

@mexx strawman busted! although I believe in some languages char = 1 byte (python 2?).

Yes alias on DU cases, but as @vasily-kirichenko mention, active pattern + functions kind of hides it, although the type signatures are going to be very confusing.

It actually feels that defining a new type is simplest way to not break existing code, I'm sure there are few people using Choice.Choice2of2 for Ok and adding semantic to it which wasn't baked in the type initially is going to choke.

Anyways, I think I expressed my opinion too much on the matter and leave it to all of you to figure out the right outcome :)

Overlord-Zurg commented 8 years ago

I'd like to make sure that any implementation of this RFC provides a ResultBuilder computation expression like the one illustrated in @vasily-kirichenko 's last post, along with any other computation expressions or functions that make sense (nothing springs to mind right now).

Is the expected behaviour of such a computation expression sufficiently "obvious"? Or are there ambiguities that need to be resolved?

vasily-kirichenko commented 8 years ago

@Overlord-Zurg the Result type is Either monad, which has well known laws as a monad and well established behavior as this concrete type of monad. A proper implementation can be found in ExtCore library (ChoiceBuilder). Also, see other builders there for inspiration.

haf commented 8 years ago

I have another opinion: when you do add this type, please also add combinators around it, like in ExtCore or maybe just a small subset of them like in YoLo

enricosada commented 8 years ago

I think it's better to have only the Result type in FSharp.Core, not a default implementation of combinators, computation exceptions, helper functions etc. Less is better.

Any external library (Ext.Core, FSharp.Core.Attempt, you create it) can add these.

A common result type (in FSharp.Core) help libraries share the type, and each library can evolve indipendently.

No need to add more functions inside FSharp.Core (it's already big). an FSharp.Core is a fixed api, with long backward compatibiliy, it's difficult to evolve.

It's not 2009 anymore, now with nuget packages, it's easier to add more assemblies (from packages) So if we want to add new combinators, helper functions, it's easier, because these can be implemented in normal library in a community fsproject or in an external awesome library like ExtCore. No need to wait for RFCs of FSharp.Core

I think FSharp.Core should contains the common types (Choice, Option, Result, Map, List) with conversion function when needed (like List.ofArray). But all functions should be in separate assemblies if possibile.

I think declaring types inside FSharp.Core and functions in external libraries, can help improve feedback loop, and quality

Maybe the only one i'd like to add to FSharp.Core is ofChoice: Choice -> Result and toChoice: Result -> Choice because is conversion to and from a type already declared inside FSharp.Core ( so it's clear that Choice1Of2 = Ok, Choice2Of2 = Error ).

vasily-kirichenko commented 8 years ago

@enricosada honestly, I don't see why Result type should be in FSharp.Core. It duplicates Choice'2.

vasily-kirichenko commented 8 years ago

It would be completely different story if FSharp.Core would use Result itself, I mean, if it'd have some (or lots of) functional API which does not throw exceptions. Unfortunately, the language does not embrace FP error handling at all (the only single exception is Async.Catch).

toburger commented 8 years ago

+1 for adding Result and the accompanying combinators to FSharp.Core.

I don't think that the argument that FSharp.Core doesn't use them is valid. That's a chicken and egg story. A higher level API (e.g Yaaf.FSharp.Scripting) could adopt Result types afterwards.

I think Result is very valuable to have as a Ok/Error indicator.

It is OK to write/include combinator functions in bigger projects, but it is annoying to include this (external dependency) every time in your smaller projects/script files.

Another point is, that, if for example Suave is using his own Result type (or Choice) and FSharp.Compiler.Services another Result type, the library consumer has to know/learn WHEN and HOW to combine/fuse them together.

elm has for example a core Result type and other libraries implement this type. You know immediately how to combine/compose one lib with another.

dsyme commented 8 years ago

@vasily-kirichenko How about if FSharp.Core had nothing but these:

type Result<'Ok, 'Error> = Choice<'Ok, 'Error>

let inline (|Ok|Error|) (result: Result<'Ok,'Error>) = result

let inline Ok v = Choice1Of2 v
let inline Error err = Choice2Of2 err

Note no match is needed for the active pattern - you can return the choice value directly.

BTW I agree with @enricosada's arguments about "less is more" in FSharp.Core, continuing to embrace the nuget package ecosystem.

forki commented 8 years ago

@dsyme Type checker would always report function results with Choice instead of Result, right? So there would be no readability improvement.

dsyme commented 8 years ago

@forki The type checker preserves abbreviations relatively well. You primarily have to type annotate all primitive functions consuming and producing Result values, e.g. begin with

type Result<'Ok, 'Error> = Choice<'Ok, 'Error>

let inline (|Ok|Err|) (result: Result<'Ok,'Error>) = result

let inline Ok v : Result<'Ok,'Error> = Choice1Of2 v
let inline Err err : Result<'Ok,'Error> = Choice2Of2 err

From there annotations should be preserved. Some secondary error messages may remove them, so yes, the equivalence may be visible to the otherwise-unaware user.

forki commented 8 years ago

Since there are no stupid questions: How much would we break if we do it the other way around and make Choice an abbreviation of Result?

mexx commented 8 years ago

@forki you have the same thoughts ;)

The current users of Choice<'A,'B> can be confused, when they didn't provide the type annotation and the resulting function signature would show up with Result<'A,'B>. But I would be fine with it. Maybe it's the compromise for good backwards compatibility story.

vasily-kirichenko commented 8 years ago

I think we actually have only two choices:

So my suggestion is to add new real Result type with Ok and Err cases (as in Rust. Error, Failure and Success are too long in practice) + solid number of functions (bind, map, mapErr, result, orElse, getOrElse, forAll, fold, foldBack, iter, toOption, fromOption, getOrRaise) + a couple of CEs (result and asyncResult - trust me, people will want the latter one very shortly if we don't supply it). All that said based on my (and my team) experience with ExtCore's choice and friends for about 3 years (instead of exceptions).

I don't get "less is more" if you are talking about a simple type and very well known functions and CEs over it. It's completely unlikely we will ever want to change them.

forki commented 8 years ago

just for me as clarification:

If we don't add Result type here and both Chessie and ExtCore would use type Result<'Ok, 'Error> = Choice<'Ok, 'Error> then Chessie's and ExtCore's results would still be compatible, right?

vasily-kirichenko commented 8 years ago

Yes. But it's a mess. I suggest to return Choice to what it was created for - active patterns representation, and forget about it in error handling context.

haf commented 8 years ago

My 5 cents are with @vasily-kirichenko here. Better not to add anything rather than add something that walks like a chicken and quacks like a duck.

enricosada commented 8 years ago

@haf walk and quack like a duck, but has a BIG RED SIGN "I AM A RESULT". It's really nice and useful ihmo. Choice it's not explicit like Result. But yes, they are the same. I think Result as an improvement on Choice on readability ( a way is successful and another is a faliure instead of two generic ways )