MarcoTrevisiol

MarcoTrevisiol

A matter of style in polymorphism: what are the best practices to obtain (safe) polymorphism?

I would like a piece of elixir code to do something like the following:

defmodule InfoManager do
  def initial_state(queried_info) do
    queried_info
    |> Stream.map(fn i -> {i, atom_to_module(i).initial_value()} end)
    |> Enum.into(%{})
  end

  def query(state, info) do
    info
    |> Stream.map(fn i -> {i, Map.get(state, i)} end)
    |> Enum.into(%{})
  end

  def receive(state, news) do
    state
    |> Stream.map(&update_value_in_state(&1, news))
    |> Enum.into(%{})
  end

  defp update_value_in_state({key, value}, news),
    do: {key, atom_to_module(key).updated_value(value, news)}

  defp atom_to_module(atom) do
    atom
    |> Atom.to_string()
    |> (fn s -> "Elixir.Info." <> String.capitalize(s) end).()
    |> String.to_atom()
  end
end

so that I can define modules like this

defmodule Info.Day_number do
  def initial_value, do: 0
  def updated_value(value, _news), do: value + 1
end

defmodule Info.News do
  def initial_value, do: nil
  def updated_value(_value, news), do: news
end

so that I can use InfoManager like this

state = InfoManager.initial_state([:day_number, :news])
state = InfoManager.receive(state, "irrelevant_news")
info = InfoManager.query([:day_number])
assert info == %{day_number: 1}

I am not satisfied by my code, right now. In particular, I am not convinced by the hack inside atom_to_module. For one, it feels a waste of resources (think about calling thousand of times the function InfoManager.query/1), but also it does not look like an idiomatic way to pass some module to a function.

What are the best practices to obtain (safe) polymorphism in situations like that? Should I use something like behaviours? Should I keep some more state around (like converting all atoms in the first call to the correct module and keep the mapping between the two around)?

Marked As Solved

mpope

mpope

I’ve worked on systems that used run-time polymorphism using similar string to atom manipulation to resolve target modules, where we had hundreds of modules. We eventually settled on building an persistent_term cache at startup of

%{{Interface, SpecificImpl} => Interface.SpecificImpl

and abandoning strings pretty much all together in favor of the atoms and tuples.

We had to resolve these modules a rather extreme amount. Using the perf support BeamAsm, the Erlang JIT — erts v15.1.2 for Linux we settled on this being both good enough and better than string to atom construction. I don’t remember the numbers off the top of my head as this was a few years ago.

For your use-case I would highly recommend using perf on your implementation under load to understand if its a bottleneck then coming up with an alternative solution. We only realized this through measuring, and I believe we saw the problem present in eprof/fprof as well.

Also Liked

al2o3cr

al2o3cr

Here’s a slightly-different implementation that uses a protocol for polymorphic dispatch:

defprotocol InfoProtocol do
  def key(data)
  def initial_value(data)
  def current_value(data)
  def updated_value(data, input)
end

defmodule Info.DayNumber do
  defstruct [:value]

  defimpl InfoProtocol do
    def key(_), do: :day_number
    def initial_value(data), do: %{data | value: 0}
    def current_value(data), do: data.value
    def updated_value(data, _input), do: %{data | value: data.value + 1}
  end
end

defmodule Info.News do
  defstruct [:value]

  defimpl InfoProtocol do
    def key(_), do: :news
    def initial_value(data), do: %{data | value: nil}
    def current_value(data), do: data.value
    def updated_value(data, input), do: %{data | value: input}
  end
end

defmodule InfoManager do
  def initial_state(queried_info) do
    Map.new(
      queried_info,
      fn mod ->
        empty = struct(mod)
        {InfoProtocol.key(empty), InfoProtocol.initial_value(empty)}
      end
    )
  end

  def query(state, info) do
    state
    |> Map.take(info)
    |> Map.new(fn {k, v} ->
      {k, InfoProtocol.current_value(v)}
    end)
  end

  def receive(state, news) do
    Map.new(state, fn {k, v} ->
      {k, InfoProtocol.updated_value(v, news)}
    end)
  end
end

Usage:

state = InfoManager.initial_state([Info.DayNumber, Info.News])
state = InfoManager.receive(state, "irrelevant_news")
info = InfoManager.query(state, [:day_number])

The only API change is that InfoManager.initial_state expects a list of atoms that can be passed to struct, rather than trying to inflect the appropriate name from a lowercased/underscored atom.

I also added current_value, so InfoManager doesn’t need to know anything about the structs it manipulates besides “they implement InfoProtocol”.

MarcoTrevisiol

MarcoTrevisiol

Thank you for each suggestion of yours, I’ll keep them in mind next time I need something similiar.

@al2o3cr I like your solution, but I am a little unconfortable with the amount of boiler plate required to write an individual Info. I think I could do some macro to fill it, but I think I am too much early in the project to use them.

@mpope In the end I chose something very similar to your solution, with the only difference that I keep the cache inside the “state” variable, as I think it is the simplest solution. Right now I am not concerned with performance, I was just wandering what is the idiomatic way to express this kind of computation.

Thank you all for the precious suggestions, as I my elixir is still very basic.

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
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
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
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
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
wernerlaude
In AR this is so simple @articles = current_user.articles How to do in Ecto? def index(conn, _params) do current_user = conn.assig...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
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
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement