Creating a custom list type

Hey all, I have a set of pipelines that need to perform operations on some list. These operations should be implemented differently depending on the type of the list, but the pipeline itself is not concerned with how they are implemented. I was thinking of using a protocol for this and creating two custom list types, one called MinList and other MaxList. Then, the pipeline can just use the protocol function and everything will behave as expected.

However, I am unable to find documentation on how to define a custom list type. I tried doing this:

defprotocol SpecialList do
  @spec empty?(t) :: boolean
  def empty?(t)

  @spec update(t, elem) :: t
  def update(t, elem)
end

defmodule MinList do
  @type t() :: list(number())
end

defimpl SpecialList, for: FinalMix.MinList do
  def empty?(list) do
    ... # Implement
  end

   def update(list, elem) do
    ... # Implement
  end
end

However, when I try iex and do

iex> MinList |> SpecialList.empty

I get errors saying the protocol is not implemented for type atom. Any tips here?

If I’m understanding you correctly why not just use simple pattern matching.

def empty?([]), do: true
def empty?(list) when is_list(list), do: false

Then you can use another function to perform specific operations.

def min_max(list) do
  case empty?(list) do
    true -> "do this"
    false -> "do that"
  end
end

This code gives you implementation for FinalMix.MinList struct, so you should call it like:

iex> %MinList{…} |> SpecialList.empty

There are many ways to do what you need …

Your example which is not best in this case should look like:

defprotocol SpecialList do
  @spec empty?(t) :: boolean
  def empty?(t)

  @spec update(t, elem) :: t
  def update(t, elem)
end

defmodule MinList do
  defstruct [:items]

  @type t() :: list(number())
end

defimpl SpecialList, for: FinalMix.MinList do
  def empty?(list) do
    ... # Implement
  end

   def update(list, elem) do
    ... # Implement
  end
end

iex> %MinList{items: []} |> SpecialList.empty()

However protocol for many structs with just one field does not looks good. You should think about it like:

defprotocol SpecialListItem do
  # …
end

defimpl SpecialListItem, for: Integer do
  # …
end

That way you can implement for example custom map:

# instead of
SpecialList.map(%MinList{items: […]})
# call
Enum.map([…], &SpecialList.map/1)

However protocols are not the only way to write such code for example:

defmodule SpecialList do
  def map([head | _tail] = list) when is_integer(head) do
    Enum.map(list, &map_integer/1)
  end

  defp map_integer(integer) do
    # …
  end 
end

Alternatively you can pass a custom atom type for example:

defmodule SpecialList do
  def map(list, :integer) do
    Enum.map(list, &map_integer/1)
  end

  defp map_integer(integer) do
    # …
  end 
end

Of course nobody stops you from splitting such implementations into multiple modules, for example:

defmodule SpecialList do
  def map(list, :integer) do
    Enum.map(list, &SpecialList.Integer.map/1)
  end
end
1 Like

That‘s because this is not possible. There are no custom datatypes on the beam. There are however conventions around certain usages of those available types, which allow for userland „types“. In erlang those are records based on tuples and in elixir it‘s structs based on maps (although it also has support for records). Protocol dispatching therefore works with structs and all the native datatypes on the beam.

1 Like