New trait: Fallible
Pre-releaseIn Haskell there's a trait called MonadFail
for monadic types to raise errors. It's not particularly effective as most tend to avoid it. I wanted to create a trait (for types that can fail) that's effective and could help standardise error handling.
That's the new Fallible<E, F>
and Fallible<F>
trait type...
Fallible
Traits that are fallible can fail (I'm quite smug about the name, I think it's pretty cool, haha)!
Fallible<E, F>
- can have a parameterised failure valueE
for structureF
(usually a functor, applicative, or monad)Fallible<F>
is equivalent toFallible<Error, F>
- which simplifies usage for the commonly usedError
type
Anything that is fallible must implement:
public static abstract K<F, A> Fail<A>(E error);
public static abstract K<F, A> Catch<A>(
K<F, A> fa,
Func<E, bool> predicate,
Func<E, K<F, A>> fail);
Fail
is for the raising of errorsCatch
can be used to catch an error if it matches apredicate
; and if so, it runs thefail
function to produce a new structure (which may also be failing, but could be used to rescue the operation and provide a sensible succeeding default).
Fallible
module
In the Fallible
module there are functions to raise failures:
public static class Fallible
{
public static K<F, A> fail<E, F, A>(E error)
where F : Fallible<E, F> =>
F.Fail<A>(error);
public static K<F, Unit> fail<E, F>(E error)
where F : Fallible<E, F> =>
F.Fail<Unit>(error);
public static K<F, A> error<F, A>(Error error)
where F : Fallible<Error, F> =>
F.Fail<A>(error);
public static K<F, Unit> error<F>(Error error)
where F : Fallible<Error, F> =>
F.Fail<Unit>(error);
}
fail
raises the parameterised error typeserror
raises theError
type
Because the traits are all interfaces we can't use operator |
for error handling (the operators can still be used for concrete types, like Eff<A>
, IO<A>
, etc.) -- and so there are now lots of Catch
extension methods for catching errors in Fallible
structures. You can view them here.
Prelude
The Prelude
now has:
public static K<F, A> pure<F, A>(A value)
where F : Applicative<F>;
public static K<F, A> fail<E, F, A>(E error)
where F : Fallible<E, F>;
public static K<F, Unit> fail<E, F>(E error)
where F : Fallible<E, F>;
public static K<F, A> error<F, A>(Error error)
where F : Fallible<F>;
public static K<F, Unit> error<F>(Error error)
where F : Fallible<F>;
So, for example, you can now construct any type (as long as it implements the Applicative
trait) using pure
:
var effect = pure<Eff, int>(100);
var option = pure<Option, string>("Hello");
var either = pure<Either<Error>, bool>(true);
And you can construct any type (as long as it implements the Fallible<E, F>
trait) using fail
or with error
(when Fallible<F>
):
var effect = error<Eff, int>(Errors.SequenceEmpty);
var trying = error<Try, int>(Errors.EndOfStream);
var option = fail<Unit, Option, string>(unit);
var either = fail<string, Either<string>, bool>("failed!");
Types that have been made Fallible
IO<A>
Eff<RT, A>
Eff<A>
Either<L, R>
EitherT<L, M, R>
Fin<A>
Option<A>
OptionT<M, A>
Try<A>
TryT<A>
Validation<F, A>
ValidationT<F, M, A>
Which means you can use .Catch(...)
on all of those types now. For example:
var res = error<Eff, int>(Errors.Cancelled)
.Catch(Errors.EndOfStream, _ => 0) // Catch a specific error and ignore with a default
.Catch(Errors.Closed, _ => pure<Eff, int>(-1)) // Run an alternative effect
.Catch(Errors.Cancelled, _ => IO.pure(-2)) // For monads that support IO, launch an IO operation
.Catch(e => Errors.ParseError(e.ToString())); // Catch-all mapping to another error
IO
changes
IO.Pure
has been renamedIO.pure
- Capital letters are used for constructor cases (like
Some
andNone
in a discriminated union) IO.Pure
doesn't create anIO<A>
type with aPure
case, it constructs a lambda that returns anA
- So, I'm going to change the construction functions where they're not really doing what they claim
- Capital letters are used for constructor cases (like
IO.Fail
has been renamedIO.fail
(see above)
Eff
running extensions
The various .Run*
methods for Eff<A>
and Eff<RT, A>
have been made into extension methods that work with K<Eff, A>
and K<Eff<RT>, A>
. That means if you end up with the more abstract representation of Eff
you can run it without calling .As()
first.
I'll be doing this for other types that are 'run'.
I have also tidied up some of the artefacts around the MinRT
runtime used by Eff<A>
. Because Eff<A>
is now backed by a transformer stack with IO<A>
as its inner monad, the MinRT
doesn't need to carry any IO environment any more, so I've removed it from MinRT
, making MinRT
into a completely empty struct. This removes some constraints from the Run*
extensions.
Prelude.ignoreF
and Functor.IgnoreF
The prelude function ignoreF
and equivalent extension method to Functor<F>
, IgnoreF
are the equivalent of calling .Map(_ => unit)
to ignore the bound-value of a structure and instead return unit
.
Transducers removed
I have removed the Transducers completely from v5
. They were originally going to be the building blocks of higher-kinds, but with the new trait-system I don't think they add enough value, and frankly I do not have the time to bring them through this v5
release process (which is already a mammoth one)! As much as I like transducers, I think we can do better with the traits system now.
Not needed traits removed
The following traits have been removed:
HasCancel
- This was used in theAff
monad and now isn't needed because theIO
monad has its own environment which carries the cancellation tokenHasFromError
- was used by the transducers, so not neededHasIO
- was used by theMinRT
runtime, which isn't needed anymoreHasSyncContextIO
- as above
New Sample
Those of you who are subscribed to my blog at paullouth.com will have seen the first newsletter this week. It took a while to get off the ground because I refused to use the terrible Mailgun integration in GhostCMS.
Instead I rolled my own, which I've been working on the past few days. So it was an opportunity to test out the effect system and trait system. I took it as far as it can go and the entire application is trait driven. Only when you invoke the application do you specify what monad and runtime to use.
This is the main operation for generating the newsletter and emailing it out to all of the members:
public static class Send<M, RT>
where RT :
Has<M, WebIO>,
Has<M, JsonIO>,
Has<M, FileIO>,
Has<M, EmailIO>,
Has<M, ConsoleIO>,
Has<M, EncodingIO>,
Has<M, DirectoryIO>,
Reads<M, RT, Config>,
Reads<M, RT, HttpClient>
where M :
Monad<M>,
Fallible<M>,
Stateful<M, RT>
{
public static K<M, Unit> newsletter =>
from posts in Posts<M, RT>.readLastFromApi(4)
from members in Members<M, RT>.readAll
from templates in Templates<M, RT>.loadDefault
from letter in Newsletter<M, RT>.make(posts, templates)
from _1 in Newsletter<M, RT>.save(letter)
from _2 in Display<M, RT>.showWhatsAboutToHappen(members)
from _3 in askUserToConfirmSend
from _4 in Email<M, RT>.sendToAll(members, letter)
from _5 in Display<M, RT>.confirmSent
select unit;
..
}
Note how the computation being run is entirely generic: M
. Which is constrained to be a Monad
, Fallible
, and Stateful
. The state is RT
, also generic, which is constrained to have various IO traits as well as a Config
and HttpClient
state. This can be run with any type that supports those traits. Completely generic and abstract from the underlying implementation.
Only when we we pass the generic argument to Send<>
do we get a concrete implementation:
var result = Send<Eff<Runtime>, Runtime>.newsletter.Run(runtime);
Here, we run the newsletter
operation with an Eff<Runtime>
monad. But, it could be with any monad we build.
Importantly, it works, so that's good :)
Source code is here . Any questions, ask in the comments below...