どうも。つじけ(tsujikenzo)です。このシリーズでは2022年2月からスタートしました「ノンプロ研オブジェクト指向でなぜつくるのか第3版輪読会」についてお送りします。飛び入り参加しました、補章の資料で、今日は第2回目です。
前回のおさらい
前回は、「関数型言語とラムダ式」をお送りしました。
今回は、「パターンマッチングと再帰」をお届けします。
今日のアジェンダ
- パターンマッチとは
- 再帰関数
- メモリのスタック領域
- 関数型プログラミングによるリファクタリング
パターンマッチとは
関数型言語における「パターンマッチ」とは、「データ型や代数的データ型が一致しているかどうか」というロジックをもちいた、関数型プログラミングの手法です。
パターンマッチを完全に理解するためには、関数型言語にしか実装されていない「代数的データ型」を理解する必要があります。
しかしながら、今回は「代数的データ型」は取り扱いません。
データ型によるパターンマッチは、「引数を判定することで、実行する処理を分岐させる」というテクニックです。
function sample2_1() {
const c = '\t';
console.log(convertTab(c)); //" "
const d = 's';
console.log(convertTab(d)); //s
}
function convertTab(c) {
if (c === '\t') {
return" ";
} else {
returnString(c);
}
}
引数の判定は、プリミティブ値だけでなく、データ型や、ユーザー定義型の判定も可能です。
動的型付け言語のJavaScriptでは、ダック・タイピングという、ユーザー型を判定する手法もありますので、いつかご紹介したいと思います。
また、引数の判定を、オブジェクトのプロパティで行えば、このようなメソッドの呼び出しが可能です。
function sample2_2() {
const getIncludeTax = {
normal(price) { return price * 1.1; },
keigen(price) { return price * 1.08; },
uchizei(price) { return price * 1; }
};
const calculateTax = (type, price) => getIncludeTax[type](price);
console.log(calculateTax('normal', 1000)); // 1100
console.log(calculateTax('keigen', 1000)); // 1080
console.log(calculateTax('uchizei', 1000)); // 1000
}
これは、インターフェイス設計にも通ずるものがあり、動的型付け言語の、設計の武器になるのではないかと夢見ています。
JavaScriptでは、このようなStrategyパターンが想定されます。
function sample2_3() {
/*Context 具体的な戦略を渡されて、参照を保持するコンテキストクラス*/
class Button {
constructor(submitFunc) {
this.onSubmit = submitFunc;
}
}
/*Strategy 戦略クラス*/
const sum = (array) => array.reduce((a, c) => a + c, 0);
const doubles = (array) => array.map(element => element * 2);
/*Client 異なる戦略をもつ2つのインスタンスを生成する */
const button1 = new Button(sum);
const button2 = new Button(doubles);
//実行用処理
const numbers = [1, 2, 3, 4, 5];
console.log(button1.onSubmit(numbers)); //15
console.log(button2.onSubmit(numbers)); //[ 2, 4, 6, 8, 10 ]
}
再帰関数
まず、こちらの「与えられた引数の階乗を求める関数定義」と、「関数呼び出し」を見てください。
階乗を求める関数定義内で、for文を使っています。
/** main */
function sample2_4() {
console.log(factorial_(4)); //24
}
/**
* 階乗を求める関数
* @param {number} n
*/
function factorial_(n) {
let result = 1;
for (let i = n; i > 0; i--) {
result *= i
}
return result;
}
しかし、前回お伝えした「文(statement)は変数に代入できない」という性質から、関数型プログラミングでは、for文やwhile文などの繰り返し構文は使いません。
その代わり、関数の中で自身を呼び出す 「再帰関数」 というテクニックを使います。
/** main */
function sample2_5() {
console.log(factorialRecursion_(4)); //24
}
/**
* 階乗を求める関数
* @param {number} n
*/
function factorialRecursion_(n) {
if (n !== 0) {
return n * factorialRecursion_(n - 1);
}
return1;
}
メモリのスタック領域
自分の中で自分を呼ぶ?ん-再帰関数は苦手だなぁ。と思っているかたも多いと思います。
そんなときは、メモリを理解すると楽ちんです。
復習ですが、HDDに記録されているソースコードは、実行するときに、メモリにコピーを作成しながら動いています。
メソッドが呼ばれて、実行されるときも、メモリにコピーを作成して動いています。
メソッドは、メモリの中のスタック領域という場所に、スタックフレームとして格納されます。
そして、メソッドを実行中に、新たなメソッドを呼び出すと、スタックフレームはどんどん上に積まれます。
メソッドの処理が最後まで終われば、スタックフレームから取り出されます。(メモリの開放)
つまり、スタック領域は、メソッドの処理順を制御するためのメモリ領域と言えるでしょう。
再帰関数もおなじです。関数が呼ばれたら、まずコピーをメモリ領域に格納します。
関数の処理が終わるまえに、また新しい関数を呼び出すわけですから、コピーが上にどんどん積まれていきます。
再帰関数は無限ループなので、スタックフレームは、限界(スタックオーバーフローエラーが発生)が来るまで上に積まれます。
無限ループを起こさないように、パターンマッチをしっかり行いましょう。
関数型プログラミングによるリファクタリング
三項演算子は、関数型プログラミングと相性がいいです。
こちらのブログでは、「リーダブルコードではご法度とされているが、三項演算子を使おう」という提案がされています。
個人的には、まさに関数型プログラミングと相性がいいのでは?と思いました。
リファクタリングの参考になればと思います。
/** main */
function sumArray() {
/**
* 配列の和を求める関数
* @param{Array} arr -配列
*/
constsum = (arr) => {
let result = 0;
for (let i = 0; i < arr.length; i++) {
result += arr[i];
}
return result
}
console.log(sum([1, 2, 3]));
}
/** main */
function recursionSumArray() {
/**
* 配列の和を求める関数
* @param{Array} arr -配列
*/
const sum = ([x, ...rest]) => (x === undefined ? 0 : x + sum(rest));
console.log(sum([1, 2, 3]));
}
まとめ
以上で、「パターンマッチングと再帰」をお送りしました。
関数型プログラミングは、コード量を少なくし、安全に動くコードをつくるのが目的です。
時代の流れとしては、オブジェクト指向から関数型プログラミングへ移行している雰囲気がありますが、どのような設計手法がいいかは、ときとばあいによります。
「オブジェクト指向はもう古い」という論調を、鵜呑みにすることだけは避けたいですね。
次回は、「型推論」 をお届けします。