Chain - An easy way to chain functions that return ok/error tuples

Hello all,

I have been using Elixir for a bit of time, and each time I wrote code like this I wondered if there were not a better way than chain with functions.

So I created Chain https://github.com/Apemb/chain

What do you think ?
Do you see it as useful ?

6 Likes

Hello and bienvenue…

You don’t like to use with?

There is also witchcraft, which might have a similar purpose.

1 Like

I find it a bit unclear when there are a lot of “errorable” functions. (like for a use case)

Some example of usage from my current projet :

  def run(%EditOneShift{} = edit_shift_data) do
    data = %{edit_shift_data: edit_shift_data}

    data
    |> Chain.new()
    |> Chain.next(&load_shift_aggregate/1)
    |> Chain.next(&verify_shift_can_be_edited/1)
    |> Chain.next(&create_update_command/1)
    |> Chain.next(&update_shift_aggregate/1)
    |> Chain.next(&save_updated_shift_aggregate/1)
    |> Chain.next(&load_updated_shift_entity/1)
    |> Chain.next(&return_shift_entity/1)
    |> Chain.run()
  end

It would be a bit less clear with with I find.

I will check out witchcraft for sure.

Additionally to @kokolegorille s list:

  • ok_jose
  • auto_pipe
  • and there was at least another one which name I can’t remember…
3 Likes

With witchcraft, it would look like this…

data
>>> load_shift_aggregate/1
>>> verify_shift_can_be_edited/1
>>> create_update_command/1
>>> update_shift_aggregate/1
>>> save_updated_shift_aggregate/1
>>> load_updated_shift_entity/1
>>> return_shift_entity/1

But there are more operators… https://github.com/witchcrafters/witchcraft#operators

2 Likes

There is also ok library, by @Crowdhailer

1 Like

Thank you for all the links :star_struck:
I have quite a bit to read on. Especially witchcarft, (even if it does seem a tad too complex to me. My team and I are no Haskell FP wizards, at least not yet).

We were quite used to the Javascript promise syntax, so the Chain API is inspired by the standard JS Promise. And is not a macro, more like the Ecto Multi Struct, that manages a transaction. So you have more freedom with were and what you do with you Chain Struct.

I will check on to see if those libs have the features we use :

  • ok/error chaining (obviously)
  • intermediate error catching (error tuples, but also thrown error)
  • we are able to manage the same workflows as we do now.

We have sort of complicated use cases, and Chain helps managing the complexity. (Like this one)

    date_until_which_generate_new_shifts
    |> Chain.new()
    |> Chain.next(&get_list_of_shift_series_candidates_ids/1)
    |> Chain.next(fn ids ->
      Enum.map(ids, fn id ->
        generation_opts = [
          date_until_which_generate_new_shifts: date_until_which_generate_new_shifts
        ]

        id
        |> Chain.new()
        |> Chain.next(&ShiftSeriesAggregateRepository.get/1)
        |> Chain.next(&ShiftSeriesAggregate.generate_future_shifts(&1, generation_opts))
        |> Chain.next(&ShiftSeriesAggregateRepository.save/1)
        |> Chain.recover(&log_individual_shift_error(&1, shift_series_id: id))
        |> Chain.run()
      end)
    end)
    |> Chain.recover(&log_global_shift_error/1)
    |> Chain.capture(&log_raised_shift_error/2)
    |> Chain.run()

Chain misses a Promise.all equivalent, or anyway to manage Enumerable that have to be mapped to something else.

Maybe I’m wrong, but isn’t the catching thrown error anti-pattern?:

  • Avoid using exceptions for control-flow
  • Avoid working with invalid data

Also you can use the result library, for example this a way.

1 Like

Yes, I suppose it is an anti-pattern.
Our need was to be able to verify that a cron job had run. As Ecto can throw in some circumstances (as far as I understand at least) having that possibility was really helpful to ensure we log any error that can happen in our job and transfer that log to sentry.
Chain gives you the stack trace with the thrown error, so in our case the error is re-thrown with the initial stack trace after we log it.

Result seems a great library, it seems simple enough and might cover much of our usecase. I will check it out, thank you.

I get it. We solved something similar I think. We have a lot of measured data from sensors at Postgres. First we came with cron job for aggregating. After that we refactored cron job to GenServer similar to José Valim’s answer + used Logger for logging errors. In the end we rewrote it with Ecto Query :slight_smile: