jTy

jTy

Dependency injection & design by interface / SMI

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:

  1. 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
  1. 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.

Most Liked

D4no0

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 IWriter call the actual functions from the modules you define? if that is only to enforce the IWriter type for specs there are easier methods to do that, putting your data in a struct of type IWriter.

That aside, by using apply/3 you lose all the compile-time power as apply is executed at runtime, so dialyzer cannot solve your types at compile-time.

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.

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.

cevado

cevado

I’m too late in the discution but the simplest way of doing dependency injection in any functional language is just passing a function. there are ways to define a contract to be followed(behaviours, that some people already mentioned), but this is loosely restrict by the runtime.
Going with functions, you could have something like:

def write_with(writer) when is_function(writer, 0), do: writer.()
def write_with(writer, args) when is_function(writer, 1), do: writer.(args)
def write_with(writer, args) when is_function(writer), do: apply(wrter, args)

edit:
Usually I go with behaviours for stuff that I need a set of functions that need to be implemented and to be used together.

dimitarvp

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.

LostKobrakai

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.

tj0

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.

Where Next?

Popular in Questions Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New

Other popular topics Top

chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

We're in Beta

About us Mission Statement