外科手術とは

本物の外科手術には3つの要素がある:

コードの外科手術も同じパターンに従う:

外科手術コードの外科手術
切開Grammarが切り取る場所を定義
切除Transformerが対象を削除・置換
縫合ギャップ保持 - マッチしないコードはそのまま
再建スキーマや仕様から新しいコードを生成

正規表現やシェルスクリプトの「雑な切り口」とは違う。外科手術は構造を理解する。メスと鉈の違いだ。

クイック例

print()console_log() に置換:

| grammar transformer source result |

grammar := Grammar from: '
    call: @func FUNC "(" @args ARGS ")"
    FUNC: /[a-zA-Z_][a-zA-Z0-9_]*/
    ARGS: /[^)]*/
    %ignore /\s+/
'.

transformer := Transformer new grammar: grammar.
transformer rule: 'call' do: [:m |
    | func args |
    func := m at: 'func'.
    args := m at: 'args'.
    (func = 'print')
        ifTrue: [ 'console_log(' , args , ')' ]
        ifFalse: [ func , '(' , args , ')' ]
].

source := 'x = print(hello); y = other(world); z = print(foo)'.
result := transformer transform: source.

"結果: x = console_log(hello); y = other(world); z = console_log(foo)"

コメント、文字列、マッチしないコードはそのまま残る。

正規表現の問題

// 正規表現: "user" を "account" に置換
s/user/account/g

// 結果: 壊れる
"username" → "accountname"     // 間違い!
"userAgent" → "accountAgent"   // 間違い!
// user についてのコメント → ...    // 間違い!

解決策

// 外科手術: 変数 "user" を "account" に置換

"username"   → "username"      // そのまま
"userAgent"  → "userAgent"     // そのまま
// コメント   → // コメント     // そのまま
user.login() → account.login() // 正しい!

実例: コールバックからasync/awaitへの移行

3000ファイルにコールバック形式の非同期コードがある:

function loadUser(id, callback) {
    db.query("SELECT * FROM users WHERE id = ?", [id], function(err, rows) {
        if (err) {
            callback(err, null);
            return;
        }
        callback(null, rows[0]);
    });
}

これをasync/awaitに移行する必要がある:

async function loadUser(id) {
    const rows = await db.query("SELECT * FROM users WHERE id = ?", [id]);
    return rows[0];
}

これは単純なテキスト置換ではない。 構造全体が変わる:

術式:

| grammar transformer |

grammar := Grammar from: '
    callback_func: "function" @name NAME "(" @params PARAMS "," "callback" ")" @body BODY
    NAME: /[a-zA-Z_][a-zA-Z0-9_]*/
    PARAMS: /[^,)]*/
    BODY: /{[^}]*}/
    %ignore /\s+/
'.

transformer := Transformer new grammar: grammar.
transformer rule: 'callback_func' do: [:m |
    | name params body |
    name := m at: 'name'.
    params := m at: 'params'.
    body := m at: 'body'.
    'async function ' , name , '(' , params , ') ' , body
].

外科手術なし:

  1. Excelでチェックリスト作成(3000行)
  2. 15人の開発者に割り当て
  3. 2週間の手作業による書き換え
  4. コードレビューで不整合発覚
  5. エッジケースの見落としで本番バグ

外科手術あり:

  1. 文法 + 変換器を書く(2時間)
  2. スクリプト実行(30秒)
  3. diffを確認、エッジケースを修正
  4. 1日で完了

SQLダイアレクト移行

500のストアドプロシージャをMySQLからPostgreSQLに移行:

変換前 (MySQL):

SELECT * FROM users LIMIT 10, 20;
IFNULL(name, 'Unknown')
DATE_FORMAT(created_at, '%Y-%m-%d')

変換後 (PostgreSQL):

SELECT * FROM users LIMIT 20 OFFSET 10;
COALESCE(name, 'Unknown')
TO_CHAR(created_at, 'YYYY-MM-DD')

術式:

| grammar source |

grammar := Grammar from: '
    limit: "LIMIT" " " @offset NUM "," " " @count NUM
    NUM: /[0-9]+/
'.

source := 'SELECT * FROM users LIMIT 10, 20;'.

Grammar replace: grammar in: source with: [:m |
    | offset count |
    offset := m at: 'offset'.
    count := m at: 'count'.
    'LIMIT ' , count , ' OFFSET ' , offset
].

"結果: SELECT * FROM users LIMIT 20 OFFSET 10;"

正規表現では確実に処理できない:

外科手術はSQLをパースし、ASTを理解し、正しく変換する。

API呼び出しに await を追加

変換前:

/* comment */ obj.fetch() and client.send() /* end */

変換後:

/* comment */ await obj.fetch() and await client.send() /* end */

術式:

| grammar source result |

grammar := Grammar from: '
    dotcall: RECV "." METHOD "()"
    RECV: /[a-z]+/
    METHOD: /[a-z]+/
    %ignore /./
'.

source := '/* comment */ obj.fetch() and client.send() /* end */'.
result := Grammar replace: grammar in: source with: [:m |
    'await ' , (m at: 'text')
].

result printNl.

コメントはそのまま。obj.fetch()client.send() だけがラップされる。

なぜ既存ツールではダメなのか

ツール制限
sed/awkテキストのみ、構文を理解しない
正規表現エッジケースで壊れる、文字列/コメントも変更
Semgrepサポート言語に限定
jscodeshiftJavaScriptのみ
RefasterJavaのみ

Lambda Smalltalk: あらゆる言語やフォーマットに対して自分で文法を定義できる。

コード生成

外科手術は「変換」だけではない。スキーマ、仕様書、ドキュメントから新しいコードを生成することもできる。

PostgreSQLスキーマ → Entityクラス

データベースに直接接続して、型安全なEntityを生成:

Module import: 'CodeGen'.  "snakeToPascal等のユーティリティを使用"

| conn schema |

"PostgreSQLに接続してスキーマを取得"
conn := Postgres connect: 'host=localhost dbname=myapp'.
schema := conn query: '
    SELECT table_name, column_name, data_type, is_nullable
    FROM information_schema.columns
    WHERE table_schema = ''public''
    ORDER BY table_name, ordinal_position
'.

"テーブルごとにグループ化してRust構造体を生成"
(schema groupBy: [:row | row at: 'table_name'])
    keysAndValuesDo: [:table :columns |
        | code data fields |

        "フィールド配列を構築"
        fields := columns collect: [:c |
            #{
                'name' -> (c at: 'column_name').
                'rust_type' -> (self pgToRust: (c at: 'data_type')
                                     nullable: (c at: 'is_nullable') = 'YES')
            }
        ].

        "テンプレートデータを構築"
        data := #{
            'struct_name' -> table snakeToPascal.
            'fields' -> fields
        }.

        code := Template render: '
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct {{struct_name}} {
{{#fields}}
    pub {{name}}: {{rust_type}},
{{/fields}}
}
' with: data.

        File write: ('src/entities/' , table , '.rs') content: code.
        ('Generated: ' , table , '.rs') printNl.
    ].

conn close.

出力 (users.rs):

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Users {
    pub id: i64,
    pub name: String,
    pub email: String,
    pub created_at: Option<DateTime<Utc>>,
}

ORMも、コード生成ツールも、設定ファイルも不要。接続して、クエリして、生成するだけ。

Excel設計書 → TypeScriptインターフェース

多くの現場では設計書がExcelで管理されている。Lambda Smalltalkは直接読み取れる:

Module import: 'CodeGen'.

| excel rows entities |

"Excel設計書を読み込み(1行目がヘッダー、辞書の配列で返る)"
excel := Excel load.
rows := excel read: 'docs/api-spec.xlsx'.

"エンティティ名でグループ化"
entities := rows groupBy: [:row | row at: 'Entity'].

"TypeScriptインターフェースを生成"
entities keysAndValuesDo: [:name :fieldRows |
    | code data fields |

    fields := fieldRows collect: [:f |
        | d |
        d := Dict new.
        d at: 'field' put: (f at: 'Field').
        d at: 'tsType' put: (self excelToTs: (f at: 'Type')).
        d at: 'required' put: ((f at: 'Required') = 'Y').
        d
    ].

    data := Dict new.
    data at: 'name' put: name.
    data at: 'fields' put: fields.

    code := Template render: '
export interface {{name}} {
{{#fields}}
    {{field}}{{^required}}?{{/required}}: {{tsType}};
{{/fields}}
}
' with: data.

    File write: ('src/types/' , name , '.ts') content: code.
].

Excel入力:

EntityFieldTypeRequired
UseridintegerY
UsernamestringY
UseremailstringY
UserphonestringN

出力 (User.ts):

export interface User {
    id: number;
    name: string;
    email: string;
    phone?: string;
}

OpenAPI仕様 → APIクライアント

| spec client |

"OpenAPI YAMLをパース"
spec := Yaml parse: (File read: 'api/openapi.yaml').

"各エンドポイントのクライアントメソッドを生成"
client := Template render: '
export class ApiClient {
    constructor(private baseUrl: string) {}

{{#endpoints}}
    async {{method}}{{operationId}}({{params}}): Promise<{{responseType}}> {
        const response = await fetch(
            `${this.baseUrl}{{path}}`,
            { method: "{{httpMethod}}" }
        );
        return response.json();
    }

{{/endpoints}}
}
' with: (self extractEndpoints: spec).

File write: 'src/api/client.ts' content: client.

なぜコード生成か?

手作業コード生成
仕様書からコピペ単一の情報源
タイポと不整合100%正確
仕様変更=手動更新スクリプト再実行
何時間もの単調作業数秒

名医はコードを変換するだけでなく、情報源からコードを生成する。

一括処理

ディレクトリ内の全ファイルを処理:

| files grammar |

grammar := Grammar from: '...'.

files := File glob: 'src/**/*.js'.
files do: [:path |
    | content result |
    content := File read: path.
    result := Grammar replace: grammar in: content with: [:m | ... ].
    File write: path content: result.
    ('処理完了: ' , path) printNl.
].

コードを超えて:データとネットワークの手術

名医はコードだけを操作するわけではない。データストリーム、クラウドリソース、ネットワークプロトコルも手術対象だ。

AWS S3をCLIなしで操作

HTTPとAWS Signature V4でS3に直接アクセス。CLIのインストールもSDKの依存も不要:

| accessKey secretKey region bucket |

accessKey := Env at: 'AWS_ACCESS_KEY_ID'.
secretKey := Env at: 'AWS_SECRET_ACCESS_KEY'.
region := 'ap-northeast-1'.
bucket := 'my-bucket'.

"AWS Signature V4を構築"
| date timestamp scope signingKey signature headers |
date := DateTime now format: '%Y%m%d'.
timestamp := DateTime now format: '%Y%m%dT%H%M%SZ'.

"署名キーを導出"
signingKey := Hmac sha256: date key: ('AWS4' , secretKey).
signingKey := Hmac sha256: region key: signingKey.
signingKey := Hmac sha256: 's3' key: signingKey.
signingKey := Hmac sha256: 'aws4_request' key: signingKey.

"正規リクエストを作成して署名"
| host url stringToSign |
host := bucket , '.s3.' , region , '.amazonaws.com'.
url := 'https://' , host , '/data/export.csv'.

stringToSign := 'AWS4-HMAC-SHA256\n' , timestamp , '\n' ,
    date , '/' , region , '/s3/aws4_request\n' ,
    (Sha2 sha256: canonicalRequest).

signature := Hmac sha256: stringToSign key: signingKey.

"署名済みヘッダーでリクエスト"
headers := #{
    'Authorization' -> ('AWS4-HMAC-SHA256 Credential=' , accessKey , '/' , date ,
        '/' , region , '/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=' , signature).
    'x-amz-date' -> timestamp.
    'Host' -> host
}.

| response |
response := Http get: url headers: headers.
response printNl.

なぜこれが重要か:

データパイプライン: CSV → 変換 → API

データファイルを処理して外部サービスにプッシュ:

| rows transformed |

"CSVを読み込んでパース"
rows := Csv parse: (File read: 'data/users.csv').

"変換: アクティブユーザーをフィルタ、メールを正規化"
transformed := (rows select: [:row | (row at: 'status') = 'active'])
    collect: [:row |
        #{
            'id' -> (row at: 'id').
            'email' -> (row at: 'email') asLowercase.
            'name' -> (row at: 'name').
            'imported_at' -> (DateTime now format: '%Y-%m-%dT%H:%M:%SZ')
        }
    ].

"APIにバッチでプッシュ"
| headers |
headers := #{
    'Content-Type' -> 'application/json'.
    'Authorization' -> ('Bearer ' , (Env at: 'API_TOKEN'))
}.

(transformed chunks: 100) do: [:batch |
    | payload response |
    payload := Json generate: batch.
    response := Http post: 'https://api.example.com/users/import'
                     body: payload
                     headers: headers.
    ('バッチインポート完了: ' , response) printNl.
].

('処理件数: ' , transformed size asString) printNl.

パイプライン:

CSVファイル → パース → フィルタ → 変換 → JSON → HTTP POST → 完了

Pandasなし。データエンジニアリングフレームワークなし。精密な切開のみ。

TCP: 生のHTTPクライアント

ワイヤー上で何が起きているかを理解したい時:

| sock request response |

"サーバーに接続"
sock := Tcp connect: 'httpbin.org' port: 80.

"生のHTTPリクエストを構築"
request := 'GET /json HTTP/1.1\r\n',
           'Host: httpbin.org\r\n',
           'User-Agent: Lambda-Smalltalk/1.0\r\n',
           'Accept: application/json\r\n',
           'Connection: close\r\n',
           '\r\n'.

"送信と受信"
sock send: request.
response := sock recv: 4096.
sock close.

"レスポンスをパース"
| lines headers body |
lines := response asString lines.
headers := lines copyFrom: 1 to: (lines indexOf: '').
body := lines copyFrom: (lines indexOf: '') + 1 to: lines size.

('ステータス: ' , (lines at: 1)) printNl.
('ボディ: ' , (body join: '\n')) printNl.

Redisクライアントを作ることも:

| redis |

redis := Tcp connect: 'localhost' port: 6379.

"PING"
redis send: '*1\r\n$4\r\nPING\r\n'.
(redis recvLine) printNl.  "=> +PONG"

"SET key value"
redis send: '*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n'.
(redis recvLine) printNl.  "=> +OK"

"GET key"
redis send: '*2\r\n$3\r\nGET\r\n$5\r\nmykey\r\n'.
(redis recvLine) printNl.  "=> $7"
(redis recvLine) printNl.  "=> myvalue"

redis close.

なぜ生のTCPか:

まとめ

このページで使ったツール:

クラス用途
Grammarパターン定義、構文認識パーシング
Transformerルールベースのコード変換
TemplateMustacheテンプレートによるコード生成
Json / Yaml / Csvデータ形式の読み書き
Http / Tcpネットワーク通信
Fileファイルの読み書き、glob検索