Closed enricosada closed 7 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.
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
@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.
Yes, but IMHO this alone is not useful enough to justify this type in the FSharp.Core
@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.
The success case should not contain a list of messages.
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.
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.
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>
.
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.
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.
(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.)
What kind of problems did you have in Chessie?
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?
Pro and cons of the different designs.
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.
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).
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.
It would be interesting to look at Chessie but with the following type declared:
RopResult<'TMessage,'TSuccess,'TError> = Log<'TMessage> * Result<'TSuccess, 'TError>
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.
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
.
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.
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.
| Bad of 'TMessage list
IMO list
is not suitable since it has slow concatenation.
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.
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
?
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.
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.
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!
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)
BTW the ExtCore Choice<'T,'Error>
functions are definitely relevant here: https://github.com/jack-pappas/ExtCore/blob/180138b6ccf77b5e1d4e411b6c1de2c428839a1f/ExtCore/Pervasive.fs#L1015
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.
+1 for Ok
, since it's short and sweet.
type Result<'a, 'e> = Choice<'a, 'e>
Use the following computation expressions:
Choice
ReaderChoice
ProtectedState
ReaderProtectedState
StatefulChoice
AsyncChoice
AsyncReaderChoice
AsyncProtectedState
AsyncStatefulChoice
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).
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!
@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
}
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.
@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 :)
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?
@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.
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
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 ).
@enricosada honestly, I don't see why Result type should be in FSharp.Core. It duplicates Choice'2.
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).
+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.
@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.
@dsyme Type checker would always report function results with Choice instead of Result, right? So there would be no readability improvement.
@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.
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?
@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.
I think we actually have only two choices:
Result
type + a lot of functions and CEs over it. 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.
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?
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.
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.
@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 )
This issue is used to track discussions of F# RFC FS-1004 - "Result type". Please discuss in thread below (if necessary)