defmodule Hike.Option do
alias __MODULE__, as: Option
@moduledoc """
The `Hike.Option` module provides an implementation of the Optional data type.
It defines a struct `Option` with a single field `value` which can either be `@none nil`
or any other value of type `<T>`. This implementation provides functions to work with
Optional data including
* mapping,
* binding
* filtering,
* applying and many more functions to the value inside the Optional data.
## Example Usage
iex> option = %Hike.Option{value: 42}
%Hike.Option{value: 42}
iex> Hike.Option.map(option, &(&1 * 2))
%Hike.Option{value: 84}
iex> Hike.Option.filter(option, &(rem(&1, 2) == 0))
%Hike.Option{value: 42}
iex> Hike.Option.apply(option, &(&1 + 10))
%Hike.Option{value: 52}
```elixir
# Define a User struct
defmodule User do
@derive {Jason.Encoder, only: [:id,:age, :name]}
defstruct [:id, :age, :name]
end
defmodule TestHike do
# Import the Hike.Option module
import Hike.Option
# Simulating a database fetch function
@spec fetch_user(number) :: Hike.Option.option(%User{}) | Hike.Option.option()
# Simulating a database fetch function
def fetch_user(id) do
# Simulating a database query to fetch a user by ID
# Returns an Option<User> with some(user) if the user is found
# Returns an Option<User> with none() if the user is not found
case id do
1 -> some(%User{id: 1, age: 30, name: "Vineet Sharma"})
2 -> some(%User{id: 2, age: 20, name: "Jane Smith"})
_ -> none()
end
end
# Function to update the user's name to uppercase
# This function takes a user, a real data type, and returns an elevated data type Option
# if `%User{}` could transform successfully return `Option` in `some` state or else in `none` state.
def update_name_to_uppercase(user) do
case user.name do
nil -> none()
name -> %User{user | name: String.upcase(user.name)} |> some
end
end
# for above function another version could be like this where user take real data type %User{}, and return update %User{}
# def update_name_to_uppercase(user) do
# uppercase_name = String.upcase(user.name)
# %User{user | name: uppercase_name}
# end
# for this case map function will be used like it's been used for `increase_age_by1`
# Function to increase the user's age by one
# This function takes a user, a real data type, and returns a real data type user
def increase_age_by1(user) do
%User{user | age: user.age + 1}
end
# Function to print a user struct as a JSON-represented string
def print_user_as_json(user) do
Jason.encode!(user) |> IO.puts
end
# Example: Fetching a user from the database, updating the name, and matching the result
def test_user() do
user_id = 1
# 1. Expressiveness: Using Hike's Option type to handle optional values
fetch_user(user_id)
|> bind(&update_name_to_uppercase/1)
|> map(&increase_age_by1/1)
|> match(&print_user_as_json/1, fn -> IO.puts("User not found") end)
user_id = 3
# 2. Safer and more predictable code: Handling all possible cases explicitly
fetch_user(user_id)
|> bind(&update_name_to_uppercase/1)
|> map(&increase_age_by1/1)
|> match(&print_user_as_json/1, fn -> IO.puts("User not found") end)
end
end
```
### Output
```shell
iex> TestHike.test_user
# User ID: 1
{"id":1,"age":31,"name":"JOHN DOE"}
# User ID: 3
User not found
```
For more information on how to use this module, please see the documentation for
the individual functions.
"""
@typedoc """
generic input type `<T>`.
"""
@type t :: any()
@typedoc """
generic return type `<TR>`.
"""
@type tr :: any()
@typedoc """
`func()` represent a function which take no parameter and return value of type `<TR>`.
## Example
@spec whatever() :: atom
def whatever(), do: :ok
"""
@type func :: (() -> tr)
@typedoc """
`func(t)` represent a function which take a parameter of type `<T>` and return a value of type `<TR>`.
## Example
@spec add_one(number) :: number
def add_one(x), do: x + 1
"""
@type func(t) :: (t -> tr)
@typedoc """
`mapper()` represent a mapping function which take no parameter and return a value of type `<TR>`.
## Example
@spec whatever() :: atom
def whatever(), do: :ok
"""
@type mapper :: (() -> tr)
@typedoc """
`mapper(t)` represent a mapping function which take a parameter of type `<T>` and return a value of type `<TR>`.
## Example
@spec square(number) :: number
def square(x), do: x * x
"""
@type mapper(t) :: (t -> tr)
@typedoc """
`binder()` represent a binding(mapping) function which take no parameter and return an `Option` of type `<TR>`.
## Example
@spec whatever() :: Hike.Option.option(atom)
def whatever(), do: some(:ok)
@spec whatever() :: Hike.Option.option()
def whatever(), do: none()
"""
@type binder :: (() -> option(tr))
@typedoc """
`binder(t)` represent a binding(mapping) function which take a parameter of type `<T>` and return an `Option` of type `<TR>`.
## Example
@spec square(pos_number) :: Hike.Option.option(number)
def square(x) when x > 0, do: x * x |> some()
@spec square(number) :: Hike.Option.option()
def square(x) do
case x when x < 0 do
true -> none()
false -> square(x)
end
end
"""
@type binder(t) :: (t -> option(tr))
@none nil
@typedoc """
Elevated data type of Option struct that represents `None` state.
## Example
```elixir
%Hike.Option{value: nil, is_some?: false}
```
"""
@type option() :: %Option{value: nil}
@typedoc """
Elevated data type of Option struct that represents `Some` state and have a value of type `<T>`.
## Example
```elixir
%Hike.Option{value: 40, is_some?: true}
```
"""
@type option(t) :: %Option{value: t}
defstruct value: @none
## Apply function region start
@doc """
Applies a given function to the value of an `Option` struct and returns the result as a new `Option`.
## Examples
iex> option = %Option{value: 42}
iex> add_one = fn x -> x + 1 end
iex> Option.apply(option, add_one)
%Option{value: 43}
iex> none_option = %Option{value: nil}
iex> Option.apply(none_option, add_one)
%Option{value: nil}
iex> option = %Option{value: "hello"}
iex> upcase_string = fn str -> String.upcase(str) end
iex> Option.apply(option, upcase_string)
%Option{value: "HELLO"}
"""
@spec apply(option(t), func(t)) :: option(tr)
def apply(%Option{value: value} = _option, func)
when value != @none and is_function(func, 1),
do: func.(value) |> some
@spec apply(option(), func()) :: option()
def apply(%Option{value: @none} = opt, func) when is_function(func, 0), do: opt
## Apply function region end
## apply_async function region start
@doc """
async version on `apply` applies the `func` function to a value wrapped in a `Task` and returns a new task with the transformed value.
## Examples
iex> task = Task.async(fn -> Hike.Option.some(10) end)
...> mapped_task = apply_async(task, &(&1 * 2))
...> Task.await(mapped_task)
%Hike.Option{value: 20}
iex> task = Task.async(fn -> Hike.Option.none() end)
iex> mapped_task = apply_async(task, &(&1))
iex> Task.await(mapped_task)
%Hike.Option{value: nil}
"""
@spec apply_async(Task.t(), func(t())) :: Task.t()
def apply_async(wrapped_task, func) when is_function(func, 1) do
case Task.await(wrapped_task) do
%Hike.Option{value: value} = opt ->
Task.async(fn ->
if value == nil do
Option.none()
else
Option.apply(opt, func)
end
end)
end
end
## apply_async function region end
## bind function region start
@doc """
Transforms an `Option<T>` struct with a non-nil value, using a binder function that returns another `Option`
option in none state or `Option<TR>`option in some state.
** If the input `Option` has a `nil` value, returns a new `Option` struct with a `nil` value.
if you have a function that return `Option<TR>` and you want to apply mapping then use bind function to avoid double elevation
## Examples
iex> option = %Option{value: 42}
iex> add_one = fn x -> Option.some(x + 1) end
iex> Option.bind(option, add_one)
%Option{value: 43}
iex> Option.bind(Option.some("hello"), fn x -> Option.some(String.upcase(x)) end)
%Option{value: "HELLO"}
iex> Option.bind(Option.none() )
%Option{value: nil}
"""
@spec bind(option(t), binder(t)) :: option(tr)
def bind(%Option{value: value}, func)
when value != @none and is_function(func, 1) do
func.(value)
end
@spec bind(option(), binder()) :: option()
def bind(%Option{value: @none} = opt, func) when is_function(func, 0), do: opt
## bind function region end
## apply_async function region start
@doc """
async version on `bind` applies the `binder` function to a value wrapped in a `Task` and returns a new task with the transformed value.
## Examples
iex> task = Task.async(fn -> Hike.Option.some(10) end)
...> mapped_task = bind_async(task, fn x -> Hike.Option.some(x + 1) end)
...> Task.await(mapped_task)
%Hike.Option{value: 20}
iex> task = Task.async(fn -> Hike.Option.none() end)
iex> mapped_task = bind_async(task, fn x -> Hike.Option.some(x + 1) end)
iex> Task.await(mapped_task)
%Hike.Option{value: nil}
"""
@spec bind_async(Task.t(), binder(t())) :: Task.t()
def bind_async(wrapped_task, func) when is_function(func, 1) do
case Task.await(wrapped_task) do
%Hike.Option{value: value} = opt ->
Task.async(fn ->
if value == nil do
Option.none()
else
Option.bind(opt, func)
end
end)
end
end
## bind_async function region end
## filter function region start
@doc """
Applies the given function to the value of the provided option, returning a new option
containing the original value if the function returns a truthy value, otherwise returning
an empty option. If the provided option has no value, this function simply returns the
empty option.
## Examples
iex> Option.filter(%Option{value: 42}, fn x -> rem(x, 2) == 0 end)
%Option{value: 42}
iex> Option.filter(%Option{value: 42}, fn x -> rem(x, 2) == 1 end)
%Option{value: nil}
iex> Option.filter(%Option{value: nil}, fn x -> rem(x, 2) == 0 end)
%Option{value: nil}
iex> list_opt = Option.some([1,2,3])
iex> list_filter = fn (lst) -> Enum.count(lst) > 5 end
iex> Option.filter(list_opt, list_filter)
%Hike.Option{value: nil}
"""
@spec filter(option(), func()) :: option()
def filter(%Option{value: @none} = opt, func) when is_function(func, 0), do: opt
@spec filter(option(t), func(t)) :: option(tr)
def filter(%Option{value: value}, func) when value != @none and is_function(func, 1) do
if func.(value) do
%Option{value: value}
else
%Option{value: @none}
end
end
## filter function region end
## is_none function region start
@doc """
Returns `true` if the `Option` is in `None` state, otherwise `false`.
## Examples
iex> Option.is_none?(Option.none())
true
iex> Option.is_none?(Option.some("hello"))
false
"""
@spec is_none?(option() | option(t)) :: boolean()
def is_none?(%Option{value: value}), do: value == @none
## is_none function region end
## is_some function region start
@doc """
Returns `true` if the `Option` is in `Some` state, otherwise `false`.
## Examples
iex> Option.is_some?(Option.some("hello"))
true
iex> Option.is_some?(Option.none())
false
"""
@spec is_some?(option() | option(t)) :: boolean()
def is_some?(%Option{value: value}), do: value != @none
## is_some function region end
## map function region start
@doc """
Applies the given mapping function to the value inside the `Option` struct and returns a new `Option`
struct containing the transformed value. If the input `Option` struct is `@none`, the function
returns a new `Option` struct in none state.
## Examples
iex> Option.map(Option.some("hello"), fn x -> String.upcase(x) end)
%Option{value: "HELLO"}
iex> Option.map(Option.none(), fn x -> String.upcase(x) end)
%Option{value: nil}
"""
@spec map(option(t), mapper(t)) :: option(tr)
def map(%Option{value: value} = _option, func)
when value != @none and is_function(func, 1),
do: func.(value) |> some
@spec map(option(), mapper()) :: option()
def map(%Option{value: @none} = opt, func) when is_function(func, 0), do: opt
## map function region end
## map_async function region start
@doc """
async version on `map` Applies the `mapper` function to a value wrapped in a `Task` and returns a new task with the mapped value.
## Examples
iex> task = Task.async(fn -> Hike.Option.some(10) end)
...> mapped_task = map_async(task, &(&1 * 2))
...> Task.await(mapped_task)
%Hike.Option{value: 20}
iex> task = Task.async(fn -> Hike.Option.none() end)
iex> mapped_task = map_async(task, &(&1))
iex> Task.await(mapped_task)
%Hike.Option{value: nil}
"""
@spec map_async(Task.t(), mapper(t())) :: Task.t()
def map_async(wrapped_task, mapper) when is_function(mapper, 1) do
case Task.await(wrapped_task) do
%Hike.Option{value: value} = opt ->
Task.async(fn ->
if value == nil do
%Hike.Option{value: nil}
else
Option.map(opt, mapper)
end
end)
end
end
## map_async function region end
## match function region start
@doc """
Pattern matches on an `option` and returns the result of the respective matching function.
Calls `some_fun` with the value of the Option if the Option is in `some` state,
or calls `none_fun` if the Option is in `none` state.
## Examples
iex> import Hike.Option
# Match on `some` value with a matching function and on `none` value with a none-matching function
iex> match(some("hello"), &(&1 <> " world"), fn -> "no value" end)
# => "hello world"
# Match on `none` value with a none-matching function even if a matching function is provided
iex> match(none(), &(&1 * 2), fn -> "no value" end)
# => "no value"
iex> Option.match(Option.some("hello"), fn x -> String.upcase(x) end, fn -> "nothing" end )
"HELLO"
iex> Option.match(Option.none(), fn x -> String.upcase(x) end, fn -> "nothing" end)
"nothing"
"""
@spec match(option(t) | option(), func(t), func()) :: tr()
def match(%Option{value: value}, some_fun, none_fun)
when is_function(some_fun, 1) and is_function(none_fun, 0) do
case value != @none do
true -> some_fun.(value)
_ -> none_fun.()
end
end
def match(_, _, _) do
none()
end
## match function region end
## map_async function region start
@doc """
async version on `match` pattern matches on an `Option` value wrapped in a `Task` and returns
the result of the matching function.
## Examples
iex> task = Task.async(fn -> Hike.Option.some(10) end)
iex> mapped_task = map_async(task, &(&1 * 2))
iex> match_async(mapped_task , fn x -> "value is : " <> x <> "." end, fn ()-> "No Result Found." end)
"value is : 20."
iex> task = Task.async(fn -> Hike.Option.none() end)
iex> mapped_task = map_async(task, &(&1))
iex> match_async(mapped_task , fn x -> "value is : " <> x <> "." end, fn ()-> "No Result Found." end)
"No Result Found."
"""
@spec match(Task.t(), func(t), func()) :: tr()
def match_async(wrapped_task, some_fun, none_fun)
when is_function(some_fun, 1) and is_function(none_fun, 0) do
case Task.await(wrapped_task) do
%Hike.Option{} = opt ->
match(opt, some_fun, none_fun)
end
end
## match_async function region end
## some function region start
@doc """
Creates an `Option` in `Some` state
## Examples
iex> Option.some("hello")
%Option{value: "hello"}
"""
@spec some(t) :: option(t)
def some(value) when value != @none, do: %Option{value: value}
## some function region end
## none function region start
@doc """
Creates an `Option` in `None` state.
## Examples
iex> Option.none()
%Option{value: :nil}
"""
@spec none() :: option()
def none(), do: %Option{value: @none}
## none function region end
@doc """
convert `Option` into respective `Either`.
if `Option` is in `None` state `Either` will be return in `Left` state. and `Option` from `Some` state will convert to `Either` `Right` state.
## Example
iex> Hike.option(9) |> Hike.Option.to_either
%Hike.Either{l_value: nil, r_value: 9, is_left?: false}
iex> Hike.option() |> Hike.Option.to_either
%Hike.Either{l_value: :error, r_value: nil, is_left?: true}
"""
@spec to_either(Hike.Option.option()) :: Hike.Either.either_left(:error)
def to_either(%Option{value: @none}), do: Hike.Either.left(:error)
@spec to_either(Hike.Option.option(t())) :: Hike.Either.either_right()
def to_either(%Option{value: value}), do: Hike.Either.right(value)
@doc """
convert `Option` into respective `MayFail`.
if `Option` is in `None` state `MayFail` will be return in `Failure` state. and `Option` from `Some` state will convert to `MayFail` `Success` state.
## Example
iex(13)> Hike.option() |> Hike.Option.to_mayfail
%Hike.MayFail{failure: :error, success: nil, is_success?: false}
iex(14)> Hike.option(3) |> Hike.Option.to_mayfail
%Hike.MayFail{failure: nil, success: 3, is_success?: true}
"""
@spec to_mayfail(Hike.Option.option()) :: Hike.MayFail.mayfail_failure(:error)
def to_mayfail(%Option{value: @none}), do: Hike.MayFail.failure(:error)
@spec to_mayfail(Hike.Option.option(t())) :: Hike.MayFail.mayfail_success(t())
def to_mayfail(%Option{value: value}), do: Hike.MayFail.success(value)
end