Current section
Files
Jump to
Current section
Files
lib/bubblewrap.ex
defmodule Bubblewrap do
@moduledoc ~S"""
Bubblewrap implements two most common monadic data types:
* `Bubblewrap.Result` - container for a result of operation or error.
Result can be created using a constructor macro: `ok(value)` or `error(e)`,
where underlying structure is a tuple: `{:ok, value}` or `{:error, e}` respectively.
* `Bubblewrap.Option` - container for a value that might be present or missing.
Simple wrapper around a value that could be `nil`.
* `Bubblewrap` - collection of utility functions to work with both of these types.
## Result
Result type fits perfectly with idiomatic Erlang/Elixir return values.
When some library function returns either `{:ok, val}` or `{:error, err}`,
you can use functions provided by Bubblewrap right away. The most typical example,
where Bubblewrap shines, is a pipeline, where each operation can fail. Normally
this would be organized in a form of nested case expressions:
final = case op1(x) do
{:ok, res1} ->
case op2(res1) do
{:ok, res2} -> op3(res2)
{:error, e} -> {:error, e}
end
{:error, e} -> {:error, e}
end
With Bubblewrap you can do the same using `flat_map` operation:
final = op1(x) |> flat_map(&op2/1) |> flat_map(&op3/1)
Once any of the operations returns `error(e)`, following operations
are skipped and the error is returned. You can either do something
based on pattern matching or provide a fallback (can be a function or a default value).
case final do
ok(value) -> IO.puts(value)
error(e) -> IO.puts("Oh, no, the error occured!")
end
final |> fallback(ok("No problem, I got it"))
## Option
Option type wraps the value. If value is present, it's `value`,
if it's missing, `nil` is used instead. With Option type, you can use the
same set of functions, such as `map`, `flat_map`, etc.
find_user(id)
|> map(&find_posts_by_user/1)
This will only request for posts if the user was found.
See docs per Result and Option modules for details.
"""
import Bubblewrap.{Result}
alias Bubblewrap.{Option, Result}
@typep m(a, b) :: Option.t(a) | Result.t(a, b)
defmacro __using__ do
quote do
import Bubblewrap
import Bubblewrap.Option
import Bubblewrap.Result
alias Bubblewrap.Option
alias Bubblewrap.Result
end
end
@doc """
Transforms the content of monadic type.
Function is applied only if it's `ok` or not nil.
Otherwise value stays intact.
Example:
f = fn (x) ->
x * 2
end
5 |> map(f) == 10
nil |> map(f) == nil
"""
@spec map(m(a, b), (a -> c)) :: m(c, b) when a: any, b: any, c: any
def map(ok(x), f) when is_function(f, 1), do: ok(f.(x))
def map(error(m), f) when is_function(f, 1), do: error(m)
def map(nil, f) when is_function(f, 1), do: nil
def map(x, f) when is_function(f, 1), do: f.(x)
@doc """
Applies function that returns monadic type itself to the content
of the monadic type. This is useful in a chain of operations, where
argument to the next op has to be unwrapped to proceed.
Example:
inverse = fn (x) ->
if x == 0 do
nil
else
1/x
end
5 |> flat_map(f) == 1/5
0 |> flat_map(f) == nil
"""
@spec flat_map(m(a, b), (a -> m(c, b))) :: m(c, b) when a: any, b: any, c: any
def flat_map(ok(x), f) when is_function(f, 1), do: f.(x)
def flat_map(error(m), f) when is_function(f, 1), do: error(m)
def flat_map(nil, f) when is_function(f, 1), do: nil
def flat_map(x, f) when is_function(f, 1), do: f.(x)
@doc """
Performs a calculation with the content of monadic container and returns
the argument intact. Even though the convention says to return nothing (Unit)
this one passes value along for convenience — this way we can perform more
than one operation.
5
|> foreach(fn x -> IO.inspect(x) end)
|> foreach(fn x -> IO.inspect(2 * x) end)
This will print: 5 10
"""
@spec foreach(m(a, b), (a -> no_return)) :: m(a, b) when a: any, b: any
def foreach(ok(x) = res, f) when is_function(f, 1),
do:
(
f.(x)
res
)
def foreach(error(_) = z, _), do: z
def foreach(nil = z, _), do: z
def foreach(x = res, f) when is_function(f, 1),
do:
(
f.(x)
res
)
end