by valenterry | 2025-06-02 14:16:22+00:00
I personally use that style in Scala with ZIO. Because the same problem exists in basically every language. I think more and more languages capture something like that and more under the term "capabilities". In a sense, the Rust borrowchecker is a very specialized case.

https://effect.website/ offers something very similar that in their effect-type; the "environment" parameter.

Basically, imagine you have many foos like `fun foo1(props: Foo1Props): string {...}` and so on, and they all call each other. Now you have one foo999 at the very bottom that is called by a foo1 at the very top. But not directly. foo1 calls foo5 which calls foo27 and so on, which then calls foo999.

Now if foo999 needs a new dependency (let's say it needs access to user-profile info that it didn't need before) you have to change the type signature and update Foo999Props. To provide it, you need to update the caller, foo567, and also add it there. And so on, until you find the first fooX that already has it. That is annoying, noisy (think of PR reviews) and so on.

Using the effect-type of effect.website you basically can the move the dependency into the returntype. So instead of returning `string`, `fun foo1` will now return `Effect<string, Error, Dependencies>` where Dependencies would be Foo1Props - which means it will not have the props parameter anymore. (or, they way I do it, I only use the Dependencies for long-lived services, not for one-off parameters)

Inside of fun1 (or funX) you can now access the "Dependencies". It's basically a kind of "inversion of dependecies" if you want so.

Seems like just moving the required dependencies from the parameter into some weird complex result type. But, there is a big difference! You have to annotate the parameter, but you can have the compiler infer the return type.

So now if you have a huge call chain / call graph, the compiler will infer automatically all dependencies of all functions for you (thank god typescript has good union types that make that work - many other languages fail at being able to do so).

So you write:

def foo5(nonReleventProps: Props) = (props) => { const x1 = foo10(); const x2 = foo20(); return x1+x2; }

Where foo10 is of type `Props => ServiceA` and foo20 is `Props => ServiceB` and the compiler will infer that foo5 returns an effect that needs both ServiceA and ServiceB - and so on. (note that you have to combine the results in a different style, I used just `const x = ...` to keep it simple)

So if you make a change very far down the call graph, it will automatically propagate up (as long as you don't explicitly annotate types). But, you still have full typesafety and can see for each function (from the inferred type) which dependencies it needs and so you know during a test hat you have to pass and what not.

Lisp is a completely different world because it is dynamically typed. In a sense it doesn't have the problem from the beginning, but it sacrifices type-safety for that.

> At a quick glance they remind me of React Context.

Yes, the intend is basically the same, but React Context is... well, an inferior version, because if you use a context where it has not been provided, it just blows up at runtime and no one protects you from that.