Fl4m3Ph03n1x

Fl4m3Ph03n1x

Option type compatible with comprehensions in Elixr?

Background

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).

Option what?

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.

How it all comes down to Elixir

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.

What does Elixir have?

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.

Questions

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:

  1. Is it possible to simulate the Option type using tagged tuples inside Elixir comprehensions?
  2. Are there any Elixir libraries in the wild that have Monadic types (like the one we saw here) usable within Elixir comprehensions? (I know about witchcraft but they have their own construct for comprehensions, which for the time being, I think is a little overkill. I am interesting in something that works with Elixir’s native comprehension functionality).

Marked As Solved

Fl4m3Ph03n1x

Fl4m3Ph03n1x

Answer

After searching for all the functional libraries Elixir has on hex, at the time of this writing none matched my main requirement:

  • Being usable with Elixir comprehensions.

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.

Say Hi to 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.

How can I use it?

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 …

What about an Elixir equivalent in tagged tuples?

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 be ok: value and error: 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. Difference is that now, you have a choice.

Also Liked

Fl4m3Ph03n1x

Fl4m3Ph03n1x

  1. Incredible insight! However, comprehensions are still more expressive than the with statement as they allow combination between generators. This is the main reason I am trying to learn comprehensions. with statements execute 1 function at a time, while comprehension are like nested flat_maps. They overlap in some cases, but comprehensions are extensible. I just need to understand how to harness that.
  2. I am no stranger to witchcraft, however, after many tries, I can’t get the easiest example going. It also doesn’t help that I need to use their own macros and that their types are not compatible with Elixir’s comprehension. But you claim otherwise, right? Could you please, please, please provide an example of witchcraft’s option type with Elixir’s native comprehension? (if that is possible)
OvermindDL1

OvermindDL1

  1. Yes, or with with if you want an else, but you’ll want to make the tagged typed be ok: value and error: 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.

  2. Oh yes, quite a lot, I’m personally a fan of expede’s libraries if you want the full haskell/scala like interfaces. There is also another library made by the same author (not at that link) that is more simple and just works with the usual elixir/erlang forms as well.

OvermindDL1

OvermindDL1

Witchcraft uses standard names and such to access things, they don’t interact with the language comprehensions, individual types might, but as a library it doesn’t as the language comprehensions aren’t powerful enough.

Where Next?

Popular Backend topics Top

chasekaylee
Hi there everyone! Recently, I have fallen in love with programming with Elixir and have been having so much fun with it. I have been do...
New
Fl4m3Ph03n1x
Background I am trying to encode a structure into json format using the Jason library. However, this is not working as expected. Code L...
New
Fl4m3Ph03n1x
Background I am trying to find a cheap and easy way to create New Types in Elixir, and Records seem to be just what I would need. Probl...
New
jeya
Dear Geeks I am new to pytest. I am following a youtube channel. I am writing the same code. learning to test login functionality of an...
New
JimmyCarterSon
I am confused about the Schema setup, I am setting up a new application and I want to seed files in it as well. I tried to mix to create...
New
Fl4m3Ph03n1x
Background I am a fan of dialyzer and friends (looking at Gradient) and I try to have sepcs in my code as much as I can. To this end, I a...
New
sona11
I studied very basic PHP (I believe). After that, I feel like I’ve gotten a handle on the language. My dream is to work as a web develope...
New
pillaiindu
Currently reading the book “Programming Phoenix LiveView”. At the end of the Chapter 1, I’m trying to solve the guess game. If the user ...
New
Fl4m3Ph03n1x
Background I have a release file inside a tarball. However I want the final release to have some additional files and to move things aro...
New
AstonJ
If you’re getting errors like this: psql: error: connection to server on socket “/tmp/.s.PGSQL.5432” failed: No such file or directory ...
New

Other popular topics Top

AstonJ
If it’s a mechanical keyboard, which switches do you have? Would you recommend it? Why? What will your next keyboard be? Pics always w...
New
malloryerik
Any thoughts on Svelte? Svelte is a radical new approach to building user interfaces. Whereas traditional frameworks like React and Vue...
New
wolf4earth
@AstonJ prompted me to open this topic after I mentioned in the lockdown thread how I started to do a lot more for my fitness. https://f...
New
PragmaticBookshelf
A PragProg Hero’s Journey with Brian P. Hogan @bphogan Have you ever worried that your only legacy will be in the form of legacy...
New
AstonJ
Curious to know which languages and frameworks you’re all thinking about learning next :upside_down_face: Perhaps if there’s enough peop...
New
AstonJ
You might be thinking we should just ask who’s not using VSCode :joy: however there are some new additions in the space that might give V...
New
AstonJ
This looks like a stunning keycap set :orange_heart: A LEGENDARY KEYBOARD LIVES ON When you bought an Apple Macintosh computer in the e...
New
Exadra37
I am asking for any distro that only has the bare-bones to be able to get a shell in the server and then just install the packages as we ...
New
foxtrottwist
A few weeks ago I started using Warp a terminal written in rust. Though in it’s current state of development there are a few caveats (tab...
New
DevotionGeo
I have always used antique keyboards like Cherry MX 1800 or Cherry MX 8100 and almost always have modified the switches in some way, like...
New