サンドイッチとは何か?
2025-04-09 08:00:25
原文: What’s a sandwich? by Mark Seemann
これは食べ物の話に見えて、実はプログラミングについての話なのです。
「サンドイッチ」という名称は、この食べ物を好んだジョン・モンタギュー4世(サンドイッチ伯爵)に由来しています。よく知られる逸話によれば、彼はカードゲームをしながらでも手を汚さずに食事ができるという実用性からこれを気に入ったといわれています。
数年前、インターネット上で「サンドイッチの正確な定義とは何か」という善意の議論が巻き起こりました。例えば、デンマークの「スモーブロー」はサンドイッチと呼べるのでしょうか?スモーブローには二種類あります。ナイフとフォークでしか食べられない豪華な 「ホイトベラグト」 と、より日常的な 「ホンマ」(文字通り「手食」) です。後者は片面がオープンながらも、通常カトラリーなしで食べることができます。
サンドイッチ伯爵の動機を基準にするなら、写真にある 「ホイトベラグトなスモーブロー」 はサンドイッチとは言えないでしょう。一方で 「ホンマ」 はサンドイッチだという主張も十分成り立ちます。
当然ながら、「ホンマ」 は通常のサンドイッチとは違う持ち方をします。使われるパン( ルブロー )は小麦パンよりもずっと密度が高く、構造的にも硬いです。食べる際は親指と人差し指を両側に置き、残りの指でパンを下から支えます。重要なのは、具材をのせた一枚のパンでも、元々の問題(手を汚さずに食事する)は解決できるということです。
反対に考えてみましょう。パン、肉、パン、肉、パンという構成はどうでしょう?このようなバーガーを見かけたことがあるような気がします。これは片手で食べられるでしょうか?それはソースの垂れ具合や具材の量次第であって、構造によるものではないでしょう。
肉が5層、パンが6層あったらどうでしょう?西洋の一般的な発酵パンはフワフワしているため、薄切りにすると構造的に崩れやすく、おそらく機能しないでしょう。しかし、別の種類のパンや薄切りの肉(またはその他の「具材」)を使えば、なぜ機能しないのか見当がつきません。
関数型プログラミングにおけるサンドイッチ
常連の読者はご存知かもしれませんが、私は食べ物が好きです。しかしこれは結局のところ、プログラミングに関するブログです。
数年前、「Impureimサンドイッチ」という関数型プログラミングのデザインパターンを紹介しました。これは「関数型コア、命令型シェル」というアーキテクチャに基づいてコードベースを構築することが多くの場合有益だという考え方です。
簡単に言えば、あらゆるエントリーポイント(Mainメソッド、メッセージハンドラー、コントローラーアクションなど)で、まず純粋関数に必要な入力データを集めるための不純な操作をすべて実行し、次にその純粋関数(多くの小さな関数から構成されるかもしれません)を呼び出し、最後に関数の戻り値に基づいて一つ以上の不純な操作を実行するというものです。これが「不純/純粋/不純のサンドイッチ」です。
このパターンを使ってきた経験から、驚くほど頻繁に適用できることがわかりました。すべての場合ではありませんが、予想以上に多くの状況で役立ちます。
ただし時には、「サンドイッチ」 という言葉をもう少し柔軟に解釈する必要があります。
記事の例を詳しく見ると、実は標準的なサンドイッチではないことがわかります。まず、色分けしたHaskellの例を考えてみましょう:
tryAcceptComposition :: Reservation -> IO (Maybe Int) tryAcceptComposition reservation = runMaybeT $ liftIO (DB.readReservations connectionString $ date reservation) >>= MaybeT . return . flip (tryAccept 10) reservation >>= liftIO . DB.createReservation connectionString
date
関数は純粋なアクセサで、reservation
から日付と時刻を取得します。C#では、通常これは読み取り専用プロパティになります:
public async Task<IActionResult> Post(Reservation reservation) { return await Repository.ReadReservations(reservation.Date) .Select(rs => maîtreD.TryAccept(rs, reservation)) .SelectMany(m => m.Traverse(Repository.Create)) .Match(InternalServerError("Table unavailable"), Ok); }
C#のプロパティを関数と考えないかもしれません。結局のところ、それは単に言語の慣用的な書き方に過ぎないからです:
public DateTimeOffset Date { get; }
さらに、関数は入力を受け取り出力を返します。この場合、入力はどこにあるのでしょうか?
C#の読み取り専用プロパティは単にゲッターメソッドの糖衣構文に過ぎないことを思い出してください。JavaであればgetDate()
というメソッドになるでしょう。「関数の同型写像」により、インスタンスメソッドはオブジェクトを入力として受け取る関数と同型です:
public static DateTimeOffset GetDate(Reservation reservation)
言い換えれば、Date
プロパティはオブジェクト自体を入力として受け取り、DateTimeOffset
を出力として返す操作です。この操作には副作用がなく、同じ入力に対して常に同じ出力を返します。つまり、これは純粋な関数であり、だからこそ上のコード例では緑色で表示しているのです。
しかし、例で示される階層化は誤解を招くかもしれません。reservation.Date
の緑色はその下のSelect
式の緑色と隣接しています。これは、サンドイッチの純粋な中央部分が上部の不純な層に部分的に広がっているように見えるかもしれません。
しかし実際はそうではありません。reservation.Date
式はRepository.ReadReservations
の 前に 実行され、その後に純粋なSelect
式が実行されます。おそらく、次のようにコードを書くとサンドイッチの実態をより正確に表現できるでしょう:
public async Task<IActionResult> Post(Reservation reservation) { var date = reservation.Date; return await Repository.ReadReservations(date) .Select(rs => maîtreD.TryAccept(rs, reservation)) .SelectMany(m => m.Traverse(Repository.Create)) .Match(InternalServerError("Table unavailable"), Ok); }
対応する「サンドイッチ図」はこのようになります:
「サンドイッチ」 という言葉を狭く解釈するなら、これはもはやサンドイッチとは言えないでしょう。「具材」が上にもあるからです。だからこそ、デンマークの 「スモーブロー」 (「オープンサンドイッチ」 とも呼ばれる)の話から始めたのです。確かに、パンの間と上の両方に肉がある 「ホンマ」 は見たことがありません。一方で、上に少量の「具材」があることが問題になるとは思えません。
初期と最終の純粋性
なぜこれが重要なのでしょうか?reservation.Date
がサンドイッチの不純な最初の層の中にある純粋さの小さな光かどうかは、実際それほど気にしていません。結局のところ、私の関心は主に認知的負荷であり、上記のようにreservation.Date
式を別の行に抽出しても得られるものはほとんどありません。
私が興味を持つ理由は、多くの場合、最初のステップは入力の検証であり、その検証は純粋な関数の集合だからです。検証は純粋ではあるものの「解決済みの問題」であるため、明示的に認識するに値するほど重要なステップかもしれません。それは単なるプロパティゲッターではなく、バグが潜む可能性があるほど複雑なものになり得ます。
「関数型コア、命令型シェル」 アーキテクチャに従っていても、最初のステップが純粋な検証である場合が多いのです。
同様に、2番目の不純な段階で不純な操作を実行した後、最終的な薄い純粋な変換層を簡単に用意できます。実際、上のC#の例にはまさにその例があります:
public IActionResult Ok(int value)
{
return new OkActionResult(value);
}
public IActionResult InternalServerError(string msg)
{
return new InternalServerErrorActionResult(msg);
}
これらは、サンドイッチの最終的な変換として使用される2つの小さな純粋関数です:
public async Task<IActionResult> Post(Reservation reservation) { var date = reservation.Date; return await Repository.ReadReservations(date) .Select(rs => maîtreD.TryAccept(rs, reservation)) .SelectMany(m => m.Traverse(Repository.Create)) .Match(InternalServerError("Table unavailable"), Ok); }
一方で、Match
操作は緑色で描画したくありませんでした。なぜなら、それは本質的にTask
の継続であり、「タスク非同期プログラミングはIOの代用」と考えるなら、少なくとも疑わしく見るべきだからです。純粋かもしれませんが、おそらくそうではないでしょう。
それでも、以下のような「逆さまのサンドイッチ」構造が残ります:
これをまだサンドイッチと主張できるでしょうか?
メタファーの限界
この最新の展開はサンドイッチのメタファーを無理に引き伸ばしているように思えます。このメタファーを維持できるでしょうか、それとも崩壊するでしょうか?
少なくとも私には、これがアレゴリー(寓喩)の拡張限界であるべきだと思います。さらに層を追加すると「ダグウッドサンドイッチ」になります。これは明らかに実用性の低い代物です。
しかし、また怪しいメタファーに頼っているので、代わりに何が起きているのかを分析しましょう。
実際のところ、最初の(純粋な)検証ステップを回避することはほぼ不可能なようです。なぜでしょうか?検証を関数型コアに移して、検証なしで不純なステップを実行することはできないのでしょうか?
簡単に言えば 「できません」 。なぜなら、適切に実装された検証は実際には構文解析だからです。エントリーポイントでは、入力が意味をなすかどうかさえわからないのです。
より現実的な例が必要なので、拙著『脳に収まるコードの書き方』のサンプルコードベースに目を向けましょう。あるブログ記事では、C#での予約投稿のための実用的な検証の実装方法を紹介しています。
典型的なHTTP POST
リクエストには次のようなJSONドキュメントが含まれるでしょう:
{
"id": "bf4e84130dac451b9c94049da8ea8c17",
"at": "2024-11-07T20:30",
"email": "snomob@example.com",
"name": "Snow Moe Beal",
"quantity": 1
}
このような単純なリクエストを処理するために、システムは一連の不純な操作を実行する必要があります。その一つは、既存の予約についてデータストアに問い合わせることです。結局のところ、レストランにはその日の空きテーブルがないかもしれません。
どの日かと聞かれたら?良い質問です。データアクセスAPIには次のようなメソッドがあります:
Task<IReadOnlyCollection<Reservation>> ReadReservations(
int restaurantId, DateTime min, DateTime max);
必要な日付の範囲を示すために min
と max
の値を指定できます。その範囲をどう決めるのでしょうか?予約希望日が必要です。上記の例では、2024年11月7日の20:30です。幸い、データはそこにあり、理解可能です。
しかし、JSONなどのワイヤーフォーマットの制約により、日付は文字列として扱われます。その値は何でもあり得ます。十分に不正な形式であれば、何を問い合わせるべきかわからないため、データベースへの問い合わせという不純な操作さえ実行できません。
サンドイッチのメタファーを損なわないようにするなら、構文解析の責任を不純な操作に押し付けることもできるでしょうが、なぜ純粋に解決できる問題をわざわざ不純にするのでしょうか?
同様の議論が、最後の純粋な翻訳ステップについても逆方向に適用できます。
したがって、サンドイッチのメタファーの理想にぴったり合わない実装に行き詰まっているようです。このメタファーを放棄するのか、それとも保持すべきなのでしょうか?
階層化されたアプリケーションアーキテクチャの「層」は実際には層ではなく、垂直「スライス」も実際にはスライスではありません。「すべてのモデルは間違っているが、いくつかは有用である」。これはここでも当てはまると思います。コードを構造化する際には、依然として「Impureimサンドイッチ」を念頭に置くべきです:不純な操作をアプリケーションの境界(「コントローラー」)に限定し、不純な段階を初期と最終の2つだけにとどめ、他のすべてでは純粋な関数の使用を最大化します。純粋な実行のほとんどを2つの不純な段階の間に置きますが、現実的には、前に純粋な検証段階と、最後に薄い翻訳層が必要になるでしょう。
結論
食べ物のイメージが広く使われているにもかかわらず、関数型プログラミングアーキテクチャに関するこの記事では「ブリトー」への言及を避けてきました。代わりに、理想的な「Impureimサンドイッチ」と現実世界の実装の詳細との間の緊張関係を検討しています。入力検証や出力データへの変換などの課題に対処する場合、純粋性の薄い層をさらに1つか2つ追加するのが実用的です。
関数型アーキテクチャでは、純粋関数の割合を最大化したいものです。純粋なコードをさらに追加することはほとんど問題になりません。
しかし逆は当てはまりません。サンドイッチに不純なスライスをさらに追加することには慎重であるべきです。したがって、調整されたImpureimサンドイッチの定義では、不純な段階は最大でも2つですが、純粋なスライスは1つから3つまでとなるようです。