# 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"]] | |
This comment has been minimized.
This comment has been minimized.
|
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
I agree with @douglascamata, instead of having an operator, why not just method chaining similarly to how Arel works in Rails nowadays? |
This comment has been minimized.
This comment has been minimized.
@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. |
This comment has been minimized.
This comment has been minimized.
Hey, Also here : https://gist.github.com/petrachi/637f9367404708ec341a |
This comment has been minimized.
This comment has been minimized.
Elixir pipes in Ruby feature request: https://bugs.ruby-lang.org/issues/10308 - thoughts? |
This comment has been minimized.
This comment has been minimized.
I tried to do something similar in https://github.com/kek/pipelining |
This comment has been minimized.
This comment has been minimized.
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 I think this can be achieved by safely monkey patch the Or, if you don't want to use that, you can extend the 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 |
This comment has been minimized.
This comment has been minimized.
Nice implementation |
This comment has been minimized.
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: