AlgEff 1.0.0

dotnet add package AlgEff --version 1.0.0
NuGet\Install-Package AlgEff -Version 1.0.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AlgEff" Version="1.0.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add AlgEff --version 1.0.0
#r "nuget: AlgEff, 1.0.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install AlgEff as a Cake Addin
#addin nuget:?package=AlgEff&version=1.0.0

// Install AlgEff as a Cake Tool
#tool nuget:?package=AlgEff&version=1.0.0

AlgEff - Algebraic Effects for F#

What are algebraic effects?

Algebraic effects provide a way to define and handle side-effects in functional programming. This approach has several important benefits:

  • Effects are defined in a purely functional way. This eliminates the danger of unexpected side-effects in otherwise pure functional code.
  • Implementation of effects (via "handlers") is separate from the effects' definitions. You can handle a given effect type multiple different ways, depending on your needs. (For example, you can use one handler for unit tests, and another for production.)

In summary, you can think of algebraic effects as functional programming's answer to dependency injection in object-oriented programming. They solve a similar problem, but in a more functional way.

Why use AlgEff?

AlgEff is one of the few algebraic effect systems for F#. It was inspired by a similar F# library called Eff and by Scala's ZIO. Reasons to use AlgEff:

  • Effects are easy to define.
  • Handlers are easy to define.
  • Programs that use effects and handlers are easy to write.
  • Strong typing reduces the possibility of unhandled effects.

Writing an effectful program

Let's write a simple effectful program that interacts with the user via a console and then logs the result:

let program =
    effect {
        do! Console.writeln "What is your name?"
        let! name = Console.readln
        do! Console.writelnf "Hello %s" name
        do! Log.writef "Name is %s" name
        return name
    }

The type of this value is:

Program<'ctx, string when 'ctx :> LogContext and 'ctx :> ConsoleContext>

The first type parameter ('ctx) indicates that the program requires handlers for both logging and console effects, and the second one (string) indicates that the program returns a string. It's important to understand that this program doesn't actually do anything until it's executed. The program value itself is purely functional -- no side-effects occurred while creating it.

Creating a runtime environment

In order to run this program (and potentially cause actual side-effects), we must define an environment that satisfies the program's requirements:

type ProgramEnv<'ret>() as this =
    inherit Environment<'ret>()

    let handler =
        Handler.combine2
            (PureLogHandler(this))
            (ActualConsoleHandler(this))
    
    interface ConsoleContext
    interface LogContext

    member __.Handler = handler

The important thing to note here is that our environment contains both a log handler (PureLogHandler) and a console handler (ActualConsoleHandler). In this case, we've decided to use a log handler that is purely functional (it doesn't perform any I/O) and a console handler that invokes a real command-line console. We could easily have made other choices.

Running an effectful program

Now that we have both a program and an environment that satisfies its requirements, we can actually run it:

let name, (log, Unit) =
    ProgramEnv().Handler.Run(program)

Running a program returns a 2-tuple where the first element is the value returned by the program (name) and the second element is the final state of the environment's handlers. Because there are two handlers, the final state is itself a 2-tuple containing the log (log) and the console state (Unit here because we used an actual console with side-effects rather than simulating I/O in memory). The resulting console might look like this:

What is your name?
Kristin
Hello Kristin

And the corresponding log would contain a single entry:

Name is Kristin

Defining an effect

To define your own effect, simply inherit a new type from the base Effect type:

/// Logs the given string.
type LogEffect<'next>(str : string, cont : unit -> 'next) =
    inherit Effect<'next>()

    /// Maps a function over this effect.
    override __.Map(f) =
        LogEffect(str, cont >> f) :> _

    /// String to log.
    member __.String = str

    /// Continuation to next effect.
    member __.Cont = cont

Handling an effect

Handling effects is also easy. The following handles log effects by accumulating strings in a list:

type PureLogHandler<'env, 'ret when 'env :> LogContext and 'env :> Environment<'ret>>(env : 'env) =
    inherit SimpleHandler<'env, 'ret, List<string>>()

    /// Start with an empty log.
    override __.Start = []

    /// Adds a string to the log.
    override __.TryStep<'stx>(log, effect, cont : HandlerCont<_, _, _, 'stx>) =
        Handler.adapt effect (fun (logEff : LogEffect<_>) ->
            let log' = logEff.String :: log
            let next = logEff.Cont()
            cont log' next)

    /// Puts the log in chronological order.
    override __.Finish(log) = List.rev log
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.0 543 7/2/2020