Subterranean Flower

【連載記事】JavaScriptでプログラミングを学ぶ その2:関数

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

前回の記事では、JavaScriptにおける基本的な要素について学びました。今回は前回学んだ知識を活かして、より高度なプログラムに挑戦してみましょう。

連載目次

  1. 基礎と文法
  2. 関数(この記事)
  3. 配列とオブジェクト
  4. オブジェクト指向
  5. データ構造とアルゴリズム
  6. HTMLとDOM(予定)
  7. 未定

今回学ぶ内容

今回学ぶのは関数です。関数という言葉は数学で何度も聞きましたね。そう、あれです。あの関数です。

関数は、if文やfor文と違って、学んだからといって特別に新しいことができるようになるわけではありません。どちらかというとコードを整理するための道具のようなもので、処理の内容はそのままに綺麗なコードを実現するために使います。

数学において関数は憎むべき敵でしたが(二次関数の試験は思い出したくもない!)、プログラミングにおいて関数は強い味方になります。ぜひ習得しましょう。

始める前に

index.htmlが前回の課題をやったときのままになっていませんか?index.htmlを元に戻しておきましょう。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>JavaScriptでプログラミングを学ぶ</title>
    <script src="main.js" defer></script>
  </head>

  <body>
  </body>
</html>

6行目のファイル名をmain.jsに戻しておきます。

関数と関数定義

数学において、関数は「ある値を別の値に変換する操作」と捉えることができます。それはプログラミングにおいてもほぼ同じです。

例えばf(x) = x + 1という数学的な一次関数を考えてみましょう。この関数f(x)はx座標を受け取り、直線上のy座標に変換する操作と言えます。実際にxに適当な値を入れてみると、f(1)は2ですし、f(2.5)は3.5になります。こんな感じで、ある値を他の値に変換するのが関数です。

このf(x)をJavaScriptで定義してみましょう。関数の定義にはfunctionキーワードを用います。main.jsを編集してみましょう:

'use strict';

// 関数fを定義する。
// 関数fはxを受け取り
// x + 1を返す
function f(x) {
  return x + 1;
}

// f(0)を計算する
const y = f(0);
console.log(y);

「1」が表示されたと思います。これは関数f(x)に0を渡して、x + 1、つまり0 + 1が計算されたためです。

関数の定義は「function 関数名(受け取る値につける名前)」で行います。今回は「f」という関数名で、「x」という名前で値を受け取る関数を定義しました。関数名のルールは、変数名と同じです。つまりアルファベットか一部の記号で始まり、数字は途中に含むことができます。

定義した関数は「関数名(渡す値)」の書式で呼び出すことができます。これを関数呼び出しと言います。今回の場合は11行目の「f(0)」が関数呼び出しです。

関数は値をreturn(返す)して、関数呼び出しはreturnしたその値へと評価(計算)されます。つまりこの場合「f(0)」は「1」として評価されますし、「f(1.5)」は「2.5」として評価されます。「const y = f(0);」は「const y = 1;」と評価されます。

このようにしてある値を他の値へと変換する操作を、functionキーワードを用いて定義することができます。

仕組みと用語

関数定義は、このようなものでした:

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

ここで、関数が受け取る値のことを引数(ひきすう)と言います。今回は引数は1個で、xという名前です。

returnによって値が返されると、関数呼び出しはreturnされた値へと評価されます。このreturnされた値のことを戻り値返り値などと呼びます。

また、関数定義はあくまで定義であるため、定義の段階では実際には実行されません。関数が実行されるのは、実際に関数を呼び出したときです。

値を受け取る関数

関数はなにも数値だけを受け取る存在ではありません。関数は任意の型の値を受け取ることができます。つまり文字列や真偽値も使えるということです。戻り値にも制限はありません。引数と戻り値の型が違っても大丈夫です。

JavaScriptは型に寛容な言語なので、文字列や真偽値を受け取るからといって、特別な宣言は必要ありません。ただ、何を受け取るのかわかりやすい引数名をつけておいたほうがいいでしょう。

例えば以下は文字列を受け取って文字列を返す関数です:

'use strict';

// 文字列を受けとる関数
// 挨拶文字列を生成して返す
function generateGreetings(name) {
  return `私の名前は${name}です。`;
}

// 関数を実行する
const myName = '古都こと';
const greetings = generateGreetings(myName);
console.log(greetings);

数値を受け取って真偽値を返すような関数も定義できます:

'use strict';

// 数値を受け取って真偽値を返す関数
// 偶数ならtrue、奇数ならfalseを返す
function isEven(n) {
  return n % 2 === 0;
}

const num = 12;

if(isEven(num)) {
  console.log(`${num}は偶数です!`);
} else {
  console.log(`${num}は奇数です!`);
}

複数の値を受け取る関数

引数は複数定義することが可能です。複数の引数を定義するときは、コンマで区切ります:

'use strict';

// 引数a,bを受け取り、
// 足して返す
function add(a, b) {
  return a + b;
}

const sum = add(2, 5);
console.log(sum); // 7

複数の引数をとる関数は、呼び出すときも複数の値を渡します。これも同じくコンマで区切ります。

なお、JavaScriptにおいては返り値は複数にすることはできず、必ずひとつです。

いろいろな関数

プログラミングにおける関数は、なかなかに柔軟です。入力が存在しない関数や、何も値を返さない関数を作ることもできます。

入力が存在しない関数は、引数がゼロ個の関数です。たとえば常に10を返す関数を作ることもできます:

'use strict';

// 関数を定義する
function ten() {
  return 10;
}

// 関数を実行する
const value = ten();
console.log(value);

ここで、引数はゼロ個ですが、関数を呼び出す際には後ろに括弧をつけます。「ten」ではなく「ten()」である必要があります。これは括弧が「関数の呼び出し」を意味しているためで、括弧がないと別の意味になります(後述します)。

また、何も値を返さない関数は、何もreturnしません。例えば値を返さずコンソールに表示するだけの関数は以下のようになります:

'use strict';

// 関数を定義する
function plusOne(x) {
  console.log(`${x} + 1は${x + 1}だ!!`)
}

// 関数を実行する
plusOne(2);

returnがない関数も一応は最終的に値として評価されるのですが、undefined(未定義)として評価されます。なので実行結果を変数に代入する意味はありません:

'use strict';

// 関数を定義する
function plusOne(x) {
  console.log(`${x} + 1は${x + 1}だ!!`)
}

// 関数を実行する
const value = plusOne(2);
console.log(value); // undefined

このとき変数valueの値はundefinedとなります。

値を返さない場合でも、returnを書くことはできます。返さないのに返す(returnする)というのもおかしな話ですが。その場合は返す値を書かず、returnだけを書きます:

'use strict';

function hello(n) {
  console.log('hello');
  return;
}

hello();

複雑な処理をする関数

関数はすぐにreturnする必要はありません。複数行に渡る複雑な処理を行うこともできます。例えば以下は1からendまでの自然数を順番に足していく関数sumを定義しています:

'use strict';

function sum(end) {
  let result = 0;

  for(let i = 1; i <= end; i++) {
    result += i;
  }

  return result;
}

console.log(sum(10)); // 55

この例では複雑な計算処理を行ってから、その結果をreturnしています。

途中でreturnする

returnは関数の途中ですることも可能です。その場合、returnが呼び出された時点で関数の実行は終了します。これは最後まで実行しなくても途中で結果がわかる場合に有効です。例えば以下の例は素数かどうかを判定する関数です:

'use strict';

// 素数かどうかを判定する関数
// ただしn > 2とする
function isPrime(n) {
  // 偶数ならすぐにfalseで終了
  if(n % 2 === 0) {
    return false;
  }

  // 1個でも割り切れる数があったらfalseで終了
  for(let i = 3; i < n; i += 2) {
    if(n % i === 0) {
      return false;
    }
  }

  // 最後までなにもなかったらtrueで終了
  return true;
}

console.log(isPrime(3331)); // true
console.log(isPrime(4444)); // false

素数判定は、偶数ならば即終了しても大丈夫です(2で割り切れるから)。また、奇数であっても、途中で割り切れる数が出てきたら、その時点でreturn falseして大丈夫です。

関数のスコープ

関数内で定義された変数、および引数のスコープ(有効範囲)は関数内になります。つまり、引数も変数も、関数を抜けた時点で有効範囲が終わります。

'use strict';

function func() {
  const n = 10;
  console.log(n);
}

func();
console.log(n); // エラー!

関数ごとにスコープがあるということは、関数同士で引数や変数の名前が被っても問題ないということです。以下のコードはエラーになりません:

'use strict';

function funcA(x) {
  const result = x + 1;
  return result;
}

function funcB(x) {
  const result = x - 1;
  return result;
}

console.log(funcA(5));
console.log(funcB(5));

よって他の関数のことを気にせずにコードを書くことができます。

また、逆に関数の中から関数の外の変数にはアクセスできます:

'use strict';

const n = 10;

function func() {
  console.log(n);
}

func(); // 10と表示される

これはif文などと同じ挙動です。

定義の巻き上げ

プログラムは上から下に順番に実行される、それが原則でした。しかし関数定義はこの原則を破ります。関数の定義は、スコープの先頭まで自動的に巻き上げられます。つまり以下のコードは有効です:

'use strict';

// 定義前に関数を呼び出す
// これでも正常に動作する!
const y = f(0);
console.log(y);

// 後ろで定義する
function f(x) {
  return x + 1;
}

今までの常識からは考えられない挙動です。ですが事実として関数定義は巻き上げられるのです。これは覚えておくとよいでしょう。

値渡し

関数に変数を渡すとき、実際に渡すのは変数そのものではなく、変数に紐づけられた値を渡しています。これを値渡しと言います。なので、関数の中で引数をいじり回しても、元の変数には影響を与えません。以下の例をみてください:

'use strict';

function plusOne(x) {
  x += 1;
  return x;
}

const n = 10;
const nPlusOne = plusOne(n);
console.log(n); // 10
console.log(nPlusOne); // 11

plusOne関数の中で、受け取ったxに1を加算して代入しています。これでxの値は書き換わってしまいます。しかしplusOne関数に渡したnの値を、plusOne関数を呼び出し後に確認しても、変化していません。これは変数n自体を渡したのではなく、nの値、つまり「10」を渡しただけだからです。

console.log関数

ところでそろそろ気づいた方もいるかもしれませんが、名前の後ろに括弧がついているものはif文やfor文などを除き、だいたいは関数です。つまり今まで使っていたconsole.logは関数ということになります。

console.log関数は実は複数の引数を受け取る関数になっていて、好きなだけ値を渡すことができます:

'use strict';

console.log(1, 2, 3, 4);

この例では、「1」「2」「3」「4」が全て出力されます。

例題

例題:数値で西暦を受け取り、平成の数値に変換して返す関数toHeiseiを定義せよ。また、その関数を使って実際にいくつか変換してみよ。ただしここで西暦は1989以上2018以下とする。

平成は1989年に始まります。なので西暦から1989を引いて1を足してやれば平成何年かが出てきます。あとはこれを関数として定義するだけです。だいたい以下のようになります:

'use strict';

// 西暦を受け取る関数
// 平成の年に直して返す
function toHeisei(year) {
  const heiseiStart = 1989;
  const heiseiYear = year - heiseiStart + 1;

  return heiseiYear;
}

// 実際に使用してみる
console.log(toHeisei(1989)); // 1
console.log(toHeisei(2017)); // 29

関数を定義したら、あとは定義したtoHeisei関数を使って何個か変換してみると、動作が正しいかがわかります。

関数の活用

関数の機能的な意味はわかりました。値を受け取って、値を返す。それが関数です。ですが、冒頭でも述べた通り、関数によって何か新しいことができるようになるわけではありません。関数を導入しても処理の流れは変わらず、プログラムの実行結果は同じです。では、それがいったいなんの役に立つのでしょう。

関数の主な役割は「複数の処理を、ひとつの名前にまとめる」ことです。普通に書いていると、複雑な処理のコードは、当然複雑になります。その複雑な処理をひとまとめにして、ひとつの名前をつけてしまえるのが関数です。

例えば最大公約数を求めるプログラムを作るとします。これはユークリッドの互除法という手法を使うことで求められます。普通に書くと以下のようになります:

'use strict';

let a = 27;
let b = 18;

// ユークリッドの互除法
// ここに関しては理解する必要はない
let r = a % b;
while(r !== 0) {
  a = b;
  b = r;
  r = a % b;
}

console.log(b);

複雑なコードが入り乱れて、いったい何が本質なのか見失ってしまいます。このとき関数を使って処理をまとめて名前をつけてやれば、何をやっているのかわかりやすくなります:

'use strict';

// ユークリッドの互除法
function gcd(a, b) {
  let r = a % b;
  while(r !== 0) {
    a = b;
    b = r;
    r = a % b;
  }

  return b;
}

// 最大公約数(GCD)を計算する
console.log(gcd(27, 18));

複雑な処理をgcdという関数に隔離してしまうことで、本来のコードの流れが清潔に保たれています。処理に名前がついたので、何をする処理なのかもわかりやすくなります。

また、関数はスコープを作るので、関数の中で定義された変数が外に漏れ出さないという点も重要です。関数を使わずにコードを書いていると、どんどん変数が増えていって、変数の管理が大変になります。ですが関数内で変数を定義すれば、関数の外から変数に触れることができないので、関数の外は綺麗に保たれますし、間違って妙な操作をしてバグになる可能性が格段に減ります。

読みやすい・わかりやすいコードというのは、それだけで価値があります。読みやすければ、ささいなミスによるバグを早期に発見できる可能性が高くなります。流れがわかりやすければ、機能を拡張するときにもどこをいじればいいのか困りません。関数を活用することで、コードの品質を高く保つことができます。

関数の役割について、より深く知りたいならば、「JavaScriptの関数で何ができるのか、もう一度考える」を読んでみてください。

関数の粒度

関数で処理を切り出して名前をつけてやると便利なのはわかりました。でも、どの程度の大きさで切り出せばいいのでしょうか。

この、切り出す大きさのことを粒度(りゅうど)と言います。結論から言いますと、粒度に明確な答えはありません。短い関数のほうがいい場合もありますし、長い関数のほうがいい場合もあります。こればっかりは経験を積んで「勘」を鍛えていくしかありません。

しかし確実に言えることがあります。あまりに長すぎる関数は悪であるということです。ここで具体的に例を挙げることはできませんが、例えば1万行の関数があったとしたら、それはより細かい関数に分けるのが適切でしょう。

短すぎる関数については難しいところです。その処理に名前をつけることでより処理が追いやすくなるのであれば、1行の関数でも良いでしょう。例えば以下のような関数があるとします:

'use strict';

function degToRad(deg) {
  // Math.PIは定数
  return deg * Math.PI / 180;
}

console.log(degToRad(360));

これは度をラジアンに変換する関数です。関数にせず計算をベタ書きしても、賢い人にはわかるでしょう。ですが、名前がついていた方がわかりやすいという見方もあります。これを関数にするのが正しいのか正しくないのかは、コードを管理する人のレベルにもよるでしょうし、一概にどうこうは言えません。

関数が少なすぎると各関数が肥大化していき、関数が多すぎると何をやっているのか逆にわかりにくくなってしまいます。このあたりは自分でいろいろ考えたり、先人のコードを参考にしたり、うまく適切な粒度を見つけていきましょう。

関数の応用的な使い方

ここで紹介するのは、関数の応用的な使い方です。全てを理解する必要はないので、「こんな機能があるんだなあ」ぐらいの気持ちで眺めておいてください。

デフォルト引数

関数呼び出しの際、一部の引数を省略可能にしたい場合があります。そういったときには引数にイコールをつけて値を書けば、引数を省略した時のデフォルトの値を指定することができます。以下が例です:

'use strict';

// aとbの和を表示する関数
// withTextにtrueを入れるとテキストと一緒に表示する
// withTextを省略するとfalse扱いになる
function printSum(a, b, withText = false) {
  const sum = a + b;

  if(withText) {
    console.log(`${a}と${b}の和は${sum}です`);
  } else {
    console.log(sum);
  }
}

printSum(4, 7);
printSum(4, 7, true);

この例では、printSum関数の3つめの引数、withTextを省略可能にしています。デフォルト値はfalseとしています。呼び出し時にprintSum(4, 7)というふうに、3つめの引数を書かずに呼び出すことができます。このとき、withTextにはfalseが入ります。

呼び出し時に省略せずに明示すれば、もちろんそれが優先されます。printSum(4, 7, true)と書けば、withTextはtrueとなります。

再帰関数

関数は自分自身を呼び出すこともできます。この関数のことを再帰関数と言います。再帰関数は頻繁に用いるものではありませんが、全く使わないというわけでもないので、覚えておくといいでしょう。

例えば階乗を求める関数factを考えましょう。これは以下のようにできます:

'use strict';

function fact(n) {
  if(n === 1) { return 1; }

  return n * fact(n-1); // 自分自身を呼び出す
}

console.log(fact(5));

関数factの中で関数factを呼び出しています。n === 1のときに止まって1を返すので、例えばfact(3)のとき、fact(3) → 3 * fact(2) → 3 * 2 * fact(1) → 3 * 2 * 1 → 6、という流れで計算が進みます。

再帰関数は難しいので、内容を完全に理解する必要はないと思います。ただ、ある程度使う場面が存在するので、その存在だけは知っておいてください。

値としての関数

多くのプログラミング言語において、関数は値(データ)ではありません。それが直感的には正しいように思えます。しかしJavaScriptにおいては、関数は値です。つまり、数値や文字列と同じように関数を扱うことができるということです。これを使えば色々と面白いことができます。ちなみに関数の型は7つある型のうちの「オブジェクト型」です。

例えば関数を変数に代入することができます:

'use strict';

function func() {
  console.log('Hello.');
}

const myFunc = func; // 関数funcをmyFuncに代入する
myFunc(); // 代入した関数を実行する

関数を値として扱うときは、後ろに括弧をつけません。関数名だけを書きます。後ろに括弧をつけると関数呼び出しとなり、関数を実行してreturnされた値として評価されますが、括弧をつけないでいると関数そのものとして扱うことができます。

関数が値である性質を使えば、ちょっと変わった関数定義もできます。以下の例を見てください:

'use strict';

// 変数funcに無名関数を代入する
const func = function () {
  console.log('Hello.')
}

// 変数funcに代入された関数を実行する
func();

この例では変数funcに名前のない関数(無名関数)を代入しています。そして変数funcに代入した無名関数を実行しています。少し遠回りというか、曲芸的な関数定義ですが、こういうこともできるということです。ただしこの方法を使って関数を定義すると、巻き上げが行われません

また、簡易的な無名関数定義の方法として、アロー関数というものがあります。アロー関数は以下のように使います:

'use strict';

// アロー関数を使った関数定義
// const add = function () { return a + b; }
// と同じ
const add = (a, b) => a + b;

console.log(add(3, 5));

(a, b) => a + b;という部分がアロー関数です。アロー関数では{}やreturnを省略できます。この定義方法も同じく巻き上げは行われません。

関数が複数行に渡る場合、アロー関数でも{}を使います。このときはreturnを明示的に書かないといけません(値を返さない関数なら不要です):

'use strict';

const calc = () => {
  const x = 10;
  const y = 20;
  return x + y;
}

console.log(calc());

アロー関数には他の役割もあるのですが、それはまたの機会に紹介します。

高階関数

関数は値です。関数は値をとることができます。つまり、関数をとる関数を作ることができます。また、関数を返す関数を作ることもできます。これらの関数のことを高階関数と言います。

高階関数は普通の関数と同じように定義することができます:

'use strict';

// 渡された関数を二度実行する関数
function twice(fn) {
  fn(); // 関数を実行する
  fn(); // もう一度
}

const myFunc = () => console.log('ヤッホー!');
twice(myFunc);

ここで関数twiceは関数を受け取り、2回実行します。

クロージャ

関数はif文などと同じように自分の外にある変数にアクセスすることができます。しかしif文などと違い関数は「値」です。関数の実行場所がどこになるのかは、プログラマにしかわかりません。

しかし実は関数がアクセスできる変数というのは、定義場所だけで決まります。どこで実行するかは関係ありません。例えば以下のコードを見てください:

'use strict';

// 関数を返す関数
function func() {
  const inner = '関数の内側!';

  return function () { console.log(inner); }
}

// 関数から関数を受け取る
const innerFunc = func();

// 受け取った関数を実行する
innerFunc();

ここで変数innerは関数func内でのみ有効です。関数funcの外からアクセスしようとしても、できません。次に関数func内で定義されている無名関数を見てみます。この無名関数は変数innerを表示しようとしています。関数の内側から表示しようとしているので、これは問題ないでしょう。

しかしこの無名関数はどこで実行されるのかがわかりません。実際、この無名関数をinnerFuncという変数に代入し、関数funcの外側から実行しようとしています。普通に考えると、これはエラーになりそうな気がします。関数funcの外側から変数innerを表示しようとしているのですから。

ですがこれはエラーになりません。普通に変数innerの値が表示されます。これは無名関数の定義場所からは変数innerにアクセスできるので、実行場所に関係なくアクセスできる、という性質があるからです。

このような性質を持つ関数をクロージャと言います。JavaScriptにおいては、関数はすべてクロージャです。つまり関数は定義された場所だけでアクセスできる変数が全く決まってしまいます。

今回のまとめ

今回学んだのは関数についてです。関数はいくつかの値を受け取り、値を返すものです(値を返さない関数もあります)。関数を使うことでコードの流れを綺麗に整理し、人間にとって読みやすいコードを書くことができました。

関数はスコープを作るという点も重要です。関数内部で定義された変数は、関数の外側からは見えません。これによって、プログラムの肥大化に伴う「変数がどんどん散らかっていく現象」を抑制することができます。

まだ小さなプログラムしか取り扱っていないので、関数のメリットは正直なかなか見えてこないと思います。しかし規模が大きくなるにつれ、関数の恩恵も大きくなっていきます。まだ小規模な今のうちに、ぜひ関数の使い方を習得しておきましょう。

例題

例題1

例題:2つの数を受け取り、その差を返す関数subを定義せよ。また、いくつかの数値で実際に関数を使用し、結果を確かめよ。

これは関数subを定義して、その中で引き算をして返せば良いでしょう。関数定義は最も基本的なfunctionキーワードをつかった方法でやります:

'use strict';

function sub(a, b) {
  return a - b;
}

console.log(sub(5, 3)); // 2
console.log(sub(2, 8)); // -6
console.log(sub(15, 4)); // 11

実際にいくつかの数の組み合わせで、ちゃんと計算できているか試してください。

例題2

例題:整数値nを受け取り、0からnまでの整数をコンソールに出力する関数printNumberを定義せよ。また、実際に関数を使用して結果が正しいことを確かめよ。

今度は値を返さず関数内で処理してしまうタイプの関数です。数値nを受け取り、あとはfor文あるいはwhile文でループさせればよいでしょう:

'use strict';

function printNumber(n) {
  for(let i = 0; i <= n; i++) {
    console.log(i);
  }
}

printNumber(10);

これで0からnまでの値が表示されます。

課題

課題1(homework2-1.js)

課題:日数dを受け取り秒数に換算した値を返す関数dayToSecを定義せよ。また、いくつかの値で計算結果が正しいことを確かめよ。

実行例:

  • dayToSec(1) → 86400
  • dayToSec(3) → 259200
  • dayToSec(5) → 432000

課題2(homework2-2.js)

課題:整数limit未満の素数を列挙する関数listPrimes(limit)を定義せよ。また、実際に関数を利用して出力が正しいことを確かめよ。

  • ヒント:関数listPrimesの中で全て処理するのではなく、素数判定をする関数isPrime(n)を定義し、listPrimesの中からisPrimeを利用すると良い。
'use strict';

// 素数判定をする関数
// 素数ならばtrue、そうでないならfalseを返す
function isPrime(n) {
  // nが2未満のときは素数ではない
  if(n < 2) { return false; }

  // 2は素数
  if(n === 2) { return true; }

  // 偶数は素数ではない
  if(n % 2 === 0) { return false; }

  // このあたりに素数判定を書く
}

// 素数を列挙する関数
function listPrimes(limit) {
  for(let i = 2; i < limit; i++) {
    // このあたりに表示処理を書く
    // iが素数ならばconsole.logで表示する
  }
}

// 実際に実行する
listPrimes(50);

応用課題

応用課題は解く必要はありません。腕に自信のある人は挑戦してみてください。

応用課題1(homework2-3.js)

課題:サンプル数samplesを受け取り、モンテカルロ法を用いて円周率を推定する関数calcPi(samples)を定義せよ。また、実際に関数を実行して、samplesが十分大きなときに3.14に近い値が得られることを確認せよ。

モンテカルロ法とは、ランダムに値を選択し、その結果を見て確率的に問題を解く方法である。具体的には、円周率を計算するのは以下の方法となる:

  1. 半径rの円に外接する正方形(1辺あたり2r)の内部にランダムに点を打つ
  2. ランダムに打たれた点の中で、円の内部に打たれた個数を計測する
  3. 円の中に打たれた点の数と全ての点の数(samples)の比は、円の面積と正方形の面積の比と一致するはずである
  4. 面積比「πr^2:4r^2」と点の比「内部点数:samples」から、πを計算する

ここで面積比=点の比となり、つまりπr^2:4r^2 = 内部点数:samplesであるので、これをπについて解くと、

  • π = 4 * 内部点数 / samples

となる。

  • ヒント1:擬似乱数(ランダムな数)はMath.random()関数で得ることができる。Math.random()は実行するたびに0.0以上1.0未満のランダムな数を返す。これらのことから、以下のように関数を定義すると良い:
'use strict';

// サンプル数samplesのとき
// モンテカルロ法を使って円周率を求める
function calcPi(samples) {
  const radius = 1; // 円の半径。単位円でよい。
  const squareLength = 2 * radius; // 正方形の長さ

  let inner = 0; // 内部に打たれた点の数
  for(let i = 0; i < samples; i++) {
    // -raidusからradiusまでの範囲の乱数
    const x = Math.random() * squareLength - radius;
    const y = Math.random() * squareLength - radius;
    
    // ランダムな点が円の内部にあればinnerを増加させる
  }

  // 円周率を計算して返す
}
  • ヒント2:円の内部にあるかどうかは、円の中心点(0, 0)から(x, y)の距離がradius以下であるかどうかで判定することができる。
  • ヒント3:平方根を求めるにはMath.sqrt関数を使うと良い。例:Math.sqrt(2);

応用課題2(homework2-4.js)

課題:関数fと開始地点startおよび終了地点end、分割数nを受け取り、関数fをstartからendをxについて積分して面積を求めて返す関数integral(f, start, end, n)を定義せよ。また、nが十分大きな時にある程度正しい計算結果が得られることを確認せよ。

ここで積分とは、x軸およびfn(x)とx = start、x = endで囲まれた部分の面積を求めることである。

一般に関数は曲線であるため、単純な幅×高さの計算では面積を求めることができない。そこで求める面積の領域をいくつかの長方形に分割して、その面積の和でおおよその面積を求めるという手法がある。これを区分求積法という。

このとき各長方形の幅(width)は、n分割しているのでwidth = (end – start) / nで求めることができる。高さはf(x)で求めることができる。また、i個目(i=0から開始)の長方形の左端のx座標は、x = start + width * iである。

  • ヒント1:関数内でlet area = 0;を宣言しておき、長方形の面積が求まるたびにareaに加算していけば良い。
  • ヒント2:領域をn分割したとき、長方形の数はn個である。つまりn回繰り返せば良い。
  • ヒント3:長方形の面積は、幅 * 高さで求めることができる。
  • ヒント4:面積の値がマイナスになることもあるが、それは正常なので気にしなくて良い。

実行例(n = 1000のとき):

  • integral(function(x){ return 5; }, 3, 6, 1000) → 約15
  • integral(function(x){ return x * x; }, 0, 3, 1000) → 約9
  • integral(function(x){ return x * x + 2 * x – 8; } , -1, 2, 1000) → 約-18

その3:配列とオブジェクトへ続く

次の記事 → その3:配列とオブジェクト

わからないときは

この連載記事を読んで、わからないところ、不思議なところ、納得のいかないところ等出てくると思います。そのときはTwitterで@kfurumiyaまでご連絡ください。できる範囲で回答いたします。

また、Twitterを使えない、Twitterでは短すぎるという場合はメールアドレス:kotofurumiya@gmail.comまでご連絡ください。

私を信用できないという場合は、以下のプログラミング質問サイトを活用してください: