Assignment

An assignment expression assigns a value to a named identifier (usually a variable). The assignment operator is the equals sign (=).

The target of an assignment can be:

  1. # Assigns to a local variable
  2. local = 1
  3. # Assigns to an instance variable
  4. @instance = 2
  5. # Assigns to a class variable
  6. @@class = 3
  7. # Assigns to a constant
  8. CONST = 4
  9. # Assigns to a setter method
  10. foo.method = 5
  11. foo[0] = 6

Method as assignment target

A method ending with an equals sign (=) is called a setter method. It can be used as the target of an assignment. The semantics of the assignment operator apply as a form of syntax sugar to the method call.

Calling setter methods requires an explicit receiver. The receiver-less syntax x = y is always parsed as an assignment to a local variable, never a call to a method x=. Even adding parentheses does not force a method call, as it would when reading from a local variable.

The following example shows two calls to a setter method in typical method notation and with assignment operator. Both assignment expressions are equivalent.

  1. class Thing
  2. def name=(value); end
  3. end
  4. thing = Thing.new
  5. thing.name=("John")
  6. thing.name = "John"

The following example shows two calls to an indexed assignment method in typical method notation and with index assignment operator. Both assignment expressions are equivalent.

  1. class List
  2. def []=(key, value); end
  3. end
  4. list = List.new
  5. list.[]=(2, 3)
  6. list[2] = 3

Combined assignments

Combined assignments are a combination of an assignment operator and another operator. This works with any target type except constants.

Some syntax sugar that contains the = character is available:

  1. local += 1 # same as: local = local + 1

This assumes that the corresponding target local is assignable, either as a variable or via the respective getter and setter methods.

The = operator syntax sugar is also available to setter and index assignment methods. Note that || and && use the []? method to check for key presence.

  1. person.age += 1 # same as: person.age = person.age + 1
  2. person.name ||= "John" # same as: person.name || (person.name = "John")
  3. person.name &&= "John" # same as: person.name && (person.name = "John")
  4. objects[1] += 2 # same as: objects[1] = objects[1] + 2
  5. objects[1] ||= 2 # same as: objects[1]? || (objects[1] = 2)
  6. objects[1] &&= 2 # same as: objects[1]? && (objects[1] = 2)

Chained assignment

The same value can be assigned to multiple targets using chained assignment. This works with any target type except constants.

  1. a = b = c = 123
  2. # Now a, b and c have the same value:
  3. a # => 123
  4. b # => 123
  5. c # => 123

Multiple assignment

You can declare/assign multiple variables at the same time by separating expressions with a comma (,). This works with any target type except constants.

  1. name, age = "Crystal", 1
  2. # The above is the same as this:
  3. temp1 = "Crystal"
  4. temp2 = 1
  5. name = temp1
  6. age = temp2

Note that because expressions are assigned to temporary variables it is possible to exchange variables’ contents in a single line:

  1. a = 1
  2. b = 2
  3. a, b = b, a
  4. a # => 2
  5. b # => 1

Multiple assignment is also available to methods that end with =:

  1. person.name, person.age = "John", 32
  2. # Same as:
  3. temp1 = "John"
  4. temp2 = 32
  5. person.name = temp1
  6. person.age = temp2

And it is also available to index assignments ([]=):

  1. objects[1], objects[2] = 3, 4
  2. # Same as:
  3. temp1 = 3
  4. temp2 = 4
  5. objects[1] = temp1
  6. objects[2] = temp2

One-to-many assignment

If the right-hand side contains just one expression, the type is indexed for each variable on the left-hand side like so:

  1. name, age, source = "Crystal, 123, GitHub".split(", ")
  2. # The above is the same as this:
  3. temp = "Crystal, 123, GitHub".split(", ")
  4. name = temp[0]
  5. age = temp[1]
  6. source = temp[2]

Additionally, if the strict_multi_assign flag is provided, the number of elements must match the number of targets, and the right-hand side must be an Indexable:

  1. name, age, source = "Crystal, 123, GitHub".split(", ")
  2. # The above is the same as this:
  3. temp = "Crystal, 123, GitHub".split(", ")
  4. if temp.size != 3 # number of targets
  5. raise IndexError.new("Multiple assignment count mismatch")
  6. end
  7. name = temp[0]
  8. age = temp[1]
  9. source = temp[2]
  10. a, b = {0 => "x", 1 => "y"} # Error: right-hand side of one-to-many assignment must be an Indexable, not Hash(Int32, String)

Splat assignment

The left-hand side of an assignment may contain one splat, which collects any values not assigned to the other targets. A range index is used if the right-hand side has one expression:

  1. head, *rest = [1, 2, 3, 4, 5]
  2. # Same as:
  3. temp = [1, 2, 3, 4, 5]
  4. head = temp[0]
  5. rest = temp[1..]

Negative indices are used for targets after the splat:

  1. *rest, tail1, tail2 = [1, 2, 3, 4, 5]
  2. # Same as:
  3. temp = [1, 2, 3, 4, 5]
  4. rest = temp[..-3]
  5. tail1 = temp[-2]
  6. tail2 = temp[-1]

If the expression does not have enough elements and the splat appears in the middle of the targets, IndexError is raised:

  1. a, b, *c, d, e, f = [1, 2, 3, 4]
  2. # Same as:
  3. temp = [1, 2, 3, 4]
  4. if temp.size < 5 # number of non-splat assignment targets
  5. raise IndexError.new("Multiple assignment count mismatch")
  6. end
  7. # note that the following assignments would incorrectly not raise if the above check is absent
  8. a = temp[0]
  9. b = temp[1]
  10. c = temp[2..-4]
  11. d = temp[-3]
  12. e = temp[-2]
  13. f = temp[-1]

The right-hand side expression must be an Indexable. Both the size check and the Indexable check occur even without the strict_multi_assign flag (see One-to-many assignment above).

A Tuple is formed if there are multiple values:

  1. *a, b, c = 3, 4, 5, 6, 7
  2. # Same as:
  3. temp1 = {3, 4, 5}
  4. temp2 = 6
  5. temp3 = 7
  6. a = temp1
  7. b = temp2
  8. c = temp3

Underscore

The underscore can appear on the left-hand side of any assignment. Assigning a value to it has no effect and the underscore cannot be read from:

  1. _ = 1 # no effect
  2. _ = "123" # no effect
  3. puts _ # Error: can't read from _

It is useful in multiple assignment when some of the values returned by the right-hand side are unimportant:

  1. before, _, after = "main.cr".partition(".")
  2. # The above is the same as this:
  3. temp = "main.cr".partition(".")
  4. before = temp[0]
  5. _ = temp[1] # this line has no effect
  6. after = temp[2]

Assignments to *_ are dropped altogether, so multiple assignments can be used to extract the first and last elements in a value efficiently, without creating an intermediate object for the elements in the middle:

  1. first, *_, last = "127.0.0.1".split(".")
  2. # Same as:
  3. temp = "127.0.0.1".split(".")
  4. if temp.size < 2
  5. raise IndexError.new("Multiple assignment count mismatch")
  6. end
  7. first = temp[0]
  8. last = temp[-1]