Skip to content

Instantly share code, notes, and snippets.

@christhekeele
Last active January 26, 2016 19:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save christhekeele/14b10671c1aeef4e3a74 to your computer and use it in GitHub Desktop.
Save christhekeele/14b10671c1aeef4e3a74 to your computer and use it in GitHub Desktop.
Cool use of subclassing Module in Ruby.
class Pipeline < Module
attr_accessor :transforms
def initialize(*transforms, &implementation)
@transforms = transforms
instance_eval &implementation if block_given?
end
def call(*args, &block)
transformers.reduce(block || default_block) do |pipeline, transformer|
-> *args { transformer.call(*args, &pipeline) }
end.call *args
end
class Before < self
# Use default behavior
end
class After < self
def apply(transformer)
-> *args, &block {
transformer.call *block.call(*args)
}
end
end
class Around < self
# Rely on implementation to yield
def apply(transformer)
transformer
end
end
class << self
def before(*args, &block)
Before.new(*args, &block)
end
def after(*args, &block)
After.new(*args, &block)
end
def around(*args, &block)
Around.new(*args, &block)
end
end
private
def transformers
transforms.reverse.map do |transform|
transformer_for transform
end.compact.map do |transformer|
apply transformer
end
end
def apply(transformer)
-> *args, &block {
block.call *transformer.call(*args)
}
end
def transformer_for(transform)
if transform.respond_to? :call
transform
elsif transform.respond_to? :to_sym
method transform.to_sym
end
end
def default_block
-> *args {
case args.length
when 0; nil
when 1; args.first;
else args; end
}
end
end
require 'pipeline'
#=> true
# Build an empty pipeline with `Pipeline.new`:
Pipeline.new
#=> #<Pipeline:0x007fcf3e0cee40>
# Call it with arguments and a block to evaluate the block with those arguments:
Pipeline.new.call(:foo, :bar) do |first, second|
puts second
end
#=> bar
# End result: a pretty clean and minimal callback chain DSL:
# Call it without a block just echo the result:
Pipeline.new.call #=> nil
Pipeline.new.call(:single_argument) #=> :single_argument
Pipeline.new.call(:multiple, :arguments) #=> [:multiple, :arguments]
# Build a pipeline with several transforms by adding methods:
p1 = Pipeline.new(:increment, :increment, :divide_by_2) do
def increment(num)
num + 1
end
def divide_by_2(num)
num / 2
end
end
# This runs in order:
p1.call(0)
#=> 1 # == (0 + 1 + 1) / 2
# We can reverse the order:
p1 = Pipeline::After.new(:increment, :increment, :divide_by_2) do
def increment(num)
num + 1
end
def divide_by_2(num)
num / 2
end
end
# This runs in reverse order:
p1.call(0)
#=> 2 # == (0 / 2) + 1 + 1)
# Or we can use `around` to decide the order in each method by yielding:
p1 = Pipeline::Around.new(:increment, :increment, :divide_by_2) do
def increment(num)
yield(num + 1)
end
def divide_by_2(num)
yield(num) / 2
end
end
# Transforms can also just be callables, not just method names
def palindromify(string)
yield string + string.reverse[1..-1]
end
Pipeline.new(-> string, &block { block.call string.upcase }, method(:palindromify) ).call("amanaplanac")
#=> "AMANAPLANACANALPANAMA"
# You can add transforms like you would any other method to a module:
module Duplicate
def duplicate(*things)
things * 2
end
end
module SuperDuplicate
def duplicate(*things)
super * 10
end
end
p2 = Pipeline.new(:duplicate) do
extend Duplicate
extend SuperDuplicate
end
p2.call(:thing)
#=> [:thing, :thing, :thing, :thing...]
p2.call(:thing).count
#=> 20 # 1 thing * 2 things * 10 things
# Pipelines are composable, since they're callable
p3 = Pipeline.new(:convert_to_string, p2) do
def convert_to_string(thing)
yield thing.to_s
end
end
p3.call(0)
#=> ["0", "0", "0", "0"...]
p3.call(0).size
#=> 20 # 1 "0" * 2 * 10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment