ทำอย่างเราให้ตัวแปลใน hash กลายเป็น local variable

ผมมี def อันนึงที่ถูกบังคับให้รับ hash เข้ามา

def computer(com)
  cpu = com[:cpu]
  ram = com[:ram]
  fan = com[:fan]
 
  ...
end

มีทางอื่นทึ่จะแปลง hash มาเป็น local variable หรือเปล่าครับ

ผมลองไล่ดูคำสั่ง render เพราะน่าจะทำคล้ายกับที่ต้องการ

render :partial => "partial" :locals => {:local => "local"}

ไปเจอแบบนี้ครับ

  1. # File action_controller/base.rb, line 832
  2. def render(options = nil, &block) #:doc:
  3. raise DoubleRenderError, "Can only render or redirect once per action" if performed?
  4.  
  5. if options.nil?
  6. return render_for_file(default_template_name, nil, true)
  7. else
  8.  
  9. ....
  10.  
  11. elsif partial = options[:partial]
  12. partial = default_template_name if partial == true
  13. add_variables_to_assigns
  14.  
  15. if collection = options[:collection]
  16. render_for_text(
  17. @template.send!(:render_partial_collection, partial, collection,
  18. options[:spacer_template], options[:locals]), options[:status]
  19. )
  20. else
  21. render_for_text(
  22. @template.send!(:render_partial, partial,
  23. ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals]), options[:status]
  24. )
  25. end
  26.  
  27. elsif options[:update]
  28.  
  29. ....
  30.  
  31. end

คิดว่ามันเข้ามาที่ render_for_text อันที่สอง แต่ผมหา render_for_text ไม่เจอ :'( ต้องไปหาใน http://noobkit.com

แต่คำตอบของผมน่าจะอยู่ใน ObjectWrapper มากกว่าเลยลองตามไปดู น่าจะอยู่แถวนี้ C:\ruby\lib\ruby\gems\1.8\gems\actionpack-2.0.2\lib\action_view\base.rb

เห็นแล้วเป็นลม + ตกใจ

  class ObjectWrapper < Struct.new(:value) #:nodoc:
  end

เจอ #:nodoc: แถมไม่มีคำสั้งอยู่ใน class นี้ซักบรรทัดเดียว คงต้องตามไปดูที่ Struct ... ผมลองตามหาดูรู้แค่ว่าเป็น Standard lib แต่ไม่รู้มันอยู่ไหน :'(

มันมีวิธีอะไรที่จะเอาค่าใน hash ออกมาเป็น local variable บ้างครับ

เวลาไล่ code ลึกๆอย่างนี้ , grep -R นี่ขาดไม่ได้เลย
ตัว method จริงๆ ที่เป็นคนแปลง Hash ให้เป็น local variables ก็คือ
method "create_template_source" ใน action_view/base.rb

      # Method to create the source code for a given template.
      def create_template_source(handler, template, render_symbol, locals)
        body = delegate_compile(handler, template)
 
        @@template_args[render_symbol] ||= {}
        locals_keys = @@template_args[render_symbol].keys | locals
        @@template_args[render_symbol] = locals_keys.inject({}) { |h, k| h[k] = true; h }
 
        locals_code = ""
        locals_keys.each do |key|
          locals_code << "#{key} = local_assigns[:#{key}]\n"
        end
 
        "def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend"
      end

คำสั่ง delegate_compile จะทำหน้าที่ compile template โดยมันจะดูชนิดของ template ด้วย
เช่น .rhtml ก็ให้ ERB เป็นคน compile
ผลลัพท์ที่ได้ ก็จะเก็บไว้ในตัวแปร body


วิธีการ compile ของ ERB มันจะไปใช้สั่งคำสั่งประมาณนี้

>> template = ERB.new('hello <%=name%>')
=> #<ERB:0x1073be8 @safe_level=nil, @filename=nil, @src="_erbout = ''; _erbout.concat \"hello \"; _erbout.concat((name).to_s); _erbout">
>> template.src
=> "_erbout = ''; _erbout.concat \"hello \"; _erbout.concat((name).to_s); _erbout"

จากนั้น มันก็จะ iterate เจ้า Hash (locals variable) ของเรา เพื่อสร้าง assignment statement
สุดท้าย มันก็จะ return (string) code ที่ทำหน้าที่ render partial นั้นๆให้เรา


code ที่ได้จะถูก compile ด้วยคำสั่ง module_eval อีกที

      # Compile and evaluate the template's code
      def compile_template(handler, template, file_name, local_assigns)
        render_symbol = assign_method_name(handler, template, file_name)
        render_source = create_template_source(handler, template, render_symbol, local_assigns.keys)
        ...
          CompiledTemplates.module_eval(render_source, file_name, -line_offset)
 
        ....
      end

ตัวที่น่าสนใจ ก็คือ definition ของ CompiledTemplates (อยู่ใน base.rb เหมือนกัน)

    module CompiledTemplates #:nodoc:
      # holds compiled template code
    end
    include CompiledTemplates

ตัว CompiledTemplates เป็นแค่ Module เปล่าๆ และถูก include ไว้ใน base.rb

เท่าที่ค้นคว้าดู ใน ruby เราไม่สามารถ dynamic create local variables ได้นะ (create ผ่าน eval ได้ แต่เวลา access ก็ต้อง access ผ่าน eval ด้วยเหมือนกัน)

มีทางอ้อมทางอื่นเช่น dynamic create getter method แบบนี้

class Test
  def initialize(hash)
    hash.each do |k, v|
      self.class.send(:define_method, k, proc { hash[k] })
    end
  end
 
  def render
    puts cpu
  end
end
 
com = {:cpu => 'intel'}
t = Test.new(com)
t.render
apirak's picture

ถ้าผมลองเลียนแบบดู น่าจะเป็นแบบนี้

computer {:cpu => "800MHz", :ram => "256M"}
----
def computer(com)
 
    local_assigns = com
    locals_keys = local_assigns.keys
 
    locals_code = ""
    locals_keys.each do |key|
          locals_code << "#{key} = local_assigns[:#{key}]\n"
    end
 
    eval locals_code
 
    puts cpu if cpu
    puts ram if ram
 
end

สมเป็นภาษา script เท่หลายๆ

ของ apirak, run แล้วไม่ error หรือ
ที่บรรทัด
puts cpu if cpu
ของผมมันจะฟ้อง error

NameError: undefined local variable or method `cpu' for main:Object

เท่าที่ผมทดลอง มันดูเหมือนว่า ตอนที่ compile, คำสั่ง puts มัน binding กับ variable ที่ชื่อ cpu ไปเรียบร้อยแล้ว, การที่เราสั่ง eval ("cpu=xx") ไม่มีผลต่อ ตัวแปร cpu ใน puts

ก็อย่างที่พี่อธิบายไว้ คงต้อง access ผ่าน eval ด้วยเหมือนกัน

def computer(com = nil)
  return false if com.nil?
  begin
    local_keys = com.keys
    local_code = ""
    local_keys.each do |key|
      local_code << %{#{key} = com[:#{key}] \n}
    end
    eval local_code
    eval("p cpu unless cpu.nil?")
    eval("p ram unless ram.nil?")
    return true
  rescue Exception => e
    puts "something wrong!: #{e}"
  end
end
 
com = {:cpu => "800MHz", :ram => "256M"}
computer(com)
apirak's picture

พลาดเลย :p ผมดันทดลองใน irb น่ะครับ คิดว่าคงเคยเรียกใช้ตัวแปร cpu มาก่อนหน้านี้ พอมาเจอ cup ใน eval มันเลยจำได้ไม่หายไปกัน eval

def computer(com)
 
    local_assigns = com
    locals_keys = local_assigns.keys
 
    locals_code = ""
    locals_keys.each do |key|
          locals_code << "#{key} = local_assigns[:#{key}]\n"
    end
 
    cpu,ram = ""
 
    eval locals_code
 
    puts cpu if cpu
    puts ram if ram
 
end
 
com = {:cpu => "800MHz", :ram => "256M"}
computer(com)

แบบนี้ถ้าต้องการใช้ก็ต้องประกาศขึ้นมาล่วงหน้า :( มีทาง eval แล้วมันออกมาข้างนอกหรือเปล่าครับ

ย้าย Codenone

ประกาศย้าย Codenone ไปใช้ Forum ของ Blognone แทนครับ ตามไปตั้งกระทู้ต่อได้ที่ Codenone Forum (รายละเอียดอ่านจากกระทู้ ย้าย Codenone ไปรวมกับ Blognone)

กระทู้เก่าๆ จะย้ายตามไปในภายหลัง ตอนนี้ปิดการโพสต์กระทู้ไว้ เหลือไว้เฉพาะอ้างอิงเท่านั้น