---
title: 'Arrays and Hashes'
source: 'https://academia.sh/en/courses/ruby-fundamentals/arrays-and-hashes'
course: 'Ruby Fundamentals'
language: en
updated: '2026-10-21T05:03:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Writes the basic operations of arrays and hashes and reads their results'
    - 'Measures with owner which class or module defines a collection method'
    - 'Shows how a default-value block responds when a key is missing'
    - 'Measures the difference between fetch and square brackets and shows that the block runs only when the key is absent'
---

# Arrays and Hashes

Ruby's two basic collections, the array and the hash, are measured for which methods they answer themselves, which a shared module answers, and how a block decides the value of a missing key.

The Symbols and Strings lesson measured that the `String` class answers the message `"name".to_sym`, and that the resulting `:name` symbol can be compared with `equal?` because it carries the same value as a single object. The symbol's real job did not show up in that lesson yet: being a key in a hash. This lesson takes the two basic collections, the array and the hash, and asks them the same question the course always asks: who answers the message sent to a method?

The course's four responders do not change: **class**, **module**, **block** and **syntax**. In prose, a bare "class" is always Ruby's `Class`, and the measurement category is called a "class responder". Variables, types and loops were established as concepts in M01/K02; here an array is an ordered list of elements numbered from zero. A hash is a mapping from keys to values, and it keeps its elements in insertion order.

## Basic Operations

The first measurement only shows behavior. An array is written with square brackets, `<<` appends an element, a negative index counts from the end, and a range gives a slice. A hash is written with curly braces and the `key: value` form; in this form the keys are symbols.

```ruby
# basics.rb — basic operations on arrays and hashes
items = [10, 20, 30]
items << 40
puts "first=#{items[0]} last=#{items[-1]} count=#{items.size} outside=#{items[10].inspect}"
puts "slice=#{items[1..2].inspect} first_two=#{items.first(2).inspect}"

person = { name: "Alice", age: 30 }
person[:city] = "Izmir"
puts "name=#{person[:name]} missing=#{person[:mail].inspect} keys=#{person.keys.inspect}"
person.each { |key, value| puts "#{key} -> #{value}" }
puts person.select { |_, value| value.is_a?(String) }.inspect
puts person.map { |key, value| key }.inspect
```
```
first=10 last=40 count=4 outside=nil
slice=[20, 30] first_two=[10, 20]
name=Alice missing=nil keys=[:name, :age, :city]
name -> Alice
age -> 30
city -> Izmir
{name: "Alice", city: "Izmir"}
[:name, :age, :city]
```

An index outside the array raises no error and returns `nil`; so does a key that is not in the hash. These two silent `nil` results are the ground for the second half of this lesson: what determines the result for a missing key? A hash keeps insertion order; the order on the `keys` line shows that `city` was appended at the end. `select` and `map` took the same two-parameter block form, but one returned a hash and the other an array. The reason is the ownership difference measured in the next section. **CB1.** The measurements were made with symbol-keyed hashes; hashes with string keys were not measured in this lesson and the results are not carried over to them.

## Which Definition Carries the Method

Two methods written on the same collection can be defined in different places even when they are called the same way. `method(:name).owner` gives the class or module that defines that method. This lesson does not show the lookup order; it only reads the result, the owner.

```ruby
# collection.rb — who owns the collection methods
items = [1, 2, 3]
pairs = { a: 1, b: 2 }

puts "items.size    #{items.method(:size).owner}"
puts "items.map     #{items.method(:map).owner}"
puts "pairs.each    #{pairs.method(:each).owner}"
puts "pairs.select  #{pairs.method(:select).owner}"
puts "pairs.fetch   #{pairs.method(:fetch).owner}"
puts "pairs.map     #{pairs.method(:map).owner}"
```
```
items.size    Array
items.map     Array
pairs.each    Hash
pairs.select  Hash
pairs.fetch   Hash
pairs.map     Enumerable
```

In Ruby 4.0.7, a class answered five of the methods: `size` and `map` are defined in `Array`, while `each`, `select` and `fetch` are defined in `Hash`. The sixth is different: `pairs.map` is not defined in `Hash` but in the `Enumerable` module. So the same `map` name is answered by the class on an array and by the module on a hash. Which definition holds which method can shift from version to version, so the result is read as a measurement of this version, not as a promise of the language. The `Enumerable` module is the third lesson and is only named here. In the previous section `select` returned a hash and `map` returned an array: `select` is `Hash`'s own method, so it builds its result as a hash too, while `map`, which comes from `Enumerable`, always produces an array.

## Who Supplies the Value of a Missing Key

In the basic operations, `person[:mail]` returned `nil`. To change that behavior, a default value can be given when the hash is built. There are two forms: `Hash.new(0)` supplies a fixed value, and `Hash.new { ... }` supplies a **block** that runs when the key is missing. The block can take the key as a parameter, so the result can depend on the key. M12/K02 measured where a change to a shared default value ends up in PHP; that question is not asked here, only who produces the default value.

The course's shared helper `reply.rb` is given first, verbatim. It classifies an expression with three questions in order: does `defined?` say it is a method call, do two different blocks give two different results, is the method's owner a `Class`. It has no output block, so it is only written to disk.

```ruby
# reply.rb — helpers that classify who answers a message

# Classification asks in order: is the expression a message at all (defined?),
# then do two different blocks give two different results, then who owns the method.
def responder(definition, owner: nil, result: nil, with_blocks: nil)
  return ["syntax", "defined? #{definition}"] unless definition == "method"

  if with_blocks
    first, second = with_blocks.call
    return ["block", "#{first.inspect} / #{second.inspect}"] unless first == second
  end

  kind = owner.instance_of?(Class) ? "class" : "module"
  [kind, "#{owner}, result #{result.inspect}"]
end
```

Now the core's `default value` observation runs. Two `Hash.new` blocks look at the same `[:x]` message: one always returns `0`, the other returns the text form of the key. After that, the fixed-default form and the two cases of `defined?` are printed.

```ruby
# defaults.rb — who supplies the value of a missing key
require_relative "reply"

zero = Hash.new { 0 }
text = Hash.new { |_, key| key.to_s }

kind, evidence = responder(defined?(Hash.new), with_blocks: -> { [zero[:x], text[:x]] })
puts "#{kind}  #{evidence}"
puts Hash.new(0)[:x]
puts defined?(Hash.new)
puts defined?(Hash.new { 0 })
```
```
block  0 / "x"
0
method
expression
```

The first line is the core's `default value` line: the **responder** is a **block**. The same `[:x]` message gave `0` and `"x"` on two hashes; the class of the object receiving the message was the same (`Hash`), and the only difference was the block supplied. So the answer is decided by the block, not by a class or a module. On the second line, `Hash.new(0)[:x]` with a fixed default also gave `0`; there is no block there, the value was supplied at construction.

The last two lines show a subtlety of the rule. `defined?(Hash.new)` returns `"method"`, but `defined?(Hash.new { 0 })`, the call with a block, returns `"expression"`. This is why the core queries the message **without** a block and measures the effect of the block in the second step; if it queried the block form, the first step would count the message as syntax. **CB2.** The `defined?` result for a call with a block was measured in Ruby 4.0.7; the same result is not assumed in another version. This detail is named in the Blocks lesson.

## Array Operations

The Basic Operations section wrote only `<<` and single-index access. The other common array operations are appending, removing from the end, and slicing. The open-ended range `a[1..]` gives everything after the first element, and `first(2)` takes two elements from the front. `push` and `<<` append; `pop` both removes the last element from the array and returns it.

```ruby
# array_ops.rb — slicing, appending and removing
a = [5, 3, 8, 1]
puts "a[1..]=#{a[1..].inspect} a[-1]=#{a[-1]} first(2)=#{a.first(2).inspect}"
a.push(9)
a << 7
puts "after=#{a.inspect}"
puts "pop=#{a.pop} rest=#{a.inspect}"
```
```
a[1..]=[3, 8, 1] a[-1]=1 first(2)=[5, 3]
after=[5, 3, 8, 1, 9, 7]
pop=7 rest=[5, 3, 8, 1, 9]
```

The slice and `first(2)` each returned an array and did not change `a`: by the time execution reached the `push` line, `a` still held its original four elements. `push(9)` and `<<` did the same job and the array grew to six elements. `pop` returned `7` and the array shrank to five elements: `pop` has two effects, returning a value and changing the array. **CB19.** Whether a slice is a new array or a view of the same memory was not examined; only the printed values were measured.

## The In-Place Form

Many array methods have two forms: one that returns a new array and one that changes the array in place. For sorting, this pair is `sort` and its in-place sorting form. The difference is measured with `equal?`: do two variables point to the same object?

```ruby
# in_place.rb — a new array versus changing the same array
a = [3, 1, 2]
b = a.sort
puts "new=#{b.inspect} a=#{a.inspect} same_object=#{a.equal?(b)}"
c = a.sort!
puts "in_place a=#{a.inspect} same_object=#{a.equal?(c)}"
puts "owner=#{a.method(:sort!).owner}"
```
```
new=[1, 2, 3] a=[3, 1, 2] same_object=false
in_place a=[1, 2, 3] same_object=true
owner=Array
```

`sort` gave a new array: `b` was sorted, `a` stayed in its original order, and `equal?` returned `false`. The in-place sorting form sorted `a` itself and returned the same object: `a` became `[1, 2, 3]` and `equal?` returned `true`. The owner of the in-place sorting form printed as `Array`, so its responder is a class; the difference between the two forms lies in whether the method's result is a new object or the receiver itself. **CB20.** The measurement was made only on the sorting pair; the same pattern is not assumed for other pairs.

## Order in a Hash and Nested Access

A hash keeps insertion order, and `to_a` turns it into an array of pairs. In nested hashes, `dig` follows the keys in order; if the path breaks somewhere, it returns `nil` instead of raising an error.

```ruby
# hash_more.rb — insertion order, to_a and dig
h = { b: 1, a: 2 }
h[:c] = 3
puts h.keys.inspect
puts h.to_a.inspect
nested = { user: { address: { city: "Izmir" } } }
puts nested.dig(:user, :address, :city)
puts nested.dig(:user, :phone, :number).inspect
```
```
[:b, :a, :c]
[[:b, 1], [:a, 2], [:c, 3]]
Izmir
nil
```

`h` was first built with the keys `b` and `a`, and `c` was appended at the end; `keys` gave this order, and `to_a` gave the same order as pairs. There is no alphabetical ordering. `dig` followed three keys and found `Izmir`; in the second call `:phone` did not exist, and `dig` returned `nil` without raising an error. **CB21.** The order behavior was measured in Ruby 4.0.7 and only with this example.

## The Shared Default Object

Giving the default value as an object instead of a block, in the form `Hash.new([])`, carries a trap: that object is a single one, and the same one comes back for every missing key. M12/K02 measured the behavior of a shared default value in another language; here the Ruby output is only read.

```ruby
# shared.rb — a shared default object versus the block form
shared = Hash.new([])
shared[:a] << 1
shared[:b] << 2
puts "shared=#{shared.inspect} default=#{shared.default.inspect} keys=#{shared.keys.inspect}"
separate = Hash.new { |h, k| h[k] = [] }
separate[:a] << 1
separate[:b] << 2
puts separate.inspect
```
```
shared={} default=[1, 2] keys=[]
{a: [1], b: [2]}
```

`shared[:a] << 1` received the default array for the missing key and appended to it; no key was assigned to the hash. That is why `shared` stayed empty, `keys` was `[]`, and the default array became `[1, 2]`: `:a` and `:b` wrote into the same array. The block form, on the other hand, ran the block for every missing key and wrote the result into the hash with `h[k] = []`; the two keys got two separate arrays. The value of a missing key is supplied by a single object in the first form and by the block each time in the second. **CB22.**

## Transforming a Hash

Producing a new hash or a single value from a hash is a frequent need. `transform_values` transforms the values, `to_h` with a block can change the keys as well, and `sum` comes through `Enumerable`. On the array side, `compact` drops `nil` elements, `uniq` removes duplicates, and `flatten` flattens a nested array.

```ruby
# transform.rb — hash transformations and array cleanup
h = { a: 1, b: 2 }
puts h.transform_values { |v| v * 10 }.inspect
puts h.to_h { |key, v| [key.to_s, v] }.inspect
puts h.sum { |_, v| v }
puts h.min_by { |_, v| -v }.inspect
puts "owner=#{h.method(:transform_values).owner} #{h.method(:sum).owner} #{h.method(:to_h).owner}"
d = [3, 1, 2, 3, nil]
puts d.compact.uniq.sort.inspect
puts [[1, [2]], [3]].flatten.inspect
```
```
{a: 10, b: 20}
{"a" => 1, "b" => 2}
3
[:b, 2]
owner=Hash Enumerable Hash
[1, 2, 3]
[1, 2, 3]
```

`transform_values` multiplied only the values and the keys were kept; when the `to_h` block returned each pair as `[key, value]`, the keys were turned into strings. `sum` added the values and gave `3`, and `min_by` chose the pair with the smallest `-v`, that is, `[:b, 2]`. The owner line shows the distinction: `transform_values` and `to_h` are in `Hash`, while `sum` is in `Enumerable`. The `compact`, `uniq` and `sort` chain dropped the `nil` and the repeated `3` and gave `[1, 2, 3]`; `flatten` turned the nested array into `[1, 2, 3]`. **CB38.** These methods are not assumed to have the same owner on other receivers.

## The Bounding Measurement

Does the block answer on every shortfall, or only in one specific case? The `fetch` method measures this directly. `fetch` looks for the key; if it finds it, it returns the value. If it does not, it runs the block when one is given and raises an error when none is. The measurement below counts with a counter how many times the block runs. This counter is not read as an answer to the question of how many times it ran: M10/K03 measured that question on another axis, and here the counter only shows that the block was never called.

```ruby
# fetch.rb — the block answers only when the key is absent
calls = 0
pairs = { a: 1 }

found = pairs.fetch(:a) { calls += 1; 0 }
missing = pairs.fetch(:b) { |key| calls += 1; key.to_s }
puts "found=#{found.inspect} missing=#{missing.inspect} calls=#{calls}"

without_block = begin
  pairs.fetch(:b)
rescue KeyError => error
  error.class
end
puts "without_block=#{without_block}"
puts "bracket=#{pairs[:b].inspect}"
puts "owner=#{pairs.method(:fetch).owner}"
```
```
found=1 missing="b" calls=1
without_block=KeyError
bracket=nil
owner=Hash
```

`fetch` gave three different results for the same missing key. The call with a block returned `"b"`, the call without a block raised `KeyError`, and square brackets returned `nil`. When the key existed, the counter-incrementing `fetch(:a)` with a block returned `1` and the counter went up only once: that increment belongs to the call whose key was absent. So the block was never called when the key was found. What the block answers is the missing key; the value of an existing key comes from the hash itself, without any block being asked.

This narrows the responder from the first section. The owner of `fetch` is `Hash`, a class, but the result is decided by the block when the key is missing. The core's classification measures the block case in a separate step for this reason: the owner of the message is always a class or a module, and the question is where the result comes from. Like `Hash.new { ... }`, `fetch` leaves the default in the caller's hands; the difference between the two is that the `Hash.new` block is bound to the hash permanently, while the `fetch` block belongs only to that call.

## Summary

- An array is an ordered list numbered from zero; a hash is a mapping from keys to values that keeps insertion order. An index outside the array and a missing key return `nil`.
- In Ruby 4.0.7, `size`, `map` (on an array), `each`, `select` and `fetch` are defined in their own classes; `pairs.map` is in the `Enumerable` module.
- This lesson's responder is the **block**: `Hash.new { 0 }[:x]` gave `0` and `Hash.new { |_, k| k.to_s }[:x]` gave `"x"`; the class receiving the message was the same, and the difference was in the block supplied.
- `defined?` returns `"expression"` on a call with a block; the core queries the message without a block.
- The bounding measurement: the block answers only when the key is absent. The counter-incrementing `fetch(:a)` with a block returns `1` for an existing key, and the block never runs.

## Next Step

In this lesson the block separated the results of two hashes, but the block itself has not yet been taken up as a topic: `{ |_, k| k.to_s }` and `{ 0 }` were only written. The Blocks lesson runs the same `map` message with two different blocks and measures how a method calls the block given to it.
