Instantly share code, notes, and snippets.

@pcreux /pipable.rb
Last active Jun 12, 2018

Embed
What would you like to do?
*nix has pipes, Elixir has pipes, Ruby deserves pipes.
# Elixir has pipes `|>`. Let's try to implement those in Ruby.
#
# I want to write this:
#
# email.body | RemoveSignature | HighlightMentions | :html_safe
#
# instead of:
#
# HighlightMentions.call(RemoveSignature.call(email.body)).html_safe
#
# Ugly implementation starts here...
def pipe_it(input, filter)
# multiplexed input!
if input.is_a? Array
return input.map { |input_item| pipe_it(input_item, filter) }
end
case filter
when Symbol
input.send(filter)
when Hash
method = filter.keys.first
arguments = Array(filter.values.first)
input.send(method, *arguments)
when Array
# multiplex!
filter.map { |filter_item| pipe_it(input, filter_item) }
else
filter.call(input)
end
end
class Pipeline
def initialize(*filters)
@filters = filters
end
attr_accessor :filters
def call(input)
filters.inject(input) do |input, filter|
pipe_it(input, filter)
end
end
end
def pipable(input)
input.define_singleton_method(:|) do |filter|
pipable pipe_it(input, filter)
end
input
end
def pipe(input, *pipeline)
Pipeline.new(*pipeline).call(input)
end
# Let's define a few filters
Reverse = ->(string) { string.reverse }
Leet = ->(string) { string.gsub(/[aeiost]/,'a'=>'4','e'=>'3','i'=>'1','o'=>'0','s'=>'5','t'=>'7') }
Mooify = ->(string) { "Cow said: " + string }
Say = ->(string) { system %|say "#{string}"|; string }
TweetTo = Struct.new(:recipient) do
def call(input)
puts %|Tweeting "#{input}" to #{@recipient}!|
input
end
end
# Time to play with different approaches...
# 1 - We make the first element pipable and we can then just pipe through!
result = pipable("moo") | Reverse | Leet | Mooify | :downcase | TweetTo.new('@pcreux') | { delete: 'o' }
puts result
# => cw said: 00m
# 2 - Pipe without defining any `|` method
puts pipe("moo", Mooify, :upcase)
# => COW SAID: MOO
# 3 - Pipeline object
pipeline = Pipeline.new(Mooify, :downcase, { gsub: ["o", "a"] })
pipeline.filters << ->(input) { input.gsub("moo", "maow") }
puts pipeline.call("moo")
# => caw said: maa
pipeline.filters.reverse!
puts pipeline.call("moo")
# => Cow said: maaw
# ZOMG! Multiplexing!
# "moo" => Mooify => :downcase => Reverse
# => :upcase => Reverse
p Pipeline.new(Mooify, [:downcase, :upcase], Reverse).call("moo")
# => ["oom :dias woc", "OOM :DIAS WOC"]
# Multi-Multiplexing... let me tell you...
p Pipeline.new(Mooify, [:downcase, :upcase], Reverse, [:reverse, Leet]).call("moo")
# => [["cow said: moo", "00m :d145 w0c"], ["COW SAID: MOO", "OOM :DIAS WOC"]]
@jmgarnier

This comment has been minimized.

jmgarnier commented Sep 24, 2014

Interesting 😄 I much prefer the functional style compared to the OOP. Much more readable.

I have spent 5 minutes reading the code and I don't understand how the | magically works in:

result = pipable("moo") | Reverse | Leet | Mooify
@pcreux

This comment has been minimized.

Owner

pcreux commented Sep 24, 2014

pipable("moo") defines the singleton method | on "moo". | will then define | on its arguments. The result will be a "pipable object" - it responds to |. It reads well, but I don't like defining singleton methods as it could overwrite existing behaviour (e.g. Array).

@vidarh

This comment has been minimized.

vidarh commented Sep 25, 2014

If you don't want to use singleton methods, use a Delegator object with "|" defined, and wrap the object that's passed in. You'll still "overwrite" existing behaviour for something that receives the wrapped object, but anyone getting the unwrapped object will only see the real behaviour.

@douglascamata

This comment has been minimized.

douglascamata commented Sep 25, 2014

I don't feel the need for pipes in Ruby. Instead of building an structure that calls this:

HighlightMentions.call(RemoveSignature.call(email.body)).html_safe

I would build something that would be:

EmailProcessor.new(email).highlight_mentions.remove_signature.html_safe

# or maybe
EmailProcessor.new(email, :highlight_mentions, :remove_signature, :html_false).result
@akitaonrails

This comment has been minimized.

akitaonrails commented Sep 25, 2014

I agree with @douglascamata, instead of having an operator, why not just method chaining similarly to how Arel works in Rails nowadays?

@fcoury

This comment has been minimized.

fcoury commented Sep 25, 2014

@akitaonrails I think the key difference is that method chaining is usually bound to an object of a given class, whereas the pipe objects are completely ignorant of who's sending the message in.

@petrachi

This comment has been minimized.

petrachi commented Sep 26, 2014

Hey,
I followed you on your ground, and tried to implement this myself. As I don't have much time, this is still a basic version, but what do you think of this : http://beta.42grounds.io/s/3d38071e2673e937cebcefa57e9a357de0d65bfd596b8969fd5c60a8e4d6959a ?

Also here : https://gist.github.com/petrachi/637f9367404708ec341a

@baweaver

This comment has been minimized.

baweaver commented Sep 30, 2014

Elixir pipes in Ruby feature request: https://bugs.ruby-lang.org/issues/10308 - thoughts?

@kek

This comment has been minimized.

kek commented Oct 1, 2014

I tried to do something similar in https://github.com/kek/pipelining

@petrachi

This comment has been minimized.

petrachi commented Oct 1, 2014

Honestly, I don't see the point in pipes. I mean, this is a nice demo of code, but my though is that the problem lies elsewhere, in modules used at wrapper.

In the firsts lines of this git, it's wrote (I shrink the quote)

# I want to write this:
# email.body | RemoveSignature
# instead of:
# RemoveSignature.call(email.body)

So, there is a RemoveSignature module somewhere, with a singleton method call. This is the problem you want to solve. And my guess is that the call you may want to make will be email.body.remove_signature

I think this can be achieved by safely monkey patch the String class, using the Refinements of ruby 2+.

Or, if you don't want to use that, you can extend the body variable with a module using definig instance methods, like this:

module RemoveSignature
  def remove_signature
    # code that remove signature, should modify self
  end
end

class Email
  def body
    @body.extend(RemoveSignature)
  end
end

email.body.remove_signature

Then, you will not have callings like RemoveSignature.call(email.body), nor your 'pipe' need.

@justinwiley

This comment has been minimized.

justinwiley commented Oct 14, 2014

Nice implementation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment