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 theresult
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()]
orlist(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?