関数型プログラミングを深く掘り下げる
Motivation
関数型プログラミングでは登場するカリー化などの関数について理解を深め
表現できるドメインの幅や開発における柔軟性をつけたい。
※本記事ではJavaScriptで実装を進めていきます。
純粋関数
同じ入力に対し、常に同じ出力を返す
また、外の部状態を更新することはなく、副作用(返り値以外の出力)もないのが特徴。
合計を反す純粋関数
function sum(a, b) {
return a + b;
}
不変性
不変性(Immutability)とは、データが一度作成されたら変更されない性質のことです。これにより、状態の予測可能性が高まり、バグの発生を防ぐ。
JavaScriptでは、const
を使って変数を宣言することで不変性を持たせることができますが、オブジェクトや配列の内部は変更可能。
完全な不変性を保つためには、Object.freeze
やライブラリを使用することが推奨されます。
const person = Object.freeze({
name: "Alice",
age: 30
});
// 変更しようとしてもエラーは出ないが、実際には変更されない
person.age = 31;
console.log(person.age); // 30
全域関数
取りうる入力全てに対応して、出力が1つずつ決まるような関数
// 全域関数
// string -> string | null
const toLowerCaseFn = str => str === null ? null : str.toLowerCase();
toLowerCaseFn("HELLO"); // hello
toLowerCaseFn(null); // null
カリー化
カリー化(Currying)とは、複数の引数を取る関数を、引数を1つだけ取る関数の連鎖に変換する技法で
これにより、関数の部分適用が可能になり、コードの再利用性が向上する。
// カリー化
const curryFunction = function (x) {
return function (y) {
console.log(x + y);
}
}
const add1 = curryFunction(1)(2);
クロージャー
内側の関数が外側の関数内で定義された変数や関数にアクセスできる技法で
プライベートの変数に状態を持たせつつ、同時にカプセル化も実現できる。
// 外側の関数は変数 "name" を定義
const pet = function (name) {
const getName = function () {
// 内側の関数は外側の関数の変数 "name" にアクセス可能
return name;
};
return getName; // 内側の関数を返すことで、外側の関数に公開する
};
const myPet = pet("Vivie");
console.log(myPet()); // "Vivie"
mdnの入れ子の関数とクロージャーで説明されている。
クロージャーについて
関数の中に関数を入れ子に (ネスト) することができます。入れ子になった (内側の) 関数は、それを含んでいる (外側の) 関数の外には非公開となります。
これによりクロージャが作られます。クロージャとは、環境に束縛された (式によって「閉じ込められた」) 変数を自由に持たせることができる式 (通常は一つの関数) のことです。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Functions#クロージャ
部分適用
部分適用(Partial Application)とは、関数に必要な引数の一部を先に渡しておき、残りの引数を後から渡すことで新しい関数を生成する技法で
部分適用を使用することで、特定の引数に対して事前に設定された関数を作成し、後から他の引数を渡すことができる。
これにより、同じ関数を異なるコンテキストで再利用することが容易になる。
// 部分適用
const blockFunction = arg1 => arg2 => {
console.log(`blockFunction: ${arg1} ${arg2}`);
}
const partialFunction = blockFunction('Hello');
partialFunction('World'); // blockFunction: Hello World
合成関数
合成関数(Function Composition)は、関数型プログラミングにおいて重要な概念の一つ。
合成関数を使用すると、複数の小さな関数を組み合わせて新しい関数を作成することができ、コードの保守性はもちろんですが、情報の隠蔽をすることができる。
// 合成関数
const compose = (f, g) => x => f(g(x));
const toUperCase = x => x.toUpperCase();
const addSymbol = x => console.log(`${x}!`);
const toUperCaseAndAddSymbol = compose(addSymbol, toUperCase);
toUperCaseAndAddSymbol('hello'); // HELLO!
// composeを使わない場合
const toUperCaseAndAddSymbol2 = x => addSymbol(toUperCase(x));
toUperCaseAndAddSymbol2('hello'); // HELLO!
情報の隠蔽
// 合成関数による情報の隠蔽
const add = x => y => x + y; // int -> int
const multiply = x => y => `${x * y}`; // int -> string
const addAndMultiply = compose(multiply(2), add(1)); // 部分適用 int -> string
console.log(addAndMultiply(3)); // string 8
モナド(仮)
モナドについてはまだ理解できた気がしないですが、
現時点の理解をメモ程度に残していく。
T => U
を使用して、U => U
のような処理を実装できたらモナド
javascriptではflatmap
がいわゆるモナドだと理解
const arr1 = [1, 2, 1];
const arrRes = arr1.flatMap((num) => (num === 2 ? [2, 2] : 1));
console.log(arrRes); // Array [1, 2, 2, 1]
const arrRes2 = arrRes.flatMap((num) => (num === 2 ? [2, 2] : 1));
console.log(arrRes2); // Array [1, 2, 2, 2, 2, 1]
Maybe
をチェーンで取り扱えるような例
(T(string|null)
=>U(Maybe)
、そしてU(Maybe)
=>U(Maybe)
を返す例)
function Maybe(value) {
this.value = value;
}
Maybe.prototype.bind = function (f) { // Maybe => Maybe
return this.value === null ? this : new Maybe(f(this.value));
}
Maybe.prototype.toString = function () {
return this.value;
}
const result = new Maybe("HELLO")
.bind(value => value.toLowerCase())
.bind(value => value.replace("h", "H"))
.toString();
const result2 = new Maybe(null)
.bind(value => value.toLowerCase())
.bind(value => value.replace("h", "H"))
.toString();
console.log(`OutPut : ${result}`); // OutPut : Hello
console.log(`OutPut : ${result2}`); // OutPut : null
javascriptのflatmap
に相当する実装を定めれば、同じモナドという枠組みで扱えるということなのだろうと理解。
Discussion