-
-
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"]] | |
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).
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.
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
I agree with @douglascamata, instead of having an operator, why not just method chaining similarly to how Arel works in Rails nowadays?
@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.
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
Elixir pipes in Ruby feature request: https://bugs.ruby-lang.org/issues/10308 - thoughts?
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
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: