This is not a pipe
This is an experimental/demo Ruby implementation of Elixir-style pipes. It allows to write code like this:
require 'not_a_pipe'
extend NotAPipe
pipe def repos(username)
username >>
"https://api.github.com/users/#{_}/repos" >>
URI.open >>
_.read >>
JSON.parse(symbolize_names: true) >>
_.map { _1.dig(:full_name) }.first(10) >>
pp
end
Basically:
not_a_pipe
is a decorator to mark methods inside which>>
works as “pipe operator”;- every step can reference
_
which would be a result of the previous step; - but it also can omit the reference and just specify a method to call; the result of the previous step would be substituted as the first argument of the method.
not_a_pipe
works by rewriting the AST and reevaluating the (rewritten) method code at the definition time and has no runtime penalty; thus achieving something akin to macros.
It is not intended to use in production codebase, but rather as an approach investigation/demonstration.
Inspired by a Python’s library that uses the similar approach, and a recent discussion in Ruby’s bug-tracker.
See also an explanatory blog-post.
Usage
Don’t. Really. The code is really naive, tested only for simple cases, and is not intended as a library that will be relied upon. It is an experiment.
But if you want to play, you can install it as a gem:
gem install not_a_pipe
...and then follow the example above.
Benchmarks
See benchmark.rb
. Compared versions are:
- “naive” Ruby code which puts values into intermediate variables;
.then
-based Ruby version that chains everything in one statement;not_a_pipe
version- pipe_envy-based solution, which is pretty simple (allows to join callable objects with
>>
) - pipe_operator-based solution, which is impressively witty looking but requires an extensive implementation with “proxy objects”
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux]
Warming up --------------------------------------
naive 3.000 i/100ms
.then 3.000 i/100ms
not_a_pipe 4.000 i/100ms
pipe_operator 1.000 i/100ms
pipe_envy 1.000 i/100ms
Calculating -------------------------------------
naive 18.488 (± 5.4%) i/s (54.09 ms/i) - 93.000 in 5.067112s
.then 15.622 (± 6.4%) i/s (64.01 ms/i) - 78.000 in 5.019804s
not_a_pipe 18.140 (± 5.5%) i/s (55.13 ms/i) - 92.000 in 5.083882s
pipe_operator 1.520 (± 0.0%) i/s (657.81 ms/i) - 8.000 in 5.266537s
pipe_envy 7.296 (±13.7%) i/s (137.06 ms/i) - 37.000 in 5.098091s
Comparison:
naive: 18.5 i/s
not_a_pipe: 18.1 i/s - same-ish: difference falls within error
.then: 15.6 i/s - 1.18x slower
pipe_envy: 7.3 i/s - 2.53x slower
pipe_operator: 1.5 i/s - 12.16x slower
Note that not_a_pipe
is the fastest version, on par only with “naive” verbose Ruby code with intermediate variables, and without .then
-chaining (truth be told, on various runs .then
-based version is frequently “sam-ish”). The rewrite-on-load approach is a rare way to introduce a DSL without any performance penalty.
Credits
- Implementation: Victor Shepelev aka zverok
- The syntax is proposed by Alexandre Magro on Ruby bug-tracker
- The AST-rewriting approach is inspired by Python’s pipe_operator library