DI for F# butterflies 🦋

jkone27
9 min readDec 29, 2023

--

F# Advent calendar 2023, thank you Sergey Tihon and thank you F#

some beautiful Java 1990’s XMASY diagrams! yey

We all know F# as a functional language, but as with any language running on dotnet, F# underlying nature and its ecosystem will be ridden with objects like little gifts 🎁 sprinkled everywhere by C# Santa 🎅 for Xmas 🎄.

ServiceCollection for all your needs

Ready to code, brew install dotnet and then Ionide and Vscode and follow through by opening a single file .fsx script.

And the fabulous results of the execution of this wonder pretty code!

wow moment for a silly .NET console log

Objects in F#?

As you are probably familiar with classes and objects in other languages or in C#, here is how to create an old-fashioned class type in F#, not very common these days, but very much common to encounter in the CLR and when interacting with any other dotnet library, like aspnetcore or entity-framework ORM or C# projects around your F# code, so kind of important to be aware of.

// 1. a DATA class, like in POJO in java (POCO in csharp), is a Record in F#.
[<CLIMutable>]
type Poco = {
Name: string
Surname: string
Age: int
}

// 2. an actual class for an Object, with properties AND methods
type Animal(name: string, age: int) =
member val Name = name with get
member val Age = age with get,set
member this.MakeSound() = $"GROOOARG, i am {name}!"

// 1. create a poco using the F# record constructor
let poco1 =
{
Name = "hey"
Surname = "hi"
Age = 100
}

// 2. create a true object instance using new and its constructor.
let animalFido = new Animal("Fido", 0)

What is “a” Dependency Injection?

If you are a FE dev and you ever used Angular with typescript, then you know what I am talking about. If you tried Java or C# you have likely used a dependency injection lib that provided you with an Inversion Of Control [IOC] container. Most likely in Python or Ruby other libraries exist for this common object-oriented solution-pattern.

The most common way dependency injection libraries work is through explicit registration of types via methods and/or declarative registration via annotations. Both techniques in general use reflection APIs to explore the code itself at runtime time, doing some metaprogramming work. In the case of annotations\attributes, that can also entail some aspect-oriented [AOP] effects like when using libraries like Lombok in Java, but I am not an expert.

In the case of .NET, it comes mostly as explicit registration, even though more advanced convention-based registration can be achieved with custom code, this is the code most frequently encountered in my experience. This is in F# but C# is almost identical.

// explicit registration example in F# or C#, can use interface or abstract
services.AddTransient<ISomeClass, SomeClass>()

// constructor injection
type SomeType(x: ISomeClass) // will resolve an instance of SomeClass

// service locator pattern (forced injection), requires a serviceProvider obj
let someClassInstance = serviceProvider.GetRequiredService<ISomeClass>()

In the case of JVM/Java for example or Angular, method and class decorators attributes are used, so the code (here still pseudo-F# because is an F# article!) would look something like

// this is done for springboot usually, 
// @Configuration is required for telling spring to use beans,
// is the most basic form.
[<Configuration>]
// @ComponentScan supports automatic package/assembly scan for
// @Component (and @Service/@Repository as special cases of it)
[<ComponentScan>]
type AppConfig() =

// here an explicit registration if we want...
[<Bean>]
member someComponent() : ISomeComponent =
new SomeComponent("hello")


[<Component>] // discovered by component scan
type SomeDiscoveredComponent() =

// autowired tells spring to auto-resolve nested dependencies
// can be used in construtors, properties, etc
[<Autowired>]
let someWiredComponent : ISomeComponent = null
// ...

[<Component>]
// project lombok allows to simplify constructor injection..
// without needing to specify autowired, but generating extra aop/waved code
[<RequiredArgsConstructor>]
type SomeOtherComponent( myComponent: ISomeComponent) =

// myComponent is discovered and injected
// from the <Bean> registration above in AppConfig()

// here a sample method using myComponent...
member testHello() =
myComponent.say() // "hello"
// ...

Why do We need a DI Container and IOC?

The regular way to create objects with a standard composition root does sometimes not satisfy object-oriented developers for several reasons, but let’s go slowly. The composition root is a safer way of instantiating objects instead of constantly instantiating whenever we need them, the reason for the composition root is to try to reduce object allocations and optimize it for a consistent path.

The Not Nice Way of creating objects

another cool 1990s vibe pic, imagine PowerPoint

Now imagine a fruit tree made of apple objects, we got the requirement, and we built our tree which looks gorgeous and yummy, but we already started noticing some issues.

If we need an Apple 🍎 class in different parts of our code, it can be the case that many times we want the same Apple 🍎 during the execution of our code, other times, a new Apple 🍏.

In addition, the new product owner already visited us, telling us that we plan to add pineapples 🍍 in, and has to be ready for next week. If it works for apples, it will surely work for pineapples 🌲, they argue.

This is just a metaphor, but all of this happens frequently in many programs, this is how some similar F# pseudocode for a composition root would look like in code.

let compositionRoot (args) =
let someDependencyOne = new SomeDependencyOne()
let someDependnecyTwo = new SomeDependencyTwo(someDependencyOne)
let someDependencyThree = new DomeDependencyThree(
someDependencyOne,
someDependencyTwo, ... // gets worse and worse
// ... build all the object graph, and return it...
new SomeType(...)

What this does is that it creates problems for developers in evolving codebases in several ways:

  • (1) No Abstraction: the implementation (object type at runtime) is coupled with its usage in all the constructor invocations, changing 1 class can have enormous ramifications in code, not great.
  • (2) Temporality: the lifetime and instances/leaves of our object graph are defined by our composition root, they are set in stone, and not very malleable. too bad for load adjustment or memory issues.
  • (3) Locality: If we need a dependency on some far leaf, we have to inject it into our root and propagate it all the way. some dependency might always be required, others seldom required, and others very often but not always, as in the case of “apples and pineapples” above.
  • + Our objects are coupled to their implementations so as a consequence, they are not testable in isolation. This can be ok for someone, but also can be very painful and unsettling for someone else, and in general, is very much likely to create bugs in production.
  • + Our tree is much like a spaghetti tree 🍝, everything is tangled and ready to explode at the first change.

Dependency Injection to the Rescue

To solve our coupling problem (1) above, we can use abstractions, so that low-level details depend on abstractions and not the other way around. This causes the Inversion Of Control [IOC] of our dependencies. The implementation is bound to the interface or abstract class, not anymore to the concrete implementation. This is the base for defining code contracts between services and placing them in the right places in our code.

What is an IOC container?

To solve points (2) and (3) above, usually, objects and their nested dependencies in the object-oriented world have to be instantiated, this means, the act of new life as an object creation from a class blueprint, after collecting and instantiating first all its dependencies, defined in the constructor. Adding this with the point (1) already solved, gives us an IOC container.

An IOC container, in summary, solves all the problems (1), (2) and (3) from before, but creates some we didn’t have before* (ofc, more on that later).

  1. allowing the use of DI and IOC, thus abstractions, we can now register dependencies in a non-coupled way, allowing for testability and development in isolation and thus better quality and software maintainability
  2. we can decide WHEN to create them: just once (singleton), define lifetime scopes (scoped), or have them created each time they are required (transient).
  3. we can decide WHERE to create them and we have several weapons at our disposal to do so, such as constructor injection, but also the service locator pattern. This pattern allows us, if in need, to request objects in an opaque way, directly from the DI container object. It can be useful for libraries and internal utilities when constructors need to be kept as simple as possible.

Some Bad News on Dynamic Dispatching and Reflection

We must note that within an object-oriented world even in statically typed languages, we have a quite dynamic affair after all. Being that polymorphism is achieved by dynamic method dispatch, so that, at runtime, the right class implementing the right interface is chosen for some specific method execution, other than another sibling, this can be annoying and decrease software predictability.

Didn't we say for a long time that we didn’t like those dynamic languages because they could give us headaches at runtime and create unpredictable results and runtime errors? But we have classes now and type-checking! ofc.

But, using a DI container can take away some of those safety features of compile-time checking our constructor method invocations, and we lean on reflection for registering our types. While the composition root was safe and sound, with its very impractical quality and maintenance expectations, in our new malleable and uncoupled IOC world, we have another problem: making sure to not shoot ourselves in the foot at runtime. A DI container in a way forces us for extra rigour with testing and configuration checks, but that’s normal, right? With extra power comes extra responsibility they say.

How does this not happen in the functional world? what do they do there?

Dependency injection in the functional world

In the functional world, in general, dependency injection is usually achieved with partial function application. that’s it, there is no object required to achieve composition, this feels like magic and we know it.

Let’s pretend below we have our function with dependencies, to make it more realistic, they are also async i/o dependency function calls like a database or network accesses, so for example we also have the classic dotnet cancellation token ct to pass along. I am using task it here because is the standard for hot async in dotnet and it is mostly equivalent to C# async/await that most people know.

// example of an async function which takes 2 other functions and 1 arg + ct
let someFunctionAsync
someDependencyOneAsync
someDependencyTwoAsync
arg
ct =
task {
let! res1 = someDependencyOneAsync(arg,ct)

let! res2 = someDependencyTwoAsync(res1, ct)

return res2
}

in ML languages with partial application, DI looks something like the below example. We want to create a function with testing dependency injected, but not its final arguments applied arg and ct , then we just pass the dependencies first and we receive a perfectly legit function back.

// use this in your unit tests, misses arg and ct to be applied
let myFunctionForTesting =
someFunctionAsync Test.fnOneAsync Test.fnTwoAsync

// use this in production, misses arg and ct to be applied
let myFunctionForProduction =
someFunctionAsync Prod.fnOneAsync Prod.fnTwoAsync

This is because in F# all functions are curried by default, thus allowing for partial function application always. One way to limit currying is to specify a single argument as a tuple. In that case, the function is not “curry-able” as it has in principle a single tuple argument.

let notCurriableSoSorry (a,b,c,d,e) = // but great for .NET/C# interop!

So in general in purely functional programs in F#, when interacting with other .NET folks is not essential, the partial application is also a viable and powerful approach to dependency management and “DI”.

Extra: HTTPClient + SwaggerProvider gist

A sample program using ServiceCollection in an F# script to get extra cool dotnet features for free, thanks to this DI abominion!!!! Seems like is worth it!

Links

--

--