Last active
August 29, 2015 14:23
-
-
Save parsonsmatt/c1abbb830b6976566198 to your computer and use it in GitHub Desktop.
ruby class currying
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Very cool.