Skip to content

New trait: Fallible

Pre-release
Pre-release
Compare
Choose a tag to compare
@louthy louthy released this 04 Aug 21:11
· 349 commits to main since this release

In 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 value E for structure F (usually a functor, applicative, or monad)
  • Fallible<F> is equivalent to Fallible<Error, F> - which simplifies usage for the commonly used Error 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 errors
  • Catch can be used to catch an error if it matches a predicate; and if so, it runs the fail 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 types
  • error raises the Error 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 renamed IO.pure
    • Capital letters are used for constructor cases (like Some and None in a discriminated union)
    • IO.Pure doesn't create an IO<A> type with a Pure case, it constructs a lambda that returns an A
    • So, I'm going to change the construction functions where they're not really doing what they claim
  • IO.Fail has been renamed IO.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 the Aff monad and now isn't needed because the IO monad has its own environment which carries the cancellation token
  • HasFromError - was used by the transducers, so not needed
  • HasIO - was used by the MinRT runtime, which isn't needed anymore
  • HasSyncContextIO - 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...