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 give 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 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> member ContinueWith: continuationAction: Action<Task<'TResult>,obj> * state: obj -> Task + 19 overloads member GetAwaiter: unit -> TaskAwaiter<'TResult> member WaitAsync: cancellationToken: CancellationToken -> Task<'TResult> + 2 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

--------------------
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>

--------------------
[<Struct>] 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 (<*>) : f: Effect<'a3,('a4 -> 'a5),'a6> * e: Effect<'a3,'a4,'a6> -> Effect<'a3,'a5,'a6> 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: 'a3 -> Effect<'a4,'a3,'a5> static member ap: 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="'r" />, or fails with <typeparamref name="'e" />. The effect is is 'cold', and only starts when run with an <typeparamref name="'r" />. </summary>
<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>
<returns></returns>
static member Effect.Create: f: ('a -> 'b) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Async<'b>) -> Effect<'a,'b,'a2>
static member Effect.Create: f: ('a -> Result<'b,'e>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Task<'b>) -> Effect<'a,'b,'c>
static member Effect.Create: f: ('a -> ValueTask<'b>) -> Effect<'a,'b,'a2>
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
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