Search code examples
erlangelixirdialyzer

Polymorphic types for dialyzer in Elixir


Background

I have a struct called MyApp.Result that is basically a representation of a Result Monad. This struct aims to be a formal structural representation for operation success and error:

defmodule MyApp.Result do

  @enforce_keys [:type]
  defstruct type: nil,
            result: nil,
            error_reason: nil,
            error_details: nil,
            input_parameters: []

  
  @type type :: :ok | :error
  @type result :: any()
  @type reason :: atom() | nil
  @type details :: any()
  @type params :: [any()]

  @type t() :: %__MODULE__{
          type: type(),
          result: result(),
          error_reason: reason(),
          error_details: details(),
          input_parameters: params()
        }

  @spec ok :: __MODULE__.t()
  def ok, do: %__MODULE__{type: :ok}


  @spec ok(result()) :: __MODULE__.t()
  def ok(result), do: %__MODULE__{type: :ok, result: result}

  @spec error(reason()) :: __MODULE__.t()
  def error(reason), do: %__MODULE__{type: :error, error_reason: reason}

  @spec error(reason(), details()) :: __MODULE__.t()
  def error(reason, details) do
    %__MODULE__{type: :error, error_reason: reason, error_details: details}
  end

  @spec error(reason(), details(), params()) :: __MODULE__.t()
  def error(reason, details, input) do
    %__MODULE__{
      type: :error,
      error_reason: reason,
      error_details: details,
      input_parameters: input
    }
  end

Problem

When showing this to one of my colleagues, he made a good point:

I see what you are trying to do here, but when I see a MyApp.Result struct I have to check the code to know what is inside of the result field in case of success. For me this is just another layer that hides things and it does not make clear what the operation returns if it succeeds.

Which to be fair, I think is a good point. Result Monads hide the result of computations until you need them. But I do think there is a better way, a way where we can still have a Result Monad that makes explicit the type of the result field.

Polymorphic types with dialyzer

I believe the solution to my problem could be in dialyzer's polymorphic types. To quote an article:

From Learn You Some Erlang When I said we could define a list of integers as [integer()] or list(integer()), those were polymorphic types. It's a type that accepts a type as an argument.

To make our queue accept only integers or cards, we could have defined its type as:

-type queue(Type) :: {fifo, list(Type), list(Type)}.
-export_type([queue/1]).

So now I know this is possible in erlang. If it is possible there, it should be possible in Elixir as well.

Error

So I have changed my code to the following:

@enforce_keys [:type]
  defstruct type: nil,
            result: nil,
            error_reason: nil,
            error_details: nil,
            input_parameters: []

  @type type :: :ok | :error
  @type result :: Type
  @type reason :: atom() | nil
  @type details :: any()
  @type params :: [any()]

  @type t(Type) :: %__MODULE__{
          type: type(),
          result: result(),
          error_reason: reason(),
          error_details: details(),
          input_parameters: params()
        }

  @spec ok :: BusyBee.Wrappers.FFmpeg.Result.t(nil)
  @spec ok :: __MODULE__.t()
  def ok, do: %__MODULE__{type: :ok}

  @spec ok(result()) :: __MODULE__.t(Type)
  def ok(result), do: %__MODULE__{type: :ok, result: result}

However this breaks. I don't know how to represent Polymorphic types in Elixir using dialyzer.

Question

How can I fix this code so my dialyzer knows Result is a polymorphic type?


Solution

  • In , variables are named starting with a capital letter (-type queue(Type) :: ….)

    %% ERLANG
    Foo = math:pow(10, 2).
    

    In , on the contrary, they are named starting with a lowercase. Capitalized names (Type) are reserved for special atoms, converted by compiler to :"Elixir.Type" atoms.

    # ELIXIR
    foo = :math.pow(10, 2)
    

    That said, the polymorphic type might be declared as

    @type foo(t) :: [t]
    

    Example from : https://github.com/elixir-ecto/ecto/blob/v3.6.2/lib/ecto/multi.ex#L124