Is there something like Protocol, but for simple Atom identifiers?

What I’m looking for is API similar to mix Bahavior and Protocol, but I don’t want to create struct for every implementation, because that struct will be not modified (only default values), so it does not make sense.
I need to have List of them dynamically, so my project could be used as a library and developer could add their own implementations.
I also want to declare optional and required API.

I’m looking for something like this:

defprotocol ProtocolName do
  # api here ...
end

defimpl ProtocolName, for: :identifier do
  # api implementation here
end

first_result =
  :identifier
  |> Protocol.find_implementation
  |> apply(:api_method, [])

second_result =
  ProtocolName
  |> Protocol.implementations_for
  |> Enum.map(&apply(&1, :api_method, []))
  |> Enum.flatten

Is it possible to write in Elixir?

2 Likes

You could just make a custom Protocol.implementations_for that encodes the module atoms and call things on them straight.

/me really should publish the hack that is PluginEx as that would fit this use-case perfectly…

2 Likes

A Protocol is just a wrapper around a Behaviour that automatically dispatches there depending on the struct-type of the first argument.

You can easily do this dispatching yourself. The simplest would be, if your identifiers are 1:1 the same as the module names, you could do:

defmodule ProtocolName do
  # api here ...
  @callback foo(int) :: String.t
end

defmodule ProtocolName.FirstIdentifierType do
  # api implementation here
  @behaviour ProtocolName
  def foo(0), do: "0!"
  def foo(n), do: "nonzero!"
end

first_result =
  FirstIdentifierType
  |> apply(:api_method, [])

This works fine.

On a side note, though, if you want to do more advanced things, like your second_result, you’ll end up re-writing most of the code that Elixir’s built-in Protocols itself has.
It is not an issue to define structs that do not contain any fields (just do defstruct []), so if you want to do more complex stuff, I’d advise you to go that route.

4 Likes

I also think about this. Looks like it’s currently closest way to achieve results that I expect unless there is already created a similar library.

I know, but this does not makes any sense to pass struct in that case. I read Protocol code and it looks like that it’s possible to create library for that case.

If someone know a library with similar features then I can accept it as a answer. Otherwise I will create my own library that will meet my needs. Heh, one more idea on my TDOO List :smile:.

2 Likes

You can look at what I did for PhstTransform, basically the user supplies a Map that matches Atoms to functions.

I wrap the use of the user-supplied map with a function that implements default functions for any Atoms the user does not supply. In my case the Atoms are the standard Elixir Data Types plus structs, but the technique could work for any list of Atoms.

2 Likes

Note: I think you need to be a bit careful with this kind of approach… It’s very easy (and tempting) to reinvent OO programming in Elixir with these techniques.

I use this kind of dynamic dispatch all the time, but I’d think twice when exposing it as a library in a large application.

1 Like

cough

2 Likes

I just pushed initial commit for my ExApi library.
Before I send it to hex.pm I want to ask about your opinions.

ping: @Qqwy, @OvermindDL1, @bbense, @ejc123 and @jeffdeville

@jeffdeville: How about add it to nh_verify project? If not then no problem I have other plans for this library usage. :smile:

1 Like

I looked at your project. It’s not 100% what I’m looking for (see my repository), but it’s also really interesting idea!
Thanks, I will bookmark it!

1 Like

Hmm, did not see from cursory scan but does it support multiple API’s on a single module?

1 Like

I don’t know what you mean.
It’s like declaring Behaviour (+ some more info that this library can read from beam files - similar to Protocol).

If you mean calling multiple defapi macro on same module name then answer is no and I don’t see a use case for it.

Protocol use case is for example Enum where you have a data to manipulate. If you don’t have them and you still need get a List of implementations then use my library.

For example:
I’m going to create a library for git hosting services (like github or gitlab), so they will have a unique and simple API (without need to care about requests). I don’t have a data to manipulate - just I have a list of unique APIs to implement for different services. I can optionally identify implementations like:

defapiidimpl MyImpl.ModuleName, MyApi.ModuleName, :github do
  # ...
end

# or:

defapiidimpl MyApi.ModuleName, :github do
  # Module name is: MyApi.ModuleName.Github
  # ...
end

You can declare Behaviour inside another modules like:

defmodule PartA.PartB do
  @callback my_func(atom) :: atom
end
defmodule PartA.PartC do
  @callback my_func(atom) :: atom
end

# or

defmodule PartA do
  defmodule PartB do
    @callback my_func(atom) :: atom
  end
  defmodule PartC do
    @callback my_func(atom) :: atom
  end
end

with my library it’s similar:

import ExApi.Kernel

defapi PartA.PartB do
  @callback my_func(atom) :: atom
end
defapi PartA.PartC do
  @callback my_func(atom) :: atom
end

# or

defmodule PartA do
  defapi PartB do
    @callback my_func(atom) :: atom
  end
  defapi PartC do
    @callback my_func(atom) :: atom
  end
end

I hope that it’s helpful.

1 Like

Ah, mine works by scanning for behaviours themselves, and I have a set of modules that have multiple behaviours that are picked up dynamically in the same way. :slight_smile:

1 Like

Oh, did you released it (on hex)? Can you share a link?

1 Like

I’ve not yet because, like yours, it is a hack by scanning beam files, it will not work in ‘all’ cases (but will work in ‘most’), and those corner cases hold me back from releasing it. ^.^

1 Like

ok, Do you have any ideas? Do you have a public repo, s I can help you? If it’s for same use case then we can cooperate instead of creating two libraries :smile:

1 Like

Heh, not entirely, the issue is that the beam files are not always available (whether in a tar file or baked into a single core file, they might not exist).

I’d like to make a macro that generates a file of all known beam files that fulfill the requirements at compile-time to hard-code in such a list, problem is that then prevents dynamically adding beam files later (think plugins to a website) without hard specifying the name (and if you do that anyway then this is not of much use).

There are just too many unhandled cases is the problem…

I don’t think I have it on any of my public repo’s, it is actually just a single file (PluginEx/plugin_ex.ex, adapted for an old erlang version I made almost a decade ago) that I just copy around from project to project. I know its limitations and I get a sick pit in my stomach every time I use it as I know that then prevents this project from working in more embedded areas. ^.^

It is the same use-case, but the problem is just that this method of our implementations is entirely broken in many important case. What needs to be made is a more generic plugin system with perhaps a registration area, but the issue is no way to know if a module implements something without it being loaded, and it may be impossible to pre-load it if the names are not known in some other way. I never really came up with a great idea to the overall problem without forcing every-single-plugin to become registered into the supervision tree some-how, even if it is just some simple callbacks, and even then you have to list the application in the startup file anyway, which is still registering a static name somewhere, which again just defeats the purpose of dynamic lookup. ^.^

1 Like

@OvermindDL1: ok, so here is what we need:

  1. An Agent like module to: add Api / Plugin, remove it and list all of them
  2. defapi / defplugin macro that calls that Agent to add it
  3. defapiimpl / defpluginimpl macro that loads Api / Plugin from Agent and use it as Behaviour
  4. Test that Agent from first point is loaded before rest code

What about using Registry for first point?

1 Like

And you still need some way to tell it to actually ‘execute’ the beam file by referencing it in some way, unless you are wanting to store the plugin list in a dynamically generated module (ala protocols)?

1 Like

@OvermindDL1: hmm, I don’t know how it work from beam side :smile:
I think about:

  1. Setup and start Agent before any Api - I think that libraries are executed before Elixir app (correct me if I’m wrong), so we need to add to mix file our application (Api library) to start Registry.
  2. When a defapi macro code is executed then we have already initialized Registry from extra applications in mix (from our library), right? In that way we can save module name in Registry.
  3. In runtime we call Registry to list Apis or it’s implementations.

A path to beam could be get by this code:

path = :code.lib_dir(:app_name, :ebin)
filename = Atom.to_string(MyModule) <> ".beam"
full_path = :filename.join(path, filename)

Am I missed something?

1 Like

That will happen at compile-time, it will not persist into run-time.

That will not work in all builds. What if they do an embedded build where the beam files are in a tar? Or what if they cook all the beam’s together into the one main core file, then you have no beam files. :wink:

2 Likes