Ruby TIPS
関数で複数の返り値を返す/関数の引数は値渡し ― コーディングミスを防ぐには?(2)
Rubyプログラミングでミスしやすい意外な落とし穴を紹介。関数を使って複数の値を返す方法と、引数による値の受け渡しに関するポイントを説明する。値渡しの関連として、非破壊的な変更と破壊的な変更についても取り上げる。
Rubyには、他のプログラミング言語にはない便利な機能が数多く備わっている。一方で意外な落とし穴もある。本稿では関数の基本的なTIPSを見ていく。特に、関数の返り値と引数についてのトピックを取り上げることとする。
関数を使って複数の返り値を返すには
Rubyの関数を利用すれば、複数の値を返すことができる。これは他のプログラミング言語にはあまりない機能だ。
方法は簡単。返り値として指定したい複数の式を、カンマで区切ってreturn
の後に記述するだけである。呼び出し側では、返り値は多重代入できるので、左辺にカンマで区切って変数を書くとよい。例えば、2つの値を入れ替える関数も、以下のように簡単に作れる。
def swap(x,y)
return y, x # 複数の値を返す
end
a = 10
b = 20
a, b = swap(a, b) # 複数の変数に複数の値を代入
puts a, b
|
2つの値を入れ替えるswap
関数の定義と利用例。swap
関数では、与えられた引数を逆順に返しているだけ。返り値を代入するときには、=
の左辺にカンマで区切って変数を順に書く。
なお、Rubyでは関数の中で最後に評価された式の値が返り値になるので、return
を書かずに、単に式を書いておくだけで返り値が指定できるのだが、ここではreturn
を書く必要がある(return y,x
の代わりに[y, x]
とだけ書き、配列として返り値を指定する方法もある)。実行例は以下の通り。
$ ruby sample001a.rb
20
10
|
実行してみると、変数a
の値と変数b
の値が入れ替わったことが分かる
返り値を多重代入せず、1つの変数に代入した場合は、自動的に配列に変換される。以下の例であれば、変数a
に[20, 10]という配列の参照が代入されることになる。
def swap(x,y)
return y, x
end
a = 10
b = 20
a = swap(a, b) # 単一の変数に複数の値を多重代入
puts a[0]
|
swap
関数の返り値は複数あるので、単一の変数a
に代入すると自動的に配列に変換される。従って、a[0]
の値が20になり、a[1]
の値が10になる。
リスト1.2のプログラムを実行すると、当然のことながら20と表示される。変数a
は配列を参照するようになったので、これ以降はa = a + 1
のような計算はできなくなる(実行しようとしてもエラーメッセージが表示される)。
$ ruby sample001b.rb
20
|
【コラム】配列のイメージ
リスト1.2のプログラムでは、変数a
に複数の値が代入されるので、自動的に配列に変換された。このとき、変数a
には配列の値ではなく配列の参照が代入される。イメージとしては以下の図のような感じになる。仕組みについての正しいイメージを持っていないと、思わぬミスに足元をすくわれることもある。「参照」は特に要注意だ。
変数a
そのものに、配列の要素である20と10が代入されるわけではない。変数a
には参照が代入される。参照とは、配列などのオブジェクトがどこにあるかという情報(この図では「123」)だと考えればよい。
図1.1の下の方が、配列のより良いイメージである。ただし、緑色の数字や矢印は説明のために加えたものなので、描かないのが普通である。仮に123という位置に配列があったものとする(実際の値ではないが、話を分かりやすくするためにそう考えよう)。すると、その123という値が変数a
に代入される。
a
を見れば、配列がどこにあるかが分かる。変数a
を使えば、配列の要素である20と10が取り扱えるようになるというわけだ。「参照を代入する」とさらっと言われただけで理解しづらいのは、代入の方向(変数a
に入れる)と、参照の方向(変数a
から配列が操作できる)が逆になるからである。このことを理解した上で、手間を省くために上のような図を描くのは構わないが、不正確なイメージのままの理解では困る。本稿のテーマは「コーディングミスを防ぐ」なのだが、単に書き方を間違わないように気を付けようということではなく、できるだけ正確なイメージを持って理解することがミスを防ぐための王道なのである。なお、左辺の変数の個数が右辺の関数の返り値の個数よりも少ない場合には、左から順に代入が行われる。
逆に、左辺の変数の個数が右辺の関数の返り値の個数よりも多い場合には、同じく左から順に代入が行われるが、対応する返り値がない変数にはnilが代入される。元の値がそのまま残っているわけではないので注意が必要だ。
実験のために、x
とy
で与えられた値を4等分するdiv4
関数を作って試してみよう。div4
関数の返り値は3つとなる。
念のため、図解しておくと以下のようになる。
x
とy
の距離をdist
とすると、3つの返り値は以下のようにして求められる。
1最初の返り値はx+dist/4.0
22番目の返り値はx+dist/2.0
33番目の返り値はx+dist*3/4.0
プログラムは以下のようになる。x
とy
のどちらが大きいかは不明なので、x
がy
より大きいときにはx
とy
の値を入れ替え、常にx
がy
以下になるようにしている(リスト1.1のswap
関数を使ってもよいが、ここでは直接入れ替えた)。また、整数同士で割り算を行うと結果も整数になり、小数点以下が切り捨てられてしまうので、4や2といった整数ではなく4.0や2.0といった浮動小数点数で割り算を行っている。
def div4(x,y)
if x > y # xがyより大きいときは、
x, y = y, x # 入れ替えて、常にxの方が小さくなるようにする
end
dist = y - x
return x + dist/4.0, x + dist/2.0, x + dist*3/4.0
end
p1, p2 = div4(10, 20)
puts "左辺が少ない場合"
p p1, p2
q4 = 30 # q4がnilになることを確認するためにq4に値を代入しておく
q1, q2, q3, q4 = div4(10, 20)
puts "左辺が多い場合"
p q1, q2, q3, q4
|
div4
関数の結果は、左辺が少ない場合については、12.5、15.0、17.5となるので、変数p1
と変数p2
には12.5と15.0が代入される。
左辺が多い場合の方は、変数q1
~変数q3
にはdiv4
関数の結果が順に代入されるが、変数q4
にはnilが代入される。
このプログラムを実行してみると、以下のようになる。出力にp
メソッドを使えば、変数q4
にnilが代入されていることが分かる(puts
ではnilと表示されない)。多重代入のときに余った変数には何も代入されない(=元の値が残っている)のではなく、nilが代入されることに要注意だ。
$ ruby sample001c.rb
左辺が少ない場合
12.5
15.0
左辺が多い場合
12.5
15.0
17.5
nil
|
左から順に値が代入されていき、左辺の変数が余った場合はnilが代入されることが分かる。
【コラム】xからyまでをn等分した位置を返す関数
リスト1.3のプログラムは実用的でないので、オマケとしてx
からy
までをn等分した位置を返すdivn
関数を掲載しておく。例えば、A4の用紙を3つに折って封筒に入れるときに、どこで折ればいいかが求められる。もっとも、実際には紙の厚みを考慮して100,100,97の幅になるように(100と200の位置で)折るのが普通なのだが、ここでは等分してしまおう。
def divn(x,y,n)
if x > y # xがyより大きいときは、
x, y = y, x # 入れ替えて、常にxの方が小さくなるようにする
end
dist = y - x # 全体の幅
part = dist * 1.0 / n # 間隔
ret = Array.new(n-1) # 配列を作る。n等分なら分割位置はn-1個
for i in 0..n-2 # 分割位置を求め、順に配列に入れる
ret[i] = x + part * (i + 1)
end
return ret # 配列を返す
end
puts divn(0, 297, 3) # A4の用紙を3等分する
|
$ ruby sample001d.rb
99.0
198.0
|
関数の引数は値渡し
続いて、引数による値の受け渡しに関するポイントを見ていく。まずは、少しばかり基本的なことをおさらいしておこう。
関数を定義するときに、関数名の後の()
の中に書くのが「仮引数(parameter)」、関数を呼び出すときに、関数に与える値として書くのが「実引数(argument)」である。文脈から判断できるときは、特に区別せずに引数と呼ぶことも多い。
関数を呼び出すと仮引数の値が実引数に渡される。さらっと読み流してしまいそうだが、値が渡されることに注意。渡された値を関数の中で変更しても、元の値は変わらない。常識中の常識だが、一応確認しておこう。以下のcube
関数は仮引数に渡された値を3乗する。
def cube(x)
x = x ** 3
end
x = 10.0
cube(x)
puts x
|
変数x
に10.0を代入した後、cube
関数に変数x
の値を渡す。cube
関数では受け取った値を3乗する。従って、cube
関数の中では変数x
の値は1000.0になる。しかし、元の変数x
とcube
関数の中の変数x
は、たまたま名前が同じであっただけで、全く別のものなので、元の変数x
の値は変わらない。
$ ruby sample002a.rb
10.0
|
当然の結果が得られた。元の変数x
の値は変わっていない。
しかしながら、引数のデータ型によっては、元の値が変わってしまうこともある。以下のcubemat
関数は引数として渡された配列の全ての要素を3乗する。
def cubemat(a)
for i in 0..a.length-1
a[i] = a[i] ** 3
end
end
x = [10.0] # xは配列になる
cubemat(x) # 配列の参照が渡される
puts x
|
変数x
は配列を参照する。要素は10.0という値だけ。cubemat
関数に変数x
を渡し、各要素を3乗する。値が渡されるなら配列a
の値は変わっても、元の配列の要素は10.0のまま変わらない……はず……なのだが。実行してみると???
[]
内にリテラルや変数などの式を書くと配列になる。複数の要素がある場合はカンマで区切ればよい。それを変数x
に代入するので、変数x
は配列を参照することになる。cubemat
関数に渡されるのは、変数x
の値である。変数x
の値とは配列の参照なので、結局は配列の参照が渡されることになる。以下の図解でイメージをつかもう。
例によって、仮に123という位置に配列があるものとしよう。緑の矢印はデータの流れ、青の矢印は参照の方向を表す。
- 1配列を参照する変数
x
には配列がどこにあるかという情報(ここでは123という値)が入っている。cubemat
関数を呼び出すときには、変数x
の値(つまり配列の参照)を渡す。従って、変数a
にも123という値が代入される。 - 2変数
a
に入っているのは配列がどこにあるかという情報。従って、変数aも変数
x
が参照する配列と同じ配列を参照するようになる。
関数に配列などのオブジェクトを渡す場合には参照渡しになる、といわれることがあるが、それでは言葉が足りない。図を見ても分かるように、あくまでも渡されているのは値である。変数x
には参照(の値)が入っていたので、それをそのまま変数a
に渡しただけである。
- × 関数の引数に配列を指定すると、配列全体が渡される……もちろん誤り。配列そのものが渡されるわけではない
- △ 関数の引数に配列を指定すると、参照渡しになる……言葉が足りない
- ○ 関数の引数に配列を参照する変数を指定すると、参照を表す値が渡される……より良いイメージ
図のイメージがつかめれば、リスト2.2の実行結果も想像できるはず。以下の通りだ。
$ ruby sample002b.rb
1000.0
|
関数に渡された参照(の値)を利用して、配列の要素の値を変更した。引数として渡した変数x
も同じ配列を参照しているので、元の配列の値が変わることになる。
【コラム】文字列の場合も参照の値が渡されるが、挙動が異なることがある
文字列の取り扱いについては、またあらためて取り上げたいが、関連する話なので、少し見ておこう。まずは、以下のプログラムを見てほしい。
変数に文字列を代入する(ように見える)文を書くと、文字列そのものが代入されるわけではなく、文字列の参照が代入される。つまり変数x
には"Ruby"という文字列の参照が入っている。これをaddVersion
という関数に渡す。文字列の参照が渡されるので、関数の中で文字列を変更すれば、元の変数x
で参照される文字列も変わるはず……と思われるかもしれないが、実はそうはならない。
def addVersion(a, v)
a += v
end
x = "Ruby" # xは文字列
addVersion(x, "2.3.0") # 文字列の参照が渡される
puts x
|
実行例は以下の通り。参照を渡したので元の文字列が変更されると思ったのだが、実際には変更されていない。
$ ruby sample002c.rb
Ruby
|
じゃあ、文字列は値渡しで、文字列そのものが渡されているの? と思う人もいるかもしれないが、そうではない。あくまでも渡されているのは変数x
の値、つまり文字列の参照である。従って、addVersion
関数の中では変数a
も同じ文字列("Ruby")を参照している。
しかし、a += v
という式がクセモノである。変数a
が参照する文字列に変数v
が参照する文字列をつなぎ合わせるのだが、元の文字列が変更されるのではなく、新しい文字列が作られ、その参照が変数a
に入れられる。このような変更は非破壊的な変更と呼ばれる。文字列には破壊的な変更を行うメソッドもあるので、それを使えば元の文字列を変更できる。例えば、文字列を連結するためのconcat
メソッドは破壊的なメソッドである。
def addVersion(a, v)
a.concat(v)
end
x = "Ruby" # xは文字列
addVersion(x, "2.3.0") # 文字列の参照が渡される
puts x
|
文字列(=String
クラス)には非破壊的なメソッドと破壊的なメソッドがあるので、その違いを意識しておくことは重要である。upcase
(大文字にするための非破壊的メソッド)とupcase!
(大文字にするための破壊的メソッド)のように、同じ名前で、最後に!が付くか付かないかで働きが違う場合もあるので要注意。
また、破壊的なメソッドを使わずに、非破壊的に変更された文字列を関数が返す方法を使ってもよい。リスト2.3のaddVersion
関数はそうなっているので、以下のようなコードが書ける(return
は書かれていないが、最後に評価される式の値が返される)。
def addVersion(a, v)
a += v # a(が参照している文字列)が返り値になる
end
x = "Ruby" # xは文字列
x = addVersion(x, "2.3.0") # 非破壊的に作られた文字列の参照を代入
puts x
|
リスト2.4も2.5も、実行すればRuby2.3.0
という結果が表示される。文字列の場合も、文字列そのものではなく、やはり文字列の参照の値が渡されている。ただし、文字列を破壊的に変更するか非破壊的に変更するかによって結果が異なるというわけだ。
まとめ
Rubyの関数は複数の値を返せる。返り値はカンマで区切った変数の並びに順に代入できる(多重代入)。Rubyの関数では、引数は値渡しである。配列などのオブジェクトを参照する変数を渡した場合も、参照を表す値が渡される。それを利用すれば元の配列を変更できる。
※以下では、本稿の前後を合わせて5回分(第1回~第5回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
2. 文の途中で改行する/if文も値を返す ― コーディングミスを防ぐには?(1)
Rubyプログラミングでミスしやすい意外な落とし穴を紹介。「式の取り扱い」に関して、改行で陥りやすいワナやif文などの制御構造が返す値を取り上げる。
3. 【現在、表示中】≫ 関数で複数の返り値を返す/関数の引数は値渡し ― コーディングミスを防ぐには?(2)
Rubyプログラミングでミスしやすい意外な落とし穴を紹介。関数を使って複数の値を返す方法と、引数による値の受け渡しに関するポイントを説明する。値渡しの関連として、非破壊的な変更と破壊的な変更についても取り上げる。
4. Rubyをインストール/アップデートするには?(Windows編)
Windows上でのRubyプログラミングを始める入門者向けに、Ruby環境の構築方法、複数バージョンのインストール方法、バージョンのアップデート方法を説明する。
5. 代入により決まる変数のデータ型/丸め誤差が発生する浮動小数点数の比較― コーディングミスを防ぐには?(3)
初心者向けにRubyプログラミングの落とし穴を紹介。代入する値により変数のデータ型が決まることに関する注意点と、浮動小数点数の比較における丸め誤差の問題と回避方法について説明する。