jTy
Hi there! I’ve been playing with Elixir for a while and stumbled upon dependency injection. The topic comes naturally to me as having C# background where it’s pretty-well known, especially amongst professionals. Since we are dealing with callbacks and behaviors instead of abstraction and implementation while there is no such concept as an interface (which by the way, I consider if occurred, would probably be somewhere between protocol and behavior in terms of both complexity and (supposedly) ergonomics), I find it as cumbersome - an obstacle that effectively stops me from considering Elixir at production whether it is small or medium-size project., not even mentioning larger ones.
Some people pointed me to libraries that concentrate on facilitating tests (in particular test doubles - mocks) although dependency injection has nothing to do with that (admittedly we can bring them together under certain circumstances). I would love to write beautiful, cohesive, loosely-coupled programs - just not sure if that is currently possible.
I thought that maybe I can utilize what’s already existing and achieve what I want with metaprogramming - for demonstration let’s consider the following example:
- We start with an interface:
i_writer.ex
defmodule IWriter do
@callback write() :: atom()
@callback write(args :: any()) :: atom()
@optional_callbacks write: 0, write: 1
defmacro __using__(_) do
quote location: :keep do
@behaviour IWriter
@after_compile IWriter
defp do_write(), do: IWriter.do_write(__MODULE__)
defp do_write(write_args), do: IWriter.do_write(__MODULE__, write_args)
end
end
def do_write(module),
do: fn -> apply(module, :write, []) end
def do_write(module, write_args),
do: fn -> apply(module, :write, write_args) end
def __after_compile__(env, _bytecode) do
:functions
|> env.module.__info__()
|> Keyword.get_values(:write)
|> case do
[] -> raise "`write/0` _or_ `write/1` is required"
[0] -> :ok # no args
[1] -> :ok # with args
[_] -> raise "Arity `0` _or_ `1` please"
[_|_] -> raise "Either `write/0` _or_ `write/1` please"
end
end
end
- Followed by composition:
text_writer.ex
defmodule TextWriter do
defstruct [:writer]
@type wrt :: IWriter
@opaque t :: %__MODULE__{
writer: wrt
}
@spec new(writer :: IWriter) :: TextWriter.t()
def new(writer) do
%__MODULE__{
writer: writer
}
end
@spec write_with(writer :: IWriter) :: :ok
def write_with(writer), do: writer.write()
@spec write_with(writer :: IWriter, args :: any()) :: :ok
def write_with(writer, args), do: writer.write(args)
end
And a sample implementation:
dummy_writer.ex
defmodule DummyWriter do
use IWriter
@type t :: __MODULE__
def write(), do: :dummy
end
Assuming the following invocation:
nw = TextWriter.new(DummyWriter)
nw.writer.write()
The drawback, one amongst many, is that Dialyzer does not help us here.
Some questions:
- Interface type - are there any plans for supporting it when proper type-system occurs in Elixir?
- Are there any relevant approaches for compile-time dependency injection in Elixir as for now?
- Are there any libraries out there that support all 4 dependency injection types (constructor injection, parameter injection, method injection, ambient context)?
…or maybe I’m thinking/doing something wrong? Please let me know.
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #ai
- #elixirconf-us
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
LostKobrakai
There are neither constructors nor methods in elixir, given there’s no objects. You can pass module names to functions, so parameter injection just works as far as I’m aware. No need for any library. Not sure what ambient context is meant to be.
Can you elaborate a bit more? It’s not exactly sure what your problem with behaviours is in the first place.
D4no0
I have a feeling that this looks like class reinvention from languages like c# and java.
What is the actual gain of having the
IWritercall the actual functions from the modules you define? if that is only to enforce theIWritertype for specs there are easier methods to do that, putting your data in a struct of typeIWriter.That aside, by using
apply/3you lose all the compile-time power as apply is executed at runtime, so dialyzer cannot solve your types at compile-time.This is quite sad to hear, as I think getting rid of classes makes the code much more readable and maintainable in the long run. Maybe instead of trying to fit elixir to the way things are done in c#, you should take a step back and learn how things are done in functional languages.
jTy
How so? I’m not sure why do you make such association. If treating interface as a concept specific to given paradigm or language is this the way to go then I’d rather see how one would describe the relation between abstraction and interfaces language-wise (since we’re on Elixir forum → in Elixir terms), but maybe that’s just me.
D4no0
Once again, I will stick to the thing that I think that interfaces and the way classes inherit them is a prominent feature OOP languages like c# and java.
Now saying all of this, I’ve had a project where we would use behaviors to implement multiple modules with same functions, then call the modules dynamically (since the module name is just an atom) based on a configuration.
From your post, it seems that you want to enforce this thing at compile-time, but the question is why and how is this different from protocols (not as implementation but as a end result)? and the other question is if you were to remove your functions from data structures and use a more functional approach, would those interfaces even make sense?
dimitarvp
After reading your code and post, I am still not 100% sure what you are after. “Dependency injection” is just a tool, and tools are used to achieve goals. What’s your goal?
If you want to have a discrete set of implementors of a certain behaviour / protocol (the Elixir meanings of them) and then look them up / use them wherever the behaviour / protocol is needed then that can be done fairly easily and we can help you get there.
You do need to show an open mind however; insisting that Elixir must have what C# has is… not an interesting discussion. Elixir has the tools to achieve similar goals.
jTy
Golang for instance, whether we consider paradigm or not, has support for interfaces and of course, inheritance (more formally: generalization) is not the only relation.
When it comes to me, I tend to use interfaces as modelling technique that is somewhat useful in design - I’d even call it as fundamental building block.
Did this hinder your ability to analyze the solution using xref or similar tool, retrospectively?
This is rather about encapsulation levels - my ‘mental model’ consists of the following:
My thinking was that in order to have the pure interface form I can either leverage protocol or ‘strip’ behavior.
I’m not sure whether the problem sits in behavior construct itself or the way it is used, e.g. consider this:
It just seems odd to me.
jTy
If you could suggest some literature or articles to read then I’d be grateful - always happy to learn something new.
Agree - that’s why I don’t insist.
LostKobrakai
So behaviours are generally meant so you can customize the internals of a large whole. E.g. for a GenServer you can just deal with some well selected callbacks of “here I want your input”, while the bulk of the process loop and a lot of internals and implementation details are hidden from you.
Your example behaviour is so shallow I’m wondering if there’s any use to it besides a small bit of documentation.
It also still doesn’t show what you’re “missing”. Some feeling of odd might be expected dealing with a new way of handling things, but I’m still not sure what all the complexity you presented in your initial post is meant to bring/add to the table.
al2o3cr
My initial reaction is that there’s something missing / inconsistent with the example:
the private
do_writefunctions that are injected byuse IWriterdon’t appear to be called anywherethe
write_withfunctions inTextWriteraren’t called anywhere eithercalling a function through this mechanism requires accessing the internals of an opaque type (with
nw.writer)There are some other parts that seem odd:
in
TextWriter,@type wrt :: IWriter- this declareswrtas a type inhabited by exactly one member, the atom:"Elixir.IWriter". You’d typically see this kind of construct used to create an enum-like with multiple atoms (@type something :: :foo | :bar | :baz) but the compiler is perfectly fine with exactly onesimilarly,
TextWriter.t()is a struct where the only expected value of thewriterfield is that same atom. This is consistent with the typing onTextWriter.new/1because it only promises to accept:"Elixir.IWriter"as an argument.however, that means that this doesn’t satisfy the typespec:
TextWriter.new(DummyWriter)sinceDummyWriteris not the expected atomRegarding the overall concept, IMO it’s reimplementing OO awkwardly with “objects” like
TextWriterneeding to explicitly hold a vtable-like thing.You may find the discussions around the removal of “tuple calls” from the BEAM back in 2017 interesting:
tj0
The IWriter/DummyWriter modules appears to be boilerplate from an elixir standpoint, so it’s quite difficult to understand your goal.
After reading this thru twice, I think what you’re after is “protocols”. They allow polymorphism of by dynamically dispatching based on the value type. I believe that you can also call modules dynamically which can allow an interface definition that can be verified by dialyzer.