関数を作って長いプログラムを整理する

今回は、これからだんだんと長いプログラムを書いていくのに向けて、関数をうまく使ってプログラムを整理整頓する方法を学びます。

目次

戻り値・返り値 (return value) と副作用 (side effect) section5-1

これまでにも何度となく使ってきた関数ですが、その使い方は2種類に分けることができます。戻り値と副作用です。 次に示す例では両方の使い方が登場します。 12個の円を丸く円周上に描くプログラムで、三角関数を利用して円の位置を決めているものです。

function setup(){
  createCanvas(200, 200);
  background(192);
  for(let i = 0; i < 12; i++){
    const theta = TWO_PI * i / 12; // TWO_PI は円周率πの2倍(ほかに PI, HALF_PI, QUARTER_PI がある)
    const x = 100 + cos(theta) * 50; // 関数 cos の戻り値を使用
    const y = 100 + sin(theta) * 50; // 関数 sin の戻り値を使用
    ellipse(x, y, 10); // 関数 ellipse の副作用で円が描画される
  }
}

まずは 6-7 行目に注目してください。 関数 setup() を実行中だったのが、関数 cos() に移り、その実行が終わったら setup() の続きを実行するために戻ってくるのです。 そのとき計算結果の値を持って戻ってきます。持って帰ってきた値のことを 戻り値 あるいは 返り値 と呼びます。

多くの数学関数が同様の使い方になります。他にこれまで出てきた関数の中で戻り値を使っていたものをいくつか挙げます:

7行目の関数 ellipse() の呼び出しでも同じように処理が移動しますが、戻ってきたときに値は持ちません。 行って帰ってくるまでの間に画面に円が描画されるだけです。 このような、計算結果以外に関数が行う処理のことを 副作用 と呼びます。 p5.js で関数の多くはほとんどがこのパターンなのに「副」扱いするのはおかしな感じがするかもしれませんが、あくまで関数の主役は計算というところから来ています。

関数を作る(定義する)

続いて、オリジナルの関数を作る方法を見ていきます。厳密には変数と同様に「関数を定義する」と言います。

(1) オリジナルの描画関数を作る section5-2

まずは、用意されている描画関数を組み合わせて、少し複雑な図形を描画する関数を作ります。 描画する位置や大きさ等を変えられると便利なので引数としてデータを渡せるようにすることにします。 引数のある関数は次のように書きます。

function 名前(引数1, 引数2, ...){
  // 引数を使って、中身を書く
}

自分で定義した関数を使っている例を示します。6-9行目が関数定義で、3行目で呼び出しています。

function setup(){
  createCanvas(200, 200);
  crossmark(50, 50, 150, 150); // 作った関数を呼び出す
}

function crossmark(x1, y1, x2, y2){
  line(x1, y1, x2, y2);
  line(x2, y1, x1, y2);
}
関数の中で関数を定義することもできたりしますが、慣れないうちはスコープなどがややこしくなるので、今日のところは外で(setup や draw などと同列に)定義するものだと思っておきましょう。

このプログラムの実行の様子は以下のようになります。ブラウザのデバッグ機能 を使って動きを観察してみることをおすすめします。

  1. まず function setup() の実行が開始される
  2. 関数呼び出しによって function crossmark の実行が開始される。呼び出し元で与えた値が順番通り x1 = 50, y1 = 50, x2 = 150, y2 = 150 と代入された状態で始まる。
  3. function crossmark の実行が終了すると、呼び出し元の位置に戻り、続きから setup() の実行が行われる。

描画関数を作るコツ1: push / pop

それではさっそくオリジナルの描画関数を作ってみましょう、と行きたいところですが、その前に使いやすい描画関数を作るコツを二つ紹介したいと思います。 一つ目のコツは「関数の中で塗りつぶしや線のスタイル(色や太さなど)を変えたらその関数内で元に戻しておく」ことです。

p5.js にはこれを簡単に行うことができる push(), pop() というセットで使う関数が用意されています。 次の例を見てください。円と斜め線からなる禁止マークを描く関数です。

function ngmark(cx, cy, r){
  push();                    // 今の描画スタイルを覚えておく
  noFill();                  // 塗りつぶしのスタイルを変える
  strokeWeight(r * 0.1);     // 線のスタイルを変える
  const d = sqrt(r * r / 8); // 補足:sqrtは平方根を計算する関数
  ellipse(cx, cy, r);
  line(cx - d, cy - d, cx + d, cy + d);
  pop();                     // 事前に覚えておいたスタイルに戻す
}

関数の始めで push() とすることでその時点でのスタイルをまとめて覚えておき、 最後に pop() とすることで覚えておいたスタイルに戻しています。

「描画関数を呼び出したらなぜか線の色が変わった」としたらそれも副作用になります。 自分で関数を作るときには,、利用者側が想定しない副作用は排除することが大切です。 たとえば上の場合、利用者は「関数呼び出しによって禁止マークが描画されること」は想定しますが、「関数呼び出しによって線の太さが変わること」は想定しないものですので、元に戻しておく方がイメージ通りに動作する使いやすい関数になります。

描画関数を作るコツ2: beginShape / endShape

続いて、四角形や円を組み合わせるだけでは描きにくい複雑な形状を描画するコツです。 p5.js には、点を順番につないでいく子どもの遊び「点つなぎ」のように図形を描画する関数 beginShape()endShape() が用意されています。 2つの関数は次の例のようにセットで使います。星マークを描く関数です。

function star(cx, cy, r){
  beginShape();    // 点つなぎを始める
  for(let i = 0; i < 5; i++){
    const theta = TWO_PI * i * 2 / 5 - HALF_PI;
    const x = cx + cos(theta) * r;
    const y = cy + sin(theta) * r;
    vertex(x, y);  // 次につなぐ点を1つ増やす
  }
  endShape(CLOSE); // 点つなぎを終わる
}

beginShape(); で点つなぎを開始し、必要なだけ vertex(x, y) てつなぐ点を追加し、最後に endShape(CLOSE); で点つなぎを終わります。 この例では最後の点から最初の点をつないで図形を閉じています (CLOSE)。閉じたくない場合は単に endShape(); と書きます。 塗りつぶしを設定すると、線でつないだ図形の中が塗りつぶされます。

よくある星マークは円周上の5つの点をつなぐことで描くことができます。

(2) 結果を戻す関数を作る section5-3

結果を戻す関数を作るには、関数内の処理が終わるところに return 値; と書きます。 次に示す例は、西暦y年がうるう年かどうかを判定し、その結果を true か false で戻す function isLeapYear(y) を作り、それを使用しているところです。

function setup(){
  for(let i = 2000; i <= 2100; i++){
    if(isLeapYear(i)){
      console.log(i + "年はうるう年です");
    }
    else{
      console.log(i + "年はうるう年ではありません");
    }
  }
}

function isLeapYear(y){
  return (y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0);
}

3行目の isLeapYear(i) が実行される様子は以下のようになります。

  1. 変数 i の値を読みだす(たとえば 2000)。
  2. 関数呼び出しによって isLeapYear(2000) の実行が開始される。引数として与えた 2000 が y に代入された状態で始まる。
  3. (y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0) の計算が行われる(結果は true か false)。
  4. 3 の結果が return されて isLeapYear(2000) の実行が終了し、呼び出し元の位置に戻る。

うるう年かを判定する条件の計算部分は3つの条件の組み合わせになっています。 「4で割り切れる年はうるう年」なんですがいくつか例外があります。 丸かっこは見やすくするために書いているもので y % 4 == 0 && y % 100 != 0 || y % 400 == 0 と書いてもOKです。

それでもやっぱり isLeapYear(i)isLeapYear(y) が出てきてわけわからん!という人向けの説明を試みます。

という2つの文章があるようなものです。2つは別の文なので違う文字を使っていても問題はないですよね?

カレンダーつながりであといくつか例を見てみましょう。 まずは y年m月が何日まであるかを返す関数 daysInMonth(y, m) です。 2月はうるう年かどうかで日数が変わるので isLeapYear(y) を使って条件分岐します。

function daysInMonth(y, m){
  if(m == 2){
    return isLeapYear(y) ? 29 : 28; // 「a ? b : c」と書く三項演算子を使っています
  }
  else if(m == 4 || m == 6 || m == 9 || m == 11){
    return 30;
  }
  else{
    return 31;
  }
}

この daysInMonth(y, m) では途中に return が出てきます。return するとたとえ途中であってもその関数の実行は終了して呼び出し元に戻ります。

(補足説明)条件分岐をコンパクトに書くことができる「三項演算子」を使っています。 a ? b : c の結果は、a が true の場合には b、false の場合には c になります。 a, b, c がどれも短いときには if 文を使うよりもわかりやすくなります。

次は y年m月d日が一年のうちで何日目かを返す関数 dayOfYear(y, m, d) です。 今作ったばかりの daysInMonth(y, m) を使い、各月の日数を繰り返しで足し合わせていくことで日数を数えています。

function dayOfYear(y, m, d){
  let count = 0;
  for(let i = 1; i < m; i++){
    count += daysInMonth(y, i); // 「count += n」 は「count を n 増やす」の意
  }
  return count + d;
}

このように、複雑なプログラムを作るときには処理を意味合い毎に関数としてまとめて書くことでプログラムをわかりやすくすることができます。

(3) 配列を受け取る関数を作る section5-4

関数は配列を引数として受け取ることもできます。 たとえば、配列の合計を求める計算を function sum(arr) としてまとめると以下のようになります。

function setup(){
  let scores = [88, 80, 76];
  let s = sum(scores);
  console.log(s);
}

function sum(arr){ // 配列を引数として受け取って、
  let n = 0;
  for(let i = 0; i < arr.length; i++){ // その合計を計算して
    n += arr[i];
  }
  return n; // 結果を戻す
}

合計の計算と同様に、平均・最大値・最小値なども関数にまとめることができて、プログラムをすっきりさせることができます。 プログラムを機能ごとに関数に分けていくと1つ1つの関数はシンプルになって理解しやすくなります。 ひとつの関数を概ね20行以内くらいに、どんなに長くなっても一画面以内に収めるように分けていくとよいでしょう。

これまでのプログラムで「ここは関数にまとめるとすっきりしそう」というところを見つけて、すっきりさせる練習をしてみてください。