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"]]
@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