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.