対象読者

  • 理解しやすく保守しやすいコードを作りたい方
  • プログラミングの関数の「適切な」使い方を知りたい方

結論

関数(function)はプログラミング学習の初級で登場する基本中の基本である。

しかし侮ることなかれ! 基礎故に奥深い。 関数は良いコードを書くための必須の土台であるが、 これを「適切に」使いこなすのは難しい。

そこで本記事では、

  • 関数がなぜ作られたのか、何を解決するための道具なのか
  • その解決に逆行する「不適切な」アンチパターン
  • 関数の適切な使用例

についてまとめる。

関数が生まれた歴史

関数はなぜ生まれたのか。 何かを解決したくてそれが発明されたはずである。 その歴史について述べる。

関数は「繰り返し使う処理を再利用するため」に生まれた。 再利用できなければ同じコードをいろんな場所にコピペするしかない。 それらを変更しようと思ったらコピペ先全てを修正しなければならない。

また、コピペで無駄に行数が膨らんだコードは読みにくい。

想像しただけで悪夢である。コピペは避けるべきだ。

関数は再利用のために生まれた

関数がまだなかった頃は 「別の場所にジャンプ(goto)して元の場所の次に戻る」 ことで再利用を実現していた。 これが「関数呼び出し」の原型であり、「プロシージャ」と呼ぶ。

当初はジャンプ先とジャンプ元の番号をベタ書きしていた。 そのせいでコードを変更する度にジャンプ元の番号を修正しなければならなかった。 なのでジャンプ元の番号を自動で記憶しておく「レジスタ」が発明された。

しかし、レジスタは1つの値しか記憶しないので、2重にジャンプをすると最初の場所に戻れなくなる。 そこで、複数のジャンプ元の番号を積んで取り出せる「スタック」が生まれた。

構造化プログラミング goto文の制限

一般にコードを行ったり来たりするジャンプはコードを読みにくくする。 goto文の多用は悪である。

そこで制限付きgoto文でコードの構造を分かりやすくしようという考え方が生まれた。 これを「構造化プログラミング」と呼ぶ。

条件分岐(if文)や繰り返し(for文)などはジャンプの仕方が決め打ちされたgoto文である。

同様にジャンプ先の管理やgoto文を意識せずに再利用を簡単に実現する仕組みが関数に加わった。

関数の構成要素

名前は番号を分かりやすくする

ジャンプ先の番号などを覚えるのは大変だ。 そこで、番号の代わりに分かりやすい文字列でそれを指し示す仕組みが生まれた。 「名前」である。

URLも同じ発想だ。IPアドレスという数の羅列を分かりやすくする。

名前は文字列と値がある場所を対応づける「辞書」として実装される。 こういう辞書を特別に「名前空間」と呼ぶ。

変数も値に名前を与える。 変数は「値を入れておく箱」というよりも名前空間のキーの1つなのである。 関数名も一連の処理に名前を与える。

グローバル変数の問題点

名前を付けて分かりやすくするのは素晴らしい。 しかし、その名前空間を参照できなければ名前を利用できない。 それができる範囲を「スコープ」と呼ぶ。 名前を利用できるのはそのスコープ内からのみである。

コード全体のどこから参照できる範囲を「グローバルスコープ」、 これを持つ変数を「グローバル変数」と呼ぶ。

グローバル変数には2つ問題がある。

  • 値がどう変化するかはコード全体を確認しないと分からない
  • 使用済みの名前を意図せず新たに宣言してしまうリスクがある

後者の問題を「名前の衝突」と呼ぶ。

要はスコープが広いと把握しづらいということだ。

コードが小さければあまり気にしなくてもいいかもしれない。 でも、複雑さは指数関数的に増大していく。 油断しているとすぐに手に負えなくなる。

ローカルスコープで影響範囲を絞る

ならば、スコープを小分けできれば把握しやすいし、名前の衝突も気にしなくて良くなる。

関数にはそのために関数専用の名前空間を作る。 これを参照できる範囲(関数内)を「ローカルスコープ」、 そこで定義された変数を「ローカル変数」と呼ぶ。

これは関数の外からは参照できない。 この関数内だけの使い捨ての変数である。 この変数の変化を追うのは容易である。

関数はコードを分けて理解しやすくしてくれる。 再利用のために生まれた仕組みではあるが、 一度しか呼び出さないとしても関数には価値がある。

引数と戻り値

隔離されたローカルスコープは外部から参照できない。 では関数はどう外部とやりとりするのか。

関数の外の変数に関数内で結果を代入することでやりとりすることは可能ではある。 しかしそれではローカルスコープの影響範囲の制限が無駄になってしまう。 関数の中身も外も読まないと変数の変化が追えなくなるからだ。

これを回避しつつクリーンに関数外部から値を取り込む機能が「引数」であり、 処理した結果を返す機能が「戻り値」である。 スタックにジャンプ元の場所と一緒に引数と戻り値を積むことでこれを実現する。

コンピュータの背後のこの仕組みのおかげで引数と戻り値だけを考えれば良くなる。 関数を作る時は関数の外の事を考えなくて良いし、 関数を使う時は関数の内部を知る必要はない。

関数はコードを分割して分かりやすくしてくれる。

いろんな関数の呼び方

関数にはいろんな呼び方がある。

  • サブルーチン
  • プロシージャ
  • ラムダ式
  • メソッド

などなどである。

  • サブルーチンは戻り値がない関数の原型(上述)
  • ラムダ式はfunction xxx … のように関数名の宣言なしに作成できる簡易的な関数である
  • メソッドはクラスのメンバの関数

という感じで厳密には意味合いが異なるが、どれも要は関数のことだと思っておけば良いと思う。

関数のアンチパターン

以上で関数の生まれた過程とそれが解決する問題を説明した。

要約すると、

  • 関数は繰り返し使われる処理を再利用するための仕組みとして生まれた
  • 関数は名前と引数と戻り値を外部に公開し、関数内の詳細を気にしなくてもよくしてくれる
  • 関数でコードを分割して影響範囲を狭めることで分かりやすいコードを書ける という感じである。

これらを踏まえて、関数の不適切な使用例についていくつか述べる。

グローバル変数の使用

グローバル変数にアクセスするような関数はイケてない。

  • この値を把握しなければ戻り値を予測できない
  • グローバル変数の値を追うにはコード全体を確認しなければならない

これではローカルスコープが台無しだ。 引数として値を渡すように修正しよう。

引数だけで戻り値が決定されるようにすることで関数のテストが容易になる。

多すぎる引数

グローバル変数を使いたくなるのは、

  • 複数の関数間で引数が共通していて、いちいちそれを引数で与えるのが面倒
  • 引数を減らすため などの動機があるかもしれない。

繰り返すが、グローバル変数は使わないで済むならその方が良い。 それでもグローバル変数を使いたいような時は、

  • 引数を辞書や構造体のような、複数の値をまとめた変数を引数として与える
  • クラスを導入する
  • いくつかの引数を受け取る関数とその戻り値と残りの引数を受け取る関数に分割する

等の対策ができるかもしれない。

長すぎる関数

ローカルスコープは影響範囲を"小さく"隔離することで処理を理解しやすくできる。 行数が多すぎる関数はこれに反している。

目標を小目標に分割するのと同じように、中間結果を出力する関数などに分割できるはずだ。

そうやって分割した関数を呼び出す関数に作り直すことで行数を減らすことができる。 そうやって1つの事をする関数に分割していけば30-50行を超えることを防ぐことができるはずだ。

小さな部品に還元することでコードが大きくなっても理解しやすく保てる。

関数の適切な使用方法

アンチパターンについて述べた。 要は関数が解決すべき問題を解決していれば良いという事だった。

抽象的な話ばかりだった。 最後に、関数のお手本の書き方の具体例を1つリーダブルコード[2]から引用する。

本書では、良い関数を作るコツとして「無関係の下位問題を抽出する」という考え方が挙げられている。

関数の本当にやりたい目標とは直接関係ないが、 それを達成するための中間結果を作成する一連の処理を別の関数として分離することである。 アンチパターンで「関数を分割せよ」と述べたのをさらに具体的に言った感じだ。

リーダブルコードの例 改善前

以下に例を添付する。 じっくり読まなくて良い。 「うわぁ」と思ったらすぐに改善後のコードを見れば良い。

// 引数の経緯lat, 緯度lng と最も近いarrayの要素を返す
// 球面上での2点間の距離で最小のものを返す
var findClosestLocation = function(lat, lng, array) {
  var closest;
  var closest_dist = Number.MAX_VALUE;
  for (var i = 0; i < array.length; i++) {
    var lat_rad = radians(lat);
    var lng_rad = radians(lng);
    var lat2_rad = radians(array[i].latitude);
    var lng2_rad = radians(array[i].longitude);

    var dist = Math.acos(
      Math.sin(lat_rad) * Math.sin(lat2_rad)
      + Math.cos(lat_rad) * Math.cos(lat2_rad)
      * Math.cos(lng_rad - lng2_rad)
    );
    if (dist < closest_dist) {
      closest = array[i];
      closest_dist= dist;
    }
  }
  return closest;
}

リーダブルコードの例 改善後

球面上での2点の距離計算を分離した。 そうすることで元の関数から細かい計算が追い出され理解しやすくなった。 抽出した関数に適切な名前を付けているのも分かりやすい

//元の関数
var findClosestLocation = function(lat, lng, array) {
  var closest;
  var closest_dist = Number.MAX_VALUE;
  for (var i = 0; i < array.length; i++) {
    var dist = spherical_distance(lat, lng, array[i].latitude, array[i].longitude);
    if (dist < closest_dist) {
      closest = array[i];
      closest_dist= dist;
    }
  }
  return closest;
}

// 抽出された下位問題(球面上での2点間の距離計算)
var spherical_distance = function(lat1, lng1, lat2, lng2) {
  var lat_rad = radians(lat1);
  var lng_rad = radians(lng1);
  var lat2_rad = radians(lat2);
  var lng2_rad = radians(lng2);

  return Math.acos(
    Math.sin(lat_rad) * Math.sin(lat2_rad)
    + Math.cos(lat_rad) * Math.cos(lat2_rad)
    * Math.cos(lng_rad - lng2_rad)
  );
}

結び

以上で関数の適切な使用方法について解説した。

  • 関数は処理を再利用のために出来た
  • 関数はローカルスコープを持ち、処理を分割して理解しやすくする
  • 一時的な変数はローカルスコープで捨てる

無関係の下位問題を抽出し、関数を適切に分割していく。 それらに適切な関数名を与える。 そうすることで処理の詳細に溺れるのではなく、 処理の目的・意図に目を向けることができるようになる。

こうして抽象度を高めていくことがプログラミングのテクニックのエッセンスである (詳細はこちら)。

参考

  1. コーディングを支える技術 [西尾泰和]
  2. リーダブルコード [Dustin Boswell, Trevor Foucher]