Literals
|
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, checked on 2026-10-18). A literal is source text that the parser turns
into an object without any method call on your part: 42, "text", :name, [1, 2], { a: 1 }, 1..5, /re/,
->(x) { x }. Ruby has more literal forms than most languages, and each one has rules about escapes, interpolation,
mutability and how many objects it creates. This page walks through every form listed in the
literals syntax guide, then covers the questions that
cross all of them: which literals create a fresh object on every evaluation, which are frozen, and how the
frozen-string-literal magic comment, the chilled strings introduced in Ruby 3.4 and the command-line switches fit
together.
It builds on Getting Started and
Program Structure and Lexical Elements
(tokens, comments, magic comments, __END__). The classes behind each literal have their own pages:
Numbers, Strings,
Encodings and Unicode,
Symbols, Arrays,
Hashes, Ranges,
Regular Expressions and
Blocks, Procs and Lambdas. Mutability and copying are
treated in depth in
Object Lifecycle, Freezing and Copying.
Every example was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 (the chilled-string example is run
without -w on purpose, and prints its warnings through code) and checked with RuboCop 1.92 using
the configuration of the running example (double-quoted strings, new cops enabled). Output is copied from real runs.
Because this page is about literal forms that style tools often rewrite, a few examples start with a
# rubocop:disable line naming the cop that would object (for example Style/CharacterLiteral) and end with the
matching # rubocop:enable: the line tells you which cop disagrees with the form being demonstrated. Sources are the
Ruby 4.0 documentation. Examples that use the shelf gem are run with
ruby -I lib; see the shelf gem source for its files and Getting Started for the gem layout.
The literal map
| Literal | Class | New object per evaluation? | Frozen? |
|---|---|---|---|
|
|
No, there is exactly one of each |
Yes |
|
|
No, small values are immediate; big values are new objects |
Yes |
|
|
No for most values (immediate "flonum"), possibly yes for extreme magnitudes |
Yes |
|
|
Not guaranteed |
Yes |
|
|
Yes by default; with the magic comment, plain literals are shared and frozen |
Chilled by default (3.4+), frozen with the magic comment |
|
|
Yes, always |
Never (since 3.0) |
|
|
No, symbols are unique by name |
Yes |
|
|
Yes, always |
No |
|
|
Yes, always |
No |
|
|
Literal with literal ends is created once at parse time |
Yes (since 3.0) |
|
|
Literal without interpolation is created once |
Yes (since 3.0) |
|
|
Yes, one per evaluation |
No |
|
|
Runs the command on every evaluation |
No |
The last two columns are verified by the identity example below.
nil, true and false
nil, true and false are keywords that evaluate to the only instances of NilClass, TrueClass and
FalseClass. They cannot be instantiated (NilClass.respond_to?(:new) is false), compare with equal?, and are
frozen. nil and false are the only falsy values: every other object, including 0, "", [] and {}, is
truthy in a condition. nil means "no value", and its conversions return empty values: nil.to_a is [],
nil.to_s is "", nil.to_h is {}. The logical operators &, | and ^ on these three objects are ordinary
methods and always return true or false (never the other operand), unlike && and ||, which are keywords that
return one of their operands; both families are compared in
Operators and Precedence.
# frozen_string_literal: true
p nil.class, true.class, false.class
p nil.to_a, nil.to_s, nil.to_h, nil.inspect
p nil & true, nil | 1, true ^ false, false ^ true
labels = [nil, false, 0, "", [], {}].map { |value| value ? "truthy" : "falsy" }
p labels
p nil.equal?(nil), NilClass.respond_to?(:new)
title = nil
p title.to_s.empty?, title&.upcase
begin
title.upcase
rescue NoMethodError => e
puts e.message
end
Output:
NilClass
TrueClass
FalseClass
[]
""
{}
"nil"
false
true
true
true
["falsy", "falsy", "truthy", "truthy", "truthy", "truthy"]
true
false
true
nil
undefined method 'upcase' for nil
The last lines show the classic failure: calling a method on nil raises NoMethodError. Since Ruby 3.3 the message
names the receiver by class and, since 3.4, quotes the method name with plain single quotes: undefined method 'upcase'
for nil. For nil, true and false the message says for nil, for true and for false; for other objects it
says for an instance of Shelf::Book. Older material shows the Ruby 3.2 and earlier form, undefined method `upcase'
for nil:NilClass. The safe navigation operator &. returns nil instead of raising, and to_s / Array() /
Hash() conversions turn nil into an empty value; see
Conversions and Coercion.
Integers
An integer literal is a sequence of digits. An underscore may separate digits for readability, and a prefix selects the
base. All of the literals below are Integer objects, and Ruby has no separate fixed-width and arbitrary-precision
classes: Integer grows as needed (the old Fixnum and Bignum classes were unified in Ruby 2.4 and their constants
removed in 3.2).
| Form | Base | Notes |
|---|---|---|
|
decimal |
underscores can appear between digits, never doubled, leading, or trailing |
|
decimal |
explicit decimal prefix; rarely useful |
|
hexadecimal |
digits |
|
octal |
a bare leading zero means octal, a classic trap; prefer |
|
binary |
digits |
# frozen_string_literal: true
p 1_234, 0xAA, 0o252, 0b1010_1010
p 0d170, 0252 # rubocop:disable Style/NumericLiteralPrefix
p 170.to_s(2), 170.to_s(16), 170.to_s(8)
p 2**100, (2**100).class
p 7 / 2, -7 / 2, 7 % 3, -7 % 3, 7.fdiv(2), 7.divmod(2)
p 1234.digits, 255.bit_length
p Integer("0x1f", 16), Integer("010"), Integer("0b11", 2)
p "010".to_i, "010".oct, "0x1f".hex
begin
Integer("08")
rescue ArgumentError => e
puts e.message
end
p(-2.abs, -2**2, (-2)**2)
p 1.frozen?, 1.equal?(1)
Output:
1234
170
170
170
170
170
"10101010"
"aa"
"252"
1267650600228229401496703205376
Integer
3
-4
1
2
3.5
[3, 1]
[4, 3, 2, 1]
8
31
8
3
10
8
31
invalid value for Integer(): "08"
2
-4
4
true
true
Source: Literals: Integer Literals
Things worth knowing:
-
Integer#/is floor division and%takes the sign of the divisor (-7 / 2is-4,-7 % 3is2). Usefdivfor a float quotient anddivmodfor both parts. See Numbers. -
A leading minus on a numeric literal is part of the literal:
-2.absis2(the literal-2), but-2**2is-4because**binds tighter than a minus sign applied to an expression. Write(-2)**2for4. -
Integer("010")follows Ruby’s literal rules and returns8, but"010".to_iignores prefixes and returns10, andInteger("08")raisesArgumentError(invalid value for Integer(): "08") because8is not an octal digit. Pass an explicit base (Integer("010", 10)) when parsing user input. -
Malformed literals are syntax errors, reported by the Prism parser (the default parser since Ruby 3.4):
1__0givesinvalid underscore placement in number,1_givestrailing '_' in number,0o9givesinvalid octal number; numeric literal without digitsand0b2the binary equivalent.1.e3is not a float: it parses as a method call and raisesNoMethodError(undefined method 'e3' for an instance of Integer). -
A numeric literal followed by a method call needs no parentheses (
3.times,255.to_s(2)), and integers are immutable objects.
Floats
A float literal needs digits on both sides of the decimal point, or an exponent. 12.34, 1234e-2, 1.234E1 and
1_000.5 are all valid; .5 and 1. are not (1. followed by a name is a method call). Floats are IEEE 754
double-precision numbers, with the usual consequences: not every decimal fraction is representable, comparisons need a
tolerance, and Float::INFINITY and Float::NAN exist as constants (and from division by 0.0; integer division by
zero raises ZeroDivisionError).
# frozen_string_literal: true
p 12.34, 1.234E1, 1_000.5, 1e3, 5e-324
p 1234e-2 # rubocop:disable Style/ExponentialNotation
p 0.1 + 0.2, (0.1 + 0.2 - 0.3).abs < Float::EPSILON
nan = 0.0 / 0.0
p Float::INFINITY, -Float::INFINITY, nan, nan == Float::NAN, nan.nan?
p Float::MAX, Float::MIN, Float::DIG, Float::EPSILON
p 1 / 0.0, 0.0 / 0.0, -1 / 0.0
p 3.7.floor, 3.7.ceil, 3.7.round, 3.75.round(1), 2.5.round, 3.5.round, 2.5.round(half: :even)
p 10.0.to_s, 1e14.to_s, 1e15.to_s, 0.0001.to_s, 0.00009.to_s
p 1.0.eql?(1), 1.0.equal?(1.0), 1.0 == 1 # rubocop:disable Lint/FloatComparison
begin
Integer(1 / 0.0)
rescue FloatDomainError => e
puts e.message
end
Output:
12.34
12.34
1000.5
1000.0
5.0e-324
12.34
0.30000000000000004
true
Infinity
-Infinity
NaN
false
true
1.7976931348623157e+308
2.2250738585072014e-308
15
2.220446049250313e-16
Infinity
NaN
-Infinity
3
4
4
3.8
3
4
2
"10.0"
"100000000000000.0"
"1.0e+15"
"0.0001"
"9.0e-05"
false
true
true
Infinity
Source: Literals: Float Literals
Points to remember:
-
Float#to_sprints the shortest representation that round-trips. It uses plain notation for magnitudes from0.0001up to just below1e15and scientific notation outside that window, as the example shows (1e14is"100000000000000.0",1e15is"1.0e+15",0.0001is"0.0001",0.00009is"9.0e-05"). -
Float::NANis not equal to anything, itself included. Test withnan?.Float::EPSILONis the gap above1.0, the usual tolerance for comparisons:(a - b).abs < Float::EPSILONfor values near one, or a relative tolerance in general. TheLint/FloatComparisoncop flags exact comparisons for this reason. -
Float#roundrounds halves away from zero by default (2.5.roundis3);half: :evenandhalf: :downchange that.Integer(Float::INFINITY)raisesFloatDomainErrorwith the messageInfinity. -
A literal outside the representable range evaluates to
Infinityandruby -wwarnsFloat 1e400 out of range. -
For money and exact decimal arithmetic use
Rational,Integercents, or thebigdecimalgem, which has been a bundled gem rather than part of the default libraries since Ruby 3.4; under Bundler it needs a Gemfile entry (see RubyGems and Bundler).
Rational and imaginary literals
Two numeric suffixes build exact and complex numbers directly in source (both since Ruby 2.1):
-
rmakes aRational:3ris(3/1),2/3ris(2/3),1.2ris(6/5)(the decimal text is converted exactly, not through a float),1_1/2_1ris(11/21), and fractions are reduced automatically. -
imakes aComplexwith a zero real part:2iis(0+2i), and3 + 4iis(3+4i). The two suffixes combine asri(12.3riis (0+(123/10)*i));iris a syntax error.
The slash in 2/3r is the ordinary division operator applied to the integer 2 and the rational 3r, so the
precedence rules of
Operators and Precedence apply. This is why 1.2/3r is
a Float divided by a Rational and produces a float (0.39999999999999997), while 1.2r / 3 is exactly (2/5).
# frozen_string_literal: true
p 1r, 2/3r, -2/3r, 2/-3r, 1.2r, 2/4r, 1_1/2_1r
p 1.2/3r, 1.2r / 3, 3r.class
p 0.1r + 0.2r == 0.3r
p 0.1 + 0.2 == 0.3 # rubocop:disable Lint/FloatComparison
p Rational("1/3"), Rational(0.75), 0.75.to_r, 0.1.rationalize(Rational(1, 100))
p 1i, 1i * 1i, 12.3ri, 3 + 4i, (3 + 4i).abs, (3 + 4i).conjugate, 2i.class
p Complex("1+2i"), Complex(1, 2) * Complex(3, 4)
p((1/3r) + (1/6r))
Output:
(1/1)
(2/3)
(-2/3)
(-2/3)
(6/5)
(1/2)
(11/21)
0.39999999999999997
(2/5)
Rational
true
false
(1/3)
(3/4)
(3/4)
(1/10)
(0+1i)
(-1+0i)
(0+(123/10)*i)
(3+4i)
5.0
(3-4i)
Complex
(1+2i)
(-5+10i)
(1/2)
Source: Literals: Rational Literals
The class methods Kernel#Rational and Kernel#Complex parse strings (Rational("1/3")) and convert floats
(Rational(0.75) is exact because 0.75 is a dyadic fraction; 3.14.to_r shows the huge exact ratio of the binary
value, so prefer rationalize or a r literal for decimal data). RuboCop’s Layout/SpaceAroundOperators dislikes
spaces around the slash of a rational literal, so the documented style is 2/3r without spaces. See
Numbers for the full numeric tower.
Strings
The most common forms are double-quoted and single-quoted strings. Both produce String objects; the difference is
what the parser does with the text.
-
Double quotes process escape sequences and interpolation. Any expression may appear inside
#{...}; its value is converted withto_s(sonilbecomes the empty string, a symbol becomes its name, an array becomes itsinspectform). The shorthand#@ivar,#@@cvarand#$gvarinterpolates a variable without braces. -
Single quotes disable interpolation and every escape except
\\(a backslash) and\'(a single quote). A backslash before any other character is kept literally. -
A string literal may span lines, and a backslash at the end of a line inside a double-quoted string joins the next line (a "continuation", an empty string).
# frozen_string_literal: true
author = "Ursula Le Guin"
count = 3
puts "Double: 'single' and \"escaped\""
puts "#{author} wrote #{count} books (#{count * 2} copies)"
puts "Tab:\tNewline:\\n Unicode: é \u{1F4DA} Hex: \x41 Octal: \101"
p "\a", "\e", "\s", "\cA", "\c?", "\M-a", "\u0000", "\0"
puts "Unknown escape: \q \z"
puts "line one \
continued"
missing = nil
symbol = :sym
list = [1, "two", nil]
ratio = 1.0
p "nil: #{missing}|", "sym: #{symbol}", "arr: #{list}", "float: #{ratio}"
p "café".length, "café".bytesize, "café".encoding, __ENCODING__
Output:
Double: 'single' and "escaped"
Ursula Le Guin wrote 3 books (6 copies)
Tab: Newline:\n Unicode: é 📚 Hex: A Octal: A
"\a"
"\e"
" "
"\u0001"
"\u007F"
"\xE1"
"\u0000"
"\u0000"
Unknown escape: q z
line one continued
"nil: |"
"sym: sym"
"arr: [1, \"two\", nil]"
"float: 1.0"
4
5
#<Encoding:UTF-8>
#<Encoding:UTF-8>
Escape sequences
Escape sequences are available in double-quoted strings, character literals, non-quoted and double-quoted heredocs,
backtick strings, double-quoted symbols and %Q, %W, %I and % literals. An unknown escape such as \q yields
the character itself ("q"), as the example output shows.
| Escape | Meaning |
|---|---|
|
bell (07), backspace (08), horizontal tab (09), newline (0A) |
|
vertical tab (0B), form feed (0C), carriage return (0D), escape (1B) |
|
space (20), useful at the end of a line in a squiggly heredoc or a |
|
a backslash |
|
octal byte value of one to three digits ( |
|
hexadecimal byte value of one or two digits |
|
Unicode code point with exactly four hex digits |
|
one or more code points, each of one to six hex digits, separated by spaces ( |
|
control character ( |
|
meta character (sets the high bit), meta-control |
a backslash then a newline |
line continuation: an empty string, used to fold a long literal |
Two cautions follow from the table. First, \x and octal escapes insert bytes, so "\xff" is an invalid UTF-8
string; \u inserts a character and is the right tool for text. Second, the encoding of a literal comes from the
source file (UTF-8 by default since Ruby 2.0, __ENCODING__), and a \u escape forces UTF-8. Details, including
force_encoding, valid_encoding? and transcoding, are in
Encodings and Unicode.
Single quotes, adjacent literals and character literals
# frozen_string_literal: true
# rubocop:disable Style/StringLiterals, Lint/InterpolationCheck, Lint/ImplicitStringConcatenation, Style/CharacterLiteral, Style/RedundantPercentQ
p 'No #{interpolation} and no \n escape'
p 'Only \\ and \' are escapes, so \q stays as is'
p 'a\nb'.length, "a\nb".length
p "con" "cat" "en" "ation"
p %q(percent) 'single' "double"
p "This string contains " \
"no newlines."
p ?a, ?\n, ?\s, ?\u{41}, ?あ, ?\C-a
# rubocop:enable Style/StringLiterals, Lint/InterpolationCheck, Lint/ImplicitStringConcatenation, Style/CharacterLiteral, Style/RedundantPercentQ
Output:
"No \#{interpolation} and no \\n escape"
"Only \\ and ' are escapes, so \\q stays as is"
4
3
"concatenation"
"percentsingledouble"
"This string contains no newlines."
"a"
"\n"
" "
"A"
"あ"
"\u0001"
-
Adjacent string literals are concatenated at parse time, whatever their quoting:
"con" "cat". A percent literal may be concatenated only if it is not the last piece. In"a" 'b' %q(c)the parser reads the%as the modulo operator and warns'%' after local variable or literal is interpreted as binary operator even though it seems like string literal; the program then fails with aNameErrorforc. Putting the percent literal first works (%q(percent) 'single' "double"). -
?ais the character literal: a question mark and one character or escape produces a one-character string (?\n,?\u{41},?\C-a). More than one character (?abc) is a syntax error. RuboCop’sStyle/CharacterLiteralprefers"a", and the form is rarely used outside golfing and old code. -
Interpolation inside a single-quoted string is not performed, which RuboCop’s
Lint/InterpolationCheckreports because'#{x}'is usually a mistake.
Percent literals
A percent sign followed by a letter and a delimiter writes a literal without needing to escape quotes. The delimiter
may be any non-alphanumeric ASCII character; the four bracket pairs (), [], {} and <> nest, so they may appear
unescaped inside as long as they are balanced. Any delimiter can be escaped with a backslash.
| Form | Produces | Interpolation and escapes |
|---|---|---|
|
a |
none; only |
|
a |
both |
|
an |
none; a backslash escapes a space |
|
an |
both, and escapes like |
|
an |
none |
|
an |
both |
|
one |
none |
|
a |
as a regexp literal, so |
|
the output of a shell command |
as backticks |
# frozen_string_literal: true
# rubocop:disable Style/PercentLiteralDelimiters, Style/RedundantPercentQ, Style/BarePercentLiterals, Style/WordArray, Style/SymbolArray
p %q(It's "quoted" #{not_interpolated})
p %Q(Total: #{1 + 1} \t tab)
p %(Parens (nested) are fine: #{2 * 3})
p %q[brackets [nested] ok], %q{braces {nested} ok}, %q<angle <nested> ok>
p %q|pipes|, %q:colons:, %q!bang!
p %q(escaped \) paren and backslash \\ and \n kept)
p %w[Dune Emma Ulysses], %W[#{1 + 1}a b\ c]
p %i[available lent overdue], %I[state_#{1 + 1} other]
p %s(a symbol with spaces)
p %r{/books/(\d+)}i
# rubocop:enable Style/PercentLiteralDelimiters, Style/RedundantPercentQ, Style/BarePercentLiterals, Style/WordArray, Style/SymbolArray
Output:
"It's \"quoted\" \#{not_interpolated}"
"Total: 2 \t tab"
"Parens (nested) are fine: 6"
"brackets [nested] ok"
"braces {nested} ok"
"angle <nested> ok"
"pipes"
"colons"
"bang"
"escaped ) paren and backslash \\ and \\n kept"
["Dune", "Emma", "Ulysses"]
["2a", "b c"]
[:available, :lent, :overdue]
[:state_2, :other]
:"a symbol with spaces"
/\/books\/(\d+)/i
Source: Literals: Percent Literals
Remarks:
-
Whitespace separates the words of
%wand%i: space, tab, newline, carriage return, form feed and vertical tab. Nested delimiters do not create nested arrays; the result is always flat (%w[foo[bar baz]qux]is["foo[bar", "baz]qux"]). -
The
%Wand%Iforms run interpolation per word, and an interpolation that yields a string with spaces stays one element. -
RuboCop’s defaults prefer
%w/%ifor arrays of three or more simple words, square brackets for%w/%i, parentheses for%q/%Q/%s/%xand braces for%r(Style/PercentLiteralDelimiters), and discourage%qand%Qwhen a plain quote works (Style/RedundantPercentQ). Those are style opinions, not language rules.
Heredocs
A here document writes a multi-line string without escaping quotes. The opener <<IDENTIFIER is an expression
that can be followed by more code on the same line (a method call, an argument separator, a closing parenthesis); the
body starts on the next line and runs until a line containing only the identifier.
-
<<IDkeeps all whitespace, and the terminator must start in column 0. -
<<-IDallows the terminator to be indented, but the body keeps its indentation. -
<<~ID(the squiggly heredoc, Ruby 2.3) also removes the indentation of the least-indented non-blank line from every line. Blank lines and lines of only whitespace are ignored when measuring. For the measurement a tab counts as advancing to the next multiple of eight columns. Interpolated text is inserted after the dedent and is never re-indented, as theItems:case in the example shows. -
The identifier may be quoted.
<<~'ID'disables interpolation and escapes (a "raw" heredoc),<<~"ID"is the default behaviour and allows any character except the quote and a newline in the identifier, and an identifier written in backticks (<<~followed byIDin backticks) runs the body as a shell command, like a backtick literal. -
Several heredocs can be opened on the same line; their bodies follow in order. This is legal but hard to read.
-
A method call goes after the opener (
<<~TEXT.lines.map(&:strip)), not after the terminator.
# frozen_string_literal: true
# rubocop:disable Layout/HeredocIndentation, Layout/ClosingHeredocIndentation
title = "Dune"
plain = <<TEXT
Plain heredoc for #{title}.
Indentation is kept.
TEXT
dash = <<-TEXT
Dash form keeps leading spaces for #{title}.
TEXT
squiggly = <<~TEXT
Squiggly form removes the common indent.
Relative indent is kept for #{title}.
Blank lines do not count.
TEXT
raw = <<~'TEXT'
Raw: #{title} and \n are literal.
TEXT
shout = <<~TEXT.upcase.lines.map(&:strip)
first line
second line
TEXT
p plain, dash, squiggly, raw, shout
def render(header, body) = "#{header.strip}|#{body.strip}"
puts render(<<~HEAD, <<~BODY)
Catalogue
HEAD
Entry: #{title}
BODY
tabbed = <<~TEXT
tab then text
two spaces
TEXT
p tabbed
titles = %w[Dune Emma]
interp = <<~TEXT
Items:
#{titles.join("\n")}
TEXT
p interp
quoted = <<~"QUOTED TEXT"
Any quoted identifier works: #{title}
QUOTED TEXT
p quoted
# rubocop:enable Layout/HeredocIndentation, Layout/ClosingHeredocIndentation
Output:
"Plain heredoc for Dune.\n Indentation is kept.\n"
" Dash form keeps leading spaces for Dune.\n"
"Squiggly form removes the common indent.\n Relative indent is kept for Dune.\n\nBlank lines do not count.\n"
"Raw: \#{title} and \\n are literal.\n"
["FIRST LINE", "SECOND LINE"]
Catalogue|Entry: Dune
"\ttab then text\ntwo spaces\n"
"Items:\nDune\nEmma\n"
"Any quoted identifier works: Dune\n"
Source: Literals: Here Document Literals
Style guidance reflected by RuboCop: prefer <<~ (Layout/HeredocIndentation flags << with an indented body),
align the closing identifier with the opener (Layout/ClosingHeredocIndentation), and use a meaningful delimiter
such as TEXT, SQL or HEADER rather than EOS or END (Naming/HeredocDelimiterNaming). With the magic comment
frozen_string_literal: true, a heredoc without interpolation is frozen and one with interpolation is not, like
any other string literal (see Frozen and chilled string literals).
Symbols
A symbol literal starts with a colon. Symbols are immutable, interned names: every use of :available is the same
object for the life of the process, so they make cheap hash keys, method names and state labels (the shelf Loan
class keeps its states as symbols). The forms are:
-
plain names:
:title,:Constant,:empty?,:save!,:name=,:@ivar,:@@cvar,:$global; -
operator names:
:+,:[]=,:<=>,:!,:=~; -
quoted names for anything else:
:"quoted symbol",:"9lives", and a double-quoted form with interpolation and escapes, :"with#{1 + 1}"; a single-quoted form:'raw #{x}'suppresses both; -
%s(name)and the arrays%i[a b]/%I[a#{x} b]; -
in a hash literal,
title:and"quoted key":build symbol keys (see Hashes).
# frozen_string_literal: true
# rubocop:disable Lint/SymbolConversion
p :available, :"quoted symbol", :"with#{1 + 1}", :'raw #{x}', %s(spaced out)
p :+, :[]=, :empty?, :save!, :name=, :Constant, :@ivar, :$global, :<=>, :"9lives"
p :"foo\sbar", :a <=> :b, %i[a b]
p :a.equal?(:a), "a".to_sym.equal?(:a), :a.frozen?, "a".equal?("a")
p :title.to_s, :title.size, :upcase.to_proc.call("Dune")
p :"".inspect, :"a-b", :"a b".to_proc.class
# rubocop:enable Lint/SymbolConversion
Output:
:available
:"quoted symbol"
:with2
:"raw \#{x}"
:"spaced out"
:+
:[]=
:empty?
:save!
:name=
:Constant
:@ivar
:$global
:<=>
:"9lives"
:"foo bar"
-1
[:a, :b]
true
true
true
true
"title"
5
"DUNE"
":\"\""
:"a-b"
Proc
Source: Literals: Symbol Literals
Symbol#inspect shows the shortest valid literal, adding quotes only when needed (:"quoted symbol", :"9lives",
:"a-b"). Dynamic symbols created with String#to_sym or :"..#{}.." are garbage collected since Ruby 2.2, so the
old advice about symbol leaks from user input no longer applies in the same form, but they still cost lookups and have
no use as free-form text. Symbol#to_proc (map(&:upcase)) has existed since Ruby 1.8.7. The string a symbol returns
from to_s is chilled since Ruby 3.4 (mutating it warns under -W:deprecated), while Symbol#name returns a frozen
string. The full symbol API is in Symbols.
Arrays
An array literal is a comma-separated list between brackets. Elements are arbitrary expressions evaluated left to
right, nesting is free, and a trailing comma is allowed (RuboCop’s Style/TrailingCommaInArrayLiteral forbids it on a
single line). The splat operator * expands an enumerable in place ([*isbns, "extra"], [*1..3, *"a".."c"]), and
[*nil] is []. %w and %i are shorthands for arrays of strings and symbols.
# frozen_string_literal: true
isbns = %w[9780134685991 9780596516178]
p [1, 1 + 1, [1 + 2, [4]]], [], %w[a b], %i[x y]
p [*isbns, "extra"], [*1..3, *"a".."c"]
p [*nil], [*[1, 2]], [*"str"] # rubocop:disable Lint/RedundantSplatExpansion
p Array.new(2) { [] }, Array.new(2, 0)
shared = Array.new(2, [])
shared[0] << :oops
p shared
fresh = Array.new(2) { [] }
fresh[0] << :ok
p fresh
def literal = []
p literal.equal?(literal), literal.frozen?
p [[1, 2], [3, 4]].to_h, Array(nil), Array(1..3), Array("a\nb")
first, (second, third), *rest = 1, [2, 3], 4, 5 # rubocop:disable Style/ParallelAssignment
p [first, second, third, rest]
Output:
[1, 2, [3, [4]]]
[]
["a", "b"]
[:x, :y]
["9780134685991", "9780596516178", "extra"]
[1, 2, 3, "a", "b", "c"]
[]
[1, 2]
["str"]
[[], []]
[0, 0]
[[:oops], [:oops]]
[[:ok], []]
false
false
{1 => 2, 3 => 4}
[]
[1, 2, 3]
["a\nb"]
[1, 2, 3, [4, 5]]
Source: Literals: Array Literals
Two traps appear in the example:
-
Array.new(2, [])stores the same inner array twice, so appending through one index changes both. The block formArray.new(2) { [] }evaluates the block per slot and creates distinct arrays. -
An array literal inside a method creates a new array on every call (
literal.equal?(literal)isfalse), which is whyStyle/MutableConstantasks you to freeze arrays assigned to constants.
first, (second, third), *rest = 1, [2, 3], 4, 5 is a multiple assignment, which also uses array syntax on its
right side; see
Assignment and Multiple Assignment.
Hashes
A hash literal is a comma-separated list of pairs between braces. Two syntaxes exist, and they can be mixed in one literal:
-
key: valuecreates a symbol key and is the idiomatic form for identifiers and for keyword arguments (the same syntax is used when calling a method with keywords). -
expression => valueallows any object as the key: strings, numbers, arrays,nil.
Further rules:
-
A quoted key,
"a b": 1, is a symbol key (:"a b"), not a string. Interpolation works ("c#{1 + 1}": 2 isc2:), andif:orclass:are fine because the colon makes it a label. -
Value omission (Ruby 3.1):
{ title:, year: }means{ title: title, year: year }, taking the value from the local variable or method of the same name. The same works in keyword arguments (Shelf::Isbn.new(digits:)). -
The double splat
**othermerges another hash in place, and later entries win, so{ **defaults, loan_days: 21 }overrides while{ loan_days: 21, **defaults }is overridden. -
Since Ruby 3.4,
Hash#inspectprints{name: "Ico", "k" => 1, 1 => 2}: symbol keys that can be written as labels usekey: value, other keys have spaces around=>. Output from earlier versions ({:name=>"Ico"}) is obsolete. -
A string used as a key is duplicated and frozen by the hash, so later mutation of the original does not corrupt the table (
h.keys.first.equal?(key)isfalse). -
Insertion order is preserved: reassigning an existing key keeps its position, deleting and re-adding moves it to the end.
-
The default value of
Hash.new([])is one shared array:counts[:a] << 1mutates the default and stores nothing (the example prints{}and[1]). UseHash.new { |hash, key| hash[key] = [] }. RuboCop reports this asLint/SharedMutableDefault.
# frozen_string_literal: true
title = "Dune"
year = 1965
p({ title:, year: })
p({ "title" => title, 1 => :one, nil => 0, [1, 2] => :pair, title: })
p({ "a b": 1, "c#{1 + 1}": 2, "a-b": 3, "9x": 4, if: 5, "x=": 6 })
defaults = { loan_days: 14, renewals: 1 }
p({ **defaults, loan_days: 21 }, { loan_days: 21, **defaults })
p({ a: 1, "a" => 2 }.keys, { 1 => :int, 1.0 => :float }.size)
key = +"mutable"
h = { key => 1 }
key << "!"
p h, h.keys.first.frozen?, h.keys.first.equal?(key)
p({}.class, {}.default, Hash.new(0)[:missing])
counts = Hash.new([]) # rubocop:disable Lint/SharedMutableDefault
counts[:a] << 1
p counts, counts[:b], counts.default
p({ a: 1 }.inspect, { "a" => 1 }.to_s, { 1 => { b: [nil] } }.inspect)
h2 = { b: 1, a: 2 }
h2[:c] = 3
h2.delete(:b)
h2[:b] = 4
p h2
Output:
{title: "Dune", year: 1965}
{"title" => "Dune", 1 => :one, nil => 0, [1, 2] => :pair, title: "Dune"}
{"a b": 1, c2: 2, "a-b": 3, "9x": 4, if: 5, "x=": 6}
{loan_days: 21, renewals: 1}
{loan_days: 14, renewals: 1}
[:a, "a"]
2
{"mutable" => 1}
true
false
Hash
nil
0
{}
[1]
[1]
"{a: 1}"
"{\"a\" => 1}"
"{1 => {b: [nil]}}"
{a: 2, c: 3, b: 4}
Source: Literals: Hash Literals
Syntax pitfalls:
-
A pair of braces at the start of a statement or as the sole argument of a command call is parsed as a block:
puts {}callsputswith an empty block and prints a blank line. Parenthesize (puts({})). -
m(a: 1)andm({ a: 1 })both pass a hash to a method taking one positional parameter; with keyword parameters the two are different since Ruby 3.0 (see Calling Methods and Argument Forwarding). -
A literal with the same key twice keeps the last value and
ruby -wwarns, for examplekey :a is duplicated and overwritten on line 1. The warning notesa:and"a":as the same key.
The complete method list is in Hashes.
Ranges
A range literal is two expressions joined by .. (inclusive) or ... (exclusive of the end). Either end may be
omitted: (1..) is endless and (..5) is beginless. (nil..nil) has neither bound. A range is not limited to
numbers: it works for any object that implements <=> and, for iteration, succ (strings, dates, your own classes).
# frozen_string_literal: true
p [1..5, 1...5, (1..), (..5), (1..nil), (nil..nil), ("a".."e")]
p (1..5).to_a, (1...5).size, ("a".."e").to_a, (1.0..2.0).include?(1.5)
p (1..).first(3), (..5).include?(3), (1..10) % 3, (1..10).step(3).to_a
p (1..5).frozen?, Range.new(1, 5, true), (1..5).eql?(1..5), (1..5).equal?(1..5)
begin
(..5).each { |n| puts n }
rescue TypeError => e
puts e.message
end
begin
(1.0..2.0).each { |n| puts n }
rescue TypeError => e
puts e.message
end
n = 3
p (1..n + 1).to_a, 1..n # rubocop:disable Lint/AmbiguousRange
p (1..10).sum, (1...1).to_a, (5..1).to_a, 5.downto(1).to_a
p (1..10) === 5, Integer === 5, # rubocop:disable Style/CaseEquality
(1..3).cover?(1..2), ("a".."z").cover?("mm"), ("a".."z").include?("mm")
p (..-1), (3..).size
Output:
[1..5, 1...5, 1.., ..5, 1.., nil..nil, "a".."e"]
[1, 2, 3, 4, 5]
4
["a", "b", "c", "d", "e"]
true
[1, 2, 3]
true
((1..10).%(3))
[1, 4, 7, 10]
true
1...5
true
false
can't iterate from NilClass
can't iterate from Float
[1, 2, 3, 4]
1..3
55
[]
[]
[5, 4, 3, 2, 1]
true
true
true
true
false
..-1
Infinity
Source: Literals: Range Literals
What to note:
-
Endless ranges (Ruby 2.6) and beginless ranges (Ruby 2.7) need parentheses when used as an argument or receiver, because
1..followed by a newline or comma is ambiguous.(1..).first(3)is[1, 2, 3], and(3..).sizeisInfinity. -
(..5).eachraisesTypeError(can't iterate from NilClass), and so does iterating a float range (can't iterate from Float);include?andcover?still work on them. -
The step operator
(1..10) % 3(Ruby 2.6) returns an arithmetic sequence likestep(3). -
Precedence:
..binds more loosely than arithmetic, so1..n + 1means1..(n + 1); RuboCop’sLint/AmbiguousRangeasks you to wrap complex bounds. -
A range whose ends are literals is created once; all range objects are frozen since Ruby 3.0, including
Range.new(1, 5, true). -
cover?compares with<=>, whileinclude?iterates for non-numeric ranges, so("a".."z").cover?("mm")istrueand("a".."z").include?("mm")isfalse. A range in acase/whenuses===, which iscover?. -
Ranges in conditions behave as flip-flops in some positions; see Operators and Precedence.
More in Ranges.
Regexp literals
/pattern/ creates a Regexp; %r{pattern} does the same with another delimiter, which avoids escaping slashes.
Trailing letters set modes: i ignores case, m makes . match newlines (Ruby’s name for dot-all; ^ and $ always
match at line boundaries), x allows whitespace and # comments in the pattern, o interpolates once, and u, n,
e, s choose the encoding. Interpolation (/#{digits}/) is allowed; use Regexp.escape for text that must be
matched literally. A regexp literal is frozen since Ruby 3.0, and a literal without interpolation is created once, so
it is cheap in loops (though two textually identical literals are two objects).
If a regexp literal with named groups appears on the left of =~, the named captures become local variables, as
the example shows with prefix and rest. An invalid pattern in a literal is a syntax error; with Regexp.new it is
a RegexpError at run time (end pattern with unmatched parenthesis: /(/).
# frozen_string_literal: true
p(/foo/, %r{foo/bar}, /foo/i, /a b # note/x, /a.b/m)
p(/\d+/.class, /x/.frozen?, /x/.equal?(/x/), /x/.source, /x/i.options)
isbn = "978-0134685991"
p isbn.match?(/\A\d{3}-\d{10}\z/), isbn =~ /\d{10}/, Regexp.last_match(0)
if /(?<prefix>\d{3})-(?<rest>\d+)/ =~ isbn
p prefix, rest
end
digits = "13"
p(/#{digits}/, /#{Regexp.escape('a.b')}/, /#{digits}/o.source)
p Regexp.union("a.b", /c|d/), Regexp.escape("1+1=2?")
begin
Regexp.new("(")
rescue RegexpError => e
puts e.message
end
p "x" !~ /y/
Output:
/foo/
/foo\/bar/
/foo/i
/a b # note/x
/a.b/m
Regexp
true
false
"x"
1
true
4
"0134685991"
"978"
"0134685991"
/13/
/a\.b/
"13"
/a\.b|(?-mix:c|d)/
"1\\+1=2\\?"
end pattern with unmatched parenthesis: /(/
true
Source: Literals: Regexp Literals
Behind the syntax lie flavour details (Onigmo, lookbehind, \h, \R, absolute anchors \A and \z, Regexp.timeout
as a defence against catastrophic backtracking) covered in
Regular Expressions. Note the use of \A and \z rather
than ^ and $ to validate whole strings, which is the safe habit for input checking.
Lambda literals
->(params) { body } creates a lambda, a Proc that checks its arity and returns from itself. The parentheses
around the parameters are optional (-> x { x }), a lambda without parameters is -> { ... }, parameters may have
defaults, a rest parameter, keywords and a block parameter, and do ... end can replace the braces
(->(x) do ... end). A lambda is called with call, .(), [] or yield; lambda? is true, and calling with the
wrong number of arguments raises ArgumentError (wrong number of arguments (given 0, expected 1)). The numbered
parameters _1, _2 and the it parameter (Ruby 3.4) can replace explicit parameters in a one-expression body.
# frozen_string_literal: true
add_one = ->(value) { value + 1 }
greet = -> { "hello" }
loose = lambda do |name, punctuation = "!"|
"#{name}#{punctuation}"
end
multi = ->(first, second = 2, *rest, key:, opt: 1, **others, &blk) { [first, second, rest, key, opt, others, blk] }
p add_one.call(1), add_one.(1), add_one[1], add_one.yield(1) # rubocop:disable Style/LambdaCall
p greet.call, loose.call("Dune"), loose.call("Dune", "?")
p add_one.lambda?, add_one.arity, multi.arity, multi.parameters
p proc { |a, b| [a, b] }.call(1), proc { |a, b| [a, b] }.call([1, 2])
begin
add_one.call
rescue ArgumentError => e
puts e.message
end
p [1, 2, 3].map { it * 2 }, [1, 2, 3].map(&add_one)
p add_one.class, add_one.curry.class
p ->(x) { x }.frozen?
Output:
2
2
2
2
"hello"
"Dune!"
"Dune?"
true
1
-3
[[:req, :first], [:opt, :second], [:rest, :rest], [:keyreq, :key], [:key, :opt], [:keyrest, :others], [:block, :blk]]
[1, nil]
[1, 2]
wrong number of arguments (given 0, expected 1)
[2, 4, 6]
[2, 3, 4]
Proc
Proc
false
Source: Literals: Lambda Proc Literals
Differences from a proc { }: proc { |a, b| } accepts too few or too many arguments and destructures a single
array argument ([1, nil] and [1, 2] in the example), while a lambda is strict. Kernel#lambda with a
non-literal block raises ArgumentError since Ruby 3.3. The literal is different from a block: a block is not an
object until captured with &. See Blocks, Procs and Lambdas.
Backticks and %x
Text in backticks, or in a %x literal, is interpolated like a double-quoted string and then run as a shell command;
the literal evaluates to the command’s standard output, and Process.last_status (the $? variable) holds the
Process::Status. A command that cannot be found raises Errno::ENOENT; a command that exits non-zero does not raise.
A heredoc opened with backticks does the same for its whole body.
# frozen_string_literal: true
version = `echo shelf`
p version, Process.last_status.success?, Process.last_status.exitstatus, Process.last_status.class
p %x(echo "#{1 + 1}" done) # rubocop:disable Style/CommandLiteral
p `exit 3`, Process.last_status.exitstatus
begin
`no-such-command-xyz`
rescue Errno::ENOENT => e
puts e.message
end
puts <<~`SHELL`
echo heredoc runs a shell
SHELL
Output:
"shelf\n"
true
0
Process::Status
"2 done\n"
""
3
No such file or directory - no-such-command-xyz
heredoc runs a shell
Source: Literals: %x Backtick Literals
A command built from untrusted input is a command-injection hole: never interpolate user data into a backtick
literal. Prefer Open3.capture2e("cmd", arg) or system("cmd", arg) with separate arguments, which bypass the
shell. See Processes, Signals and Shell and
Security.
Pseudo-variables and other literal-like keywords
A few keywords evaluate like literals but depend on where they appear:
-
__FILE__is the path of the current source file,__LINE__its line number,__dir__(a method) the directory of the file,__method__the name of the current method (as a symbol, ornilat top level) and__ENCODING__the encoding of the source file. -
selfis the current object (mainat top level). -
__END__on a line by itself ends the program text; everything after it is readable through theDATAconstant, an openFilepositioned after the marker, but only in the main script.
# frozen_string_literal: true
def where = [__method__, __LINE__]
p __FILE__, __dir__.class, __LINE__, __ENCODING__, where, self, self.class
p defined?(DATA), DATA.read
__END__
Dune,9780441172719
Output:
"pseudo.rb"
String
5
#<Encoding:UTF-8>
[:where, 3]
main
Object
"constant"
"Dune,9780441172719\n"
Source: Miscellaneous Syntax
if __FILE__ == $PROGRAM_NAME is the usual guard that runs code only when a file is executed directly rather than
required; it is covered in
Command-Line Programs.
Which literals create a new object
The question matters for performance, for aliasing bugs and for thread and Ractor sharing. The rules, checked below:
-
Evaluating an array or hash literal creates a new, mutable object every time. A string literal creates a new mutable one every time unless the magic comment is on, in which case every evaluation of the same literal returns the same frozen object (and identical literals in the same file are deduplicated).
-
Integers (of immediate size), symbols,
nil,true,falseand most floats are immediate values: no allocation, always identical. -
Range and regexp literals without interpolation are created once at parse time and frozen.
# frozen_string_literal: true
def objects
[[1, 2], { a: 1 }, +"text", "text", 1..2, /re/, :sym, 42, 4.2, nil]
end
first = objects
second = objects
first.zip(second).each do |a, b|
puts format("%-10<kind>s same object: %-5<same>s frozen: %<frozen>s",
kind: a.class, same: a.equal?(b), frozen: a.frozen?)
end
Output:
Array same object: false frozen: false
Hash same object: false frozen: false
String same object: false frozen: false
String same object: true frozen: true
Range same object: true frozen: true
Regexp same object: true frozen: true
Symbol same object: true frozen: true
Integer same object: true frozen: true
Float same object: true frozen: true
NilClass same object: true frozen: true
The +"" and -"" unary operators give explicit control: +str returns the string itself if it is mutable and a
mutable copy if it is frozen or chilled; -str returns a frozen, deduplicated copy. dup and String.new always
produce a mutable string.
Frozen and chilled string literals
Strings are the one literal whose mutability depends on configuration. Three mechanisms interact.
The magic comment
A comment at the top of a file (in the first comment section, before any code) controls the literals of that file only:
# frozen_string_literal: true
With it, a plain string literal is allocated once at parse time and frozen. Interpolated strings, String.new,
dup, ""` and string concatenation results are not frozen (interpolated literals stopped being frozen in Ruby 3.0).
A string literal with `# frozen_string_literal: false+ is mutable and unchanged. The default is no comment, which is
now "chilled" (next section). Emacs-style -*- frozen_string_literal: true -*- also works. Every library file of the
shelf gem starts with the comment, and RuboCop’s Style/FrozenStringLiteralComment enforces it.
# frozen_string_literal: true
title = "Dune"
p title.frozen?, "Dune".equal?(title), "Dun" "e".frozen? # rubocop:disable Lint/ImplicitStringConcatenation
count = 1
p "a#{count}b".frozen?, <<~TEXT.frozen?, <<~TEXT.frozen?
plain heredoc
TEXT
interpolated #{title}
TEXT
p %w[a b].first.frozen?, :sym.to_s.frozen?, 1.to_s.frozen?, nil.to_s.frozen?
begin
title << " Messiah"
rescue FrozenError => e
puts e.message
p e.receiver.equal?(title)
end
copy = title.dup
copy << " Messiah"
unfrozen = +title
unfrozen << "!"
p copy, unfrozen, (+title).equal?(title), (+copy).equal?(copy), (-copy).frozen?
p String.new("Dune").frozen?, String.new.encoding, "".encoding, String.new(capacity: 64).encoding
buffer = +""
buffer << "built " << "in place"
p buffer, buffer.frozen?
Output:
true
true
true
false
true
false
true
false
false
true
can't modify frozen String: "Dune"
true
"Dune Messiah"
"Dune!"
false
true
true
false
#<Encoding:BINARY (ASCII-8BIT)>
#<Encoding:UTF-8>
#<Encoding:BINARY (ASCII-8BIT)>
"built in place"
false
Details shown by the example:
-
The literal and a second occurrence of the same literal are the same object (
"Dune".equal?(title)). -
FrozenErrorhas areceiverand the messagecan't modify frozen String: "Dune". -
String.newwith no argument returns an empty string with the BINARY (ASCII-8BIT) encoding, while""` is UTF-8 because it copies the literal. Use `""orString.new(capacity: 64, encoding: "UTF-8")for buffers of text. -
+titlereturned a copy because the literal is frozen;+copyreturnedcopyitself because it was already mutable.
Chilled strings (Ruby 3.4 and later)
When a file has no magic comment, string literals (and the string returned by Symbol#to_s) are chilled: they behave
as mutable strings and frozen? returns false, but Ruby remembers that the literal is "supposed" to be frozen in
the future. The first mutation of a chilled string emits a warning of the deprecated category and then turns the
string into an ordinary mutable one. By default the deprecated category is off, so nothing is printed; it is
enabled with ruby -w, ruby -W:deprecated, RUBYOPT=-W:deprecated or Warning[:deprecated] = true.
# rubocop:disable Style/FrozenStringLiteralComment
# No magic comment: string literals are "chilled".
title = "Dune"
p title.frozen?
title << " Messiah"
p title
Warning[:deprecated] = true
other = "Emma"
other << " Woodhouse"
other << " again"
p other
literal = :sym.to_s
literal << "bol"
p literal
p "x".equal?("x")
# rubocop:enable Style/FrozenStringLiteralComment
Output:
false
"Dune Messiah"
"Emma Woodhouse again"
"symbol"
false
Standard error (the deprecation warnings):
chilled.rb:11: warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)
chilled.rb:15: warning: string returned by :sym.to_s will be frozen in the future
The warning appears once per string. It names the line and offers --debug-frozen-string-literal, which adds a second
line saying where the string was created:
chilled.rb:6: warning: literal string will be frozen in the future
chilled.rb:4: info: the string was created here
Source: Warning.categories
(That is the real output of the first mutation in the example when it is run with -W:deprecated
--debug-frozen-string-literal, chilled.rb being the file name used here.) The category is documented in
Warning.categories, which lists :deprecated,
:experimental, :performance and :strict_unused_block. The String#+@ operator was changed in 3.4 to return a
copy of a chilled string, so +"text" is the portable way to ask for a mutable string.
Command-line switches and environment
| Switch | Effect |
|---|---|
|
Freezes every string literal in files without an explicit comment, as if each had |
|
Turns chilled behaviour off: literals are plain mutable strings and no deprecation warning is produced. This is the documented way to silence the 3.4 warning. |
|
Records where each chilled or frozen literal was created; the warning or the |
|
Enables or disables the |
|
The same switches through the environment, for gems and test runs without editing a command line; a source file’s own magic comment wins. |
Running the earlier chilled example with --enable-frozen-string-literal stops at the first mutation with
chilled.rb:6:in '<main>': can't modify frozen String: "Dune" (FrozenError).
Where this is heading
Strings are still not frozen by default in Ruby 4.0; the discussion of making them frozen is the open bug tracker
tracker item Feature #20205 ("Enable frozen_string_literal by
default"), still open, and chilled strings are the migration path: Ruby warns now so code can be fixed before any change. The practical
advice is:
-
Put
# frozen_string_literal: truein every file you write and let RuboCop enforce it. -
Build strings you will mutate with
""`, `String.new` (mind the encoding), `dup` or `<<` on a `""buffer; prefer"#{a} #{b}"orArray#jointo repeated<<. -
Run the test suite once with
-W:deprecated(orRUBYOPT=-W:deprecated) and fix the warnings; run it once with--enable-frozen-string-literalto find the rest. -
Do not add
.freezeto a string literal when the magic comment is present: it is redundant (Style/RedundantFreeze), andStyle/MutableConstantstill asks you to freeze arrays, hashes and interpolated strings assigned to constants.
Freezing is not deep (a frozen array can hold mutable strings) and the shareable_constant_value magic comment, the
Ractor.make_shareable method and Object#freeze are discussed in
Object Lifecycle, Freezing and Copying
and Ractors.
Literals in the shelf scenario
The running example uses literals everywhere: a heredoc of CSV-like rows, %w-style words, hash value omission through
keyword arguments, and a frozen-constant list of symbols. This script builds a Shelf::Catalog from literal data.
# frozen_string_literal: true
require "shelf"
rows = <<~CSV
9780134685991,Effective Java,Joshua Bloch
9780596516178,The Ruby Programming Language,David Flanagan
CSV
catalog = Shelf::Catalog.new
rows.each_line(chomp: true) do |line|
digits, title, name = line.split(",")
catalog.add(Shelf::Book.new(isbn: Shelf::Isbn.new(digits:), title:, author: Shelf::Author.new(name:)))
end
catalog.each { |book| puts book }
p catalog.map(&:isbn).map(&:to_s)
p Shelf::Loan::STATES
begin
Shelf::Isbn.new(digits: "978-0-13-468599-2")
rescue ArgumentError => e
puts e.message
end
Output:
Effective Java by Joshua Bloch
The Ruby Programming Language by David Flanagan
["9780134685991", "9780596516178"]
[:available, :lent, :overdue, :returned]
invalid ISBN: 978-0-13-468599-2
Source: Literals
Shelf::Isbn.new(digits:) uses hash/keyword value omission. Shelf::Loan::STATES is %i[available lent overdue
returned].freeze: a symbol array literal frozen so callers cannot change it. The last two lines show the validation of
the Isbn value object: a literal ISBN with a bad checksum raises ArgumentError at construction.
Warnings and errors that literals produce
| Source | What Ruby says (ruby -w, Ruby 4.0.7) |
|---|---|
|
syntax error: |
|
syntax error: |
|
syntax error: |
|
|
|
warning: |
|
warning: |
|
warning: |
|
warning: |
|
warning: |
|
no error: the braces are an empty block, so a blank line is printed |
mutation of a chilled string with |
warning: |
mutation of a frozen literal |
|
|
|
Where older material is outdated
Books and articles written before 2024 give advice that no longer matches Ruby 4.0. For the concepts on this page:
| Older claim | Current behaviour |
|---|---|
"String literals are frozen by default since Ruby 3.4." |
They are chilled: mutable, |
|
|
|
|
|
One |
|
|
Symbols are never garbage collected. |
Dynamic symbols are collected since Ruby 2.2; static symbols in source are not. |
Interpolated strings are frozen by the magic comment. |
Not since Ruby 3.0. |
Range and regexp literals are mutable objects. |
Frozen since Ruby 3.0. |
Hash value omission, endless ranges, |
Value omission |
|
Both are bundled gems in Ruby 4.0 and need a Gemfile entry under Bundler. |
Summary
-
Know the quoting rules (single, double, percent, heredoc, character) and the one escape you must remember in single quotes (
\\and\'). -
Use
0ofor octal,_for readability,randifor exact and imaginary numbers, and never compare floats with==. -
Prefer
key: value, value omission and%w/%i; remember thatHash.new([])andArray.new(n, [])share one object. -
Parenthesize endless and beginless ranges; use
\A/\zwith regexp literals for validation. -
Put
# frozen_string_literal: truein every file, mutate through+"", and test with-W:deprecatedand--enable-frozen-string-literal.
References
-
Literals (Ruby 4.0 syntax guide)
-
Comments and magic comments (Ruby 4.0 syntax guide)
-
Miscellaneous syntax (Ruby 4.0 syntax guide)
-
Ruby command-line options (Ruby 4.0)
-
String, Symbol, Integer, Float, Rational, Complex, Array, Hash, Range, Regexp and Proc (Ruby 4.0 core documentation)
-
Warning (warning categories)
-
NEWS for Ruby 3.4.0 (chilled strings,
Hash#inspect,it) -
NEWS for Ruby 3.0.0 (frozen Range and Regexp literals, interpolated strings)
-
NEWS for Ruby 3.1.0 (hash value omission)