-
-
Save pcreux/2f87847e5e4aad37db02 to your computer and use it in GitHub Desktop.
# 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"]] | |
I tried to do something similar in https://github.com/kek/pipelining
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.
Nice implementation
Elixir pipes in Ruby feature request: https://bugs.ruby-lang.org/issues/10308 - thoughts?