Functors for dependency injection?

Hi, so I’m quite familiar with dependency injection in object-oriented languages such as Java and JavaScript.

I’ve been doing some reading about DI in FP, and Ch. 9 of RWO v1 states that functors can solve dependency injection. I have also found an article that perfectly illustrates this, and I can see the pattern there.

I want to ask, how common is this used in practice? Does dependency injection in FP a thing at all (or am I approaching this the wrong way)? How do OCaml devs test modules that interact with other modules/libraries (which are probably impure) in isolation?

I am also aware that you can write your functions accepting the dependency functions as arguments and utilize currying. Is this pattern more reasonable?

If there are projects that make use of either approach for DI and testing, I would love to be pointed to it.

7 Likes

Functors, high order functions and parametric polymorphism are used in functional programming to achieve this kind of abstraction. But so far, I have never heard anyone refer to it as dependency injection. I think that phrase if more relevant to object oriented thinking.

I think the technique is used in Mirage, I remember @avsm talking about it in a podcast: they have signatures that specify operations like I/O, and implement them for different target platforms: Xen, Unix, etc.

Yeah it might be true, but seems like that’s not too far off? e.g. if module A calls module B then module B is a dependency of A, and using functor we can “inject” B to A instead of it being a direct call… nevertheless, I’m glad to hear that those techniques are common.

Thanks! I also end up checking out the Irmin project that seems to utiilize functor for that. Could you point me to the mentioned podcast?

Sure: https://twit.tv/shows/floss-weekly/episodes/302

1 Like

In OOP, because mutable state and functions that act on that state are bound together, it’s much harder to test them reliably. Just switching to immutable state and functions already makes it easier to test your code, without any need for dependency injection. You don’t get the complexity buildup that mutable state causes.

For mutable state that does exist in your programs, functors are good, as are first-class modules. In fact, OCaml is almost ideally suited for this due to its obsession with abstraction of types. I would venture though that many programmers making heavy use of FP don’t deal with a lot of state. Most of this kind of thing happens in large companies or specific organizations (such as Mirage), resulting in the fact that DI isn’t commonly discussed in the FP world.

3 Likes

Awesome thanks @bluddy , I’m from the Java world and I code a lot of Kotlin with Dagger2 as DI framework as my daily work.

From my understanding, Module in OCaml is some kind of Static , mean while Class is Dynamic . And for the cases of dependency injection, the framework will generates a lot of boilerplate code(New a class, for example) for us.

Just wondering if maybe you could help to create a minimal case that show how to use mutable state + functors that do the DI work?

I was heavily involved in the Java world when DI was (ahem) “invented”. And it was clear at the time, that it was a poor substitute for ML functors. Others have described a few of the ways in which functors can solve the problems that DI was used to solve; I’ll go further and argue that it would be shocking to find a use-case for DI, that functors couldn’t solve better. Perhaps some sort of case where the dependencies are manipulated dynamically, but such a thing would be anathema.

1 Like

The basic idea is that functors get an argument when instantiated which stipulates a precise interface of what functions and types they can access. Classes, by default, have no such argument. They could optionally be built to receive such an argument, but it then needs to be carried around by anything that constructs them and things get messy.

First class modules can also serve as a complement/alternative to functors.

One thing that apparently Java DI supports which OCaml does not is runtime resolution of dependencies. This is not a great idea IMO, and is anathema to OCaml’s notion of compile-time resolution.

It’s at runtime, but executed by “framework code” (like Spring) so in fact, it happens before any real user code executes. Might as well be static. The “dependency” in DI pretty much corresponds to a functor-argument, and the “injection” to functor-application.

1 Like

It’s also worth noting, at least as a side point, that in my experience (in C, C++, python, and some OCaml), interfaces that can mix and match with multiple services are mostly a myth. You end up with terrible hacks to make those interfaces work, and you get stuck with nasty hierarchies that eventually lead to full rewrites. Interfaces as separation of concerns are generally a good idea. As an ability to change components, I rarely see them work out well unless they were deliberately designed with those multiple components in mind, and you’re generally better off refactoring code than trying to get to the mythical place of not needing to modify code.

The nice thing in OCaml is that with modules, you’re not limited to where you place those separations of concerns, wheres with some OOP languages, you’re mostly confined to the class level, which is restrictive (but may not feel that way unless you’ve used modules).

1 Like

Lots of platitudes are being thrown around in this thread about how DI is bad and modules are good. My experience with java leads me to believe that it isn’t so simple.

If you use functors to implement DI (for the purpose of separating interfaces from implementations) you will quickly run into a combinatorial explosion of sharing constraints. It’s bad enough that the mirage team had to create a code generation tool to write them - see functoria.

Classes and objects seem to be more appropriate to solve this problem because they are structurally typed. However, you will still need to wire the dependency graph completely manually as OCaml lacks reflection.

My suggestion would be to learn more about other decoupling techniques, in particular, those that are successful in other FP languages. Free monads and tagless final come to mind. If we ever get typed effects, we’ll have another tool in our arsenal.

4 Likes

Three Four thoughts:
(0) ML sharing constraints express (statically checkable) constraints that are simply inexpressible in Java. The equivalent constraints are only dynamically-checkable in Java, and typically aren’t checked. Programmers can and will make mistakes, and there’s not much there to save them.

(1) I feel that you’re implying that people dissing DI have no (or insufficient) experience with Java, and while this may be true of some, it’s certainly not true of all.

(2) I think that perhaps you’re judging the most complex ML systems, written by rather competent people, and finding them wanting. But most Java systems deployed in the wilderness aren’t written by the caliber of programmer that works on ML [and on this, I have personal knowledge] and furthermore the (ahem) “corporate/career/resume/political constraints” of Java systems force all sorts of awful stuff to get pushed into standards and standard implementations [again, I have personal knowledge; also think about the fact that Apache is -funded- by major corps, hence a resume-enhancement channel for senior architects].

(3) I was talking with a friend who works for a “major Internet streaming video company, all of whose major IT systems are written in Java” and he was lamenting that basically everyone around him is in the mode of “just repeat the incantation that somebody told you and hope it works” and nobody actually knows how any of the enterprise Java infrastructure they’re using is wired-together. He is dinged on his performance reviews for trying to get to the bottom of problems, instead of just applying somebody’s “incantation” and hoping it works.
He specifically cited Spring in this regard.

Look: I get that you’re trying to say “let’s not pat ourselves on the back too much”. But having spent 1998-2008 debugging Java internals, app-servers, and deployed systems, both inside IBM and at probably every major IBM customer in the US and many around the world, having seen how the sausage gets made, I think it’s completely accurate to say that the ways the Java world has invented to cope with complexity are much worse than the ways the ML world has invented.

It could have been different: Java could have adopted a system of static, checkable modularity, and on that basis could have had something not unlike ML functors by now. After all, MSFT’s assemblies are nearly all static. But they didn’t choose that path, and I can -assure- you that it happened for PURELY career-driven reasons: technical correctness didn’t enter into it.

1 Like

It’s not that I disagree with that is said, it’s just that all this information doesn’t seem to be directly relevant to what is being asked in this thread. Put it this way, if I knew Java and was curious about OCaml, I would be just as confused as to whether I can - or should, use functors for DI in OCaml.

Naively restated, the OP is curious about the consequences of porting standard Java code like this:

interface Service {
  Response run(Request request);
}

class App {
  Service service;
  App(Service service) {
    this.service = service;
  }
}

into OCaml written in this style:

module type Service = sig
  type t
  val run : t -> Request.t -> Response.t
end

module App (Service : Service) = struct
  ..
end

I’m telling him that while it’s possible, there are some serious practical disadvantages that explain why the majority of OCaml code isn’t written this way. Discussions about immutability vs mutability, frameworks, whether DI is good or bad, the virtue of ML modules, do not help the OP to understand the trade-offs of this technique.

I probably came off a little too strong than I’ve liked in my first reply, but it just seemed like the OP asked a very concrete question, and received responses containing a lot of fascinating, but largely inapplicable theory crafting about OO and FP.

6 Likes

Well, except that this sort of separation is actually quite tractable in ML. I’ve done a ton of it (as I’m sure others have). Right off the top of my head, I can remember:
(1) a tran-log for a blockchain system, that I wrote unit-tests for, using a mock-ed Filesystem in order to inject data corruption and test recovery (think “the tran-log at the bottom of leveldb/rocksdb”).
(2) for the same, a layer between the tran-log and its client code, that allowed for having multiple instances of the client, but not flushing the tran-log each time the client requested (so I could run multiple client instances in a single process (for testing) without saturating the laptop SSD)
(3) similar tricks to allow the clients (which communicated over Thrift) to have multiple instances run in a single address-space, communicating -not- using Thrift (so faster, but also no network).
That’s just off the top of my head.
The use of functors and modularity to allow this sort of “wiring together in the main program, and not before” is something many systems-builders find quite appealing about ML.

I also use this style when it seems like the weight of the abstraction is justified. It’s just that it’s never my first choice because how heavy it is and the fact that there are good alternatives. In Java on the other hand, DI is definitely my first choice.

1 Like

I’m surprised you think it’s heavy. There’s the code to write for the actual mock, and there’s the interface (signature) to interpose, modifyin the client code to use the operations in the interface, and then writing tests on top of that. But only writing the actual mock is nontrivial, and that’s gonna depend on how much interesting behaviour it displays.

OTOH, for sure in Java, there’s no other choice, so I get why you use DI.

Is there really no other choice? I’m asking because I really don’t know. In C++, you don’t normally write entirely to interfaces unless you really need to for the sake of something like an enterprise codebase where you only own a specific part. What is it about Java that makes this an essential style? Or was it just popularized that much in Java that people over-engineer right away?

I’m not the only one who thinks this way. After all, functoria is a thing and bigfunctors are one of the oldest features on the wishlist.

For a small library that does a lot of wiring, have a look at libmonda. Quite a bit of boilerplate for a fairly small library. I can’t imagine building a large application this way.

Isn’t a big part of the reason for functoria the fact that Mirage has multiple backends and such, all of which cannot be compiled together? So it’s a compilation resolution mechanism at the same time as stringing together the functors.

I agree that there is syntactic overhead for functor application, but for serious codebases concerned with interfacing and especially with being able to substitute large pieces of code for each other (as DI is), I think it’s a good solution. And here is where you can question the merits of DI in the first place, at least as it pertains to most code found in the open source community. In reality, if you’re going to be switching the dependencies around, it probably isn’t a big deal in most codebases to just make small modifications as needed, including constructing other inner classes. FP style functions can be tested in isolation as well. For giant codebases maintained by separate groups within an org, it’s probably a different story.