Skip to content

Instantly share code, notes, and snippets.

@kkuchta
Created June 20, 2019 13:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kkuchta/4ba3ef8feb14b9e90e5580b4c3c293ac to your computer and use it in GitHub Desktop.
Save kkuchta/4ba3ef8feb14b9e90e5580b4c3c293ac to your computer and use it in GitHub Desktop.
List comprehension in ruby
require 'pry-byebug'
module ListComprehension
def self.execute(block)
context = OuterBlockContextClass.new
# Set up variables + blocks
context.instance_eval(&block)
# evaluate the comprehension
context.run
end
# Collects variable names and sets
class Variable
attr_reader :name, :set
def initialize(name)
@name = name
end
def <<(set)
@set = set
self
end
end
# Wrapper class to make the pipe syntax work
class PipeTo
def initialize(action)
@action = action
end
def |(*args)
@action[*args]
end
end
# The context in which the comprehension will be evaluated.
class OuterBlockContextClass
# Make variable declarations like `x` and `k` work.
def method_missing(symbol, *args)
Variable.new(symbol)
end
# Declare one of the two proc sections (either condition or map)
def p(&block)
if @map
# must be last section
@condition = block
else
# Must be first section
@map = block
PipeTo.new(proc { |variables| @variables = variables; PipeTo.new(proc{}) })
end
end
# Makes a proc that evaluates `prok` in the context of our @variables
def make_scoped_proc(prok)
proc do |args|
o = Object.new
@variables.zip(args).each do |variable, value|
o.define_singleton_method(variable.name) { value }
end
o.instance_eval(&prok)
end
end
# Run the comprehension, after everything's all set up.
def run
comp(
sets: @variables.map(&:set).map(&:to_a),
condition: make_scoped_proc(@condition),
map: make_scoped_proc(@map)
)
end
# *Atually* run the comprehension.
def comp(sets: [], condition: proc{}, map: proc{})
sets[0]
.product(*sets[1..])
.select(&condition)
.map(&map)
end
end
end
# Shorthand for comprehensions. Todo: maybe find a good operator to overload
# for this instead? Would love to overload the array literal `[]` syntax if I
# can figure out how.
def c(&block)
ListComprehension.execute(block)
end
puts c{ p{ x + k } | [ x << (1...5), k << (1...10) ] | p{ k % x == 0 } }
@kkuchta
Copy link
Author

kkuchta commented Jun 20, 2019

Just a fun experiment in metaprogramming and syntax hacking. This is about as succinct as I could get a list comprehension syntax in ruby. Open to suggestions for improvements!

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