Solution has had quite some time during which it seems like the interface as it currently stands works well.
Therefore, a stable 1.0 version will probably be released soon .
Solution has had quite some time during which it seems like the interface as it currently stands works well.
Therefore, a stable 1.0 version will probably be released soon .
Stable version 1.0.0 has now been released!
There are no API changes, although there have been a few cleanups to the README and the documentation.
Did you try to look at hex.pm before you wrote your library? It seems to me that your lib has a lot of common with a few others, e.g. result or ok.
An excellent question. I most certainly did!
Solution
is meant to be more general and more lightweight/idiomatic in its approach.
Let’s compare it with the six other commonly mentioned libraries that work with ok/error tuples:
ok
library::ok
or :error
, the second being a value). This means that plain :ok
or :error
are not handled, nor are results like {:ok, value, meta}
which are relatively common in production Elixir (c.f. Ecto Multi).OK.for
which sort of takes the place of Elixir’s with
but not completely (I think?), which is also subject to above caveat.OK.try
which is sort of like OK.for
but wraps it in an extra try/rescue block (I think?).Solution’s swith
and scase
statements on the other hand work 1:1 like the built-in counterparts, except that you can add the ok(...)
and error(...)
macros at the LHS of the matches to match any ok/error tuple that has at least the required length. I believe that the library therefore has much less of a learning curve and much less mental overhead.
result
library:case
or with
statements.{:ok, value}
/{:error, problem}
.exceptional
library:Solution
, so it does not have this functionality.towel
library:use Towel
).:ok
/:error nor for
{:ok, multiple, things}`.ok_jose
library:|>
to work differently for ok/error tuples.|>
.:ok
/:error nor for
{:ok, multiple, things}`.with
was available in Elixir.So the Tl;Dr is that Solution has a slightly different approach to these libraries:
:ok
and :error
, as well as the versions with more than two elements like {:ok, data, metadata}
.case
and with
syntax, rather than adding variants of the pipe-operator and/or require an understanding of monads to be used successfully.I hope that answers your question!
Yes it does, thank you for the comprehensive answer. I think, it would be useful if you added your reply to library repo.
I only have an experience with a result library. Therefore I can’t speak to the rest libs of the list which you’ve mentioned.
Result only accepts exactly the format {:ok, value}/{:error, problem}
That’s right. You can catch situations where we have :ok/:error
by pattern matching. For example:
def do_something(:ok) do
Result.ok("result is ok")
end
def do_something(_) do
Result.error("result is error")
end
And these functions you can use further in your pipes with combination Result.map, Result.and_then
, etc… For {:ok, data, metadata}
is appropriate to put data, metadata
into e.g. Map and then use Result
functions again in pipes.
Result has a bunch of monad-inspired functions, but no overloaded case
or with
statements.
Because Result
doesn’t need it. Check following examples how Result
is used for these cases. For case
: functions save and below write/close
, for with
: fold
Great idea
It seems to me like you are attempting to ‘defend’ the result
library. I would like to clarify that my intention with the comparison was not to attack the other libraries, but only to point out what the differences between them are.
I think it is great that a diverse number of libraries that handle dealing with failure circumstances in different ways. I personally like the way Solution does it the best (for failure circumstances related to ok/error tuples) because I created it myself so it matches my needs perfectly (No pun intended ).
Other people might like other libraries more, and other situations might call for other libraries. And that’s great!
Any defend wasn’t and isn’t my purpose (and also I didn’t perceive that the comparison would have be attack) and I apology if it would sound this way. I just wanted to point out how to Result
can be used.
Yes I agree with that.
Huh?
iex> {:error, :an_error, :meta} |> normalize()
%ErlangError{original: :an_error}
It handles them fine, there’s just no place to put the ‘meta’ field into the ErlangError struct (this is an :"Elixir.ErlangError"
struct, not an exceptional thing).
Ok tuples with extra fields are passed through unchanged, as otherwise it wouldn’t know how to process the extra data or in what form you want it, these are by design.
However, if you do know how you want something like {:ok, :a_value, :meta}
to be processed, say as a map, you can pass in a transformation function to the normalize
call (great for handling lots of possibilities to normalize them all!).
So I’m not really sure what this is referencing for exceptional, it handles all of these variants both with a default style handling and a way to override as well?
Never seen towel
, lol. ^.^
I tend to use exceptional, mostly because I have a lot of stuff built around it anymore and its pervasive through my work project. It would be interesting to see handling differences between Solution
and Exceptional
, if you can think of both basic and unique cases that Solution
handles well? Would be a good comparison for people to read.
A quick conversion of the cases of Solution to Exceptional from the OP would be:
scase {:ok, 10} do
ok() -> "Yay!"
_ -> "Failure"
end
#=> "Yay!"
Exceptional branch (not my style):
iex> branch {:ok, 10},
...> value_do: fn _ -> "Yay!" end.(),
...> exception_do: fn _ -> "Failure" end.()
"Yay!"
My wrapper('ish) around branch
that I use at work (I have a lot of things wrapping exceptional, I could easily release it as a separate library now ^.^; ):
iex(6)> xbranch {:ok, 10} do
...(6)> _ -> "Yay!"
...(6)> else
...(6)> _ -> "Failure"
...(6)> end
"Yay!"
Exceptional if_exception
style, exceptional pipes into things for these and it’s normal branch blocks, I prefer the above style of whatever_matcher -> ...
personally, so I have a lot of wrappers around these for that, but this is exceptional’s normal one too, not really a fan of the names either…:
iex(7)> if_exception {:ok, 10} do
...(7)> case do
...(7)> _ -> "Failure"
...(7)> end
...(7)> else
...(7)> case do
...(7)> _ -> "Yay!"
...(7)> end
...(7)> end
"Yay!"
Exceptional as a tagged tuple:
iex(3)> case to_tagged_status({:ok, 10}) do
...(3)> {:ok, _} -> "Yay!"
...(3)> _ -> "Failure"
...(3)> end
"Yay!"
Solution’s:
scase {:ok, "foo", 42} do
ok(res, extra) ->
"result: \#{res}, extra: \#{extra}"
_ ->
"Failure"
end
#=> "result: foo, extra: 42"
Exceptional in my project:
iex(6)> branch {:ok, "foo", 42} do
...(6)> {:ok, res, extra} -> "result: #{res}, extra: #{extra}"
...(6)> else
...(6)> _ -> "Failure"
...(6)> end
"result: foo, extra: 42"
Though with a normalize helper:
iex> h = fn
...> {:ok, value, meta} -> {value, meta}
...> otherwise -> otherwise
...> end
#Function<6.128620087/1 in :erl_eval.expr/5>
iex> {:ok, "foo", 42} |>
...> normalize(h) |>
...> branch do
...> {res, extra} -> "result: #{res}, extra: #{extra}
...> else
...> _ -> "Failure"
...> end
"result: foo, extra: 42"
x = {:ok, 10}
y = {:ok, 33, 44, %{a: "other stuff"}}
swith ok(res) <- x,
ok(res2) <- y do
"We have: \#{res} \#{res2}"
else
_ -> "Failure"
end
#=> "We have: 10 33"
Eh, not exceptional, but one of my helpers:
iex> x = {:ok, 10}
{:ok, 10}
iex> y = {:ok, 33, 44, %{a: "other stuff"}}
{:ok, 33, 44, %{a: "other stuff"}}
iex> {x, y} |>
...> xnormalize() |> # This 'essentially' just recursively runs `normalize` and returns first failure if found
...> xbranch do # I could just write a transformer to pass to `xnormalize`
...> {res, {:ok, res2, _, _} -> "We have: #{res} #{res2}
...> else
...> _ -> "Failure"
...> end
"We have: 10 33"
Now I do like Solution
a lot better, it’s more composable and such, though exceptional would work very well with it via to_tagged_tuple
.
Optimally :ok
/{:ok, data}
and :error
/{:error, data}
would be used everywhere, there’s very little consistency in the ecosystem around this, Solution
would work very well with all this!
Apologies; I did not attempt to execute the code but only read the implementation of Exceptional.Normalize.normalize/{1,2,3}, and mis-read line 80, thinking that it was still part of the case-statement body that starts at line 78.
So, after experimentation, I now know that:
{:error, problem, trace}
tuple.{:error, problem, explanation, trace}
and as you mentioned {:ok, value, meta}
are not handled by the library (although you can indeed pass in a custom function to handle them).Interestingly, for instance the new Mint HTTP client library frequently (a) uses more than two elements in its return values (because, amongst other things, it has to pass the altered conn
object back to you) and (b) uses return types like {:error, conn, reason}
and {:error, conn, reason, [responses]}
in some places, which would not be handled by Exceptionals method of operation either. (For these two examples it would create an %ErlangError{original: conn}
)
Explicit pattern matching is a solution, but I would argue that you have more freedom if, for these ok/error tuples you match on the fields you need rather than also restricting the size of the tuple.
Please do! (and thanks for all your comparison examples by the way). It looks interesting, and I think that having more different flavours of error-handling libraries would help people.
Yes, facilitating this is exactly the goal of the library .
Does Solution have the equivalent of OK’s pipe operators (~> and ~>>)?
I’m looking for something that simply returns out of a pipeline as soon as a function in the pipeline returns {:error, …}
I don’t like nested case (ugly) and I’m not a fan of with (not as neat as pipe).
No, Solution does not overload any operators.
The choice was made to keep in line with Elixir’s own decision to use with
statements as a more explicit alternative to ‘short-circuiting pipes’. The only change that Solution does w.r.t. the built-in versions of Elixir’s with
(and case
) is that the caller of a function does no longer have to care how many elements the returned tuple has, but only if it is an ok
or an error
.
If you really want short-circuiting pipe operators, I’d suggest either you look at this post earlier in this topic for a comparison of the various libraries that have making ok/error tuples nicer to work with as goal. Many of them do either overload the pipe operator, introduce a new operator, or introduce a ‘monadic do’-style syntax.
EDIT: That said, it also is possible to define your own custom pipe-like operator on top of the is_ok
/is_error
guards that Solution provides (which, as mentioned, let you match any of:ok
, {:ok, val}
, {:ok, val, meta}
, {:ok, val, meta, other}
, etc and similarly for is_error
). Let me conjure up a code snippet for you…
Here you are
defmodule ShortCircuitingPipe do
require Solution
@doc """
A pipeline-like operator wrapping `is_ok/1`,
so it will short-circuit as soon as a value that is not one of:
- `{:ok, val}`
- `{:ok, val, meta}`
- a tuple with more elements whose first element is `:ok`
is passed.
iex> require ShortCircuitingPipe
iex> import ShortCircuitingPipe
iex> {:ok, %{a: 42}} ~> Map.fetch(:a) ~> fn x -> {:ok, x * 2} end.()
{:ok, 84}
iex> require ShortCircuitingPipe
iex> import ShortCircuitingPipe
iex> {:error, :boom} ~> Map.fetch(:a) ~> fn x -> {:ok, x * 2} end.()
{:error, :boom}
iex> require ShortCircuitingPipe
iex> import ShortCircuitingPipe
iex> {:ok, %{a: 42}} ~> Map.fetch(:unexistent) ~> fn x -> {:ok, x * 2} end.()
:error
Note that only `val` (the element at index `1` in the tuple)
will be passed on to the next stage of the pipeline:
iex> require ShortCircuitingPipe
iex> import ShortCircuitingPipe
iex> {:ok, "I", "have", "many", "elements"} ~> fn(x) -> [x, x] end.()
["I", "I"]
Also note that just passing `:ok` will not be matched in this case,
since there is no `value` to pass on to the next stage of the pipeline.
iex> require ShortCircuitingPipe
iex> import ShortCircuitingPipe
iex> :ok ~> fn (x) -> [x, x] end.()
:ok
"""
defmacro lhs ~> {call, line, args} do
# Prepend a variable that will be called 'value' to the list of arguments,
# to be used in the happy path.
value = quote do: value
prepended_args = [value | args || []]
quote do
Solution.scase unquote(lhs) do
ok(value) -> unquote({call, line, prepended_args})
other -> other
end
end
end
end
Hi there
Thanks for the response, and the code you supplied.
I am rather new to Elixir (and fp in general), so I still have some way to go to finding my feet.
As far as I can see, I do need to handle multiple data items (e.g. {:ok, "more", "than", "one", "item"}
) in the return (perhaps I’ll figure a better way out in the future?). And it looks like Solution is only error-handling lib that deals with this use case.
Given that with
is the officially “mandated” pipeline short-circuiting way, I’m gonna give Solution’s swith
a whirl. My code base is pretty tiny at the moment, so changing the error handling is not a huge effort.
Wish me luck!
Sorry if this is a stupid question, but does/can Solution handle boolean functions?
As a silly example:
def valid_input?(input) when is_binary(input) do
String.contains?("secret token:")
end
def valid_input?(input) do
false
end
Or do we have to explicitly convert the boolean result to an :ok/:error atom (or tuple, where appropriate)?
def valid_input?(input) when is_binary(string) do
case String.contains?("secret token:") do
true -> :ok
false -> :error
end
end
def valid_input?(_) do
:error
end
Which, at the end of the day, I wanna call like this:
def do_stuff(input) do
swith ok(res1) <- valid_input?(input),
ok(res2) <- do_next_thing(input),
ok(res3) <- do_another_thing(res2) do
"Life is good with #{res3}"
else
_ -> "Run for the hills"
end
end
In the same way as a normal with
, you can match on e.g. true
instead. (and potentially alter false
in the else
clause to something more descriptive)
def do_stuff(input) do
swith true <- valid_input?(input),
ok(res2) <- do_next_thing(input),
ok(res3) <- do_another_thing(res2) do
"Life is good with #{res3}"
else
_ -> "Run for the hills"
end
end
Yes of course! That makes perfect sense. Thanks
I’m running into a slight problem with scase
and alias
ed module names. I’m not sure if I’m doing something wrong. Here follows a simple test case to demonstrate the issue.
First I define a simple module:
iex(1)> defmodule Domain.Module1 do
...(1)> def wrap(something) do
...(1)> {:ok, something}
...(1)> end
...(1)> end
Then alias it and ensure the alias works:
iex(5)> alias Domain.Module1
Domain.Module1
iex(6)> Module1.wrap("asdf")
{:ok, "asdf"}
Now we test with a normal case
statement:
iex(7)> case Module1.wrap("asdf") do
...(7)>
...(7)> {:ok, stuff} -> "All good in the 'hood: #{stuff}"
...(7)>
...(7)> _ -> "Ehh... what happened?"
...(7)>
...(7)> end
"All good in the 'hood: asdf"
All fine so far. Now the equivalent scase
:
iex(9)> import Solution
Solution
iex(10)> scase Module1.wrap("asdf") do
...(10)>
...(10)> ok(stuff) -> "All good in the 'hood: #{stuff}"
...(10)>
...(10)> _ -> "Ehh... what happened?"
...(10)>
...(10)> end
** (UndefinedFunctionError) function Module1.wrap/1 is undefined (module Module1 is not available)
Module1.wrap("asdf")
iex(10)>
Thank you! This is an interesting problem. It indeed seems like some odd behaviour is going on here.
It would be very helpful if you could open up an issue for it on the repository .
Issue opened
As a heads-up: the issue has been resolved!
Version 1.0.1 has been released.
Awesome! Thanks a million