Design Philosophy and Coming from Other Languages
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7) and was checked on 2026-10-18. Features newer than Ruby 3.0 name the version that introduced them. It explains why Ruby looks the way it does, corrects a common misunderstanding about how a Ruby file is executed, and then maps the concepts a programmer already knows from another language onto their Ruby counterparts. The other pages of this reference go into each topic in depth; this one is the orientation that makes them easier to read.
Every example was run with Ruby 4.0.7 with warnings enabled (ruby -w), and the output shown is what it printed. Examples
that start with require "shelf" use the small shelf library-catalogue gem that serves as the running scenario of this
section (Shelf::Isbn, Shelf::Author, Shelf::Book, Shelf::Catalog; see the shelf gem source). The first page,
Getting Started, shows how to install Ruby and run a file.
The design goals
Ruby was created by Yukihiro Matsumoto ("Matz") and first released in 1995. The language’s own materials present it as a dynamic, open source language that values simplicity and productivity. Matz has repeatedly said that the goal is to make programmers happy, not to make machines fast or compilers simple; where the two conflict, Ruby usually chooses the human. Most of what follows, and most of what surprises newcomers, is a consequence of that choice.
| Idea | What it means in practice |
|---|---|
Programmer happiness |
Code should read close to the intent. Ruby accepts a larger language (several ways to say the same thing,
optional parentheses, |
Everything is an object |
Numbers, |
Principle of least surprise |
The language tries to behave the way an experienced Rubyist expects. Matz’s own caveat: the surprise is measured against his expectations, and yours may differ. |
More than one way to do it |
Ruby offers aliases and overlapping methods ( |
Duck typing |
An object is judged by what it can do (which messages it answers), not by its declared class or interface. |
Open classes |
Classes and modules are never closed: you can reopen any of them, including core ones, to add or replace methods. |
Blocks and iterators |
Control flow over collections is expressed by passing code (blocks) to methods, not by index loops. |
Source: About Ruby (ruby-lang.org) and Ruby FAQ (ruby-lang.org)
Everything is an object
Every value is an instance of a class, and every class is itself an object, an instance of Class. There are no
primitives, so there is no difference between 5 and Integer(5) and nothing to box or unbox. Operators are ordinary
method calls with special syntax: 1 + 2 is 1.+(2).
# frozen_string_literal: true
require "shelf"
p 42.class
p nil.class
p :isbn.class
p (1..3).class
p Shelf::Book.class
p Shelf::Book.class.class
p 42.respond_to?(:+)
p 1.+(2)
p nil.to_a
p nil.to_s
p nil.inspect
p Integer.ancestors.first(4)
p BasicObject.superclass
p Class.superclass
p Module.superclass
p Object.superclass
Output:
Integer
NilClass
Symbol
Range
Class
Class
true
3
[]
""
"nil"
[Integer, Numeric, Comparable, Object]
nil
Module
Object
BasicObject
Source: Object, BasicObject and NilClass
Three consequences are worth knowing early:
-
nilis an object (the only instance ofNilClass), so calling a method on it raisesNoMethodErrorwith a readable message rather than a null-pointer crash.nil.to_a,nil.to_sandnil.inspectare all defined and useful; the safe-navigation operator&.skips the call when the receiver isnil. -
The hierarchy has no gaps:
BasicObject(a nearly empty root) is the superclass ofObject, andClassdescends fromModule, which descends fromObject. A class is therefore an object that you can pass around, store in a hash or call methods on. -
Because operators are methods, you define
+,==,<=>,[]or<<for your own classes by writing methods with those names. See Operators and Precedence.
The Ruby way of writing code
A handful of conventions give Ruby code its recognisable look. All of them are optional; all of them are what you will find in nearly every library.
-
The last expression is the return value. A method ends with the value it computes; an explicit
returnis reserved for leaving early. Everything is an expression:if,caseandbeginall produce values. -
Parentheses are optional for method calls with arguments, and are usually omitted for "command-like" calls (
puts "x",raise Error,attr_reader :name) and for DSLs. They are kept when the call is nested, chained or ambiguous. -
Method chaining reads left to right, with each call feeding the next; line breaks before a leading
.are allowed. -
Names carry meaning. A trailing
?marks a predicate that returns a boolean (empty?,include?), a trailing!marks the more dangerous variant of a method (sort!mutates,sortcopies), and a trailing=defines a setter. The suffixes are conventions, not syntax, and they are always part of the method name. -
Prefer the iterator to the loop.
select,map,sum,each_with_objectand friends replace hand-written index loops and accumulator variables.&:nameturns a method name into a block, and Ruby 3.4 added the implicit block parameterit. -
Ask the collection.
sizeandlengthare the same,countalso accepts a block,empty?is preferred over comparing the size with zero, andany?,all?,none?andinclude?replace manual searches.
# frozen_string_literal: true
require "shelf"
isbn = Shelf::Isbn.new("978-0-306-40615-7")
author = Shelf::Author.new("Ada")
book = Shelf::Book.new(isbn, "Notes", author)
catalog = Shelf::Catalog.new
catalog.add(book).add(Shelf::Book.new(Shelf::Isbn.new("9780131103627"), "The C Language", Shelf::Author.new("Brian")))
# No return keyword, no parentheses, method chaining.
titles = catalog.map(&:title).sort.map(&:upcase)
p titles
puts catalog.first
p catalog.count
p(catalog.any? { |b| b.title.start_with?("N") })
p catalog.to_a.empty?
p [3, 1, 2].sort!.first
Output:
["NOTES", "THE C LANGUAGE"]
Notes by Ada
2
true
false
1
Source: Enumerable and Methods (syntax)
The Catalog in the example never defines map, count, first or any?: it defines each and includes Enumerable,
and the module supplies dozens of methods built on it. That pattern, one required method plus a mixin that adds the rest,
recurs throughout the language and is covered in Modules and Mixins
and Enumerable. The idioms themselves, with their trade-offs, are
collected in Idioms, Style and Clean Code.
The principle of least surprise
The principle is often quoted as "Ruby behaves as you would expect". Matz has been explicit that this is his own sense of what is natural, shaped by Lisp, Smalltalk, Perl and others, so a programmer from a different background will find places where Ruby surprises them. Knowing where those places are is most of the work of learning Ruby.
# frozen_string_literal: true
# Things that surprise newcomers.
truthiness = ->(v) { v ? "truthy" : "falsy" }
p [0, "", [], {}].map(&truthiness)
p [nil, false].map(&truthiness)
p 7 / 2
p(-7 / 2)
p 7.fdiv(2)
p "ab" * 3
p [1, 2].join(", ")
p 0.1 + 0.2
a = "x"
b = a
p b.equal?(a)
p "x".equal?("x")
p :x.equal?(:x)
p 1.equal?(1)
nothing = nil
flag = false
puts [1, [2, [nothing, 3]]].inspect
puts nothing.to_s.empty?
puts "#{nothing}|#{flag}|#{[nothing]}"
Output:
["truthy", "truthy", "truthy", "truthy"]
["falsy", "falsy"]
3
-4
3.5
"ababab"
"1, 2"
0.30000000000000004
true
true
true
true
[1, [2, [nil, 3]]]
true
|false|[nil]
Source: Integer#/, BasicObject#equal? and Control Expressions
The usual sources of surprise:
| What you may expect | What Ruby does |
|---|---|
|
Only |
|
Integer division of two integers is an integer, rounded toward negative infinity: |
|
Floats are IEEE 754 binary doubles, as in every mainstream language, so the result is |
|
|
Strings are immutable (Java, JavaScript, Python) |
Strings are mutable objects. Assigning a string to another variable copies the reference, not the text, so |
|
With |
|
|
|
It is the one-character string |
A name you never declared is an error |
Assignment creates a local variable. An unassigned instance variable reads as |
Two settings change how much of this you see. The magic comment # frozen_string_literal: true makes string literals in
that file frozen, which is why almost every file in this reference starts with it. Since Ruby 3.4, a literal in a file
without the comment is "chilled": mutating it still works but emits a warning of the form literal string will be
frozen in the future when deprecation warnings are enabled (-W:deprecated or $VERBOSE = true). In Ruby 4.0 this is still
only a warning; the programs of a codebase should be migrated with the magic comment, as described in
Program Structure and Lexical Elements and
Strings.
More than one way to do it
Perl’s motto "there is more than one way to do it" describes Ruby too, though Ruby leans on a shared set of idioms more
than Perl does. The language and its library are full of intentional overlap: map/collect, select/filter,
inject/reduce, size/length, Array#first/take, unless/if !, and/&&, statement modifiers versus block
forms, %w[] versus array literals, string interpolation versus format. The overlap exists to let code read
naturally in context, and it is why a style guide matters more in Ruby than in languages with a single dominant idiom.
# frozen_string_literal: true
numbers = [1, 2, 3, 4]
p numbers.sum
p numbers.reduce(:+)
p(numbers.inject { |acc, n| acc + n })
total = 0
numbers.each { |n| total += n }
p total
p numbers.each_with_object([0]) { |n, acc| acc[0] += n }.first
# Several ways to say "the evens, squared".
p(numbers.select(&:even?).map { |n| n**2 })
p(numbers.filter_map { |n| n**2 if n.even? })
p(numbers.each_slice(2).map(&:last).map { it**2 })
Output:
10
10
10
10
10
[4, 16]
[4, 16]
[4, 16]
Source: Enumerable#sum, Enumerable#inject and Enumerable#filter_map
All the variants above are correct. A reasonable rule of thumb: pick the most specific method that names what you mean
(sum rather than inject(:+), filter_map rather than select then map, select rather than an each that
pushes onto an array), let RuboCop enforce the team’s choice, and do not mix synonyms such as collect and map in
one codebase. See Style, Linting and Static Analysis.
Duck typing
Ruby never asks "what class are you?" when it can ask "can you do this?". A method that calls item.title works with
any object that has a title method, whatever its ancestry. There are no interfaces to declare and no common superclass
to share. When the object cannot answer, Ruby raises NoMethodError at the point of the call, and the exception carries
the method name and the receiver.
# frozen_string_literal: true
require "shelf"
# Duck typing: anything with #title works, no common ancestor needed.
Pamphlet = Struct.new(:title)
def shout(item)
item.title.upcase
end
book = Shelf::Book.new(Shelf::Isbn.new("9780306406157"), "Notes", Shelf::Author.new("Ada"))
p shout(book)
p shout(Pamphlet.new("flyer"))
begin
shout(42)
rescue NoMethodError => e
puts e.message
p e.name
p e.receiver
end
p [book, Pamphlet.new("x"), 42].select { |o| o.respond_to?(:title) }.size
# A role by convention: each + Enumerable.
class Rack
include Enumerable
def initialize(*items) = @items = items
def each(&)
return enum_for(:each) unless block_given?
@items.each(&)
end
end
s = Rack.new(3, 1, 2)
p s.sort
p s.min
p s.include?(2)
p s.each_slice(2).to_a
Output:
"NOTES"
"FLYER"
undefined method 'title' for an instance of Integer
:title
42
2
[1, 2, 3]
1
true
[[3, 1], [2]]
Source: NoMethodError, Object#respond_to? and Enumerable
Ruby is dynamically typed (types are attached to values and checked when a method runs) and strongly typed (it does not silently convert between unrelated types; see the table later on this page). Idiomatic code therefore:
-
tests capabilities with
respond_to?only when it must, and otherwise lets the call fail; -
converts at the boundary with explicit conversion methods (
to_s,Integer(),to_str), see Conversions and Coercion; -
documents the expected "shape" of arguments in comments, in RBS signatures or in tests, rather than in declarations;
-
uses
is_a?,case/when Classor pattern matching deliberately when behaviour really depends on the kind of object.
The cost is that mistakes surface at run time, which is why Ruby projects lean on tests (see Testing with Minitest) and, increasingly, on optional static checking (see Type Checking with RBS, TypeProf, Steep and Sorbet).
Open classes
A class or module block does not define a class so much as open it: if the constant already exists the body is
evaluated inside the existing class and its definitions are added to it. This is how Ruby extends classes across files,
how libraries add features, and how the language lets you patch behaviour.
# frozen_string_literal: true
require "shelf"
# Open classes: reopening a class adds to it.
module Shelf
class Author
def initials = name.split.map { |part| part[0] }.join
end
end
p Shelf::Author.new("Ada Lovelace").initials
p Shelf::Author.instance_method(:initials).owner
Output:
"AL"
Shelf::Author
Source: Modules and Classes (syntax)
Core classes are open too:
# frozen_string_literal: true
# Even core classes are open. Convenient, and dangerous.
class Integer
def double = self * 2
end
p 21.double
p Integer.instance_method(:double).owner
p Integer.instance_method(:double).source_location.last
Output:
42
Integer
5
Source: Module#instance_method
Changing core classes globally ("monkey patching") is powerful and risky: the change is visible to every library in the
process, two libraries can define the same method differently, and a later Ruby release may add a method with the same
name and a different meaning. The contained alternative is a refinement, whose changes apply only in the file (or
module body) that activated them with using:
# frozen_string_literal: true
# A refinement limits the change to a lexical scope.
module Shouting
refine String do
def shout = "#{upcase}!"
end
end
begin
"hi".shout
rescue NoMethodError => e
puts e.message
end
using Shouting
p "hi".shout
Output:
undefined method 'shout' for an instance of String
"HI!"
Source: Refinements (syntax) and Module#refine
Rules of thumb for open classes: reopen your own classes freely; reopen third-party classes rarely and document it; prefer a refinement, a subclass, a wrapper or a mixin to changing a core class; never redefine a core method with a different contract. The details are in Refinements and Monkey Patching.
What happens when Ruby runs your file
A frequent claim in introductions is that Ruby is "parsed and executed line by line". That describes a REPL such as IRB,
which reads one expression at a time, but not what ruby file.rb does. Ruby reads the whole file, parses it into a
syntax tree, compiles the tree to bytecode and only then starts executing. A syntax error anywhere in the file, even
after code that would have run, stops the program before the first line executes.
-
The parser is Prism, the default since Ruby 3.4 (it replaced the older
parse.yparser). It is also available to programs as theprismdefault gem, which is how many tools read Ruby code. -
The compiler produces YARV instruction sequences (
RubyVM::InstructionSequence), which the virtual machine in MRI (the reference implementation, also called CRuby) interprets. -
YJIT, the just-in-time compiler, is built into MRI since 3.1, is considered production-ready since 3.2, and is enabled with
--yjitorRUBY_YJIT_ENABLE=1. ZJIT, a newer method-based JIT, appeared in Ruby 4.0 as an experimental option and is not used in the examples here. The older MJIT and RJIT were removed; the page YJIT, ZJIT and the VM covers them. -
Requiring a file (
require,load) repeats the same steps for that file at the moment of the call, which is why code can be loaded conditionally and whyevalis possible.
A file with a syntax error shows the "whole file first" behaviour; the puts on line 1 never runs:
puts "this line never runs"
def broken(
Output (exit status 1, nothing printed before the error):
$ ruby broken.rb
broken.rb: --> broken.rb
Unmatched `(', missing `)' ?
Unmatched keyword, missing `end' ?
> 1 puts "this line never runs"
> 2 def broken(
broken.rb:2: syntax errors found (SyntaxError)
1 | puts "this line never runs"
> 2 | def broken(
| ^~~ expected an `end` to close the `def` statement
| ^ unexpected end-of-input, assuming it is closing the parent top level context
| ^ unexpected end-of-input; expected a `)` to close the parameters
Source: Prism
The parser and compiler are visible from Ruby itself:
# frozen_string_literal: true
require "prism"
source = <<~RUBY
puts "this line never runs"
def broken(
RUBY
result = Prism.parse(source)
p result.success?
puts result.errors.map(&:message).first(2)
ok = Prism.parse("a = 1 + 2")
p ok.success?
p ok.value.statements.body.first.class
p ok.value.statements.body.first.value.class
iseq = RubyVM::InstructionSequence.compile("a = 1 + 2")
puts iseq.disasm.lines.grep(/putobject|opt_plus|setlocal|leave|putself/).map(&:strip)
Output:
false
unexpected end-of-input; expected a `)` to close the parameters
unexpected end-of-input, assuming it is closing the parent top level context
true
Prism::LocalVariableWriteNode
Prism::CallNode
0000 putobject_INT2FIX_1_ ( 1)[Li]
0001 putobject 2
0003 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0006 setlocal_WC_0 a@0
0008 leave
Source: Prism and RubyVM::InstructionSequence
The exact error text and the instruction names are implementation details of MRI and change between releases. What stays
stable is the model: parse the file, compile it, run it. ruby -c file.rb (and ruby -wc, which also reports
warnings) performs only the parse step and prints Syntax OK; see
Ruby Command Line and Environment.
Coming from other languages
The official site collects short guides for readers who know C and C++, Java, Perl, PHP or Python. The sections below cover the same ground, extended to JavaScript, C# and Go and focused on the places where a habit from another language produces wrong Ruby. They are not a substitute for the topic pages, which they link to.
Concept map
| Concept | In other languages | In Ruby |
|---|---|---|
Interface |
Java and C# |
No keyword. Duck typing for call sites, and a module
mixed in with |
Abstract class |
|
A class whose methods |
Generics |
|
None at run time. Containers hold any object. RBS adds generic signatures, |
Exceptions |
|
|
Lambdas and closures |
Java lambdas, C# delegates, JavaScript arrow functions, Python |
Blocks ( |
Packages and imports |
Java |
|
Annotations and decorators |
Java |
No annotations. Class-level method calls ("class macros") such as |
Properties and fields |
C# properties, Java getters and setters, PHP typed properties |
Instance variables ( |
Constructors |
|
|
Records and value types |
Java |
|
Enums |
|
No keyword. Symbols ( |
Switch |
|
|
Null |
|
|
Iteration |
|
|
Increment |
|
|
Async |
|
Threads (GVL-limited), Fibers with a scheduler, Ractors; no |
Package manager |
Maven, npm, pip, NuGet, Composer, Go modules |
RubyGems ( |
Test frameworks |
JUnit, pytest, Jest, PHPUnit, |
Minitest (bundled) and RSpec. |
Source: Pattern Matching (syntax) and Data
Truthiness and equality across languages
| Value | Ruby | Python | JavaScript | PHP | Java, C#, Go |
|---|---|---|---|---|---|
|
truthy |
falsy |
falsy |
falsy |
not a boolean (compile error) |
|
truthy |
falsy |
falsy |
falsy |
not a boolean |
|
truthy |
falsy |
truthy |
falsy |
not a boolean |
|
truthy |
falsy |
truthy |
falsy (empty array) |
not a boolean |
|
falsy |
falsy |
falsy |
falsy |
not a boolean |
|
falsy |
falsy |
falsy |
falsy |
false |
|
truthy |
truthy |
truthy |
falsy |
not a boolean |
Because only nil and false are falsy, if count is true for zero. Test what you mean: count.zero?, list.empty?,
name.nil?. The ||= operator therefore only fills in missing (nil or false) values, which is what most memoisation
needs but is a trap when false is a legitimate stored value.
A method definition does not close over locals
def starts a new local scope. A method cannot see the local variables of the surrounding code, while blocks and
lambdas can. This differs from JavaScript and Python, where a nested function closes over the enclosing scope, and it
is the reason a script’s top-level variables cannot be used inside its methods.
# frozen_string_literal: true
# def opens a new scope; blocks and lambdas close over the surrounding one.
limit = 3
def uses_outer
limit
end
begin
uses_outer
rescue NameError => e
puts e.message
end
p([1, 2, 3, 4].select { |n| n < limit })
p(-> { limit }.call)
p defined?(limit)
p defined?(undefined_thing)
p defined?(puts)
p defined?(String)
p defined?(@name)
# No declarations: assignment creates the variable, nil is the default for instance variables.
class Tracker
def peek = @seen
end
p Tracker.new.peek
# for does not create a scope; each does.
for i in 1..2 do end
p i
[1, 2].each { |j| j }
p defined?(j)
# self is always the receiver; there is no rebinding of this.
p self
p self.class
Output:
undefined local variable or method 'limit' for main
[1, 2]
3
"local-variable"
nil
"method"
"constant"
nil
nil
2
nil
main
Object
Source: Methods (syntax) and Miscellaneous (syntax), defined?
The for loop in that example is the one construct that does not create a scope: its variable remains after the loop.
That surprising behaviour is a reason the community prefers each. Scope rules in full are in
Variables, Constants and Scope.
Strong, dynamic typing
Ruby will not turn a number into a string, or a string into a number, to make an operator work. This sits between JavaScript and PHP (which coerce freely) and Java, C# and Go (which refuse at compile time).
# frozen_string_literal: true
# Ruby is dynamically but strongly typed: no implicit conversions between unrelated types.
prefix = "total: "
count = 3
text = "3"
begin
prefix + count
rescue TypeError => e
puts e.message
end
begin
count + text
rescue TypeError => e
puts e.message
end
begin
nil + 1
rescue NoMethodError => e
puts e.message
end
p "total: #{count}"
p text.to_i + count
p "abc".to_i
p Integer("42")
p "1" == 1
p [1, 2] + [3]
p nil&.length
p nil.to_a
p({ a: 1 }[:b])
p({ a: 1 }.fetch(:a))
begin
{ a: 1 }.fetch(:b)
rescue KeyError => e
puts e.message
end
p [1, 2][5]
p 2**100
p((2**100) / (2**98))
p 10.0 / 3
p 10 % 3
p(-10 % 3)
Output:
no implicit conversion of Integer into String
String can't be coerced into Integer
undefined method '+' for nil
"total: 3"
6
0
42
false
[1, 2, 3]
nil
[]
nil
1
key not found: :b
nil
1267650600228229401496703205376
4
3.3333333333333335
1
2
Source: Integer#+, TypeError, Hash#fetch and Kernel#Integer
The same example shows three more habits: "abc".to_i returns 0 where Integer("abc") raises (use the strict Integer()
at trust boundaries); Hash#[] and Array#[] return nil for a missing key or index while fetch raises KeyError;
and integers have arbitrary precision (2**100 does not overflow), so there are no int/long choices and no wrap-around.
Modulo follows the sign of the divisor, as in Python and unlike C, Java and JavaScript.
From Java and C#
-
Classes and files. There is no one-class-per-file rule, no
publicclass naming the file and no package-private access. A file can define any number of classes and modules; by convention (and by the autoloaders) a file is named after its constant,book.rbforBook. -
Interfaces become duck typing plus mixins. A method that needs
titletakes anything withtitle. Shared behaviour goes into a module included in several classes (below). Java default methods and C# extension methods are both approximated byinclude, and by refinements. -
No overloading. Defining a method twice replaces it (with a warning under
-w). Use optional and keyword arguments,*args, or inspect the argument. -
Visibility is about the receiver, not the class.
privatemethods can be called only without an explicit receiver (andself.receivers for setters and, since Ruby 2.7, any method);protectedallows calls from instances of the same class; instance variables are always private to the object. There is nofinal, butfreezeandprivate_constantprovide the spirit. See Visibility and Encapsulation. -
Single inheritance plus modules. A class has one superclass and any number of mixed-in modules; the method lookup order is the ancestor chain, see Method Lookup and super.
-
==is not identity.a == bcalls a method that classes override. Compare identity withequal?. -
Properties. C# properties and Java getters and setters are
attr_reader,attr_writerandattr_accessor, or hand-written methods namednameandname=. Assigning withobj.name = valueis a method call. -
LINQ and streams.
Enumerable, andEnumerator::Lazyfor lazy pipelines, withmap,select,group_by,sort_by,sum,each_slice,tally,zipand the rest in Enumerable. -
using/try-with-resources. Methods that take a block manage the resource and close it afterwards:File.open(path) { |f| ... },Mutex#synchronize { ... }. -
Nullable references. There is no compile-time null check.
&.(safe navigation),Array#compactandHash#fetchwith a default cover the common cases. -
Annotations,
@Override. Nothing marks an override; the keywordsupercalls the parent implementation, and RuboCop or RBS can check intent. -
Static members.
class << self,def self.nameandmodule_functiontake the place ofstatic; class-level state lives in class instance variables. See Singleton Classes and Class-Level Behavior. -
Generics,
async,partial class. See the sections below;partial classis simply reopening a class.
Java and C# readers should see the Java Reference and C# Reference on this site for the other side of each comparison.
Interfaces and mixins
Where Java declares an interface and several classes implement it, Ruby writes a module with the shared behaviour and
mixes it into each class. A module that depends on a method the class must supply (label here) is the Ruby analogue of
an abstract method, but the check happens at call time:
# frozen_string_literal: true
# The Java "interface" becomes a convention plus a mixin.
module Describable
def describe = "#{self.class.name.downcase}: #{label}"
end
Magazine = Struct.new(:label) { include Describable }
Poster = Struct.new(:label) { include Describable }
p Magazine.new("monthly").describe
p Poster.new("A2").describe
p Magazine.include?(Describable)
p Magazine.new("x").is_a?(Describable)
p Describable.instance_methods
# The module needs #label, but nothing checks that until it is called.
Broken = Class.new { include Describable }
begin
Broken.new.describe
rescue NameError => e
puts e.message
p e.class
end
# Comparable gives <, <=, ==, >, >= and between? from a single <=>.
Version = Struct.new(:major, :minor) do
include Comparable
def <=>(other) = to_a <=> other.to_a
end
p Version.new(1, 2) < Version.new(1, 10)
p Version.new(1, 2).between?(Version.new(1, 0), Version.new(2, 0))
p [Version.new(2, 0), Version.new(1, 5)].min
Output:
"magazine: monthly"
"poster: A2"
true
true
[:describe]
undefined local variable or method 'label' for an instance of Broken
NameError
true
true
#<struct Version major=1, minor=5>
Source: Module#include, Comparable and Struct
Comparable and Enumerable are the two mixins worth learning first: define <=> and you get the comparison operators,
between?, clamp and sort/min/max support; define each and you get the whole collection API.
Exceptions
try/catch/finally becomes begin/rescue/ensure, with an optional else that runs when nothing was raised.
raise Class, "message" throws, a bare raise inside rescue re-raises, and retry re-runs the begin block.
A def body and a do ... end block (since Ruby 2.5) can carry rescue and ensure clauses directly, without an
enclosing begin.
# frozen_string_literal: true
require "shelf"
# try/catch/finally becomes begin/rescue/ensure (and an optional else).
def parse_isbn(text)
Shelf::Isbn.new(text)
rescue ArgumentError => e
warn "rejected: #{e.message}"
nil
ensure
puts "checked #{text.inspect}"
end
p parse_isbn("9780306406157")
p parse_isbn("123")
# A custom exception hierarchy, and retry.
class NotFound < Shelf::Error; end
attempts = 0
begin
attempts += 1
raise NotFound, "no such book" if attempts < 3
puts "found after #{attempts} attempts"
rescue NotFound
retry
end
# raise with no argument re-raises; exceptions carry a cause.
begin
begin
Integer("abc")
rescue ArgumentError
raise NotFound, "lookup failed"
end
rescue Shelf::Error => e
p e.class
p e.cause.class
puts e.cause.message
end
# Many conversions take exception: false instead of raising.
p Integer("x", exception: false)
p Float("1.5", exception: false)
Output:
checked "9780306406157"
#<data Shelf::Isbn digits="9780306406157">
rejected: invalid ISBN: 123
checked "123"
nil
found after 3 attempts
NotFound
ArgumentError
invalid value for Integer(): "abc"
nil
1.5
Source: Exceptions (syntax), Exception and Kernel#Integer
Differences that catch newcomers:
-
A bare
rescuehandlesStandardErrorand its subclasses only; neverrescue Exception, which also trapsSystemExitandInterrupt. -
ensureruns on every exit, includingreturn,breakandthrow; an explicitreturninsideensureswallows an exception in flight. -
The value of a
beginblock with anelseis the value of theelsebranch, not of the body. -
Kernel#catchandKernel#throware an unrelated, lightweight non-local-exit mechanism; they are not exceptions. -
Ruby has no checked exceptions and no
throwsclause; methods are not annotated.
Lambdas and closures
Java lambdas, C# delegates and JavaScript arrow functions correspond to Ruby’s three closure forms. A block is an
anonymous chunk of code attached to a method call; a proc is a block turned into an object; a lambda is a proc
with method-like argument checking and return. They all capture the variables around them.
# frozen_string_literal: true
# Blocks, procs and lambdas: closures for lambdas / function values.
double = ->(n) { n * 2 }
p double.call(4)
p double[4]
p double.lambda?
p [1, 2, 3].map(&double)
p [1, 2, 3].map(&:to_s)
compose = double >> ->(n) { n + 1 }
p compose.call(5)
counter = 0
increment = -> { counter += 1 }
3.times { increment.call }
p counter
def adder(step) = ->(n) { n + step }
p adder(10).call(1)
def each_twice(items)
return to_enum(__method__, items) unless block_given?
items.each { |i| 2.times { yield i } }
end
each_twice([1, 2]) { |i| print i, " " }
puts
p each_twice([1, 2]).to_a
pr = proc { |a, b| [a, b] }
p pr.call(1)
p pr.call(1, 2, 3)
p pr.call([4, 5])
begin
double.call
rescue ArgumentError => e
puts e.message
end
p method(:puts).to_proc.lambda?
Output:
8
8
true
[2, 4, 6]
["1", "2", "3"]
11
3
11
1 1 2 2
[1, 1, 2, 2]
[1, nil]
[1, 2]
[4, 5]
wrong number of arguments (given 0, expected 1)
true
Source: Proc and Calling Methods (syntax): Block Argument
Notes for readers from other languages:
-
Calling a method needs no parentheses, but referring to a method without calling it is not possible:
method(:puts)returns aMethodobject, and&method(:name)or&:namepass it where a block is expected. There is no bare function reference as in JavaScript or Python. -
A lambda checks its argument count (
wrong number of arguments (given 0, expected 1)), a proc does not (missing values arenil, extra values are dropped, a single array argument is destructured). -
returnin a lambda returns from the lambda;returnin a proc or block returns from the enclosing method. -
yieldcalls the block given to the current method, andblock_given?tests whether there is one. A method that returns an enumerator when called without a block is a common Ruby idiom (return to_enum(__method__, items) unless `block_given?). -
The compose operators
>>and<<combine callables. Currying isProc#curry.
Annotations become class macros
Ruby has no annotation syntax. The same needs (declare an accessor, mark a method private, register a callback,
deprecate a name) are met by calling methods on the class from inside its body, which run when the class is defined.
attr_accessor, private, include, private_constant and define_method are all ordinary methods.
# frozen_string_literal: true
# Annotations become class-level method calls ("class macros").
class Settings
attr_accessor :host
attr_reader :port
def initialize
@host = "localhost"
@port = 8080
end
DEFAULTS = { host: "localhost" }.freeze
private_constant :DEFAULTS
def self.deprecated_alias(old, replacement)
define_method(old) do |*args, &blk|
warn "#{old} is deprecated, use #{replacement}"
public_send(replacement, *args, &blk)
end
end
deprecated_alias :legacy_host, :host
end
s = Settings.new
p s.legacy_host
p Settings.instance_methods(false).sort
p Settings.private_instance_methods(false)
begin
Settings::DEFAULTS
rescue NameError => e
puts e.message
end
p s.frozen?
p s.freeze.frozen?
begin
s.host = "x"
rescue FrozenError => e
p e.class
end
Output:
legacy_host is deprecated, use host
"localhost"
[:host, :host=, :legacy_host, :port]
[:initialize]
private constant Settings::DEFAULTS referenced
false
true
FrozenError
This is the foundation of Ruby DSLs, covered in Dynamic Dispatch and Method Definition
and Hooks, eval and DSLs. The magic comments at the top of a
file (frozen_string_literal, shareable_constant_value) are the closest thing to file-level annotations, and
documentation comments with YARD tags (@param, @return) are read by tools, not by the interpreter; see
Documentation with RDoc and YARD.
Generics become duck typing plus RBS
An Array holds anything, a method takes anything, and the only "type error" is a NoMethodError or TypeError at the
moment a value is misused:
# frozen_string_literal: true
# Without generics: containers hold anything, types are checked at use.
mixed = [1, "two", :three, nil, 4.0, [5]]
p mixed.map(&:class)
p mixed.grep(Numeric)
def total(items)
items.sum
end
p total([1, 2, 3])
p total([1.5, 2])
begin
total(%w[a b])
rescue TypeError => e
puts e.message
end
p total([[1], [2]].map(&:first))
p %w[a b].sum("")
Output:
[Integer, String, Symbol, NilClass, Float, Array]
[1, 4.0]
6
3.5
String can't be coerced into Integer
3
"ab"
Source: Enumerable#sum and Enumerable#grep
Static types are optional and live outside the program. Since Ruby 3.0, RBS is the standard language for describing
types in separate .rbs files; tools such as TypeProf (inference), Steep (checking) and Sorbet (a separate system with its
own inline syntax) consume them. RBS has generics, so Array[Book] and Enumerable[Book] are expressible:
module Shelf
class Catalog
include Enumerable[Book]
def initialize: (?store: Store::Memory) -> void
def add: (Book book) -> self
def each: () { (Book) -> void } -> void
| () -> Enumerator[Book, void]
end
end
Source: RBS (github.com/ruby/rbs)
Packages become gems and modules
Java’s package plays two roles that Ruby separates. Namespacing is done by module (or class) nesting:
Shelf::Store::Memory. Distribution is done by gems, versioned archives with a .gemspec, installed by RubyGems and
resolved by Bundler from a Gemfile. Loading is separate again: nothing is imported automatically, a file must be
`require`d (or autoloaded) before its constants exist, and the file layout carries no meaning to the interpreter.
# frozen_string_literal: true
require "shelf"
# Packages become gems (distribution) and modules (namespacing); files are not namespaces.
p Shelf.constants.sort.first(5)
p Shelf::VERSION
p($LOADED_FEATURES.grep(/shelf\.rb/).map { File.basename(it) })
p require("shelf")
p defined?(Shelf::Store::Memory)
p Shelf::Store::Memory.name
module Shelf
module Store
p Memory
end
end
p Set.new([1, 2, 3]).size
Output:
[:Author, :Book, :Catalog, :Config, :Error]
"0.1.0"
["shelf.rb"]
false
"constant"
"Shelf::Store::Memory"
Shelf::Store::Memory
3
Source: Kernel#require and Module#constants
Note that Set needs no require: it has been autoloaded since Ruby 3.2 and is a core class in Ruby 4.0. Key points: require returns false the second time
(a file loads once), require_relative resolves from the current file, and autoload/Zeitwerk load constants on demand.
See Code Loading: require and autoload and
RubyGems and Bundler.
Fields, constructors and value types
A class declares no fields. Instance variables appear when first assigned (usually in initialize), are visible only
inside the object’s methods, and are exposed through methods. Where Java uses a record or Python a dataclass, Ruby
has Struct (mutable, positional by default) and Data (immutable, since Ruby 3.2):
# frozen_string_literal: true
# Java-style class vs Ruby: fields, accessors, constructors.
class Reader
attr_reader :name
attr_accessor :email
def initialize(name, email: nil)
@name = name
@email = email
end
def to_s = "#{name} <#{email || 'n/a'}>"
end
r = Reader.new("Ada", email: "ada@example.com")
puts r
r.email = nil
puts r
p r.instance_variables
p r.respond_to?(:name=)
Pair = Struct.new(:left, :right)
Point = Data.define(:x, :y)
p Pair.new(1, 2)
p Point.new(x: 1, y: 2)
p Point.new(1, 2).with(y: 5)
first = Point.new(1, 2)
p first == Point.new(1, 2)
p Reader.new("a").eql?(Reader.new("a"))
Output:
Ada <ada@example.com>
Ada <n/a>
[:@name, :@email]
false
#<struct Pair left=1, right=2>
#<data Point x=1, y=2>
#<data Point x=1, y=5>
true
false
Source: Data, Struct and Module#attr_reader
Two ordinary classes with equal fields are not equal unless the class defines == (and eql? and hash if it is used
as a hash key); Struct and Data define all three from their members. See
Struct, Data and Value Objects and
Classes and Objects.
From Python
Ruby and Python occupy similar territory (dynamic, strongly typed, garbage collected, interactive) and differ mostly in syntax and in how code is organised.
-
Blocks, not indentation. Blocks end with
end; indentation is style, not syntax. -
No list comprehensions.
[x * 2 for x in xs if x > 1]isxs.select { |x| x > 1 }.map { |x| x * 2 }orfilter_map. -
selfis implicit. It is not a parameter; callself.methodonly when needed (setters,privaterules). Methods are called with a receiver, and instance variables carry an@. -
Methods are not attributes.
obj.namealways calls a method; there is no attribute lookup separate from method lookup, so there is nopropertydecorator, onlyattr_*and plain methods. -
Mixins instead of multiple inheritance. A class has one parent and any number of modules.
-
Functions are not values. A bare name calls the method; use
method(:name)for a reference. -
Strings are mutable and
str[0]is a one-character string. Symbols (:name) are interned identifiers, often used for hash keys and options. -
Dictionaries are
Hash, ordered by insertion since Ruby 1.9, with{ key: value }as the common literal. -
No tuples. Use arrays,
StructorData; multiple return values are an array destructured by multiple assignment (a, b = pair). -
Truthiness. Empty collections,
0and""are truthy; see the table above. -
Integer division floors like Python’s
//, but spelled/between integers. -
Errors.
raise/rescueinstead ofraise/except;ensureinstead offinally;retryhas no equivalent. -
Standard library.
with open(...)isFile.open(...) { ... };importisrequire;pipisgemand Bundler; virtual environments are replaced by aGemfileandbundle execplus a version manager;unittest/pytestare Minitest/RSpec. -
Concurrency. Like CPython, MRI has a global VM lock for Ruby threads; Ractors and Fibers are the other tools.
See the Python Reference for the other side of the comparison.
From JavaScript and TypeScript
-
constis not Ruby’s constant. A Ruby constant (capitalised name) can be reassigned, with a warning (already initialized constant); it is a naming convention for shared values, and the value stays mutable unless it is frozen. -
thisisself, and it is always the receiver of the current method. There is no rebinding by call site;instance_evalandinstance_execchange it explicitly. -
No
undefined, no hoisting, novar. Variables come into existence on assignment, and a read before assignment of a local raisesNameError. -
Object literals are hashes.
{ name: "Ada" }is aHashwith symbol keys, accessed ash[:name], noth.name.Struct,Dataor a class gives dot access. -
Arrow functions are blocks,
procor->lambdas. Ruby has no function-valued property syntax. -
Strings convert explicitly.
"1" + 1raisesTypeError;"1" == 1isfalse; interpolation ("#{n}") replaces template literals. -
==and===differ from JS. Ruby==does no coercion; Ruby===is case equality used bycase/when(Integer === 3is true) and not strict equality. -
async/awaitdo not exist. Concurrency uses threads, fibers (with a scheduler for non-blocking I/O) and Ractors; an event loop comes from a gem. -
Destructuring and spread are multiple assignment (
a, (b, c) = ...), the splat operators*and**, and pattern matching (case/in). -
npm/package.json/node_modulescorrespond togem,Gemfileand Bundler’s vendored or system gems. -
Optional chaining
?.is&.; nullish coalescing??has no direct equivalent:||treatsfalseas missing,Hash#fetchandObject#thencover the rest.
See the JavaScript Development guide.
From PHP
-
No sigil on variables.
$nameisname; sigils in Ruby (@,@@,$) denote instance, class-variable and global scope, not type. Globals are rare. -
->and::become.for method calls;::is only for constants and namespacing.new FooisFoo.new. -
Arrays split in two. PHP’s single ordered map is
Array(positional) plusHash(keyed, insertion-ordered). -
null,isset,emptybecomenil,nil?,empty?anddefined?. There is no "falsy empty array". -
Namespaces and
usebecome nestedmodule`s; `requireis explicit, with Bundler and autoloading (Zeitwerk) replacing Composer’s classmap/PSR-4. -
Exceptions work as in PHP, with
rescueclauses instead ofcatch; all exceptions inherit fromException. -
Type juggling is gone.
"10" == 10isfalse,"abc" + 1raises. -
No request/response globals. Web frameworks (Rails, Sinatra, Rack applications) provide them; plain Ruby has no built-in web page model.
See the PHP Reference.
From Go
-
Interfaces are implicit in both languages, but Go checks them at compile time and Ruby only when a message is sent. A Go interface becomes a convention, a module or an RBS interface.
-
Errors are exceptions, not return values: there is no
if err != nilladder, andensurereplacesdefer. -
Goroutines and channels map to threads,
Thread::Queue, fibers and Ractors. Ruby threads are limited by the global VM lock for Ruby code, so CPU-bound parallelism needs processes or Ractors. -
Structs and methods become classes: methods live inside the class body, with no receiver declaration.
StructandDatagive struct-like values. -
Composition by embedding is composition by delegation or by
include; there is no struct embedding. -
Zero values do not exist: an uninitialised instance variable is
nil. -
go mod,gofmt,go vet,go testcorrespond to Bundler, RuboCop (not part of Ruby itself),ruby -wcand Minitest. -
A single static binary has no Ruby equivalent: a Ruby program needs an interpreter and its gems on the target machine, or a packaged distribution.
The Go reference on this site covers the other side of the comparison.
From C and C++
-
No pointers, no manual memory. Objects are references and the garbage collector reclaims them;
freeand destructors are replaced byensureand block-based resource management. -
Integers do not overflow (they become big integers), floats are doubles, and there are no typedefs or casts, only conversion methods.
-
No headers, no preprocessor.
requireloads a file at run time; macros are replaced by methods and blocks. -
Native code is reached through C extensions, FFI (the
ffigem, orFiddle, a bundled gem in Ruby 4.0) or WebAssembly; see C Extensions, FFI and WebAssembly. -
Templates are replaced by duck typing: any object that responds to the used messages works.
See the C Reference and C++ Reference.
From Perl
Ruby inherited a lot from Perl: regular expression literals and =~, $~-style match globals, heredocs, __END__, the
-n, -p and -e command-line switches and the unless/until modifiers. The differences: variables have no type
sigils, there is no context (scalar versus list); everything is an object; use strict-like behaviour is built in; references
are implicit; and modules are classes. See
Regular Expressions and
Ruby Command Line and Environment.
Other implementations in one paragraph
Everything above describes MRI (CRuby), the reference implementation. Alternatives share the language but differ in runtime. JRuby runs on the JVM and integrates with Java libraries, TruffleRuby runs on GraalVM, and mruby is a small embeddable implementation for constrained environments. Library authors check which implementations they support; most application code runs on MRI.
Where the books are outdated
Older books and tutorials teach Ruby 1.8 to 2.x. Most of what they say about philosophy remains right; the details below have changed, and the concept pages in this reference have the current rules.
| Older statement | Current state (Ruby 4.0) |
|---|---|
Ruby is "parsed and executed line by line" |
The whole file is parsed by Prism, compiled to YARV bytecode and then run. Only IRB evaluates line by line. Syntax errors stop the program before it starts. |
MRI is a slow interpreter with a global lock and no JIT |
MRI compiles to bytecode, has YJIT (production-ready since 3.2), and experimental ZJIT in 4.0 (RJIT and MJIT are gone); the VM lock still serialises Ruby threads, and Ractors provide parallelism. |
|
|
Interfaces and generics cannot be expressed |
RBS (3.0) describes both; Steep and TypeProf use it; Sorbet is an alternative. |
|
|
String literals are mutable |
Literals are chilled since 3.4: mutation warns under |
A trailing hash is accepted as keyword arguments |
Ruby 3.0 separated positional and keyword arguments; a trailing hash is no longer converted automatically. |
Pattern matching does not exist |
|
Numbered block parameters |
|
|
Process creation by |
Source: Ruby 4.0 NEWS
The full list of changes by version is in What’s New and Migrating Older Code and Versions, Releases and Compatibility.
Where to go next
-
The next topic page is Literals, followed by Variables, Constants and Scope.
-
For the style conventions in depth, Idioms, Style and Clean Code and Common Mistakes and Gotchas.
-
Other language references on this site, for the comparisons above: Java, C#, Python, JavaScript, PHP, Kotlin, Rust, C and C++ (the Go reference is a sibling section of this one).
References
-
Ruby from Other Languages, with the C and C++, Java, Perl, PHP and Python guides
-
Ruby 4.0 syntax documentation, methods, exceptions, refinements and pattern matching
-
Object, Kernel, Module, Comparable and Enumerable