関数型プログラミング
関数型プログラミングは、数学的関数の考え方をプログラミングに適用したプログラミングパラダイムです。関数型プログラミングは、プログラムを関数の集合として考え、関数の合成や関数の適用を用いてプログラムを構成する考え方をとります。
プログラミング・パラダイムの整理
プログラミングのパラダイムは大きく、命令型プログラミングと宣言型プログラミングに大別される。
命令型プログラミング
命令型プログラミングは、プログラムを命令文の集合として考え、命令文の実行順序によってプログラムの処理を実行する考え方をとる。命令型プログラミングには、手続き型プログラミングがある。
手続き型プログラミング
手続き型プログラミングは、プログラムを手続きの集合として考え、手続きの呼び出しによってプログラムの処理を実行する考え方をとる。
COBOL、BASIC、Pascal、Cといった古い言語が該当するが、Goもこれに該当する。
宣言型プログラミング
宣言型プログラミングは、プログラムをデータの集合として考え、データの変換の規則によってプログラムの処理を実行する考え方をとる。宣言型プログラミングには、関数型プログラミングがある。
宣言型プログラミングでは出力を得る方法ではなく、出力の性質・あるべき状態を文字通り宣言することでプログラムを構成する。
代表例は、SQLである。SQLでは、どのようにデータベースにアクセスしてデータを取得してくるか、という手続きではなく、『どんなデータが欲しいか』を宣言することで出力が得られる。
オブジェクト指向プログラミングはどこに分類されるか?
オブジェクト指向プログラミングは、構造化されたデータとおの振る舞いをカプセル化したもの同士を相互に作用させて最終結果を得る。オブジェクト指向と手続き型は矛盾しないが、命令型、宣言型とは独立したパラダイムになる。
オブジェクト指向と手続き型、両方の性質を兼ね備えた言語がもっともメジャーで、Java, C++, C#, Python, Rubyが該当する。
関数型プログラミングの特徴
関数型プログラミングでは、関数の実行結果は、実行時の状態に依存しない。つまり、関数の実行結果は、関数の実行時の引数にのみ依存する。この性質を参照透過性と呼ぶ。
関数型プログラミングでは、副作用を持たない関数を作ることを推奨している。関数型プログラミングでは、関数の実行結果は、関数の実行時の引数にのみ依存するので、副作用を持つ関数を作ると、関数の実行結果が引数に依存しなくなる。
具体例
1から100までの範囲における8の倍数を格納した配列を作る
手続き型で書いた場合
手続き型は過程を記述する。
{
const octuples = [];
for (let n = 1; n <= 100; n++) {
if (n % 8 === 0) {
octuples.push(n);
}
}
console.log(octuples); // [8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96]
}
関数型で書いた場合
関数型は結果を記述する。
{
const octuples = Array(100)
.fill(null)
.map((_, i) => i + 1)
.filter(n => n % 8 === 0);
console.log(octuples); // [8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96]
}
関数型言語としてのJavaScript
コレクションの反復
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(
arr.map((x) => x * 2), // [2, 4, 6, 8, 10, 12, 14, 16, 18]
arr.filter((x) => x % 2 === 0), // [2, 4, 6, 8]
arr.find((x) => x % 2 === 0), // 2
arr.findIndex((x) => x % 2 === 0), // 1
arr.every((x) => x !== 0), // true
arr.some((x) => x >= 10), // false
)
const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(
arr.reduce((n, m) => n + m), // 45
arr.sort((n, m) => n > m ? -1 : 1), // [9, 8, 7, 6, 5, 4, 3, 2, 1]
arr.reverse(), // [1, 2, 3, 4, 5, 6, 7, 8, 9]
)
const arr3 = [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(arr.includes(5)) // true
console.log(arr.includes(10)) // false
オブジェクトの反復
const user = {
id: 3,
name: 'John',
username: 'johnny',
address: 'Tokyo',
};
console.log(
Object.keys(user), // ["id", "name", "username", "address"]
Object.values(user), // [3, "John", "johnny", "Tokyo"]
Object.entries(user), // [["id", 3], ["name", "John"], ["username", "johnny"], ["address", "Tokyo"]]
);
// keyとvalueを反復処理の中で扱う
Object.keys(user).map((key) => {
console.log(key, user[key])
});
Object.entries(user).map(([key, value]) => {
console.log(key, value)
});
高階関数 (Higher-order function)
関数を引数に取る関数、関数を返す関数。
const greeter = (target) => {
return () => {
console.log(`Hello ${target}`)
}
}
const greeter2 = (target) => () => console.log(`Hello ${target}`)
const greet = greeter('John');
greet(); // Hello John
カリー化 (Currying)
{
const multiply = (x, y) => x * y
console.log(multiply(2, 3)) // 6
}
// カリー化された関数
{
const withMultiply = (x) => {
return (y) => {
return x * y
}
}
console.log(withMultiply(2)(3)) // 6
}
{
const withMultiply2 = (x) => (y) => x * y
console.log(withMultiply2(2)(3)) // 6
}
// 部分適応 (Partial Application)
{
const withMultiply = (x) => (y) => x * y
const triple = withMultiply(3)
console.log(triple(3)) // 9
}
クロージャ (Closure)
関数のスコープを保持する。
let COUNT = 0;
const increment = () => {
return COUNT++;
}
// クロージャーを使ったカウンター
const count2 = ( count = 0 ) => (adds = 1) => count += adds;
const increment2 = count2();
console.log(increment2()) // 1
console.log(increment2()) // 2
console.log(increment2()) // 3
参考