The Pipeline
Refinement module bundles a method duo that builds
a clean and clever pure-Ruby solution to rightward method piping.
A reminder that Refinement modules are activated with using
and last only
for that module/class block or file (if top-level). See: refinements.rdoc
(on docs.ruby-lang.org)
Synopsis Example
Here’s the example from Kernel#then
straight off the Ruby 3.2 docs:
require 'open-uri'
require 'json'
construct_url(arguments).
then {|url| URI(url).read }.
then {|response| JSON.parse(response) }
With the new Refinement active: (Oh gosh, this looks like a brand-new language!)
using Pipeline
arguments.then_pipe(
`construct_url`,
`URI`,
:read,
JSON.`:parse
)
Walkthrough Introduction
We traditionally pipe methods leftwards as nesting arguments:
def parse(string) = …
do_stuff = ->(a){ … }
STDOUT.puts(
do_stuff.call(
parse(
input
)
)
)
The trending Rightward Operations improve readability –
especially for wordy expressions like the above –
by matching English’s left-to-right writing direction.
We currently accomplish this with Object#then
and light-weight Procs:
input
.then { parse _1 }
.then { do_stuff.call _1 }
.then { STDOUT.puts _1 }
We alternatively can deconstruct the corresponding #to_proc
objects with &
:
input
.then(&method(:parse))
.then(&do_stuff)
.then(&STDOUT.method(:puts))
The Refinement module Pipeline
introduces Object#then_pipe
to
eliminate repeating then(&…)
. It gives a beautiful vibe similar to
that of the Pipe Operator |>
in some other languages (namely Elixir).
using Pipeline
input.then_pipe(
method(:parse),
do_stuff,
STDOUT.method(:puts)
)
However, unlike Lisp-1 languages like Python or JavaScript,
Lisp-2s like Ruby face the additional challenge of namespace disambiguation.
Ruby solves it with the reflection methods method
& co., but, as seen above,
their verbosity makes them eyesores inside otherwise elegant syntax.
Therefore, Pipeline
further improves the grammar by replacing the `…`
syntax (powered by Kernel#`
) with an alias
of the bulky Object#method
:
using Pipeline
input.then_pipe(
`parse`,
do_stuff,
STDOUT.`:puts
)
Why #`
?
- It is the backend of both
`…`
and %x{…}
syntaxes –
self.`:meth
can instead be %x:meth:
or `meth`
. - A Ruby script is not (strictly) a Shell script. User system differences aside,
summoning subshells should be the last resort when there are no Ruby/Gem APIs.
Dedicating the
`
char to subshells is a waste;
e.g., you typically see $(…)
in bash rather than `…`
.
Pipeline
assigns #`
a new and recurrent purpose; it also alias
es
the original Kernel#`
to Object#sys
in the event of its preference.
Limitations
- The new
#`
ignores method visibilities (private
or protected
).
- This is a Ruby limitation – one can bypass visibilities with
Object#method
and Method#call
regardless of this Refinement.
A security engineer would love reflection APIs that respect visibilities. - The original design for the new
#`
was to match Object#public_method
,
but it couldn’t retrieve oneself’s own private
and protected
methods.
#then_pipe
cannot pass additional arguments at each step.
- The current
Object#then
-based solution is as good as it can get –
There are no syntactical benefits to avoid a block while calling rightwards.
Hey, unlike #then_pipe
, it welcomes (inner) blocks.
do_stuff = ->(a, o, z){ … }
o.then { do_stuff.(a, _1, z) }
o.then_pipe ->{ do_stuff.(a, _1, z) }
[a, o, b].then { do_stuff.(*_1) }
Miniblog: My insights on a Pipeline Operator for Ruby
https://gist.github.com/ParadoxV5/4f563e609decbdac07d40e5f2dead751
License
Copyright (c) 2023 ParadoxV5
Licensed under the Universal Permissive License v 1.0