第6章: 関数型データ構造¶
この章で学ぶこと¶
- イミュータブルなデータ構造の操作
- 配列とオブジェクトの不変更新
- 再帰的データ構造
- レンズの基礎
イミュータブルなデータ操作¶
関数型プログラミングでは、データを変更する代わりに新しいデータを作成します。
配列のイミュータブル操作¶
// 要素の追加
const append = <T>(arr: T[], item: T): T[] => [...arr, item];
const prepend = <T>(arr: T[], item: T): T[] => [item, ...arr];
// 要素の削除
const removeAt = <T>(arr: T[], index: number): T[] => [
...arr.slice(0, index),
...arr.slice(index + 1)
];
// 要素の更新
const updateAt = <T>(arr: T[], index: number, value: T): T[] => [
...arr.slice(0, index),
value,
...arr.slice(index + 1)
];
// 使用例
const numbers = [1, 2, 3, 4, 5];
const updated = updateAt(numbers, 2, 99);
console.log(numbers); // [1, 2, 3, 4, 5] - 元の配列は変更されない
console.log(updated); // [1, 2, 99, 4, 5]
オブジェクトのイミュータブル操作¶
// プロパティの更新
const updateProp = <T, K extends keyof T>(
obj: T,
key: K,
value: T[K]
): T => ({
...obj,
[key]: value
});
// プロパティの削除
const removeProp = <T, K extends keyof T>(
obj: T,
key: K
): Omit<T, K> => {
const { [key]: _, ...rest } = obj;
return rest;
};
// ネストしたオブジェクトの更新
const updateNested = <T>(
obj: T,
path: string[],
value: any
): T => {
if (path.length === 0) return value;
const [head, ...tail] = path;
return {
...obj,
[head]: updateNested((obj as any)[head], tail, value)
};
};
// 使用例
type User = {
id: number;
name: string;
address: {
city: string;
country: string;
};
};
const user: User = {
id: 1,
name: "Alice",
address: { city: "Tokyo", country: "Japan" }
};
const updated = updateNested(user, ["address", "city"], "Osaka");
console.log(user.address.city); // "Tokyo" - 元のオブジェクトは変更されない
console.log(updated.address.city); // "Osaka"
再帰的データ構造¶
リスト構造¶
// シンプルなリストの実装
type List<T> = null | { value: T; next: List<T> };
// リスト操作関数
const cons = <T>(value: T, list: List<T>): List<T> => ({ value, next: list });
const head = <T>(list: List<T>): T | undefined => list?.value;
const tail = <T>(list: List<T>): List<T> => list?.next ?? null;
// リストの変換
const mapList = <T, U>(fn: (value: T) => U, list: List<T>): List<U> => {
if (list === null) return null;
return cons(fn(list.value), mapList(fn, list.next));
};
// リストのフィルタリング
const filterList = <T>(predicate: (value: T) => boolean, list: List<T>): List<T> => {
if (list === null) return null;
if (predicate(list.value)) {
return cons(list.value, filterList(predicate, list.next));
}
return filterList(predicate, list.next);
};
// 使用例
const myList = cons(1, cons(2, cons(3, cons(4, null))));
const doubled = mapList(x => x * 2, myList);
const evens = filterList(x => x % 2 === 0, myList);
ツリー構造¶
// 二分木の定義
type Tree<T> =
| { type: "leaf" }
| { type: "node"; value: T; left: Tree<T>; right: Tree<T> };
// ツリーの作成
const leaf = <T>(): Tree<T> => ({ type: "leaf" });
const node = <T>(value: T, left: Tree<T>, right: Tree<T>): Tree<T> =>
({ type: "node", value, left, right });
// ツリーの操作
const mapTree = <T, U>(fn: (value: T) => U, tree: Tree<T>): Tree<U> => {
if (tree.type === "leaf") return leaf();
return node(
fn(tree.value),
mapTree(fn, tree.left),
mapTree(fn, tree.right)
);
};
// ツリーの畳み込み
const foldTree = <T, U>(
onLeaf: () => U,
onNode: (value: T, left: U, right: U) => U,
tree: Tree<T>
): U => {
if (tree.type === "leaf") return onLeaf();
return onNode(
tree.value,
foldTree(onLeaf, onNode, tree.left),
foldTree(onLeaf, onNode, tree.right)
);
};
// ツリーの高さを計算
const height = <T>(tree: Tree<T>): number =>
foldTree(
() => 0,
(_, left, right) => 1 + Math.max(left, right),
tree
);
レンズの基礎¶
レンズ(Lens)は、ネストしたデータ構造へのアクセスと更新を抽象化する機能パターンです。
シンプルなレンズの実装¶
// レンズの型定義
type Lens<S, A> = {
get: (s: S) => A;
set: (a: A) => (s: S) => S;
};
// レンズの作成
const lens = <S, A>(
get: (s: S) => A,
set: (a: A) => (s: S) => S
): Lens<S, A> => ({ get, set });
// プロパティレンズ
const prop = <T, K extends keyof T>(key: K): Lens<T, T[K]> =>
lens(
s => s[key],
a => s => ({ ...s, [key]: a })
);
// レンズの合成
const compose = <A, B, C>(
lens1: Lens<A, B>,
lens2: Lens<B, C>
): Lens<A, C> =>
lens(
a => lens2.get(lens1.get(a)),
c => a => lens1.set(lens2.set(c)(lens1.get(a)))(a)
);
// レンズの操作
const view = <S, A>(lens: Lens<S, A>, s: S): A => lens.get(s);
const set = <S, A>(lens: Lens<S, A>, a: A, s: S): S => lens.set(a)(s);
const over = <S, A>(lens: Lens<S, A>, fn: (a: A) => A, s: S): S =>
lens.set(fn(lens.get(s)))(s);
レンズの使用例¶
type Company = {
name: string;
address: Address;
};
type Address = {
street: string;
city: string;
country: string;
};
type Employee = {
name: string;
company: Company;
};
// レンズの定義
const companyLens = prop<Employee, "company">("company");
const addressLens = prop<Company, "address">("address");
const cityLens = prop<Address, "city">("city");
// レンズの合成
const employeeCityLens = compose(
compose(companyLens, addressLens),
cityLens
);
// 使用例
const employee: Employee = {
name: "Alice",
company: {
name: "TechCorp",
address: {
street: "123 Main St",
city: "Tokyo",
country: "Japan"
}
}
};
// データの取得
const city = view(employeeCityLens, employee);
console.log(city); // "Tokyo"
// データの更新
const updated = set(employeeCityLens, "Osaka", employee);
console.log(updated.company.address.city); // "Osaka"
console.log(employee.company.address.city); // "Tokyo" - 元のデータは変更されない
// 関数を適用
const uppercased = over(employeeCityLens, s => s.toUpperCase(), employee);
console.log(uppercased.company.address.city); // "TOKYO"
実践的なデータ構造の操作¶
イミュータブルな状態管理¶
// アプリケーションの状態
type AppState = {
users: User[];
selectedUserId: number | null;
filters: {
searchTerm: string;
isActive: boolean;
};
};
// 状態更新関数
const updateState = <K extends keyof AppState>(
state: AppState,
key: K,
value: AppState[K]
): AppState => ({ ...state, [key]: value });
// ユーザーの追加
const addUser = (state: AppState, user: User): AppState =>
updateState(state, "users", [...state.users, user]);
// フィルターの更新
const updateFilter = <K extends keyof AppState["filters"]>(
state: AppState,
filterKey: K,
value: AppState["filters"][K]
): AppState => ({
...state,
filters: {
...state.filters,
[filterKey]: value
}
});
// レンズを使ったアプローチ
const filtersLens = prop<AppState, "filters">("filters");
const searchTermLens = prop<AppState["filters"], "searchTerm">("searchTerm");
const appSearchTermLens = compose(filtersLens, searchTermLens);
// 検索ワードの更新
const updateSearchTerm = (state: AppState, term: string): AppState =>
set(appSearchTermLens, term, state);
実践演習¶
演習1: イミュータブルな配列操作¶
以下の関数を実装してください:
// 指定したインデックスに要素を挿入
function insertAt<T>(arr: T[], index: number, item: T): T[] {
// 実装
}
// 条件に一致する最初の要素を更新
function updateFirst<T>(
arr: T[],
predicate: (item: T) => boolean,
update: (item: T) => T
): T[] {
// 実装
}
解答
function insertAt<T>(arr: T[], index: number, item: T): T[] {
return [
...arr.slice(0, index),
item,
...arr.slice(index)
];
}
function updateFirst<T>(
arr: T[],
predicate: (item: T) => boolean,
update: (item: T) => T
): T[] {
const index = arr.findIndex(predicate);
if (index === -1) return arr;
return [
...arr.slice(0, index),
update(arr[index]!),
...arr.slice(index + 1)
];
}
演習2: レンズの活用¶
ネストしたオブジェクトを操作するユーティリティ関数を作成してください:
type Blog = {
title: string;
author: {
name: string;
email: string;
profile: {
bio: string;
avatar: string;
};
};
posts: Post[];
};
type Post = {
id: number;
title: string;
content: string;
tags: string[];
};
// 以下の操作をレンズを使って実装:
// 1. 著者のバイオを更新
// 2. 特定のポストにタグを追加
解答
// レンズの定義
const authorLens = prop<Blog, "author">("author");
const profileLens = prop<Blog["author"], "profile">("profile");
const bioLens = prop<Blog["author"]["profile"], "bio">("bio");
const postsLens = prop<Blog, "posts">("posts");
// 著者のバイオにアクセスするレンズ
const authorBioLens = compose(
compose(authorLens, profileLens),
bioLens
);
// 著者のバイオを更新
const updateAuthorBio = (blog: Blog, newBio: string): Blog =>
set(authorBioLens, newBio, blog);
// 特定のポストにタグを追加
const addTagToPost = (blog: Blog, postId: number, tag: string): Blog =>
over(postsLens, posts =>
posts.map(post =>
post.id === postId
? { ...post, tags: [...post.tags, tag] }
: post
),
blog
);
// 使用例
const blog: Blog = {
title: "My Blog",
author: {
name: "Alice",
email: "alice@example.com",
profile: {
bio: "Software Developer",
avatar: "avatar.jpg"
}
},
posts: [
{ id: 1, title: "First Post", content: "...", tags: ["intro"] }
]
};
const updated1 = updateAuthorBio(blog, "Senior Software Developer");
const updated2 = addTagToPost(blog, 1, "typescript");
まとめ¶
この章では、関数型データ構造について学びました:
- ✅ イミュータブルなデータ操作により、予測可能なコードが書ける
- ✅ 再帰的データ構造は関数型プログラミングで自然に扱える
- ✅ レンズにより、ネストしたデータのアクセスと更新が簡潔になる
- ✅ これらのパターンにより、複雑な状態管理も安全に行える
次の章では、関数型プログラミングにおける「エラーハンドリング」について学びます。