Friday, March 02, 2007

Ruby 'require'

คำสั่งง่ายๆคำสั่งหนึ่งที่หลายคน (รวมผมด้วย) มองข้ามไปก็คือ require
คำว่ามองข้าม หมายถึง ใช้พอเป็น, แต่่ไม่สนใจกลไกของมัน
ถ้ามีปัญหาก็มั่วอย่างเดียว จนกว่าจะใช้ได้

วันนี้ก็เลยนั่งสะสาง ความเข้าใจเกี่ยวกับเจ้า require

require คือกลไกการ include source code อย่างหนึ่งของ ruby
มีคำสั่งที่เหมือนกันอีกคำสั่งหนึ่งก็คือ load
สิ่งที่ต่างกันก็คือ ถ้าสั่ง require ซ้ำๆกัน
มันจะ load แค่ครั้งแรกครั้งเดียว

ลองสร้าง file p.rb ที่ current directory
a = 1
b = 2
@c = 3
def d
"hi"
end

จากนั้นก็เรียก irb แล้วสั่ง
$ irb
>> require 'p'
=> true
>> require 'p' # load ครั้งที่ 2 return false เพราะถือว่า load ไปแล้ว
=> false

เงื่อนไขอย่างหนึ่งของการ require ก็คือ
local variable ที่อยู่ใน p.rb จะไม่ตามมาด้วย
>> require 'p'
=> true
>> a
NameError: undefined local variable or method `a' for #
from (irb):3
>> b
NameError: undefined local variable or method `b' for #
from (irb):4
>> @c
=> 3
>> d
=> "hi"


คำถามถัดมาก็คือ แล้วเวลาสั่ง require 'p'
มันมองหา file ที่ชื่อ p.rb ที่ไหนบ้าง,
ruby มีตัวแปร array อยู่ตัวหนึ่งที่ชื่อ $LOAD_PATH
ซึ่งตัวแปรนี้มี short-name อีกชื่อหนึ่งคือ $:
(คนไม่ชอบ perl เห็นแล้วคงร้องยี้)
>> $LOAD_PATH
=> ["/usr/local/lib/ruby/gems/1.8/gems/wirble-0.1.2/bin",
"/usr/local/lib/ruby/gems/1.8/gems/wirble-0.1.2/.",
"/usr/local/lib/ruby/site_ruby/1.8",
"/usr/local/lib/ruby/site_ruby/1.8/powerpc-darwin8.7.0",
"/usr/local/lib/ruby/site_ruby", "/usr/local/lib/ruby/1.8",
"/usr/local/lib/ruby/1.8/powerpc-darwin8.7.0", "."]
>> $:
=> ["/usr/local/lib/ruby/gems/1.8/gems/wirble-0.1.2/bin",
"/usr/local/lib/ruby/gems/1.8/gems/wirble-0.1.2/.",
"/usr/local/lib/ruby/site_ruby/1.8",
"/usr/local/lib/ruby/site_ruby/1.8/powerpc-darwin8.7.0",
"/usr/local/lib/ruby/site_ruby",
"/usr/local/lib/ruby/1.8",
"/usr/local/lib/ruby/1.8/powerpc-darwin8.7.0", "."]
>>

ดังนั้นถ้าเราอยาก add load path ส่วนตัวของเรา
เราก็สามารถ append path เข้าไปใน ตัวแปรนี้ได้เลย
>> $LOAD_PATH << '/tmp' 


ที่นี้ลองไปดูที่ rails บ้าง
rails เตรียม custom configuration ให้เรา set load_path
ไว้ใน file $RAILS_ROOT/config/environment.rb
Rails::Initializer.run do |config|
config.load_paths += %W( #{RAILS_ROOT}/extras )
end

การทำงานภายใน ก็คือมันไปเรียกใช้ตัวแปร $LOAD_PATH
แต่แทนที่จะ append ไปข้างหลัง $LOAD_PATH array
มันใช้วิธีไส่ไว้ข้างหน้าแทน เพราะกลไกการหามันจะเริ่มจาก
หน้าไปหลัง การใส่ไว้ข้างหน้า ทำให้สามารถ override file ข้างหลังได้
# Set the <tt>$LOAD_PATH</tt> based on the value of
# Configuration#load_paths. Duplicates are removed.
def set_load_path
configuration.load_paths.reverse.each { |dir| $LOAD_PATH.unshift(dir) if File.directory?(dir) }
$LOAD_PATH.uniq!
end

Related link from Roti

3 comments:

Anonymous said...

ถ้าเข้าใจไม่ผิด LOAD_PATH มันจะเหมือน sys.path ใน python ใช่หรือเปล่าครับ

ประมาณนี้

>> import sys
>> print '\n'.join(sys.path)
...
...
...

>> sys.path.insert(0, 'c:\\temp')

require ใน ruby คือ import ใน python หรือเปล่าครับ ?

PPhetra said...

LOAD_PATH กับ sys.path น่าจะเหมือนกันครับ
แต่ import ของ python
มี semantic มากกว่า require ของ ruby ครับ

เพราะในแง่ของ ruby แล้ว
มันเป็นเหมือนการ include source code เข้ามาเฉยๆ ไม่มีประเด็นเรื่อง namespace หรือ module อะไรเลย

ของ python เข้าใจว่ามีการ binding แล้วก็
initialize callback ด้วย

ziddik::zdk said...

on ruby 1.8.4 (2005-12-24) [i486-linux]

module HelloModule

Version = "1.0"

def hello(name)

puts("Hello, #{name}. ")

end


module_function :hello


end


class Hello

include HelloModule
end
h = Hello.new

h.hello("Ruby")

I got this error
module1.rb:19: private method `hello' called for #Hello:0xb7cd19c0 (NoMethodError)

Is this ruby 1.8.4 bug?