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?
Effectstructure 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?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.