Implementing pipe in Ruby - Part 2



Last month, Shane Emmons did a post on implementing pipe in ruby. Check out his post here.

As Shane mentioned, the unix pipe operator is extremely powerful and simple to use / understand. The result of the first operation is passed as the argument for the next operation and so on. Last summer, shortly before I came onboard, Shane implemented our first version:

1
2
3
4
5
6
def pipe(initial_env, through:)
  through.reduce(initial_env) do |env, method|
    status, _, _ = env
    success?(status) ? send(method, env) : env
  end
end

As we progressed, changes were made and additional, slightly different, versions of the pipe method started creeping in.

Here they are as of the beginning of 2/16/2015:

1
2
3
4
5
def pipe(response, through:)
  through.reduce(response) { |resp, method|
    resp.success? ? send(method, resp) : resp
  }.to_ary
end
1
2
3
4
5
def pipe(response, through:)
  through.reduce(response) { |resp, method|
    resp.success? ? send(method, resp) : resp
  }
end
1
2
3
4
5
6
7
8
9
10
11
def pipe(message, through: [])
  through.reduce(message) { |msg, method|
    break unless msg.continue?

    begin
      send(method, msg)
    rescue => e
      handle_error(e, { method: method, message: msg })
    end
  }
end

Sure enough, as I started on my most recent task, I came across the need to write another slightly different version of the method. The code duplication got to me. Another problem that the whole team was encountering had to do with debugging and tracing errors. Ruby is normally very good at providing detailed information about what's going wrong. In this case, however, errors would occur in one of the methods specified in the through array and the stack trace would point back to the pipe method. We didn't know which method was failing or even what arguments were passed to it. Many of us found ourselves adding STDOUT.puts in each method called and debugging from there. For me at least, this brought back nightmarish memories of alert debugging JavaScript prior to the days of Firebug and Chrome's JS console.

In an attempt to rid myself of these nightmares, the pipe-ruby gem was born.

Now what?

As I looked at our implementations, there were a number of requirements popped out at me.

Additionally, I found that I would be iterating over an array and passing each value to pipe. It seemed easiest to add that to the GEM, so pipe_each was born.

The Pipe::Config object provides the fine tuning capabilities defined by our existing requirements.

1
2
3
4
5
6
7
8
Pipe::Config.new(
  :error_handlers => [],   # an array of procs to be called when an error occurs
  :raise_on_error => true, # tells Pipe to re-raise errors which occur
  :skip_on => false,       # a truthy value or proc which tells pipe to skip the
                           # next method in the `through` array
  :stop_on => false        # a truthy value or proc which tells pipe to stop
                           # processing and return the current value
)

There are a number of ways to send a custom config to #pipe or #pipe_each. They are fully described in the README's Configurable Options section. The README also does a great job of outlining error handling and skipping / stopping execution.

comments powered by Disqus