Refactored Error
type + Eff
and Aff
applicative functors
#1074
louthy
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
There have been a number of calls on the Issues page for a
ValidationAsync
monad, which although it's a reasonable request (and I'll get to it at some point I'm sure), when I look at the example requests, it seems mostly the requestors want a smarter error handling story in general (especially for the collection of multiple errors).The error-type that I'm building most of the modern functionality around (in
Fin
,Aff
, andEff
for example) is thestruct
type:Error
. It has been designed to handle both exceptional and expected errors. But the story around multiple errors was poor. Also, it wasn't possible to carry additional information with theError
, it was a closed-type other than ability to wrap up anException
- so any additional data payloads was cumbersome and ugly.Error
refactorSo, I've bitten the bullet and refactored
Error
into anabstract record
type.Error
sub-typesThere are a few built-in sub-types:
Exceptional
- An unexpected errorExpected
- An expected errorManyErrors
- Many errors (possibly zero)These are the key base-types that indicate the 'flavour' of the error. For example, a 'user not found' error isn't
something exceptional, it's something we expect to happen. An
OutOfMemoryException
however, isexceptional - it should never happen, and we should treat it as such.
Most of the time we want sensible handling of expected errors, and bail out completely for something exceptional. We also want to protect ourselves from information leakage. Leaking exceptional errors via public APIs is a sure-fire way to open up more information to hackers than you would like. The
Error
derived types all try to protect against this kind of leakage without losing the context of the type of error thrown.When
Exceptional
is serialised, only theMessage
andCode
component is serialised. There's no serialisation of the innerException
or its stack-trace. It is also possible to construct anExceptional
message with an alternative message:That means if the
Error
gets serialised, we only get a"There was a problem"
and an error-code.Error
methods and propertiesEssentially an error is either created from an
Exception
or it isn't. This allows for expected errors to be represented without throwing exceptions, but also it allows for more principled error handling. We can pattern-match on thetype, or use some of the built-in properties and methods to inspect the
Error
:IsExceptional
-true
for exceptional errors. ForManyErrors
this istrue
if any of the errors are exceptional.IsExpected
-true
for non-exceptional/expected errors. ForManyErrors
this istrue
if all of the errors are expected.Is<E>(E exception)
-true
if theError
is exceptional and any of the the internalException
values are of typeE
.Is(Error error)
-true
if theError
matches the one provided. i.e.error.Is(Errors.TimedOut)
.IsEmpty
-true
if there are no errors in aManyErrors
Count
-1
for most errors, orn
for the number of errors in aManyErrors
Head()
- To get the first errorTail()
- To get the tail of multiple errorsError
constructionThe
Error
type can be constructed as before, with the various overloadedError.New(...)
calls.For example, this is an expected error:
When expected errors are used with codes then equality and matching is done via the code only:
And this is an exceptional error:
Finally, you can collect many errors:
Or more simply:
Error
types with additional dataYou can extend the set of error types (perhaps for passing through extra data) by creating a new record that inherits
Exceptional
orExpected
:By default the properties of the new error-type won't be serialised. So, if you want to pass a payload over the wire, add the
[property: DataMember]
attribute to each member:Using this technique it's trivial to create new error-types when additional data needs to be moved around, but also there's a ton of built-in functionality for the most common use-cases.
Error
breaking changesError
isn't astruct
any more,default(Error)
will now result innull
. In practice this shouldn't affect anyone.BottomException
is now inLanguageExt.Common
Error
documentationThere's also a big improvement on the API documentation for the
Error
typesAff
andEff
applicative functorsNow that
Error
can handle multiple errors, we can implement applicative behaviours forAff
andEff
. If you think of monads enforcing sequential operations (and therefore can only continue if each operation succeeds - leading to only one error report if it fails), then applicative-functors are the opposite in that they can run independently.By adding
Apply
toAff
andEff
, we can now do the same kind of validation-logic both synchronously and asynchronously.Contrived example
First let's create a simple asynchronous effect that delays for a period of time:
Now we'll combine that so we get an effect that parses a
string
into anint
, and adds a delay of1000
milliseconds (the delay is to simulate calling some external IO).:
Next we'll use the applicative behaviour of the
Aff
to run two operations in parallel. When they complete the values will be applied to the function that has been lifted bySuccessAff
.To measure what we're doing, let's add a simple function called
report
. All it does is run anAff
, measures how long it takes, and prints the results to the screen:Finally, we can run it:
The output for the two operations is this:
Notice how the first one (which succeeds) takes
1032ms
- i.e. the two parse operations ran in parallel. And on the second one, we get both of the errors returned. The reason that one finished so quickly is because the delay was after theparseInt
call, so we exited immediately.Of course, it would be possible to do this:
Which is more elegant. But the success path would take
2000ms
, and the failure path would only report the first error.Hopefully that gives some insight into the power of applicatives (even if they're a bit ugly in C#!)
Beta
This will be in beta for a little while, as the changes to the
Error
type are not trivial.This discussion was created from the release Refactored `Error` type + `Eff` and `Aff` applicative functors.
Beta Was this translation helpful? Give feedback.
All reactions