Qqwy

Qqwy

TypeCheck Core Team

Putting non-protocol functions in a protocol?

As you know, Elixir protocols dispatch to the particular implementation based on the first argument passed to their functions.

For many use-cases this is fine.
Sometimes, however, it is not.
One such situation is when we want to add a function that creates a datatype in a generic way. At this time we probably know the module name of the implementation we’d like to use, but do not have a struct of that type.

This leaves a couple of different possibilities. I am hoping for some feedback on which technique you’d prefer:

1. Have a separate Behaviour module. Structs implementing the protocol should implement this behaviour for structs as well.

Advantage:

  • Does not need any ‘hacks’.

Disadvantage:

  • It is easy to miss/forget implementing the behaviour
  • It might be confusing that there both is a protocol and a behaviour that together specify the interface that needs to be implemented.

2. Add a function to the protocol which does not actually expect the struct as first argument.

Advantage:

  • Only a single module which defines the interface.

Disadvantage: Seems a bit like a ‘hack’:

  • It requires manual protocol dispatch, which is hackish as since we do not have a struct of the protocol yet (but only a module name), we cannot rely on ProtocolName.impl_for(datatype). Manually concatenating module names currently works, but seems like relying on an implementation detail.
  • It might mess with protocol consolidation.
  • Elixir and/or tools like Dializer or Credo might produce warnings.

3. Using a library-provided ‘extended protocol’

One example of a library providing extended protocols would be protocol_ex.

Advantage:

  • it might be possible to implement this pattern directly.

Disadvantage:

  • It might be overkill
  • Improved developer complexity: It’s a new library that developers need to understand.
  • Circumventing normal protocols will mean that improvements to normal protocols (like e.g. better consolidation) cannot be used.

4. ???

Maybe there are other possibilities as well?


If you need more context, this recently came up here, PR #32 of the Arrays library.

Most Liked

eksperimental

eksperimental

First of all I would like to mention that defining callbacks with the @callback directive is discouraged.
While it can still be used, ExDoc will not list these callbacks in the protocol documentation.
See this issue for reference.

The approach that I ended up taking was to define a submodule called Behaviour, where you can end up placing all your @callbacks. There is a caveat though, that the functions that define functions/macros are not available, this is due to the way protocol is implementing disabling these functions in the parent module. One way to solve this is to create a module outside the protocol definition (defmodule MyProtocol.Behaviour).
But for clarity I preferred to keep it as a submodule.
You can see the implementaiton of this here:

Please let me know what you think about this approach

Qqwy

Qqwy

TypeCheck Core Team

It seems to me like we are treading in unexplored waters, and am eager to learn what people of the Elixir core have to say about this situation.

Qqwy

Qqwy

TypeCheck Core Team

Let me give some extra context. There definitely are situations in which I’d go for @LostKobrakai 's approach, but it cannot be used here.

Arrays and some similar libraries (e.g. okasaki, sets, prioqueue) have a unified interface module (in this case Arrays) which contains some generic code. For some functions this generic code calls a particular protocol implementation.

This pattern is common elsewhere in Elixir. For instance we have an Enum module which contains generic code that internally uses the Enumerable protocol implementations.

The idea is that user code should (only) use the unified interface, and that they can specify in configuration (either in config.exs or by passing explicit options to Arrays.empty/1 or Arrays.new/2) which array implementation they want to use.

This works great, except when actually creating the initial structs (. Here we cannot dispatch to the protocol implementation because we do not have a struct yet. We only have the module name.
And that is where the conundrum lies.
What do we do for this situation?

Adding a function (also called empty) to the module that contains the defstruct as @al2o3cr is indeed the current approach.
To make it slightly more clear that we need this module to implement this particular function we use a behaviour.

However, this means we have ánd a behaviour ánd a protocol, with the pros and cons outlined in the first post above.
We’re looking for ways to make the interface of the library as a whole better.
(For both users as well as implementers of new e.g. array backends).

LostKobrakai

LostKobrakai

ˋArraysˋ is not the protocol though – ˋArrays.Protocolˋ is. Your public API is not your protocol, and given the constraint you mentioned it cannot be. Same for e.g. ˋEnumˋ btw. The protocol is ˋEnumerableˋ while ˋEnumˋ uses it to provide a nice API. But on the other hand take a look at ˋAccessˋ, which is actually a behaviour (even when often mentioned to be a protocol), which you need to implement to have datatypes be accessible as data containers.

You‘re obviously in a situation where you need both the behaviour version of datatypes providing certain functions as well as the protocol side. What I suggested would at least remove the need of explicitly defining the protocol implementation unless something truly custom needs to happen. The downside to behaviours however is that they cannot be implemented in userland like protocols can.

In the end I question if you actually need ˋemptyˋ on the protocol, because how useful is ˋArray.Protocol.empty(…)ˋ if there‘s nothing to dispatch on. For ˋEnumˋ one would also do ˋMapSet.new(list) |> Enum.map(…)ˋ as well.

ityonemo

ityonemo

Protocol already defines a behaviour, so you should just declare that behaviour in an outer module that defdelegates to the protocol module.

If you do the following, you will see that dialyxir/elixir_ls indicates the error “type mysmatch for @callback f/1 in P behaviour”

defprotocol P do
  @spec f(term) :: atom()
  def f(x)
end

defmodule I do
  defstruct []
  defimpl P do
    def f(_x), do: 32
  end
end

defmodule Pr do
  @behaviour P

  defdelegate f(x), to: P
end

moreover, if you do Code.Typespec.fetch_callbacks(P) it will show you the correct callback for the P.f/1 function.

I also just verified that you can add extra callbacks to the Protocol callback if you desire.

Where Next?

Popular in Questions Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
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
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
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
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

Other popular topics Top

aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New

We're in Beta

About us Mission Statement