DEV Community

Dependency injection in functional programming

Pin-Sho Feng on January 13, 2019

Dependency injection in functional programming (Kotlin) tl;dr DI in FP can be achieved using partial function application or...
Collapse
 
patroza profile image
Patrick Roza • Edited

I like the Partial application approach, I use it in Typescript with a custom DI container which resolves also functions, not just class instances.

/**
 * Configures a function for dependency injection, dependency types are automatically inferred based on
 * the passed `deps` configuration. Dependency configuration is provided as last argument.
 */
export function configure<
  TDependencies extends DependencyDefinitions,
  TFunctionConstructor extends (dependencies: A.Compute<Dependencies<TDependencies>>) => TFunction,
  TFunction
>(func: TFunctionConstructor, deps: () => TDependencies) {
  const injectedFunc = (func as any) as ConfiguredFunction<TDependencies, TFunctionConstructor, TFunction>
  injectedFunc.$$inject = deps
  return injectedFunc
}
Enter fullscreen mode Exit fullscreen mode

called like so:

const myFunction = configure(
  function ({ dep1, dep2 }) { // the types of dep1 and dep2 are inferred.
    return (input: string) => ... do stuff
  },
 () => ({ dep1, dep2 }), // a function, so that it supports late-binding
)
Enter fullscreen mode Exit fullscreen mode

Of course you could store the dependency configuration also in some map, instead of writing onto the function, it works fine like this in Typescript, not sure you could do this in other languages.

as to

though you could also argue that we'd do that at the expense of having the business logic pushed to the outer layers just so we can have a pure functional core

I'm not sure how the list of todos is considered business logic. So retrieving data is imo something we can do in the app layer. Impure effects however, e.g on 3rd party apis, message busses, etc, I would handle with Domain events - do all the business decisions, logic, policy in the domain, put the decisions in an Event and let the event handlers call the relevant infra.

Collapse
 
psfeng profile image
Pin-Sho Feng • Edited

Thanks for sharing! I'm not familiar with the JS/TS world but out of curiosity and ignorance, why not Lodash or Ramda? Don't they support partial function application? What is the purpose of late-binding (assuming the dependencies are immutable references)?

I'm not sure how the list of todos is considered business logic. So retrieving data is imo something we can do in the app layer. Impure effects however, e.g on 3rd party apis, message busses, etc, I would handle with Domain events - do all the business decisions, logic, policy in the domain, put the decisions in an Event and let the event handlers call the relevant infra.

Agreed, that sounds like an approach where the code would be pure. I was referring to the difference between

getCompletedTodos(repository::getUserTodos, userId)

vs

val todos = repository.getUserTodos(userId)
getCompletedTodos(todos, userId)

In the first version, getCompletedTodos is referentially transparent only if repository::getUserTodos is too, which can't be guaranteed.

Collapse
 
patroza profile image
Patrick Roza • Edited

Follow-up, I think my confusion comes from our different definition of the Domain.
See for example this naive implementation in Typescript, for how I see it:
gist.github.com/patroza/8f2d634b23...

Main take aways:

  • The Interactor is not Domain, but Application, and is allowed to leverage interfaces from Infra.
  • The return view from the Interactor is complete, including userId. The only job of the Controller (request handler) is to transform HTTP request details into basic data structure for input to the interactor, and to transform the Response from the basic data structure into JSON.
  • I added a completeTodo mutation, because I think commands are a lot more interesting, because they require business rules/policy to be applied usually - which is where the domain model shines. where-as queries are often just bolted on top of database/remote apis, just a simple View, often pre-generated in a model convenient for Reading (if you follow CQRS for sure).

Finally, I think that for the Interactor taking the input as parameters and resolving the result by returning it as value, is a shortcut to the Clean Architecture's "Controller" calls Input interface implemented by "Interactor" and "Interactor" calls Output interface implemented by "Presenter", but I found that I hardly need the extra level of complexity it brings if you go literal.

Thread Thread
 
psfeng profile image
Pin-Sho Feng

Indeed, we had different definitions of Domain. I was thinking in terms of three layers: Presentation, Domain and Data, where domain includes business rules that are encoded in the use cases or interactors, like depicted here from this post.

If I'm understanding well what you say, your layers are the ones suggested in Clean Architecture by Uncle bob where you have, from inner to outer circles:

  • Domain models and functions that can transform them
  • Application layer that decides what domain models and what transformations to use and handle interactions with infra/presentation
  • Infra/Presentation - this would be where the data comes in or goes out.

Makes a lot of sense too, it reminds me of the "ports and adapters" architecture, where your infra/presentation functions would be the ports and adapters. My understanding of Domain is basically Application + Domain in your case I'd say.

I think we're talking about the same thing. In your code

const getCompletedTodos = (getTodos: ReturnType<typeof getTodosApi>)

getCompletedTodos is pure iff getTodos is pure. To make it 100% referentially transparent you'd have to pass the Todos, but then the getTodos logic would be pushed to the infrastructure layer.

Small nit: you could make your domain pure by passing in new Date() instead of instantiating it within complete.

Thread Thread
 
patroza profile image
Patrick Roza • Edited

Thanks!

I would say we got as far pure as makes sense. The HTTP Request Handler can't be pure, and the application use-case interactor shouldn't be pure, imo - at least if you like the Interactor to still add meaningful value and sticking to the fact that the Request Handler is just a delivery mechanism detail. However the pure business case, is pure.

I would say that the Interactor is the red parts here, blog.ploeh.dk/2020/03/02/impureim-... while the HTTP request handler is just the mount point for this usecase or workflow.

They do something similar in fsharpforfunandprofit.com/ddd/ which im a huge fan of, I read his book and am intruiged by his railway oriented programming.

Small nit: you could make your domain pure by passing in new Date() instead of instantiating it within complete.

Very good point! I'm still too stuck in non FP languages, that I don't realise these subtle cheats :)
gist.github.com/patroza/8f2d634b23...

How about the Exception? Is that considered Pure? The alternative would be the use of Either.

Thread Thread
 
patroza profile image
Patrick Roza • Edited

Update:
Regarding the post; fernandocejas.com/2014/09/03/archi...
I find the demo project a little boring due to it only implementing Queries, and the Interactors doing nothing more than redirecting calls to a Repository. At least some mapping of Entities to Views would be interesting, but Commands would be of more interest to me :)

I'm personally also more fan of Database contexts, which give lower level access to the data, instead of the more high level one exact query per method like in Repositories, as often in my experience each method is actually only used once, or maximum twice anyway.
But of course you don't give your entities access to these database contexts, that remains in the Interactor. Anyway, it depends on the project.

Old but gold: lostechies.com/jimmybogard/2015/05...
Basically I see Interactors as either Queries or Commands as per how JimmyBogard described them.
What I like about this pattern also is that you can extend the Mediator pipeline with additional behaviours through decorators like validating requests, performing authentication, etc.

Finally - if you're interested, here are some of my own experimentations with all these patterns, in a Functional Programming style, latest fp-ts version github.com/patroza/fp-app-framework2

It's doing CQRS with separate read and write contexts - file-based json databases - living the Uncle Bob dream ;-), domain events, DI, exploring functional programming and trying to find a balance between FP and OO, e.g FP-OO-FP sandwich, which may or may not lead to a 100% FP implementation ;-) Multi level validation (basic JSON fields in delivery mechanism, and Domain level validation in the Interactors/Domain objects)
It's runnable, and has a documented schema so you can easily discover what to post.

Collapse
 
koresar profile image
Vasyl Boroviak • Edited

Few people, few articles, over few years - I couldn't understand how to do DI in FP. I finally get it! Your explanation is great.

I read some opinions in the internet that mocking (which is another term for DI) is a code smell. Would you agree with that?

Collapse
 
psfeng profile image
Pin-Sho Feng • Edited

Thanks a lot, I'm very glad to hear that!

I generally agree that mocking is a smell. Note that mocking refers to a testing technique, it's not a synonym for DI. Strictly speaking, mocking is about verifying that certain methods are called on some object and as such you're testing implementation details. See here.

Testing implementation details has the problem that it makes tests more difficult to maintain. For example, if you change the name of the method you're expecting to be called, you have to change all the tests that depend on this method, but the behavior may be exactly the same.

I prefer feature testing and avoid mocking as much as possible. Check these resources:
blog.twitter.com/engineering/en_us...
blog.kentcdodds.com/write-tests-no...

Collapse
 
koresar profile image
Vasyl Boroviak

I mock only I/O related things in my tests. Thus I call it mocking. Probably wrong term usage. Apologies.

Do I understand it right that a way of replacing side effect function is called DI in the article above?
And isn't it just a way to mock that function?

I'm confused.

Thread Thread
 
psfeng profile image
Pin-Sho Feng

DI refers to dependency injection, which is basically passing a dependency as a parameter to a function instead of having the function call it directly. For example:

fun doSomething() {
    fetchItemsFrom("http://someurl.com/items") // this is hardcoding the URL that you want to use
}

fun doSomething(url: String) {
    fetchItemsFrom(url) // now url is "injected" to the function.
} 

fun doSomething(fetchItems: () -> Unit) {
    fetchItems() // you can also choose to pass the whole function
}

DI and mocking for testing are related in that you can pass mocks (or stubs) to the function under test. If what you're injecting is a side-effectful function, you can indeed replace it with some mock function that doesn't have side-effects, just for the purposes of testing.

Let me know if it's clearer now!

Thread Thread
 
koresar profile image
Vasyl Boroviak

The only difference I see is that with DI the replaced value can be data.

Sorry. I still believe DI and ability to mock is the same concept - ability to replace a thing down the call chain.

Thread Thread
 
psfeng profile image
Pin-Sho Feng

Dependency injection is a concept and the ability to mock is a consequence of this concept, but it's by no means the only benefit.

You could, for example, use some class across different sections of an app but have it behave differently depending on the section. Using the dependency injector you can configure the different instances of the class that will be used in each section.

Collapse
 
carstenk_dev profile image
Carsten

some of the problems you noticed with Reader vanish if you use functions like withReaderT

overall much of this is a problem of composing getters/setters - one of the answers to that is lenses/optics and so maybe you'll find this lens-module interesting too (deals with Readers)

Of course in languages like Elm or F# this might not be an option but Scala, PureScript and - I think - Kotlin with Arrow you might get lucky ;)

Collapse
 
psfeng profile image
Pin-Sho Feng

Hey Carsten, thanks for the feedback. Could you please provide an example of how withReaderT would solve those problems?

Collapse
 
erdo profile image
Eric Donovan

Really excellent article! helped a lot with my understanding, thanks for taking the trouble :)

Collapse
 
zenventzi profile image
Zen Ventzi

WOW! Hands down the most insightful engineering articles I've read about fp/oop relationship. Thank you very much for putting this piece of art together.

That's the kind of devs I'd love to be working with!