6

Given some local variables, what would be the easiest way to compact them in Ruby?

def foo
  name = 'David'
  age = 25
  role = :director
  ...
  # How would you build this:
  # { :name => 'David', :age => 25, :role => :director }
  # or
  # { 'name' => 'David', 'age' => 25, 'role' => :director }
end

In PHP, I can simply do this:

$foo = compact('name', 'age', 'role');
Andrew Marshall
  • 95,083
  • 20
  • 220
  • 214
Misha Moroshko
  • 166,356
  • 226
  • 505
  • 746
  • 2
    http://stackoverflow.com/questions/6589894/access-local-variables-from-a-different-binding-in-ruby – just somebody Jul 02 '13 at 10:29
  • 3
    I am honestly baffled about PHP allowing such aberra..., mmm, unorthodox code. Does `compact` really take local variables by name and access them from within another function? – tokland Jul 02 '13 at 14:01

2 Answers2

9

I came up with a significant improvement on my original answer. It is cleaner if you inherit from Binding itself. The to_sym is there because older versions of ruby have local_variables as strings.

instance method

class Binding
  def compact( *args )
    compacted = {}
    locals = eval( "local_variables" ).map( &:to_sym )
    args.each do |arg|
      if locals.include? arg.to_sym
        compacted[arg.to_sym] = eval( arg.to_s ) 
      end 
    end 
    return compacted
  end
end

usage

foo = "bar"
bar = "foo"
binding.compact( "foo" ) # => {:foo=>"bar"}
binding.compact( :bar ) # => {:bar=>"foo"}

Original answer

This is the nearest I could get to a method that behaves like Php's compact -

method

def compact( *args, &prok )
  compacted = {}
  args.each do |arg|
    if prok.binding.send( :eval, "local_variables" ).include? arg
      compacted[arg.to_sym] = prok.binding.send( :eval, arg ) 
    end
  end 
  return compacted
end

example usage

foo = "bar"
compact( "foo" ){}
# or
compact( "foo", &proc{} )

Its not perfect though, because you have to pass a proc in. I am open to suggestions about how to improve this.

Bungus
  • 592
  • 2
  • 11
2

This is a variant on Bungus' answer, but here's a one-liner that is decidedly uglier, but doesn't extend Binding or anything:

foo = :bar
baz = :bin
hash = [:foo, :baz].inject({}) {|h, v| h[v] = eval(v.to_s); h }
# hash => {:baz=>:bin, :foo=>:bar}

You can also make it look kinda-sorta like a method call by abusing a block binding - again, a variant on Bungus' original answer:

module Kernel
  def compact(&block)
    args = block.call.map &:to_sym
    lvars = block.binding.send(:eval, "local_variables").map &:to_sym
    (args & lvars).inject({}) do |h, v|
      h[v] = block.binding.send(:eval, v.to_s); h
    end
  end
end

foo = :bar
baz = :bin
compact {[ :foo, :bar, :baz ]}
# {:foo=>:bar, :baz=>:bin}

(I'll just tell myself that {[..]} is the trash compactor symbol.)

If you use the binding_of_caller gem, you can forgo the proc and explicit binding all together:

require 'binding_of_caller'
module Kernel
  def compact(*args)
    lvars = binding.of_caller(1).send(:eval, "local_variables").map &:to_sym
    (args.map(&:to_sym) & lvars).inject({}) do |h, v|
      h[v] = binding.of_caller(2).send(:eval, v.to_s); h
    end
  end
end

foo = :bar
baz = :bin
compact :foo, :bar, :baz
# {:foo=>:bar, :baz=>:bin}

Be warned, it's slow. In production code, you should probably never attempt to do this and instead just keep a hash of values so the programmer who has to maintain this after you doesn't hunt you down and kill you in your sleep.

Chris Heald
  • 61,439
  • 10
  • 123
  • 137