参考にさせていただいた記事:
https://codezine.jp/article/detail/18087
関数型プログラミングとは
- 挙動の予測可能性が高い関数の組み合わせでプログラムを組み立てるスタイル
- 以下に一部抜粋したような原則を守った関数を組み合わせることでコードの予測可能性、デバッグの効率、拡張性を高めることができる。
関数型プログラミングの重要原則
①純粋関数・不変性
②宣言的プログラミング
必用な視点
- コードの予測可能性と信頼性を高めること
- 抽象化、再利用
- デバッグの効率化
- 手続き型との比較
学習トピックの整理
①純粋関数
②不変性
③宣言型プログラミング
④Reactへの簡単な導入
純粋関数
純粋関数の定義
-
参照透過性
同じ入力に対して必ず同じ出力を返す。関数の結果は、外部の状態や時刻などに依存せず、引数だけから一意に決まる。ここでの同じ入力とは同じ変数や同じ参照を持つ変数といったことではなく、「同じ値」を持つことである。 -
副作用がない
関数の評価によって、外部の状態(グローバル変数、I/O操作、APIリクエストなど)が変更されない。すなわち、関数の実行はその結果の返却以外に、プログラムの状態に影響を及ぼさない。
副作用の定義
関数や式の評価がその結果の返却以外に、プログラムの外部状 態やグローバルな状態を変更したり、影響を及ぼしたりすること。
-
状態の変更: グローバル変数や外部変数の書き換え
- 入出力操作: ファイルの読み書き、コンソールへの出力、ネットワーク通信など
- 例外の発生: 計算結果に直接関係なく、例外をスローすること
不変性
純粋関数は入力の値が一意に定まれば一意な出力を返すという原則に従って予測可能性をたかめる。しかし入力が予測不能な状況ではプログラム全体での予測可能性は担保できない。不変性を守ることで入力の源となる外部状態を予測可能にする。
複合型データの内部構造の不変性(主に関数型プログラミングの文脈ではこちらのみをさす)
// 商品データ
const product = {
id: "p123",
name: "スマートフォン",
price: 50000,
stock: 10,
features: ["カメラ", "防水"]
};
// 商品表示コンポーネント
function ProductDisplay(productData) {
// 表示用に商品データをフォーマット
function formatForDisplay() {
// 不変性を破る: 表示用に直接オブジェクトを変更
productData.price = `${productData.price.toLocaleString()}円`;
return productData;
}
// 商品を表示
function render() {
const formattedProduct = formatForDisplay();
console.log(`商品名: ${formattedProduct.name}`);
console.log(`価格: ${formattedProduct.price}`);
console.log(`在庫: ${formattedProduct.stock}個`);
}
return { render };
}
// 価格計算コンポーネント
function PriceCalculator(productData) {
// 合計金額を計算(数量 × 価格)
function calculateTotal(quantity) {
const total = productData.price * quantity;
console.log(`${quantity}個の合計: ${total.toLocaleString()}円`);
return total;
}
return { calculateTotal };
}
// アプリケーション実行
console.log("=== 商品情報 ===");
const display = ProductDisplay(product);
display.render();
console.log("\n=== 価格計算 ===");
const calculator = PriceCalculator(product);
calculator.calculateTotal(2); // エラー: productData.price.toLocaleString is not a function
実行結果:
=== 価格計算 ===
TypeError: productData.price.toLocaleString is not a function
問題点:
- 時間的結合:
PriceCalculatorを先に実行すれば防げた問題ではありますがこれは本来受ける必要の内時間的束縛であり、モジュール独立性、拡張性の低下が起る。
- 予測不可能な動作:
ProductDisplayコンポーネントは表示のためだけにpriceを文字列に変換するつもりが、元のオブジェクトを変更してしまっていた。
この変更は他のコンポーネントにも影響し、予期しないエラーを引き起こしてしまう
- デバッグの難しさ:
エラーが発生した場所(calculator.calculateTotal)と実際に問題が発生した場所(formatForDisplay)が異なる。問題の原因を特定するのが困難になる
//改善
// 商品データ
const product = {
id: "p123",
name: "スマートフォン",
price: 50000,
stock: 10,
features: ["カメラ", "防水"]
};
// 商品表示コンポーネント
function ProductDisplay(productData) {
// 表示用に商品データをフォーマット(不変性を守る)
function formatForDisplay() {
// 新しいオブジェクトを作成して返す
return {
...productData,
price: `${productData.price.toLocaleString()}円`
};
}
// 商品を表示
function render() {
const formattedProduct = formatForDisplay();
console.log(`商品名: ${formattedProduct.name}`);
console.log(`価格: ${formattedProduct.price}`);
console.log(`在庫: ${formattedProduct.stock}個`);
}
return { render };
}
// 価格計算コンポーネント
function PriceCalculator(productData) {
// 合計金額を計算(数量 × 価格)
function calculateTotal(quantity) {
const total = productData.price * quantity;
console.log(`${quantity}個の合計: ${total.toLocaleString()}円`);
return total;
}
return { calculateTotal };
}
// アプリケーション実行
console.log("=== 商品情報 ===");
const display = ProductDisplay(product);
display.render();
console.log("\n=== 価格計算 ===");
const calculator = PriceCalculator(product);
calculator.calculateTotal(2); // 正常に動作: 100,000円
- 予測可能な動作:
各コンポーネントは他のコンポーネントの動作に影響を与えない
表示用のフォーマットは表示コンポーネント内に閉じ込められ、外部に影響しません - 並行処理の安全性:
複数のコンポーネントが同時に同じデータを処理しても、互いに干渉しない
これは特に非同期処理や並列処理が行われる場合に重要 - 時間的結合の排除:
コンポーネントの実行順序に依存しなくなる
例えば、PriceCalculatorを先に実行しても、後に実行しても同じ結果が得らる
参照の不変性(再代入の禁止)
- ローカルスコープに閉じていれば許容される
- 外部から観測できず、参照透過性を損なわなければ問題ない
その他の不変性を守らないことで起こる問題
- react/reduxとの仕様の不一致
- デバッグツールのタイムトラベル機能が使えない
宣言型プログラミング
-
計算を高水準で記述
- 低レベルの制御フローを記述せず、抽象的な構造で表現する。
- 具体的な手続きを隠蔽し実装意図のわかりやすさを優先する
-
例: for文を使わず
map()
,filter()
を使うことで配列内の要素の変換がしたい、絞り込みがしたいといった意図が見えやすくする
-
副作用の排除または制御
- 各モジュールの独立性をたかめる
-
数理論理学に基づいた設計
- 関数的な概念(同じ入力に対して同じ出力を返す)
Reactへの導入
状態管理やその他ロジックをカスタムフックに閉じ込めコンポーネントをUIに専念させることが一番ではないかと思いました。
カスタムフックの利点と副作用の管理
カスタムフックを利用する利点
カスタムフック(Custom Hook)を使用すると、副作用をコンポーネントの外部に閉じ込めることができ、コンポーネントの責務を UI の描画ロジックに集中させることが可能 になります。
この考え方は、関数型プログラミングの「純粋関数(Pure Function)」の概念に近く、可読性と保守性を向上させます。
カスタムフックのメリット
- コンポーネントの状態管理と副作用を分離 し、コンポーネントをシンプルに保つ。
- 副作用のロジックを再利用可能 にし、コードの重複を削減。
- 関心の分離(Separation of Concerns)を実現 し、各機能を独立したモジュールに整理。
- テストが容易 になり、副作用を持たないコンポーネント単体のテストが可能。
結論
・関数は引数から結果が予測できるようにしよう
そのために...
・外部変数への依存や影響を極力なくそう。
・外部変数を使う時は依存性を注入しよう。
・共有する変数は不変性を保ってなるべく更新を控えよう。
・複合型を扱う際はメソッドに気を付けよう