Skip to content
academia.sh

Lesson 01 / 12

The Design Philosophy of Ruby

In Ruby, 5 + 3, x = 5, the map of an array, and 5.between?(1, 10) are written the same way, yet the code that produces each result lives in four different places. This lesson measures those four responders with twelve observations and shows that a design that puts readability first turns even operators into methods.

Published Updated

In this lesson

  • Shows by running code that operators and readable method names work through the same mechanism, a message
  • Classifies by rule whether a message is answered by a class, a module, a block, or syntax
  • Distinguishes a module's ability from the method in the receiver's class that it relies on, using an owner measurement
Contents

M12 closed with PHP. There we measured which channel a question is heard on, where a change reaches, and whether a part runs within a request. The Ruby curriculum starts from a different question. 5 + 3, x = 5, [1, 2, 3].map { ... }, and 5.between?(1, 10) look the same on screen: a value, a dot or an operator, a name. Yet the code that produces the result of these four expressions lives in four different places. This course measures each mechanism with a single question: who answers this message?

A message is a name and arguments sent to an object; the object that receives it is called the receiver, and the code that runs in response is called a method. The Programming Fundamentals course (M01/K02) established the concepts of variable, type, and function; what is new here is the mechanism through which Ruby provides them. This lesson builds the vocabulary of the mechanism and the core of the measurement, and the next five lessons each take a slice from the same core.

The Four Responders

There are four places where the code that handles a message can be found. These four names do not change throughout the course; the name of the category itself is responder. Because the word “class” collides with Ruby’s Class, the measurement category is written as “the class responder”; a bare “class” means only the Ruby class.

Responder Definition
class The method that handles the message is defined in a class the receiver belongs to
module The method is defined in a module and has been mixed into the class
block The method relies on the block the caller supplies; the same message gives a different result with a different block
syntax There is no message; the language itself processes the expression

This distinction looks like a question from earlier curricula but is not the same. M08/K03 asked whose word a behavior is: whether the language, a library, or the programmer promises it. Here no promise is sought, only code; what is measured is not the party that promises the behavior but the definition that handles the message at that moment.

LB1. In this lesson, the “responder” is the place where the definition of the method that handles the message is found. The order in which a method is searched for and found is not measured in this course; only the result, that is, the owner, is measured. The lookup order is the subject of the next course in the curriculum.

The Readability Priority

Ruby’s design choice is to put the readability of code like a sentence ahead of operator economy. This choice is most visible in named methods: 5.between?(1, 10) reads exactly like a question. The second consequence is less visible: operators are methods too. The file below gives the same sum in three spellings.

# operators.rb — shows that operators are methods too
puts 1 + 2
puts 1.+(2)
puts 5.send(:+, 3)
puts 5.between?(1, 10)
puts 5.clamp(1, 3)
3
3
8
true
3

1 + 2 and 1.+(2) give the same result because the first is the short spelling of the second: receiver 1, message +, argument 2. send takes the name of the message as a symbol, and 5.send(:+, 3) sends the same message. So although a spelling difference remains between 5 + 3, x.upcase, and 5.between?(1, 10), no difference in mechanism remains: all three are messages sent to a receiver. This is the intuition the later lessons rest on; in a language where every operation is a message, the question “who answers” becomes meaningful.

LB2. The measurements were run on Ruby 4.0.7. Which class or module a method is defined in can shift from version to version; the owner names written in the lesson are the result of this version, not a promise of the language.

Two Spellings of the Same Job

The readability choice shows up not only in method names but also in the fact that the same job has more than one spelling. In Ruby there is no single way to do a job, and the spelling the author chooses determines how clearly the job’s purpose is stated to the reader. The two files below ask the measurable part of this choice: do two spellings of the same job differ only in appearance, or do they call a different method? The axis of measurement is the responder; where possible, the owner handling each spelling’s message is printed next to the output.

# two_spellings.rb — two spellings of the same job and their responders
list = [4, 5, 6]
counter = 0
list.each { |n| counter += n }
puts counter
puts list.sum
puts list.method(:each).owner
puts list.method(:sum).owner
x = 7
puts "small" unless x > 10
puts "small" if !(x > 10)
15
15
Array
Array
small
small

The counter spelling and sum gave the same total, 15. The responder of both turned out to be Array: each and sum are both defined in the array’s class. So the difference between the two spellings is not in the responder but in the reading. In the counter spelling, opening a variable, writing a block, and updating the variable inside the block are read together; in the sum spelling, the intent is stated with a single name. The unless and the negated if printed the same line twice. Here the responder question carries a different answer: unless and if are keywords, not methods; so the responder of these two lines is counted as the syntax of the language. The x > 10 inside the condition is a message; its owner was not printed in this file. The negation operator in the negated spelling is also a separate message and does not appear in the unless spelling; its owner was not measured here either. Turning a negative condition around and writing it with a negated if also works, but the reader has to combine two separate operations in the head, first the comparison, then the negation.

# parentheses.rb — calls with and without parentheses are the same message
items = [3, 1, 2]
a = items.push 9
b = items.push(9)
puts a.equal?(b)
puts items.size
puts items.method(:push).owner
puts items.sort.first
puts items.sort().first()
puts method(:puts).owner
true
5
Array
1
1
Kernel

The parenthesis-free items.push 9 and the parenthesized items.push(9) returned the same array: equal? gave true. Both calls returned the same object, and the array grew twice, reaching 5 elements. The responder of the push message is Array. sort.first and sort().first() gave the same value 1, and the owner of the puts call turned out to be Kernel. Neither of the two spelling pairs is a separate mechanism: parentheses only mark where the arguments end, and change neither the receiver nor the message. The rule that comes out of this measurement is: in Ruby, readability is a freedom in the author’s hands, and the freedom does not change the mechanism. The spelling chosen to do the same job should make the reader’s work easier, not the machine’s.

LB3. Which spelling reads faster was not measured. What was measured is only that the two spellings gave the same result and that the responder did not change; the readability judgment depends on a team’s spelling preference and is not a claim of this course.

The Limit of Readability

The readability priority grants a freedom, but not every freedom can be written without changing the message. The question of this section is: where the spelling changes, does the message change too? When two spellings are placed side by side, sometimes the message stays the same, and sometimes a spelling difference corresponds to a separate message. The measure is the responder and the result; the file prints the owners of four messages it uses.

# spelling_limit.rb — does the message change when the spelling changes
a = 2
b = 3
puts a + b * 2
total = (a + b) * 2
puts total == 10
puts a.+(b).*(2)
puts a.method(:+).owner
puts a.method(:*).owner
puts 10.fdiv(4)
puts 10 / 4
puts 10.method(:fdiv).owner
puts 10.method(:/).owner
8
true
10
Integer
Integer
2.5
2
Integer
Integer

The expression a + b * 2 gave 8; the multiplication was done before the addition. (a + b) * 2 gave 10, and the comparison total == 10 was true. The spelling a.+(b).*(2) on the third line also gave 10: receiver a, message +, argument b, then the * message was sent to the result. So the precedence choice the author makes with parentheses gives exactly the same result as the chaining order of messages in dotted spelling. Operator precedence is therefore not a responder question; it is a spelling rule that determines which message goes to which receiver. The owner lines showed that both operators are defined in the Integer class.

What is interesting for readability is the two spellings of division. 10.fdiv(4) gave 2.5, while 10 / 4 gave 2. The responder of both turned out to be Integer, but the results differ: / does integer division, fdiv gives a fractional result. The spelling difference here is not two forms of the same job but two separate messages. Ruby’s design makes this distinction by name: someone who wants a fractional result writes fdiv, and the reader understands the intent from its name. This is also the conclusion of the section: spelling freedom is used safely in named messages; with operators, whether the result stays the same is known by measuring.

LB4. That integer division gives 2 for Integer was measured in this version. That division behaves the same way for other kinds of numbers was not measured.

The Core: A Helper and Twelve Observations

The course uses a measurement core made of two files. The first is a helper that classifies a message and is written as a rule: the order of the questions decides, not a label. First defined? says whether the expression is a method call; if it is not, the responder is syntax. Then, if the same message gives two different results with two different blocks, the responder is a block. Finally, if the owner of the method is an instance of Class, it is a class; otherwise it is a module.

# reply.rb — helpers that classify who answers a message

# Classification asks in order: is the expression a message at all (defined?),
# then do two different blocks give two different results, then who owns the method.
def responder(definition, owner: nil, result: nil, with_blocks: nil)
  return ["syntax", "defined? #{definition}"] unless definition == "method"

  if with_blocks
    first, second = with_blocks.call
    return ["block", "#{first.inspect} / #{second.inspect}"] unless first == second
  end

  kind = owner.instance_of?(Class) ? "class" : "module"
  [kind, "#{owner}, result #{result.inspect}"]
end

This file has no output block: it prints nothing on its own and is only a helper that the other file pulls in. The second file makes the twelve observations. Each observation is inside a lambda; when the lambda is called, it gives the responder helper the defined? result of the message, the owner of the method, and its result. The file writes a note.txt to the working directory, because the last observation reads it.

# responder.rb — twelve observations: who answers this message
require_relative "reply"

File.write("note.txt", "first line\nsecond line\n")
numbers = [1, 2, 3]

observations = {
  "readable comparison" => -> { responder(defined?(5.between?(1, 10)), owner: 5.method(:between?).owner, result: 5.between?(1, 10)) },
  "integer addition"    => -> { responder(defined?(5 + 3), owner: 5.method(:+).owner, result: 5 + 3) },
  "local assignment"    => -> { responder(defined?(x = 5)) },
  "range match"         => -> { responder(defined?((1..10) === 7), owner: (1..10).method(:===).owner, result: (1..10) === 7) },
  "receiverless call"   => -> { responder(defined?(format("%03d", 7)), owner: method(:format).owner, result: format("%03d", 7)) },
  "symbol conversion"   => -> { responder(defined?("name".to_sym), owner: "name".method(:to_sym).owner, result: "name".to_sym) },
  "default value"       => -> { responder(defined?(Hash.new), with_blocks: -> { [Hash.new { 0 }[:x], Hash.new { |_, k| k.to_s }[:x]] }) },
  "element mapping"     => -> { responder(defined?(numbers.map), with_blocks: -> { [numbers.map { |n| n * 2 }, numbers.map { |n| n.to_s }] }) },
  "slicing"             => -> { responder(defined?((1..5).each_slice(2)), owner: (1..5).method(:each_slice).owner, result: (1..5).each_slice(2).to_a) },
  "lambda call"         => -> { responder(defined?(->(n) { n * 2 }.call(3)), owner: ->(n) { n * 2 }.method(:call).owner, result: ->(n) { n * 2 }.call(3)) },
  "rescue"              => -> { responder(defined?(begin; Integer("x"); rescue ArgumentError; 0; end)) },
  "file reading"        => -> { responder(defined?(File.open("note.txt")), with_blocks: -> { [File.open("note.txt") { |f| f.gets.chomp }, File.open("note.txt") { |f| f.readlines.size }] }) },
}

tally = { "class" => 0, "module" => 0, "block" => 0, "syntax" => 0 }

printf("%-22s%-11s%s\n", "observation", "responder", "evidence")
observations.each do |name, observation|
  kind, evidence = observation.call
  tally[kind] += 1
  printf("%-22s%-11s%s\n", name, kind, evidence)
end

puts
tally.each { |kind, n| printf("%-11s%d\n", kind, n) }
puts "total observations: #{tally.values.sum}"
observation           responder  evidence
readable comparison   module     Comparable, result true
integer addition      class      Integer, result 8
local assignment      syntax     defined? assignment
range match           class      Range, result true
receiverless call     module     Kernel, result "007"
symbol conversion     class      String, result :name
default value         block      0 / "x"
element mapping       block      [2, 4, 6] / ["1", "2", "3"]
slicing               module     Enumerable, result [[1, 2], [3, 4], [5]]
lambda call           class      Proc, result 6
rescue                syntax     defined? expression
file reading          block      "first line" / 2

class      4
module     3
block      3
syntax     2
total observations: 12

Of the twelve observations, a class answered 4, a module answered 3, a block answered 3; in 2 there was no message, and the syntax processed the expression. These numbers are binding for all the lessons of the course: later lessons do not change these numbers, and each takes its own row from the table and measures it in detail.

Four readings come out of the table. The class responder is the most direct: the + in 5 + 3 looks like an operator but is a method defined in the Integer class; "name".to_sym is defined in String, and the lambda’s call in Proc. The module responder shows an ability that is not found in the class itself: between? is in Comparable, not in Integer; each_slice is in Enumerable, not in Range; the receiverless format is in Kernel. The block responder leaves the result to the caller: Hash.new, map, and File.open gave two results with the same message, and the difference was only in the block supplied. In the syntax responder there is neither a receiver nor a message: for x = 5, defined? returned "assignment", and for begin ... rescue ... end, "expression".

There are also things deliberately not printed in the output: object_id, inspect output that carries a memory address, and exception message text. These values change on every run or depend on the environment; in their place a class name, a boolean result, and a number are printed.

The Bounding Measurement

This lesson’s row is readable comparison, and its responder is a module: the Comparable module handles the message 5.between?(1, 10). But if this sentence is left half finished, it misleads. The method the module defines does not do the comparison itself; it asks the receiver. To say whether a value lies between two bounds, between? sends the <=> message to the receiver, and how the comparison is done lies in the <=> method of the receiver’s class. Print the owners side by side.

# comparison.rb — between? is in Comparable, <=> is in the receiver's class
[5, "a", :a].each do |receiver|
  printf("%-4s between? %-11s <=> %s\n", receiver.inspect, receiver.method(:between?).owner, receiver.method(:<=>).owner)
end
5    between? Comparable  <=> Integer
"a"  between? Comparable  <=> String
:a   between? Comparable  <=> Symbol

For all three receivers the owner of between? is the same, and the owner of <=> is each time the receiver’s own class. The same module gives three different classes the same ability, because all three answer the comparison themselves. The module therefore does not answer alone: its ability comes from the module, its basis from the class.

This bound prevents reducing the responder to a single owner name. The classification with four responders finds the definition that handles the message; that definition relying on another message is a new measurement question, and its answer is often a class.

Summary

  • In Ruby, every operation is a message sent to a receiver; 1 + 2 and 1.+(2) are the same message, and send sends a message by its name.
  • The measurement question is “who answers this message”, and there are four responders: class, module, block, syntax.
  • A rule gives the responder, not a label: first defined?, then the comparison with two blocks, then the owner of the method.
  • In the core’s twelve observations the distribution is 4 class, 3 module, 3 block, and 2 syntax.
  • The responder of this lesson is a module: between? is in Comparable, but <=>, the comparison itself, is in the receiver’s class.
  • Two spellings of the same job (a counter with each and sum, calls with and without parentheses) did not change the measured owners; the unless and the negated if gave the same result. Readability is the author’s choice and does not change the mechanism.

Next Step

That the + of 5 + 3 is defined in Integer silently assumes something: 5 is an object and belongs to a class. Whether primitive values are objects too, and how the receiver and the responder are separated from each other, is the subject of the next lesson, An Intuition for the Object Model.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close