外科手術とは
本物の外科手術には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];
}
これは単純なテキスト置換ではない。 構造全体が変わる:
- コールバック引数が削除される
- 関数が
asyncになる - ネストしたコールバックが
awaitになる - エラー処理が try/catch に変わる
- 戻り値のスタイルが完全に異なる
術式:
| 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
].
外科手術なし:
- Excelでチェックリスト作成(3000行)
- 15人の開発者に割り当て
- 2週間の手作業による書き換え
- コードレビューで不整合発覚
- エッジケースの見落としで本番バグ
外科手術あり:
- 文法 + 変換器を書く(2時間)
- スクリプト実行(30秒)
- diffを確認、エッジケースを修正
- 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;"
正規表現では確実に処理できない:
LIMIT offset, count→LIMIT count OFFSET offset(引数の順序入れ替え)- 文字列リテラル内の関数名(変更してはいけない)
- ネストした関数呼び出し
外科手術は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 | サポート言語に限定 |
| jscodeshift | JavaScriptのみ |
| Refaster | Javaのみ |
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入力:
| Entity | Field | Type | Required |
|---|---|---|---|
| User | id | integer | Y |
| User | name | string | Y |
| User | string | Y | |
| User | phone | string | N |
出力 (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.
なぜこれが重要か:
- 最小限の環境(Alpine、scratchコンテナ)にデプロイ可能
- AWS操作にPython/Nodeランタイム不要
- 単一バイナリで全てを処理
データパイプライン: 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 | ルールベースのコード変換 |
| Template | Mustacheテンプレートによるコード生成 |
| Json / Yaml / Csv | データ形式の読み書き |
| Http / Tcp | ネットワーク通信 |
| File | ファイルの読み書き、glob検索 |