Solution
is a library to help you with working with ok/error-tuples in case
and with
-expressions by exposing special matching macros, as well as some extra helper functions.
Installation
You can install Solution by adding solution
to your list of dependencies in mix.exs
:
def deps do
[
{:solution, "~> 0.2.0"}
]
end
Rationale
ok/error
tuples, which are also known by many other names some common ones being 'Tagged Status' tuples, 'OK tuples', 'Success Tuples', 'Result tuples', 'Elixir Maybes'.
Working with these types is however a bit complicated, since functions of different libraries (including different approaches in the Elixir standard library and the Erlang standard library) indicate a successful or failure result, in practice, in one of the following formats:
{:ok, val}
when everything went well{:error reason}
when there was a failure.:ok
, when everything went well but there is no useful return value to share.:error
, when there was a failure but there is no useful return value to share.{:ok, val, extra}
ends up being used by some libraries that want to return two things on success.{:error, val, extra}
ends up being used by some libraries that want to return two things on failure.- In general,
{:ok, ...}
or{:error, ...}
with more elements have seen some (albeit luckily limited) use.
Clearly, a simple pattern match does not cover all of these cases. This is where Solution
comes in:
- It defines clever guard macros that match either of these groups (
Solution.is_ok/1
,Solution.is_error/1
,Solution.is_okerror/1
) - It defines macros to be used inside special
case
andwith
statements that use these guards and are also able to bind variables:
For instance, you might use Solution.ok/0
to match any ok-type datatype, and Solution.error/0
to match any error-type datatype.
But they will also bind variables for you: So you can use ok(x)
to bind x = 42
regardless of whether {:ok, 42}
, {:ok, 42, "foo"}
or {:ok, 42, 3,1,4,1,5,9,2,6,5}
was passed. Higher-arity forms like ok(x, y, z)
also exist.
Examples
The following example snippets assume that you run
require Solution
import Solution
(although of course, Solution
is also fully usable without importing it.)
Guards
Solution
exposes three guard-safe functions: is_ok(x)
, is_error(x)
and is_okerror(x)
Solution.is_ok/1
will match:ok
,{:ok, _}
,{:ok, _, _}
,{:ok, _, _, __}
and any longer tuple whose first element is:ok
.Solution.is_error/1
will match:error
,{:error, _}
,{:error, _, _}
,{:error, _, _, __}
and any longer tuple whose first element is:error
.Solution.is_okerror/1
matches both of these.
Solution also exposes versions of these that take a 'minimum-length' as second argument. A length of 0
works jus the same as above versions. Longer lengths only match tuples that have at least that many elements (as well as starting with the appropriate tag).
SCase
Solution.scase/2
works like a normal case
-statement,
but will expand ok()
, error()
and okerror()
macros to the left side of ->
.
scase {:ok, 10} do
ok() -> "Yay!"
_ -> "Failure"
end
#=> "Yay!"
You can also pass arguments to ok()
, error()
or okerror()
which will then be bound and available
to be used inside the case expression:
scase {:ok, "foo", 42} do
ok(res, extra) ->
"result: \#{res}, extra: \#{extra}"
_ -> "Failure"
end
#=> "result: foo, extra: 42"
Note that for ok(...)
and error(...)
, the first argument will match the first element after the :ok
or :error
tag.
On the other hand, for okerror(...)
, the first argument will match the tag :ok
or :error
.
SWith
Solution.swith/2
works like a normal with
-statement,
but will expand ok()
, error()
and okerror()
macros to the left side of <-
.
x = {:ok, 10}
y = {:ok, 33}
swith ok(res) <- x,
ok(res2) <- y do
"We have: \#{res} \#{res2}"
else
_ -> "Failure"
end
#=> "We have: 10 33"
You can also pass arguments to ok()
, error()
or okerror()
which will then be bound and available
to be used inside the rest of the swith
-expression:
x = {:ok, 10}
y = {:error, 33}
z = {:ok, %{a: 42}}
swith ok(res) <- x,
error(res2) <- y,
okerror(tag, metamap) <- z,
%{a: val} = metamap do
"We have: \#{res} \#{res2} \#{tag} \#{val}"
else
_ -> "Failure"
end
#=> "We have: 10 33 ok 42"
Note that for ok(...)
and error(...)
, the first argument will match the first element after the :ok
or :error
tag.
On the other hand, for okerror(...)
, the first argument will match the tag :ok
or :error
.
Converting from Nillable types
When functions return a value that can never be the atom nil
, they often represent error by returning nil
.
Erlang also has its own equivalent of nil
that is used in many places in the Erlang standard library and other Erlang libraries: :undefined
.
Solution.from_nillable/1
can be used to convert these types into either {:ok, thing}
if thing
is nil
nor :undefined
, and into {:error, nil}
or {:error, :undefined}
otherwise.
Solution.Enum
The Solution.Enum
module contains helper functions to work with enumerables of ok/error tuples.
Solution.Enum.combine/1
Changes a list of oks into {:ok, list_of_values}
Solution.Enum.combine([{:ok, 1}, {:ok, "a", %{meta: "this will be dropped"}}, {:ok, :asdf}])
#=> {:ok, [1, "a", :asdf]}
Solution.Enum.combine([{:ok, 1}, {:ok, 2}, {:error, 3}])
#=> {:error, 3}
Solution.Enum.combine([{:ok, 1}, {:ok, 2}, {:error, 3, 4, 5}])
#=> {:error, 3, 4, 5}
Solution.Enum.oks/1
Returns a list of only all ok
-type elements in the enumerable.
Solution.Enum.oks([{:ok, 1}, {:error, 2}, {:ok, 3}])
#=> [{:ok, 1}, {:ok, 3}]
Similarly, there also exists errors/1
Solution.Enum.ok_vals/1
Returns a list of the values of all ok
-type elements in the enumerable.
Solution.Enum.ok_vals([{:ok, 1}, {:ok, 2,3,4,5}])
#=> [1, 2]
Similarly, there also exists Solution.Enum.error_vals/1
, as well as Solution.Enum.ok_valstuples/1
and Solution.Enum.error_valstuples/1
.
Documentation
Full documentation can be found at https://hexdocs.pm/solution.
Changelog
- 1.0.2 - A slight typographic fix in the documentation. Thank you, @dwmcc!
- 1.0.1 - Bugfix where certain macros were expanded in the incorrect calling context.
- 1.0.0 - Release of stable version. No API changes since last version.
- 0.2.1 - Proper handling of
:undefined
(likenil
). - 0.2.0 - the
Solution.Enum
module was added - 0.1.0 - Initial version