Functional approaches to dependency injection (2016)
fsharpforfunandprofit.com
fsharpforfunandprofit.com
I've been doing OOPs from very early in my career (I run into FP from time to time but never as a core of my own work), and much of the example he's implementing here seems like make-work simply to enforce a specific paradigm. Am I missing something?
For example, at first his concepts seem brilliant, until you get to the refactoring steps, which feel painful.
Is this the end result of high-level extreme veteran coders use of FP? With OOPs, simple inheritance & factories seems like it would solve all the dependency issues he's addressing.
In the refactoring section he's mainly dealing with things like trying to minimize exceptions with `Result` instead and maintaining syntactically linear control flow.
So essentially yes that section is make-work if all you care about is dependency injection. It's not if you care about other things beyond dependency injection. That work is not necessary for dependency injection with FP.
As far as whether he's in line with "core principles of functional programming", that's maybe a phrase that's well-designed to start a fight. Even more so than in object-oriented languages, different ways of doing things tend to cluster around different functional languages. In broad strokes, what he's doing seems fine coming from a F# developer. I think it would translate well enough to Haskell, though I'm nowhere near as experienced in that language. In Clojure, on the other hand, I'm guessing you'd want to do something completely different.
I'm definitely in favor of making dependencies explicit, but I wish there was a typed language that allowed me to avoid mentioning "less important" dependencies when I want to. Logging is a perfect example:
function log(msg: string) =
ImplicitDependencies<ILogInfo>().Write(msg)
Let's imagine that "ImplicitDependencies" is some special keyword. The compiler would give the "log" function a type of (string -> (implicit ILogInfo) -> unit). But my code is allowed to say that its type is (string -> unit) if it wants. Now if I use this "log" function in UpdateCustomerProfile, then UpdateCustomerProfile gets an (implicit ILogInfo) parameter too.The compiler would also give me some way to designate a function as "root level" meaning that all implicit dependencies must be resolved. I'd imagine something like Racket's parameterize for this. (https://docs.racket-lang.org/guide/parameterize.html)
The idea is, most of the time, I don't want to keep mentioning logging in my type signatures. I just want to silently kick it up the call stack.
It does not, however, automatically infer the implicit parameters based on usage of implicit arguments in the function body, though.
https://dotty.epfl.ch/docs/reference/implicit-function-types...
https://www.youtube.com/watch?v=YXDm3WHZT5g&feature=youtu.be
The idea is that the type system knows everything, but allows you ignore these implicit parameters if you want.
function square3( x : int ) : div int
My code should be allowed say function square3( x : int ) : int
Basically, the problem is that in most typed languages, if you add a logger dependency to a function, then you have to rewrite the signatures of all the functions that depend on that function. That sucks.It's also unprincipled as heck, from both an object-oriented and a functional perspective. I think my alignment in the logging department might be chaotic good.
It seems similar to traditional OO dependency injection, except without all the ceremony of adding a constructor parameter and corresponding class member to hold it.
It's a case of multiple implementation inheritance, so I could see a lot of people balking at it.
If you have a language that supports thread-locals and lambdas, you can get pretty far with something like:
public static S parameterize<T,S>(ThreadLocal<T> param, T value, Callable<S> body) {
T old = param.get();
param.set(value);
try {
S result = body.call();
} finally {
param.set(old);
}
return result;
}
You can then do something like: public static final ThreadLocal<Logger> LOG = ThreadLocal.withInitial(() -> getRootLogger());
And when you need a different logger, you can do: String result = parameterize(LOG, myFancyLogger, () -> {
return methodThatLogsThings();
}); [ImplicitDependency(LOG)]
void methodThatLogsThings()
Which would cascade up the call hierarchy until it gets satisfied.https://blog.carbonfive.com/2018/03/19/lightweight-dependenc...
For context, Elixir (which includes Erlang) is a compiled language, but it doesn't/can't catch a lot of compile time type errors that you'd only uncover at runtime. For example, one process can send a message of any type to another process. There just isn't a great way (for the Elixir compiler) to catch that at compile time. It gives you more flexibility, but you can't have confidence that it'll work just because it compiled. Dependency injection with tests is a really helpful tool in Elixir.
A lot of the design patterns I used to adore and look at as if they were rare gems in a sea of bad code. Over time my opinions have changed. Many patterns are good, but I don't really like to think of those as patterns.
A pattern implies repetition. Something that you can used each time you have a situation that warrents it. In my opinion, this openly contradicts "Don't repeat yourself".
So I think we should start trading design patterns for simple and well designed languages. No code always beats good code.
The problem is that a simple language will lack an implementation of hitherto-unknown patterns. That's why it's so important to use a language which permits itself to be evolved & improved — and that's why Peter Norvig noted that design patterns tend not to be needed in dynamic languages[0].
I just think this long overhyped Object Oriented style was the wrong way to go for programming.
What am I missing here? We've got to be working with different definitions of the term "dependency injection" or something.
Using monkey patching as a complete replacement for dependency injection seems absolutely bonkers to me. The codebase I'm imagining when I here that is one where no module's run-time behavior can be understood by simply reading its source code, because every single one of its run-time dependencies is being monkey patched in from some where else entirely. It's also one where, to take the same class and instantiate two different copies with two different sets of run-time dependencies, I'd have to do something wacky with the monkey patching instead of just creating them with different constructor arguments.
Instead of just making modules I had to put everything in a class. I had to write code to decide what things are being injected where. It didn't look like python anymore, so I stopped doing it, and I've never had problems, and I've written much less code.
I only ever use monkey patching for testing. People talk about how it makes it easier to switch out other implementations, but in reality it's easier. You just import a different module.
I realize there were difficulties in integrating with the rest of the .NET platform/ecosystem, but it's really too bad that the module system was left out, as it's one of the best parts about OCaml.
The really cool one though is abstract types. You can distinguish types purely in the type system, with no change to run-time representation, using module signatures that don't specify the concrete type of a type entry.