MoonBit・Haskell・Elixir・TypeScript/Effect で学ぶ
カリー化からモナドまで
この教材は、関数型プログラミングの「難しい」と言われる概念を、4つの異なる言語で比較しながら学ぶことを目的としています。
人によって「しっくりくる」言語は異なります。Haskellの抽象的な記法が苦手でも、TypeScriptなら理解できるかもしれません。MoonBitのRust風の文法が好みかもしれません。入り口を増やすことで、概念の本質に到達する確率を上げるのがこの教材の狙いです。
| 言語 | 特徴 | こんな人向け |
|---|---|---|
| MoonBit | Rust風の文法、WASMを出力、実用的な関数型 | Rust経験者、実践重視派 |
| Haskell | 純粋関数型の王道、理論的な美しさ | 理論を深く学びたい人 |
| Elixir | 実用的な関数型、並行処理が得意 | Ruby経験者、実務で使いたい人 |
| TypeScript + Effect | 既存のJSエコシステム、型安全な関数型 | フロントエンド開発者、JS経験者 |
関数型プログラミングは「新しいパラダイム」ではありません。1958年のLISPから始まり、現代のすべての主要言語に影響を与えています。あなたが使っているmap、filter、reduceも関数型の概念です。
このチュートリアルでは4つの言語を使ってコード例を示します。各言語の基本的な文法を簡単に紹介します。
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 } |
| コメント | // 一行コメント |
純粋関数型言語の代表格。強力な型システムと遅延評価が特徴。
| 構文 | 例 |
|---|---|
| 関数定義 | add x y = x + y |
| 型シグネチャ | add :: Int -> Int -> Int |
| 変数束縛 | let x = 5 in ... |
| パターンマッチ | case opt of Just v -> v; Nothing -> 0 |
| コメント | -- 一行コメント |
Erlang VM上で動作する動的型付け言語。並行処理とフォールトトレランスに優れる。
| 構文 | 例 |
|---|---|
| 関数定義 | def add(x, y), do: x + y |
| モジュール | defmodule Math do ... end |
| 変数束縛 | x = 5 |
| パターンマッチ | case opt do {:ok, v} -> v; :error -> 0 end |
| コメント | # 一行コメント |
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 |
| コメント | // 一行コメント |
読み方のコツ:各言語の細かい文法を覚える必要はありません。コード例を見て「何をしているか」を理解することに集中してください。どの言語でも、関数型の考え方は同じです。
関数型プログラミングの土台となる2つの概念、純粋関数と不変性から始めましょう。
純粋関数は以下の2つの性質を持つ関数です:
純粋関数は「数学の関数」と同じです。f(x) = x² という関数に 3 を入れれば、いつ、どこで、何回計算しても必ず 9 が返ってきます。朝計算しても夜計算しても、日本で計算してもアメリカで計算しても、結果は変わりません。
// 純粋関数: 同じ入力 → 同じ出力
fn add(x: Int, y: Int) -> Int {
x + y
}
fn square(x: Int) -> Int {
x * x
}
// add(2, 3) は常に 5
// square(4) は常に 16
-- 純粋関数: 同じ入力 → 同じ出力
add :: Int -> Int -> Int
add x y = x + y
square :: Int -> Int
square x = x * x
-- add 2 3 は常に 5
-- square 4 は常に 16
# 純粋関数: 同じ入力 → 同じ出力
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
// 純粋関数: 同じ入力 → 同じ出力
const add = (x: number, y: number): number =>
x + y;
const square = (x: number): number =>
x * x;
// add(2, 3) は常に 5
// square(4) は常に 16
// ❌ 外部状態に依存(非純粋)
let counter = 0;
const increment = (): number => {
counter += 1; // 副作用!
return counter;
};
// increment() の結果は呼び出すたびに変わる
// ❌ 外部状態に依存(非純粋)
const getRandomAdd = (x: number): number =>
x + Math.random();
// 同じ引数でも結果が変わる
// ✅ 純粋: 状態を引数で受け取り、新しい状態を返す
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
不変性とは、一度作成したデータを変更しないという原則です。データを「変更」する代わりに、変更を反映した「新しいデータ」を作成します。
// 不変のリスト操作
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はデフォルトで不変
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のデータは全て不変
original = [1, 2]
updated = [0 | original]
# original: [1, 2](変わらない)
# updated: [0, 1, 2]
IO.inspect(original) # [1, 2]
IO.inspect(updated) # [0, 1, 2]
// スプレッド演算子で不変性を保つ
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!
不変性のメリット:データが変わらないことが保証されると、「このデータは他の場所で変更されているかも」という心配が不要になります。並行処理でも安全、デバッグも容易、タイムトラベルデバッグ(状態の履歴を遡る)も可能になります。
以下の関数は純粋関数ですか?理由とともに答えてください。
let count = 0;
const increment = () => ++count;
Q1のincrement関数を純粋関数に書き換えてください。
高階関数とは、関数を引数として受け取るか、関数を戻り値として返す関数のことです。
以下のいずれかを満たす関数が高階関数です:
高階関数は「レシピを受け取るシェフ」のようなものです。普通の関数が「材料を受け取って料理を作る」のに対し、高階関数は「調理法(レシピ)を受け取って、その方法で料理を作る」ことができます。
mapは「リストの各要素に関数を適用する」という操作を抽象化したものです。
// 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]
-- 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]
# 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]
// 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]
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]
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]
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]
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)は、リストを1つの値に「畳み込む」操作です。これは最も汎用的な高階関数で、mapもfilterもreduceで実装できます。
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
-- 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)
# 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])
// 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を理解すれば、あらゆるリスト操作の本質を理解したことになります。
配列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] から、奇数だけを取り出し、それぞれを3倍して、合計を求めてください。
reduceを使ってmap関数を実装してください。
const myMap = (arr, fn) => ???
カリー化は関数型プログラミングで最も重要な概念の1つですが、最も誤解されやすい概念でもあります。
カリー化とは、複数の引数を取る関数を、1つの引数を取る関数のチェーンに変換することです。
f(a, b, c) → f(a)(b)(c)
カリー化は「注文カスタマイズ」に似ています。「チーズバーガー、ポテトM、コーラL」と一度に注文する代わりに、まず「メイン:チーズバーガー」を選び、次に「サイド:ポテトM」を選び、最後に「ドリンク:コーラL」を選ぶ。各段階で「次の選択を待つ状態」が作られます。
// 通常の関数(非カリー化)
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は全ての関数がデフォルトでカリー化
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でカリー化を手動で実装
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
// カリー化された関数
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
注意:カリー化と部分適用は混同されがちですが、異なる概念です。
// 部分適用: 一部の引数を固定
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では部分適用が自然
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!"
-- カリー化により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]
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))のように、処理のパラメータを先に設定し、後からデータを流し込むスタイルが可能になります。これがパイプライン処理との相性の良さの秘密です。
3つの数を掛け算する関数 multiply(a, b, c) をカリー化してください。
カリー化された add 関数を使って、配列の全要素に10を足す関数を作ってください。
const add = a => b => a + b;
代数的データ型(Algebraic Data Types, ADT)は、複雑なデータ構造を型安全に表現するための強力な仕組みです。
直積型は「AND」、住所は「都道府県 AND 市区町村 AND 番地」のように全部必要。直和型は「OR」、支払い方法は「現金 OR クレジット OR 電子マネー」のようにどれか1つ。
// 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()
}
}
-- 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では {: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"
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)); // "エラー..."
// 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
}
}
-- 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
# {: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
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}`
})
);
// 二分木の定義
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
-- 二分木の定義
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
# タプルで表現
# :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
// 判別共用体(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の威力:代数的データ型とパターンマッチングを使うと、「あり得るすべてのケースを漏れなく処理する」ことをコンパイラがチェックしてくれます。新しいケースを追加したら、処理し忘れている箇所がコンパイルエラーで教えてくれるのです。
リストの先頭要素を安全に取得する head 関数を Option を使って実装してください。
文字列を数値に変換する parseNumber 関数を Result 型を使って実装してください。
ここからが関数型プログラミングの「深い」部分です。まずはファンクター(Functor)から。
ファンクターは「mapできるもの」です。より正確には、「文脈を保ったまま、中身の値に関数を適用できる構造」です。
ファンクターは「宅配便の箱」のようなものです。箱を開けずに、中身だけ変換できます。「この箱の中のリンゴを全部ジュースにして」と頼むと、箱のまま中身がジュースになって返ってきます。箱の「形」は変わりません。
map(id) = id - 何もしない関数をmapしても何も変わらないmap(f . g) = map(f) . map(g) - 関数を合成してmapしても、別々にmapしても結果は同じ-- 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]
// 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でのファンクター的操作
# リストの 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"}
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のまま。エラーの中身を変換しようとしても、エラーのまま。文脈が自動的に伝搬するのです。
mapOption(Some(5), x => x) は何を返しますか?これはファンクターのどの法則を示していますか?
Some(Some(5)) に対して内側の値を2倍する方法を考えてください。
ファンクターでは「普通の関数」を「箱の中の値」に適用できました。では、関数自体も箱の中にある場合は?
ファンクターのmapは (a -> b) -> f a -> f b という型です。
では f (a -> b) -> f a -> f b のように、関数も文脈の中にある場合はどうする?
ファンクターは「外から箱を操作する」イメージでしたが、アプリカティブは「箱の中の道具を使って、別の箱の中身を加工する」イメージです。2つの宅配便を受け取り、中を開けずにそれぞれの中身を組み合わせる、そんな感じです。
-- 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]
// 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)
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"}
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
-- フォームのバリデーション
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
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つでも失敗したら全体が失敗、全部成功したら組み合わせた結果が得られる。バリデーションや設定読み込みなど、「独立した複数の処理」を合成する場面で活躍します。
2つのOption値を足し算する関数を作ってください。どちらかがNoneなら結果もNoneになるようにします。
いよいよモナドです。「モナドはバリトだ」「モナドは単なる自己関手の圏におけるモノイド対象だ」など様々な説明がありますが、ここではプログラマ向けに実用的に説明します。
アプリカティブでは独立した処理を組み合わせました。
では、前の処理の結果に応じて次の処理が変わる場合は?
これがモナドの出番です。
アプリカティブは「同時に複数の注文をする」イメージ。モナドは「前の料理を食べてから、次に何を食べるか決める」イメージです。最初の一皿がまずかったら帰る(エラー)かもしれないし、美味しかったらメインを注文するかもしれない。前の結果を見てから次を決める、これがモナドです。
-- 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
// 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)
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}
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
-- 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
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)
pure a >>= f = f am >>= pure = m(m >>= f) >>= g = m >>= (\x -> f x >>= g)これらは「bindを使ったチェーンが期待通り動く」ための条件です。
モナドの本質:モナドは「順序付けられた計算をつなぐ」ための抽象化です。前の計算の結果を見て、次に何をするか決められる。しかも、「失敗」「非同期」「状態」などの文脈を自動的に管理してくれる。これがモナドの力です。
| モナド | 文脈 | 用途 |
|---|---|---|
| Maybe / Option | 値があるかもしれない | null安全、検索結果 |
| Either / Result | 成功または失敗(エラー情報付き) | エラーハンドリング |
| List | 複数の可能性 | 非決定計算、全探索 |
| IO | 副作用を持つ計算 | 入出力、外部システム連携 |
| State | 状態を持つ計算 | ゲーム状態、パーサー |
| Reader | 環境を読む計算 | 設定、依存性注入 |
| Writer | ログを書く計算 | ロギング、トレース |
数値を受け取り、正の数なら平方根を返す safeSqrt 関数を作り、Some(16) に対して2回適用した結果を求めてください。
Some(5) に対して x => Some(x * 2) を適用するとき、mapとflatMapで結果がどう違うか説明してください。
実際のプログラムでは、「失敗するかもしれない」かつ「IOが必要」かつ「状態を持つ」など、複数の文脈を同時に扱うことがよくあります。
Maybe と IO を両方使いたい場合、IO (Maybe a) や Maybe (IO a) のようにネストさせると、>>= の使い方が複雑になります。
モナド変換子は、複数のモナドを「重ねる」ための仕組みです。
モナドが「特殊能力を持つ箱」だとすると、モナド変換子は「箱に追加能力を付与するアダプター」です。普通の箱に「失敗するかも」能力を追加したり、「ログを出力」能力を追加したりできます。
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
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}歳)`;
});
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
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)スタイルが生まれた背景です。
最後に、関数型プログラミングの最前線であるエフェクトシステムを見ていきましょう。
エフェクトシステムは、プログラムが持つ副作用を型レベルで追跡・管理する仕組みです。「この関数はファイルを読む」「この関数はネットワーク通信する」などが型に現れます。
普通の関数は「材料を入れたら料理が出てくるブラックボックス」。エフェクトシステムを使った関数は「必要な調理器具が外箱に書いてあるレシピキット」です。「オーブンが必要」「鍋が必要」と書いてあるので、実行する前に準備できます。
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);
-- 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でもトレイトで抽象化
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{})
}
# 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です。
この教材を読んで「ハッ」とした瞬間があれば、それはあなたの中で新しい概念が芽生えた証拠です。その気づきを大切に、実際のコードで試してみてください。