Polymorphism / Inheritance

Trying Elixir polymorphism. The following code is an example i’ve written

defprotocol NetworkMessage do
  @doc "a network message"
  def serialize(msg);
end

defmodule HelloMessage do
  @enforce_keys [:key, :content]
  defstruct key: nil, content: nil

  defimpl NetworkMessage do
    def serialize(msg) do
      msg.key <> " " <> msg.content
    end
  end

  def deserialize(msg) do
    [key, content] = String.split(msg, " ", parts: 2)
    %HelloMessage{key: key, content: content}
  end
end

defmodule NetworkProtocol do
  def test do
    message = %HelloMessage{key: "abc", content: "defgh"}
    performed = NetworkMessage.serialize(message) |> HelloMessage.deserialize |> IO.inspect

    if message === performed do IO.puts("true") end
  end
end

Is there a way to enforce a function to be implemented on modules? Such as an Interface on a Class in java? In my case, I would like to force the function deserialize to be implemented, but I can’t put it in NetworkMessage (it gonna makes no sens).

I’d do a serialisable and a deserialisable protocol.

Interfaces though are roughly like callbacks.

I’d like to go more into detail but am in mobile, maybe later today if no-one else chimes in…

2 Likes

You’re right, I think I got it

defmodule NetworkMessage do
  @doc "a network message"
  @callback serialize(msg :: NetworkMessage) :: String.t;
  @callback deserialize(msg :: String.t) :: NetworkMessage;
end

defmodule HelloMessage do
  @behaviour NetworkMessage

  @enforce_keys [:key, :content]
  defstruct key: nil, content: nil

  def serialize(msg) do
    msg.key <> " " <> msg.content
  end

  def deserialize(msg) do
    [key, content] = String.split(msg, " ", parts: 2)
    %HelloMessage{key: key, content: content}
  end
end

defmodule NetworkProtocol do
  def test do
    message = %HelloMessage{key: "abc", content: "defgh"}
    performed = HelloMessage.serialize(message)
                |> HelloMessage.deserialize
                |> IO.inspect

    if message === performed do
      IO.puts("true") end
  end
end

You really should do protocolls here.

Would make your code a bit more generic, your current version does just do a remote call and callback or not, it would just work.

Roughly I’d come up with the following:

defprotocol Serializable do
  @type t :: term
  @spec serialize(t) :: binary
  def serialize(data)
end

defprotocol Deserializable do
  @type t :: term
  @spec deserialize_into(t, binary) :: t
  def deserialize_into(t, msg)
end

defmodule ExampleMsg do
  defstruct [:key, :content]

  defimpl Serializable do
    def serialize(%ExampleMsg{key: k, content: c}), do: :erlang.term_to_binary({k, c})
  end

  defimpl Deserializable do
    def deserialize_into(_, msg) do
      {k, c} = :erlang.binary_to_term(msg)
      %ExampleMsg{key: k, content: c}
    end
  end
end

msg = IO.inspect(Serializable.serialize(%ExampleMsg{key: "key", content: "content"}))
IO.inspect(Deserializable.deserialize_into(%ExampleMsg{}, msg))

This writes:

<<131, 104, 2, 109, 0, 0, 0, 3, 107, 101, 121, 109, 0, 0, 0, 7, 99, 111, 110,
  116, 101, 110, 116>>
%ExampleMsg{content: "content", key: "key"}

As a rule of thumb, if you want tp specify polymorphic functions over a datatype then you use a protocol, while you choose @behaviour and @callback when you want to enforce some API in a module, mostly unrelated to a datatype and very often used to define an API for plugin systems (eg. Mix.Task) or managed processes (eg. GenServer).

3 Likes

Interessting. Is this a MUST to put the type in the deserialize parameters? I see why you put it but Elixir does not require the type at Runtime.

You are not required to, but I really prefer to have them especially in protocolls.