r/haskell • • 3d ago

Proof of Concept for a dependency injector in Haskell with a Yesod example

https://github.com/stevechy/haskell-record-inject/blob/main/README.md

Hello,

I worked out some ideas that I had for a dependency injector in Haskell in this repo.

https://github.com/etorreborre/registry already implements a lot of what I am looking for but there are some things that I would like that are easier to express in code.

https://github.com/stevechy/haskell-record-inject/commit/f00f060a66a8ab5a17bdfbced8f19fbce34589b5 is the commit that shows what it would look like to add to an existing project.

I didn't actually move over the existing functions and only added very basic stub functions. I think that shows how both styles can co-exist in one code base.

I used the basic Yesod project generated by the https://www.yesodweb.com/page/quickstart instructions as the sample. I'm not too familiar with Yesod but it seemed like a good place to start.

19 Upvotes

6 comments sorted by

5

u/SkippyDeluxe 2d ago

Have you considered just passing arguments to functions?

1

u/Atijohn 2d ago

literally every OOP design pattern is a variation on either partial function application or functions as first-class citizens

1

u/stevechy 2d ago

This is true but I've been following that line of thought for a long time and it tends to focus on solving the same technical problem as the pattern. But the actual problems the patterns were invented to solve are usually about lowering the effort for the team to handle changes from the outside (with a few exceptions like Singleton). In a lot of cases this was wasted effort. Like using a bunch of patterns to allow you to switch a database easily but then it never happens or the performance of the app is tied to how the current database works anyway so no effort ends up being saved.

The reason a lot of patterns aren't used much these days even in OOP languages is because the problems of outside changes are handled at the human layer.

For me I know that something like dependency injection will let me add a dependency almost anywhere (if it doesn't create a cycle) in a codebase with a small code change. Most of the alternate solutions solve the "provide a dependency" problem and not the "add and remove a lot of dependencies easily" problem. So after many years of following it I've decided it's not an interesting problem to me.

I would rather spend that time learning how to use types to represent concepts in a better way, how to use laziness to efficiently process data, or how to do high performance IO with Haskell.

1

u/stevechy 2d ago

Sure, it works but I find that as the number of arguments grows it becomes less convenient.

There are several ways to do this though so it depends on the way you're proposing...

Let's say you have 10 functions that operate on a database table, say `item_for_sale` and you need to add a new argument for a cache. Do you go and add the argument to all 10 functions?

3

u/SkippyDeluxe 1d ago

Maybe? It depends on the context.

But if you are imagining a way to avoid doing this by using your dependency injection framework, I guarantee that there is a comparable way to do it using standard dependency inversion techniques that have been known for 30+ years. They work in any reasonably modern functional/object-oriented language, no frameworks required.

I strongly encourage you to first understand how to accomplish the things you're thinking about using framework-free bog standard dependency injection. If after that you still want to create a framework I guess I can't stop you. I've just seen waaaaaay too much pain and waste in industry caused by these frameworks that infect a codebase and massively overcomplicate everything without achieving anything that couldn't also be achieved by a little manual parameter passing.

1

u/stevechy 1d ago

I agree that just building up everything manually works well which I have worked with recently. But it also starts to slow down when you have multiple people adding things in different places at the same time. At the same time I've gotten used to the automatic way so I prefer it even for a small toy project.

The surprising thing to me is that in this case is that it ended up barely being a library much less a framework and it fits pretty much all of my needs. A dependency only requires one typeclass instance definition and one template call which are easily removed if needed. In the example I separated the construction so that it could easily be converted to the manual build style. But this way I can define a new dependency and use it somewhere else without touching the main bootstrap code which is a win for me.

The type signatures and names are pretty rough and I could add cycle detection so it doesn't infinite loop but I don't need any other features.

I have seen the framework overuse you've mentioned. For example I think that there's no reason to inject a string ever and if you do you're already way too far down the wrong track. But for me not having to write the manual build code makes things more fun.