dynamic languages, for software craftmanship group
DESCRIPTION
Reuven Lerner's talk about dynamic programming languages in general, and about Ruby in particular. Why would you want to use a dynamic language? What can you do with one that isn't possible (or easy) with a static language?TRANSCRIPT
Dynamic languagesReuven M. Lerner • [email protected] Craftsmanship Group, Tel Aviv
March 22nd, 2011
1
Who am I?
• Web developer, software architect, consultant, trainer
• Linux Journal columnist since 1996
• Mostly Ruby on Rails + PostgreSQL, but also Python, PHP, jQuery, and lots more...
2
Language wars!
• Static vs. dynamic languages
• I’m here representing the good guys
• (Just kidding. Mostly.)
• Not just a different language — a different mindset and set of expectations
3
What is a dynamic language?
• Dynamic typing
• Usually interpreted (or JIT compiled)
• Interactive shell for experimenting
• Closures (anonymous code blocks)
• Flexible, “living” object model
• Result: Short, powerful, reusable code
4
Who is in charge?
• Static language: The language is in charge, and it’s for your own good!
• Dynamic language: The programmer is in charge, and changes the language to suit his or her needs
5
6
Examples
• Lisp
• Smalltalk
• Python
• Ruby
• JavaScript
7
Examples
• Lisp
• Smalltalk
• Python
• Ruby
• JavaScript
7
Values, not variables, have types
x = 5
=> 5
x.class
=> Fixnum
x = [1,2,3]
=> [1, 2, 3]
x.class
=> Array
8
Values, not variables, have types
x = 5
=> 5
x.class
=> Fixnum
x = [1,2,3]
=> [1, 2, 3]
x.class
=> Array
8
Values, not variables, have types
x = 5
=> 5
x.class
=> Fixnum
x = [1,2,3]
=> [1, 2, 3]
x.class
=> Array
8
Values, not variables, have types
x = 5
=> 5
x.class
=> Fixnum
x = [1,2,3]
=> [1, 2, 3]
x.class
=> Array
8
Values, not variables, have types
x = 5
=> 5
x.class
=> Fixnum
x = [1,2,3]
=> [1, 2, 3]
x.class
=> Array
8
Less code!
• No need for variable declarations
• No function parameter declarations
• No function return-type declarations
9
Ahhhh!
• Are you serious?
• Can real software be developed without a compiler and type checking?
• How can you possibly work this way?
10
Ahhhh!
• Are you serious?
• Can real software be developed without a compiler and type checking?
• How can you possibly work this way?
• Answer: Very well, thank you.
10
11
Flexible collections
a = [1, 2, 'three', [4, 5, 6]]
=> [1, 2, "three", [4, 5, 6]]
a.length
=> 4
a << {first_name:'Reuven', last_name:'Lerner'}
=> [1, 2, "three", [4, 5, 6], {:first_name=>"Reuven", :last_name=>"Lerner"}]
12
Flexible collections
a = [1, 2, 'three', [4, 5, 6]]
=> [1, 2, "three", [4, 5, 6]]
a.length
=> 4
a << {first_name:'Reuven', last_name:'Lerner'}
=> [1, 2, "three", [4, 5, 6], {:first_name=>"Reuven", :last_name=>"Lerner"}]
Integer
12
Flexible collections
a = [1, 2, 'three', [4, 5, 6]]
=> [1, 2, "three", [4, 5, 6]]
a.length
=> 4
a << {first_name:'Reuven', last_name:'Lerner'}
=> [1, 2, "three", [4, 5, 6], {:first_name=>"Reuven", :last_name=>"Lerner"}]
Integer String
12
Flexible collections
a = [1, 2, 'three', [4, 5, 6]]
=> [1, 2, "three", [4, 5, 6]]
a.length
=> 4
a << {first_name:'Reuven', last_name:'Lerner'}
=> [1, 2, "three", [4, 5, 6], {:first_name=>"Reuven", :last_name=>"Lerner"}]
Integer String Array
12
Flexible collections
a = [1, 2, 'three', [4, 5, 6]]
=> [1, 2, "three", [4, 5, 6]]
a.length
=> 4
a << {first_name:'Reuven', last_name:'Lerner'}
=> [1, 2, "three", [4, 5, 6], {:first_name=>"Reuven", :last_name=>"Lerner"}]
Integer String Array
Hash
12
Collections
• Built-in collections (arrays, hashes) are good for most basic data structures
• Only create a class when you need to!
• Learning to use arrays and hashes is a key part of dynamic programming, and can save you lots of time
13
Functional tricks
• collect (map)
• detect (find)
• select (find_all)
• reject
• inject
14
Sorted username list
File.readlines('/etc/passwd').
reject {|line| line =~ /^#/}.
map {|line| line.split(':').
first}.
sort
15
Sorted username list
File.readlines('/etc/passwd').
reject {|line| line =~ /^#/}.
map {|line| line.split(':').
first}.
sort
15
Sorted username list
File.readlines('/etc/passwd').
reject {|line| line =~ /^#/}.
map {|line| line.split(':').
first}.
sort
15
Sorted username list
File.readlines('/etc/passwd').
reject {|line| line =~ /^#/}.
map {|line| line.split(':').
first}.
sort
15
Sorted username list
File.readlines('/etc/passwd').
reject {|line| line =~ /^#/}.
map {|line| line.split(':').
first}.
sort
15
Sorted username list
File.readlines('/etc/passwd').
reject {|line| line =~ /^#/}.
map {|line| line.split(':').
first}.
sort
15
Domain counterdomains = Hash.new(0)
File.readlines(list_member_file).each do |line|
email, domain = line.chomp.split('@')
domains[domain.downcase] += 1
end
domains.sort_by {|d| -d[1] }.
each {|d| puts "#{d[1]} #{d[0]}" }
domains.sort_by {|d| [-d[1], d[0]] }.
each {|d| puts "#{d[1]} #{d[0]}" }
16
Domain counterdomains = Hash.new(0)
File.readlines(list_member_file).each do |line|
email, domain = line.chomp.split('@')
domains[domain.downcase] += 1
end
domains.sort_by {|d| -d[1] }.
each {|d| puts "#{d[1]} #{d[0]}" }
domains.sort_by {|d| [-d[1], d[0]] }.
each {|d| puts "#{d[1]} #{d[0]}" }
16
Domain counterdomains = Hash.new(0)
File.readlines(list_member_file).each do |line|
email, domain = line.chomp.split('@')
domains[domain.downcase] += 1
end
domains.sort_by {|d| -d[1] }.
each {|d| puts "#{d[1]} #{d[0]}" }
domains.sort_by {|d| [-d[1], d[0]] }.
each {|d| puts "#{d[1]} #{d[0]}" }
16
Domain counterdomains = Hash.new(0)
File.readlines(list_member_file).each do |line|
email, domain = line.chomp.split('@')
domains[domain.downcase] += 1
end
domains.sort_by {|d| -d[1] }.
each {|d| puts "#{d[1]} #{d[0]}" }
domains.sort_by {|d| [-d[1], d[0]] }.
each {|d| puts "#{d[1]} #{d[0]}" }
16
Domain counterdomains = Hash.new(0)
File.readlines(list_member_file).each do |line|
email, domain = line.chomp.split('@')
domains[domain.downcase] += 1
end
domains.sort_by {|d| -d[1] }.
each {|d| puts "#{d[1]} #{d[0]}" }
domains.sort_by {|d| [-d[1], d[0]] }.
each {|d| puts "#{d[1]} #{d[0]}" }
16
Domain counterdomains = Hash.new(0)
File.readlines(list_member_file).each do |line|
email, domain = line.chomp.split('@')
domains[domain.downcase] += 1
end
domains.sort_by {|d| -d[1] }.
each {|d| puts "#{d[1]} #{d[0]}" }
domains.sort_by {|d| [-d[1], d[0]] }.
each {|d| puts "#{d[1]} #{d[0]}" }
16
Domain counterdomains = Hash.new(0)
File.readlines(list_member_file).each do |line|
email, domain = line.chomp.split('@')
domains[domain.downcase] += 1
end
domains.sort_by {|d| -d[1] }.
each {|d| puts "#{d[1]} #{d[0]}" }
domains.sort_by {|d| [-d[1], d[0]] }.
each {|d| puts "#{d[1]} #{d[0]}" }
16
Total cost of order
• Total price of an order of books:
total_price = books.
inject(0) do |sum, b|
sum + (b.price * b.quantity)
end
17
Real-time require
require 'jobs/a'require 'jobs/b'require 'jobs/c'
Dir["#{Rails.root}/app/jobs/*.rb"]. each { |file| require file }
18
Real-time require
require 'jobs/a'require 'jobs/b'require 'jobs/c'
Dir["#{Rails.root}/app/jobs/*.rb"]. each { |file| require file }
18
Real-time require
require 'jobs/a'require 'jobs/b'require 'jobs/c'
Dir["#{Rails.root}/app/jobs/*.rb"]. each { |file| require file }
18
Classes
• Classes allow you to abstract behavior
• Classes contain data and methods
• It’s easy and fast to create a class
19
Defining classesclass Person
end
=> nil
p = Person.new
=> #<Person:0x00000102105740>
p.class
=> Person
p.class.ancestors
=> [Person, Object, Wirble::Shortcuts, PP::ObjectMixin, Kernel, BasicObject]
20
Defining classesclass Person
end
=> nil
p = Person.new
=> #<Person:0x00000102105740>
p.class
=> Person
p.class.ancestors
=> [Person, Object, Wirble::Shortcuts, PP::ObjectMixin, Kernel, BasicObject]
20
Defining classesclass Person
end
=> nil
p = Person.new
=> #<Person:0x00000102105740>
p.class
=> Person
p.class.ancestors
=> [Person, Object, Wirble::Shortcuts, PP::ObjectMixin, Kernel, BasicObject]
20
Defining classesclass Person
end
=> nil
p = Person.new
=> #<Person:0x00000102105740>
p.class
=> Person
p.class.ancestors
=> [Person, Object, Wirble::Shortcuts, PP::ObjectMixin, Kernel, BasicObject]
20
Defining classesclass Person
end
=> nil
p = Person.new
=> #<Person:0x00000102105740>
p.class
=> Person
p.class.ancestors
=> [Person, Object, Wirble::Shortcuts, PP::ObjectMixin, Kernel, BasicObject]
20
Reflection> p.methods # or Person.instance_methods
=> [:po, :poc, :pretty_print, :pretty_print_cycle, :pretty_print_instance_variables, :pretty_print_inspect, :nil?, :, :, :!, :eql?, :hash, :=>, :class, :singleton_class, :clone, :dup, :initialize_dup, :initialize_clone, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :freeze, :frozen?, :to_s, :inspect, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variables, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :instance_of?, :kind_of?, :is_a?, :tap, :send, :public_send, :respond_to?, :respond_to_missing?, :extend, :display, :method, :public_method, :define_singleton_method, :__id__, :object_id, :to_enum, :enum_for, :pretty_inspect, :ri, :, :equal?, :!, :!, :instance_eval, :instance_exec, :__send__]
21
Predicates
p.methods.grep(/\?$/)
=> [:nil?, :eql?, :tainted?, :untrusted?, :frozen?, :instance_variable_defined?, :instance_of?, :kind_of?, :is_a?, :respond_to?, :respond_to_missing?, :equal?]
22
“Local” methodsclass Person
def blah
"blah"
end
end
=> nil
p = Person.new
p.class.instance_methods - p.class.superclass.instance_methods
=> [:blah]
23
“Local” methodsclass Person
def blah
"blah"
end
end
=> nil
p = Person.new
p.class.instance_methods - p.class.superclass.instance_methods
=> [:blah]
23
“Local” methodsclass Person
def blah
"blah"
end
end
=> nil
p = Person.new
p.class.instance_methods - p.class.superclass.instance_methods
=> [:blah]
23
“Local” methodsclass Person
def blah
"blah"
end
end
=> nil
p = Person.new
p.class.instance_methods - p.class.superclass.instance_methods
=> [:blah]
23
Calling methods
a.length
=> 5
a.send(:length)
=> 5
a.send("length".to_sym)
=> 5
24
Calling methods
a.length
=> 5
a.send(:length)
=> 5
a.send("length".to_sym)
=> 5
24
Calling methods
a.length
=> 5
a.send(:length)
=> 5
a.send("length".to_sym)
=> 5
24
Calling methods
a.length
=> 5
a.send(:length)
=> 5
a.send("length".to_sym)
=> 5
24
Preferences become method calls
def bid_or_ask
is_ask? ? :ask : :bid
end
trade_amount = net_basis * (end_rate.send(bid_or_ask) - start_rate.send(bid_or_ask))
25
Preferences become method calls
def bid_or_ask
is_ask? ? :ask : :bid
end
trade_amount = net_basis * (end_rate.send(bid_or_ask) - start_rate.send(bid_or_ask))
25
Preferences become method calls
def bid_or_ask
is_ask? ? :ask : :bid
end
trade_amount = net_basis * (end_rate.send(bid_or_ask) - start_rate.send(bid_or_ask))
25
Define similar methods
['preview', 'applet', 'info', 'procedures', 'discuss', 'files', 'history', 'tags', 'family', 'upload', 'permissions'].each do |tab_name|
define_method(
"browse_#{tab_name}_tab".to_sym ) do
render :layout => 'browse_tab'
end
end
26
Define similar methods
['preview', 'applet', 'info', 'procedures', 'discuss', 'files', 'history', 'tags', 'family', 'upload', 'permissions'].each do |tab_name|
define_method(
"browse_#{tab_name}_tab".to_sym ) do
render :layout => 'browse_tab'
end
end
26
Define similar methods
['preview', 'applet', 'info', 'procedures', 'discuss', 'files', 'history', 'tags', 'family', 'upload', 'permissions'].each do |tab_name|
define_method(
"browse_#{tab_name}_tab".to_sym ) do
render :layout => 'browse_tab'
end
end
26
Define similar methods
['preview', 'applet', 'info', 'procedures', 'discuss', 'files', 'history', 'tags', 'family', 'upload', 'permissions'].each do |tab_name|
define_method(
"browse_#{tab_name}_tab".to_sym ) do
render :layout => 'browse_tab'
end
end
26
Define similar methods
['preview', 'applet', 'info', 'procedures', 'discuss', 'files', 'history', 'tags', 'family', 'upload', 'permissions'].each do |tab_name|
define_method(
"browse_#{tab_name}_tab".to_sym ) do
render :layout => 'browse_tab'
end
end
26
Searching by result'a'.what?('A')
"a".upcase == "A"
"a".capitalize == "A"
"a".swapcase == "A"
"a".upcase! == "A"
"a".capitalize! == "A"
"a".swapcase! == "A"
[:upcase, :capitalize, :swapcase, :upcase!, :capitalize!, :swapcase!]
27
Searching by result'a'.what?('A')
"a".upcase == "A"
"a".capitalize == "A"
"a".swapcase == "A"
"a".upcase! == "A"
"a".capitalize! == "A"
"a".swapcase! == "A"
[:upcase, :capitalize, :swapcase, :upcase!, :capitalize!, :swapcase!]
27
Searching by result'a'.what?('A')
"a".upcase == "A"
"a".capitalize == "A"
"a".swapcase == "A"
"a".upcase! == "A"
"a".capitalize! == "A"
"a".swapcase! == "A"
[:upcase, :capitalize, :swapcase, :upcase!, :capitalize!, :swapcase!]
27
Searching by result'a'.what?('A')
"a".upcase == "A"
"a".capitalize == "A"
"a".swapcase == "A"
"a".upcase! == "A"
"a".capitalize! == "A"
"a".swapcase! == "A"
[:upcase, :capitalize, :swapcase, :upcase!, :capitalize!, :swapcase!]
27
Monkey patching!class Fixnum
def *(other)
self + other
end
end
6*5
=> 11
28
Monkey patching!class Fixnum
def *(other)
self + other
end
end
6*5
=> 11
28
Monkey patching!class Fixnum
def *(other)
self + other
end
end
6*5
=> 11
28
Example: “Whiny nils”
• Try to invoke “id” on nil, and Rails will complain
@y.id
RuntimeError: Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
29
Example: RSpec it "should have a unique name" do
c1 = Currency.new(@valid_attributes)
c1.save!
c2 =
Currency.new(
:name => @valid_attributes[:name],
:abbreviation => 'XYZ')
c1.should be_valid
c2.should_not be_valid
end
30
Example: RSpec it "should have a unique name" do
c1 = Currency.new(@valid_attributes)
c1.save!
c2 =
Currency.new(
:name => @valid_attributes[:name],
:abbreviation => 'XYZ')
c1.should be_valid
c2.should_not be_valid
end
30
Example: RSpec it "should have a unique name" do
c1 = Currency.new(@valid_attributes)
c1.save!
c2 =
Currency.new(
:name => @valid_attributes[:name],
:abbreviation => 'XYZ')
c1.should be_valid
c2.should_not be_valid
end
30
method_missing
• If no method exists, then method_missing is invoked, passing the method name, args
31
Dynamic findersReading.find_all_by_longitude_and_device_id(0, 3)
=> [#<Reading id: 46, longitude: #<BigDecimal:24439a8,'0.0',9(18)>, latitude: ... ]
Reading.instance_methods.grep(/long/)
=> []
32
Dynamic findersReading.find_all_by_longitude_and_device_id(0, 3)
=> [#<Reading id: 46, longitude: #<BigDecimal:24439a8,'0.0',9(18)>, latitude: ... ]
Reading.instance_methods.grep(/long/)
=> []
32
Dynamic findersReading.find_all_by_longitude_and_device_id(0, 3)
=> [#<Reading id: 46, longitude: #<BigDecimal:24439a8,'0.0',9(18)>, latitude: ... ]
Reading.instance_methods.grep(/long/)
=> []
32
Hash methods as keys
class Hash
def method_missing(name, *args)
if has_key?(name)
self[name]
end
end
end
33
Exampleh = {a:1, b:2}
=> {:a=>1, :b=>2}
h[:a]
=> 1
h.a
=> 1
h['c'] = 3
=> 3
h.c
=> nil
h[:d] = 4
=> 4
h.d
=> 4
34
Exampleh = {a:1, b:2}
=> {:a=>1, :b=>2}
h[:a]
=> 1
h.a
=> 1
h['c'] = 3
=> 3
h.c
=> nil
h[:d] = 4
=> 4
h.d
=> 4
34
Exampleh = {a:1, b:2}
=> {:a=>1, :b=>2}
h[:a]
=> 1
h.a
=> 1
h['c'] = 3
=> 3
h.c
=> nil
h[:d] = 4
=> 4
h.d
=> 4
34
Exampleh = {a:1, b:2}
=> {:a=>1, :b=>2}
h[:a]
=> 1
h.a
=> 1
h['c'] = 3
=> 3
h.c
=> nil
h[:d] = 4
=> 4
h.d
=> 4
34
Exampleh = {a:1, b:2}
=> {:a=>1, :b=>2}
h[:a]
=> 1
h.a
=> 1
h['c'] = 3
=> 3
h.c
=> nil
h[:d] = 4
=> 4
h.d
=> 4
34
Exampleh = {a:1, b:2}
=> {:a=>1, :b=>2}
h[:a]
=> 1
h.a
=> 1
h['c'] = 3
=> 3
h.c
=> nil
h[:d] = 4
=> 4
h.d
=> 4
34
Exampleh = {a:1, b:2}
=> {:a=>1, :b=>2}
h[:a]
=> 1
h.a
=> 1
h['c'] = 3
=> 3
h.c
=> nil
h[:d] = 4
=> 4
h.d
=> 4
34
Exampleh = {a:1, b:2}
=> {:a=>1, :b=>2}
h[:a]
=> 1
h.a
=> 1
h['c'] = 3
=> 3
h.c
=> nil
h[:d] = 4
=> 4
h.d
=> 4
34
Hash method accessclass Hash
def method_missing(method_name, *arguments)
if has_key?(method_name)
self[method_name]
elsif has_key?(method_name.to_s)
self[method_name.to_s]
else
nil
end
end
end
35
Hash method accessclass Hash
def method_missing(method_name, *arguments)
if has_key?(method_name)
self[method_name]
elsif has_key?(method_name.to_s)
self[method_name.to_s]
else
nil
end
end
end
35
Hash method accessclass Hash
def method_missing(method_name, *arguments)
if has_key?(method_name)
self[method_name]
elsif has_key?(method_name.to_s)
self[method_name.to_s]
else
nil
end
end
end
35
Hash method accessclass Hash
def method_missing(method_name, *arguments)
if has_key?(method_name)
self[method_name]
elsif has_key?(method_name.to_s)
self[method_name.to_s]
else
nil
end
end
end
35
Hash method accessclass Hash
def method_missing(method_name, *arguments)
if has_key?(method_name)
self[method_name]
elsif has_key?(method_name.to_s)
self[method_name.to_s]
else
nil
end
end
end
35
Hash method accessclass Hash
def method_missing(method_name, *arguments)
if has_key?(method_name)
self[method_name]
elsif has_key?(method_name.to_s)
self[method_name.to_s]
else
nil
end
end
end
35
xml.instruct! :xml, :version=>"1.0"xml.rss(:version=>"2.0"){ xml.channel{ xml.title("Modeling Commons updates") xml.link("http://modelingcommons.org/") xml.description("NetLogo Modeling Commons") xml.language('en-us') for update in @updates xml.item do xml.title(post.title) xml.description(post.html_content) xml.author("Your Name Here") xml.pubDate(post.created_on.strftime("%a, %d %b %Y %H:%M:%S %z")) xml.link(post.link) xml.guid(post.link) end end }}
36
xml.instruct! :xml, :version=>"1.0"xml.rss(:version=>"2.0"){ xml.channel{ xml.title("Modeling Commons updates") xml.link("http://modelingcommons.org/") xml.description("NetLogo Modeling Commons") xml.language('en-us') for update in @updates xml.item do xml.title(post.title) xml.description(post.html_content) xml.author("Your Name Here") xml.pubDate(post.created_on.strftime("%a, %d %b %Y %H:%M:%S %z")) xml.link(post.link) xml.guid(post.link) end end }}
36
xml.instruct! :xml, :version=>"1.0"xml.rss(:version=>"2.0"){ xml.channel{ xml.title("Modeling Commons updates") xml.link("http://modelingcommons.org/") xml.description("NetLogo Modeling Commons") xml.language('en-us') for update in @updates xml.item do xml.title(post.title) xml.description(post.html_content) xml.author("Your Name Here") xml.pubDate(post.created_on.strftime("%a, %d %b %Y %H:%M:%S %z")) xml.link(post.link) xml.guid(post.link) end end }}
36
xml.instruct! :xml, :version=>"1.0"xml.rss(:version=>"2.0"){ xml.channel{ xml.title("Modeling Commons updates") xml.link("http://modelingcommons.org/") xml.description("NetLogo Modeling Commons") xml.language('en-us') for update in @updates xml.item do xml.title(post.title) xml.description(post.html_content) xml.author("Your Name Here") xml.pubDate(post.created_on.strftime("%a, %d %b %Y %H:%M:%S %z")) xml.link(post.link) xml.guid(post.link) end end }}
36
xml.instruct! :xml, :version=>"1.0"xml.rss(:version=>"2.0"){ xml.channel{ xml.title("Modeling Commons updates") xml.link("http://modelingcommons.org/") xml.description("NetLogo Modeling Commons") xml.language('en-us') for update in @updates xml.item do xml.title(post.title) xml.description(post.html_content) xml.author("Your Name Here") xml.pubDate(post.created_on.strftime("%a, %d %b %Y %H:%M:%S %z")) xml.link(post.link) xml.guid(post.link) end end }}
36
xml.instruct! :xml, :version=>"1.0"xml.rss(:version=>"2.0"){ xml.channel{ xml.title("Modeling Commons updates") xml.link("http://modelingcommons.org/") xml.description("NetLogo Modeling Commons") xml.language('en-us') for update in @updates xml.item do xml.title(post.title) xml.description(post.html_content) xml.author("Your Name Here") xml.pubDate(post.created_on.strftime("%a, %d %b %Y %H:%M:%S %z")) xml.link(post.link) xml.guid(post.link) end end }}
36
Module includesmodule MyStuff
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def foo; end
end
end
37
Module includesmodule MyStuff
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def foo; end
end
end
37
Module includesmodule MyStuff
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def foo; end
end
end
37
Module includesmodule MyStuff
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def foo; end
end
end
37
Thanks!(Any questions?)
• Call me: 054-496-8405
• E-mail me: [email protected]
• Interrupt me: reuvenlerner (Skype/AIM)
38