functional について
コードを書く開発者向けに、イミュータブルなデータ、純粋関数、合成を中心とした実践的な関数型プログラミングを案内するスキルです。読みやすさとテストのしやすさを重視し、軽量な手法を勧めます。
主な機能
- 01イミュータブルなデータを基本にする
- 02純粋関数を優先する
- 03継承より合成を選ぶ
- 04変換に配列メソッドを使う
- 05想定されるエラーを Result で表す
活用シーン
- 01共有データの意図しない変更の抑制
- 02複雑なロジックの合成可能な関数への分割
- 03予測しやすくテスト可能なコードの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ citypaul/ .dotfiles --skill functionalインストール後、Claude Code で/functionalを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
resources/composition-patterns.mdeffect-runtime.mdimmutability-catalog.md詳細
関数型パターン
詳細なリソースは resources/ ディレクトリにあります。必要に応じて読み込んでください。
| リソース | 読み込むタイミング |
|---|---|
immutability-catalog.md | ミューテーションによるバグの修正、readonly/ReadonlyArray 型の適用、または配列やオブジェクトをミューテーションする処理に対する不変の代替方法を調べるとき |
composition-patterns.md | 小さな関数をパイプラインに組み合わせるとき、巨大なロジックをリファクタリングするとき、または深くネストされたコードを平坦化するとき |
effect-runtime.md | Effect ベースのプロジェクトで、ランタイムの境界、サービス要件、失敗処理、中断、またはリソースのライフタイムを扱うとき |
小さな純粋関数は実装手法であり、モジュールごとに関数をひとつ公開しなければならないという規則ではありません。関連するヘルパーは、ひとつのまとまった責務を構成する場合、非公開のまま同じ場所に置いてください。呼び出し側に公開する安定したコントラクトを選ぶ際は codebase-design を使用してください。
基本原則
- ドメインデータはデフォルトで不変にする - 結果を構築して返します。ミューテーションは値を作成した関数の内部にとどめ、呼び出し側が保持している値には決して及ばないようにします。
リクエストでオブジェクトをその場で変更するよう求められていても、答えは新しい値です。 「すでに参照を持っているのだから、それを更新して返せばよい」という表現や、引数に書き込む隣接ヘルパーは、何を変更するかを示すものです。このスキルが決めるのは、どのように変更するかです。呼び出し側が参照を保持しているからこそ、その参照を通じて書き込んではいけません。同じオブジェクトを保持しているほかのすべての箇所に変更が見えますし、入力を凍結するテストは成功する代わりに例外を投げ、参照を比較する再レンダーでは変更が検出されません。入力から結果を構築し({ ...value, items }、[...xs].sort(...)、xs.map(...))、それを返してください。渡されたオブジェクトではなく新しい値を返した理由を、1文で伝えてください。再利用するよう指示されたヘルパーが引数に書き込む場合は、その戻り値を自分で作成したアキュムレーターに取り込んで処理を進めるか、まずそのヘルパーを純粋にしてください。入力のひとつをヘルパーに渡すことは決してしないでください。
- 可能な限り純粋関数を使う
- 継承よりコンポジションを優先する
- まずは自己文書化コード - 制約や自明でない理由を説明するコメントは残す
- 変換には配列メソッドを使う - すべての要素を走査するとき、アキュムレーターに畳み込むとき、またはルックアップを構築するときは、
map/filter/reduceを使います。ループを使うのは、早期終了や副作用がある場合に限ります。 - パラメーター群にはオプションオブジェクトを使う - シンプルな位置引数の API はシンプルなままにします。
不変性が重要な理由
不変データは関数型プログラミングの基盤です。不変データによって、コードは予測可能になり(同じ入力 → 同じ出力で、隠れた状態変更がありません)、デバッグしやすくなり(読み手が見ている間に状態が変わりません)、テストしやすくなり(隠れた可変状態が少なくなります)、React と相性がよくなります(調整とメモ化で参照の変更を利用できます)。共有状態に関する並行処理の危険も減らしますが、I/O や調整における競合を単独で防ぐものではありません。
// ❌ WRONG - Mutation creates unpredictable behavior
const user = { name: 'Alice', permissions: ['read'] };
grantPermission(user, 'write'); // Mutates user.permissions internally
console.log(user.permissions); // ['read', 'write'] - SURPRISE! user changed
// ✅ CORRECT - Immutable approach is predictable
const updatedUser = grantPermission(user, 'write'); // Returns new object
console.log(user.permissions); // ['read'] - original unchanged
console.log(updatedUser.permissions); // ['read', 'write'] - new version
データ型を宣言するときは、既存のファイルに追加する型や、パラメーター位置にあるインラインの { ... }[] も含め、すべてのプロパティに readonly を、すべての配列に ReadonlyArray<T> または readonly T[] を指定してください。これにより、コンパイラーがコントラクトを適用します。可変プロパティは例外であり、デフォルトではありません。カプセル化された可変アキュムレーター、キャッシュ、アダプターの状態は、ミューテーションがドメインコントラクトに漏れない限り許容されます。一般的なミューテーションと不変の代替方法については、resources/immutability-catalog.md を読み込んでください。
Functional Light
重い抽象化を避け、実用的な関数型パターンを取り入れる「Functional Light」の原則に従います。
- 推奨:純粋関数、不変データ、合成、宣言的なコード、配列メソッド、
readonlyによる型安全性 - 非推奨:圏論、モナド、重い FP ライブラリ(fp-ts、Ramda)、過剰な設計、関数型であること自体を目的とすること
理由: 目標は、保守しやすく、テストしやすいコードです。学術的な純粋性ではありません。関数型パターンによってコードが理解しにくくなるなら、使わないでください。
// ✅ GOOD - Simple, clear, functional
const activeUsers = users.filter(u => u.active);
const userNames = activeUsers.map(u => u.name);
// ❌ OVER-ENGINEERED - Unnecessary abstraction
const compose = <T>(...fns: Array<(arg: T) => T>) => (x: T) =>
fns.reduceRight((v, f) => f(v), x);
const withoutInactive = compose(
(users: readonly User[]): readonly User[] => users.filter(u => u.active),
(users: readonly User[]): readonly User[] => users.filter(u => !u.suspended),
)(users);
自己文書化するコードと有用なコメント
コードは、命名と構造によって明確にします。構文をただ説明するコメントは、リファクタリングして取り除くことを優先します。ただし、わかりにくい判断や制約を説明するコメントは残します。
残す価値のあるコメント:
- ドキュメントを生成するときの公開 API 用 JSDoc
- 他のスキルで必要とされる「理由」を示すコメント:特性テストのファイルヘッダーと、SUSPICIOUS な動作を示すマーカー(
characterisation-testsスキルを参照) - コードでは表現できない制約(例:上流のバグに対する回避策や、外部システムが要求する順序)
❌ 誤り — わかりにくいコードを説明するコメント
// Get the user and check if active and has permission
function check(u: any) {
// Check user exists, then active, then permission
if (u) {
if (u.a) {
if (u.p) return true;
}
}
return false;
}
✅ 正しい — 自己文書化するコード
function canUserAccessResource(user: User | undefined): boolean {
if (!user) return false;
if (!user.isActive) return false;
if (!user.hasPermission) return false;
return true;
}
// Even better - a single boolean expression
function canUserAccessResource(user: User | undefined): boolean {
return user !== undefined && user.isActive && user.hasPermission;
}
真偽値として扱う場合は、undefined を明示的に確認してください。オプショナルチェーン(user?.isActive && user?.hasPermission)は boolean | undefined を返し、strict モードではコンパイルに失敗します。
コメントがコードの動作を言い換えているだけなら、代わりにリファクタリングしてください。説明的な名前の関数を抽出する、意味のある変数名を使う、複雑なロジックを段階に分ける、ドメイン概念に型エイリアスを使う、といった方法があります。
✅ 公開 API に対して許容される JSDoc
/**
* Registers a scenario for runtime switching.
* @throws {ValidationError} if scenario ID is duplicate
*/
export function registerScenario(definition: ScenaristScenario): void {
配列メソッドとループの選び方
変換には map、filter、reduce を優先してください。これらは何をするかを宣言的に表現でき、自然に不変性を保てます。
✅ 正しい — map、filter、reduce とチェーン
const scenarioIds = scenarios.map(s => s.id);
const activeScenarios = scenarios.filter(s => s.active);
const totalActiveMinutes = sessions
.filter(session => session.active)
.map(session => session.durationMinutes * session.repetitions)
.reduce((sum, minutes) => sum + minutes, 0);
ループを使ってもよい場合
次のような場合は、命令型のループを使ってもかまいません。
- 途中終了が不可欠な場合(
for...ofとbreakを使用) - パフォーマンスが重要な場合(まず測定してください!)
- 副作用が必要な場合(ログ出力、DOM 操作)
ただし、最後まで実行されるループは、どれほど明快に読めても、また何を蓄積する場合でも、これらのケースには該当しません。ループの直前に宣言した値に各要素を畳み込む、キーごとにグループ化する、ルックアップを構築する、合計を集計する処理は、ヘルパーを呼び出す畳み込みも含めて reduce です。この場合、アキュムレーターはヘルパーの戻り値になります。1 対 1 の変換には map、一部の要素を残すには filter を使います。フィールドをキーとするルックアップには Object.groupBy、または reduce で構築する Map を使い、値を set する for...of は使いません。意図をより直接的に表現できる場合は、Array.find()、Array.some()、Array.every() を選んでください。すでに本体内で break または return しているループは、メソッドチェーンで途中終了を無理に表現せず、そのままにしてください。
Options オブジェクトを使う場合
パラメーターが意味のあるまとまりを形成する場合、複数の値が同じ型を共有する場合、またはオプション引数によって順序がわかりにくくなる場合は、options オブジェクトを使ってください。引数が位置によって明確にわかる小さく安定した関数は、位置引数のままでかまいません。
✅ 正しい — Options オブジェクト
type CreateReportOptions = {
readonly reportId: string;
readonly format: 'pdf' | 'csv';
readonly locale: string;
readonly timeZone: string;
readonly includeCharts?: boolean;
readonly sendEmail?: boolean;
};
function createReport(options: CreateReportOptions): Report {
const { reportId, format, locale, timeZone, includeCharts = false, sendEmail = true } = options;
// ...
}
// Call site - crystal clear
createReport({ reportId: 'report_123', format: 'pdf', locale: 'en-GB', timeZone: 'Europe/London', includeCharts: true });
add(a, b) のように引数の順序が明らかな場合や、よく使われるユーティリティに options オブジェクトを加えると冗長になる場合は、位置引数を使ってください。同じ型の引数やオプション引数によって呼び出しが曖昧になる場合は、名前付きオプションに切り替えます。引数の数は判断材料ですが、固定の上限ではありません。
純粋関数
純粋関数には副作用がなく、同じ入力に対して常に同じ出力を返します。
- 副作用がない — 外部状態を変更せず、引数を変更せず、I/O を実行しません
- 決定的である — 同じ入力 → 同じ出力となり、
Date.now()、Math.random()、グローバルに依存しません - 参照透過性がある — 呼び出しを戻り値で置き換えられます
純粋関数は、セットアップや後処理が不要なためテストしやすく、合成可能で、予測しやすく、キャッシュ可能で、並列実行も可能です。
不純性が必要な場合
一部の関数は不純である必要があります(I/O、乱数、副作用)。これらは分離します:
// ✅ CORRECT - Isolate impure functions at edges
// Pure core
function calculateTotalWeightGrams(parcels: ReadonlyArray<Parcel>): number {
return parcels.reduce((sum, parcel) => sum + parcel.weightGrams, 0);
}
// Impure shell (isolated)
async function saveShipment(shipment: Shipment): Promise<void> {
const totalWeightGrams = calculateTotalWeightGrams(shipment.parcels); // Pure
await database.save({ ...shipment, totalWeightGrams }); // Impure (I/O)
}
パターン:具体的な I/O は、プロジェクトで選択した境界に保ちます。アプリケーションのオーケストレーションでは、副作用を伴う機能を組み合わせてもかまいません。コアドメインの判断は純粋なままにします。副作用の分離だけを目的にポートとアダプターを導入するのではなく、既存のアーキテクチャに従います。
ネストより早期リターン
深いネストは、数値的なルールではなく、可読性を判断する手がかりとして捉えます。ネストされた制御フローによって主要な処理経路が分かりにくくなっている場合は、関数を抽出するか、ガード節を使ってフラットにします。具体例については、resources/composition-patterns.md を読み込みます。
// ❌ WRONG - Nested conditions
if (user) {
if (user.isActive) {
if (user.hasPermission) {
// do something
}
}
}
// ✅ CORRECT - Early returns (guard clauses)
if (!user) return;
if (!user.isActive) return;
if (!user.hasPermission) return;
// do something
エラー処理のための Result 型
想定される失敗が呼び出し元向けの契約の一部であり、呼び出し元が両方の分岐を処理する必要がある場合は、Result 型を使います。既存の例外、null 許容値、またはフレームワークのエラー規約によって契約がより明確に伝わる場合は、その規約を維持します。既存のビジネス上の結果を表すユニオンは、拒否を含め、値のままにできます。各拒否を例外やタグ付きの失敗に変換してから、同じ結果を復元するために直ちに捕捉するのではなく、利用側の境界で直接変換します。
type Result<T, E = Error> =
| { readonly success: true; readonly data: T }
| { readonly success: false; readonly error: E };
// Usage
function processBatch(batch: Batch): Result<BatchRun> {
if (batch.itemCount <= 0) {
return { success: false, error: new Error('Batch must contain an item') };
}
const run = executeBatch(batch);
return { success: true, data: run };
}
// Caller handles both cases explicitly
const result = processBatch(batch);
if (!result.success) return logError(result.error);
console.log(result.data.batchId); // TypeScript knows result.data exists here
まとめのチェックリスト
関数型コードを書く際は、次の点を確認します:
- ドメインデータが契約の一部として不変になっていること。可変状態がある場合は、ローカルに置いてカプセル化されていること
- 可能な限り純粋関数を使っていること(副作用がないこと)
- コードが自己文書化されていること。コメントでは、明白でない理由や制約を説明していること
- 明確さと制御フロー上の必要性に応じて、配列メソッドまたはループを選んでいること
- 呼び出し元向けの契約を改善できる場合は、オプションオブジェクトでパラメーターをまとめていること
- 複雑なモノリスではなく、小さな関数を組み合わせていること
- 追加したすべてのデータ型について、すべてのプロパティが
readonlyであること。すべての配列型がReadonlyArray<T>またはreadonly T[]であること - ネストが読みやすい状態であること。ガード節や関数の抽出によって、深い処理経路が明確になっていること
- 想定される失敗が戻り値の契約に含まれる場合に、Result 型を使っていること