Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
Ruby TIPS

Ruby TIPS

関数で複数の返り値を返す/関数の引数は値渡し ― コーディングミスを防ぐには?(2)

2016年1月13日

Rubyプログラミングでミスしやすい意外な落とし穴を紹介。関数を使って複数の値を返す方法と、引数による値の受け渡しに関するポイントを説明する。値渡しの関連として、非破壊的な変更と破壊的な変更についても取り上げる。

ローグ・インターナショナル 羽山 博
  • このエントリーをはてなブックマークに追加

 Rubyには、他のプログラミング言語にはない便利な機能が数多く備わっている。一方で意外な落とし穴もある。本稿では関数の基本的なTIPSを見ていく。特に、関数の返り値と引数についてのトピックを取り上げることとする。

関数を使って複数の返り値を返すには

 Rubyの関数を利用すれば、複数の値を返すことができる。これは他のプログラミング言語にはあまりない機能だ。

 方法は簡単。返り値として指定したい複数の式を、カンマで区切ってreturnの後に記述するだけである。呼び出し側では、返り値は多重代入できるので、左辺にカンマで区切って変数を書くとよい。例えば、2つの値を入れ替える関数も、以下のように簡単に作れる。

Ruby
def swap(x,y)
  return y, x      # 複数の値を返す
end
a = 10
b = 20
a, b = swap(a, b)  # 複数の変数に複数の値を代入
puts a, b
リスト1.1 複数の値を返す関数の定義と利用(sample001a.rb)

2つの値を入れ替えるswap関数の定義と利用例。swap関数では、与えられた引数を逆順に返しているだけ。返り値を代入するときには、=の左辺にカンマで区切って変数を順に書く。

 なお、Rubyでは関数の中で最後に評価された式の値が返り値になるので、returnを書かずに、単に式を書いておくだけで返り値が指定できるのだが、ここではreturnを書く必要がある(return y,xの代わりに[y, x]とだけ書き、配列として返り値を指定する方法もある)。実行例は以下の通り。

コンソール
$ ruby sample001a.rb 
20
10
実行例1.1 swap関数を呼び出した結果

実行してみると、変数aの値と変数bの値が入れ替わったことが分かる

 返り値を多重代入せず、1つの変数に代入した場合は、自動的に配列に変換される。以下の例であれば、変数a[20, 10]という配列の参照が代入されることになる。

Ruby
def swap(x,y)
  return y, x
end
a = 10
b = 20
a = swap(a, b)    # 単一の変数に複数の値を多重代入
puts a[0]
リスト1.2 単一の変数に複数の値を多重代入すると配列に変換される(sample001b.rb)

swap関数の返り値は複数あるので、単一の変数aに代入すると自動的に配列に変換される。従って、a[0]の値が20になり、a[1]の値が10になる。

 リスト1.2のプログラムを実行すると、当然のことながら20と表示される。変数aは配列を参照するようになったので、これ以降はa = a + 1のような計算はできなくなる(実行しようとしてもエラーメッセージが表示される)。

コンソール
$ ruby sample001b.rb
20
実行例1.2 配列aの0番目の要素の値を表示してみる

【コラム】配列のイメージ

 リスト1.2のプログラムでは、変数aに複数の値が代入されるので、自動的に配列に変換された。このとき、変数aには配列の値ではなく配列の参照が代入される。イメージとしては以下の図のような感じになる。仕組みについての正しいイメージを持っていないと、思わぬミスに足元をすくわれることもある。「参照」は特に要注意だ。

配列のイメージ
図1.1 配列の参照を代入する

変数aそのものに、配列の要素である2010が代入されるわけではない。変数aには参照が代入される。参照とは、配列などのオブジェクトがどこにあるかという情報(この図では「123」)だと考えればよい。

 図1.1の下の方が、配列のより良いイメージである。ただし、緑色の数字や矢印は説明のために加えたものなので、描かないのが普通である。仮に123という位置に配列があったものとする(実際の値ではないが、話を分かりやすくするためにそう考えよう)。すると、その123という値が変数aに代入される。

 従って、変数aを見れば、配列がどこにあるかが分かる。変数aを使えば、配列の要素である2010が取り扱えるようになるというわけだ。「参照を代入する」とさらっと言われただけで理解しづらいのは、代入の方向(変数a入れる)と、参照の方向(変数aから配列が操作できる)が逆になるからである。このことを理解した上で、手間を省くために上のような図を描くのは構わないが、不正確なイメージのままの理解では困る。本稿のテーマは「コーディングミスを防ぐ」なのだが、単に書き方を間違わないように気を付けようということではなく、できるだけ正確なイメージを持って理解することがミスを防ぐための王道なのである。

 なお、左辺の変数の個数が右辺の関数の返り値の個数よりも少ない場合には、左から順に代入が行われる。

 逆に、左辺の変数の個数が右辺の関数の返り値の個数よりも多い場合には、同じく左から順に代入が行われるが、対応する返り値がない変数にはnilが代入される。元の値がそのまま残っているわけではないので注意が必要だ。

 実験のために、xyで与えられた値を4等分するdiv4関数を作って試してみよう。div4関数の返り値は3つとなる。

 念のため、図解しておくと以下のようになる。

div4関数の引数と返り値
図1.2 div4関数はxとyを4等分したときの分割位置の値を返す

xyの距離をdistとすると、3つの返り値は以下のようにして求められる。
1最初の返り値はx+dist/4.0
22番目の返り値はx+dist/2.0
33番目の返り値はx+dist*3/4.0

 プログラムは以下のようになる。xyのどちらが大きいかは不明なので、xyより大きいときにはxyの値を入れ替え、常にxy以下になるようにしている(リスト1.1のswap関数を使ってもよいが、ここでは直接入れ替えた)。また、整数同士で割り算を行うと結果も整数になり、小数点以下が切り捨てられてしまうので、42といった整数ではなく4.02.0といった浮動小数点数で割り算を行っている。

Ruby
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
リスト1.3 多重代入で、変数の数が異なる場合の動作を確認するコード(sample001c.rb)

div4関数の結果は、左辺が少ない場合については、12.515.017.5となるので、変数p1と変数p2には12.515.0が代入される。

左辺が多い場合の方は、変数q1~変数q3にはdiv4関数の結果が順に代入されるが、変数q4にはnilが代入される。

 このプログラムを実行してみると、以下のようになる。出力にpメソッドを使えば、変数q4nilが代入されていることが分かる(putsではnilと表示されない)。多重代入のときに余った変数には何も代入されない(=元の値が残っている)のではなく、nilが代入されることに要注意だ。

コンソール
$ ruby sample001c.rb 
左辺が少ない場合
12.5
15.0
左辺が多い場合
12.5
15.0
17.5
nil
実行例1.3 多重代入の左辺と右辺の式の数が異なる場合の結果

左から順に値が代入されていき、左辺の変数が余った場合はnilが代入されることが分かる。

【コラム】xからyまでをn等分した位置を返す関数

 リスト1.3のプログラムは実用的でないので、オマケとしてxからyまでをn等分した位置を返すdivn関数を掲載しておく。例えば、A4の用紙を3つに折って封筒に入れるときに、どこで折ればいいかが求められる。もっとも、実際には紙の厚みを考慮して100,100,97の幅になるように(100200の位置で)折るのが普通なのだが、ここでは等分してしまおう。

Ruby
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等分する
リスト1.4 A4の用紙を3つ折りにする位置を求めるプログラム(sample001d.rb)
コンソール
$ ruby sample001d.rb
99.0
198.0
実行例1.4 A4の用紙は99.0、198.0の位置が3つ折の位置

関数の引数は値渡し

 続いて、引数による値の受け渡しに関するポイントを見ていく。まずは、少しばかり基本的なことをおさらいしておこう。

 関数を定義するときに、関数名の後の()の中に書くのが「仮引数(parameter)」、関数を呼び出すときに、関数に与える値として書くのが「実引数(argument)」である。文脈から判断できるときは、特に区別せずに引数と呼ぶことも多い。

 関数を呼び出すと仮引数の値が実引数に渡される。さらっと読み流してしまいそうだが、値が渡されることに注意。渡された値を関数の中で変更しても、元の値は変わらない。常識中の常識だが、一応確認しておこう。以下のcube関数は仮引数に渡された値を3乗する。

Ruby
def cube(x)
  x = x ** 3
end
x = 10.0
cube(x)
puts x
リスト2.1 関数の引数が値渡しであることを確認するコード(sample002a.rb)

変数x10.0を代入した後、cube関数に変数xの値を渡す。cube関数では受け取った値を3乗する。従って、cube関数の中では変数xの値は1000.0になる。しかし、元の変数xcube関数の中の変数xは、たまたま名前が同じであっただけで、全く別のものなので、元の変数xの値は変わらない。

コンソール
$ ruby sample002a.rb
10.0
実行例2.1 関数の値を渡しても元の変数の値は変わらない

当然の結果が得られた。元の変数xの値は変わっていない。

 しかしながら、引数のデータ型によっては、元の値が変わってしまうこともある。以下のcubemat関数は引数として渡された配列の全ての要素を3乗する。

Ruby
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
リスト2.2 関数に配列の参照を渡す(sample002a.rb)

変数xは配列を参照する。要素は10.0という値だけ。cubemat関数に変数xを渡し、各要素を3乗する。値が渡されるなら配列aの値は変わっても、元の配列の要素は10.0のまま変わらない……はず……なのだが。実行してみると???

 []内にリテラルや変数などの式を書くと配列になる。複数の要素がある場合はカンマで区切ればよい。それを変数xに代入するので、変数xは配列を参照することになる。cubemat関数に渡されるのは、変数xである。変数xの値とは配列の参照なので、結局は配列の参照が渡されることになる。以下の図解でイメージをつかもう。

関数に配列を渡す
図2.1 関数に配列の参照を渡す

例によって、仮に123という位置に配列があるものとしよう。緑の矢印はデータの流れ、青の矢印は参照の方向を表す。

  • 1配列を参照する変数xには配列がどこにあるかという情報(ここでは123という値)が入っている。cubemat関数を呼び出すときには、変数xの値(つまり配列の参照)を渡す。従って、変数aにも123という値が代入される。
  • 2変数aに入っているのは配列がどこにあるかという情報。従って、変数aも変数xが参照する配列と同じ配列を参照するようになる。

 関数に配列などのオブジェクトを渡す場合には参照渡しになる、といわれることがあるが、それでは言葉が足りない。図を見ても分かるように、あくまでも渡されているのはである。変数xには参照(の値)が入っていたので、それをそのまま変数aに渡しただけである。

  • × 関数の引数に配列を指定すると、配列全体が渡される……もちろん誤り。配列そのものが渡されるわけではない
  • △ 関数の引数に配列を指定すると、参照渡しになる……言葉が足りない
  • ○ 関数の引数に配列を参照する変数を指定すると、参照を表す値が渡される……より良いイメージ

 図のイメージがつかめれば、リスト2.2の実行結果も想像できるはず。以下の通りだ。

コンソール
$ ruby sample002b.rb
1000.0
実行例2.2 関数に配列の参照を渡して、配列の要素の値を変更する

関数に渡された参照(の値)を利用して、配列の要素の値を変更した。引数として渡した変数xも同じ配列を参照しているので、元の配列の値が変わることになる。

【コラム】文字列の場合も参照の値が渡されるが、挙動が異なることがある

 文字列の取り扱いについては、またあらためて取り上げたいが、関連する話なので、少し見ておこう。まずは、以下のプログラムを見てほしい。

 変数に文字列を代入する(ように見える)文を書くと、文字列そのものが代入されるわけではなく、文字列の参照が代入される。つまり変数xには"Ruby"という文字列の参照が入っている。これをaddVersionという関数に渡す。文字列の参照が渡されるので、関数の中で文字列を変更すれば、元の変数xで参照される文字列も変わるはず……と思われるかもしれないが、実はそうはならない。

Ruby
def addVersion(a, v)
  a += v
end
x = "Ruby"              # xは文字列
addVersion(x, "2.3.0")  # 文字列の参照が渡される
puts x
リスト2.3 関数に文字列の参照を渡す(sample002c.rb)

 実行例は以下の通り。参照を渡したので元の文字列が変更されると思ったのだが、実際には変更されていない。

コンソール
$ ruby sample002c.rb
Ruby
実行例2.3 関数に配列の参照を渡して、配列の要素の値を変更する

 じゃあ、文字列は値渡しで、文字列そのものが渡されているの? と思う人もいるかもしれないが、そうではない。あくまでも渡されているのは変数xの値、つまり文字列の参照である。従って、addVersion関数の中では変数aも同じ文字列("Ruby")を参照している。

 しかし、a += vという式がクセモノである。変数aが参照する文字列に変数vが参照する文字列をつなぎ合わせるのだが、元の文字列が変更されるのではなく、新しい文字列が作られ、その参照が変数aに入れられる。このような変更は非破壊的な変更と呼ばれる。文字列には破壊的な変更を行うメソッドもあるので、それを使えば元の文字列を変更できる。例えば、文字列を連結するためのconcatメソッドは破壊的なメソッドである。

Ruby
def addVersion(a, v)
  a.concat(v)
end
x = "Ruby"              # xは文字列
addVersion(x, "2.3.0")  # 文字列の参照が渡される
puts x
リスト2.4 破壊的なメソッドを使う(sample002d.rb)

 文字列(=Stringクラス)には非破壊的なメソッドと破壊的なメソッドがあるので、その違いを意識しておくことは重要である。upcase(大文字にするための非破壊的メソッド)とupcase!(大文字にするための破壊的メソッド)のように、同じ名前で、最後に!が付くか付かないかで働きが違う場合もあるので要注意。

 また、破壊的なメソッドを使わずに、非破壊的に変更された文字列を関数が返す方法を使ってもよい。リスト2.3のaddVersion関数はそうなっているので、以下のようなコードが書ける(returnは書かれていないが、最後に評価される式の値が返される)。

Ruby
def addVersion(a, v)
a += v     # a(が参照している文字列)が返り値になる
end
x = "Ruby" # xは文字列
x = addVersion(x, "2.3.0") # 非破壊的に作られた文字列の参照を代入
puts x
リスト2.5 非破壊的に変更された文字列の参照を代入する(sample002d.rb)

 リスト2.4も2.5も、実行すればRuby2.3.0という結果が表示される。文字列の場合も、文字列そのものではなく、やはり文字列の参照の値が渡されている。ただし、文字列を破壊的に変更するか非破壊的に変更するかによって結果が異なるというわけだ。

まとめ

 Rubyの関数は複数の値を返せる。返り値はカンマで区切った変数の並びに順に代入できる(多重代入)。Rubyの関数では、引数は値渡しである。配列などのオブジェクトを参照する変数を渡した場合も、参照を表すが渡される。それを利用すれば元の配列を変更できる。

処理対象:関数 カテゴリ:文法

※以下では、本稿の前後を合わせて5回分(第1回~第5回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

Ruby TIPS
1. Rubyとは? ― オブジェクト指向のスクリプト言語

Rubyとは何か。入門者向けに、その概要と特徴を紹介し、基本的な文法を使った簡単なコード例を示す。

Ruby TIPS
2. 文の途中で改行する/if文も値を返す ― コーディングミスを防ぐには?(1)

Rubyプログラミングでミスしやすい意外な落とし穴を紹介。「式の取り扱い」に関して、改行で陥りやすいワナやif文などの制御構造が返す値を取り上げる。

Ruby TIPS
3. 【現在、表示中】≫ 関数で複数の返り値を返す/関数の引数は値渡し ― コーディングミスを防ぐには?(2)

Rubyプログラミングでミスしやすい意外な落とし穴を紹介。関数を使って複数の値を返す方法と、引数による値の受け渡しに関するポイントを説明する。値渡しの関連として、非破壊的な変更と破壊的な変更についても取り上げる。

Ruby TIPS
4. Rubyをインストール/アップデートするには?(Windows編)

Windows上でのRubyプログラミングを始める入門者向けに、Ruby環境の構築方法、複数バージョンのインストール方法、バージョンのアップデート方法を説明する。

Ruby TIPS
5. 代入により決まる変数のデータ型/丸め誤差が発生する浮動小数点数の比較― コーディングミスを防ぐには?(3)

初心者向けにRubyプログラミングの落とし穴を紹介。代入する値により変数のデータ型が決まることに関する注意点と、浮動小数点数の比較における丸め誤差の問題と回避方法について説明する。

サイトからのお知らせ

Twitterでつぶやこう!