檜山正幸のキマイラ飼育記 このページをアンテナに追加 RSSフィード Twitter

キマイラ・サイトは http://www.chimaira.org/です。
トラックバック/コメントは日付を気にせずにどうぞ。
連絡は hiyama{at}chimaira{dot}org へ。
蒸し返し歓迎!
このブログの更新は、Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama
ところで、アーカイブってけっこう便利ですよ。

2006-03-22 (水)

returnも嫌いな理由

| 09:26 | returnも嫌いな理由を含むブックマーク

キーワードnewが嫌いだって話をしました。僕、returnも嫌いなんですよ。

メイヤー先生の言い分

ここんところネタにし続けたメイヤー先生に再び登場してもらいます。メイヤー先生は、「returnはよろしくない」理由を3つあげています(『入門』の(5.8.4)、ただし以下の箇条書きは文責:檜山)。

  1. return 変数という形を使うとき、この変数は、計算の中間結果を保持するためにだけ存在している。
  2. return文をあっちこっちに散りばめた、複数の出口を持つ関数が作られやすい。
  3. 関数が実行した最後の文がreturn文でないとき、関数の戻り値が何であるべきか定義する必要がある。

1番目は、これだけだと何言ってるかわかりません。メイヤー先生は、「中間結果を保持する変数を使うなら、そういう変数は“戻り値専用”に決めてしまえ」と言っているのです。実際、Eiffelで採用した方法は、予約された変数Resultです。

2番目は、「そうかも知れない」と思うけど、3番目は、「何であるべきか定義」すればいいだけと思います。いずれもイマイチ説得力に欠けます。

「めんどくさい」という理由

僕がreturnがイヤな理由は、まず「書くのがめんどくさい」から(newのときと同じだ)。んで結局、PerlRubyのように「最後に評価された式が戻り値となる」でいいのじゃないかと思います。メイヤー案と比べてみると、明示的な変数Resultは導入しませんが、式文(式だけからなる文、値は捨てられる)があったときは、あたかもResult := 式(Resultへの代入文)のように解釈することになります。

この方式を採用すると、「関数の戻り値が、どこで返されているか」が分かりにくくなります。メイヤー方式でもこの弊害はありますが、変数Resultを探して追いかければいいので、目印(Result)があるだけ良いといえます。

こんな弊害がある方法を、「めんどくさい」というイイカゲンな理由だけで推奨はできませんね。でも、デメリットをメリットとみなす理屈があるにはあります。

returnを書かなくても分かりやすくする

returnと書いてあれば、そこで関数から抜けて値が返されるのは明白です。こういう明らかな目印がなくても「関数の出口が分かる」ように書くにはどうしたらいいのでしょう。次のような例題で考えます。

  • 関数calcTotalは、数値の列の合計を求める。
  • 第1引数numarrayは、nullであるか、または数値の配列(型が指定できるなら Number[]型)。
  • 第2引数nは、合計する数値の最大個数(先頭から何個を使うか)を示す。

まずはこれ:

function calcTotal(numarray, n) {
 // numarrayがnullのとき0でいいのか? -- それは置いといて
 if (numarray == null) return 0; // 1
 if (numarray.length == 0) return 0; // 2
 if (n <= 0) return 0; // 3
 var total = 0;
 for (var i = 0; i < n; i++) {
  if (i == numarray.length) return total; // 4
  total += numarray[i];
 }
 return total; // 5
}

出口が(字面の上で)5個あります。わざとらしい? (個人的にはコレ、ある意味“わかりやすい”気がしないでもない。)

2番目と3番目のreturnはどうせ不要だから取り除きましょう。for文のなかのreturnはbreakにしてもいいけど、breakもgotoっぽいのでループの上限回数を最初から制限しておくことにします。すると:

function min(x, y) { return (x < y)? x : y; }

function calcTotal(numarray, n) {
 if (numarray == null) return 0; // 1
 var total = 0;
 for (var i = 0; i < min(numarray.length, n); i++) {
  total += numarray[i];
 }
 return total; // 2
}

おおー、出口が2個に減った。が、elseのないif文だと、「最後に評価した式を戻り値とする」方式では問題があります。キチっと場合分けして:

function calcTotal(numarray, n) {
 if (numarray == null) {
  return 0; // 1
 } else  {
  var total = 0;
  for (var i = 0; i < min(n, numarray.length); i++) {
   total += numarray[i];
  }
  return total; // 2
 }
}

totalの初期化を早めにすれば:

function calcTotal(numarray, n) {
 var total = 0;
 if (numarray != null) {
  for (var i = 0; i < min(n, numarray.length); i++) {
   total += numarray[i];
  }
 }
 return total; // 1
}

出口が1個になった、しかも関数の最後。これならreturnがなくても紛らわしくはないでしょう。

メイヤー先生は、こういう出口が少ないコーディングを推奨しているようで、そのとき、totalの代わりに予約された名前Resultを使えば、とおっしゃっています。「最後に評価した式を戻り値とする」方式でも事情は同様で、returnがないことが、よいコーディングへの強制力(むしろ、悪いコーディングへの抑止力)になるかもしれません(強制も抑止も無視されてひどいコーディングになる可能性もあり)。

returnがイヤなほんとの理由

僕が、returnの存在を心底うっとうしく感じるのはラムダ式風の構文においてです。λx.(x + 1)(型付きならλx:int.(x + 1):int)をいくつかの言語で書いてみます。

Scheme
    (lambda (x) (+ x 1))
Caml:
    (fun x -> x + 1)
Groovy:
    {x -> x + 1}
Python
    lambda x: x + 1
JavaScript:
    function (x) {return x + 1}
C++
    <> (int x) -> int {return x + 1;} 

JavaScriptとC++(提案)のreturnがいかにも不格好です。コサキさんは<>->を嫌がっていたけど、僕にはreturnの存在のほうが気に障るぅー。

とおりすがりとおりすがり 2006/03/22 22:47 2番目のやつ(returnが2個)と最後のやつ(returnが1個)どっちがいいかといわれたら、自分は2番目のやつのほうが好きだな。
returnの数が増えるより、ネストが増えるほうがいやなもんで。

m-hiyamam-hiyama 2006/03/23 09:19 とおりすがりさん、
> returnの数が増えるより、ネストが増えるほうがいやなもんで。
判断基準、価値観が違えば「良し悪し」も変わりますね。出口が5つのを「ある意味“わかりやすい”気がしないでもない」と書いたのは、次の理由; 仕様を箇条書きにします。
1. numarrayがnullなら値は0。
2. numarrayが[]なら値は0。
3. nが0以下なら値は0。
4. 配列のn番目の項目までの和を求めて、値とする。
5. 配列が終わってしまったら、そこまでの和が値。
各出口が、箇条書きの各項に一致しているので、確認しやすいですよね。