I am trying to up my Functional Programming (FP) skills and one of the things that newcomers first learn in FP is the Option
Type (aka, Maybe Monad).
This construct is present in many languages, Haskell has Maybe and Java and Python (yes, Python!) have Optional.
Basically this type models a value that may or may not be there.
Most FP languages have comprehensions, Scala and Elixir have the for
construct while Haskell has its famous do
notation.
In Scala and Haskell, these comprehensions work not only with Enumerables (such as Lists) but also with our Option
type (which is not an enumerable).
I mention this, because according to my understanding, Elixir's comprehensions only works on Enumerables. Furthermore, as far as I know, there is not Option
type datastructure in Elixir.
Elixir has tagged tuples in the form of {:ok, val}
or {:error, reason}
. Now while Elixir comprehensions can pattern match with tagged tuples:
iex> values = [good: 1, good: 2, bad: 3, good: 4]
iex> for {:good, n} <- values, do: n * n
[1, 4, 16]
It also ignores values that do not pattern match:
iex> values = [good: 1, good: 2, bad: 3, good: 4]
iex> for {:bananas, n} <- values, do: n * n
[]
However, this does not replicate the behaviour of the Option
type correctly. Following is an example in Scala:
for {
validName <- validateName(name)
validEnd <- validateEnd(end)
validStart <- validateStart(start, end)
} yield Event(validName, validStart, validEnd)
Having in mind this signatures:
def validateName(name: String): Option[String]
def validateEnd(end: Int): Option[Int]
def validateStart(start: Int, end: Int): Option[Int]
The result of the full comprehension expression, should any function return None
, will be None
.
With Elixir, the bad result would be ignored and the pipeline would simply continue happily ever after.
At this point I am thinking that implement this Option
type as a structure that implements the Enumerable
Protocol (so it can be used in Elixir comprehensions) is something that should be possible.
However, I am not sure I want to go down that route if I can simulate similar behavior using tuples.
So I have the following questions:
Option
type using tagged tuples inside Elixir comprehensions?After searching for all the functional libraries Elixir has on hex, at the time of this writing none matched my main requirement:
Some say Elixir comprehensions are not powerful enough for such cases. This is a falsifiable claim, so I decided to go ahead and try to falsify it.
Option.ex
Yes, the name is not inspiring. Originality has never been my forté. But what is this?
Simply put, this is an option type for elixir, aka, Option/Maybe monad. Yes, another one.
And just like what most people coming from languages like Scala/Haskell/Python have come to know, it has a couple of subtypes Some
and None
.
option.ex
defmodule Option do
@type t(elem) :: __MODULE__.Some.t(elem) | __MODULE__.None.t()
defmodule Some do
@type t(elem) :: %__MODULE__{val: elem}
defstruct [:val]
defimpl Collectable do
@impl Collectable
def into(option), do: {option, fn acc, _command -> {:done, acc} end}
end
defimpl Enumerable do
@impl Enumerable
def count(_some), do: {:ok, 1}
@impl Enumerable
def member?(some, element), do: {:ok, some.val == element}
@impl Enumerable
def reduce(some, acc, fun)
def reduce(_some, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(some, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(some, &1, fun)}
def reduce([], {:cont, acc}, _fun), do: {:done, acc}
def reduce(%Option.Some{} = some, {:cont, acc}, fun),
do: reduce([], fun.(some.val, acc), fun)
@impl Enumerable
def slice(_option), do: {:error, __MODULE__}
end
end
defmodule None do
@type t :: %__MODULE__{}
defstruct []
defimpl Collectable do
@impl Collectable
def into(option) do
{option,
fn
_acc, {:cont, val} ->
%Option.Some{val: val}
acc, :done ->
acc
_acc, :halt ->
:ok
end}
end
end
defimpl Enumerable do
@impl Enumerable
def count(_none), do: {:error, __MODULE__}
@impl Enumerable
def member?(_none, _element), do: {:error, __MODULE__}
@impl Enumerable
def reduce(none, acc, fun)
def reduce(_none, {:cont, acc}, _fun), do: {:done, acc}
def reduce(_none, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(none, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(none, &1, fun)}
@impl Enumerable
def slice(_option), do: {:error, __MODULE__}
end
end
@spec new(any) :: __MODULE__.Some.t(any)
def new(val), do: %__MODULE__.Some{val: val}
@spec new :: __MODULE__.None.t()
def new, do: %__MODULE__.None{}
end
This works with Elixir comprehensions, and it makes use of the fact that the Optional type is a Functor. This means its main requirement is being able to be mapped over. By converting an abstract container into specific implementation detail (like lists in Elixir) I was able to make it work.
The main purpose of this was to add an Option type to elixir to use with comprehensions. So a comparison to other languages is useful:
In Scala:
def parseShow(rawShow: String): Option[TvShow] = {
for {
name <- extractName(rawShow)
yearStart <- extractYearStart(rawShow)
yearEnd <- extractYearEnd(rawShow)
} yield TvShow(name, yearEnd, yearStart)
}
In Elixir:
@spec parse_show(String.t()) :: Option.t(TvShow.t())
def parse_show(raw_show) do
for name <- extract_name(raw_show),
year_start <- extract_year_start(raw_show),
year_end <- extract_year_end(raw_show),
into: Option.new() do
%TvShow{name: name, year_end: year_end, year_start: year_start}
end
end
You will see, these two pieces of code are basically identical, with the exception of the line into: Option.new()
, which is implicit in the Scala example. Elixir requires it to be explicit, which I personally prefer as well.
I could go on with examples from other languages, but they would all read basically the same. This is because comprehensions are basically the same in most FP languages.
But this doesn't answer the full original post ...
You can't use tagged tuples to achieve the same thing using comprehensions. This is impossible. However, if we discard comprehensions and focus on Elixir's other constructs, we can come a little bit closer.
Quoting another prominent member of Elixir's community, @OvermindDL1 :
Is it possible to simulate the
Option
type using tagged tuples inside Elixir comprehensions?Yes, or with
with
if you want an else, but you’ll want to make the tagged typed beok: value
anderror: reason
(which is closer to a result type, but it’s a limitation of elixir tuple lists in that they are always tuples). Traditionally{:ok, value}
and:error
is the “option” type in Elixir, where{:ok, value}
and{:error, reason}
is the “result” type in Elixir.
So, if you are coming from a different setting, from a functional language into Elixir, this post and my option.ex
is most certainly going to help you.
If however, you'd rather stay away from Mathematical Categories and other functional concepts like you want to stay away from the plague, with
statements with other elixir's constructs ought to serve you well enough.
One is not better than the other, they have different costs/benefits. It's up to you.