Skip to content

Instantly share code, notes, and snippets.

@nilium
Last active August 29, 2015 13:56
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 nilium/9152452 to your computer and use it in GitHub Desktop.
Save nilium/9152452 to your computer and use it in GitHub Desktop.
Fun little key path thing for Ruby.
#
# Key path constructor object. Builds up a key path for most instance methods
# called against itself.
#
class KeyPathCtor
def self.forwarding_method(name, old_name = nil)
old_name ||= name
define_method(name, -> (*args) do
if args.empty?
method_missing(old_name)
else
method_missing(-> (obj) { obj.__send__(name, *args) })
end
end)
end
def initialize(paths = nil, &block)
@paths = paths
@defined = !paths.nil?
instance_exec(&block) if block
end
forwarding_method :object_id
forwarding_method :type, :class
forwarding_method :nil?
forwarding_method :frozen?
forwarding_method :hash
forwarding_method :inspect
forwarding_method :to_s
forwarding_method :singleton_type, :singleton_class
forwarding_method :tainted?
forwarding_method :untrusted?
def this(&block)
kp = method_missing(::KeyPath::SelfKey)
kp = kp.method_missing(block) if block
kp
end
def map(&block)
raise ArgumentError, "No block given" unless block
this(
&-> (obj) do
if obj.respond_to?(:map)
obj.map(&block)
else
block[obj]
end
end
)
end
def select(&block)
raise ArgumentError, "No block given" unless block
this(
&-> (obj) do
if obj.respond_to?(:select)
obj.select(&block)
elsif block[obj]
obj
end
end
)
end
alias_method :filter, :select
def reject(&block)
raise ArgumentError, "No block given" unless block
this(
&-> (obj) do
if obj.respond_to?(:reject)
obj.reject(&block)
elsif !block[obj]
obj
end
end
)
end
# Operators
def +(x)
method_missing(-> (obj) { obj + x })
end
def *(x)
method_missing(-> (obj) { obj * x })
end
def **(x)
method_missing(-> (obj) { obj * x })
end
def /(x)
method_missing(-> (obj) { obj / x })
end
def -(x)
method_missing(-> (obj) { obj - x })
end
def %(x)
method_missing(-> (obj) { obj % x })
end
def <=>(x)
method_missing(-> (obj) { obj <=> x })
end
def >=(x)
method_missing(-> (obj) { obj >= x })
end
def <=(x)
method_missing(-> (obj) { obj <= x })
end
def >(x)
method_missing(-> (obj) { obj > x })
end
def <(x)
method_missing(-> (obj) { obj < x })
end
def ===(x)
method_missing(-> (obj) { obj === x })
end
def ==(x)
method_missing(-> (obj) { obj == x })
end
def !=(x)
method_missing(-> (obj) { obj != x })
end
def =~(x)
method_missing(-> (obj) { obj =~ x })
end
def !~(x)
method_missing(-> (obj) { obj !~ x })
end
def !
method_missing(-> (obj) { !obj })
end
def +@
method_missing(-> (obj) { +obj })
end
def -@
method_missing(-> (obj) { -obj })
end
def <<(x)
method_missing(-> (obj) { obj << x })
end
def >>(x)
method_missing(-> (obj) { obj >> x })
end
def &(x)
method_missing(-> (obj) { obj & x })
end
def |(x)
method_missing(-> (obj) { obj | x })
end
def ^(x)
method_missing(-> (obj) { obj ^ x })
end
def ~(x)
method_missing(-> (obj) { obj ~ x })
end
def method_missing(msg, *args, &block)
if (!args.empty? || block) && msg.kind_of?(Symbol)
name = msg
if block
msg = -> (obj) { obj.__send__(name, *args, &block) }
else
msg = -> (obj) { obj.__send__(name, *args) }
end
end
if @paths && @defined
@paths << msg unless msg == ::KeyPath::SelfKey && @paths.last == ::KeyPath::SelfKey
elsif @paths && !@defined
raise "Cannot define multiple paths"
elsif !@paths
@paths = [msg]
end
self.__send__(:class).new(@paths)
end
# Subscript operator (subscript-assign not provided)
def [](*args)
method_missing(::KeyPath::Subscript.new(*args))
end
end
#
# Key path class -- can be initialized with any set of valid keys (see
# PATH_TYPES for valid types) and applied to any object. Provides kp[obj]
# as a convenience method for kp.apply(obj).
#
# May be constructed using the global keypath function below.
#
class KeyPath
class Subscript
attr_reader :args
def initialize(*args)
@args = args.freeze
end
end
module SelfKey ; end
PATH_TYPES = [
::KeyPath::Subscript,
::KeyPath::SelfKey,
Symbol,
Proc
].freeze
def initialize(path)
unless path.kind_of?(Array) && path.all? do |k|
PATH_TYPES.any? { |c| k.kind_of?(c) || k == c }
end
raise ArgumentError, "Invalid path"
end
@path = path.freeze
end
def to_proc
kp = self
-> (obj) { kp.apply(obj) }
end
def apply(object)
result = object
index = 0
keys = @path
len = keys.length
while index < len
key = keys[index]
no_enum = (key == SelfKey)
if no_enum
index += 1
break unless index < len
key = keys[index]
end
result =
case key
when Subscript then apply_subscript_key(result, key, no_enum)
when Proc then apply_proc_key(result, key, no_enum)
else apply_key(result, key, no_enum)
end
index += 1
end
result
end
alias_method :[], :apply
private
def apply_key(object, key, no_enum)
case
when no_enum || !object.respond_to?(:map)
object.__send__(key)
when object.kind_of?(Array) || object.include?(Enumerable) || object.respond_to?(:map)
r = object.map(&key)
if r.kind_of?(Array)
if r.respond_to?(:flatten!)
r.flatten!(1)
elsif r.respond_to?(:flatten)
r = r.flatten(1)
end
end
r
end
end
def apply_subscript_key(object, key, no_enum)
args = key.args
case
when no_enum || !object.respond_to?(:map)
object[*args]
when object.kind_of?(Array) || object.include?(Enumerable) || object.respond_to?(:map)
r = object.map { |item| item[*args] }
if r.kind_of?(Array)
if r.respond_to?(:flatten!)
r.flatten!(1)
elsif r.respond_to?(:flatten)
r = r.flatten(1)
end
end
r
end
end
def apply_proc_key(object, proc, no_enum)
case
when no_enum || !object.respond_to?(:map)
proc[object]
when object.kind_of?(Array) || object.include?(Enumerable) || object.respond_to?(:map)
r = object.map(&proc)
if r.kind_of?(Array)
if r.respond_to?(:flatten!)
r.flatten!(1)
elsif r.respond_to?(:flatten)
r = r.flatten(1)
end
end
r
end
end
end
class Object
#
# Object extension for compiling and running a key path against the receiver.
#
def value_of(kp = nil, &block)
if kp && block_given?
raise ArgumentError, "Must provide either a keypath or a block -- not both"
elsif kp.nil? && !block
raise ArgumentError, "No block given"
end
if block
keypath(&block)[self]
else
kp[self]
end
end
end
#
# Creates a key path and returns it. If an object is provided, it will apply
# the resulting key path to the object and return the result.
#
def keypath(obj = nil, &block)
kp = KeyPath.new(KeyPathCtor.new(nil, &block).instance_variable_get(:@paths))
if obj
kp[obj]
else
kp
end
end
#=============================================================================
# TEST CODE #
#=============================================================================
# A
kp = keypath { (self ** 2).select { |k| k < 5 } }
puts kp[[1, 2, 3, 4]].inspect
# B
puts [1, 2, 3, 4].value_of(kp).inspect
# C
puts [1, 2, 3, 4].value_of { (self ** 2).select { |k| k < 5 } }.inspect
puts [:foo, :bar, :baz].value_of { ((hash ** 2) & 0xFF).this[1] }
nesting = {
a: { e: { i: 1, m: 2 }, q: { u: 3, y: 4 }},
b: { f: { j: 5, n: 6 }, r: { v: 7, z: 8 }},
c: { g: { k: 9, o: 10 }, s: { w: 11, aa: 12 }},
d: { h: { l: 13, p: 14 }, t: { x: 15, ab: 16 }}
}
puts nesting.value_of { this.values.values.each_key.to_a.this.sort { |a, b| a <=> b }.to_s.rjust(2, ' ') }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment