Option type compatible with comprehensions in Elixr?

Let’s assume I have an app that receives data in String format. This data can come from a DB, an API, anyplace.
Our job is to parse this data into a format our application can understand, TvShows:

defmodule TvShow do
  @enforce_keys [:name, :year_start, :year_end]
  defstruct [:name, :year_start, :year_end]

  @type t :: %__MODULE__{
          name: integer(),
          year_start: integer(),
          year_end: integer()
        }
end

Now, how we we parse this?
We need to parse the name, start year and end year. Here is an example of how a parse_show could look like:

defmodule SafeFunctionsThatReturnOptions do
  alias TvShow
  alias Option
  alias StringHelpers

  @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

  @spec extract_name(String.t()) :: Option.t(String.t())
  def extract_name(raw_show) do
    bracket_open_index = StringHelpers.index_of(raw_show, "(")

    if valid_index?(bracket_open_index) do
      raw_show
      |> String.slice(0..(bracket_open_index - 1))
      |> String.trim()
      |> Option.new()
    else
      Option.new()
    end
  end

  @spec extract_year_start(String.t()) :: Option.t(non_neg_integer)
  def extract_year_start(raw_show) do
    bracket_open = StringHelpers.index_of(raw_show, "(")
    dash = StringHelpers.index_of(raw_show, "-")

    for year_str <- parse_year_start(bracket_open, dash, raw_show),
        year <- StringHelpers.to_int_maybe(year_str),
        into: Option.new() do
      year
    end
  end

  @spec extract_year_end(String.t()) :: Option.t(non_neg_integer)
  def extract_year_end(raw_show) do
    dash = StringHelpers.index_of(raw_show, "-")
    bracket_close = StringHelpers.index_of(raw_show, ")")

    for year_str <- parse_year_end(dash, bracket_close, raw_show),
        year <- StringHelpers.to_int_maybe(year_str),
        into: Option.new() do
      year
    end
  end

  @spec parse_year_start(integer, integer, String.t()) :: Option.t(String.t())
  defp parse_year_start(bracket_open_index, dash_index, raw_show) do
    if valid_index?(bracket_open_index) and dash_index >= bracket_open_index + 1 do
      raw_show
      |> String.slice((bracket_open_index + 1)..(dash_index - 1))
      |> Option.new()
    else
      Option.new()
    end
  end

  @spec parse_year_end(integer, integer, String.t()) :: Option.t(String.t())
  defp parse_year_end(dash_index, bracket_end_index, raw_show) do
    if valid_index?(bracket_end_index) and dash_index <= bracket_end_index - 1 do
      raw_show
      |> String.slice((dash_index + 1)..(bracket_end_index - 1))
      |> Option.new()
    else
      Option.new()
    end
  end

  @spec valid_index?(integer) :: boolean
  defp valid_index?(index), do: index != -1
end

I also took the liberty of adding some helper modules:

StringHelpers:

defmodule StringHelpers do
  alias ListHelpers

  @spec index_of(String.t(), String.t()) :: integer
  def index_of(str, elem),
    do:
      str
      |> String.codepoints()
      |> ListHelpers.index_of(elem)

  @spec to_int_maybe(String.t()) :: Option.t(integer)
  def to_int_maybe(val) do
    case Integer.parse(val) do
      {num, ""} -> Option.new(num)
      _error -> Option.new()
    end
  end
end

And ListHelpers:

defmodule ListHelpers do
  @spec index_of([any], any) :: integer
  def index_of(list, elem), do: search(list, elem, 0)

  @spec search([any], any, integer) :: integer
  defp search([], _elem, _index), do: -1

  defp search([head | tail], elem, index) do
    if head == elem do
      index
    else
      search(tail, elem, index + 1)
    end
  end
end

I prepared these small examples to look like APIs from other functional languages. My StringHelpers is basically an adaption of something you could find in Java (imperative language mostly) and Scala.

This is how you would use it:

SafeFunctionsThatReturnOptions.parse_show("")
-> None

SafeFunctionsThatReturnOptions.parse_show("Game of Thrones (2000-2018)")
-> %TvShow{ ... }

No. This is not what the Option type is for. What you are describing is the Result/Error Monad, which is another construct from functional programming. You can however, take the principles applied to create the option.ex and use it to create a result.ex if you want.

1 Like

Your example is parsing and you are essentially hiding the parse errors. Is Option not the absence of something? Wouldn’t a Result be a better fit here?

What’s the advantage over using with or changesets in this context?

1 Like

Having Option.new be used both for success and error is non-intuitive. For readability just have constructors for each variant: Option.some(value) and Option.none().

You are trying very hard to use for here for no good reason IMO. Having a with statement with several parsing / validation steps that returns Option.none() should any of them fail and only returns Option.some(year) if they all succeed is more readable, I believe.

Not to mention that you could eliminate most of your hand-rolled parsing code if you use nimble_parsec. :person_shrugging:


I guess it all boils down to these questions: is the code supposed to be read by somebody else, ever? Is it only for your satisfaction and to exercise and gauge the validity of your idea? If so, then by all means, do continue. :smiley:

3 Likes

The Option type is not meant to tell you about errors, it is meant to discard them.
A Result/Error Monad would be a better fit if you want o know exactly what failed and why.

If you are confused as to what these types are supposed to be, I recommend a series:

This example was always only focused on the Option type. Discussing the Result type is out of scope as I don’t have an implementation to make comparisons.


@dimitarvp Your opinion in the API has some merit. However, I decided the follow the same model as Witchcraft does, as I find it very user friendly. I guess it comes down to personal taste. If you invoke Option.new with a value, you get Some and if not, you get None. Alternatively, you can use the structs themselves as returns Option.Some... or Option.None. You have the choice if you want to go down that route. I just figured that a Option.new was simple enough.

In regards to your personal opinion in relation to the validity of this approach, we can have this discussion in another place (if you insist on having. I personally don’t).

Please note some posts have been edited/removed to clean up the thread following OP’s request to remove the contentious part of their post.

4 Likes