Logo Orsak

In the beginning, there was OOP

Let's say you have an application that is architected according to Functional Core, imperative shell. And all the functional code is easy to test and easy to reuse. But in the imperative shell, how do we compose the side-effectful code?

If you are coming from an object-oriented background, you might be thinking about SOLID. Certainly that was my background. So I made IRepositories and IClients and IMessageHandlers, and injected them into IDomainService and IThisService and IThatService, and all that jazz. And inevitably I ended up with too many constructor-parameters, and interfaces with too many members, and weird code duplication. And at the end of the day I thought to my self "There must be a better way. If this is the Right Waytm, why is it so hard?"

In the end, I believe that while this is the least bad way of doing oo-programming, it is not enough.

Putting the 'fun' in programming

If the I in solid is taken to its natural conclusion, your interfaces end up with a single member. Now, this is not an original observation, but one I believe merits repeating. But in OO the object is the natural method of composition, and even with dependency injection creating SRP-conforming objects quickly becomes tedious. But given that objects are merely a poor man's closures, maybe we can unlock reuse and composability some other way?

type IMessageClient =
    abstract member FetchData: string -> Task<string>

type IMessageClientProvider =
    abstract member Client: IMessageClient

module MessageClient =
    let fetchData s =
        Effect.Create(fun (provider: #IMessageClientProvider) -> provider.Client.FetchData s)

type IMessageRepository =
    abstract member StoreData: string -> Task<unit>

type IMessageRepositoryProvider =
    abstract member Repository: IMessageRepository

module MessageRepository =
    let storeData s =
        Effect.Create(fun (provider: #IMessageRepositoryProvider) -> provider.Repository.StoreData s)

let fetchAndStore s = eff {
    let! data = MessageClient.fetchData s
    do! MessageRepository.storeData data
}

If functions are objects with a single method, we can view the function above as some bespoke domain-service, where injecting dependencies is as easy as just calling a function, while maintaining the benefits of testability and customizability of dependency injection. And in turn, reusing that bespoke service is as easy as calling a function. All dependencies are added and tracked automatically, and are visible in the function signature.

And beyond reuse, it also unlocks aspect-like capabilities, but without any magic. Since the effects only execute when run, they can be safely passed around, and binding them with 'let!' creates natural join points.

let weaver (beforeAdvice: Effect<_, _, _>) (effect: Effect<_, _, _>) (afterAdvice: Effect<_, _, _>) = eff {
    do! beforeAdvice
    let! result = effect
    do! afterAdvice
    return result
}
namespace Orsak
namespace System
namespace System.Threading
namespace System.Threading.Tasks
type IMessageClient = abstract FetchData: string -> Task<string>
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
Multiple items
type Task = interface IAsyncResult interface IDisposable new: action: Action -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable + 1 overload member ContinueWith: continuationAction: Action<Task,obj> * state: obj -> Task + 19 overloads member Dispose: unit -> unit member GetAwaiter: unit -> TaskAwaiter member RunSynchronously: unit -> unit + 1 overload member Start: unit -> unit + 1 overload member Wait: unit -> unit + 5 overloads ...
<summary>Represents an asynchronous operation.</summary>

--------------------
type Task<'TResult> = inherit Task new: ``function`` : Func<obj,'TResult> * state: obj -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable<'TResult> + 1 overload member ContinueWith: continuationAction: Action<Task<'TResult>,obj> * state: obj -> Task + 19 overloads member GetAwaiter: unit -> TaskAwaiter<'TResult> member WaitAsync: cancellationToken: CancellationToken -> Task<'TResult> + 4 overloads member Result: 'TResult static member Factory: TaskFactory<'TResult>
<summary>Represents an asynchronous operation that can return a value.</summary>
<typeparam name="TResult">The type of the result produced by this <see cref="T:System.Threading.Tasks.Task`1" />.</typeparam>


--------------------
Task(action: System.Action) : Task
Task(action: System.Action, cancellationToken: System.Threading.CancellationToken) : Task
Task(action: System.Action, creationOptions: TaskCreationOptions) : Task
Task(action: System.Action<obj>, state: obj) : Task
Task(action: System.Action, cancellationToken: System.Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task
Task(action: System.Action<obj>, state: obj, cancellationToken: System.Threading.CancellationToken) : Task
Task(action: System.Action<obj>, state: obj, creationOptions: TaskCreationOptions) : Task
Task(action: System.Action<obj>, state: obj, cancellationToken: System.Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task

--------------------
Task(``function`` : System.Func<'TResult>) : Task<'TResult>
Task(``function`` : System.Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(``function`` : System.Func<'TResult>, cancellationToken: System.Threading.CancellationToken) : Task<'TResult>
Task(``function`` : System.Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : System.Func<obj,'TResult>, state: obj, cancellationToken: System.Threading.CancellationToken) : Task<'TResult>
Task(``function`` : System.Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : System.Func<'TResult>, cancellationToken: System.Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : System.Func<obj,'TResult>, state: obj, cancellationToken: System.Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
type IMessageClientProvider = abstract Client: IMessageClient
val fetchData: s: string -> Effect<#IMessageClientProvider,string,'b>
val s: string
Multiple items
union case Effect.Effect: EffectDelegate<'r,'a,'e> -> Effect<'r,'a,'e>

--------------------
module Effect from Orsak
<summary> Functions for running, combining, recovering and repeating effects. </summary>

--------------------
type Effect = static member Create: f: ('a -> Task<Result<'b,'e>>) -> Effect<'a,'b,'e> + 2 overloads static member Error: error: 'e -> Effect<'a,'b,'e>
<summary> Creates effects. <c>Effect.Create</c> turns a function of an environment into an effect, and is how effects are made from the interfaces that describe side effects. The function may return a plain value, a <c>Result</c>, a <c>Task</c>, a <c>ValueTask</c> or an <c>Async</c>, of a value or of a <c>Result</c>; returning a <c>Result</c> lets the effect fail. </summary>
<example> The usual pattern: an interface describing the side effect, a provider interface exposing it, and a function creating the effect, which requires any environment that implements the provider (<c>#IConsoleProvider</c>). <code lang="fsharp"> type IConsole = abstract ReadLine: unit -&gt; string abstract WriteLine: string -&gt; unit type IConsoleProvider = abstract Console: IConsole module Console = let readLine () = Effect.Create(fun (p: #IConsoleProvider) -&gt; p.Console.ReadLine()) let writeLine line = Effect.Create(fun (p: #IConsoleProvider) -&gt; p.Console.WriteLine line) </code> The Orsak.Myriad generators can write the functions of such a module from the interface. </example>


--------------------
type Effect<'r,'a,'e> = | Effect of EffectDelegate<'r,'a,'e> member Run: r: 'r -> AsyncResult<'a,'e> member RunOrFail: r: 'r -> Task<'a> static member (<*>)<'env,'arg,'res,'a (requires member (+))> : f: Effect<'env,('arg -> 'res),'a0> * e: Effect<'env,'arg,'a0> -> Effect<'env,'res,'a0> (requires member (+)) static member (>>=) : h: Effect<'r,'a,'e> * f: ('a -> Effect<'r,'b,'e>) -> Effect<'r,'b,'e> static member Map: h: Effect<'r,'a,'e> * f: ('a -> 'b) -> Effect<'r,'b,'e> static member Return<'a,'b,'c> : a: 'a0 -> Effect<'b,'a0,'c> static member ap<'r,'a,'b,'e> : applicative: Effect<'r,('b -> 'a),'e> -> e: Effect<'r,'b,'e> -> Effect<'r,'a,'e>
<summary> Describes an effect that can either succeed with <typeparamref name="'a" />, or fail with <typeparamref name="'e" />. The effect is 'cold', and only starts when run with an <typeparamref name="'r" />. </summary>
<remarks> Effects are usually written with the <c>eff</c> computation expression, and created from interfaces that describe side effects with <c>Effect.Create</c>. The environment <typeparamref name="'r" /> is inferred from what the effect uses, and supplied when the effect is run. </remarks>
<example><code lang="fsharp"> let greet () = eff { let! name = Console.readLine () do! Console.writeLine $"Hello {name}" } // at the composition root, with an environment providing the console effects: let! result = greet () |&gt; Effect.run env </code></example>
<typeparam name="'r"> The environment required to run the effect </typeparam>
<typeparam name="'a"> The resulting type when the effect runs successfully </typeparam>
<typeparam name="'e"> The resulting type when the effect fails</typeparam>
static member Effect.Create: f: ('a -> 'b) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Async<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> Result<'b,'e>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Task<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> ValueTask<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> Async<Result<'b,'e>>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> ValueTask<Result<'b,'e>>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Task<Result<'b,'e>>) -> Effect<'a,'b,'e>
val provider: #IMessageClientProvider
property IMessageClientProvider.Client: IMessageClient with get
abstract IMessageClient.FetchData: string -> Task<string>
type IMessageRepository = abstract StoreData: string -> Task<unit>
type unit = Unit
type IMessageRepositoryProvider = abstract Repository: IMessageRepository
val storeData: s: string -> Effect<#IMessageRepositoryProvider,unit,'b>
val provider: #IMessageRepositoryProvider
property IMessageRepositoryProvider.Repository: IMessageRepository with get
abstract IMessageRepository.StoreData: string -> Task<unit>
val fetchAndStore: s: string -> Effect<'a,unit,'b> (requires 'a :> IMessageRepositoryProvider and 'a :> IMessageClientProvider)
val eff: EffBuilder
<summary> The computation expression for writing effects. Binding (<c>let!</c>, <c>do!</c>, <c>return!</c>) works with other effects, and with <c>Task</c>, <c>ValueTask</c>, <c>Async</c> and <c>Result</c>, so .NET APIs can be used directly. It supports <c>for</c>, <c>while</c>, <c>use</c>, <c>try/with</c> and <c>try/finally</c>, and <c>and!</c> runs effects concurrently. A failed effect stops the rest of the expression, with its error. </summary>
<example><code lang="fsharp"> let placeOrder (order: Order) = eff { let! id = GuidGenerator.genGuid () let! now = Time.utcNow () do! Orders.save { order with Id = id; Placed = now } let! customer = Customers.load order.CustomerId and! stock = Stock.reserve order.Lines return id } </code></example>
val data: string
module MessageClient from 2_Motivation
module MessageRepository from 2_Motivation
val weaver: beforeAdvice: Effect<'a,unit,'b> -> effect: Effect<'a,'c,'b> -> afterAdvice: Effect<'a,unit,'b> -> Effect<'a,'c,'b>
val beforeAdvice: Effect<'a,unit,'b>
val effect: Effect<'a,'c,'b>
val afterAdvice: Effect<'a,unit,'b>
val result: 'c

Type something to start searching.