Skip to content

Instantly share code, notes, and snippets.

@pcreux
Last active June 12, 2018 17:08
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save pcreux/2f87847e5e4aad37db02 to your computer and use it in GitHub Desktop.
Save pcreux/2f87847e5e4aad37db02 to your computer and use it in GitHub Desktop.
*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
Copy link

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
Copy link
Author

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
Copy link

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
Copy link

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
Copy link

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

@fcoury
Copy link

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
Copy link

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
Copy link

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

@kek
Copy link

kek commented Oct 1, 2014

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

@petrachi
Copy link

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
Copy link

Nice implementation

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