the alchemist's happy path with elixir
If you are using Elixir >= 1.2, you might prefer happy_with
Available in Hex, the package can be installed as:
- Add happy to your list of dependencies in
mix.exs
:
def deps do
[{:happy, "~> 1.3.1"}]
end
Ok, so I was just trying to find a nice way (beautiful syntax, yet flexible enough) to handle
errors in elixir. Handling :ok
/:error
like tuples without lots of if
/case
s.
After creating ok_jose, looking at Elixir's with special form and other alternatives, I wanted to create this tiny library with the following goals in mind:
- The happy path must be immediately obvious to the eyes.
- Code should not be cluttered and should just work using the elixir you already know.
- Avoid introducing noisy operators
~>>
, requiring commas after each pattern - Should provide a way to recover when not so happy moments come.
If you dont need special features like tags, and are using elixir ~> 1.2, checkout happy_with
wich is just tiny sugar around with
special form.
import Happy
The happy_path
macro takes a do
block and rewrites any first-level pattern matching expression into a case
.
happy_path do
{:ok, b} = a
{:ok, d} = b
c(d)
end
gets rewritten to something like:
case(a) do
{:ok, b} ->
case (b) do
{:ok, d} -> c(d)
end
end
Note that a variable pattern match (assignment) is not rewritten, as it will always match and would cause warnings.
happy_path do
x = some(thing) # simple assignment is left as is
end
If you want to handle non-matching values,
provide an else
block with additional
matching clauses:
happy_path do
{:ok, b} = a
c(b)
else
{:error, x} -> x
end
Sometimes you would want to share common error handling code on many happy_paths, for example in an api controller with many actions, all of which handle common invalid cases like parameter validation.
In those cases you can provide happy_path
with an
default error handler as first argument. Note that if no local
else clause matches, the error value is piped into
the provided error handler. Thus the handler is anything
you can pipe the error value into.
happy_path(else: handler) do
{:ok, x} = foo
x 1
else
{:error, y} -> y
end
gets rewritten to something like:
case foo do
{:ok, x} ->
x 1
{:error, y} ->
y
err ->
err |> handler
end
Just like with case
you can include guard tests.
happy_path do
x when not is_nil(x) = some(foo)
x 1
end
Tags is an special feature of happy_path
not found on
alternatives like elixir's with
expression.
Tags look like module attributes but they are not, they are just shorthand for tagging a pattern.
happy_path do
# using the `foo` tag
@foo {:ok, x} = y
# is exactly the same as
{:foo, {:ok, x}} = {:foo, y}
else
{:foo, {:error, e}} -> "Foo error"
end
Tags can help error handlers to get a clue about which
context the mismatch was produced on. It's mostly useful
for distingishing between lots of {:error, _}
like tuples.
The special tag @happy
lets you mark a pattern matching expression
to be skipped by happy_path
. For example when you know something
will always match.
happy_path do
@happy {this, would} = {"always", "match"}
end
produces just:
{this, would} = {"always", "match"}
happy_path do
%{valid?: true} = ch = User.changeset(params)
{:ok, user} = Repo.insert(ch)
render(conn, "user.json", user: user)
else
%{valid?: false} = ch -> render(conn, "validation_errors.json", ch: ch)
{:error, ch} -> render(conn, "db_error.json", ch: ch)
_ -> text(conn, "error")
end