Skip to content
academia.sh

Lesson 05 / 12

Methods

Receiverless calls live in the Kernel module; method definitions with default values, variadic arguments, and keyword arguments; implicit return; a top-level def is written into Object and stays private.

Published Updated

In this lesson

  • Finds the responder of receiverless calls with method and owner
  • Combines a default value, variadic arguments, and a keyword argument in one method definition
  • Measures that a method defined with a top-level def is owned by Object and stays private
Contents

The previous lesson showed that case sends a message to the branch object when choosing a branch, and it left a question: when format("%03d", 7) or puts "name" is written, there is no receiver in sight. Where does the message go, and who answers it? The concept of a function was introduced in the Programming Fundamentals course (M01/K02); it is not repeated here. This lesson reads Ruby’s methods with a single measure: who is the owner that handles a receiverless call?

Who Handles a Receiverless Call

In the expression format("%03d", 7) there is no dot and no receiver. But we learned in the earlier lessons: in Ruby every call is a message, and a message has a receiver. When no receiver is written, the receiver is the object the code is running in at that moment. At the top level, this object is the main object of the file. The file below uses defined? to ask whether this call is a method, and method and owner to ask who owns it.

# receiverless.rb — the responder of a receiverless call
puts defined?(format("%03d", 7))
puts method(:format).owner
puts method(:format).owner.instance_of?(Class)
puts format("%03d", 7)
method
Kernel
false
007

format is a method, and its owner is Kernel. Kernel is a module, not a Class, so instance_of?(Class) returned false. In this case the classification rule of the course counts the responder as a module. The top-level object of the file also reaches the Kernel methods, so even when no receiver is written, the call finds an owner.

LB12. In this lesson, a “method” is a unit of work that is defined with def or by the language and called by its name. method(:name).owner gives the owner of the method, that is, the class or module where its definition is found. The lookup chain itself (the order in which places are searched) is not measured in this course; only the result, the owner, is printed.

Familiar Calls in Kernel

In the earlier lessons, puts and p were used without any explanation. The file below asks for the owner of all six calls.

# kernel_calls.rb — the owner of receiverless calls
[:puts, :p, :format, :Integer, :raise, :block_given?].each do |name|
  printf("%-14s %s\n", name, method(name).owner)
end
puts           Kernel
p              Kernel
format         Kernel
Integer        Kernel
raise          Kernel
block_given?   Kernel

All six are in Kernel. puts and p write to the screen, format formats, Integer converts, raise raises an exception, and block_given? says whether a method has a block. They all look like “words of the language”, but each is actually an ordinary method with a known owner. This differs from if and while in the previous lesson: those were syntax, and these are messages.

Parameter Kinds and Implicit Return

We write our own method with def. The parameter list can hold required arguments, default values, variadic arguments, and keyword arguments together. Even when return is not written, the method returns the value of its last expression; this is called implicit return.

# parameters.rb — parameter kinds and implicit return
def assemble(name, title = "mr", *extras, city: "north")
  "#{title} #{name} #{extras.size} #{city}"
end

puts assemble("pat")
puts assemble("pat", "dr", 1, 2, city: "south")
p method(:assemble).parameters
mr pat 0 north
dr pat 2 south
[[:req, :name], [:opt, :title], [:rest, :extras], [:key, :city]]

In the first call only name was given; title and city took their default values, and extras stayed empty. In the second call, "dr" overrode the default, 1 and 2 were collected into the extras array as variadic arguments, and city: was given as a keyword argument. parameters labels the kind of each one: req for required, opt for one with a default, rest for variadic, and key for keyword. There is no return in the method; the last expression, the string, was returned. The #{...} spelling inside the string is string interpolation.

Methods That Take a Block

A block can be given to a method call with curly braces or do ... end. The method runs the block with yield and asks whether a block was given with block_given?. The owner of block_given? came out as Kernel two sections earlier: this too is an ordinary method. In a call without a block, the method can return a different value. What happens when a required argument is missing can also be measured.

# block_receiving.rb — yield and block_given?
def repeat(count)
  return :empty unless block_given?
  count.times { |index| yield index }
  :done
end

p repeat(2) { |index| puts index }
p repeat(2)

begin
  repeat
rescue ArgumentError => e
  puts e.class
end
0
1
:done
:empty
ArgumentError

In the call with a block, the block ran twice (0 and 1 were written) and the method returned :done. In the call without a block, block_given? was false, so the method returned early with :empty. The call that did not give the required count argument raised ArgumentError, and only its class was printed. So the interface of a method is not only its parameter list; the block is also part of the call, but it does not appear in the parameter list of the method. If the block is named explicitly in the form &block, it enters the list; the :block label in a later section shows this.

Returning Multiple Values

In Ruby, a method returns a single value, but that value can be an array, and the caller can split the array into pieces in one step. This also works together with implicit return: the last expression of the method is a two-element array in square brackets.

# multiple_returns.rb — a method that returns an array and multiple assignment
def divide(a, b)
  [a / b, a % b]
end

result = divide(17, 5)
p result
quotient, remainder = divide(17, 5)
puts quotient
puts remainder
puts result.class
[3, 2]
3
2
Array

The method returned a single Array. The spelling quotient, remainder = ... unpacks this array into two names; this is not a feature of methods but the spelling of assignment, and it ties back to the syntax observation of the earlier lesson. So the return value of the method and how the caller reads it remain two separate jobs.

Probing the Parameter Kinds

This section measures the parameter kinds. In the method below, seven parameter kinds stand together: required, with a default, splat (*rest), required keyword, optional keyword, keyword collector (**opts), and block taker (&block). The measure is the method describing itself: method(:name).parameters reports the parameter list of the method, and arity reports how many arguments it expects. The axis is the owner; the owner of the defined method is printed too.

# parameter_kinds.rb — parameter kinds and arity
def example(a, b = 2, *rest, c:, d: 4, **opts, &block); end
puts method(:example).parameters.inspect
puts method(:example).arity
def two(a, b); end
puts method(:two).arity
def with_default(a, b = 1); end
puts method(:with_default).arity
puts method(:example).owner
[[:req, :a], [:opt, :b], [:rest, :rest], [:keyreq, :c], [:key, :d], [:keyrest, :opts], [:block, :block]]
-3
2
-2
Object

The parameters output gave each parameter with a kind label: :req is required, :opt has a default, :rest is splat, :keyreq is required keyword, :key is optional keyword, :keyrest is the keyword collector, and :block is the block-taking parameter. The arity values showed three separate cases: 2 for two, which has two required parameters, -2 for with_default, which has one required and one with a default, and -3 for example. A negative number means “not a fixed number of arguments”; there are optional or unbounded parameters. The absolute value is one more than the minimum number of arguments needed: -2 asks for at least one, and -3 asks for at least two. In example, next to the required a there are a required keyword c: and optional parameters, and the result was -3. A required keyword parameter counts as a single extra argument in this calculation. The rule was measured here only on these three methods. On the last line the owner of example came out as Object: the responder of a method defined at the top level is Object.

LB13. The sign of the arity value was read only on these three methods. That the same rule holds for other parameter layouts was not measured.

# key_error.rb — missing and extra keywords, and implicit return
def box(width:, height: 1)
  width * height
end
begin
  box(height: 2)
rescue ArgumentError => e
  puts e.class
end
begin
  box(width: 1, depth: 3)
rescue ArgumentError => e
  puts e.class
end
puts box(width: 3)
def implicit
  42
end
def explicit
  return 42
end
puts implicit == explicit
def early(n)
  return :negative if n < 0
  :positive
end
puts early(-1).inspect
puts early(1).inspect
puts method(:box).owner
ArgumentError
ArgumentError
3
true
:negative
:positive
Object

box(height: 2) was called without the required width: keyword and raised ArgumentError; box(width: 1, depth: 3) raised ArgumentError again because an undefined keyword was given. In both cases only e.class was printed, because the text of an error message may depend on the version. The valid call box(width: 3) gave 3. These three results show that keyword parameters are a contract: a missing one and an extra one are both rejected with the same class. In the second half, implicit return was compared with return: implicit and explicit returned the same 42. return is not redundant; the early(-1) line shows that return ends the method early, and the early(1) line shows that the value of the last expression is the return value. The owner of the method came out as Object for box as well. So the return value is determined in two ways: by the last expression of the method, or by the value given with return.

Holding a Method as an Object

A method can also be turned into an object. The call method(:name) gives the method as an object of the Method class; this object can be called, converted into a block, and asked for its receiver and owner. The question of this section is who a method defined at the top level belongs to, and whether it appears public when asked from the outside. The axis is the owner; we print the receiver and the owner together.

# method_object.rb — holding a method as an object
def square(n)
  n * n
end
m = method(:square)
puts m.class
puts m.call(4)
puts [1, 2, 3].map(&m).inspect
puts m.name.inspect
puts m.receiver == self
puts m.owner
puts send(:square, 3)
puts respond_to?(:square, true)
puts respond_to?(:square)
Method
16
[1, 4, 9]
:square
true
Object
9
true
false

method(:square) gave a Method object. m.call(4) returned 16; because the method is an object, it could be held in a variable and called later. The expression [1, 2, 3].map(&m) converted the method object into a block with & and gave [1, 4, 9]. The name :square is a symbol. m.receiver == self was true: the receiver of a method defined at the top level is the top-level main object, and m.owner came out as Object. This also explains why a receiverless call square(3) works: when no receiver is written, the message goes to the current self object.

The last three lines measure the difference in access. send(:square, 3) gave 9, so sending the message by its name worked. respond_to?(:square, true) printed true, and respond_to?(:square) printed false. The second argument says to count private methods too; the question without an argument counts only public methods, and the top-level method is not among them. So square is defined in the Object class and is not counted among the public methods, but it can be reached with a receiverless call or with send. What a call written with an explicit receiver would give was not measured here. This difference shows that the Object owner and the access level are two separate properties.

LB14. That a top-level method is private was inferred from the respond_to? pair; the access level was not verified with another measurement (such as private_methods).

Splat on the Call Side

The * and ** marks seen in a parameter definition do the opposite on the call side: they expand an array or a hash into the argument list. This section measures both directions together. The axis is the owner; how many arguments a method can expect is printed with arity, and who defined it with owner.

# splat_call.rb — * and ** on the call side
def add_all(*numbers)
  numbers.sum
end
list = [1, 2, 3]
puts add_all(*list)
puts add_all(list.first, *list.last(2))
puts add_all
def options(**o)
  o.inspect
end
h = {a: 1}
puts options(**h)
puts options(b: 2, **h)
def single(x)
  x
end
puts single([1, 2]).inspect
puts method(:add_all).arity
puts method(:add_all).owner
6
6
0
{a: 1}
{b: 2, a: 1}
[1, 2]
-1
Object

add_all(*list) expanded the array into three separate arguments and gave 6. The spelling add_all(list.first, *list.last(2)) combined one single argument with two expanded ones and again came out as 6. The call add_all with no arguments gave 0: a splat parameter accepts an empty array. options(**h) expanded the hash into keyword arguments and gave {a: 1}; options(b: 2, **h) combined an explicitly written key with an expanded key and gave {b: 2, a: 1}. In the merge, the explicitly written key came first.

The contrasting example is the call single([1, 2]): an array given without a star stayed a single argument, and the method returned [1, 2]. So expansion is written on purpose on the call side; where there is no star, an array is an array. method(:add_all).arity gave -1: no fixed number of arguments is expected, and at least zero are needed. The owner came out as Object. The spelling of the language expands the arguments of a call, but what receives the expanded list is still the parameter definition of the method; when the two sides match, the call works. The parameter definition and the call side use the same mark, but their directions are opposite: in a definition * collects the arguments, and in a call * expands the array.

The Bounding Measurement

The responder of this lesson is a module, but there is a slip: does the owner of a method we write ourselves also become a module? When def is written at the top level of a file, where does the method go? The measurement below asks this.

# top_level.rb — a top-level def is written into Object and is private
def square(n)
  n * n
end

puts method(:square).owner
puts method(:square).owner.instance_of?(Class)
puts respond_to?(:square)
puts respond_to?(:square, true)
puts square(4)
Object
true
false
true
16

A top-level def square writes the method into the Object class. Object is an instance of Class (true), so in this case the responder is not a module but a class. The method also stays private: respond_to?(:square) returned false, because private methods are invisible when asked from the outside. respond_to?(:square, true) includes private methods and gives true. The receiverless call square(4) returned 16, because when no receiver is written, private methods are reachable too. The methods in Kernel are owned by a module; our own def is owned by Object, that is, by a class.

Summary

  • In a receiverless call, the receiver is the object the code is running in at that moment; the Kernel module answers the call format("%03d", 7).
  • puts, p, format, Integer, raise, and block_given? are ordinary methods, and the owner of all six is Kernel.
  • A method can carry required arguments, default values, variadic arguments, and keyword arguments together; if return is not written, the last expression is returned.
  • A top-level def writes the method into the Object class and leaves it private; respond_to?(:square) returns false and respond_to?(:square, true) returns true.
  • The responder of this lesson is a module: the owner of receiverless calls is the Kernel module, while the owner of a top-level method we write ourselves is Object, which is a class.
  • method(:name).parameters names the seven parameter kinds in the measured method; a missing or undefined keyword gives ArgumentError; on the call side * and ** expand an array and a hash into arguments.

Next Step

The owners of methods were measured, and we saw where a receiverless call goes. Next come strings: when "name".to_sym is written, a string turns into a symbol, and a symbol is also an object. Once a string turns into a symbol, who answers the message, and does a magic comment change that? The next lesson, Symbols and Strings, measures this.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close