https://github.com/zverok/not_a_pipe
An Elixir-like pipes for Ruby (oh no not again). Experiment/demo
https://github.com/zverok/not_a_pipe
Last synced: 9 months ago
JSON representation
An Elixir-like pipes for Ruby (oh no not again). Experiment/demo
- Host: GitHub
- URL: https://github.com/zverok/not_a_pipe
- Owner: zverok
- Created: 2024-11-16T13:33:46.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2024-11-16T18:29:40.000Z (over 1 year ago)
- Last Synced: 2024-11-27T21:34:07.344Z (over 1 year ago)
- Language: Ruby
- Size: 25.4 KB
- Stars: 8
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# This is not a pipe
This is an experimental/demo Ruby implementation of Elixir-style pipes. It allows to write code like this:
```ruby
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:
* `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](https://github.com/Jordan-Kowal/pipe-operator?tab=readme-ov-file#-elixir-like-implementation) that uses the similar approach, and a [recent discussion](https://bugs.ruby-lang.org/issues/20770#note-34) in Ruby’s bug-tracker.
See also an [explanatory blog-post](https://zverok.space/blog/2024-11-16-elixir-pipes.html).
## 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:
```bash
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](https://github.com/hopsoft/pipe_envy)-based solution, which is pretty simple (allows to join callable objects with `>>`)
* [pipe_operator](https://github.com/LendingHome/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](https://zverok.space)
* The syntax is proposed by Alexandre Magro [on Ruby bug-tracker](https://bugs.ruby-lang.org/issues/20770#note-34)
* The AST-rewriting approach is inspired by Python’s [pipe_operator](https://github.com/Jordan-Kowal/pipe-operator) library
