Multiple callbacks for same function, different arguments

My application has many serializers for turning structs into something suitable for clients. These serializers have many serialize/1 functions defined to handle the nil case, list case, and finally the struct itself:

defmodule MyApp.ItemSerializer do
  def serialize(nil), do: nil
  def serialize(items) when is_list(items) do
    Enum.map(items, serialize/1)
  end

  def serialize(item) do
    %{
          # ...
        }
  end
end

I’d like to have a @behaviour MyApp.Serializer directive to guarantee that individual implementations follow the contract of implementing all the serialize/1 functions. The problem is, it looks like callbacks only respect one function implementation, and creating many callbacks with the same function name but different typespecs does not help.

Is this the expected --ahem-- behavior of callbacks?

This is linked to how functions are differentiated in the BEAM. Functions are differentiated by the module they reside in, their name and arity (number of arguments the function accepts). As such serialize/1 is the same function and the various function clauses a contraction of an if-else.

If having these 3 cases are import the define the 3 different functions in the behaviour individually e.g. serialize_nil/1, serialize_lists/1, serialize_item/1. The behaviour can auto-generate a serialize/1 which calls the relevant callbacks.

If you have many different structs perhaps look into using Protocols.

1 Like

Why don’t you make it a protocoll? Then you could implement the list-case once in a defimpl X, for: List, the nil in defimpl X, for: Atom, while having discrete defimpl X, for: Y for your different structs.

2 Likes

The problem with using a Protocol is I want to be able to support different versions of the serializer for the same object, so GameSerializer.V1.serialize/1 and GameSerializer.V2.serialize/1, with the caller selecting which one to use. So I can’t just call a generic Serializer.serialize/1 without knowing what I’m passing in (list, nil, actual object) which I believe rules out the use of a single protocol. Unless I’m missing something?

How do you determine the version?

The controller itself calls either the V1 or V2 serializer, since the controller/route is namespaced with versions as well. So basically hardcoded.

Why not encode that into the dispatch argument then? :slight_smile:

Or use a protocol per version that can delegate to the lower version for the parts that are the same.

Or use my ProtocolEx to dispatch on both the version and value.

There are even more things you can do. :slight_smile:

Here’s an idea - use a protocol with two callbacks - serialize_v1 and serialize_v2. For implementations where those are close enough or even the same, it’s pretty easy to delegate to a common implementation.

2 Likes

Or use a protocol per version that can delegate to the lower version for the parts that are the same.

Yep, precisely my second suggestion. ^.^