mcpField
F
functional-core-imperative-shell

functional-core-imperative-shell

github.com/ed3dai開発253183 回インストール7 週間前に更新
リポジトリ

functional-core-imperative-shell について

Functional Core, Imperative Shell(FCIS)は、アプリケーションコードの設計、レビュー、リファクタリングを支援するスキルです。純粋な業務ロジックと I/O の調整を分け、実行時コードを含むファイルに分類を付けます。

主な機能

  1. 01純粋なロジックと I/O の分離
  2. 02実行時コードを含むファイルの分類
  3. 03収集・処理・永続化の流れ
  4. 04混在したロジックの分割方法

活用シーン

  1. 01新しいファイルのコード構成
  2. 02データベースやファイル操作を含む処理の分割
  3. 03ユニットテストの複雑なモックの削減
  4. 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 つのルール:

  1. Functional Core: 純粋関数のみ。ロギング以外の I/O は行いません。テストが容易です。
  2. Imperative Shell: I/O の調整のみ。ロジックは最小限にします。Core を呼び出します。
  3. 実行時の動作があるすべてのファイルを分類する。 型のみのファイル、定数、バレル、テスト、生成されたファイルは対象外です。

迷った場合: 外部依存なしで実行できますか? → Functional Core。それ以外は Imperative Shell です。

ロギングの例外: ロガーはどこでも使用できます。ユニットテストでは何もしないロガーを渡してください。

関心事が混在している場合は、リファクタリングが必要です。 抽出し、分離し、分類してください。後回しにせず、今すぐ実施してください。

コメント