/Orsak

Primary LanguageF#MIT LicenseMIT

Orsak


eff {
    let! line = Effects.Console.readLine()
    do! Effect.Console.writeLine $"Hello {line}"
}

Orsak is a library for function effects in F#, allowing pragmatic and composable modelling of side-effects through interfaces. It is inspired by Zio, with F# encoding similar to the approach described by Bartosz Sypytkowski here. The library intends to be as complete as is practical, including a computation expression, supporting functions and operators, and should be compatible with FSharpPlus.

However, Orsak is not intended to be a framework. Rather, it is encouraged and recommended to make use of the existing .NET ecosystem, and Orsak is intended to make that as easy and friction-free as possible. It can be used piecemeal, and introduced to existing codebases in isolated chunks.


This implementation makes use of resumable code to implement the computation expression builder for the lowest possible overhead. The builder also has various overloads for Bind, allowing for friction free interop with TaskLike<'a>, Async<'a>, Result<'T,'E> and more. It supports for, while, use, try-with and try-finally, and also applicative bind when the error-type supports +. While the computation expression is the intended way of working with effects, the corresponding module should contain all the expected functions.

NuGet Badge Build & Tests

Show me the code


Imagine having the following interfaces, which each describes some none-pure operation, that is, they both have side-effects.

type IButtonPusher =
    abstract member PushButton: unit -> Task<unit>
    
type IBeanCounter =
    abstract member CountBeans: unit -> Task<int>

By creating the following helper-interfaces

type IButtonPusherProvider =
    abstract member ButtonPusher: IButtonPusher
    
type IBeanCounterProvider =
    abstract member BeanCounter: IBeanCounter

and the following helpers

let pushButton () =
    Effect.Create(fun (provider: #IButtonPusherProvider) -> provider.ButtonPusher.PushButton())
    
let countBeans () =
    Effect.Create(fun (provider: #IBeanCounterProvider) -> provider.BeanCounter.CountBeans())

then it is possible to partially apply, or 'inject' these interfaces into your code by simply making functions calls:

let pushAndCount<'a, 'b when 'a :> IButtonPusherProvider and 'a :> IBeanCounterProvider> ()
    : Effect<'a, int, 'b> =
    eff {
        do! pushButton ()
        let! beans = countBeans ()
        return beans * 2
    }

In essence, this is a partially applied function, waiting for an instance of 'a which implements both IButtonPusherProvider and IBeanCounterProvider. Now, in practise you do not need to specify any of this, the type inference will keep track of the requirements of your provider.

let pushAndCount () =
    eff {
        do! pushButton()
        let! beans = countBeans()
        return beans * 2
    }

And adding further calls to functions with side-effects will apply the correct constraints seamlessly.


Main documentation is available at https://johsand.github.io/Orsak/