sallaumen

sallaumen

@spec best practices when having defdelegates

ElixirForum

Hello, my beloveds, I was trying to reach in the best practices with some of my work colleagues @liryanne and @tom335, but we were not able to find an answer to this question. In this way, I would like to ask your opnion about a codding pattern that I could not find anywhere.

When using defdelegate and writing the needed @spec to my methods, where is the correct place to set these @specs?

Example:

ModuleA:

defmodule ImplementationModule do
  @spec inspect_data(data :: any()) :: {:ok, data :: any()}
  def inspect_data(data) do
    IO.puts("Inspecting_data...")
    {:ok, IO.inspect(data)}
  end
end

ModuleB which is the public interface for the internal ModuleA (and in a real case, lots of other internal modules):

defmodule DelegateModule do
  @spec inspect_data(data :: any()) :: {:ok, data :: any()}
  defdelegate inspect_data(data), to: ImplementationModule
end

IMHO, it gets redundant re-writing the @spec twice, this is the pattern I’ve been currently using, because this is my team’s project pattern, but I really would love to know what would be the best recommendation for this scenario

Marked As Solved

zachallaun

zachallaun

I agree that this is a great question. Maybe it being bumped now will elicit some more thoughts and opinions.

Here’s why I think that duplicating the specs is correct:

defdelegate is merely a convenient way of generating a function, but the fact that the call is being delegated should be invisible to the caller. The spec above the defdelegate is the public contract – from a compatibility standpoint, that’s the one you want to maintain. From the caller’s perspective, these are equivalent:

@spec inspect_data(any()) :: {:ok, any())
defdelegate inspect_data(data), to: ImplementationModule

# same as

@spec inspect_data(any()) :: {:ok, any()}
def inspect_data(data), do: ImplementationModule.inspect_data(data)

If someone changes the spec in the private implementation, I don’t want that to silently “leak” into the public API. For instance, let’s say that we want ImplementationModule.inspect_data/1 to stop returning a tuple, so it becomes:

defmodule ImplementationModule do
  @spec inspect_data(any()) :: any()
  def inspect_data(data) do
    ...
  end
end

I’d want dialyzer to yell at me! Hey, your public module’s spec returns a tuple, but the function it’s calling doesn’t! Now I have to make an explicit decision: Am I changing the public contract (a breaking change), or am I going to wrap it in a way that maintains the previous behavior?

@spec inspect_data(any()) :: {:ok, any()}
def inspect_data(data) do
  {:ok, ImplementationModule.inspect_data(data)}
end

If the public module implicitly “delegated” its spec as well, it is now fully exposing what should be an implementation detail. Duplicating the specs requires that you make a conscious decision if the implementation spec changes and that, in my opinion, makes it the correct option.

Also Liked

al2o3cr

al2o3cr

I could see this being tricky - defdelegate takes specific steps to not have a compile-time dependency on the targeted module, but the only way it could fetch a @spec is from the compiled target…

tom335

tom335

As described by @sallaumen, we’ve the feeling that the specs should only appear once, in the actual implementation; however it can be useful to replicate the specs in the module which delegate the same methods, in terms of documentation or even when publishing an external API, for example. It would be great to hear other use cases on similar situations.

darraghenright

darraghenright

This is a great question. A minor enough quibble perhaps, but it certainly feels redundant to add specs twice, so to speak.

sallaumen

sallaumen

Really liked your point of view, thinking about the relation between the defdelegate function and the public function that it is delegating to, which has to keep consistent and by adding @spec on both dialyzer can help us with it too!

Thx for the answer @zachallaun

Where Next?

Popular in Questions Top

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
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
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
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
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
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

Other popular topics 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
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
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

We're in Beta

About us Mission Statement