Skip to content

Instantly share code, notes, and snippets.

@parsonsmatt
Last active August 29, 2015 14:23
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 parsonsmatt/c1abbb830b6976566198 to your computer and use it in GitHub Desktop.
Save parsonsmatt/c1abbb830b6976566198 to your computer and use it in GitHub Desktop.
ruby class currying
# gem install 'beethoven'
require 'beethoven'
# Beethoven enables class composition. Unfortunately, the mechanism
# seems to be somewhat limited: each class only takes a single argument
# in the `new` method.
#
# While composing classes in this manner can be powerful, the true
# power is lost without the ability to "curry" this class composition.
#
# The classic example of currying is an adder:
add = -> (x) { -> (y) { x + y } }
add5 = add[5]
add5[8]
# => 13
# This is mostly useful when composing higher order functions.
[1,2,3].map(&add[5])
# => [6, 7, 8]
# What's our end game? Here's a filtering module that should make
# for very nicely composable classes:
module Filterer
def initialize(x)
@records = x.records
end
def records
filter @records
end
def filter(items)
fail NotImplementedError
end
end
# And, for *very* simple things, it works well:
class Over50
include Filterer
def filter(items)
items.select { |x| x > 50 }
end
end
Container = Struct.new(:records)
Over50.new(Container.new([25,50,75])).records
# => 75
# or, compositionally,
(Container | Over50).new([25,50,75]).records
# But immediately, I want to rip out that 50, make it a variable,
# and make it possible to do something like:
# Over.new(50).new(object)
# which would make it easy to put it in a composition pipeline:
#
# Between50And100 = Over.new(50) * LessThan.new(100)
# Between50And100.new([25,75,125]).records
# => [75]
# Composer defines new for instances of itself. Perhaps we can do
# that here. Composer will expect to call `new` on the class itself,
# so we'll need to preempt that by overriding initialize.
class Over
include Filterer
def filter(items)
items.select { |x| x > @bound }
end
def initialize(bound)
@bound = bound
end
def new(x)
@records = x.records
self
end
end
(Container | Over.new(50)).new([25,50,75]).records
# => [75]
# Cool! Let's put that logic in Filterer, so as to DRY up the implementation
# of classes that include it.
module Filterer2
def initialize(bound)
@bound = bound
end
def new(x)
@records = x.records
self
end
def records
filter @records
end
def filter(items)
fail NotImplementedError
end
end
class Under
include Filterer2
def filter(items)
items.select { |x| x < @bound }
end
end
(Container | Over.new(50) | Under.new(100)).new([25,75,125]).records
# => [75]
# So you might store a hash in the database with the following format:
filter = {
Over => 50,
Under => 100
}
# And you might want to dynamically generate a filtering class
# based on that:
CustomFilter = filter.map { |k, v| k.new(v) }.reduce(Container, &:|)
CustomFilter.new([50, 60, 70, 1000]).records
# => [60, 70]
@wallace
Copy link

wallace commented Jun 29, 2015

Very cool.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment