Lesson 06 / 12
Symbols and Strings
A string turning into a symbol and the identity difference between the two representations; string literals frozen by a magic comment, and the comment taking effect per file; the magic comment not changing the responder.
Published Updated
In this lesson
- Shows with equal? that a string turns into a symbol with to_sym and why the identity of the two representations differs
- Measures with frozen? and FrozenError that the magic comment freezes string literals
- Shows that the magic comment does not change the responder, and that the comment takes effect per file
Contents
The previous lesson measured the responder of a receiverless call, and it left a question: when "name".to_sym is written, a string turns into a symbol, and a symbol is also an object. Once a string turns into a symbol, who answers the message? And does a magic comment written at the top of a file change that answer? In the JavaScript Fundamentals course (M06/K01), the primitive types and the symbol were measured; in Ruby, a symbol is not a separate primitive but an instance of the Symbol class. A string, on the other hand, is a mutable object, and this lesson measures the difference between those two objects.
In M11/K03, the question “who sees this change” was measured. In this lesson, a mutable string is only a subject of observation; a single example is seen in which two names share the same string, but the axis of the lesson is the responder, not sharing.
From String to Symbol
A symbol is an immutable value that has a name: :name. It is obtained from a string with to_sym. The file below measures the conversion, the identity of the two representations, and the methods of the symbol itself. equal? is used for identity: do two expressions point at the same object?
# symbol_conversion.rb — a string turns into a symbol
puts "name".method(:to_sym).owner
p "name".to_sym
puts :name.equal?(:name)
puts "name".equal?("name")
p :name.to_s
p :name.upcase
puts :name.method(:to_s).owner
puts :name.method(:upcase).owner
String :name true false "name" :NAME Symbol Symbol
to_sym is defined in the string’s own class, and the return value is printed as :name. The real difference is in identity: :name.equal?(:name) returned true, so a symbol with the same name is always the same object. "name".equal?("name") is false: two string literals are two separate objects even when their contents are the same. :name.to_s produces a string again, and :name.upcase produces a new symbol; the Symbol class answers both.
LB15. In this lesson, a “string literal” is a string written in quotes in the source code. What is measured is whether it produces a new string object on every evaluation. No address or object_id is printed; identity is compared only with equal?.
String Operations
A string carries many methods. The file below tries length, upper-casing, slicing, splitting, joining, repeating, and formatting on a string. The identifiers and the printed strings were kept ASCII.
# string_operations.rb — string methods
text = "welcome"
puts text.length
puts text.upcase
puts text[0, 3]
p "a,b,c".split(",")
puts text + " world"
puts "ha" * 3
puts "%s-%03d" % ["code", 7]
puts "name".to_sym.class
puts :name == "name"
7 WELCOME wel ["a", "b", "c"] welcome world hahaha code-007 Symbol false
length gives seven, upcase gives the upper-case copy, and [0, 3] gives the first three characters. split returns an array, + joins two strings, * repeats, and % formats. These operations were called not to change text but to produce a new value; length returned a number, split an array, and the others a new string. The last two lines show the boundary between the two representations: "name".to_sym gives a Symbol, and :name == "name" returns false; a symbol and a string that carries the same name are not counted as equal.
Character and Byte
A string is a sequence of characters, but it is kept in memory as bytes. Turkish letters are a single character each, yet they take more than one byte. Turkish letters are used on purpose in this measurement, because this is exactly its subject.
# char_byte.rb — the character count and the byte count are separate things puts "ig".length puts "ig".bytesize puts "ığ".length puts "ığ".bytesize
2 2 2 4
"ig", made of ASCII letters, is two characters and two bytes. "ığ" stays two characters but grows to four bytes, because ı and ğ take two bytes each. So length counts characters and bytesize counts bytes; the two diverge in non-ASCII text. The encoding of the symbol was not measured here; this lesson measures the identity of a symbol, not its content.
Symbol Spellings
There is more than one way to write a symbol. A name that contains a space is written in quotes, the %i[...] shortcut is used to write several symbols together, and string interpolation works in a symbol just as in a string.
# symbol_spellings.rb — ways to write a symbol
p :"full name"
p %i[alpha beta gamma]
label = "rose"
p :"prefix_#{label}"
p :ant <=> :bee
p %i[gamma alpha beta].sort
:"full name" [:alpha, :beta, :gamma] :prefix_rose -1 [:alpha, :beta, :gamma]
:"full name" is the quoted spelling, and it keeps its quotes when printed, because the name contains a space. %i[...] produces an array of three symbols. :"prefix_#{label}" builds the symbol at run time with interpolation. Symbols can be ordered: <=> gives -1 according to the character order of the names, and sort arranges the array accordingly. A symbol is still an object; these spellings only open a different door to it.
The Magic Comment
The line # frozen_string_literal: true, written at the very top of a file, is called a magic comment. It freezes the string literals in the file. In the file below the comment is on the first line, and the line carrying the file name is the second.
# frozen_string_literal: true
# commented.rb — the magic comment freezes string literals
puts "name".frozen?
puts "name".equal?("name")
text = "name"
begin
text << "x"
rescue FrozenError => e
puts e.class
end
puts "name".dup.frozen?
mutable = +"name"
puts mutable.frozen?
true true FrozenError false false
While the comment was in effect, "name".frozen? was true, and two separate "name" literals turned out to be the same object (equal? was true): because the literals are frozen, Ruby can share the same content in a single object. Appending to the string with << raised FrozenError; only the class of the caught exception was printed. dup does not give a frozen copy, and +"name" returns an unfrozen string; both gave false. A frozen string stays frozen; if a mutable string is needed, a copy of it is taken.
Measuring the Difference Between a Symbol and a String
The earlier sections measured the spellings and the mutability of symbols and strings. This section puts their conversion into each other and their identity behavior side by side. The axis is the owner: who answers the to_s and << messages?
# round_trip.rb — a round trip between a symbol and a string
s = :name
puts s.to_s
puts s.to_s.to_sym == s
puts s.equal?(:name)
puts "name".equal?("name")
puts "name".to_sym.equal?(:name)
label = "Anna"
puts "Hello #{label}"
puts 'Hello #{label}'
puts "Hello #{label.size + 1}"
puts s.method(:to_s).owner
name
true
true
false
true
Hello Anna
Hello #{label}
Hello 5
Symbol
:name.to_s gave "name", and the way back with to_sym produced the same symbol: s.to_s.to_sym == s is true. In this example the conversion lost no information on the round trip. Identity is a separate question. s.equal?(:name) gave true: the same symbol spelling is the same object everywhere. "name".equal?("name") gave false: even though the two string literals carry the same text, they are two separate objects. "name".to_sym.equal?(:name) was true again: once a string moves to a symbol, the identity is the identity of the symbol. These three measurements show the division of labor between a symbol and a string: a symbol is used for identity, and a string is used for text. If we are going to hold a name as a label, it helps that a symbol is the same object everywhere; if the text is going to be changed or formatted, a string is needed.
Interpolation worked in double quotes: "Hello #{label}" printed Hello Anna, while the single-quoted 'Hello #{label}' printed the text as it is, including #{label}. Inside "#{label.size + 1}" an expression ran and 5 was placed, because Anna has four letters. On the last line, the owner of the to_s message came out as Symbol: the conversion of a symbol to a string is answered by the class of the symbol itself. That interpolation does not work in single quotes prevents the two spellings from being used in place of each other: whoever wants to add the value of a variable into the text chooses double quotes, and whoever wants to keep the text as it is chooses single quotes. The interpolated expression can also be a number; the output Hello 5 shows this.
# in_place_new.rb — << is in place, + makes a new object; the Turkish letter measurement a = +"name" b = a a << "x" puts b c = a + "y" puts c puts a.equal?(b) puts a.equal?(c) puts "ı".upcase puts "İ".downcase.size puts "ı".size puts "ı".bytesize puts a.method(:<<).owner
namex namexy true false I 2 1 2 String
a << "x" worked in place: b, which points at the same object as a, became namex too. a + "y", on the other hand, produced a new string and did not change a; c became namexy. a.equal?(b) gave true and a.equal?(c) gave false: << changes the same object, and + returns a new object. The owner of << came out as String on the last line. The file started with +"name", because this spelling gives a mutable string; there is no magic comment in this block.
The Turkish letters make a separate measurement. "ı".upcase gave I. "İ".downcase.size gave 2: a single letter turned into two characters. If the expected result were a single-character i, the size would be 1. "ı".size came out 1 and "ı".bytesize came out 2: one character, two bytes. This is why the character count and the byte count cannot be used in place of each other. What these two measurements have in common is that they show the limit of thinking of a string as a flat sequence of characters. size counts characters and bytesize counts bytes; upcase and downcase do not have to turn one letter into one letter. Code that computes the length of a string, or whether it fits in a field, must know which unit it is counting.
LB16. That the result of "İ".downcase is two characters is the result of the default letter mapping of this version. That it is the same in another version or locale was not measured.
Formatting and Comparison
A string is not only joined and changed; it is also formatted and sorted. This section writes a string with a format specification, aligns it, repeats it, moves it to the next string, compares it, and converts between a letter and a code point. The axis is the owner: of the three messages whose owner is printed, two are owned by String and one by Kernel.
# formatting.rb — formatting and comparing a string
puts "%05.1f" % 3.14159
puts format("%-4s|", "ab")
puts "ab".center(6, "*")
puts "ab" * 3
puts "az".succ
puts "a" <=> "b"
puts "Ab".casecmp?("aB")
puts "a".ord
puts 98.chr
puts "ab".method(:center).owner
puts "ab".method(:%).owner
puts method(:format).owner
003.1 ab | **ab** ababab ba -1 true 97 b String String Kernel
The expression "%05.1f" % 3.14159 gave 003.1: the %05.1f format wrote the number with a total width of five, one decimal, and zero padding at the start. format("%-4s|", "ab") uses the same specification language; the - mark left-aligns the text and ab | came out. Their owners differ: % is a method of String, and format is a method of Kernel. The same format language can be called through two different doors. "ab".center(6, "*") added stars on both sides and gave **ab**, and "ab" * 3 wrote the string three times in a row: ababab.
"az".succ gave ba: the next string is the value that follows in order, starting from the last letter, and after z comes a with a carry into a left digit. "a" <=> "b" gave -1; the ordering message reports the result as a number. "Ab".casecmp?("aB") returned true, ignoring case. "a".ord gave 97, and 98.chr returned b; the two directions are inverses of each other. The owner lines showed String for center and %; in these two jobs, where the receiver is a string, the responder is the string’s own class. No owner was printed for succ, <=>, casecmp?, and ord. Formatting a string needs no separate helper class: a string carries alignment, repetition, and format specification as its own methods.
Encoding and the Byte Reality
The character count and the byte count of a string can differ; this difference comes from the encoding of the string. This section asks about the encoding directly: the name of the encoding, the code point of a letter, validity, and the raw byte view. The axis is the owner; which class answers the byte query is printed at the end.
# encoding.rb — encoding, code point, and byte
puts "a".encoding
puts "ı".bytes.inspect
puts "ı".ord
puts 305.chr(Encoding::UTF_8)
puts "ı".valid_encoding?
puts "\xff".valid_encoding?
puts "ı".unpack("U*").inspect
puts "ı".b.size
puts "a".method(:bytes).owner
UTF-8 [196, 177] 305 ı true false [305] 2 String
The encoding of a string in the source file came out as UTF-8. "ı".bytes gave two bytes, [196, 177]; "ı".ord gave the code point of the letter, 305. A byte and a code point are two separate numbers: the code point identifies the letter, and the bytes identify the encoded form of the letter. 305.chr(Encoding::UTF_8) produced the letter back from the code point, and ı was printed. So ord and chr form a round trip.
The validity measurement gave two results. "ı".valid_encoding? was true, and "\xff".valid_encoding? was false: the byte 0xff on its own does not form a valid letter in UTF-8 encoding. "ı".unpack("U*") returned [305]; that is, the same letter is decoded into a single code point from the combination of two bytes. "ı".b.size gave 2: because b turns the string into its raw byte view, the size showed the byte count, not the character count. The size and bytesize difference of the earlier section reappears here at the level of encoding. The owner came out as String: the byte query is answered by the string’s own class as well.
The Bounding Measurement
The responder of this lesson is a class. Two slips are possible here. The first: one may think the magic comment changes the responder of a message. The second: one may think that once the comment is written, it affects the whole program. Both are wrong; the measurement puts two files side by side. First a file without the comment is written to disk, and then the main file with the comment pulls it in with require_relative.
# uncommented.rb — no magic comment def uncommented_frozen "name".frozen? end def uncommented_owner "name".method(:upcase).owner end
# frozen_string_literal: true # main.rb — the comment is per file require_relative "uncommented" puts "name".method(:upcase).owner puts uncommented_owner puts "name".frozen? puts uncommented_frozen
String String true false
The owner of the message "name".upcase is the same in both files: String. The comment did not change the responder; what changed is the production of the literal. "name".frozen? was true in the file with the comment and stayed false in the file without it, although both ran in the same process. The comment takes effect per file: a comment in one file does not freeze the literals of another file that it pulls in.
Summary
"name".to_symturns a string into a symbol and theStringclass answers the conversion;:name.to_sand:name.upcasebelong to theSymbolclass.- A symbol with the same name is always the same object (
:name.equal?(:name)istrue); two string literals are different objects even if their contents are the same. - The magic comment
# frozen_string_literal: truefreezes string literals:frozen?istrue, and<<raisesFrozenError. dupand+"name"give an unfrozen string; a frozen string itself stays frozen.- The responder of this lesson is mostly a class (
String,Symbol; theKernelmodule forformat): the magic comment does not change the responder of a message and affects only the literals in the file where it appears. sizecounts characters andbytesizecounts bytes (1and2for"ı");<<changes in place and+produces a new string;ordandchrform a round trip for the code point.
Next Step
Strings and symbols were measured, but the question of who answers was always asked about single-valued objects. In a collection, for example in a hash, when a key that does not exist is asked about, who gives the answer? The next lesson, Arrays and Hashes, measures who supplies the value of a missing key.
To keep your progress and take notes, Log in
My notes
Log in to take notes.