Last active
May 7, 2016 21:05
-
-
Save Jeff-Russ/036becc52ae20c1b68adf74dd131c745 to your computer and use it in GitHub Desktop.
Ruby: chained vs nested methods
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# chainable.rb | |
module Chainable | |
def self.inner | |
puts "inner" | |
return self | |
end | |
def self.outer | |
puts "outer" | |
return self | |
end | |
def self.arg arg | |
puts "arg == '#{arg}'" | |
return self | |
end | |
def self.opt arg=nil | |
puts "opt == '#{arg}'" | |
return self | |
end | |
end | |
# Here we have a solution in module form, but it wuold be similar as a class. | |
Chainable.inner.outer | |
# prints: | |
# inner | |
# outer | |
# Just for giggles, I added `return self` to outer as which means this works too: | |
Chainable.outer.inner | |
# prints: | |
# outer | |
# inner | |
# You'll see there's another method that takes and arg, let's chain that on: | |
Chainable.inner.arg "YO" | |
# prints: | |
# inner | |
# arg == 'YO' | |
# This means the argument is unused by .inner and waits until .arg is called. | |
# If it didn't, inner would trow an error for get an argument it didn't want. | |
# Flip to Chainable.inner.arg "YO" and you'll get an error. | |
# All of this means we can't have the inner method take a arg unless it defualt to nil: | |
Chainable.opt.arg "YO" | |
# prints: | |
# opt == | |
# arg == 'YO' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# nestdef.rb | |
# The following example, where the actual keyword `def` is nested is not | |
# recommended. `def nested` is ran only when `container` is called. This | |
# example is not in a class or module (for no particular reason), but could be. | |
def container arg=nil | |
puts "container method called #{arg}" | |
unless arg.nil? | |
puts " with '#{arg}'" | |
else | |
def nested | |
puts "nested method called" | |
end | |
end | |
end | |
container.nested | |
=begin prints: | |
container method called | |
nested method called | |
although you would think nested is only available via container.nested you | |
are wrong. You would be right if the same code was in a class or module, though. | |
=end | |
nested # => "nested method called" | |
=begin | |
Okay let's test it's argument accepting abilities. The following should have | |
.container take the argument and then return the Object for .nexted to call, | |
without any argument but it FAILS miserably. | |
=end | |
container.nested "HELLO" | |
=begin prints: | |
container method called | |
./nestdef.rb in `nested': wrong number of arguments (1 for 0) (ArgumentError) | |
If you add some debugging you'll see that first, .conatainer is called WITHOUT | |
being passed the argument and, since it's nil, `def nested` is executed, .container | |
finished and then Ruby send the "HELLO" argument to .nested. | |
Inner method is not really "owned" by container but it's creation is dependent on it! | |
If you comment out the calls above and run this you will see an error | |
=end | |
nested # ERROR | |
container.nested # this should fix it | |
nested # ERROR, again? | |
nested # ERROR. | |
=begin | |
This entire example can be rewritten like this: | |
def container arg=nil | |
puts "container method called #{arg}" | |
unless arg.nil? | |
puts " with '#{arg}'" | |
else | |
define_singleton_method "nested" do | |
puts "nested method called" | |
end | |
end | |
end | |
which is designed for dynamic creating of methods. There is also `define_method` | |
which would not work in a class or module. In any case, you'd get the same | |
behaivors as you would with `def` and probably similar performance. | |
WHAT DID WE LEARN? | |
Defining methods separately in a manner that makes them chainable unless you want | |
the chained (furthest right) method to have at the argument. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Ruby: chained vs nested methods |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# unchainable.rb | |
class Unchainable | |
def a | |
puts "a" | |
end | |
def b | |
puts "b" | |
end | |
end | |
nochain = Unchainable.new | |
nochain.a # prints 'a' | |
nochain.a.b # ERROR: undefined method `b' for nil:NilClass (NoMethodError) | |
# What is nil here? The method .b is supposed to be called on the nochain object! | |
# Weeeell the way chained methods work by executing the leftmost one and then | |
# it's return is the object to with the next it called on. | |
# Let's see what nochain.a is returning to .b | |
puts nochain.a # "a" | |
puts nochain.a.class # "NilClass" | |
puts (nochain.a).class # "NilClass" | |
# If you think about it, the above is the same as: | |
puts (puts "a").class # "NilClass" | |
# If we changed this to a module with self.a and self.b we would get the same. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Exploring the wild (often unrecommended) world of chaining method calls in Ruby and how they are defined. Placing a method definition inside another has unexpected and often unwanted behaviors but it is possible. There is also a better option to create chainable methods without nesting them while being defined.