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 + 2and1.+(2)are the same message, andsendsends 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 inComparable, but<=>, the comparison itself, is in the receiver’s class. - Two spellings of the same job (a counter with
eachandsum, calls with and without parentheses) did not change the measured owners; theunlessand the negatedifgave 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.