Skip to content

How to deal with side effects

Paul Louth edited this page Oct 23, 2023 · 46 revisions

Introduction

One of the main tenets of functional programming is to have no side-effects. Code without side-effects is deterministic and doesn't code rot. This is especially valuable when writing large applications, it minimises technical debt and long-term maintenance costs. It is also cognitively much easier for the engineers working on the code to manage: composing code without side-effects leads to larger pieces of functionality that also don't have side-effects.

The way to write code without side-effects is to write pure functions.

What are pure functions?

  • Functions depend entirely on the arguments passed to them
  • Functions don't cause side-effects (i.e. writing to a log, writing to a database, writing to a global variable, ...)
  • Functions don't use external sources of global state (i.e. reading from a database, reading from a global variable, ...)
  • Functions shouldn't throw exceptions

This clearly limits somewhat the ability to write useful programs. Programs tend to need to update the state of the world to be considered viable. So, how do we define this?

Pure World

Before going any further, you'll need to use these for many of the examples to work.

    using LanguageExt;
    using LanguageExt.Common;
    using static LanguageExt.Prelude;

The most pure way to do this is to contain all 'global' state in a world, and then fold over it. Imagine a single, pure, HandleEvent function which took the entire state of the World as an argument, an event instance, and then returned a new state of the World:

    World HandleEvent(World world, Event event) => ...;

Now, we could fold over a series of events to have an entirely pure application:

    eventualWorldState = events.Fold(initialWorldState, HandleEvent);

This works, but it requires all global state to be in-memory, in a type called World. This isn't practical for all but the simplest applications. It certainly doesn't support a file-system, a 3rd-party database, etc.

If you haven't come across Fold yet, it works like Aggregate in LINQ. i.e. for the stream of events it will first call HandleEvent with initialWorldState and the first event in the stream. HandleEvent will return a new World instance, which is then passed back to HandleEvent for the subsequent event in the stream. This continues until the events stream is exhausted. You can think of this as modelling time, with a history of World instances. When the final World instance is returned from the Fold operation which represents now.

To deal with the real-world World, we need to somehow capture actions on the world as pure data, so that we can have a controlled fold-like operation on the real-world.

What is a pure data representation of side-effect? The simplest is a lambda:

    static Func<Option<string>> readAllText(string path) =>
        () => {
            try
            {
                return File.ReadAllText(path);
            }
            catch
            {
                return None;
            }
        };

The Func<A> is a data structure that captures the invocation, it captures the exception, and can be run on-demand. So, if you collected a sequence of Func then you could force them to invoke in-sequence, or build in whatever constraints you need.

IO<A>

Instead of using Func<A> we could instead define a delegate:

    public delegate Either<Error, A> IO<A>();

Then update our readAllText and add a writeAllText function:

    static IO<string> readAllText(string path) =>
        () => File.ReadAllText(path);

    static IO<Unit> writeAllText(string path, string text) =>
        () => { File.WriteAllText(path, text); return unit; }

And some supporting functions that make IO<A> into a functor and a monad:

    public static class IO
    {
        // Allows us to lift pure values into the IO domain
        public static IO<A> Pure<A>(A value) => 
            () => value;

        // Wrap up the error handling
        public static Either<Error, A> Run<A>(this IO<A> ma)
        {
            try
            {
                 return ma();
            }
            catch(Exception e)
            {
                 return Error.New("IO error", e);
            }
        }

        // Functor map
        public static IO<B> Select<A, B>(this IO<A> ma, Func<A, B> f) => () =>
            ma().Match(
                Right: x => f(x),
                Left:  Left<Error, B>);

        // Functor map
        public static IO<B> Map<A, B>(this IO<A> ma, Func<A, B> f) => 
            ma.Select(f);

        // Monadic bind
        public static IO<B> SelectMany<A, B>(this IO<A> ma, Func<A, IO<B>> f) => () =>
            ma().Match(
                Right: x => f(x)(),
                Left:  Left<Error, B>);

        // Monadic bind
        public static IO<B> Bind<A, B>(this IO<A> ma, Func<A, IO<B>> f) =>
            SelectMany(ma, f);

        // Monadic bind + projection
        public static IO<C> SelectMany<A, B, C>(
            this IO<A> ma, 
            Func<A, IO<B>> bind, 
            Func<A, B, C> project) => 
            ma.SelectMany(a => bind(a).Select(b => project(a, b)));    
    }

We can now compose our IO:

    var computation = from text in readAllText(inpath)
                      from _    in writeAllText(outpath, text)
                      select unit;

This gives us a pure data representation of the IO, we can then safely invoke it:

    Either<Error, Unit> result = computation.Run();

What have we gained?

  • The construction of computation is pure
  • All side-effects are now encapsulated and run in a single function Run
  • Exceptions are elegantly handled, returning a type that we can work with: Either<Error, A>

What about our pure functions? How are they used with IO<A>? We lift them into the IO monad:

    // A pure function for turning all lower-case characters into upper-case characters
    static string Capitalise(string text) => 
        new string(text.Map(x => Char.IsLower(x) ? Char.ToUpper(x) : x).ToArray());

    // Use it in the IO context
    var computation = from text  in readAllText(inpath)
                      from ntext in IO.Pure(Capitalise(text))
                      from _     in writeAllText(outpath, ntext )
                      select unit;

That's great, we can now elevate pure functions into the IO monad. A slightly more elegant way would be:

    var computation = from ntext in readAllText(inpath).Map(Capitalise)
                      from _     in writeAllText(outpath, ntext)
                      select unit;

Or even:

    var computation = from text  in readAllText(inpath)
                      let ntext = Capitalise(text)
                      from _     in writeAllText(outpath, ntext)
                      select unit;

All approaches are valid and have the effect of lifting the context of pure functions into the IO domain. You can always go up to a higher level, i.e. from pure A to IO<A>, but you should never go back down: i.e. by calling computation.Run() - once you go back down, you're back into the land of uncontrolled/interleaved side-effects. And so our goal is to never call Run, except once:

    class Program
    {
        public static void Main(string[] args)
        {
            MainIO(args).Run();
        }

        static IO<Unit> MainIO(string[] args) =>
           // ... build your entire application here
    }

If you think about it, that makes sense, we want the side-effects to be at the outer edges of everything, and then everything within to be pure. Remember the IO<A> monad is a pure data representation, and so it's only realised concretely when Run is called.

There are some edge-cases here though, because C# doesn't have any native support for an IO monad, we often have to work with other frameworks like ASP.NET which are loaded with side-effects. A place you may want to explicitly call Run might be in a request/response handler - clearly you're going to need the concrete value to respond to a client, so getting the value out of the computation requires Run.

In theory you could build your own web-server framework that sat entirely in an IO monad, but that's waaaay to much effort in C#-land. Another approach is to wrap (lift) an existing framework in IO<A> (as I showed above with File.ReadAllText and File.WriteAllText). This is a good approach, but sometimes pragmatism needs to prevail. If you only have a few places in your app where Run is called, then you are already winning. If you have lots of Run invocations, you're losing.

You should always think that "If I'm in an IO monad, I can't get out".

If you look at every Haskell project, you'll see its main function declared like so:

main :: IO ()
main = -- build your entire application here

IO () is the equivalent to IO<Unit> in C#. And so this is a function that takes no arguments and returns an IO monad of Unit. The Haskell runtime will do the difficult work of actually running the real-world IO. And so, this is why Haskell is known as a pure language, because its side-effects are pushed right outside of your application.

Ok, this is pretty good, we're some way along the journey of capturing side-effects. For experienced users of this library IO<A> may be familiar to you in its behaviour: it's Try<A>.

What can't we do?

  • Abstract away from the IO implementations themselves:
    • We can't inject mocked IO calls
    • Running the computation is a bit of a 'black box'

IO<Env, A>

Let's try to solve the injection of mocked IO. How would we do that? Using a DI framework is not an option, we want to do this in a purely functional way. If we extend the IO<A> type to have an environment, then we could thread the environment through the computation. You could think of the environment like the World type mentioned earlier:

    public delegate Either<Error, A> IO<Env, A>(Env env);

We've extended the IO delegate to have an additional parameter of Env. It can be anything we want. But importantly, we don't return an environment, and so it's fixed. Let's change the implementation:

    public static class IO
    {
        // Allows us to lift pure values into the IO domain
        public static IO<Env, A> Pure<A, Env>(A value) => 
            (Env env) => value;

        // Wrap up the error handling
        public static Either<Error, A> Run<Env, A>(this IO<Env, A> ma, Env env)
        {
            try
            {
                 return ma(env);
            }
            catch(Exception e)
            {
                 return Error.New("IO error", e);
            }
        }

        public static IO<Env, B> Select<Env, A, B>(this IO<Env, A> ma, Func<A, B> f) => env =>
            ma(env).Match(
                Right: x => f(x),
                Left: Left<Error, B>);

        public static IO<Env, B> Map<Env, A, B>(this IO<Env, A> ma, Func<A, B> f) => 
            Select(ma, f);

        public static IO<Env, B> SelectMany<Env, A, B>(this IO<Env, A> ma, Func<A, IO<Env, B>> f) => 
            env =>
                ma(env).Match(
                    Right: x => f(x)(env),
                    Left: Left<Error, B>);

        public static IO<Env, B> Bind<Env, A, B>(this IO<Env, A> ma, Func<A, IO<Env, B>> f) => 
            SelectMany(ma, f);

        public static IO<Env, C> SelectMany<Env, A, B, C>(
            this IO<Env, A> ma, 
            Func<A, IO<Env, B>> bind, 
            Func<A, B, C> project) =>
            ma.SelectMany(a => bind(a).Select(b => project(a, b)));
    }

Then update our readAllText and add a writeAllText function:

    public interface FileIO
    {
        string ReadAllText(string path);
        Unit WriteAllText(string path, string text);
    }

    static IO<Env, string> readAllText<Env>(string path) 
        where Env : FileIO =>
            env => env.ReadAllText(path);

    static IO<Env, Unit> writeAllText<Env>(string path, string text)
        where Env : FileIO =>
            env => env.WriteAllText(path, text);

We have now defined an interface called FileIO and added constraints to the Env that is passed to readAllText and writeAllText. That means we can inject our own definitions of readAllText and writeAllText. It also means we're declaring upfront (via the constraints) the expected IO operations in the function.

    public class LiveEnv : FileIO
    {
        public string ReadAllText(string path) => File.ReadAllText(path);
        public Unit WriteAllText(string path, string text) { File.WriteAllText(path, text); return unit; }
    }

    // Create the application environment (we do this once at app startup)
    var appEnv = new LiveEnv();

    // Create the pure computation
    var computation = from text in readAllText<LiveEnv>(inpath)
                      from _    in writeAllText<LiveEnv>(outpath, text)
                      select unit;

    // Run the computation with the environment:
    Either<Error, Unit> result = computation.Run(appEnv);

What have we gained?

  • The construction of computation is pure
  • All side-effects are now encapsulated and run in a single function Run
  • Exceptions are elegantly handled, returning a type that we can work with: Either<Error, A>
  • More declarative functions (constraints)
  • We have completely abstracted away from the side-effecting operations
    • We can inject our own effects, which makes testing easier
  • As well as injectable behaviours, we can use the Env to pass through configuration data, which also abstracts away from global configuration state.

We have captured some of the original HandleEvent example from the beginning of this document. The main difference is we don't return a new Env, because we're modifying the real world.

This new definition of IO<Env, A> is the Reader<Env, A> from language-ext. So think of the Reader as a way of threading a static world through the computation.

What haven't we gained?

  • An opinionated way to build Reader environments, or any infrastructure around Reader to deal with common IO or the real-world async/sync mismatch.

Aff and Eff monad

I have lost count of how many times I've been asked about how to do IO in functional programming, as you can see above there are several approaches. I figured it would be valuable for this library to have an opinionated approach, with infrastructure built around those opinions. This is where the Aff and Eff monads come in:

Type Behaviour
Eff<A> A synchronous effect - equivalent to Try<A>
Aff<A> An asynchronous effect - equivalent to TryAsync<A>
Eff<RT, A> A synchronous effect with an injectable runtime environment - equivalent to Reader<RT, A>
Aff<RT, A> An asynchronous effect with an injectable runtime environment - no current equivalent

These types have all been designed to work seamlessly with each other, and so can be combined in the same LINQ expression for example. Eff<A> and Aff<A> are explicitly for capturing side-effects that don't need to reference an external environment. Eff<RT, A> and Aff<RT, A> are there to allow external injection of 'global' configuration and behaviour, like Reader.

They have also been highly optimised to minimise allocations and to stop repeated lazy invocation (memoisation). They're structs which reduces heap-allocation, and internally [the asynchronous types] use ValueTask, which again is a struct and is much more optimal than Task.

Finally, there are built-in wrappers for the existing IO behaviours in the .NET BCL. With support for building an extensible runtime.

Aff<A> and Eff<A> monads

Before we get on to the runtime versions of Aff and Eff, let's look at the non-runtime versions. The idea with the Aff<A> and Eff<A> types is to lift pure functions into the effect-space. Because they have no runtime, they can't possibly interact with the outside world, right?

Obviously, this is C#, so they can. It is up to you to decide how pure you want to go. It is possible to do side-effects within an Aff<A> or Eff<A>, but then you'd lose the benefits of creating an injectable runtime. However, you could avoid the runtime versions of Aff and Eff altogether. Here's an example:

public static class FileAff
{
    public static Aff<Seq<string>> readAllLines(string path) =>
        Aff(async () => (await File.ReadAllLinesAsync(path)).ToSeq());
    
    public static Aff<Unit> writeAllLines(string path, Seq<string> lines) =>
        Aff(async () =>
        {
            await File.WriteAllLinesAsync(path, lines);
            return unit;
        });
}

This wraps up File.ReadAllLinesAsync and File.WriteAllLinesAsync into a new FileAff type. It's not too much of a stretch to think you could make this use an injected interface, or similar:

public interface FileIO
{
    ValueTask<string[]> ReadAllLinesAsync(string path);
    ValueTask<Unit> WriteAllLinesAsync(string path, string[] lines);
}

public static class FileAff
{
    static readonly FileIO injected;
    
    public static Aff<Seq<string>> readAllLines(string path) =>
        Aff(async () => (await injected.ReadAllLinesAsync(path)).ToSeq());
    
    public static Aff<Unit> writeAllLines(string path, Seq<string> lines) =>
        Aff(async () =>
        {
            await injected.WriteAllLinesAsync(path, lines.ToArray());
            return unit;
        });
}

It's for the reader to decide how injected would get populated. I am not necessarily advocating this as the best approach to injection of side-effects, but it's certainly a way to simplify the usage of Aff and Eff, so that a runtime isn't needed.

Now, we've got that out of the way. I'd like to make it clear that the original intention of Aff<A> and Eff<A> was for lifting pure values and functions into the effect-space. i.e.

    var mx = SuccessEff(100);  // lift a pure int into `Eff<int>`
    var my = SuccessAff(100);  // lift a pure int into `Aff<int>`
    var mf = FailAff<int>("There was an error");

These 'pure' effects can then be combined easily with Aff<RT, A> and Eff<RT, A> to allow pure and effectful behaviours to work side-by-side with minimal friction.

Setting up your application to work with runtimes

Building a runtime

The key aspect of this opinionated approach is less about the monadic types themselves, and more about how we build a runtime environment for our application. We use ad-hoc polymorphism to make this work. Let's define our FileIO from before.

    public interface FileIO
    {
        string ReadAllText(string path);
        Unit WriteAllText(string path, string text);
    }

It's the same. The difference is how this makes it into the runtime, we don't derive our runtime environment type from FileIO, instead we create a trait interface. By convention we will prefix the trait interface name with Has, where we state the runtime 'Has file IO' for example:

    public interface HasFile<RT> 
        where RT : struct, HasFile<RT>
    {
        Eff<RT, FileIO> FileEff { get; }
    }

It is important to follow the constraint convention too. We're saying the runtime must be a struct, and must be derived from HasFile<RT>. These constraints will help the C# type-checker to give us friendly errors when we mess up.

The FileEff property simply returns a FileIO implementation lifted into an Eff. Let's see how that works in the runtime:

    public struct LiveRuntime : HasFile<LiveRuntime>
    {
        public Eff<LiveRuntime, FileIO> FileEff => SuccessEff(LiveFileIO.Default);
    }

This is where the use of traits makes this a bit easier to manage. Instead of including all of the functions of FileIO in our runtime environment, we have a single property that redirects to our live FileIO implementation. This also allows us to plug 'n' play existing implementations, making it easy for you or others to wrap up other side-effecting libraries and provide them as nu-get packages, we then simply add the trait to our runtime and we're ready to go.

If we can't take an off-the-shelf library, then we can implement ourselves:

    public struct LiveFileIO : FileIO
    {
        public static readonly FileIO Default = new LiveFileIO();

        public string ReadAllText(string path) =>
            System.IO.File.ReadAllText(path);

        public Unit WriteAllText(string path, string text)
        {
            System.IO.File.WriteAllText(path, text); 
            return unit; 
        }
    }

Now we have a simple runtime and an implementation of FileIO, let's try to use it for real:

    // Define a runtime agnostic file copying function
    public static Eff<RT, Unit> CopyFile<RT>(string source, string dest) 
        where RT : struct, HasFile<RT> =>
            from text in default(RT).FileEff.Map(rt => rt.ReadAllText(source))
            from _    in default(RT).FileEff.Map(rt => rt.WriteAllText(dest, text))
            select unit;

    // Try it
    var result = CopyFile<LiveRuntime>(src, dest);
    result.RunIO(new LiveRuntime());

Eurggh! That's not an improvement, that default(RT).FileEff.Map nonsense is ugly and hard to read. We're not declarative here. So we wrap them in static functions

    public static class File<RT>
        where RT : struct, HasFile<RT>
    {
        public static Eff<RT, string> readAllText(string path) =>
            default(RT).FileEff.Map(rt => rt.ReadAllText(path));

        public static Eff<RT, Unit> writeAllText(string path, string text) =>
            default(RT).FileEff.Map(rt => rt.WriteAllText(path, text));
    }

Now we can write the CopyFile function like this:

    // Define a runtime agnostic file copying function
    public static Eff<RT, Unit> CopyFile<RT>(string source, string dest) 
        where RT : struct, HasFile<RT> =>
            from text in File<RT>.readAllText(source)
            from _    in File<RT>.writeAllText(dest, text)
            select unit;

This may seem like a lot of effort to define the IO operations: building an interface, building a trait-interface, and building a static class to run them. But we do this once for each type of IO we do. We build our runtime behaviours once as well. Once the IO behaviours are built, we just use them like we use System.IO.File.ReadAllText, it's baked in. Luckily, a lot of this is built into language-ext, so you only need to build your own runtime and supplement with any bespoke IO you have. I hope that others will start to build their own libraries that wrap access to SQL Server, ASP.NET Core, etc.

Building a test runtime

How would we mock the file-system? There are two approaches:

  1. Build a bespoke runtime type for each unit-test
  2. Build a single test runtime that can accept mocked data

I believe the second option is simplest and easier to work with:

First let's define the FileIO implementation:

    public struct TestFileIO : FileIO
    {
        readonly Dictionary<string, string> files;

        public TestFileIO(Dictionary<string, string> files) =>
            this.files = files;

        public string ReadAllText(string path) =>
            files.ContainsKey(path)
                ? files[path]
                : throw new FileNotFoundException(path);

        public Unit WriteAllText(string path, string text)
        {
            if(files.ContainsKey(path))
            {
                files[path] = text;
            } 
            else
            {
                files.Add(path, text);
            }
            return unit;
        }
    }

You might be shocked to see the use of Dictionary as its a mutable data-structure, and flies in the face of everything we believe as functional programmers. But remember we're trying to replicate a mutable file-system. So this is fine. It's a slightly naïve implementation, we're not necessarily replicating the exceptions properly for example, but it shows that we could have an in-memory file-system.

Now we can implement the test-runtime:

    public record TestRuntimeEnv(Dictionary<string, string> Files);
 
    public struct TestRuntime : HasFile<TestRuntime>
    {
        readonly TestRuntimeEnv Env;

        public TestRuntime(TestRuntimeEnv env) => 
            Env = env;

        public Eff<TestRuntime, FileIO> FileEff => 
            Eff<TestRuntime, FileIO>(static rt => new TestFileIO(rt.Env.Files));
    }

The major difference from before is that the TestRuntime takes its own environment called TestRuntimeEnv. This isn't a requirement, but because TestRuntime is a struct, the more fields we have, or field-backed properties, the less efficient it will be. So, for the environmental state we want to create a simple record type that we'll pass through. In that enviroment is a Dictionary of files, this will be what we will use to mock the file-system.

The other difference is the implementation of FileEff. We're not working with a stateless FileIO derived class any more, and so it needs to be initialised from the state within the runtime. Remember the implementation of the static functions:

    public static class File<RT>
        where RT : struct, HasFile<RT>
    {
        public static Eff<RT, string> readAllText(string path) =>
            default(RT).FileEff.Map(rt => rt.ReadAllText(path));

        public static Eff<RT, Unit> writeAllText(string path, string text) =>
            default(RT).FileEff.Map(rt => rt.WriteAllText(path, text));
    }

They use default(RT). And so we won't have any state if we simply wrote:

    // This will fail
    public Eff<TestRuntime, FileIO> FileEff => 
        SuccessEff(Env.Files);

The environmental state will be available when the computation runs, it isn't available when the pure computation is being built. And so, we create an 'inline effect', that will be run when the computation runs:

    // This will work
    public Eff<TestRuntime, FileIO> FileEff => 
        Eff<TestRuntime, FileIO>(static rt => new TestFileIO(rt.Env.Files));

rt is the real stateful runtime, and so we can access the members to initialise the stateful TestFileIO.

Built-in functionality

LanguageExt.Sys

The LanguageExt.Sys nu-get package is where all of the built-in traits, bar HasCancel, are defined (HasCancel is the only trait in LanguageExt.Core). It also includes live and test implementations of those traits.

On top of the implementations of the traits, there are two pre-built runtimes:

You can use these without having to build your own runtimes, but you will be limited to only the IO functionality that language-ext provides out-of-the-box. Instead you should copy these as the basis for your own application-runtime, and supplement them with any additional IO functionality you need. You should only need one runtime for any application (and a test runtime, for unit-tests), remember we're trying to describe the real-world, and there's only one of those.

Purists may argue that having progressively more constrained runtimes might help, especially across domain boundaries. This makes sense, but note you will need to map to those more constrained runtimes, and I'm not sure it's really necessary for the vast majority of use-cases. The main way to constrain what sub-systems do is to constrain the functions in your application to only use the Has.. traits that they need.

At the time of writing the following traits exist:

  • HasCancel - provides a cancellation token for asynchronous operations - this is a requirement for all Aff usage
  • HasConsole - provides an interface to System.Console
    • The test implementation is a fully in-memory console, with the ability to iterate over what's in the console feed and write mocked key-presses
  • HasFile - provides an interface to System.IO.File
    • The test implementation is a fully in-memory file-system
  • HasDirectory - provides an interface to System.IO.Directory
    • The test implementation is a fully in-memory file-system
  • HasEncoding - threads a text-encoding configuration through the computation
  • HasTextRead - provides an interface to System.IO.TextReader
  • HasTime - provides an interface to DateTime.Now, DateTime.UtcNow, Task.Delay, etc.
    • The test implementation allows time to stop completely, or run from a fixed point in time
  • HasEnvironment - provides an interface to System.Environment
    • The test implementation is entirely in-memory, and can be initialised from the machine's real environment but then updated with mocked data

As well as the traits, the live, and test implementations, there's also the static wrapper functions to make use easy.

Some examples

Here's a simple example of reading a line from the console and writing it back out

using LanguageExt;
using LanguageExt.Sys;
using LanguageExt.Sys.Live;
using static LanguageExt.Prelude;
using static LanguageExt.Sys.Console<LanguageExt.Sys.Live.Runtime>;

namespace SimpleExample
{
    internal class Program
    {
        public static void Main(string[] args) =>
            Main().Run(Runtime.New())
                  .ThrowIfFail();
        
        static Eff<Runtime, Unit> Main() =>
            repeat(from l in readLine
                   from _ in writeLine(l)
                   select unit);
    }
}

Note, how I've baked the Runtime in with a using static for access to Sys.Console. This simple application isn't really supporting any injectable IO., but it is abstracting away from side-effects.

The using static technique is quite useful when building unit-tests, because you know you're always going to be using the test-runtime. For the main application to make it fully unit-testable, you want to do something like this:

using LanguageExt;
using LanguageExt.Sys;
using LanguageExt.Sys.Live;
using LanguageExt.Sys.Traits;
using static LanguageExt.Prelude;

namespace SimpleExample
{
    class Program
    {
        public static void Main(string[] args) =>
            Main<Runtime>().Run(Runtime.New())
                           .ThrowIfFail();
        
        static Eff<RT, Unit> Main<RT>() 
            where RT : struct, HasConsole<RT> =>
                repeat(from l in Console<RT>.readLine
                       from _ in Console<RT>.writeLine(l)
                       select unit);
    }
}

It's starting to look like the Haskell main! That is intentional. But now Main<RT>() is also unit testable, by providing a test-runtime as the RT parameter. And in fact there's a unit-test that looks just like this in the language-ext unit-tests, it uses mocked data to pretend a user is writing the text.

One other interesting part of this is the repeat function. It will continually re-run the Eff or Aff computation until it fails. In our case it simply loops: waiting for user input, printing it to the console, then going back around again. Note, there are fluent variants of all the functions, i.e.

(from l in Console<RT>.readLine
 from _ in Console<RT>.writeLine(l)
 select unit)
.Repeat();

There are many support functions for Aff and Eff that are explicitly in-place to support effectful operations. This is what I meant by being more opinionated about an approach. The other IO like monadic types are more general, the Aff and Eff types are being built for the sole reason of representing effects, and mostly IO effects.

The LanguageExt.Sys nu-get package will expand over the coming years to cover as much of the .NET BCL as possible, to help functional programmers get a head-start with handling side-effects. Each trait will have both a 'live' and a 'test' implementation provided by default. I would also expect users of this library to start building their own libraries that wrap around (lift) common projects that have side-effects into the Aff and Eff space.

Lifting pure functions into the Aff and Eff

One aspect of using the Aff and Eff monads (with a runtime) is that we're forced to declare the traits we need upfront in function and method constraints. This might seem quite annoying to use, especially as it's more effort for every side-effecting function you will write. To a certain extent this is intentional. The idea is to encourage you to partition your application properly: split out the pure functionality from your side-effecting functionality.

If we try to reproduce the lifting of the, pure, Capitalise function from earlier we would do this:

static Eff<RT, Unit> Main<RT>() 
    where RT : struct, HasConsole<RT> =>
        repeat(from l in Console<RT>.readLine
               from u in SuccessEff(Capitalise(l))
               from _ in Console<RT>.writeLine(u)
               select unit);

// A pure function for turning all lower-case characters into upper-case characters
static string Capitalise(string text) => 
    new string(text.Map(x => Char.IsLower(x) ? Char.ToUpper(x) : x).ToArray());

The Capitalise function doesn't need a runtime, it doesn't need any constraints, it is pure.

Some of the awkwardness of the Aff and Eff monads will hopefully take you down a much more principled software development route.

Working with other monads within an Aff or Eff context

One of the core values of this library is to make an ecosystem that just works. Each monadic type has a ToEff or ToAff (where appropriate) which does a natural transformation between the types. For example:

static Eff<RT, Unit> Main<RT>() 
    where RT : struct, HasConsole<RT> =>
        repeat(from l in Console<RT>.readLine
               from v in readInteger(l)
               from _ in Console<RT>.writeLine($"{v}")
               select unit);

static Eff<int> readInteger(string line) =>
    parseInt(line)
       .ToEff(Error.New("Integers only please"));

Here we take the parseInt function from the language-ext Prelude. It returns an Option<int>. We use ToEff with a default error value to transform it into a non-runtime Eff. This example will continue running whilst the user types integer values, and will fail with "Integers only please" when a non-integer string is typed.

The non-runtime Eff and Aff types are there to represent pure values and pure functions that are lifted into the effectful space. You can use them to wrap side-effects too, but then they're acting like Try<A> or the first IO<A> defined in this post: they're not easily unit testable. So it's better to use them to lift pure values and functions only.

Error handling and filtering

How to fail within an Aff or Eff context

Sometimes we will want a computation to end. We don't want to throw exceptions (although those will be caught automatically by the Eff and Aff monad); exceptions should be for exceptional events, often failure is expected.

Internally Aff and Eff uses the LanguageExt.Common.Error type. This has support for exceptional and non-exceptional errors (through error code and messages).

When the Aff and Eff monads run, they result in a Fin<A>. The Fin monad is equivalent to Either<Error, A>, i.e. it can either be an Error or an A. This is how success or failure is made concrete when a pure Aff or Eff is run.

And so to fail, we need to propagate an Error. This is done with FailEff or FailAff:

static Eff<RT, Unit> Main<RT>() 
    where RT : struct, HasConsole<RT> =>
        repeat(from l in Console<RT>.readLine
               from d in l == "" 
                             ? FailEff<Unit>(Error.New("user exited"))
                             : unitEff
               from _ in Console<RT>.writeLine(l)
               select unit);

Here we're testing if the user typed anything, if they didn't we fail with "user exited". The FailEff is using a Unit bound-type, and so we can use the built-in unitEff, which is an Eff that always successfully returns a Unit value (there's also trueEff and falseEff) - that means, continue processing because we have a value.

Guards

This pattern of testing a predicate and then choosing SuccessEff<Unit> or FailEff<Unit> looks a lot like a where operation, and it is very similar, except where can't provide an alternative value (the error). This pattern is so common that there is a built in way to do this: guard:

repeat(from l in Console<RT>.readLine
       from f in guard(l != "", Error.New("user exited"))
       from _ in Console<RT>.writeLine(l)
       select unit);

guard will only allow the computation to continue if the predicate value is true, otherwise it fails with the error value. guardnot is the same but only allows the computation to continue if the predicate value is false:

repeat(from l in Console<RT>.readLine
       from f in guardnot(l == "", Error.New("user exited"))
       from _ in Console<RT>.writeLine(l)
       select unit);

And so this allows us to shortcut out of the computation, like None in Option, or Left in Either.

How to catch errors from Aff or Eff sub-expressions

Clearly we won't want the entire application to end every time we get an error. We need strategies for dealing with errors in sub-expressions, something like catch, but functional:

First approach is the | operator, which will lazily coalesce:

static Eff<RT, Unit> Main<RT>()
    where RT : struct, HasConsole<RT> =>
    AskUser<RT>() | AskUser<RT>() | FailEff<Unit>(Error.New("user really exited"));

static Eff<RT, Unit> AskUser<RT>() 
    where RT : struct, HasConsole<RT> =>
        repeat(from l in Console<RT>.readLine
               from f in guardnot(l == "", Error.New("user exited"))
               from _ in Console<RT>.writeLine(l)
               select unit);

Here we've slightly changed the implementation, we've renamed the Main function to AskUser and added a new Main function. The new Main function now expects the user to enter two empty lines before really failing.

We could use SuccessEff to catch any errors and provide a default value if necessary:

    AskUser<RT>() | AskUser<RT>() | SuccessEff(unit);

That provides a default value of unit, and will stop the real entry Main function from throwing an exception. Obviously, you can use any default value you like as long as the types align with the other operands in the | expression.

The above can also be written:

    AskUser<RT>() | AskUser<RT>() | unitEff;

This approach is very useful, but other monadic types allow you to use various combinations of Match, IfFail, IfLeft, etc. to get a concrete value out of them (and deal with errors). Aff and Eff have a similar set of functions for handling the two cases: Succ and Fail, however they won't give a concrete value, they themselves will return an Aff or an Eff:

static Eff<RT, Unit> Main<RT>()
    where RT : struct, HasConsole<RT> =>
        from r in AskUser<RT>().Match(Succ: x => "success",
                                      Fail: e => "failure")
        from _ in Console<RT>.writeLine(r)
        select unit;

Here we map both the success and error values to a string, we then continue. The Console<RT>.writeLine(r) will run with the value of r.

It is also possible to run sub-expressions in the Match cases (using MatchEff and MatchAff):

static Eff<RT, Unit> Main<RT>()
    where RT : struct, HasConsole<RT> =>
        AskUser<RT>().MatchEff(Succ: x => Console<RT>.writeLine($"{x}"),
                               Fail: e => Console<RT>.writeLine($"{e}"));

This allows for branching based on the success or failure state, and recovery from error if required.

There are lots of additional helper overloads for MatchEff and MatchAff. For example, if we didn't care about the values of the match and just want to branch:

static Eff<RT, Unit> Main<RT>()
    where RT : struct, HasConsole<RT> =>
        AskUser<RT>().MatchEff(Succ: Console<RT>.writeLine($"success"),
                               Fail: Console<RT>.writeLine($"failure"));

Or, if we want to deal with just the error case:

static Eff<RT, Unit> Main<RT>()
    where RT : struct, HasConsole<RT> =>
        AskUser<RT>().IfFailEff(e => Console<RT>.writeLine($"{e}"));

This just prints the error to the console and then continues on without ending the computation.

@catch

Using Match or IfFail is a valid approach to dealing with errors, but in the real-world there's often different types of errors for any one sub-system, and we may want to deal with them differently. We may, from the previous example, consider the user-exiting to be something we can safely ignore - but any exceptions we may want to handle differently.

Here's a refactored version of the previous example. I have moved the RT to the class definition, which simplifies the definitions of Main and AskUser: now they're properties that return an Eff computation:

public class AffTests<RT>
    where RT : struct, HasConsole<RT>
{
    static readonly Error UserExited = Error.New(100, "user exited");
    static readonly Error SafeError = Error.New(200, "there was a problem");
    
    public static Eff<RT, Unit> main =>
        from _1 in askUser
                      | @catch(ex => ex is SystemException, Console<RT>.writeLine("system error"))
                      | @catch(SafeError)
        from _2 in Console<RT>.writeLine("goodbye")
        select unit;

    static Eff<RT, Unit> askUser =>
        repeat(from ln in Console<RT>.readLine
               from _1 in guard(notEmpty(ln), UserExited)
               from _2 in guardnot(ln == "sys", () => throw new SystemException())
               from _3 in guardnot(ln == "err", () => throw new Exception())
               from _4 in Console<RT>.writeLine(ln)
               select unit)
             | @catch(UserExited, unit);
}

There are a few other things to note here:

  • Two error fields have been defined: UserExited and SafeError
    • By predefining our errors, we can use them to match upon
    • By using error codes, the matching will be done using just those codes, not the text
      • Useful if localising error messages
  • In the main computation we're using @catch to match on the type of error resulting from askUser:
    • The first one matches on an exceptional error of SystemException, if it matches it logs a message to the console, this flips the error into a SuccessEff(unit) (because Console.writeLine will always succeed)
    • The second one matches on any error and will change it to SafeError
      • This can be useful when you don't want to leak sensitive error information across a domain boundary.
      • It is also functionally equivalent to the earlier example of AskUser<RT>() | FailEff<Unit>(SafeError)
  • The askUser computation now has three guards in it:
    • One to test for the user pressing enter, which produces a non-exceptional error
    • One to test if the user typed "sys" and if so, throw a SystemException
    • One to test if the user typed "err" and if so, throw an Exception
    • It also has a @catch in there. We know that repeat will only stop repeating when the inner computation is in an errored state. And so, we catch the error we expect (UserExited) and flip it to be a SuccessEff(unit).

The result of running this is that if the user presses enter on an empty line, they will see the message "goodbye" shown in the console. If they type "sys" they will see "system error" on the screen, followed by "goodbye"; and if they type "err" the computation will end without printing "goodbye" and it will be in a failure state of Error(200, "there was a problem").

@catch therefore is a very powerful way of matching on the exceptional and non-exceptional errors that come out of a side-effecting computation. It allows for errors to be flipped to successes, and allows for errors to be sanitised or processed before being passed on. It works, and feels, a lot like catching exceptions in 'imperative-land', but it's purely functional and has power to work with non-exceptional errors in a way that throwing exceptions can never do.

API overview

cancel

Description

Cancels the current running Aff<RT, A> and any forked processes by triggering the cancelation token in the runtime.

Definition

Aff<RT, A> cancel<RT>();

Example

    public class CancelExample<RT>
        where RT: struct, 
        HasCancel<RT>, 
        HasConsole<RT>
    {
        public static Aff<RT, Unit> main =>
            repeat(from k in Console<RT>.readKey
                   from _ in k.Key == ConsoleKey.Enter
                                 ? cancel<RT>()
                                 : unitEff
                   from w in Console<RT>.write(k.KeyChar)
                   select unit);
    }

fold

Description

Repeatedly calls the ma target effect to retrieve a stream of A values. Each one is passed to the fold delegate, along with an aggregate state.

  • The fold operation will end when if an error is raised by the ma operation
  • If the Schedule expires, then the latest S state will be returned
  • Or, if the predicate triggers (true → false for foldWhile and false → true for foldUntil)
    • This also returns the latest S state

Fluent variant

There's .Fold, .FoldWhile, and .FoldUntil fluent variants that work with Aff and Eff instances.

Definitions

Aff<S> fold<S, A>(Aff<A> ma, S state, Func<S, A, S> fold);
Eff<S> fold<S, A>(Eff<A> ma, S state, Func<S, A, S> fold);
Aff<S> fold<S, A>(Schedule schedule, Aff<A> ma, S state, Func<S, A, S> fold);
Eff<S> fold<S, A>(Schedule schedule, Eff<A> ma, S state, Func<S, A, S> fold);

Aff<RT, S> fold<RT, S, A>(Aff<RT, A> ma, S state, Func<S, A, S> fold);
Eff<RT, S> fold<RT, S, A>(Eff<RT, A> ma, S state, Func<S, A, S> fold);
Aff<RT, S> fold<RT, S, A>(Schedule schedule, Aff<RT, A> ma, S state, Func<S, A, S> fold);
Eff<RT, S> fold<RT, S, A>(Schedule schedule, Eff<RT, A> ma, S state, Func<S, A, S> fold);

Aff<S> foldWhile<S, A>(Aff<A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Eff<S> foldWhile<S, A>(Eff<A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Aff<S> foldWhile<S, A>(Schedule schedule, Aff<A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Eff<S> foldWhile<S, A>(Schedule schedule, Eff<A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);

Aff<RT, S> foldWhile<RT, S, A>(Aff<RT, A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Eff<RT, S> foldWhile<RT, S, A>(Eff<RT, A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Aff<RT, S> foldWhile<RT, S, A>(Schedule schedule, Aff<RT, A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Eff<RT, S> foldWhile<RT, S, A>(Schedule schedule, Eff<RT, A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);

Aff<S> foldUntil<S, A>(Aff<A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Eff<S> foldUntil<S, A>(Eff<A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Aff<S> foldUntil<S, A>(Schedule schedule, Aff<A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Eff<S> foldUntil<S, A>(Schedule schedule, Aff<A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);

Aff<RT, S> foldUntil<RT, S, A>(Aff<RT, A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Eff<RT, S> foldUntil<RT, S, A>(Eff<RT, A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Aff<RT, S> foldUntil<RT, S, A>(Schedule schedule, Aff<RT, A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);
Eff<RT, S> foldUntil<RT, S, A>(Eff<RT, A> ma, S state, Func<S, A, S> fold, Func<A, bool> pred);

fork

Description

Causes the effect to run without being awaited. This allows for many sub-tasks to be launched without stalling a parent task.

The Aff<RT, A> version of this will return an Eff<Unit> as its bound value. This Eff<Unit> can be used to cancel the running forked operation. Aff<A> effects can't be cancelled and are effectively fire and forget.

Fluent variant

There's a .Fork fluent variant that works with Aff instances

Definitions

Eff<RT, Eff<Unit>> fork<RT, A>(Aff<RT, A> ma)
Eff<Unit> fork<A>(Aff<A> ma)

Example

This example forks the inner effect, which loops over digit ten times, each with a delay of 1 second. Each time it prints a * to the screen. After ten iterations the sum of 1+1+1+1+1+1+1+1+1+1 is shown on the screen.

The parent effect (that launched the forked operation) will immediately continue to the readKey stage, waiting for a key-press. If the user presses a key before the 10 seconds is up, then cancel runs and forces the forked operation to quit - therefore not showing the total.

public class ForkCancelExample<RT>
    where RT: struct, 
    HasCancel<RT>, 
    HasConsole<RT>, 
    HasTime<RT>
{
    public static Aff<RT, Unit> main =>
        from cancel  in fork(inner)
        from key     in Console<RT>.readKey
        from _1      in cancel 
        from _2      in Console<RT>.writeLine("done")
        select unit;

    static Aff<RT, Unit> inner =>
        from x in sum
        from _ in Console<RT>.writeLine($"total: {x}")
        select unit;
    
    static Aff<RT, int> sum =>
        digit.Fold(Schedule.Recurs(10) | Schedule.Spaced(1*second), 0, (s, x) => s + x);

    static Aff<RT, int> digit =>
        from one in SuccessAff<RT, int>(1)
        from _   in Console<RT>.writeLine("*")
        select one;
}

repeat

Description

Repeats the effect until:

  • An error occurs - the error is propagated
  • The Schedule expires - the last value from the effect is propagated
  • The predicate returns false for repeatWhile - the last value from the effect is propagated
  • The predicate returns true for repeatUntil - the last value from the effect is propagated

NOTE: Using a Schedule that has a time-delay in will block synchronous effect threads (Eff). It's best to use schedule delays with Aff

Fluent variant

There's a .Repeat fluent variant that works with Aff and Eff instances

Definitions

Aff<A> repeat<A>(Aff<A> ma);
Eff<A> repeat<A>(Eff<A> ma);
Aff<A> repeat<A>(Schedule schedule, Aff<A> ma);
Eff<A> repeat<A>(Schedule schedule, Eff<A> ma);

Aff<RT, A> repeat<RT, A>(Aff<RT, A> ma);
Eff<RT, A> repeat<RT, A>(Eff<RT, A> ma);
Aff<RT, A> repeat<RT, A>(Schedule schedule, Aff<RT, A> ma);
Eff<RT, A> repeat<RT, A>(Schedule schedule, Eff<RT, A> ma);

Aff<A> repeatWhile<A>(Aff<A> ma, Func<A, bool> predicate);
Eff<A> repeatWhile<A>(Eff<A> ma, Func<A, bool> predicate);
Aff<A> repeatWhile<A>(Schedule schedule, Aff<A> ma, Func<A, bool> predicate);
Eff<A> repeatWhile<A>(Schedule schedule, Eff<A> ma, Func<A, bool> predicate);

Aff<RT, A> repeatWhile<RT, A>(Aff<RT, A> ma, Func<A, bool> predicate);
Eff<RT, A> repeatWhile<RT, A>(Eff<RT, A> ma, Func<A, bool> predicate);
Aff<RT, A> repeatWhile<RT, A>(Schedule schedule, Aff<RT, A> ma, Func<A, bool> predicate);
Eff<RT, A> repeatWhile<RT, A>(Schedule schedule, Eff<RT, A> ma, Func<A, bool> predicate);

Aff<A> repeatUntil<A>(Aff<A> ma, Func<A, bool> predicate);
Eff<A> repeatUntil<A>(Eff<A> ma, Func<A, bool> predicate);
Aff<A> repeatUntil<A>(Schedule schedule, Aff<A> ma, Func<A, bool> predicate);
Eff<A> repeatUntil<A>(Schedule schedule, Eff<A> ma, Func<A, bool> predicate);

Aff<RT, A> repeatUntil<RT, A>(Aff<RT, A> ma, Func<A, bool> predicate);
Eff<RT, A> repeatUntil<RT, A>(Eff<RT, A> ma, Func<A, bool> predicate);
Aff<RT, A> repeatUntil<RT, A>(Schedule schedule, Aff<RT, A> ma, Func<A, bool> predicate);
Eff<RT, A> repeatUntil<RT, A>(Schedule schedule, Eff<RT, A> ma, Func<A, bool> predicate);

Example

This example repeatedly writes the current time to the console. Each loop there's a delay that grows using the fibonnaci sequence, and therefore gets larger and larger, it is clamped to a maximum delay of 10 seconds. After 15 iterations the process ends.

public class TimeExample<RT>
    where RT : struct, 
    HasTime<RT>, 
    HasCancel<RT>, 
    HasConsole<RT>
{
    public static Eff<RT, Unit> main =>
        repeat(Schedule.Spaced(10 * second) | Schedule.Recurs(15) | Schedule.Fibonacci(1*second),
               from tm in Time<RT>.now
               from _1 in Console<RT>.writeLine(tm.ToLongTimeString())
               select unit);
}

retry

Description

Retries the effect until the effect succeeds, unless:

  • The Schedule expires - the last error from the effect is propagated
  • The predicate returns false for retryWhile - the last error from the effect is propagated
  • The predicate returns true for retryUntil - the last error from the effect is propagated

NOTE: Using a Schedule that has a time-delay in will block synchronous effect threads (Eff). It's best to use schedule delays with Aff

Fluent variant

There's a .Retry fluent variant that works with Aff and Eff instances

Definitions

Aff<A> retry<A>(Aff<A> ma);
Eff<A> retry<A>(Eff<A> ma);
Aff<A> retry<A>(Schedule schedule, Aff<A> ma);
Eff<A> retry<A>(Schedule schedule, Eff<A> ma);

Aff<RT, A> retry<RT, A>(Aff<RT, A> ma);
Eff<RT, A> retry<RT, A>(Eff<RT, A> ma);
Aff<RT, A> retry<RT, A>(Schedule schedule, Aff<RT, A> ma);
Eff<RT, A> retry<RT, A>(Schedule schedule, Eff<RT, A> ma);

Aff<A> retryWhile<A>(Aff<A> ma, Func<Error, bool> predicate);
Eff<A> retryWhile<A>(Eff<A> ma, Func<Error, bool> predicate);
Aff<A> retryWhile<A>(Schedule schedule, Aff<A> ma, Func<Error, bool> predicate);
Eff<A> retryWhile<A>(Schedule schedule, Eff<A> ma, Func<Error, bool> predicate);

Aff<RT, A> retryWhile<RT, A>(Aff<RT, A> ma, Func<Error, bool> predicate);
Eff<RT, A> retryWhile<RT, A>(Eff<RT, A> ma, Func<Error, bool> predicate);
Aff<RT, A> retryWhile<RT, A>(Schedule schedule, Aff<RT, A> ma, Func<Error, bool> predicate);
Eff<RT, A> retryWhile<RT, A>(Schedule schedule, Eff<RT, A> ma, Func<Error, bool> predicate);

Aff<A> retryUntil<A>(Aff<A> ma, Func<Error, bool> predicate);
Eff<A> retryUntil<A>(Eff<A> ma, Func<Error, bool> predicate);
Aff<A> retryUntil<A>(Schedule schedule, Aff<A> ma, Func<Error, bool> predicate);
Eff<A> retryUntil<A>(Schedule schedule, Eff<A> ma, Func<Error, bool> predicate);

Aff<RT, A> retryUntil<RT, A>(Aff<RT, A> ma, Func<Error, bool> predicate);
Eff<RT, A> retryUntil<RT, A>(Eff<RT, A> ma, Func<Error, bool> predicate);
Aff<RT, A> retryUntil<RT, A>(Schedule schedule, Aff<RT, A> ma, Func<Error, bool> predicate);
Eff<RT, A> retryUntil<RT, A>(Schedule schedule, Eff<RT, A> ma, Func<Error, bool> predicate);

Example

This example repeatedly writes the current time to the console. Each loop there's a delay that grows using the fibonnaci sequence, and therefore gets larger and larger, it is clamped to a maximum delay of 10 seconds. After 15 iterations the process ends.

This example asks the user to say hello. If they don't type "hello" then an error is raised. This is caught by the retry and the question is asked again.

There's a Schedule that states we must retry at-most 5 times. If the user doesn't answer with "hello" in 5 attempts then the error is propagated.

public class RetryExample<RT>
    where RT : struct, 
    HasCancel<RT>, 
    HasConsole<RT>
{
    readonly static Error Failed = 
        ("I asked you to say hello, and you can't even do that?!");
        
    public static Eff<RT, Unit> main =>
        retry(Schedule.Recurs(5),
              from _ in Console<RT>.writeLine("Say hello")
              from t in Console<RT>.readLine
              from e in guard(t == "hello", Failed)  
              from m in Console<RT>.writeLine("Hi")
              select unit);
}

runtime

[TODO]

Sequence

[TODO]

timeout

[TODO]

use

Constructing Aff and Eff on the fly

[TODO]

  • AffMaybe(..)
  • Aff(..)
  • EffMaybe(..)
  • Eff(..)
  • SuccessEff(..)
  • FailEff(..)

Asynchrony

[TODO]

  • How Aff and Eff work together
  • Parallel processing
  • Infinite folds

Streams

[TODO]

Events and Observables

[TODO]

Composition

[TODO]

  • Using LINQ and the || operation

Resource managment

[TODO]

  • use

Structuring your program

[TODO]

  • Run once in Main
  • Run on each web-request

WIP -- more to come

Clone this wiki locally