トラック
/
Ruby
Ruby
/
演習
/
機関車の運転士
機関車の運転士

機関車の運転士

学習演習

はじめに

分解とは、ArrayやHashのようなコレクションの要素を取り出す操作のことです。 分解した値は、同じ文の中で変数に代入できます。

多重代入は、1つの文の中で複数の変数に値を分解しながら代入する機能です。 これによってコードをより簡潔で読みやすくでき、代入する変数はfirst, second, third = [1, 2, 3]のようにカンマで区切ります。

分解の場面では、スプラット演算子(*)と二重スプラット演算子(**)がよく使われます。

Caution

*<variable_name>と**<variable_name>を、*と**と混同しないでください。 *と**はそれぞれ掛け算とべき乗に使われますが、*<variable_name>と**<variable_name>は合成演算子と分解演算子として使われます。

多重代入

多重代入を使うと、複数の変数に1行で代入できます。 値を区切るには、カンマ,を使います:

>> a, b = 1, 2
=> [1, 2]
>> a
=> 1

多重代入は、1つのデータ型に限られるものではありません:

>> x, y, z = 1, "Hello", true
=> [1, "Hello", true]
>> x
=> 1
>> y
=> 'Hello'
>> z
=> true

多重代入は、配列の要素を入れ替えるのにも使えます。 この手法は、ソートアルゴリズムでよく使われます。 たとえば、次のようになります:

>> numbers = [1, 2]
=> [1, 2]
>> numbers[0], numbers[1] = numbers[1], numbers[0]
=> [2, 1]
>> numbers
=> [2, 1]
Note

これは「並列代入」とも呼ばれ、一時変数を避けるために使えます。

値よりも変数のほうが多い場合、余分な変数にはnilが代入されます:

>> a, b, c = 1, 2
=> [1, 2]
>> b
=> 2
>> c
=> nil

分解

Rubyでは、配列やハッシュの要素を分解することで、それぞれ別の変数に代入できます。 配列の値にはインデックスの順番があるので、同じ順番で変数に展開されます:

>> fruits = ["apple", "banana", "cherry"]
>> x, y, z = fruits
>> x
=> "apple"

不要な値がある場合は、_を使って「集めたけれど使わない」ことを示せます:

>> fruits = ["apple", "banana", "cherry"]
>> _, _, z = fruits
>> z
=> "cherry"

深い分解

配列の中にある配列(入れ子の配列とも呼ばれます)から値を分解して代入する方法は、浅い分解と同じように機能しますが、値の文脈や位置を明確にするために区切り付き分解式(())が必要です:

>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>> (a, b), (c, d) = fruits_vegetables
>> a
=> "apple"
>> d
=> "potato"

入れ子になった配列の一部だけを深く展開することもできます:

>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>> a, (c, d) = fruits_vegetables
>> a
=> ["apple", "banana"]
>> c
=> "carrot"

分解するときに変数の位置が正しくなかったり、値の数が合わなかったりすると、構文エラーになります:

fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]

(a, b), (d) = fruits_vegetables
# syntax error, unexpected '=', expecting '.' or &. or :: or '['

((a, b), (d)) = fruits_vegetables
# syntax error, unexpected ')', expecting '.' or &. or :: or '['

ここで試してみましょう。すると、右辺にある使える値ではなく、最初のパターンが優先されることに気づくはずです。 この構文エラーはデータ構造とは関係ありません。

スプラット演算子(*)を使った配列の分解

配列を分解するとき、スプラット演算子(*)を使って「残りの」値をまとめて取り出せます。 これは、配列をスライスするよりも明確です(状況によっては読みにくくなることがあります)。 たとえば、最初の要素を取り出し、残りの値を最初の要素を含まない新しい配列として代入できます:

>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>> x, *last = fruits
>> x
=> "apple"
>> last
=> ["banana", "cherry", "orange", "kiwi", "melon", "mango"]

配列の先頭と末尾の値を取り出しつつ、真ん中の値をまとめることもできます:

>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>> x, *middle, y, z = fruits
>> y
=> "melon"
>> middle
=> ["banana", "cherry", "orange", "kiwi"]

深い分解でも*を使えます:

>> fruits_vegetables = [["apple", "banana", "melon"], ["carrot", "potato", "tomato"]]
>> (a, *rest), b = fruits_vegetables
>> a
=> "apple"
>> rest
=> ["banana", "melon"]

Hashの分解

ハッシュの分解は、配列の分解とは少し異なります。 ハッシュを展開するには、まず配列に変換する必要があります。 そうしないと分解できません:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory
>> x
=> {:apple=>6, :banana=>2, :cherry=>3}
>> y
=> nil

Hashを配列に変換するには、to_aメソッドを使います:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> fruits_inventory.to_a
=> [[:apple, 6], [:banana, 2], [:cherry, 3]]
>> x, y, z = fruits_inventory.to_a
>> x
=> [:apple, 6]

キーを展開したい場合は、keysメソッドを使います:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory.keys
>> x
=> :apple

値を展開したい場合は、valuesメソッドを使います:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory.values
>> x
=> 6

合成

合成とは、複数の値を1つの配列にまとめて変数に代入する機能です。 値を_分解_し、変更を加えてから、その結果を変数に_合成_し直したいときに便利です。 また、2つ以上の配列やハッシュをマージすることもできます。

スプラット演算子(*)を使った配列の合成

配列の合成は、スプラット演算子(*)を使って行えます。 これは、すべての値を1つの配列にまとめます:

>> fruits = ["apple", "banana", "cherry"]
>> more_fruits = ["orange", "kiwi", "melon", "mango"]

# fruits and more_fruits are unpacked and then their elements are packed into combined_fruits
>> combined_fruits = *fruits, *more_fruits

>> combined_fruits
=> ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]

二重スプラット演算子(**)を使ったハッシュの合成

ハッシュの合成は、二重スプラット演算子(**)を使って行います。 これは、あるハッシュのキーと値のペアをすべて別のハッシュにまとめたり、2つのハッシュを組み合わせたりします:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> more_fruits_inventory = {orange: 4, kiwi: 1, melon: 2, mango: 3}

# fruits_inventory and more_fruits_inventory are unpacked into key-values pairs and combined.
>> combined_fruits_inventory = {**fruits_inventory, **more_fruits_inventory}

# then the pairs are packed into combined_fruits_inventory
>> combined_fruits_inventory
=> {:apple=>6, :banana=>2, :cherry=>3, :orange=>4, :kiwi=>1, :melon=>2, :mango=>3}

メソッドにおけるスプラット演算子(*)と二重スプラット演算子(**)の使い方

メソッドの仮引数を使った合成

任意の数の引数を受け取るメソッドを作るときは、メソッドの定義で*argumentsや**keyword_argumentsを使えます。 *argumentsは任意の数の位置引数(キーワードではない引数)をまとめるために使い、**keyword_argumentsは任意の数のキーワード引数をまとめるために使います。

*argumentsの使い方:

# This method is defined to take any number of positional arguments
# (Using the single line form of the definition of a method.)

>> def my_method(*arguments)= arguments

# Arguments given to the method are packed into an array

>> my_method(1, 2, 3)
=> [1, 2, 3]

>> my_method("Hello")
=> ["Hello"]

>> my_method(1, 2, 3, "Hello", "Mars")
=> [1, 2, 3, "Hello", "Mars"]

**keyword_argumentsの使い方:

# This method is defined to take any number of keyword arguments

>> def my_method(**keyword_arguments)= keyword_arguments

# Arguments given to the method are packed into a dictionary

>> my_method(a: 1, b: 2, c: 3)
=> {:a => 1, :b => 2, :c => 3}

定義したメソッドにキーワード引数のための仮引数(**keyword_argumentsまたは<key_word>: <value>)が1つもない場合、キーワード引数はハッシュにまとめられ、最後の仮引数に代入されます。

>> def my_method(a)= a

>> my_method(a: 1, b: 2, c: 3)
=> {:a => 1, :b => 2, :c => 3}

*argumentsと**keyword_argumentsは、組み合わせて使うこともできます:

def my_method(*arguments, **keyword_arguments)
  p arguments.sum
  for (key, value) in keyword_arguments.to_a
    p key.to_s + " = " + value.to_s
  end
end


my_method(1, 2, 3, a: 1, b: 2, c: 3)
6
"a = 1"
"b = 2"
"c = 3"

*argumentsの前後に引数を書いて、特定の位置引数を指定することもできます。 これは、配列を分解するのと同じように機能します。

Caution

引数は、特定の順序で並べる必要があります:

def my_method(<positional_arguments>, *arguments, <positional_arguments>, <keyword_arguments>, **keyword_arguments)

この順序に従わないと、エラーになります。

def my_method(a, b, *arguments)
  p a
  p b
  p arguments
end

my_method(1, 2, 3, 4, 5)
1
2
[3, 4, 5]

*argumentsの前後に位置引数を書くこともできます:

>> def my_method(a, *middle, b)= middle

>> my_method(1, 2, 3, 4, 5)
=> [2, 3, 4]

位置引数、*arguments、キーワード引数、**keyword_argumentsを組み合わせることもできます:

>> def my_method(first, *many, last, a:, **keyword_arguments)
     p first
     p many
     p last
     p a
     p keyword_arguments
     end

>> my_method(1, 2, 3, 4, 5, a: 6, b: 7, c: 8)
1
[2, 3, 4]
5
6
{:b => 7, :c => 8}

引数を間違った順序で書くと、エラーになります:

def my_method(a:, **keyword_arguments, first, *arguments, last)
  arguments
end

my_method(1, 2, 3, 4, a: 5)

syntax error, unexpected local variable or method, expecting & or '&'
... my_method(a:, **keyword_arguments, first, *arguments, last)

メソッド呼び出しでの分解

スプラット演算子(*)を使って、配列にまとめた引数をメソッド呼び出しで展開できます:

def my_method(a, b, c)
  p c
  p b
  p a
end

numbers = [1, 2, 3]
my_method(*numbers)
3
2
1

二重スプラット演算子(**)を使って、ハッシュにまとめた引数をメソッド呼び出しで展開することもできます:

def my_method(a:, b:, c:)
  p c
  p b
  p a
end

numbers = {a: 1, b: 2, c: 3}
my_method(**numbers)
3
2
1

説明

友人のLinusは、都市間で貨物列車を運転する機関士です。列車の扱いは素晴らしいのですが、物流やコンピューターの扱いは得意ではありません。Linusは、列車の詳細を整理し、経路データの間違いを修正するために、あなたのプログラミングの助けを借りたいと思っています。

Note

スライシング、インデックス、そしてさまざまなhashメソッドを使えば、この演習は簡単に解けます。 ただし、以下の各タスクを解くにあたっては、パッキング、アンパッキング、多重代入を練習してほしいと思います。

1. すべての貨車の配列を作成する

友人は貨車ごとの識別子(ID)を記録し続けてきましたが、システムが一度に処理しなければならない貨車の数はいつも定かではありません。 このデータが統一された配列にまとめられていれば、残りの物流プログラムはずっと簡単になります。

任意の数の貨車IDを受け取るメソッドgenerate_list_of_wagons()を実装してください。 各IDは正の整数です。 メソッドは、与えられたIDを単一の配列としてreturnする必要があります。

LocomotiveEngineer.generate_list_of_wagons(1, 7, 12, 3, 14, 8, 5)
# => [1, 7, 12, 3, 14, 8, 5]

2. 貨車の配列を修正する

ここまでで、データとそれが物流プログラムでどのように使われるかが、だんだんわかってきたのではないでしょうか。 IDシステムは常に機関車に1のIDを割り当て、列車内の残りの貨車には1より大きいランダムに選ばれたIDを割り当てます。

友人は新しい貨車を2両列車につなげなければならなかったのですが、システムを更新するのを忘れてしまいました! そのため、列車の配列の最初の2両を末尾に移動させなければなりません。そうしないと、すべてが順不同になってしまいます。

さらにややこしいことに、友人は、不足している貨車IDが含まれているらしい2つ目の配列をちょうど見つけました。 覚えているのは、新しい貨車を移動させたあと、この2つ目の配列のIDを指定された機関車の直後に配置する必要がある、ということだけです。

Linusは、間違いを修正してデータを統合してくれたら、とても感謝するでしょう。

貨車IDを含む2つの配列を受け取るメソッドfix_list_of_wagons()を実装してください。 最初の配列の最初の2つの要素を末尾に移動し、2つ目の配列の値を機関車のID(1)の後ろ(右側)に挿入する必要があります。 メソッドは、変更を加えた配列をreturnする必要があります。

LocomotiveEngineer.fix_list_of_wagons([2, 5, 1, 7, 4, 12, 6, 3, 13], [3, 17, 6, 15])
# => [1, 3, 17, 6, 15, 7, 4, 12, 6, 3, 13, 2, 5]

3. 不足している停車駅を追加する

貨車のデータがすべて正しくなったので、Linusは、システムの経路情報を更新してほしいと思っています。 輸送経路では、列車が貨物を積み下ろしするために、いくつかの異なる駅に停車することがあります。 それぞれの旅で、こうした中間配送地点の数は異なることがあります。 友人は、不足している、あるいは追加の配送情報で、システムの経路ハッシュを更新してほしいと思っています。

経路のハッシュと、それに続く可変個のキーワード引数を受け取るメソッドadd_missing_stops()を実装してください。 これらの引数は、1つ以上の停車駅を保持するハッシュの形か、任意の数のstop_<number>: "city"キーワードペアの形を取ることができます。 メソッドは、追加されたすべての停車駅を順番に保持する追加のキーで更新された経路ハッシュを返す必要があります。

LocomotiveEngineer.add_missing_stops({from: "New York", to: "Miami"},
                    stop_1: "Washington, DC", stop_2: "Charlotte", stop_3: "Atlanta",
                    stop_4: "Jacksonville", stop_5: "Orlando")
# => {from: "New York", to: "Miami", stops: ["Washington, DC", "Charlotte", "Atlanta", "Jacksonville", "Orlando"]}
Note

停車駅は常に正しい順序で渡されるので、並べ替えについて心配する必要はありません。

4. 経路情報を拡張する

Linusは経路プログラムに取り組んでいて、特定の経路にいくつかの重要な詳細が欠けていることに気づきました。 初期の経路情報はハッシュとして構築されており、友人は、欠けているものがあればそのハッシュを更新してほしいと思っています。 システム内のすべての経路はそれぞれ少しずつ異なる詳細を必要とするので、Linusは汎用的な解決策をぜひ希望しています。

2つのハッシュを受け取るextend_route_information()というメソッドを実装してください。 最初のハッシュには、列車の経路が結ぶ出発地と目的地の都市が含まれます。

2つ目のハッシュには、列車の速度、長さ、温度などの他の経路の詳細が含まれます。 メソッドは、すべての経路情報を含む統合されたハッシュを返す必要があります。

Note

2つ目のハッシュには、例に示されているものとは異なる、あるいはより多くのプロパティを含めることができます。

LocomotiveEngineer.extend_route_information({"from": "Berlin", "to": "Hamburg"}, {"length": "100", "speed": "50"})
# => {"from": "Berlin", "to": "Hamburg", "length": "100", "speed": "50"}
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Ruby Exercism

機関車の運転士を始める準備はできましたか?

Exercismに登録すれば、21個のコンセプト121個の演習、そして本物の人間によるメンタリングとともに、Rubyを学んでマスターできます。すべて無料です。