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.