どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。
前回のおさらい
前回は、「関数、メソッド、コンストラクタの、呼び出しの違いを理解する」をお届けしました。
今回は、「高階関数を快適に使えるようにしよう」 をお届けします。
テキスト第3章「関数の扱い」の項目19に対応しています。
今日のアジェンダ
- 関数型プログラミング
- コールバック関数
- データはイミュータブルに
- for文などの繰り返し処理は使わない
- オリジナルコールバック関数
関数型プログラミング
手続き型のプログラミング では、このように順次処理を行っていきます。
function myFunction3_19_01() {
const name = "Tsujike";
const greeting = "Hi, I'm ";
console.log(`${greeting}${name}`); //Hi, I'm Tsujike
}
関数型プログラミング とは、処理を関数化して呼び出す手法です。
function myFunction3_19_02() {
function greet(name) {
const greeting = "Hi, I'm ";
return `${greeting}${name}`;
}
console.log(greet("Tsujike")); //Hi, I'm Tsujike
}
関数型プログラミングには、いくつかのお作法があります。
- 関数は純粋関数である(グローバル領域を参照したり戻り値以外の処理を行わない)
- 高階関数を使う
- 関数内のデータは変更しない(イミュータブル)
- for文などの繰り返し処理は使わない
純粋関数とは、関数内で処理を完結する関数で、引数を入力、戻り値を出力とする純粋な関数です。
このように、グローバル領域を参照する自由変数 があったり、関数内でconsole.log()を動作するものは、純粋であると言えません。
function myFunction3_19_03() {
const name = "Tsujike";
function greet() {
const greeting = "Hi, I'm ";
console.log(`${greeting}${name}`);//Hi, I'm Tsujike
}
greet("Tsujike");
}
項目11のクロージャでもお伝えしましたが、JavaScriptでは、戻り値として関数をもつ関数を書くことができます。
下記のコードでは、常に「”Hi, I’m “」を返すmakeAdjecttifier関数を作成しました。
インスタンスを生成している感覚にもなるかもしれません。
function myFunction3_19_04() {
function makeAdjecttifier(adjective) {
return function (string) {
return `${adjective} ${string} `;
}
}
const greetingifier = makeAdjecttifier("Hi, I'm ");
console.log(greetingifier("Tsujike")); //Hi, I'm Tsujike
}
アロー関数で書くと、もっとすっきりします。
function myFunction3_19_05() {
const makeAdjecttifier = adjective => string => `${adjective} ${string} `;
//【補足】継承ではないのでクラスではない
const greetingifier = makeAdjecttifier("Hi, I'm ");
console.log(greetingifier("Tsujike")); //Hi, I'm Tsujike
}
コールバック関数
JavaScriptでは、戻り値だけでなく、引数に関数を渡すこともできます。
引数に関数を渡すことをコールバック関数、または高階関数と呼びます。
普段は、mapメソッドなどで書いてしまうので、引数に関数を渡す様子をイメージできませんが、このように定義するとわかりやすいかもしれません。
function myFunction3_19_06() {
const array = [1, 2, 3, 4, 5];
const double = array.map(element => element * 2);
console.log(double); //[ 2, 4, 6, 8, 10 ]
const doublerFunc = function (element) { return element * 2 };
const double2 = array.map(doublerFunc);
console.log(double2); //[ 2, 4, 6, 8, 10 ]
}
map, reduce, filterなどのメソッドは、コールバック関数を使った処理で、GAS中級以降大活躍するのでぜひ習得しましょう。
データはイミュータブルに
配列は参照渡しであるため、変更を加えると、もとの配列も変更する破壊的処理 になります。
function myFunction3_19_07() {
const rooms = ['H1', 'H2', 'H3'];
rooms[2] = 'H4';
console.log(rooms); //[ 'H1', 'H2', 'H4' ]
}
関数型プログラミングでは、プリミティブ型ではない配列などの値は、変更ができないようにイミュータブル である必要があります。
これは、オブジェクト指向でも言われていることです。
解決策として、配列の複製を作成して、処理をします。
function myFunction3_19_08() {
//ミューテーションがなくても間違った内容の配列があったとき、これをその場で変更する代わりに、mapを使って新しいrooms配列を作成する
const rooms = ['H1', 'H2', 'H3'];
const newRooms = rooms.map(room => {
if (room === 'H3') return 'H4';
else return room;
});
console.log(rooms); //[ 'H1', 'H2', 'H3' ]
console.log(newRooms); //[ 'H1', 'H2', 'H4' ]
}
配列のコピーがどんどん作られると、ソースコードの管理が大変になります。
その問題をTree構造で解決しようという取り組みが、永続データ構造(Persistent data structure)というプリンシプルです。
数多くのJSライブラリが提供されているようですが、いつか考察してみたいと思います。
for文などの繰り返し処理は使わない
GAS初級講座では、配列の要素を変更するための、for文による反復処理 を習いました。
function myFunction3_19_09() {
const names = ['Tom', 'Bob', 'Ivy'];
const upperNames = [];
for (let i = 0; i < names.length; i++) {
upperNames.push(names[i].toUpperCase());
}
console.log(upperNames); //[ 'TOM', 'BOB', 'IVY' ]
}
コールバック関数を習得すると、for文による処理をあまり書かなくなります。
mapメソッドは、反復処理ではなく、配列のそれぞれの要素を処理する、写像 です。
マッピングという言葉を耳にすると思いますので、反復処理との違いを理解しましょう。
function myFunction3_19_10() {
const names = ['Tom', 'Bob', 'Ivy'];
const upperNames = names.map(name => name.toUpperCase());
console.log(upperNames); //[ 'TOM', 'BOB', 'IVY' ]
}
オリジナルコールバック関数
書籍では最後に、オリジナルなコールバック関数を作ってみよう、という試みが紹介されていました。
このような3つの処理があります。
function myFunction3_19_11() {
const aIndex = 'a'.charCodeAt(0); //コードユニット97
//コードユニット97からはじまるアルファベット26文字
let alphabet = '';
for (let i = 0; i < 26; i++) {
alphabet += String.fromCharCode(aIndex + i);
}
console.log(alphabet); //abcdefghijklmnopqrstuvwxyz
//10桁の数値を返す処理
let digits = '';
for (let i = 0; i < 10; i++) {
digits += i;
}
console.log(digits); //0123456789
//ランダムな文字列を作成する処理
let ramdom = '';
for (let i = 0; i < 8; i++) {
ramdom += String.fromCharCode(Math.floor(Math.random() * 26) + aIndex);
}
console.log(ramdom); //phzvbowg
}
3つに共通するのは、カウント変数によりループすることと、コールバック関数を受け取ることです。
この2つを抽出して、別途、関数化してみましょう。
function myFunction3_19_12() {
const buildString = ((n, callBack) => {
let result = '';
for (let i = 0; i < n; i++) {
result += callBack(i);
}
return result;
});
}
共通する処理を関数化したことで、コードがスッキリしました。
function myFunction3_19_13() {
const buildString = ((n, callBack) => {
let result = '';
for (let i = 0; i < n; i++) {
result += callBack(i);
}
return result;
});
const aIndex = 'a'.charCodeAt(0); //コードユニット97
const getAlphabet = i => String.fromCharCode(aIndex + i);
const alphabet = buildString(26, getAlphabet);
console.log(alphabet); //abcdefghijklmnopqrstuvwxyz
const getDigits = (i) => i;
const digits = buildString(10, getDigits);
console.log(digits); //0123456789
const getRandom = () => String.fromCharCode(Math.floor(Math.random() * 26) + aIndex);
const random = buildString(8, getRandom);
console.log(random); //phzvbowg
}
これで、メンテナンスも楽になります。。。。。
とのことですが、これには少し異論があるかもしれません。
なぜかというと、ちょっと読みづらいからです。。。。。
共通する処理をなんでもかんでも抽象化して、関数として切り分けた方がいいかどうか は、ときとばあいによるかもしれません。
このような、「処理ごとの切り分け」にこだわるより、「業務ごとによる関数化やパーツ化」をした方が、のちのちの変更が楽かも知れませんね。
そのようなプリンシプルを DDD(ドメイン駆動設計) と呼びます。
絶賛、勉強中でございます。
まとめ
以上で、「高階関数を快適に使えるようにしよう」をお届けしました。
メインは、コールバック関数の復習となりましたが、オブジェクト指向の次に注目されている関数型プログラミングの基礎 にも触れてみました。
次回は、「カスタムレシーバ付きでメソッドを呼び出すにはcallを使おう」 をお届けします。
参考資料
このシリーズの目次
- [EffectiveJavaScript輪読会3]関数、メソッド、コンストラクタの、呼び出しの違いを理解する
- [EffectiveJavaScript輪読会3]高階関数を快適に使えるようにしよう
- [EffectiveJavaScript輪読会3]カスタムレシーバ付きでメソッドを呼び出すにはcallを使おう
- [EffectiveJavaScript輪読会3]いくつでも引数をとれる関数を呼び出すにはapplyを使おう
- [EffectiveJavaScript輪読会3]可変長引数関数を作るには、argumentsを使う
- [EffectiveJavaScript輪読会3]argumentsオブジェクトを書き換えない
- [EffectiveJavaScript輪読会3]argumentsへのリファレンスは変数に保存する