PiedPiper
This gem provides "Unix-like" pipe functionality in Ruby.
The inspiration for this gem were |>
pipes and functional programming in Elixir.
After trying to introduce the same |>
pipe operator in Ruby, which I found out isn't possible due to syntactic reasons ( without hacking the underlying C code ), I settled for another well-known pipe operator, the |
Unix pipe operator.
If you want to read about the inspiration for the name PiedPiper
, it's an old german fairy tale, the Pied Piper of Hamelin, a guy who played pipe and hypnotized and lured all children out of town with his music and they were never be seen again.
Another thing worth seeing, regarding "Pied Piper" and coding, is Silicon Valley - Company Name. It's hilarious :-D ( Thanks, Michael! )
Despite the word "pipe" there's also another common thing between the fairy tale and pipes in this gem:
There's a "piper" object who lures "children" (other objects) away ( through pipes ) until they never be seen again ( are transformed into other objects ). :-)
If you never worked with pipes, this little analogy may help, to understand what's happening.
Have fun with PiedPiper and don't let him lure you away... :-)
Installation
Add this line to your application's Gemfile:
gem 'pied_piper'
And then execute:
$ bundle
Or install it yourself as:
$ gem install pied_piper
Usage
First you have to initialize a piper object with another object of your choice:
require "pied_piper"
p = PiedPiper.new("rats and kids")
A shortcut to get a piper object anywhere you want, is shown in the following example. Only use this if your name is "Chuck Norris of Hamelin", since it "roundhouse pipes" (monkey-patches) Ruby's Kernel
module :-D
Note: It only adds the piper
method to Kernel
which isn't defined anywhere else and doesn't change existing Ruby behaviour, so using it doesn't actually make you this badass ;-)
require 'pied_piper/kernel'
p = piper("rats and kids")
Once you have a piper object you can pipe it "Unix-style".
Our initial object (e.g: "rats and kids"
), is passed around through each pipe from left to right and transformed by one of the following objects:
Symbol/String Pipes
The easiest way to create a pipe is using symbols or strings. This will call the method with the same name as the Symbol/String on the wrapped object:
p = piper("rats and kids")
p | :upcase | p.end
# => "RATS AND KIDS"
More about p.end
below.
Chaining Pipes
Pipes can be chained of course:
p = piper("rats and kids")
p | :upcase | :reverse | p.end
# => "SDIK DNA STAR"
Ending Pipes
Note: Since "piping" is not a native Ruby syntax feature, rather than a method call in disguise (e.g: "foo".|(:upcase)
), the underlying PiedPiper
class, which wraps the initial object (e.g: "foo"
) and on which the pipe functionality is called, is just a wrapper for the initial object which handles piping logic.
Everytime you transformed an object through a pipe you create a new object of class PiedPiper
, which wraps the mutated inital object again (e.g: from "foo"
to "FOO"
).
This way, we can build pipe-chains of arbitrary length, but at the end of each pipe-chain, the piped object has to be "unwrapped" again by some kind of "terminator" object.
This happens by "terminating" the pipe-chain with the .end
method, which is defined on every piped object, or by just writing PiedPiper::EndOfPipe
or if you have required pied_piper/kernel
you can just write p_end
.
p = piper("rats and kids")
p | :upcase | p.end
# => "RATS AND KIDS"
p | :upcase | PiedPiper::EndOfPipe
# => "RATS AND KIDS"
p | :upcase | p_end # when PiedPiper::Kernel was required
# => "RATS AND KIDS"
It would be possible to avoid writing p.end
at the end of the chain, by implementing the gem in another way, but that would have included to monkey-patch existing Ruby classes, since the |
method is already implemented by some of them.
I decided against that, since I only wanted to add pipe functionality and not alter existing Ruby behaviour in any way.
Thus the PiedPiper
class was born.
Ruby and its Kernel module
If you want to add pipe functionality everywhere, we already talked about how to implement it above by requiring pied_piper/kernel
.
This will provide pipe functionality on every object which has Object
in one of its superclasses ( thus practically every object in Ruby besides BasicObject
which is the highest class in Ruby's inheritance hierarchy ) with the least amount of monkey-patching/side-effects, since we only add one Kernel
method named piper
and don't alter existing behaviour.
In case you didn't know:
Object
includes Kernel
as a module.
Included modules can be seen in Ruby like this:
Object.included_modules
# => [Kernel]
If you want to see the whole inheritance chain of a class (with superclasses and included/prepended modules) you can do this:
Object.ancestors
# => [Object, Kernel, BasicObject]
Since Object includes Kernel
, Kernel
follows after Object
.
If Object
would prepend Kernel
, Kernel
would be before Object
.
Methods who are intended to be globally available, like puts
and gets
, and who aren't intended to be available with an explicit receiver like "foo".puts
are defined as private instance methods on Kernel
.
All private instance methods can only be called with an implicit receiver (implicit self) in Ruby.
That's why things like this work with an implicit receiver/self, because were always "inside" an object:
puts self
# main
# nil
# implicit receiver/self for puts
puts "foo"
... but this doesn't, since we called puts
on an explicit receiver:
# explicit receiver/self for puts
self.puts "foo"
# => NoMethodError: private method `puts' called for main:Object
So if you want to provide functionality that's available everywhere like puts
, but cannot be called on an object, the usual approach is to define a private instance method on Kernel.
That's what has been done with the piper
and p_end
methods and can be seen in the source here :-)
Just a bit of background information, in case you're curious how this gem was implemented.
But back to usage...
Array Pipes
An Array, whose first element (Symbol/String) again acts as a method call on the piped object and additonal elements which act as parameters to the method call.
p = piper("Pied Piper")
concat = [:concat, " of", " Hamelin"]
p | concat | p.end
# => "Pied Piper of Hamelin"
If the first array element is a Symbol and the last is an object of class Proc
, we can also use methods which accept blocks:
p = piper("Pied Piper")
map_double = [:map, ->(str) { str * 2 }]
p | :split | map_double | :join | p_end
# => "PiedPiedPiperPiper"
It's also possible to use methods with arguments and blocks, arguments have to between the method name (first element of the array) and the last element of the array which is a block:
p = piper("Pied Piper")
map_double_array = [:each_with_object, [], ->(str, array) { |str| array << [str * 2]}]
p | :split | map_double_array | p_end
# => [["PiedPied"], ["PiperPiper"]]
Proc Object Pipes
An Object of Proc
class which takes exactly one parameter:
Proc.new { |kid| ... }
proc { |kid| ... }
lambda { |kid| ... }
->(kid) { ... }
p = piper("Hypnotized kid")
no_happy_end = ->(kid) { kid + " was never seen again..." }
p | no_happy_end | p.end
# => "Hypnotized kid was never seen again..."
Method Object Pipes
An object of the Method
class, where the piped object will be used as the first parameter. You can pass an Array if you need additional parameters:
class PiedPiperOfHamelin
def self.plays_song_on_pipe(audience = "Kids", effect = "slightly")
puts "#{audience} already feel #{effect + " "}hypnotized!"
end
end
p = piper("You")
hypnotize = PiedPiperOfHamelin.method(:plays_song_on_pipe)
p | hypnotize | p.end
# => "You already feel slightly hypnotized!"
p | [hypnotize, "VERY"] | p.end
# => "You already feel VERY hypnotized!"
Combining Pipes
Once you know the basic building blocks, you can combine them and build your own pipes of arbitrary length:
p = piper('You')
lures = [:+, " feel"]
you = ->(str) { str + " hypnotized!" }
away = :upcase
p | lures | you | away | piper.end
# => "YOU FEEL HYPNOTIZED!"
Multiline Pipes
Sadly Ruby's syntax doesn't allow for totally nice multiline pipes ( at least not in a way I already found out ). Thus multiline pipes have to be written with a twist ( since they're actually just inline syntactic sugar for methods like obj.|(arg)
under the hood:
With Backslashes:
In order to tell Ruby that our (inline) expression hasn't ended yet, when writing it over multiple lines, we can put a backslash at the end of a line to avoid syntax errors:
p = piper('You')
lures = [:+, " feel"]
you = ->(str) { str + " hypnotized!" }
away = :upcase
p \
| lures \
| you \
| away \
| h.end
# => "YOU FEEL HYPNOTIZED!"
As explicit method calls:
As we already noticed that pipes |
are simply method calls in disguise, we can explicitly call them on subsequent lines, which is valid Ruby syntax:
p = piper('You')
lures = [:+, " feel"]
you = ->(str) { str + " hypnotized!" }
away = :upcase
p
.|(lures)
.|(you)
.|(away).
.|(h.end)
# => "YOU FEEL HYPNOTIZED!"
What kind of advantages can pipes offer?
Actually we can do nothing else with pipes then we can also do with regular Ruby syntax ( since it only builds on already existing Ruby functionality and is not a totally new language feature ).
But piping objects can make some operations more clear/readable because we go from left to right in a linear fashion, instead from inside to outside like in regular function calls.
This offers advantages when for example working in "functional style" instead of using methods:
class Foo
def self.one
-> {|x| ... }
end
end
class Bar
def self.two
-> {|x| ... }
end
end
class Baz
def self.three
-> {|x| ... }
end
end
# This
Baz.three.call(Bar.two.call(Foo.one.call(x)))
# becomes this
piper(x) | Foo.one | Bar.two | Baz.three | p_end
# or
piper(x) \
| Foo.one \
| Bar.two \
| Baz.three \
| p_end
I guess you won't use these kind of functional programming in Ruby too often, since Ruby follows another philosophy.
PiedPiper is more a kind of experiment how Ruby can be modified to resemble concepts used in other programming languages like for example Elixir.
As far as I can judge Ruby does quite a good job, the flexible language constructs that Ruby has to offer, makes it one of the nicest programming languages to work with. :-)
What other kind of good use cases for pipes can you come up with?
If you know some ( or missing features ), feel free to open up a pull request, so we can augment code/documentation :-)
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/christophweegen/pied-piper. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the PiedPiper project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.