Skip to content

Instantly share code, notes, and snippets.

@Jeff-Russ
Last active May 7, 2016 21:05
Show Gist options
  • Save Jeff-Russ/036becc52ae20c1b68adf74dd131c745 to your computer and use it in GitHub Desktop.
Save Jeff-Russ/036becc52ae20c1b68adf74dd131c745 to your computer and use it in GitHub Desktop.
Ruby: chained vs nested methods
#!/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'
#!/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.
Ruby: chained vs nested methods
#!/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.
@Jeff-Russ
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment