-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add new syntax, fix credo #18
base: master
Are you sure you want to change the base?
Changes from all commits
501cb15
baa671c
e895f17
5dfd0c3
791e028
dedf98f
7fe96b5
7918a48
b7e8bdc
3b0caa5
fedd89d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -277,6 +277,69 @@ end | |
#=> "error message" | ||
``` | ||
|
||
### [Block](https://hexdocs.pm/exceptional/Exceptional.Block.html) | ||
|
||
Kind of a combination of Elixir's normal | ||
[`with`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1) | ||
special form in addition to a monad-style `do` pipeline. | ||
|
||
This automatically-wraps every return value with | ||
[`normalize`](https://hexdocs.pm/exceptional/Exceptional.Normalize.html). | ||
|
||
```elixir | ||
block do | ||
a <- {:ok, 2} | ||
b = a * 2 | ||
c <- {:ok, b * 2} | ||
c * 2 | ||
end | ||
#=> 16 | ||
|
||
block do | ||
a <- {:ok, 2} | ||
b = a * 2 | ||
_ = 42 | ||
c <- {:error, "Failed: #{b}"} | ||
c * 2 | ||
end | ||
#=> %ErlangError{original: "Failed: 4"} | ||
|
||
conversion_fun = fn | ||
{:blah, reason} -> %ErlangError{original: "Blah: #{reason}"} | ||
e -> e | ||
end | ||
block conversion_fun: conversion_fun do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I use the conversion function on occasion so I needed it exposed in at least the base method here. ^.^; |
||
a <- {:ok, 2} | ||
b = a * 2 | ||
_ = 42 | ||
c <- {:blah, "Failed: #{b}"} | ||
c * 2 | ||
else | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 Hmm, I wonder if there's a more descriptive word than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well we can only use an Elixir block keyword if we want it block level here, which I really think we should for consistency with the rest of the language, so |
||
%ErlangError{original: "Blah: "<>_} = exc -> exc | ||
_ -> {:error, "unknown error"} | ||
end | ||
#=> %ErlangError{original: "Blah: Failed: 4"} | ||
|
||
block! do | ||
a <- {:ok, 2} | ||
b = a * 2 | ||
_ = 42 | ||
c <- {:error, "Failed: #{b}"} | ||
c * 2 | ||
end | ||
#=> ** (ErlangError) Erlang error: "Failed: 4" | ||
|
||
# Early return: | ||
|
||
block do | ||
a <- {:ok, 2} | ||
b = a * 2 | ||
:wrong <- b * 2 # Returning 8 here due to wrong match | ||
b * 4 | ||
end | ||
8 | ||
``` | ||
|
||
## Related Packages | ||
|
||
- [Phoenix/Exceptional](https://hex.pm/packages/phoenix_exceptional) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
defmodule Exceptional.Block do | ||
@moduledoc ~S""" | ||
Convenience functions to wrap a block of calls similar to `with`. | ||
|
||
## Convenience `use`s | ||
|
||
Everything: | ||
|
||
use Exceptional.Block | ||
|
||
""" | ||
|
||
defmacro __using__(_) do | ||
quote do | ||
import unquote(__MODULE__) | ||
end | ||
end | ||
|
||
@doc ~S""" | ||
This specifies a block that is tested as normal similar to Elixir's `with`. | ||
|
||
This will auto-normalize every return value, so expect a raw `value` return, | ||
not something like `{:ok, value}` when using `<-`. `=` is unwrapped and | ||
unhandled. | ||
|
||
This requires a `do` body, last expression is final returned value. | ||
- Inside the `do` body it accepts `matcher <- expression` and will early | ||
return if it is a bad match or the expression returns an error. | ||
- Values will be unwrapped as in `normalize` | ||
|
||
This accepts an `else` body, which takes cases to handle error conditions | ||
and transform them as necessary. | ||
|
||
This takes a `conversion_fun: some_fun` optional argument to pass in to the | ||
`normalize` call to transform normalization errors into custom values and/or | ||
errors. | ||
|
||
## Examples | ||
|
||
iex> use Exceptional.Block | ||
...> block do | ||
...> _ <- {:ok, 2} | ||
...> end | ||
2 | ||
|
||
iex> use Exceptional.Block | ||
...> block do | ||
...> a <- {:ok, 2} | ||
...> b = a * 2 | ||
...> c <- {:ok, b * 2} | ||
...> c * 2 | ||
...> end | ||
16 | ||
|
||
iex> use Exceptional.Block | ||
...> block do | ||
...> a <- {:ok, 2} | ||
...> b = a * 2 | ||
...> 8 <- b * 2 # Match's supported on the values | ||
...> b * 4 | ||
...> end | ||
16 | ||
|
||
iex> use Exceptional.Block | ||
...> block do | ||
...> a <- {:ok, 2} | ||
...> b = a * 2 | ||
...> :wrong <- b * 2 # Returning 8 here due to wrong match | ||
...> b * 4 | ||
...> end | ||
8 | ||
|
||
iex> use Exceptional.Block | ||
...> block do | ||
...> a <- {:ok, 2} | ||
...> b = a * 2 | ||
...> :wrong = b * 2 # Match Exception is raised here | ||
...> b * 4 | ||
...> end | ||
** (MatchError) no match of right hand side value: 8 | ||
|
||
iex> use Exceptional.Block | ||
...> block do | ||
...> a <- {:ok, 2} | ||
...> b = a * 2 | ||
...> _ = 42 | ||
...> c <- {:error, "Failed: #{b}"} | ||
...> c * 2 | ||
...> end | ||
%ErlangError{original: "Failed: 4"} | ||
|
||
iex> use Exceptional.Block | ||
...> block do | ||
...> a <- {:ok, 2} | ||
...> b = a * 2 | ||
...> _ = 42 | ||
...> c <- {:error, "Failed: #{b}"} | ||
...> c * 2 | ||
...> else | ||
...> _ -> {:error, "unknown error"} | ||
...> end | ||
%ErlangError{original: "unknown error"} | ||
|
||
iex> use Exceptional.Block | ||
...> conversion_fun = fn | ||
...> {:blah, reason} -> %ErlangError{original: "Blah: #{reason}"} | ||
...> e -> e | ||
...> end | ||
...> block conversion_fun: conversion_fun do | ||
...> a <- {:ok, 2} | ||
...> b = a * 2 | ||
...> _ = 42 | ||
...> c <- {:blah, "Failed: #{b}"} | ||
...> c * 2 | ||
...> else | ||
...> %ErlangError{original: "Blah: "<>_} = exc -> exc | ||
...> _ -> {:error, "unknown error"} | ||
...> end | ||
%ErlangError{original: "Blah: Failed: 4"} | ||
|
||
""" | ||
defmacro block(opts, bodies \\ []) do | ||
opts = bodies ++ opts | ||
gen_block(opts) | ||
end | ||
|
||
@doc ~S""" | ||
The auto-throwing version of `block`, will raise it's final error. | ||
|
||
## Examples | ||
|
||
iex> use Exceptional.Block | ||
...> block! do | ||
...> a <- {:ok, 2} | ||
...> b = a * 2 | ||
...> _ = 42 | ||
...> c <- {:error, "Failed: #{b}"} | ||
...> c * 2 | ||
...> end | ||
** (ErlangError) Erlang error: "Failed: 4" | ||
|
||
""" | ||
defmacro block!(opts, bodies \\ []) do | ||
opts = bodies ++ opts | ||
body = gen_block(opts) | ||
quote do | ||
# credo:disable-for-lines:1 Credo.Check.Design.AliasUsage | ||
Exceptional.Raise.ensure!(unquote(body)) | ||
end | ||
end | ||
|
||
defp gen_block(opts) do | ||
{:__block__, _meta, do_body} = wrap_block(opts[:do] || throw "Must specify a `do` body clause with at least one expression!") | ||
conversion_fun_ast = | ||
case opts[:conversion_fun] do | ||
nil -> quote do fn x -> x end end | ||
call -> call | ||
end | ||
conversion_fun = gen_unique_var("$conversion_fun") | ||
else_fun_ast = | ||
case opts[:else] do | ||
nil -> quote do fn x -> x end end | ||
clauses -> | ||
# credo:disable-for-lines:7 /Alias|Nesting/ | ||
quote do | ||
fn x -> | ||
x | ||
|> case do | ||
unquote(clauses) | ||
end | ||
|> Exceptional.Normalize.normalize(unquote(conversion_fun)) | ||
end | ||
end | ||
end | ||
else_fun = gen_unique_var("$else_fun") | ||
body = gen_block_body(do_body, conversion_fun, else_fun) | ||
quote generated: true do | ||
unquote(conversion_fun) = unquote(conversion_fun_ast) | ||
unquote(else_fun) = unquote(else_fun_ast) | ||
unquote(body) | ||
end | ||
end | ||
|
||
defp wrap_block({:__block__, _, _} = ast), do: ast | ||
defp wrap_block(ast), do: {:__block__, [], [ast]} | ||
|
||
defp gen_block_body(exprs, conversion_fun, else_fun) | ||
defp gen_block_body([{:<-, meta, [binding, bound]} | exprs], conversion_fun, else_fun) do | ||
value = Macro.var(:"$val", __MODULE__) | ||
next = | ||
case exprs do | ||
[] -> value | ||
_ -> gen_block_body(exprs, conversion_fun, else_fun) | ||
end | ||
{call, gen_meta, args} = | ||
quote generated: true do | ||
# credo:disable-for-lines:1 Credo.Check.Design.AliasUsage | ||
case Exceptional.Normalize.normalize(unquote(bound), unquote(conversion_fun)) do | ||
%{__exception__: _} = unquote(value) -> unquote(else_fun).(unquote(value)) | ||
unquote(binding) = unquote(value) -> unquote(next) | ||
unquote(value) -> unquote(else_fun).(unquote(value)) | ||
end | ||
end | ||
{call, meta ++ gen_meta, args} | ||
end | ||
defp gen_block_body([expr | exprs], conversion_fun, else_fun) do | ||
value = gen_unique_var(:"$val") | ||
next = | ||
case exprs do | ||
[] -> value | ||
_ -> gen_block_body(exprs, conversion_fun, else_fun) | ||
end | ||
quote generated: true do | ||
unquote(value) = unquote(expr) | ||
unquote(next) | ||
end | ||
end | ||
defp gen_block_body(exprs, _conversion_fun, _else_fun) do | ||
throw {:UNHANDLED_EXPRS, exprs} | ||
end | ||
|
||
defp gen_unique_var(name) do | ||
id = Process.get(__MODULE__, 0) | ||
Process.put(__MODULE__, id + 1) | ||
name = | ||
if id === 0 do | ||
String.to_atom(name) | ||
else | ||
String.to_atom("#{name}_#{id}") | ||
end | ||
Macro.var(name, __MODULE__) | ||
end | ||
|
||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a more descriptive term than
block
?handleable
?protected
?errorable
? Block is very general and could mean anything when reading through the code.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was actually paralyzed via Decision Paralysis for a long while so I chose the most simple and descriptive name, even if it is overly generic, and left it up to you if you wanted it changed. ^.^;
I did a search through a variety of libraries though and found it an unused function/macro name surprisingly, so it seemed worth grabbing. :-)
I'd personally think
block
is overall fine though, it is a generic block that can be enhanced later with far more functionality if it is deemed useful, and renaming it later or having 20 slightly differently named variants for different purposes seems excessive to me. ^.^;I can rename it though if you wish?