3

tldr; how does one deal with logic that depends on data that are too heavy to fetch up-front when subscribing to the functional core, imperative shell line of thought?

Years ago I was inspired by Gary Bernhard's Functional Core - Imperative shell talks (also in Boundaries). When I watched those, the return values of the functions was simply data. Recently, I saw a super-inspiring talk on Javazone (in Norwegian, sorry, but it's not important for the question here) that basically used that pattern fully, though in Clojure. They returned lists of Effects, where an Effect had a type and some associated data. This was essentially a plan of what was to happen in the imperative shell. To me, this is the Command pattern in FP lingo.

Example in Typescript

function planTrip(inspector, date, restaurant): Effect[] {
    return [
        { 
            kind: "db-transact",
            data: [
                { "trip__planned-date":  PlannedDate(date) },
                { "trip__participants":  [ Participant(inspector.id, Role.RESPONSIBLE_INSPECTOR) ] },
                { "trip__preferences":  inspector.preferences },
            ]
        },
        ...(inspector.preferences.syncToCalender? [{ kind: "ms-graph/request", "data": createCalendarPayload(inspector, date) }]: [])
    ]
    
}

That made asserting on what was to happen as easy as asserting on the list of effects, essentially pure data produced by the domain logic (the functional core). No faking or mocking needed. Super nice - in its simplest, most demo-friendly form. What was missing, though, was a generally how such an approach dealt with logic that depend on results from expensive side-effects. Is this where the pattern breaks down in non-fp friendly languages? Usually, you can fetch a lot of data up front, like in the example above, but fetching all data for every permutation in your functional logic could prove very expensive. How would this look in a non-pure (but popular and modern) language like Kotlin or Typescript? This is more interesting, as Bernhard popularised the term using Ruby and "Faux OO".

Contrived, typical procedural/quazi-OO example:

if(allowed){
    if(heavyDbCheck())
        const participants = findParticipantsMatching(criteriaA)
        participants.map(p -> sendNotificationEmail(p))
    } else {
        const participants = findParticipantsMatching(criteriaB)
        participants.map(p -> sendRefusalEmail(p))
    }
}

I know Clojure and other LISPs can assert on the returns, as it is just data, but I cannot think of how to do this as pure data in a non-fp language, resorting to something like closures wrapped in an object.

Something a la this in Java for instance

effects.add(new WhenEffects( () -> heavyDbCheck())
    .then( () -> {
        // db lookup
        return new DbCommand(new FindParticipantsMatching(criteriaA))
            .then(participants -> participants.map(
                        p -> new SendNotificationEmail(p)))
    })
    .else( () -> {
        // db lookup
        return new DbCommand(new FindParticipantsMatching(criteriaB))
            .then(participants -> participants.map(
                        p -> new SendRefusalEmail(p)))
    }));

This kind of composite could then be evaluated again in some effects processor, gradually expanding into more primitive effects, which ultimately would be a list of simpler (non-closure) effects, that could be asserted on. Like

processAndExpand([StoreFoo, WhenEffects, SyncCalendar])
    => (executing StoreFoo and expanding WhenEffects)
        [ChainedDbEffect<FindParticipantsMatching>, SyncCalendar]
    => (executing the ChainedDbEffect)
        [ SendNotificationEmail, SendNotificationEmail, ..., SyncCalendar]

Not sure if I am on to something here?

8
  • Co-incidentally, I had lined up a presentation by Scott Wlaschin on Youtube which covers exactly this, which I watched tonight. The segment is at 40:35: youtube.com/watch?v=P1vES9AgfC4&t=2435s Commented Oct 16 at 18:17
  • 1
    That Effect structure is typically a monad or at least applicative functor, so you can build chains. Not sure if that's the gist of the approach you're looking for, or if you already know that (from fp languages) and are only asking specifically for implementing that in a non-fp language? Commented Oct 16 at 22:40
  • "applicative functor". How to spot a functional programmer ;) I am mostly looking for the most practical way of achieving FP,IS in a non-fp language. It could well be a pragmatic middle-way that does not solve it all the way. Commented Oct 16 at 22:45
  • 1
    I don't know Kotlin, but I wouldn't classify TypeScript as a "non-fp language". Sure, it doesn't enforce purity, but it has first-class functions and creates closures just fine. I'm not exactly sure what you mean by "something like closures wrapped in an object", but yes, ultimately it's always some data structure created (lazily) by the functional core that is getting interpreted by the imperative shell. In TypeScript, you could achive elegant syntax for this by using generator functions, see redux-saga for an example. Commented Oct 16 at 22:54
  • Thanks for good input! Yeah, I meant typical FP languages with loads of syntactical sugar built-in to make stuff like that nice to work with. I quite enjoy TypeScript (and JS), so I would definitely be interested in checking that out as well. What I meant was to have delayed eval in something like Java, I assume you would typically have to create something like a command When(lazyCondition, lazyChoice1, lazyChoice2), where these lazy bits would be what's called lambda expressions in Java (essentially functions are first class objects). Seems to fit what you said. Commented Oct 17 at 9:23

2 Answers 2

4

Since this is a frequently-asked question about functional programming, I wrote a 20-part article series to cover as many aspects of this problem as possible. Unfortunately, it's not in Kotlin, but rather has examples in C#, F#, and Haskell. Even so, I hope you can read enough of at least one of these languages to get something out of the articles.

In short, however, there are plenty of 'patterns' you can apply, each with their own limitations, but fetching data up-front is by far the simplest. As I write in one of those articles, data sets that may seem prohibitively large to humans turn out to be perhaps a few hundred megabytes, and often quite amenable to caching.

If all else fails, however, you can model 'effects' and 'continuations' as free monads, but I consider that a last resort in most languages, because in languages like C# or Java, at least, it would be so unidiomatic that it's hardly warranted. Even in F# I would consider it exotic.


As to the 'specific' example, it's hard to answer, exactly because the devil's in the details. How 'heavy' is heavyCheck? How much data is involved in pre-fetching findParticipantsMatching for both criteria? If not too much, the simplest design is still to load it all up front, but if not, you define a functor that describes the expensive actions, and then turn it into a monad using the free construction.

I'm sorry I don't know how to do this in Kotlin, but I can do it in either Haskell, F#, or C# if you'd like, although the C# version would be highly unidiomatic and complicated.


FWIW, here's a sketch of implementing the above 'procedural/quasi-OO example' in F# using a free monad. I'll mostly follow the free monad recipe, with a few additions to the computation expression builder taken from Song recommendations with F# free monads.

First, we need to define the instruction set. I didn't know exactly what to call it, so just settled on 'Q' for (Stack Overflow) Question.

type QInstruction<'a> =
    | HeavyDbCheck of (bool -> 'a)
    | FindParticipantsMatching of (Criterion * (Participant list -> 'a))
    | SendNotificationEmail of (Participant * (unit -> 'a))
    | SendRefusalEmail of (Participant * (unit -> 'a))

Because of the 'a type parameter in the covariant position, you can make this type a functor. Try it, it's a good exercise. (Hint: You'll need to define a map function with the type ('a -> 'b) -> QInstruction<'a> -> QInstruction<'b>.)

We now define the free construction and turn it into a monad:

type QProgram<'a> =
    | Free of QInstruction<QProgram<'a>>
    | Pure of 'a

let rec bind f = function
    | Free instr -> instr |> QI.map (bind f) |> Free
    | Pure x -> f x

where the QI.map function is the functor map left as an exercise, above.

Not strictly necessary, but nice for syntactic sugar, now define a computation expression builder:

type QBuilder () =
    member _.Bind (x, f) = bind f x
    member _.Return x = Pure x
    member _.ReturnFrom x = x
    member _.Zero () = Pure ()
    member this.While (guard, body) =
        if not (guard ())
        then this.Zero ()
        else this.Bind (body (), fun () -> this.While (guard, body))
    member this.TryFinally (body, compensation) =
        try this.ReturnFrom (body ())
        finally compensation ()
    member this.Using (disposable : #System.IDisposable, body) =
        let body' = fun () -> body disposable
        this.TryFinally (body', fun () ->
            match disposable with
            | null -> ()
            | disp -> disp.Dispose ())
    member this.For (sequence : seq<_>, body) =
        this.Using (sequence.GetEnumerator(), fun enum ->
            this.While (enum.MoveNext, fun () -> body enum.Current))

let q = QBuilder ()

Using a few helper functions, you can now write the desired program, which has the type QProgram<unit>:

let example allowed =
    q {
        if allowed
        then
            let! dbOk = heavyDbCheck
            if dbOk
            then
                let! participants = findParticipantsMatching CriterionA
                for p in participants do
                    do! sendNotificationEmail p
            else
                let! participants = findParticipantsMatching CriterionB
                for p in participants do
                    do! sendRefusalEmail p
    }

This programs produced by example true and example false are a pure values, and you can run various interpreters over them in order to 'run' them.

As you can tell, there's quite a bit of boilerplate involved, so even in F#, where the end result is nice and idiomatic, I'd usually look for other alternatives first. In Haskell, on the other hand, most of this automatically follows from type definitions, so it's much more lightweight, and therefore more idiomatic, there.


All that said, the problem with these kinds of 'what-if' principal questions is that the examples are often so vague that there's almost no logic left. I've previously demonstrated a realistic example of that in the article Refactoring registration flow to functional architecture. Once you allow the introduction of some standard combinators, there's not much logic left.

For instance, in Haskell, imagine that we have the four concrete (i.e. non-abstract) IO-bound actions heavyDbCheck, findParticipantsMatching, sendNotificationEmail, and sendRefusalEmail. In that case, you can compose the desired action entirely from combinators:

example :: Bool -> IO ()
example allowed = do
  when allowed $
    heavyDbCheck >>=
      bool
        (findParticipantsMatching CriterionB >>= mapM_ sendRefusalEmail)
        (findParticipantsMatching CriterionA >>= mapM_ sendNotificationEmail)

As I point out in Song recommendations from combinators, such a composition is an entirely impure action, so not strictly 'functional' , but on the other hand, simple enough that if pushed to the boundary of the application architecture, is that much of a problem?

Sign up to request clarification or add additional context in comments.

2 Comments

Both F# and C# is fine (and some limited Haskell), so no worries. I usually try to find the most pragmatic approach I can pull into everyday business programming that other non-fp-versed programmers will accept as reasonable. Like your Windsor Castle DI examples, working beats perfect a lot of the time :-) It seems to me as if I am off in the wrong direction once I start re-creating conditional syntax as commands in a OOP language, as above. I have already had a look at some of your articles, but not all 20 :) I will have a deeper look and see what I can find there!
Sorry it took a bit of time, but I've now expanded my answer with some F# and Haskell code.
2

To add another take, which I found immediately applicable, I found a little jewel of a presentation by Scott Wlaschin at NDC London: Moving IO to the edges of your app: Functional Core, Imperative Shell. In it, he addresses this in a very pragmatic way.

Now, people always say, "What if I really, really, really need to do I/O in the middle of my workflow? Please, please, I need to do this.” Well, yes, of course, sometimes you need to load something up. You make a decision, you need to load a particular record from the database, and then, based on that, you make another decision, and then you know, save something. That's a legitimate workflow. Sometimes. I think it's less common than people think, but yeah, it's legitimate. Your answer is, you just break it into pieces, the same way, right? You break it into little pieces, and each piece is is either pure or it's I/O, but not both, right? So each of the little pieces are testable. Each little piece can be unit tested.

I can live with that, if that allows the code to be simpler, even if not as elegant as some Lisp where even the lazily evaluated impure-commands would still just be data I could assert on.

The take-aways with regards to practical applicability is this

  • Do try to encapsulate the outcome of the functional core in decisions, commands, effects or whatever you want to call it

  • There is no need to build a general command processor: make the code as simple and testable as can be for the specific case and adjust as you go.

  • If you have a workflow where it's infeasible to fetch everything up-front, that's fine. Break the workflow into pieces and unit test each piece.

With regards to several unit tests not giving you assurance that everything fits together functionally (hence the reference to this joke), you can always cover that entire workflow with a E2E test.

3 Comments

Regarding the E2E test... Just remember, if you have a mock, you are decidedly not testing that everything fits together. After all, if production code is not configured correctly, such a test would still pass.
And as @markSeemann said in his articles, are you sure the heavyDbCheck is really heavy? DBs can fetch a lot of data pretty quickly...
Scott Wlashin is a wonderful communicator, and pushing IO to the edge of the system is, indeed, the preferred way to deal with most of these issues. In most cases, you can simply pre-fetch the data you're going to need, even if it may require loading a few more kilobytes than strictly necessary. In other cases, you may need to compose impure actions. To put things into perspective, Scott and I are in alignment, and this approach is what I cover in Song recommendations from combinators.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.