functional-core-imperative-shell について
Functional Core, Imperative Shell(FCIS)は、アプリケーションコードの設計、レビュー、リファクタリングを支援するスキルです。純粋な業務ロジックと I/O の調整を分け、実行時コードを含むファイルに分類を付けます。
主な機能
- 01純粋なロジックと I/O の分離
- 02実行時コードを含むファイルの分類
- 03収集・処理・永続化の流れ
- 04混在したロジックの分割方法
活用シーン
- 01新しいファイルのコード構成
- 02データベースやファイル操作を含む処理の分割
- 03ユニットテストの複雑なモックの削減
- 04ファイルの責務とコード境界のレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ed3dai/ ed3d-plugins --skill functional-core-imperative-shellインストール後、Claude Code で/functional-core-imperative-shellを入力して読み込まれたか確認します。
詳細
Functional Core, Imperative Shell(FCIS)
概要
基本原則: 純粋なビジネスロジック(Functional Core)と副作用(Imperative Shell)を分離します。純粋関数は1つのファイルに、I/O 操作は別のファイルに配置します。
重要な理由: 純粋関数は簡単にテストできます(モックは不要です)。I/O コードは薄いシェルに分離されます。ビジネスロジックに副作用がなければ、バグが構造上発生し得なくなります。
使用する場面
FCIS を使用する場面:
- 新しいコードファイルを作成するとき
- 既存のコードをリファクタリングするとき
- アーキテクチャ上の判断のためにコードをレビューするとき
- ロジックをどこに配置するか決めるとき
該当する兆候:
- 「この関数はどこに配置すればよいか?」
- 新しいファイルを作成する
- ロジックにデータベース呼び出しを追加する
- 計算処理にファイル I/O を追加する
- 複雑なモックが必要なテストを書く
必須:ファイルの分類
実行時の動作を含むすべてのファイルに、パターンコメントを追加する必要があります。
// pattern: Functional Core
// pattern: Imperative Shell
// pattern: Mixed (needs refactoring)
ファイルを本当に分離できない場合(まれです)は、その理由を記載してください。
// pattern: Mixed (unavoidable)
// Reason: [specific technical justification]
// Example: Performance-critical path where separating I/O causes unacceptable overhead
実行時の動作を含むファイルには、必ず分類が必要です。 関数、メソッドを持つクラス、またはオーケストレーションロジックを含むファイルを、このコメントなしで作成した場合、要件に違反しています。
対象外:実行時の動作を含まないファイル
以下にはパターンコメントを追加しないでください。
- 型のみのファイル - 型、インターフェース、または型エイリアスのみをエクスポートするファイル(実行時コードなし)
- 定数/列挙型に類するファイル - 関数を含まない静的データ宣言
- バレル/index ファイル - 再エクスポートのみを行うファイル(
export * from './foo') - テストファイル - テストはコア/シェルのコードを実行しますが、それ自体はコアでもシェルでもありません
- 生成ファイル - 機械生成されたコード
- Bash/シェルスクリプト(.sh、.bash)- 本質的に命令型です
- 設定ファイル(eslint.config.js、tsconfig.json、.env など)
- Markdown ドキュメント(.md)
- HTML ファイル(.html)
- タスクランナーファイル(justfile、Makefile など)
- パッケージマニフェスト(package.json、pyproject.toml など)
- データファイル(JSON、YAML、CSV など)
注: 対象外のファイルに実行時ロジックを追加した場合(例:「types」ファイルにヘルパー関数を追加する、または定数ファイルにファクトリー関数を追加するなど)、しきい値を超えるため、分類が必須になります。
分類は、実行時の動作を含むアプリケーションのソースファイル(ロジックを持つ関数、メソッドを持つクラス、I/O のオーケストレーション)に適用されます。
ファイルの種類の定義
Functional Core ファイル
含めるもの:
- 純粋関数(常に同じ入力から同じ出力が得られる)
- ビジネスロジック、検証、計算、変換
- データ構造の操作
- ロギング(例外:Functional Core ではロガーの使用が許可されます)
含めてはいけないもの:
- ファイル I/O(ファイルの読み取り、書き込み)
- データベース操作(クエリ、更新、接続)
- HTTP リクエストまたはレスポンス
- 環境変数へのアクセス
- Date.now()、Math.random() などの非決定的な関数
- 関数のスコープ外での状態の変更
ロギングの例外: 関数はロガーを受け取り、使用してもかまいません。ユニットテストでは、何もしないロガーを渡します。これは Functional Core で許可される唯一の副作用です。
テストの形式: 単純なアサーションを使用し、(ロガーを使用する場合を除き)モックは不要です。
Imperative Shell ファイル
含めるもの:
- I/O 操作:ファイルシステム、データベース、HTTP、環境
- オーケストレーション:データを収集 -> Functional Core を呼び出す -> 結果を永続化する
- I/O の失敗に対するエラー処理
- 最小限のビジネスロジック(調整のみ)
含めてはいけないもの:
- 複雑な計算
- ビジネスルールの検証
- 形式変換を超えるデータ変換
テストの形式: 実際の依存関係またはテストダブルを使用した統合テスト。
コードフローのパターン
1. GATHER (Shell): Collect data from external sources
2. PROCESS (Core): Transform input to output (pure)
3. PERSIST (Shell): Save results externally
すべての操作はこの順序に従います。例外はありません。
判断フレームワーク
関数を書く前に、次の点を確認してください。
digraph fcis_decision {
"Writing a function" [shape=ellipse];
"Can run without external dependencies?" [shape=diamond];
"Does it coordinate I/O?" [shape=diamond];
"Functional Core" [shape=box, style=filled, fillcolor=lightblue];
"Imperative Shell" [shape=box, style=filled, fillcolor=lightgreen];
"STOP: Refactor or escalate" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
"Writing a function" -> "Can run without external dependencies?";
"Can run without external dependencies?" -> "Functional Core" [label="yes"];
"Can run without external dependencies?" -> "Does it coordinate I/O?" [label="no"];
"Does it coordinate I/O?" -> "Imperative Shell" [label="yes"];
"Does it coordinate I/O?" -> "STOP: Refactor or escalate" [label="no"];
}
確認する項目:
- このロジックは、ファイルシステム、データベース、ネットワーク、環境に依存せずに実行できますか?
- YES -> Functional Core
- NO -> I/O を調整する処理、またはビジネスロジックが含まれていますか?
- I/O の調整 -> Imperative Shell
- ビジネスロジック + I/O -> STOP。リファクタリングするか、ユーザーにエスカレーションしてください。
よくある間違いと自己正当化
| 言い訳/思考パターン | 実態 | 対応 |
|---|---|---|
| 「この計算でファイルを 1 つ読み込むだけ」 | ファイル I/O は副作用です。Functional Core ではありません。 | Shell に切り出します。データをパラメーターとして渡します。 |
| 「データベースをパラメーターとして渡すので、純粋です」 | データベース操作は I/O です。純粋ではありません。 | Shell に移します。Core が受け取るのはデータであり、DB 接続ではありません。 |
| 「この検証ではファイルが存在するか確認する必要があります」 | ファイルシステムの確認は I/O です。Functional Core ではありません。 | Shell でファイルを確認し、真偽値を Core の検証処理に渡します。 |
| 「小さな HTTP 呼び出しなら問題ありません」 | HTTP は副作用です。純粋性の保証が損なわれます。 | Shell でリクエストを行い、Core で応答データを処理します。 |
| 「タイムスタンプの計算に Date.now() が必要です」 | 非決定的です。純粋ではありません。 | Shell からタイムスタンプをパラメーターとして渡します。 |
| 「ログ出力は副作用なので削除するべきです」 | 誤りです。 ログ出力は明示的に許可されています。 | ロガーを残します。これは例外です。 |
| 「この関数はロジックと I/O の両方を処理しますが、そのほうが簡単です」 | 関心事が混在すると、モックなしではテストできません。 | Core(ロジック)と Shell(I/O)に分割します。Core は簡単にテストできます。 |
| 「ファイルの分類は余計な手間です」 | さまざまな種類のバグを未然に防げます。これは必須です。 | 分類コメントを追加します。10 秒で済みます。 |
| 「あとでリファクタリングします」 | あとでは決して来ません。今すぐ実行してください。 | 今すぐ分類して分離します。 |
| 「パフォーマンスのためには混在させる必要があります」 | ベンチマークで証明してください。たいていは誤りです。 | まず分離します。根拠に基づいて最適化します。正当な理由を示して Mixed(不可避)と分類します。 |
危険信号 — STOP してリファクタリング
次のいずれかを行っていることに気づいたら、すぐに STOP してください。
- 「純粋」関数でのファイル I/O(open、read、write、exists の確認)
- Functional Core にパラメーターとして渡すデータベース(クエリ、更新、接続)
- ビジネスロジック内での HTTP リクエスト(fetch、axios、requests)
- 計算での環境変数の使用(process.env、os.getenv)
- Functional Core での Math.random() または Date.now() の使用(非決定的)
- パターン分類コメントなしで、実行時の動作を伴うファイルを作成する
- 関心事の混在について「今回だけ」と考える
これらはすべて、次のことを意味します。 I/O を Shell に切り出します。データを Core に渡します。ファイルを正しく分類します。
実装パターン
Functional Core パターン
# pattern: Functional Core
def calculate_total_with_tax(items, tax_rate, logger=None):
"""Pure calculation: same inputs always produce same output."""
if logger:
logger.debug(f"Calculating total for {len(items)} items")
subtotal = sum(item['price'] * item['quantity'] for item in items)
tax = subtotal * tax_rate
total = subtotal + tax
return {
'subtotal': subtotal,
'tax': tax,
'total': total
}
I/O なし。データベースなし。ファイルシステムなし。計算のみです。
Imperative Shell パターン
# pattern: Imperative Shell
def process_order(order_id, db, logger):
"""Orchestrates: gather -> process -> persist."""
# GATHER: Collect data from external sources
items = db.get_order_items(order_id)
tax_rate = db.get_tax_rate_for_order(order_id)
# PROCESS: Call Functional Core (pure logic)
result = calculate_total_with_tax(items, tax_rate, logger)
# PERSIST: Save results externally
db.update_order_total(order_id, result['total'])
return result
Shell は薄く保ちます。主要な処理は Core が担います。個別にテストできます。
Mixed(リファクタリングが必要)— 悪い例
# pattern: Mixed (needs refactoring)
def calculate_and_save_total(order_id, db):
"""BAD: Mixes calculation with I/O. Hard to test."""
items = db.get_order_items(order_id) # I/O
subtotal = sum(item['price'] for item in items) # Logic
tax_rate = db.get_tax_rate_for_order(order_id) # I/O
tax = subtotal * tax_rate # Logic
total = subtotal + tax # Logic
db.update_order_total(order_id, total) # I/O
return total
このコードのテストにはデータベースのモックが必要です。壊れやすい構成です。上記のパターンを使ってリファクタリングしてください。
ロガーの例外に関する詳細
ロガーは Functional Core で明示的に許可されています。
# pattern: Functional Core
def validate_order(order_data, logger=None):
"""Pure validation with logging."""
if logger:
logger.info(f"Validating order {order_data.get('id')}")
errors = []
if not order_data.get('items'):
errors.append("Order must have items")
if order_data.get('total', 0) < 0:
errors.append("Total cannot be negative")
if logger and errors:
logger.warning(f"Validation failed: {errors}")
return {'valid': len(errors) == 0, 'errors': errors}
ユニットテストの場合: no-op ロガーまたは None を渡します。関数はテストで純粋なままです。
リファクタリングのパターン
関心事を分離するための一般的なパターン:
不純な関数から純粋な Core を切り出す
症状: 関数内で I/O とロジックが混在している
# BEFORE - hard to test
def process_order(order_id: str) -> None:
order = db.fetch(order_id) # I/O
discount = calculate_discount(order) # Pure logic
total = apply_discount(order, discount) # Pure logic
db.save(order_id, total) # I/O
# AFTER - pure core extracted
def calculate_order_total(order: Order, rules: DiscountRules) -> Decimal:
"""Pure function - easy to test."""
discount = calculate_discount(order, rules)
return apply_discount(order, discount)
def process_order(order_id: str) -> None:
"""Thin I/O wrapper."""
order = db.fetch(order_id)
total = calculate_order_total(order, get_discount_rules())
db.save(order_id, total)
変更する代わりに値を返す
症状: メソッドがその場で値を変更するため、変更前後の比較が難しい
# BEFORE - mutation
def sort_tasks(tasks: list[Task]) -> None:
tasks.sort(key=lambda t: t.priority)
# AFTER - returns new value
def sorted_tasks(tasks: list[Task]) -> list[Task]:
return sorted(tasks, key=lambda t: t.priority)
不足している逆操作を追加する
症状: 一方向の操作はあるものの、往復テストに必要な逆操作がない
# BEFORE - only encode exists
def encode_message(msg: dict) -> bytes:
return msgpack.packb(msg)
# AFTER - add decode for roundtrip testing
def decode_message(data: bytes) -> dict:
return msgpack.unpackb(data)
ハードコードされた依存関係を置き換える
症状: 関数がグローバル変数やハードコードされた設定を使っているため、エッジケースをテストできない
# BEFORE - uses global
def validate_input(data: str) -> bool:
return len(data) <= CONFIG.max_length
# AFTER - dependency injected
def validate_input(data: str, max_length: int) -> bool:
return len(data) <= max_length
リファクタリングの優先順位
| パターン | 影響 | 労力 | 優先度 |
|---|---|---|---|
| 純粋な Core の切り出し | 高 | 中 | 最初に実施 |
| 不足している逆操作の追加 | 高 | 低 | すぐに得られる効果 |
| 変更する代わりに値を返す | 中 | 低 | 簡単な改善 |
| 依存関係の注入 | 中 | 中 | テストが妨げられている場合に実施 |
リファクタリングのチェックリスト
関心事が混在している場合:
- 純粋な計算(ロジック、計算、検証)を特定する
- 純粋なコードを Functional Core ファイルに抽出する
- I/O 操作(ファイル、データベース、HTTP、環境)を特定する
- I/O は Imperative Shell ファイルに保持する
- Shell でデータを収集し、Core を呼び出して、結果を永続化する
- 両方のファイルにパターン分類のコメントを追加する
- Core を簡単なアサーションでテストする(ロガー以外はモックを使わない)
- Shell を統合テストでテストする
分離できない場合: 具体的な技術的根拠を添えてユーザーにエスカレーションします。混在が必要だと決めつけないでください。
まとめ
FCIS の 3 つのルール:
- Functional Core: 純粋関数のみ。ロギング以外の I/O は行いません。テストが容易です。
- Imperative Shell: I/O の調整のみ。ロジックは最小限にします。Core を呼び出します。
- 実行時の動作があるすべてのファイルを分類する。 型のみのファイル、定数、バレル、テスト、生成されたファイルは対象外です。
迷った場合: 外部依存なしで実行できますか? → Functional Core。それ以外は Imperative Shell です。
ロギングの例外: ロガーはどこでも使用できます。ユニットテストでは何もしないロガーを渡してください。
関心事が混在している場合は、リファクタリングが必要です。 抽出し、分離し、分類してください。後回しにせず、今すぐ実施してください。