OK: Elegant error handling for elixir pipelines. Version 1.0 released

I have been updating a library that allows you to pipe between functions that use the erlang result tuple convention.

Assuming you have a double function with the spec double(int()) :: {:ok, int()} and safe dividing function. safe_div(int(), int()) :: {:ok, int()} | {:error, term()}.

Ok now can be used to create a pipeline that looks very much like the native pipe operator.

      iex> {:ok, 6} ~>> safe_div(3) ~>> double
      {:ok, 4.0}

      iex> {:ok, 6} ~>> safe_div(0) ~>> double
      {:error, :zero_division}

I have opened a pull request for this new version and pushed a release candidate. Hopefully itā€™s now interesting to people.

10 Likes

Very concise syntax for tagged fluent builder statements!

def instance(identity_ib_gibs, dest_ib, opts) do
  {:ok, identity_ib_gibs}
  ~>> TB.plan("[src]", opts)
  ~>> TB.add_fork("fork1", dest_ib)
  ~>> TB.add_rel8("rel8_2_src", "[plan.src]", ["instance_of"])
  ~>> TB.yo
end

And Iā€™m planning on more, slightly longer factory methods just like these and this helps clean things up very nicely. Thanks! :smile:

2 Likes

Thanks for the feedback. I havenā€™t really thought about the builder pattern in elixir, I normally have a great map I pass to a single function if need be but itā€™s certainly a nice usecase and one where you get to ensure that functions behave the way you want.

Version 0.2.0 released.

I will make a 1.0 release soon as there is almost no API to decide on. The one change that I think perhaps should be made is renaming the OK module to Ok so is follows mix patterns for capitalization?

3 Likes

Iā€™d recommend Ok as well as it is common vernacular now. However, OK is correct as it is short for (what is considered by etymologists(sp?)) Oll Korrekt, a vernacular of ā€œAll Correctā€ popularized in 1840ā€™s New York (told to me by google ^.^). :slight_smile:

So, although I tend to use Ok myself, technically OK is correct. :slight_smile:

3 Likes

Awesome tangential knowledge. I may well have to add some documentation to the effect of recording that.

2 Likes

Someone wrote a great article about railway programming in Elixir. They used the >>> operator but itā€™s quite similar!

Hereā€™s the link: http://www.zohaib.me/railway-programming-pattern-in-elixir/

1 Like

Nice finding. :heartpulse:
I assume this is based on article Railway oriented programming from great F# blog http://fsharpforfunandprofit.com/ :wink:

Robert Brown ā€¢ 2 years ago
I too read this RoP article and made an Elixir implementation of it. You can find it here: https://github.com/rob-brown/Mā€¦ Your implementation simulates the behavior described in the article.However, RoP is intended to be built using monads. By using monads, you can use the bind operation with more than just success/failure, ex. maybe monad and state monad.

Would you describe railway programming as a pattern or a technique.

I see that that article gives a bunch of examples on implementing the macros. My life would have been much easier if i had found that first.

Would you have a use for any of the other concepts, such as the tee. I havenā€™t noticed them missing from my code. I think it might be an 80/20 rule. ~>> gives the vast majority of the benefits and the rest are just minor improvements

1 Like

Although I love the concept, I found some issues with the implementation in practice for user errors, which is where this pattern is intended to be used. In particular:

  1. not all functions return the same shape.
    • one of my own functions that returned {:ok, :name, %metadata{}} which wouldnā€™t fit.
    • some of the Elixir functions I needed didnā€™t return an error tuple. Map.fetch/2 for example just returns :error.
  2. Although it was nice to break free when an error happened, I found error handling unnecessarily difficult because rather than handling the :error in context, I was doing it somewhere down the line. Often this meant I had to reconstruct context in order to provide a meaningful message to the UI.

To resolve #1, I decided to change how it measured the results and tested either the result == :error || elem(result, 0) == :error, and any other result was just passed through. This worked better for me and allowed for the shapes to be different. It could be expanded to capture more errors, but it still assumes that it would know all error conditions.

Overall, after using it for a while, it felt more like I was using try/catch or GOTO for flow control, which I am not fond of. I found it to basically be just a prettier but less comprehensible/usable version of with.

I still love the idea of a flow chart for flow control, though!

3 Likes

I agree with both of these comments wholeheartedly. This is why I use with statements prodigiously in my code, but I think OK really shines :sunny: in the builder use case as I mentioned above.

1 Like

@brainbag

  1. Have you looked at the exceptional library on hex.pm? It includes the functionality of ā€˜OKā€™ here as well as more to handle different shapes (handles :ok/:error 2-tuples, however you can integrate in more shape handlers, handles exceptions, handles raw values, and can convert between them all)) and has support for a new error handling paradigm (which I think is ingeniously simple/wonderful, but that is the story for another thread, but it is returning raw values or returning (not raising) exceptions).

For just ā€˜okā€™ 2-tuple handling I think this library handles its purpose wonderfully. :slight_smile:

All interesting points. Iā€™ve run across a some of them but not all.

@brainbag
some of the Elixir functions I needed didnā€™t return an error tuple. Map.fetch/2 for example just returns :error.

This one annoys me the most. I would happily petition away to have this changed to {:error, :not_found} to match convention. In reality if I care enough I just wrap any functions like that.

@brainbag
one of my own functions that returned {:ok, :name, %metadata{}} which wouldnā€™t fit.

why not create something like response = %{name: :name, meta: %metadata{}. If you have more than one extra thing in the tuple it wouldnā€™t be clear which is to pass to the next function. and the one that was not passed would be lost for ever.

Interesting point about handling errors out of context which is essential the goal of many error handling strategies.

1 Like

Thanks @OvermindDL1 :relaxed:. I should probably tag a 1.0 release and move on to another project.

1 Like

Just pushed a 1.0 release to hex.

4 Likes

Just pushed up a possible extension to OK which gives if result blocks inspired by scala for comprehensions.

It allows you to match within the value so you could possibly solve the metadata problem by writing the following.
requires first function to return {:ok, {data, metadata}}

OK.try do
  {data, metadata} <- first_function_with_errors
  _ <- second_function_with_errors(data)
end
2 Likes