関数型プログラミング完全ガイド

MoonBit・Haskell・Elixir・TypeScript/Effect で学ぶ
カリー化からモナドまで

0はじめに - なぜ関数型プログラミングなのか

この教材は、関数型プログラミングの「難しい」と言われる概念を、4つの異なる言語で比較しながら学ぶことを目的としています。

4つの言語を使う理由

人によって「しっくりくる」言語は異なります。Haskellの抽象的な記法が苦手でも、TypeScriptなら理解できるかもしれません。MoonBitのRust風の文法が好みかもしれません。入り口を増やすことで、概念の本質に到達する確率を上げるのがこの教材の狙いです。

登場する言語たち

言語 特徴 こんな人向け
MoonBit Rust風の文法、WASMを出力、実用的な関数型 Rust経験者、実践重視派
Haskell 純粋関数型の王道、理論的な美しさ 理論を深く学びたい人
Elixir 実用的な関数型、並行処理が得意 Ruby経験者、実務で使いたい人
TypeScript + Effect 既存のJSエコシステム、型安全な関数型 フロントエンド開発者、JS経験者

関数型プログラミングは「新しいパラダイム」ではありません。1958年のLISPから始まり、現代のすべての主要言語に影響を与えています。あなたが使っているmapfilterreduceも関数型の概念です。

04言語の文法クイックガイド

このチュートリアルでは4つの言語を使ってコード例を示します。各言語の基本的な文法を簡単に紹介します。

MoonBit

Rustに似た静的型付け言語。WebAssembly向けに設計され、ブラウザで実行可能。

構文
関数定義fn add(x: Int, y: Int) -> Int { x + y }
変数束縛let x = 5
型注釈let x: Int = 5
パターンマッチmatch opt { Some(v) => v, None => 0 }
コメント// 一行コメント
MoonBit Playgroundで試す →

Haskell

純粋関数型言語の代表格。強力な型システムと遅延評価が特徴。

構文
関数定義add x y = x + y
型シグネチャadd :: Int -> Int -> Int
変数束縛let x = 5 in ...
パターンマッチcase opt of Just v -> v; Nothing -> 0
コメント-- 一行コメント
Haskell Playgroundで試す →

Elixir

Erlang VM上で動作する動的型付け言語。並行処理とフォールトトレランスに優れる。

構文
関数定義def add(x, y), do: x + y
モジュールdefmodule Math do ... end
変数束縛x = 5
パターンマッチcase opt do {:ok, v} -> v; :error -> 0 end
コメント# 一行コメント
Elixir Playgroundで試す →

TypeScript/Effect

JavaScriptに静的型付けを追加した言語。Effectライブラリで関数型パターンを実現。

構文
関数定義const add = (x: number, y: number) => x + y
型注釈const x: number = 5
ジェネリクスfunction id<T>(x: T): T { return x }
アロー関数(x) => x * 2
コメント// 一行コメント
TypeScript Playgroundで試す →

読み方のコツ:各言語の細かい文法を覚える必要はありません。コード例を見て「何をしているか」を理解することに集中してください。どの言語でも、関数型の考え方は同じです。

1純粋関数と不変性 - すべての基礎

関数型プログラミングの土台となる2つの概念、純粋関数不変性から始めましょう。

純粋関数とは何か

純粋関数の定義

純粋関数は以下の2つの性質を持つ関数です:

  1. 参照透過性:同じ引数に対して、常に同じ結果を返す
  2. 副作用がない:外部の状態を変更しない、外部の状態に依存しない

純粋関数は「数学の関数」と同じです。f(x) = x² という関数に 3 を入れれば、いつ、どこで、何回計算しても必ず 9 が返ってきます。朝計算しても夜計算しても、日本で計算してもアメリカで計算しても、結果は変わりません。

純粋関数の例

MoonBit
// 純粋関数: 同じ入力 → 同じ出力
fn add(x: Int, y: Int) -> Int {
  x + y
}

fn square(x: Int) -> Int {
  x * x
}

// add(2, 3) は常に 5
// square(4) は常に 16
Haskell
-- 純粋関数: 同じ入力 → 同じ出力
add :: Int -> Int -> Int
add x y = x + y

square :: Int -> Int
square x = x * x

-- add 2 3 は常に 5
-- square 4 は常に 16
Elixir
# 純粋関数: 同じ入力 → 同じ出力
defmodule Math do
  def add(x, y), do: x + y

  def square(x), do: x * x
end

# Math.add(2, 3) は常に 5
# Math.square(4) は常に 16
TypeScript
// 純粋関数: 同じ入力 → 同じ出力
const add = (x: number, y: number): number =>
  x + y;

const square = (x: number): number =>
  x * x;

// add(2, 3) は常に 5
// square(4) は常に 16

非純粋関数の例(アンチパターン)

TypeScript - 非純粋関数
// ❌ 外部状態に依存(非純粋)
let counter = 0;
const increment = (): number => {
  counter += 1;  // 副作用!
  return counter;
};
// increment() の結果は呼び出すたびに変わる

// ❌ 外部状態に依存(非純粋)
const getRandomAdd = (x: number): number =>
  x + Math.random();
// 同じ引数でも結果が変わる
TypeScript - 純粋に書き直す
// ✅ 純粋: 状態を引数で受け取り、新しい状態を返す
const increment = (counter: number): number =>
  counter + 1;

// increment(0) は常に 1
// increment(5) は常に 6

// ✅ 純粋: 乱数を引数として受け取る
const addRandom = (x: number, rand: number): number =>
  x + rand;
// addRandom(5, 0.5) は常に 5.5

不変性(Immutability)

不変性とは、一度作成したデータを変更しないという原則です。データを「変更」する代わりに、変更を反映した「新しいデータ」を作成します。

MoonBit
// 不変のリスト操作
fn append(list: List[Int], value: Int) -> List[Int] {
  // 元のlistは変更されない
  // 新しいリストを返す
  Cons(value, list)
}

fn main {
  let original = Cons(1, Cons(2, Nil))
  let updated = append(original, 0)
  // original: [1, 2](変わらない)
  // updated:  [0, 1, 2]
}
Haskell
-- Haskellはデフォルトで不変
prepend :: a -> [a] -> [a]
prepend x xs = x : xs

main = do
  let original = [1, 2]
  let updated = prepend 0 original
  -- original: [1, 2](変わらない)
  -- updated:  [0, 1, 2]
  print original  -- [1, 2]
  print updated   -- [0, 1, 2]
Elixir
# Elixirのデータは全て不変
original = [1, 2]
updated = [0 | original]

# original: [1, 2](変わらない)
# updated:  [0, 1, 2]

IO.inspect(original)  # [1, 2]
IO.inspect(updated)   # [0, 1, 2]
TypeScript
// スプレッド演算子で不変性を保つ
const original = [1, 2];
const updated = [0, ...original];

// original: [1, 2](変わらない)
// updated:  [0, 1, 2]

console.log(original);  // [1, 2]
console.log(updated);   // [0, 1, 2]

// ❌ 破壊的操作は避ける
// original.push(3); // これはNG!

不変性のメリット:データが変わらないことが保証されると、「このデータは他の場所で変更されているかも」という心配が不要になります。並行処理でも安全、デバッグも容易、タイムトラベルデバッグ(状態の履歴を遡る)も可能になります。

Try it! - 純粋関数を書いてみよう

Playground: Pure Functions
Code
Output

                    

練習問題

Q1 純粋関数の判定

以下の関数は純粋関数ですか?理由とともに答えてください。

let count = 0;
const increment = () => ++count;
Q2 純粋関数への書き換え

Q1のincrement関数を純粋関数に書き換えてください。

2高階関数 - 関数を操る関数

高階関数とは、関数を引数として受け取るか、関数を戻り値として返す関数のことです。

高階関数の定義

以下のいずれかを満たす関数が高階関数です:

  • 引数として1つ以上の関数を受け取る
  • 戻り値として関数を返す

高階関数は「レシピを受け取るシェフ」のようなものです。普通の関数が「材料を受け取って料理を作る」のに対し、高階関数は「調理法(レシピ)を受け取って、その方法で料理を作る」ことができます。

map - 変換の抽象化

mapは「リストの各要素に関数を適用する」という操作を抽象化したものです。

MoonBit
// map: 各要素に関数を適用
fn map[A, B](list: List[A], f: (A) -> B) -> List[B] {
  match list {
    Nil => Nil
    Cons(head, tail) => Cons(f(head), map(tail, f))
  }
}

// 使用例
let numbers = Cons(1, Cons(2, Cons(3, Nil)))
let doubled = map(numbers, fn(x) { x * 2 })
// 結果: [2, 4, 6]
Haskell
-- map: 各要素に関数を適用
map :: (a -> b) -> [a] -> [b]
map _ []     = []
map f (x:xs) = f x : map f xs

-- 使用例
numbers = [1, 2, 3]
doubled = map (*2) numbers
-- 結果: [2, 4, 6]

-- ポイントフリースタイル
doubled' = map (*2) [1, 2, 3]
Elixir
# map: 各要素に関数を適用
numbers = [1, 2, 3]
doubled = Enum.map(numbers, fn x -> x * 2 end)
# 結果: [2, 4, 6]

# キャプチャ演算子を使った短縮形
doubled = Enum.map(numbers, &(&1 * 2))

# パイプ演算子で読みやすく
result = [1, 2, 3]
  |> Enum.map(&(&1 * 2))
# 結果: [2, 4, 6]
TypeScript
// map: 各要素に関数を適用
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2);
// 結果: [2, 4, 6]

// Effect での map
import { pipe } from "effect";
import * as A from "effect/Array";

const result = pipe(
  [1, 2, 3],
  A.map(x => x * 2)
);
// 結果: [2, 4, 6]

filter - 条件による選別

MoonBit
fn filter[A](list: List[A], pred: (A) -> Bool) -> List[A] {
  match list {
    Nil => Nil
    Cons(h, t) =>
      if pred(h) { Cons(h, filter(t, pred)) }
      else { filter(t, pred) }
  }
}

let evens = filter(
  Cons(1, Cons(2, Cons(3, Cons(4, Nil)))),
  fn(x) { x % 2 == 0 }
)
// 結果: [2, 4]
Haskell
filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter p (x:xs)
  | p x       = x : filter p xs
  | otherwise = filter p xs

evens = filter even [1, 2, 3, 4]
-- 結果: [2, 4]

-- リスト内包表記でも書ける
evens' = [x | x <- [1..4], even x]
Elixir
evens = Enum.filter([1, 2, 3, 4], fn x ->
  rem(x, 2) == 0
end)
# 結果: [2, 4]

# 短縮形
evens = Enum.filter([1, 2, 3, 4], &(rem(&1, 2) == 0))

# パイプで組み合わせ
[1, 2, 3, 4]
|> Enum.filter(&(rem(&1, 2) == 0))
|> Enum.map(&(&1 * 10))
# 結果: [20, 40]
TypeScript
const evens = [1, 2, 3, 4].filter(x => x % 2 === 0);
// 結果: [2, 4]

// Effect でのチェーン
import { pipe } from "effect";
import * as A from "effect/Array";

const result = pipe(
  [1, 2, 3, 4],
  A.filter(x => x % 2 === 0),
  A.map(x => x * 10)
);
// 結果: [20, 40]

reduce / fold - 畳み込み

reduce(またはfold)は、リストを1つの値に「畳み込む」操作です。これは最も汎用的な高階関数で、mapfilterreduceで実装できます。

MoonBit
fn fold_left[A, B](
  list: List[A],
  init: B,
  f: (B, A) -> B
) -> B {
  match list {
    Nil => init
    Cons(h, t) => fold_left(t, f(init, h), f)
  }
}

// 合計
let sum = fold_left(
  Cons(1, Cons(2, Cons(3, Nil))),
  0,
  fn(acc, x) { acc + x }
)
// 結果: 6
Haskell
-- foldl: 左からの畳み込み
foldl :: (b -> a -> b) -> b -> [a] -> b
foldl _ acc []     = acc
foldl f acc (x:xs) = foldl f (f acc x) xs

-- 合計
sum' = foldl (+) 0 [1, 2, 3]
-- 結果: 6

-- foldr: 右からの畳み込み(遅延評価と相性◎)
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr _ acc []     = acc
foldr f acc (x:xs) = f x (foldr f acc xs)
Elixir
# reduce: 畳み込み
sum = Enum.reduce([1, 2, 3], 0, fn x, acc ->
  acc + x
end)
# 結果: 6

# 短縮形
sum = Enum.reduce([1, 2, 3], 0, &(&2 + &1))

# Enum.sum もある
sum = Enum.sum([1, 2, 3])
TypeScript
// reduce: 畳み込み
const sum = [1, 2, 3].reduce(
  (acc, x) => acc + x,
  0
);
// 結果: 6

// Effect での reduce
import { pipe } from "effect";
import * as A from "effect/Array";

const sum2 = pipe(
  [1, 2, 3],
  A.reduce(0, (acc, x) => acc + x)
);
// 結果: 6

reduce/foldは万能ナイフ:map、filter、find、every、some...これらすべてはreduceで実装できます。つまりreduceを理解すれば、あらゆるリスト操作の本質を理解したことになります。

Try it! - 高階関数を使ってみよう

Playground: Higher-Order Functions
Code
Output

                    

練習問題

Q1 map/filter/reduceの組み合わせ

配列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] から、奇数だけを取り出し、それぞれを3倍して、合計を求めてください。

Q2 reduceでmapを実装

reduceを使ってmap関数を実装してください。

const myMap = (arr, fn) => ???

3カリー化 - 関数を分解する魔法

カリー化は関数型プログラミングで最も重要な概念の1つですが、最も誤解されやすい概念でもあります。

カリー化の定義

カリー化とは、複数の引数を取る関数を、1つの引数を取る関数のチェーンに変換することです。

f(a, b, c)f(a)(b)(c)

カリー化の視覚的イメージ

flowchart LR subgraph normal["通常の関数"] A["f(a, b, c)"] --> B["結果"] end subgraph curried["カリー化後"] C["f(a)"] --> D["g(b)"] --> E["h(c)"] --> F["結果"] end style A fill:#3178c6 style C fill:#8b5cf6 style D fill:#8b5cf6 style E fill:#8b5cf6

カリー化は「注文カスタマイズ」に似ています。「チーズバーガー、ポテトM、コーラL」と一度に注文する代わりに、まず「メイン:チーズバーガー」を選び、次に「サイド:ポテトM」を選び、最後に「ドリンク:コーラL」を選ぶ。各段階で「次の選択を待つ状態」が作られます。

カリー化の実例

MoonBit
// 通常の関数(非カリー化)
fn add_normal(x: Int, y: Int) -> Int {
  x + y
}

// カリー化された関数
fn add(x: Int) -> (Int) -> Int {
  fn(y: Int) { x + y }
}

// 使い方
let add5 = add(5)       // 5を足す関数
let result = add5(3)    // 8
let result2 = add(5)(3) // 8(一気に呼ぶことも可能)
Haskell
-- Haskellは全ての関数がデフォルトでカリー化
add :: Int -> Int -> Int
add x y = x + y
-- これは add :: Int -> (Int -> Int) と同じ

-- 部分適用
add5 = add 5        -- 5を足す関数
result = add5 3     -- 8
result2 = add 5 3   -- 8

-- 演算子のセクション(部分適用の糖衣構文)
addFive = (+5)      -- 5を足す関数
double = (*2)       -- 2を掛ける関数
Elixir
# Elixirでカリー化を手動で実装
add = fn x ->
  fn y -> x + y end
end

add5 = add.(5)       # 5を足す関数
result = add5.(3)    # 8
result2 = add.(5).(3) # 8

# キャプチャ演算子での部分適用風
add5 = &(&1 + 5)
double = &(&1 * 2)

result = add5.(3)    # 8
result = double.(4)  # 8
TypeScript
// カリー化された関数
const add = (x: number) => (y: number): number =>
  x + y;

const add5 = add(5);     // 5を足す関数
const result = add5(3);  // 8
const result2 = add(5)(3); // 8

// 汎用カリー化関数
const curry2 = <A, B, C>(
  f: (a: A, b: B) => C
) => (a: A) => (b: B): C => f(a, b);

const normalAdd = (x: number, y: number) => x + y;
const curriedAdd = curry2(normalAdd);
const add10 = curriedAdd(10);
add10(5); // 15

部分適用との違い

注意:カリー化と部分適用は混同されがちですが、異なる概念です。

  • カリー化:関数の変換(構造を変える)
  • 部分適用:一部の引数を固定して新しい関数を作る(関数を使う)
TypeScript - 部分適用
// 部分適用: 一部の引数を固定
const greet = (greeting: string, name: string): string =>
  `${greeting}, ${name}!`;

// bind を使った部分適用
const sayHello = greet.bind(null, "Hello");
sayHello("Alice");  // "Hello, Alice!"

// カリー化版
const greetCurried = (greeting: string) =>
  (name: string): string => `${greeting}, ${name}!`;

const sayHi = greetCurried("Hi");
sayHi("Bob");  // "Hi, Bob!"
Haskell - 自然な部分適用
-- Haskellでは部分適用が自然
greet :: String -> String -> String
greet greeting name = greeting ++ ", " ++ name ++ "!"

-- 第1引数だけ適用
sayHello = greet "Hello"
sayHello "Alice"  -- "Hello, Alice!"

-- flipで引数の順序を変えて部分適用
greetAlice = flip greet "Alice"
greetAlice "Hey"  -- "Hey, Alice!"

カリー化の実用的な活用

Haskell
-- カリー化によりmapと組み合わせやすい
multiplyBy :: Int -> Int -> Int
multiplyBy n x = n * x

-- 全要素を2倍
doubled = map (multiplyBy 2) [1, 2, 3]
-- [2, 4, 6]

-- 全要素に10を足す
added = map (+10) [1, 2, 3]
-- [11, 12, 13]

-- フィルタと組み合わせ
greaterThan5 = filter (>5) [1, 8, 3, 9]
-- [8, 9]
TypeScript + Effect
import { pipe } from "effect";
import * as A from "effect/Array";

// カリー化されたユーティリティ
const multiplyBy = (n: number) => (x: number) => n * x;
const greaterThan = (n: number) => (x: number) => x > n;

// pipeで組み合わせ
const result = pipe(
  [1, 2, 3, 4, 5],
  A.map(multiplyBy(2)),
  A.filter(greaterThan(5))
);
// [6, 8, 10]

カリー化の本質:カリー化により、関数は「設定可能な処理」になります。filter(greaterThan(5))のように、処理のパラメータを先に設定し、後からデータを流し込むスタイルが可能になります。これがパイプライン処理との相性の良さの秘密です。

Try it! - カリー化を体験しよう

Playground: Currying
Code
Output

                    

練習問題

Q1 カリー化された関数の作成

3つの数を掛け算する関数 multiply(a, b, c) をカリー化してください。

Q2 部分適用の活用

カリー化された add 関数を使って、配列の全要素に10を足す関数を作ってください。

const add = a => b => a + b;

4代数的データ型とパターンマッチング

代数的データ型(Algebraic Data Types, ADT)は、複雑なデータ構造を型安全に表現するための強力な仕組みです。

代数的データ型の2種類

  • 直積型(Product Type):「AかつB」- 複数のフィールドを持つ(構造体、タプル)
  • 直和型(Sum Type):「AまたはB」- 複数の選択肢のいずれか(列挙型、バリアント)

直積型と直和型の構造

flowchart TB subgraph product["直積型 (AND)"] P1["Person"] --> P2["name: String"] P1 --> P3["age: Int"] P1 --> P4["email: String"] end subgraph sum["直和型 (OR)"] S1["Option[T]"] --> S2["Some(value)"] S1 --> S3["None"] end

直積型は「AND」、住所は「都道府県 AND 市区町村 AND 番地」のように全部必要。直和型は「OR」、支払い方法は「現金 OR クレジット OR 電子マネー」のようにどれか1つ。

Option / Maybe 型 - nullの安全な代替

MoonBit
// Option型の定義
enum Option[T] {
  None
  Some(T)
}

// 安全な割り算
fn safe_div(a: Int, b: Int) -> Option[Int] {
  if b == 0 { None }
  else { Some(a / b) }
}

// パターンマッチで値を取り出す
fn show_result(opt: Option[Int]) -> String {
  match opt {
    None => "エラー: 0で割れません"
    Some(n) => "結果: " + n.to_string()
  }
}
Haskell
-- Maybe型(標準ライブラリ)
data Maybe a = Nothing | Just a

-- 安全な割り算
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv a b = Just (a `div` b)

-- パターンマッチ
showResult :: Maybe Int -> String
showResult Nothing  = "エラー: 0で割れません"
showResult (Just n) = "結果: " ++ show n

-- case式でも書ける
showResult' m = case m of
  Nothing -> "エラー"
  Just n  -> show n
Elixir
# Elixirでは {:ok, value} と :error のタプル
defmodule SafeMath do
  def safe_div(_, 0), do: :error
  def safe_div(a, b), do: {:ok, div(a, b)}

  def show_result(:error), do:
    "エラー: 0で割れません"
  def show_result({:ok, n}), do:
    "結果: #{n}"
end

# 使用例
SafeMath.safe_div(10, 2)
|> SafeMath.show_result()
# "結果: 5"
TypeScript + Effect
import { Option, pipe } from "effect";

// 安全な割り算
const safeDiv = (a: number, b: number): Option.Option<number> =>
  b === 0 ? Option.none() : Option.some(a / b);

// パターンマッチ風の処理
const showResult = (opt: Option.Option<number>): string =>
  pipe(
    opt,
    Option.match({
      onNone: () => "エラー: 0で割れません",
      onSome: (n) => `結果: ${n}`
    })
  );

// 使用例
showResult(safeDiv(10, 2));  // "結果: 5"
showResult(safeDiv(10, 0));  // "エラー..."

Either / Result 型 - エラー情報も持てる

MoonBit
// Result型の定義
enum Result[T, E] {
  Ok(T)
  Err(E)
}

// エラーメッセージ付き割り算
fn safe_div(a: Int, b: Int) -> Result[Int, String] {
  if b == 0 { Err("0での除算はできません") }
  else { Ok(a / b) }
}

fn handle(r: Result[Int, String]) -> String {
  match r {
    Ok(n) => "成功: " + n.to_string()
    Err(msg) => "失敗: " + msg
  }
}
Haskell
-- Either型(標準ライブラリ)
data Either a b = Left a | Right b
-- Left: エラー、Right: 成功("right"="正しい"の語呂合わせ)

safeDiv :: Int -> Int -> Either String Int
safeDiv _ 0 = Left "0での除算はできません"
safeDiv a b = Right (a `div` b)

handle :: Either String Int -> String
handle (Left msg) = "失敗: " ++ msg
handle (Right n)  = "成功: " ++ show n
Elixir
# {:ok, value} と {:error, reason}
defmodule SafeMath do
  def safe_div(_, 0), do:
    {:error, "0での除算はできません"}
  def safe_div(a, b), do:
    {:ok, div(a, b)}
end

# with式でエラー処理をチェーン
with {:ok, x} <- SafeMath.safe_div(10, 2),
     {:ok, y} <- SafeMath.safe_div(x, 2) do
  {:ok, y}  # {:ok, 2}
else
  {:error, msg} -> {:error, msg}
end
TypeScript + Effect
import { Either, pipe } from "effect";

// Either型を使用
const safeDiv = (
  a: number, b: number
): Either.Either<number, string> =>
  b === 0
    ? Either.left("0での除算はできません")
    : Either.right(a / b);

const handle = (
  r: Either.Either<number, string>
): string =>
  pipe(
    r,
    Either.match({
      onLeft: (msg) => `失敗: ${msg}`,
      onRight: (n) => `成功: ${n}`
    })
  );

カスタムデータ型の定義

MoonBit
// 二分木の定義
enum Tree[T] {
  Leaf
  Node(Tree[T], T, Tree[T])
}

// 木の合計値
fn sum_tree(tree: Tree[Int]) -> Int {
  match tree {
    Leaf => 0
    Node(left, value, right) =>
      sum_tree(left) + value + sum_tree(right)
  }
}

// 使用例
let tree = Node(
  Node(Leaf, 1, Leaf),
  2,
  Node(Leaf, 3, Leaf)
)
// sum_tree(tree) = 6
Haskell
-- 二分木の定義
data Tree a = Leaf
            | Node (Tree a) a (Tree a)
  deriving (Show)

-- 木の合計値
sumTree :: Tree Int -> Int
sumTree Leaf = 0
sumTree (Node left val right) =
  sumTree left + val + sumTree right

-- 使用例
tree = Node (Node Leaf 1 Leaf) 2 (Node Leaf 3 Leaf)
-- sumTree tree = 6
Elixir
# タプルで表現
# :leaf または {:node, left, value, right}

defmodule Tree do
  def sum_tree(:leaf), do: 0
  def sum_tree({:node, left, val, right}) do
    sum_tree(left) + val + sum_tree(right)
  end
end

# 使用例
tree = {:node,
  {:node, :leaf, 1, :leaf},
  2,
  {:node, :leaf, 3, :leaf}
}
Tree.sum_tree(tree)  # 6
TypeScript
// 判別共用体(Discriminated Union)
type Tree<T> =
  | { tag: "leaf" }
  | { tag: "node"; left: Tree<T>; value: T; right: Tree<T> };

const sumTree = (tree: Tree<number>): number => {
  switch (tree.tag) {
    case "leaf": return 0;
    case "node": return
      sumTree(tree.left) + tree.value + sumTree(tree.right);
  }
};

// ヘルパー関数
const leaf = <T>(): Tree<T> => ({ tag: "leaf" });
const node = <T>(l: Tree<T>, v: T, r: Tree<T>): Tree<T> =>
  ({ tag: "node", left: l, value: v, right: r });

ADTの威力:代数的データ型とパターンマッチングを使うと、「あり得るすべてのケースを漏れなく処理する」ことをコンパイラがチェックしてくれます。新しいケースを追加したら、処理し忘れている箇所がコンパイルエラーで教えてくれるのです。

Try it! - Option型を使ってみよう

Playground: Option Type
Code
Output

                    

練習問題

Q1 Option型の実装

リストの先頭要素を安全に取得する head 関数を Option を使って実装してください。

Q2 Result型でのエラー処理

文字列を数値に変換する parseNumber 関数を Result 型を使って実装してください。

5ファンクター - 箱の中身を変換する

ここからが関数型プログラミングの「深い」部分です。まずはファンクター(Functor)から。

ファンクターとは

ファンクターは「mapできるもの」です。より正確には、「文脈を保ったまま、中身の値に関数を適用できる構造」です。

map操作のイメージ

flowchart LR subgraph mapOp["map操作"] A["Some(5)"] -->|"map(x => x * 2)"| B["Some(10)"] C["None"] -->|"map(x => x * 2)"| D["None"] end

ファンクターは「宅配便の箱」のようなものです。箱を開けずに、中身だけ変換できます。「この箱の中のリンゴを全部ジュースにして」と頼むと、箱のまま中身がジュースになって返ってきます。箱の「形」は変わりません。

ファンクターの法則

2つの法則

  1. 恒等法則map(id) = id - 何もしない関数をmapしても何も変わらない
  2. 合成法則map(f . g) = map(f) . map(g) - 関数を合成してmapしても、別々にmapしても結果は同じ

様々なファンクター

Haskell
-- Functor型クラスの定義
class Functor f where
  fmap :: (a -> b) -> f a -> f b

-- List は Functor
instance Functor [] where
  fmap = map

-- Maybe は Functor
instance Functor Maybe where
  fmap _ Nothing  = Nothing
  fmap f (Just x) = Just (f x)

-- 使用例
fmap (+1) [1, 2, 3]      -- [2, 3, 4]
fmap (*2) (Just 5)       -- Just 10
fmap (*2) Nothing         -- Nothing

-- <$> は fmap の中置演算子
(+1) <$> [1, 2, 3]        -- [2, 3, 4]
MoonBit
// Option の map
fn map_option[A, B](
  opt: Option[A],
  f: (A) -> B
) -> Option[B] {
  match opt {
    None => None
    Some(x) => Some(f(x))
  }
}

// Result の map
fn map_result[A, B, E](
  r: Result[A, E],
  f: (A) -> B
) -> Result[B, E] {
  match r {
    Err(e) => Err(e)
    Ok(x) => Ok(f(x))
  }
}

// 使用例
map_option(Some(5), fn(x) { x * 2 })  // Some(10)
map_option(None, fn(x) { x * 2 })     // None
Elixir
# Elixirでのファンクター的操作

# リストの map
Enum.map([1, 2, 3], &(&1 + 1))  # [2, 3, 4]

# {:ok, value} の map 相当
defmodule Functor do
  def map({:ok, x}, f), do: {:ok, f.(x)}
  def map({:error, e}, _f), do: {:error, e}
  def map(:error, _f), do: :error
end

Functor.map({:ok, 5}, &(&1 * 2))
# {:ok, 10}

Functor.map({:error, "oops"}, &(&1 * 2))
# {:error, "oops"}
TypeScript + Effect
import { Option, Either, pipe } from "effect";

// Option の map
const doubled = pipe(
  Option.some(5),
  Option.map(x => x * 2)
);  // Some(10)

const nope = pipe(
  Option.none(),
  Option.map((x: number) => x * 2)
);  // None

// Either の map
const success = pipe(
  Either.right(10),
  Either.map(x => x + 1)
);  // Right(11)

const failure = pipe(
  Either.left("error"),
  Either.map((x: number) => x + 1)
);  // Left("error")

ファンクターの本質:「文脈」を壊さずに「中身」を変換できる、これがファンクターの力です。Nothingの中身を変換しようとしても、ちゃんとNothingのまま。エラーの中身を変換しようとしても、エラーのまま。文脈が自動的に伝搬するのです。

Try it! - mapとflatMapの違いを体験

Playground: Functor & flatMap
Code
Output

                    

練習問題

Q1 ファンクター則の確認

mapOption(Some(5), x => x) は何を返しますか?これはファンクターのどの法則を示していますか?

Q2 ネストしたOptionのmap

Some(Some(5)) に対して内側の値を2倍する方法を考えてください。

6アプリカティブファンクター - 文脈内で関数を適用

ファンクターでは「普通の関数」を「箱の中の値」に適用できました。では、関数自体も箱の中にある場合は?

アプリカティブの問題意識

ファンクターのmapは (a -> b) -> f a -> f b という型です。

では f (a -> b) -> f a -> f b のように、関数も文脈の中にある場合はどうする?

独立した計算の合成

flowchart TB subgraph success["両方Someの場合"] A["Some(3)"] & B["Some(5)"] --> C["合成"] C -->|"両方Someなら"| D["Some(8)"] end subgraph failure["どちらかNoneの場合"] E["Some(3)"] & F["None"] --> G["合成"] G -->|"どちらかNoneなら"| H["None"] end

ファンクターは「外から箱を操作する」イメージでしたが、アプリカティブは「箱の中の道具を使って、別の箱の中身を加工する」イメージです。2つの宅配便を受け取り、中を開けずにそれぞれの中身を組み合わせる、そんな感じです。

アプリカティブの基本操作

Haskell
-- Applicative型クラス
class Functor f => Applicative f where
  pure  :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

-- Maybe のアプリカティブ
Just (+3) <*> Just 5      -- Just 8
Just (+3) <*> Nothing      -- Nothing
Nothing <*> Just 5        -- Nothing

-- 複数の Maybe を組み合わせる
add :: Int -> Int -> Int
add x y = x + y

-- 両方 Just の時だけ計算
add <$> Just 3 <*> Just 5   -- Just 8
add <$> Just 3 <*> Nothing  -- Nothing

-- リストのアプリカティブ(全組み合わせ)
[(+1), (*2)] <*> [1, 2, 3]
-- [2, 3, 4, 2, 4, 6]
MoonBit
// ap: アプリカティブの適用
fn ap_option[A, B](
  f_opt: Option[(A) -> B],
  a_opt: Option[A]
) -> Option[B] {
  match (f_opt, a_opt) {
    (Some(f), Some(a)) => Some(f(a))
    _ => None
  }
}

// 2つの Option を組み合わせる
fn map2[A, B, C](
  a: Option[A],
  b: Option[B],
  f: (A, B) -> C
) -> Option[C] {
  match (a, b) {
    (Some(x), Some(y)) => Some(f(x, y))
    _ => None
  }
}

// 使用例
map2(Some(3), Some(5), fn(a, b) { a + b })
// Some(8)
Elixir
defmodule Applicative do
  # 2つの結果を組み合わせる
  def map2({:ok, a}, {:ok, b}, f) do
    {:ok, f.(a, b)}
  end
  def map2({:error, e}, _, _), do: {:error, e}
  def map2(_, {:error, e}, _), do: {:error, e}
end

# 使用例
Applicative.map2(
  {:ok, 3},
  {:ok, 5},
  &(&1 + &2)
)
# {:ok, 8}

Applicative.map2(
  {:ok, 3},
  {:error, "oops"},
  &(&1 + &2)
)
# {:error, "oops"}
TypeScript + Effect
import { Option, pipe } from "effect";

// zipWith: 2つのOptionを組み合わせ
const result = pipe(
  Option.all([Option.some(3), Option.some(5)]),
  Option.map(([a, b]) => a + b)
);  // Some(8)

// 一方が None なら None
const failed = pipe(
  Option.all([Option.some(3), Option.none()]),
  Option.map(([a, b]) => a + b)
);  // None

// Effect の gen (do記法風)
import { Effect } from "effect";

const program = Effect.gen(function* () {
  const a = yield* Effect.succeed(3);
  const b = yield* Effect.succeed(5);
  return a + b;
});  // Effect

バリデーションへの応用

Haskell
-- フォームのバリデーション
data User = User String Int String  -- 名前、年齢、メール

validateName :: String -> Maybe String
validateName s
  | length s > 0 = Just s
  | otherwise    = Nothing

validateAge :: Int -> Maybe Int
validateAge n
  | n >= 0 && n < 150 = Just n
  | otherwise         = Nothing

-- アプリカティブで組み合わせ
createUser :: String -> Int -> String -> Maybe User
createUser name age email =
  User <$> validateName name
       <*> validateAge age
       <*> pure email
TypeScript + Effect
import { Option, pipe } from "effect";

type User = {
  name: string;
  age: number;
  email: string;
};

const validateName = (s: string): Option.Option<string> =>
  s.length > 0 ? Option.some(s) : Option.none();

const validateAge = (n: number): Option.Option<number> =>
  n >= 0 && n < 150 ? Option.some(n) : Option.none();

// 全てのバリデーションを組み合わせ
const createUser = (
  name: string, age: number, email: string
): Option.Option<User> =>
  pipe(
    Option.all([validateName(name), validateAge(age)]),
    Option.map(([n, a]) => ({ name: n, age: a, email }))
  );

アプリカティブの威力:複数の「失敗するかもしれない処理」を、個々の失敗を気にせず組み合わせられます。どれか1つでも失敗したら全体が失敗、全部成功したら組み合わせた結果が得られる。バリデーションや設定読み込みなど、「独立した複数の処理」を合成する場面で活躍します。

練習問題

Q1 複数のOptionを組み合わせる

2つのOption値を足し算する関数を作ってください。どちらかがNoneなら結果もNoneになるようにします。

7モナド - 文脈を繋ぐ力

いよいよモナドです。「モナドはバリトだ」「モナドは単なる自己関手の圏におけるモノイド対象だ」など様々な説明がありますが、ここではプログラマ向けに実用的に説明します。

モナドの問題意識

アプリカティブでは独立した処理を組み合わせました。

では、前の処理の結果に応じて次の処理が変わる場合は?

これがモナドの出番です。

モナドのチェーン (flatMap/bind)

flowchart LR subgraph success["成功パス: 32 -> 16 -> 8 -> 4"] A["Some(32)"] -->|"flatMap(half)"| B["Some(16)"] B -->|"flatMap(half)"| C["Some(8)"] C -->|"flatMap(half)"| D["Some(4)"] end subgraph failure["途中で失敗: 20 -> 10 -> 5 -> ???"] E["Some(20)"] -->|"flatMap(half)"| F["Some(10)"] F -->|"flatMap(half)"| G["Some(5)"] G -->|"flatMap(half)"| H["None"] end

アプリカティブは「同時に複数の注文をする」イメージ。モナドは「前の料理を食べてから、次に何を食べるか決める」イメージです。最初の一皿がまずかったら帰る(エラー)かもしれないし、美味しかったらメインを注文するかもしれない。前の結果を見てから次を決める、これがモナドです。

モナドの基本操作: bind / flatMap

Haskell
-- Monad型クラス
class Applicative m => Monad m where
  (>>=) :: m a -> (a -> m b) -> m b

-- Maybe の bind
Just 5 >>= \x -> Just (x + 1)   -- Just 6
Nothing >>= \x -> Just (x + 1)  -- Nothing

-- 連鎖する例
half :: Int -> Maybe Int
half x
  | even x    = Just (x `div` 2)
  | otherwise = Nothing

-- 20 -> 10 -> 5 -> Nothing
Just 20 >>= half >>= half >>= half
-- Nothing(5は割り切れない)

-- 32 -> 16 -> 8 -> 4
Just 32 >>= half >>= half >>= half
-- Just 4
MoonBit
// flatMap / bind の実装
fn flat_map[A, B](
  opt: Option[A],
  f: (A) -> Option[B]
) -> Option[B] {
  match opt {
    None => None
    Some(x) => f(x)
  }
}

fn half(x: Int) -> Option[Int] {
  if x % 2 == 0 { Some(x / 2) }
  else { None }
}

// チェーン
let result = flat_map(
  flat_map(Some(32), half),
  half
)
// Some(8)
Elixir
defmodule Monad do
  # flat_map / bind
  def bind({:ok, x}, f), do: f.(x)
  def bind({:error, e}, _f), do: {:error, e}
end

def half(x) when rem(x, 2) == 0, do: {:ok, div(x, 2)}
def half(_x), do: {:error, "not even"}

# with式でモナド的なチェーン
with {:ok, a} <- half(32),
     {:ok, b} <- half(a),
     {:ok, c} <- half(b) do
  {:ok, c}
end
# {:ok, 4}
TypeScript + Effect
import { Option, pipe } from "effect";

const half = (x: number): Option.Option<number> =>
  x % 2 === 0 ? Option.some(x / 2) : Option.none();

// flatMap でチェーン
const result = pipe(
  Option.some(32),
  Option.flatMap(half),
  Option.flatMap(half),
  Option.flatMap(half)
);  // Some(4)

const failed = pipe(
  Option.some(20),
  Option.flatMap(half),  // Some(10)
  Option.flatMap(half),  // Some(5)
  Option.flatMap(half)   // None (5は奇数)
);  // None

map と flatMap の違い

flowchart TB subgraph mapResult["map: 箱が入れ子になる"] M1["Some(5)"] -->|"map(x => Some(x*2))"| M2["Some(Some(10))"] end subgraph flatMapResult["flatMap: 平坦化される"] F1["Some(5)"] -->|"flatMap(x => Some(x*2))"| F2["Some(10)"] end

do記法 / for内包表記 - モナドを読みやすく

Haskell - do記法
-- bindの連鎖(読みにくい)
compute :: Maybe Int
compute =
  Just 10 >>= \x ->
  Just 20 >>= \y ->
  Just (x + y)

-- do記法(読みやすい!)
compute' :: Maybe Int
compute' = do
  x <- Just 10
  y <- Just 20
  return (x + y)
-- Just 30

-- 途中で失敗すると全体が失敗
computeFail :: Maybe Int
computeFail = do
  x <- Just 10
  y <- Nothing
  return (x + y)
-- Nothing
TypeScript + Effect (gen)
import { Effect, Option, pipe } from "effect";

// Effect.gen で do記法風に書ける
const program = Effect.gen(function* () {
  const x = yield* Effect.succeed(10);
  const y = yield* Effect.succeed(20);
  return x + y;
});

// 途中で失敗
const failing = Effect.gen(function* () {
  const x = yield* Effect.succeed(10);
  const y = yield* Effect.fail("error!");
  return x + y;  // ここには到達しない
});

// Option でも同様
const optProgram = Option.gen(function* () {
  const a = yield* Option.some(32);
  const b = yield* half(a);
  const c = yield* half(b);
  return c;
});  // Some(8)

モナドの法則

3つの法則

  1. 左単位元pure a >>= f = f a
  2. 右単位元m >>= pure = m
  3. 結合法則(m >>= f) >>= g = m >>= (\x -> f x >>= g)

これらは「bindを使ったチェーンが期待通り動く」ための条件です。

モナドの本質:モナドは「順序付けられた計算をつなぐ」ための抽象化です。前の計算の結果を見て、次に何をするか決められる。しかも、「失敗」「非同期」「状態」などの文脈を自動的に管理してくれる。これがモナドの力です。

代表的なモナドたち

モナド 文脈 用途
Maybe / Option 値があるかもしれない null安全、検索結果
Either / Result 成功または失敗(エラー情報付き) エラーハンドリング
List 複数の可能性 非決定計算、全探索
IO 副作用を持つ計算 入出力、外部システム連携
State 状態を持つ計算 ゲーム状態、パーサー
Reader 環境を読む計算 設定、依存性注入
Writer ログを書く計算 ロギング、トレース

Try it! - pipeとcomposeでパイプライン処理

Playground: Pipe & Compose
Code
Output

                    

練習問題

Q1 flatMapのチェーン

数値を受け取り、正の数なら平方根を返す safeSqrt 関数を作り、Some(16) に対して2回適用した結果を求めてください。

Q2 mapとflatMapの違い

Some(5) に対して x => Some(x * 2) を適用するとき、mapとflatMapで結果がどう違うか説明してください。

8モナド変換子 - モナドを組み合わせる

実際のプログラムでは、「失敗するかもしれない」かつ「IOが必要」かつ「状態を持つ」など、複数の文脈を同時に扱うことがよくあります。

モナド変換子の問題意識

Maybe と IO を両方使いたい場合、IO (Maybe a)Maybe (IO a) のようにネストさせると、>>= の使い方が複雑になります。

モナド変換子は、複数のモナドを「重ねる」ための仕組みです。

モナドスタック

flowchart TB subgraph stack["モナドスタック"] A["MaybeT"] --> B["ReaderT"] B --> C["IO"] end style A fill:#ec4899 style B fill:#f59e0b style C fill:#10b981

モナドが「特殊能力を持つ箱」だとすると、モナド変換子は「箱に追加能力を付与するアダプター」です。普通の箱に「失敗するかも」能力を追加したり、「ログを出力」能力を追加したりできます。

MaybeT - 失敗可能性を追加

Haskell
import Control.Monad.Trans.Maybe
import Control.Monad.IO.Class

-- MaybeT IO a は「失敗するかもしれないIO」
getUserName :: MaybeT IO String
getUserName = do
  liftIO $ putStrLn "名前を入力:"
  name <- liftIO getLine
  if null name
    then MaybeT $ return Nothing  -- 失敗
    else return name               -- 成功

getAge :: MaybeT IO Int
getAge = do
  liftIO $ putStrLn "年齢を入力:"
  input <- liftIO getLine
  case reads input of
    [(n, "")] -> return n
    _         -> MaybeT $ return Nothing

-- 両方成功したときだけ結果を得る
program :: MaybeT IO String
program = do
  name <- getUserName
  age  <- getAge
  return $ name ++ " さん (" ++ show age ++ "歳)"

main = do
  result <- runMaybeT program
  case result of
    Nothing -> putStrLn "入力エラー"
    Just s  -> putStrLn s
TypeScript + Effect
import { Effect, Console } from "effect";

// Effectは最初から複数の文脈を統合
// Effect<A, E, R>
//   A: 成功時の値
//   E: エラーの型
//   R: 必要な環境(依存性)

const getUserName = Effect.gen(function* () {
  yield* Console.log("名前を入力:");
  const name = yield* Console.readLine;
  if (name.trim() === "") {
    return yield* Effect.fail("名前が空です");
  }
  return name;
});

const getAge = Effect.gen(function* () {
  yield* Console.log("年齢を入力:");
  const input = yield* Console.readLine;
  const age = parseInt(input, 10);
  if (isNaN(age)) {
    return yield* Effect.fail("数値ではありません");
  }
  return age;
});

const program = Effect.gen(function* () {
  const name = yield* getUserName;
  const age = yield* getAge;
  return `${name} さん (${age}歳)`;
});

ReaderT - 環境(設定)を追加

Haskell
import Control.Monad.Reader

data Config = Config { dbHost :: String, dbPort :: Int }

-- ReaderT Config IO a は「設定を読めるIO」
getConnectionString :: ReaderT Config IO String
getConnectionString = do
  config <- ask
  return $ dbHost config ++ ":" ++ show (dbPort config)

connectDB :: ReaderT Config IO ()
connectDB = do
  connStr <- getConnectionString
  liftIO $ putStrLn $ "接続: " ++ connStr

main = do
  let config = Config "localhost" 5432
  runReaderT connectDB config
TypeScript + Effect
import { Effect, Context, Layer } from "effect";

// サービスを定義
class Config extends Context.Tag("Config")<
  Config,
  { dbHost: string; dbPort: number }
>() {}

// サービスを使うプログラム
const getConnectionString = Effect.gen(function* () {
  const config = yield* Config;
  return `${config.dbHost}:${config.dbPort}`;
});

const connectDB = Effect.gen(function* () {
  const connStr = yield* getConnectionString;
  yield* Effect.log(`接続: ${connStr}`);
});

// 依存性を提供して実行
const ConfigLive = Layer.succeed(
  Config,
  { dbHost: "localhost", dbPort: 5432 }
);

Effect.runPromise(
  Effect.provide(connectDB, ConfigLive)
);

モナド変換子の難点:スタックが深くなると、liftが大量に必要になり、パフォーマンスも低下します。これがEffect SystemやMTL(Monad Transformer Library)スタイルが生まれた背景です。

9エフェクトシステム - 副作用を型で管理

最後に、関数型プログラミングの最前線であるエフェクトシステムを見ていきましょう。

エフェクトシステムとは

エフェクトシステムは、プログラムが持つ副作用を型レベルで追跡・管理する仕組みです。「この関数はファイルを読む」「この関数はネットワーク通信する」などが型に現れます。

依存性の注入

flowchart LR subgraph deps["依存性の注入"] P["Program"] -->|"requires"| D["Database"] P -->|"requires"| L["Logger"] D2["DatabaseLive"] -->|"provides"| D L2["LoggerLive"] -->|"provides"| L end

普通の関数は「材料を入れたら料理が出てくるブラックボックス」。エフェクトシステムを使った関数は「必要な調理器具が外箱に書いてあるレシピキット」です。「オーブンが必要」「鍋が必要」と書いてあるので、実行する前に準備できます。

TypeScript + Effect

TypeScript + Effect
import {
  Effect, Context, Layer, Console, pipe
} from "effect";

// サービス(エフェクト)の定義
class Database extends Context.Tag("Database")<
  Database,
  {
    query: (sql: string) => Effect.Effect<unknown[], Error>;
  }
>() {}

class Logger extends Context.Tag("Logger")<
  Logger,
  {
    log: (msg: string) => Effect.Effect<void>;
  }
>() {}

// サービスを使うプログラム
// 型に「DatabaseとLoggerが必要」と現れる
const getUsers = Effect.gen(function* () {
  const db = yield* Database;
  const logger = yield* Logger;

  yield* logger.log("ユーザー取得開始");
  const users = yield* db.query("SELECT * FROM users");
  yield* logger.log(`${users.length}件取得`);

  return users;
});
// 型: Effect<unknown[], Error, Database | Logger>

// 実装を提供
const DatabaseLive = Layer.succeed(Database, {
  query: (sql) => Effect.succeed([{ id: 1, name: "Alice" }])
});

const LoggerLive = Layer.succeed(Logger, {
  log: (msg) => Console.log(`[LOG] ${msg}`)
});

// 全ての依存性を提供して実行
const program = pipe(
  getUsers,
  Effect.provide(DatabaseLive),
  Effect.provide(LoggerLive)
);

Effect.runPromise(program);
Haskell (効果システム風)
-- MTLスタイル: 型クラス制約で効果を表現
class Monad m => MonadDatabase m where
  query :: String -> m [User]

class Monad m => MonadLogger m where
  logMsg :: String -> m ()

-- 制約でエフェクトを宣言
getUsers :: (MonadDatabase m, MonadLogger m) => m [User]
getUsers = do
  logMsg "ユーザー取得開始"
  users <- query "SELECT * FROM users"
  logMsg $ show (length users) ++ "件取得"
  return users

-- 実行時に具体的な実装を提供
instance MonadDatabase IO where
  query _ = return [User 1 "Alice"]

instance MonadLogger IO where
  logMsg = putStrLn . ("[LOG] " ++)

MoonBitのエフェクトシステム

MoonBit
// MoonBitでもトレイトで抽象化
trait Logger {
  log(Self, String) -> Unit
}

trait Database {
  query(Self, String) -> Array[User]
}

// 抽象的なプログラム
fn get_users[L: Logger, D: Database](
  logger: L,
  db: D
) -> Array[User] {
  logger.log("ユーザー取得開始")
  let users = db.query("SELECT * FROM users")
  logger.log("取得完了")
  users
}

// 実装
struct ConsoleLogger {}
impl Logger for ConsoleLogger with log(self, msg) {
  println(msg)
}

struct MockDB {}
impl Database for MockDB with query(self, sql) {
  [{ id: 1, name: "Alice" }]
}

// 実行
fn main {
  let users = get_users(ConsoleLogger{}, MockDB{})
}
Elixir (Behaviour)
# Behaviourで効果を抽象化
defmodule Logger do
  @callback log(String.t()) :: :ok
end

defmodule Database do
  @callback query(String.t()) :: {:ok, list()} | {:error, term()}
end

# 抽象的なプログラム
defmodule UserService do
  def get_users(logger_mod, db_mod) do
    logger_mod.log("ユーザー取得開始")
    {:ok, users} = db_mod.query("SELECT * FROM users")
    logger_mod.log("#{length(users)}件取得")
    users
  end
end

# 実装
defmodule ConsoleLogger do
  @behaviour Logger
  def log(msg), do: IO.puts("[LOG] #{msg}")
end

# 実行(DIパターン)
UserService.get_users(ConsoleLogger, MockDB)

エフェクトシステムのメリット

メリット 説明
テスト容易性 依存性を差し替えてモックできる
型安全性 必要な依存性が型に現れ、忘れるとコンパイルエラー
合成可能性 小さなエフェクトを組み合わせて大きなプログラムを作れる
リソース管理 リソースの確保・解放を自動管理
並行処理 構造化された並行性をサポート

エフェクトシステムの本質:「何を行うか(What)」と「どう実行するか(How)」を分離します。プログラムは抽象的な「効果の記述」として書き、実行時に具体的な実装を注入します。これにより、同じビジネスロジックを本番環境でもテスト環境でも動かせます。

まとめ - 関数型プログラミングの階層

これまで学んできた概念を整理しましょう。

概念の階層構造

純粋関数・不変性高階関数(map, filter, reduce)カリー化・部分適用代数的データ型・パターンマッチングファンクター(mapできるもの)
    ↓
アプリカティブ(独立した計算の合成)
    ↓
モナド(依存する計算の連鎖)
    ↓
モナド変換子(モナドの組み合わせ)
    ↓
エフェクトシステム(副作用の型レベル管理)
                

各言語の特徴まとめ

言語 強み 学びのポイント
Haskell 純粋関数型、遅延評価、型クラス 理論を深く学ぶ、型の力を最大限に
MoonBit Rust風文法、WASM、実用的 パフォーマンスと安全性の両立
Elixir 並行処理、パターンマッチ、実務向け 関数型の実践、スケーラブルなシステム
TypeScript + Effect 既存エコシステム、段階的導入 JSからの移行、実務での関数型

最後に:関数型プログラミングの概念は、一度に全て理解する必要はありません。まずは純粋関数不変性から始め、map/filter/reduceを使いこなし、徐々にOption/Resultでnull安全を実現する。そこから先は必要に応じて学んでいけばOKです。

この教材を読んで「ハッ」とした瞬間があれば、それはあなたの中で新しい概念が芽生えた証拠です。その気づきを大切に、実際のコードで試してみてください。