Qqwy

Qqwy

TypeCheck Core Team

Solution: library to do pattern-matching with general ok/error types in case/with statements -- v1.0.1

logo_text

hex.pm version Build Status Documentation

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.

hex - docs - github


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 bht 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:

  1. It defines clever guard macros that match either of these groups (is_ok(x), is_error(x), is_okerror(x))
  2. It defines macros to be used inside special case and with statements that use these guards and are also able to bind variables:

For instance, you might use ok() to match any ok-type datatype, and error() 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.

Examples

Guards

Solution exposes three guard-safe functions: is_ok(x), is_error(x) and is_okerror(x)

  • ok(x) will match :ok, {:ok, _}, {:ok, _, _}, {:ok, _, _, __} and any longer tuple whose first element is :ok.
  • error(x) will match :error, {:error, _}, {:error, _, _}, {:error, _, _, __} and any longer tuple whose first element is :error.
  • okerror(x) 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.scaseworks 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 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, 44, %{a: "other stuff"}}
 swith ok(res)  <- x,
       ok(res2) <- y do
         "We have: \#{res} \#{res2}"
    else
      _ -> "Failure"
  end
#=> "We have: 10 33"

For more examples and more info about the helper functions, check the GitHub page or the Documentation. :slightly_smiling_face:


Please let me know what you think!

First 10 of 20 Posts Switch mode

Qqwy

Qqwy OP

TypeCheck Core Team

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 :slight_smile: .

Qqwy

Qqwy OP

TypeCheck Core Team

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. :slight_smile:

ondrej-tucek

ondrej-tucek

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.

Qqwy

Qqwy OP

TypeCheck Core Team

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:

Comparing it to the ok library:

  • OK requires ok/error tuples to always exactly have two elements (the first being :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 introduces a new ‘keyword’ called 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 has a wrapper called OK.try which is sort of like OK.for but wraps it in an extra try/rescue block (I think?).
  • OK contains a couple of custom operators to allow ‘monadic piping’.

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.

Comparing it to the result library:

  • Result has a bunch of monad-inspired functions, but no overloaded case or with statements.
  • Just like OK, Result only accepts exactly the format {:ok, value}/{:error, problem}.

Comparing it to the exceptional library:

  • Exceptional has features to declaw exceptions and turn them into plain structs or possibly ok/error tuples, and reraise them later. Handling exceptions is not a design goal of Solution, so it does not have this functionality.
  • Exceptional also has custom ‘monadic piping’ operators.
  • Handling different kinds of tagged tuples is not a design goal of Exceptional, so it does not have functionality to work with these datatypes (other than a basic ‘normalization’ conversion, which also does not handle ok/error tuples with more than one value inside). EDIT: It’s slightly more nuanced than that. see this follow-up.

Comparing it to the towel library:

  • Towel also takes the approach of a ‘monadic pipeline’. Not with special syntax this time, but using a couple of functions (that are probably part of your global namespace since it advocates to use Towel).
  • Towel does not have support for values of the type :ok/:error nor for {:ok, multiple, things}`.
  • Towel’s last commit was more than two years ago.

Comparing it to the ok_jose library:

  • OkJose overloads |> to work differently for ok/error tuples.
  • OkJose allows you to create other overloaded versions of |>.
  • OkJose does not have support for values of the type :ok/:error nor for {:ok, multiple, things}`.
  • OkJose’s last commit was more than two years ago, and the README mentions that it was made before with was available in Elixir.

So the Tl;Dr is that Solution has a slightly different approach to these libraries:

  • It strives to support all possible ok/error values that are used in practice: Not only the two-element tuple versions, but also the single atoms :ok and :error, as well as the versions with more than two elements like {:ok, data, metadata}.
  • It strives to be lightweight by using the existing case and with syntax, rather than adding variants of the pipe-operator and/or require an understanding of monads to be used successfully.
  • It is focused on working with ok/error tuples. It does not deal with exceptions or other kind of failure indications.

I hope that answers your question! :smile:

ondrej-tucek

ondrej-tucek

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

Qqwy

Qqwy OP

TypeCheck Core Team

Great idea :+1:

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 :stuck_out_tongue_winking_eye: ).
Other people might like other libraries more, and other situations might call for other libraries. And that’s great! :slight_smile:

ondrej-tucek

ondrej-tucek

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.

OvermindDL1

OvermindDL1

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. :slight_smile:

A quick conversion of the cases of Solution to Exceptional from the OP would be:

SCase

 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"

SWith

 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!

Qqwy

Qqwy OP

TypeCheck Core Team

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:

  • Exceptional does handle the case of ‘having a stack trace’ as third element of an {:error, problem, trace} tuple.
  • Other kinds of ok and error tuples, like for instance {: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 :smile:.

Daevor

Daevor

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).

Where Next?

Trending in Announcing Top

bluzky
You may know https://ui.shadcn.com/, a UI component library for React. I really love it’s design style and components. I’ve built some co...
387 14960 120
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
shahryarjb
The Chelekom project is a library of Phoenix and LiveView components generated via Mix tasks to fit developer needs seamlessly. One of i...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New

Other Trending Topics Top

type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
bjorng
We want to introduce a new native datatype to Erlang: native records. Although replacing all tuple records with native records is not our...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New

We're in Beta

About us Mission Statement