Subterranean Flower

JavaScriptの関数で何ができるのか、もう一度考える

Author
古都こと
ふよんとえんよぷよぐやま!!!

関数(Function)は、ほぼ全ての現代的なプログラミング言語が備えている、プログラミングにおける基本的な構成要素です。もちろんJavaScriptも関数を備えています。

関数はシンプルで強力です。ですが、それゆえに多くの人を混乱させることもあります。私が大学のプログラミング実習のティーチングアシスタントを担当していたときも、「関数」でつまずく学生が少なくありませんでした。

関数は普段何気なく呼吸をするように使っているものです。ですが、改めて、関数とは何か、何ができるのか、ということを考え出すと、意外と難しいことに気がつきます。そこで、JavaScriptにおける関数について、いったい何ができるのか、もう一度ゆっくり考え直してみることにしましょう。

関数って何ができるんだっけ

複数の命令をまとめて名前をつける

関数の最も基本的な機能は、複数の命令をひとつにまとめて、名前をつけることです。

function printGreeting() {
  console.log('こんにちは!');
  console.log('私は古都ことです。')
}

printGreeting();

名前をつけた命令群は、「関数名()」のシンタックスで呼び出すことができます。関数が呼び出されると、関数の中に記述した命令群が実行されます。関数の呼び出しは何度でも可能です。

ここでは挨拶を表示する命令群に「printGreeting」という名前をつけています。関数の名前のあとにカッコをつけることで、つまり「printGreeting()」とすることで、一連の命令を呼び出すことができます。

値を受け取って処理する

関数は、ただ命令をまとめるだけではありません。関数を呼び出すときに、いくつかの値を受け取ることができます。受け取った値を処理し、実行結果に反映させることが可能です。

function printSum(x, y) {
  const sum = x + y;
  console.log(`${x}+${y}は${sum}です。`);
}

printSum(1, 2); // 1+2は3です。
printSum(3, 4); // 3+4は7です。

この受け取る値のことを引数(ひきすう)と言います。ここでは変数aと変数bが引数となります。

引数を使えば、実行ごとに異なる処理を行うことが可能となります。上の例では、足す数を引数にすることにより、1+2の場合も、3+4の場合も、同じ関数を使うことができるようになっています。引数を活用することで、柔軟なプログラムを書くことが可能になります。

値を返す

関数内で全ての処理をしてしまうのではなく、値を返すこともできます。関数が値を返すと、関数呼び出しはその返した値に置き換えられ、処理が進みます。

function sum(x, y) {
  return x + y;
}

const a = 1;
const b = 2;
const c = 3;
const d = 4;

const sumAB = sum(a, b); // 3
const sumCD = sum(c, d); // 7

console.log(`${a}+${b}は${sumAB}です。`);
console.log(`${c}+${d}は${sumCD}です。`);

値を返すには、return文を使用します。return文を使用して値を返すと、関数呼び出しはその値に置き換えられます。この例の場合、例えばsum(1, 2)は1+2を返すので、値は3となります。

これは数学における関数の動きとよく似ています。例えばf(x) = x^2 + 1という数学的関数があった場合、x=2を代入するとf(2) = 5となります。これと同じです。このf(x)をJavaScriptで実装してみましょう。

function f(x) {
  return x * x + 1;
}

console.log(f(2)); // 5

ほら!全く同じ仕組みです!

スコープを作る

関数はスコープを作ります。スコープというのは、変数の有効範囲(寿命)のことです。関数内で宣言された変数は、関数内で寿命を終えます。つまり、関数の中で宣言された変数は、関数の外からアクセスすることはできません。

function func() {
  const val = 10;
  console.log(`valは${val}です!`); // valは10です!
}

func();
console.log(`valは${val}です!`); // エラー

この例では、関数の中の変数であるvalの値を、関数の中と外からそれぞれ出力しようと試みています。関数の中からのアクセスは成功して値が表示されますが、関数の外からvalにアクセスしようとするとエラーが出ます。これがスコープです。

関数のスコープを邪魔だと思う人もいるかもしれません。ですが、このスコープのおかげで関数の外は常に清潔に保たれます。「中」と「外」を明確に区別することで、変数の混乱を防ぐことができるのです。

つまり:値を受け取って処理して返す

つまり、関数というものは、値を受け取って、処理して、値を返す。それだけの全くのシンプルな道具です。関数は柔軟性に富み、様々な場面で活用することができます。

それで、関数は何に使うの?

関数はシンプルで便利な道具です。どんな場面でも使えますし、どんな用途にも適合します。しかし、だからこそ次のような疑問が湧いてきます。「結局のところ何に使うものなの?」と。

大学でティーチングアシスタントをしていて、学生から「関数の使用方法はわかる。けど何に使うのかさっぱりわからない」という声を何度も聞きました。私もプログラミングを学びたての頃は全く同じ疑問を持ちましたし、プログラマを目指す人にとって共通の悩みなのかもしれません。

関数というものは、(大学の講義に限らず)プログラミングを習うにあたって序盤で触れることが多い機能です。しかし、短いプログラムしか書くことのない初めの頃では、関数の説明を聞いてもいまいち実感が湧きませんし、高度なプログラムが要求されるようになってくる頃には今更「関数」の説明はされません。関数の活用方法についての説明は、なにかとうやむやにされがちなのです。

そこで、ここでは関数の活用方法について、いくつか実例を交えながら紹介していきたいと思います。

命令をまとめる(重複コードの削除)

関数の最も基本的な活用方法は、繰り返しを避けることです。巷ではDRY原則(Don’t Repeat Yourself; 同じことを繰り返すな)と言われるものの実践です。関数は複数の命令をひとつにまとめることができ、何度でも呼び出すことができます。これを利用して、プログラムの中から同じ「繰り返し」を省くことができる場合があります。

例えば以下のようなコードを考えてみましょう:

const startTime = '09:30'
const endTime   = '12:03'

// 与えられた文字列から時と分に切り分けて、
// 適切な文字列に変換する。
const startSplit = startTime.split(':');
const startHour = parseInt(startSplit[0], 10);
const startMinute = parseInt(startSplit[1], 10);
const startStr = `${startHour}時${startMinute}分`;

// もう一回同じことをやる!
const endSplit = endTime.split(':');
const endHour = parseInt(endSplit[0], 10);
const endMinute = parseInt(endSplit[1], 10);
const endStr = `${endHour}時${endMinute}分`;

console.log(`${startStr}から${endStr}までです。`);

これは文字列として用意された時刻を、数値にパースして、適切に表示するコードです。やっていることはシンプルですが、コードの中に、全く同じことをしている重複コードを見つけることができます。

重複コードは厄介なものです。重複コードがあると、プログラマを混乱させ、プログラム品質を低下させます。同じことを何度も書いているから無駄、というだけではなく、仮に変更があった場合に全ての重複コードに対して同じ変更を行わなければならず、変更漏れなどがあった場合、バグの発生原因となります。

重複コードはできる限り取り除くというのが、プログラミングにおける一般常識となっています。今回の場合は関数を使って命令をまとめ、重複コードを取り除くことができます。実際にやってみましょう。

function parseTimeString(timeStr) {
  const split = timeStr.split(':');
  const hour = parseInt(split[0], 10);
  const minute = parseInt(split[1], 10);
  
  return `${hour}時${minute}分`;
}

const startTime = '09:30'
const endTime   = '12:03'

const startStr = parseTimeString(startTime);
const endStr = parseTimeString(endTime);

console.log(`${startStr}から${endStr}までです。`);

重複コードを省くことができました!関数を使うことで、一連のコードをひとつにまとめ、コードの見通しを良くすることができるのです。

現実には重複コードはあまり存在しない

……と、ここまでが出来の悪い教科書によく書いてある「関数の使い方」です。しかし大きな問題があります。現実には、重複コードなんてものには、ほとんど出会わないのです。

そもそもプログラムというものは繰り返し処理が得意なものです。同じことを繰り返すような重複コードがあった場合、配列やループをうまく使うことで、簡単に繰り返しを取り除くことができます。先ほどの例を考えてみましょう。関数を使わずとも、以下のように書くことができます:

const startTime = '09:30'
const endTime   = '12:03'

const timeStrList = [startTime, endTime];
const parsedStrList = [];

for(let str of timeStrList) {
  const split = str.split(':');
  const hour = parseInt(split[0], 10);
  const minute = parseInt(split[1], 10);
  
  parsedStrList.push(`${hour}時${minute}分`);
}

const startStr = parsedStrList[0];
const endStr = parsedStrList[1];

console.log(`${startStr}から${endStr}までです。`);

つまり、プログラムが直線的で、コードが十分整って書かれているなら、重複コードは発生しないのです。さらに、重複コードはコードに関わる人数が少ないほど減ります。また、コードが短ければ、その分だけ重複コード発生の可能性は低くなります。

よって、「ひとり」で「短いコード」を書く、大学のプログラミング演習においては、重複コードに出会うことは全く無く、従って関数の活用方法を全く見出せないまま一年が終わってしまうのです。学生が関数の扱いに困るのも当然です。

処理に名前をつける

では、関数は何に使えばいいのでしょう?少し視点を変えてみましょう。関数は、複数の命令をひとまとめにして名前をつけるものでした。「重複コードの削除」では「複数の命令をひとまとめにする」に焦点を当てた使い方を検討しました。今回は「名前をつける」のほうに着目してみましょう。

例えば以下のようなコードを考えてみましょう:

const width = 10.5;
const height = 8;
const area = width * height;

console.log(area);

実にシンプルなコードです。きっと四角形の面積を求めるコードでしょう。重複コードはありませんし、関数を導入する余地はないように思えます。しかし、ここにあえて関数を導入してみましょう:

function calcTriangleArea(width, height) {
  return width * height;
}

const width = 10.5;
const height = 8;
const area = calcTriangleArea(width, height);

console.log(area);

関数を使うことで、簡単な計算に名前がつきました。「calcTriangleArea」と。これは一見全く無駄なように思えます。一行で済んでいたことを、わざわざ関数にして行数を増やしたのですから!

しかしこの作業によって驚くべきことが判明します。このコードは、実は三角形の面積を求めるコードだったのです!……つまりこのコードにはバグが潜んでいます。三角形の面積は「底辺x高さ/2」です。バグを直してみましょう。

function calcTriangleArea(width, height) {
  return width * height / 2;
}

const width = 10.5;
const height = 8;
const area = calcTriangleArea(width, height);

console.log(area);

これはたった一行の関数ですが、処理にわざわざ名前をつけることで、計算が何をしているのかという意味が明確になり、バグを発見することもできました。名前をつけるということは、それほどまでに大きな意味を持ちます。

関数の用途を、重複コードの削除に限定する必要はありません。たった一行の計算式でも、名前をつけるだけで、プログラムの読み手に、意味を今までより明確に示すことができるかもしれません。プログラムが読みやすくなれば、結果的にバグが減り、コードの品質が上がります。ただ名前をつけるだけで、です。

関数を使わなくとも「名前をつける」ことはできる

ただし、これは極端な例です。この場合は、変数「area」の名前を「triangleArea」に変更するだけで解決できるかもしれません。コメントに「// 三角形の面積を求める」と書くことでも解決できるでしょう。よりシンプルな解決方法がある場合は、現実的にはそちらを採用すべきです。

そう考えると、「名前をつける」という関数の活用方法も、少し怪しくなってきました。変数の名前を少し工夫したり、ちょっとコメントを付け加えるだけで解決できる問題の方が、世の中には多いからです。

「命令をまとめる」も「名前をつける」も、どちらもコード品質の向上に貢献することは間違いありません。しかし、これらの場合において、関数はあくまで問題解決のための一手段として選択肢に挙がるだけで、「関数があったからこそ実現できた」とは言いがたい状況です。

では、関数はプログラミングにおいて不要なのでしょうか。何か他に活用方法はないのでしょうか。

具体と抽象

ここから重要な話をします!メモを取ってください!

……と言われたら、あなたはメモ帳を取り出して、メモを取る準備を始めるでしょう。「メモを取ってください」で意味が通じるからです。なので、普通の人は次のような言い方はしません:

「ポケットからメモ帳を取り出して、ポケットからペンを取り出して、利き手でペンを持って、メモ帳を開いて、メモ帳の開いてる箇所に、重要だと思ったことを、利き手に持ったペンを使って、書き留めてください」

突然こんな煩雑で長ったらしいことを言われたら、何をしていいかわからなくなるでしょう。しかし我々はなぜこういう言い方をしないのでしょう?

これには具体抽象という概念が関わってきます。具体とはありのままの姿のことで、抽象というのは物事の本質をついた表面的なもののことです。メモ帳の例えの場合は、無駄に長くバカバカしい方が具体、端的でわかりやすい方が抽象です。

人間は抽象、プログラムは具体

我々人間は抽象の世界で生きています。物事を抽象的に捉え、抽象的にコミュニケーションを行います。日常生活の中で、具体が顔を出すことは、まずありません。我々は「コーラが飲みたい」と表現することはあっても、「台所に行って、コップを取って、冷蔵庫を開けて、コーラを取って、コーラの蓋を開けて、コップにコーラを入れて、コーラを入れたコップをここまで持ってきてほしい」とは言いません。人間にとって、具体を扱うことは非常に困難なのです。

しかしコンピュータは違います。コンピュータは具体が得意です。抽象はうまく扱えません。「変数aに1を代入して変数bに2を代入して、変数sumにa+bを代入して、変数sumを表示する」と具体的な指示が必要です。それがプログラムです。プログラムでは具体的な命令を書き連ねて、コンピュータを制御します。

この人間とコンピュータの違いが悲しいすれ違いを生みます。プログラムのコードは常に具体で書かれるのに、人間は具体を理解することができません。コードが数行程度の小規模なら頑張って理解することはできます。しかし規模が大きくなると、コードを書いた本人にも読んで理解することができなくなります。

「読めないコード」は、いわゆる「品質の低いコード」となります。品質の低いコードは変更に弱く、大量のバグを生み、最悪の場合動作すらしません。人間の具体に対する理解力の低さが、品質の低いコードを生み出しているのです。

具体を抽象化して人間にわかるようにする

それでは人間は具体に太刀打ちできないのでしょうか。品質の低いコードが生まれることからは逃れられないのでしょうか。

そう悲観することでもありません。人間には抽象化という武器があります。抽象化というのは、一連の具体をまとめあげ、わかりやすい端的な名前をつけて、具体を抽象に変化させる行為のことです。例えば「コップを取って、冷蔵庫を開けて、コーラを取って、コーラの蓋を開けて、コーラをコップに入れて、飲む」は「コーラを飲む」に抽象化できますし、「スマートフォンを手にとって、スリープを解除して、ゲームのアイコンをタップして、ゲームで遊ぶ」は「ゲームをする」に抽象化することができます。一連の具体的な事象を抽象化することで、人間にわかりやすい表現に変換することができます。

関数で具体的コードをまとめ、名前をつけて、抽象化する

ところで「具体をまとめあげ、わかりやすい名前をつける」という行為に見覚えはないでしょうか?

そうです。関数です。関数には「命令群をまとめて、名前をつける」機能がありました。「命令をまとめて名前をつける」……これは抽象化の作業と全く同じです。そう、関数は抽象化という概念そのものだと言うことができます。

ようやく関数に与えられた使命が明確になってきました。関数は、プログラミングにおいて、人間にわかりにくいコード群をまとめあげ、わかりやすい名前をつけて抽象化し、人間にもわかるコードに変換することが主な役割になるのです。

人間にとってわかりにくい具体コード群を関数で抽象化することで、どんな複雑なコードでも人間に読める形に変換することができます。人間にとって読みやすいコードは、処理の流れも理解しやすく、簡単に変更を加えられるようになったり、バグを発見しやすくなったりします。

実際に関数で抽象化してみる

実際に関数による抽象化を体験してみましょう。以下のコードを見てください:

const matrix = [
  [2, 3, 5],
  [7, 11, 13],
  [17, 19, 23]
];

const rowLength = matrix.length;
const columnLength = matrix[0].length;

let everyValueIsPrime = true;

for(let row = 0; row < rowLength; row++) {
  for(let column = 0; column < columnLength; column++) {
    const value = matrix[row][column];
    if(value < 2) { everyValueIsPrime = false; }
    else if(value > 2 && value % 2 == 0) { everyValueIsPrime = false; }
    else {
      for(let i = 3; i < value; i+=2) {
        if(value % i == 0) { everyValueIsPrime = false; }
      }
    }
  }
}

if(everyValueIsPrime) {
  console.log('全て素数です!');
} else {
  console.log('全てが素数ではありません…');
}

ぎゃー!このコードは直視したくありません!何が書いてあるのかさっぱりです。このコードがダメなコードであることは誰にでもわかります。しかしプログラム的にはこれは正しいコードのはずです。いったいどうしましょう。

問題はこれが非常に具体的なコードであることです。コード全体を眺めてみて、なんとか読めそうな範囲から推察するに、このコードの本質、つまり抽象は以下のようになるはずです:

「行列の全ての要素が素数なら『全て素数です!』と出力し、そうでなければ『全てが素数ではありません…』と出力する」

しかし実際のコードは非常に具体的で、以下のようになっています:

「0からrowLength未満の値の範囲で1ずつ増加していく変数rowに対する0からcolumnLength未満の値の範囲で1ずつ増加していく変数columnに対して、matrix[row][column]の値を変数valueに格納し、valueが2未満なら変数everyValueIsPrimeにfalseを格納し、そうでなくもしvalueが2より大きく2で割った余りが0ならば変数everyValueIsPrimeにfalseを格納し、そのどちらでもなければ3からvalue未満までの値の範囲で2ずつ増加していく変数iに対してvalueをiで割った余りが0ならば変数everyValueIsPrimeにfalseを格納する。everyValueIsPrimeがtrueならば『全て素数です!』と出力し、そうでなければ『全てが素数ではありません…』と出力する」

わけがわかりません。本質(抽象)と実際のコード(具体)とで、言ってることが全く違うように見えます。この、我々が求める抽象と具体的コードの間に存在する非常に大きなギャップが、コードのわかりにくさを作り出しているのです。我々が求めているのは「行列の全ての要素が素数なら『全て素数です!』と出力し、そうでなければ『全てが素数ではありません…』と出力する」であって、「0からrowLength未満の値の範囲で1ずつ増加していく変数rowに対する以下略」ではありません。

この深いギャップを埋めるために、関数を用いて一連の処理を抽象化してみましょう。つまり、処理をまとめて、名前をつけるのです。例えば以下のようになります:

function everyValueIsPrime(matrix) {
  const rowLength = matrix.length;
  const columnLength = matrix[0].length;
  
  for(let row = 0; row < rowLength; row++) {
    for(let column = 0; column < columnLength; column++) {
      const value = matrix[row][column];
      if(value < 2) { return false; }
      else if(value > 2 && value % 2 == 0) { return false; }
      else {
        for(let i = 3; i < value; i+=2) {
          if(value % i == 0) { return false; }
        }
      }
    }
  }
  
  return true;
}

const matrix = [
  [2, 3, 5],
  [7, 11, 13],
  [17, 19, 23]
];

if(everyValueIsPrime(matrix)) {
  console.log('全て素数です!');
} else {
  console.log('全てが素数ではありません…');
}

とりあえず応急処置ということで臭いものに蓋をしただけですが、それなりにまともになりました。ここからメインの処理となる部分だけ抜き出してみましょう。

const matrix = [
  [2, 3, 5],
  [7, 11, 13],
  [17, 19, 23]
];

if(everyValueIsPrime(matrix)) {
  console.log('全て素数です!');
} else {
  console.log('全てが素数ではありません…');
}

実にわかりやすい!「行列の全ての要素が素数なら『全て素数です!』と出力し、そうでなければ『全てが素数ではありません…』と出力する」という抽象的な書き方ができています。このコードは我々の直感と一致している、読みやすいコードです。関数を用いることで、具体的なコードを抽象的なコードに変換して、人間にも読みやすいコードにすることができるのです。

しかし複雑な処理をeveryValueIsPrime関数の中に隔離しただけで、関数の中身はまだ具体的なコードのままですね。ついでにこの関数の中も抽象化してみましょう。関数だけを抜き出すと以下の通りです:

function everyValueIsPrime(matrix) {
  const rowLength = matrix.length;
  const columnLength = matrix[0].length;
  
  for(let row = 0; row < rowLength; row++) {
    for(let column = 0; column < columnLength; column++) {
      const value = matrix[row][column];
      if(value < 2) { return false; }
      else if(value > 2 && value % 2 == 0) { return false; }
      else {
        for(let i = 3; i < value; i+=2) {
          if(value % i == 0) { return false; }
        }
      }
    }
  }
  
  return true;
}

うーん、汚いですね。この関数内部の抽象を考えてみましょう。この関数内部の抽象は「行列の全ての要素を走査し、全てが素数ならtrueを、そうでないならばfalseを返す」でしょう。それに対して現実に我々の目の前に存在する具体は以下のようなものです:

「0からrowLength未満の値の範囲で1ずつ増加していく変数rowに対する0からcolumnLength未満の値の範囲で1ずつ増加していく変数columnに対して、matrix[row][column]の値を変数valueに格納し、valueが2未満ならfalseを返し、そうでなくもしvalueが2より大きく2で割った余りが0ならばfalseを返し、そのどちらでもなければ3からvalue未満までの値の範囲で2ずつ増加していく変数iに対してvalueをiで割った余りが0ならばfalseを返す。最後にtrueを返す」

意味不明ですね。コードを眺めて、この中から頑張って抽象化できそうな部分を探しましょう。どうやら「valueが2未満ならfalseを返し、以下略」は、「valueが素数でなければfalseを返す」に抽象化できそうな気がします。

このためには「与えられた値が素数ならばtrue、そうでなければfalseを返す」関数があればできそうですね。やってみましょう。

function isPrime(value) {
  if(value < 2) { return false; }
  if(value == 2) { return true; }
  if(value % 2 == 0) { return false; }
  
  for(let i = 3; i < value; i+=2) {
    if(value % i == 0) { return false; }
  }
  
  return true;
}

function everyValueIsPrime(matrix) {
  const rowLength = matrix.length;
  const columnLength = matrix[0].length;
  
  for(let row = 0; row < rowLength; row++) {
    for(let column = 0; column < columnLength; column++) {
      const value = matrix[row][column];
      if(!isPrime(value)) { return false; }
    }
  }
  
  return true;
}

const matrix = [
  [2, 3, 5],
  [7, 11, 13],
  [17, 19, 23]
];

if(everyValueIsPrime(matrix)) {
  console.log('全て素数です!');
} else {
  console.log('全てが素数ではありません…');
}

素数判定をisPrime関数に切り離すことで、everyValueIsPrime関数の中が非常にスッキリしました。これぐらいなら頑張れば読めるでしょう。二重forループもなくしたいところですが、少し高度なテクニックが必要になるので、ここでは割愛します。

まだいくつかコードが具体的すぎる部分が残っていますが、これぐらいなら我々人間にも理解ができますし、十分でしょう。関数を活用することで、具体的な読みにくいコードを、抽象的な読みやすいコードに変換することができました。

実装の隠蔽と責任の分離

関数を用いた抽象化には、嬉しい誤算があります。それは実装が隠蔽(いんぺい)されることです。

例えば先ほどの例では、isPrime関数は「与えられた数値が素数ならtrueを、そうでないならfalseを返す」関数です。このとき、isPrime関数が「どうやって素数判定をしているのか」については全く触れられていません。この「具体的にどうやっているのか」のことを実装と言います。関数の実装は実際に関数の中をのぞいてみるまではわからず、関数の利用者側からすると、関数が何を受け取って何を返すかという、抽象的な部分のみが残ります。実装はひた隠しにされているのです。これを実装の隠蔽と言います。

隠蔽というのは悪い響きがありますが、実際にはいいことづくめです。関数は実装が隠蔽されているので、我々は実装を知らなくとも関数を抽象的に使うことができます。「素数ならばtrueを返す」関数は「素数ならばtrueを返す」関数としてのみ扱えます。中身の実装は知らなくてもいいのです。難しいことには一切触れず、抽象的な利益のみを享受することができるのです。

そして実装には大いなる責任が伴います。実装は具体的なコードなのですから、関数が標榜している抽象を実現できているかどうか、責任が発生するのです。しかし実装は隠蔽されているのです。隠蔽されているのだから、我々は何も知りません。知らないのだから責任も発生しないはずです!つまり抽象化された関数を無責任に使うことができます。もしバグがあっても関数が悪いのであって、我々は悪くありません。もし関数の実行速度が遅くても関数が悪いのであって、我々が悪いのではありません。全て関数の責任です。関数が悪いんです。私には関係のないことです。

言い方を変えると、関数の中と外で、責任を分離できたということになります。関数は任された仕事をやり遂げる。我々は関数を抽象的に利用するだけ。責任は全て関数にあります。……大人ってこういうもっともらしい言い回しで責任逃れするから卑怯ですよね。ですが今回は乗っておきましょう。責任逃れ万歳です。

実装を隠蔽し、責任の分離がうまくできていると、コードの見通しが良くなりますし、バグの発見がしやすくなります。例えば先ほどの例で言えば、素数を判定するのはisPrime関数の責任なので、素数判定が間違っている場合はisPrime関数だけを調べればよくなります。その外側を調べる必要は一切ありません。

まとめ

関数はコードをまとめ、名前をつける

関数の主な機能は、コードをまとめ、名前をつけることです。複数の命令群を関数という形にまとめ、新たな名前をつけることで、わかりやすい名前で複雑な機能を呼び出すことができるようになります。

関数は値を受け取ることもできます。外部から値を受け取り、その内容によって処理内容を変えることができます。外部からの値を受け入れることで、より柔軟な関数を作ることができます。

さらに関数は値を返すこともできます。関数が値を返すことで、関数呼び出しを別の値に置き換え、呼び出し元で別の処理に使うことができます。

加えてスコープを作る性質もあります。スコープの内部で宣言された変数は、スコープの外へ出ると寿命を終え、アクセスできなくなります。スコープがあることで、関数の外部を汚染せず、綺麗に保つことができます。

関数でコードを抽象化し、責任を分離する

関数に関して、様々な活用方法を検討しました。

まずは関数の「命令群をまとめる」特性を利用して重複コードの削除に臨みました。関数を利用することで重複コードを削除し、ひとつの関数にまとめることができました。しかし大規模なプログラムならまだしも、学校の演習で扱うような小規模なプログラムでは重複コードとはなかなか出会えません。そして仮に重複コードがあってもうまくコードを書けば関数を使わずとも重複コードを回避することができます。なので重複コードの削除は少しニッチな活用方法だと言えそうです。

次に「名前をつける」特性に着目しました。処理に名前をつけることで、何をしているのかを明確にし、読み手の理解を助け、バグの早期発見に貢献することができました。しかしこれは関数を使わなくともできることです。変数の名前を工夫するとか、適切なコメントを入れるとか、もっと賢い方法が存在します。これもあまり役に立つとは言えなさそうです。

最後に「命令群をまとめ、名前をつける」特性に注目し、「関数を用いたコードの抽象化」について考えました。我々人間は抽象の世界に生きる生き物です。対してコンピュータは具体の世界に生きており、それゆえにプログラムも具体でなければいけません。人間にとって具体は難しすぎて、そのままコードを書くと全く読めないものが誕生します。そこで関数を用いてコードを抽象化する手法を編み出しました。コードを抽象化することによって、人間にもわかりやすい、抽象的でシンプルなコードに変換することができます。加えて関数を使えば実装を隠蔽し、責任を分離することもできました。

関数を使えば、コードを人間にとってわかりやすいように変換し、コード品質を上げることができます。

深く、もっと深く

関数による抽象化は奥が深いものです。この記事では表面上の簡単な概念のみを説明しましたが、実際にはどこを抽象化するか、どのように抽象化するかを決めるのは難しいことです。この話題だけで最低でもこの記事の2倍の長さの記事は書けるはずです。

さらにJavaScriptにおいて関数はファーストクラスオブジェクトです。関数も他のオブジェクトと同じように取り回し、高階関数などを実現することができます。これも面白い話題で、これだけでも記事が書けます。

関数は面白い概念です。深く見ていけば、どこまで深く踏み込むことができます。ですが、今回はここまでです。追い求めると、きりがありませんからね。

それでは、よいJavaScript生活を。