The best days are ahead of us.

レストランのサンドイッチ

2025-04-09 10:00:25

原文: A restaurant sandwich by Mark Seemann

C#におけるImpureimサンドイッチの例

関数型プログラミング(FP)を学ぶ際、コードをどう構成すればよいか、多くの人が悩みます。純粋性をどのように見極め、保つか?FPで依存関係の注入はどう実現するのか?関数型アーキテクチャとは、具体的にはどのようなものでしょうか?

関数型プログラミングでよく用いられるデザインパターンの一つに、Impureimサンドイッチがあります。アプリケーションのエントリーポイントは必然的に副作用を伴う(純粋ではない)ため、副作用のある操作(不純な操作)はすべてシステムの境界部分に寄せ集める、という考え方です。これは関数型コア、命令型シェル (functional core, imperative shell) とも呼ばれます。マイクロオペレーションベースのアーキテクチャ(Webベースのシステムはすべてこれに含まれます)を採用している場合、「サンドイッチ」構造が有効なことが多いです。まず副作用のある操作を実行して必要なデータをすべて集め、次にそのデータを純粋な関数に渡します。最後に、純粋な関数から得られた参照透過な戻り値を処理するために、再び副作用のある操作を使います。

どのようなデザインパターンも万能ではなく、このパターンも例外ではありません。しかし私の経験上、このアーキテクチャを適用できるケースは驚くほど多く、パレートの法則でいう80%をはるかに超えています。

例を使うことで、パターンの理解が深まり、その適用範囲を探ることにも役立ちます。この記事では、REST APIのエントリーポイント、具体的には書籍『脳に収まるコードの書き方』に付属するサンプルコードベースのPUTハンドラをリファクタリングした過程を紹介します。

出発点

この本で述べたように、サンプルコードベースのアーキテクチャは、実際には「関数型コア、命令型シェル」です。しかし、これは本の主要なテーマではなく、コードも明示的にImpureimサンドイッチを適用しているわけではありません。考え方としてはそのパターンに従っていますが、コードを見ただけではそれは分かりにくいでしょう。これは、他のソフトウェアエンジニアリングの実践に焦点を当てたかったため、私が意図的に選択した設計です。結果として、Impureimサンドイッチの構造は見えにくくなっています。

例えば、本書では80/24ルール(1メソッド80文字x24行以内)を厳密に守っています。これは教育的な意図からの選択でした。現実のコードベースではメソッドが大きすぎるものが多いため、小さなコードブロックでも複雑なコードベースを開発・保守できるという点を強調したかったのです。しかしその結果、HTTPリクエストハンドラ(ASP.NETではControllerのアクションメソッドと呼ばれる)を分割する必要がありました。

最も複雑なHTTPハンドラは、予約に対するPUTリクエストを処理するものです。クライアントは、レストランの予約を変更したい場合にこのアクションを使用します。

HTTPリクエストによって実際に呼び出されるアクションメソッドは、以下のPutメソッドです:

[HttpPut("restaurants/{restaurantId}/reservations/{id}")]
public async Task<ActionResult> Put(
    int restaurantId,
    string id,
    ReservationDto dto)
{
    if (dto is null)
        throw new ArgumentNullException(nameof(dto));
    if (!Guid.TryParse(id, out var rid))
        return new NotFoundResult();
 
    Reservation? reservation = dto.Validate(rid);
    if (reservation is null)
        return new BadRequestResult();
 
    var restaurant = await RestaurantDatabase
        .GetRestaurant(restaurantId).ConfigureAwait(false);
    if (restaurant is null)
        return new NotFoundResult();
 
    return
        await TryUpdate(restaurant, reservation).ConfigureAwait(false);
}

教育的な理由から各メソッドを80x24の枠内に収めたかったため、やや不自然な設計上の選択をいくつか行いました。上記のコードもその一つです。完全に擁護できないとは思いませんが、このメソッドはまず入力の検証と確認を少し行い、その後の処理をTryUpdateメソッドに委ねています。

このPutメソッドがTryUpdateを呼び出す唯一の箇所であることを考えると、これはあまり良い設計とは言えないかもしれません。TryUpdateでも同様のことが起こっています。これもまた、呼び出し元が一つしかないメソッドを呼び出しています。これら二つのメソッドをインライン化して、Impureimサンドイッチの構造が見えるようになるか試してみましょう。

インライン化されたトランザクションスクリプト

これら二つのメソッドをインライン化すると、より大きなトランザクションスクリプト風のエントリーポイントになります:

[HttpPut("restaurants/{restaurantId}/reservations/{id}")]
public async Task<ActionResult> Put(
    int restaurantId,
    string id,
    ReservationDto dto)
{
    if (dto is null)
        throw new ArgumentNullException(nameof(dto));
    if (!Guid.TryParse(id, out var rid))
        return new NotFoundResult();
 
    Reservation? reservation = dto.Validate(rid);
    if (reservation is null)
        return new BadRequestResult();
 
    var restaurant = await RestaurantDatabase
        .GetRestaurant(restaurantId).ConfigureAwait(false);
    if (restaurant is null)
        return new NotFoundResult();
 
    using var scope = new TransactionScope(
        TransactionScopeAsyncFlowOption.Enabled);
 
    var existing = await Repository
        .ReadReservation(restaurant.Id, reservation.Id)
        .ConfigureAwait(false);
    if (existing is null)
        return new NotFoundResult();
 
    var reservations = await Repository
        .ReadReservations(restaurant.Id, reservation.At)
        .ConfigureAwait(false);
    reservations =
        reservations.Where(r => r.Id != reservation.Id).ToList();
    var now = Clock.GetCurrentDateTime();
    var ok = restaurant.MaitreD.WillAccept(
        now,
        reservations,
        reservation);
    if (!ok)
        return NoTables500InternalServerError();
 
    await Repository.Update(restaurant.Id, reservation)
        .ConfigureAwait(false);
 
    scope.Complete();
 
    return new OkObjectResult(reservation.ToDto());
}

実際のコードベースではもっと長いメソッドを見かけることもありますが、このバージョンでもすでに大きすぎて私のラップトップ画面には収まりません。全体を読むには上下にスクロールする必要があります。メソッドの下部を見ているときには上部の内容はもう見えず、記憶しておかなければなりません。

『脳に収まるコードの書き方』の要点の一つは、プログラマの生産性を制限するのは人間の認知能力だということです。メソッド全体を一度に見られずにスクロールが必要なら、それは「頭に入る」コードでしょうか?おそらく、そうではないでしょう。

さて、このコードからImpureimサンドイッチの構造が見えるでしょうか?

見えなくても無理はありません。コードには細かな判定処理がいくつも含まれているため、明確ではありません。例えば、以下の判定は参照透過であると言えるかもしれません:

if (existing is null)
    return new NotFoundResult();

これら2行のコードは決定論的で副作用がありません。この分岐はexisting is nullの場合にのみNotFoundResultを返します。さらに、これらのコード行の前後には副作用のある操作(不純な操作)があります。これがサンドイッチなのでしょうか?

いいえ、違います。これは典型的な命令型コードの形です。別の記事の図を借りるなら、純粋なコードと不純なコードが規律なく混在している状態です:

ほとんどが不純な(赤色)コードで、純粋なコードを表す緑色の縦縞がある箱。

それでも、上記のPutメソッドは「関数型コア、命令型シェル」アーキテクチャを実装しています。Putメソッドが「命令型シェル」ですが、「関数型コア」はどこにあるのでしょうか?

シェルの視点

認識すべき重要な点は、命令型シェルのコードからは、関数型コアはほとんど見えないということです。なぜなら、それは通常、単なる関数呼び出しの一つに過ぎないからです。

上記のPutメソッドでは、これが関数型コアにあたります:

var ok = restaurant.MaitreD.WillAccept(
    now,
    reservations,
    reservation);
if (!ok)
    return NoTables500InternalServerError();

わずか数行のコードであり、1行80文字という制約がなければ、次のように配置してokフラグをインライン化することもできたでしょう:

if (!restaurant.MaitreD.WillAccept(now, reservations, reservation))
    return NoTables500InternalServerError();

試してみると、実際にはこれでも80文字以内に収まります。正直なところ、なぜ以前のコードの代わりにこちらを使わなかったのかは分かりません。おそらく後者の選択肢が密度が高すぎると感じたか、単に思いつかなかったのでしょう。コードは滅多に完璧ではありません。通常、しばらく離れていたコードを見直すと、変更したい点が見つかるものです。

いずれにせよ、それは本題ではありません。ここで重要なのは、命令型シェルのコードを見ると、関数型コアは取るに足らないように見える、ということです。一瞬見逃してしまうほど小さいものです。他の小さな純粋な判定(if文など)をすべて無視し、すでにImpureimサンドイッチが存在すると仮定しても、この視点からはアーキテクチャはこのように見えます:

上部に大きな赤いセクション、中央に薄い緑のスライバー、下部に別の大きな赤いパートがある箱。

ここで疑問に思うかもしれません。「何をそんなに大げさに言っているのか?」「なぜ気にする必要があるのか?」と。

これはコードを読む人にとっては、ごく自然な流れです。結局のところ、コードベースをよく知らない場合、アプリケーションが特定の刺激(HTTPのPUTリクエストなど)をどのように処理するかを理解するには、エントリーポイントから読み始めることが多いでしょう。そうすると、関数型コアのコードを見る前に、命令型シェルのコードをすべて目にすることになります。これにより、責任範囲のバランスについて誤った印象を与えかねません。

結局のところ、上記のPutメソッドのようなコードは、副作用のあるコード(不純なコード)のほとんどをインライン化して目の前に提示しています。確かに、Repository.ReadReservationsなどの背後にはまだコードが隠れていますが、命令型コードのかなりの部分がメソッド内に直接書かれているのが見えます。

一方、関数型コアは、単なる関数呼び出し一つです。仮にその関数の中身もすべてインライン化したとすると、図はむしろこのようになるかもしれません:

上部に薄い赤いスライス、中央に厚い緑の部分、下部に薄い赤いスライスがある箱。

これは明らかに、純粋なコードと命令型コードの実際の比率次第です。いずれにせよ、純粋なコードのインライン化はあくまで思考実験です。関数型アーキテクチャの要点は、参照透過な関数は頭に入るということだからです。MaitreD.WillAccept関数の背後にどれほど複雑で大量のコードが隠れていようとも、その戻り値は関数呼び出しそのものと等価なのです。これは究極の抽象化と言えるでしょう。

標準コンビネータ

すでに示唆したように、インライン化されたPutメソッドはトランザクションスクリプトのように見えます。幸い、循環的複雑度マジックナンバー7±2程度であり、分岐はもっぱらガード節として整理されています。それ以外に、ネストしたif文やforループはありません。

ガード節を除けば、これはほとんど上から下へまっすぐに実行される手続きのように見えます。例外は、処理が途中で終了する可能性のある小さな条件文です。例えば、次のような条件です:

if (!Guid.TryParse(id, out var rid))
    return new NotFoundResult();

あるいは

if (reservation is null)
    return new BadRequestResult();

このようなチェックがメソッド全体に散らばっています。これらはそれぞれ、命令型コードの中にある小さな純粋な部分(島)ですが、アドホックなものです。各チェックは処理を続行できるか確認し、できない場合は何らかのエラー値を返します。

このような「主要フローからの逸脱」をモデル化する方法はあるでしょうか?

Scott WlaschinのRailway Oriented Programming (鉄道指向プログラミング)に触れたことがあるなら、この議論がどこに向かっているか、もうお分かりかもしれません。鉄道指向プログラミングは優れたメタファーです。主線がありつつも、一部の列車を振り分ける側線もある、という情景を思い浮かべることができます。そして、一度側線に入った列車は主線には戻れません。

これがEitherモナドの仕組みです。アドホックなif文はすべて、「標準コンビネータ」と呼べるもので置き換えられるはずです。これらのコンビネータの中で最も重要なのがモナディックバインドです。Putのようなトランザクションスクリプトを標準コンビネータで構成すると、これらの細かな判定処理が隠蔽され、サンドイッチ構造がより明確になります。

純粋なコードだけなら、Either値を返す関数を単純に合成できたでしょう。残念ながら、Putメソッドで起こることの多くはTaskベースのコンテキストで発生します。幸い、Eitherはうまくネストできるモナドの一つであり、組み合わせをTaskEitherモナドに変換できます。リンク先の記事はTaskEitherSelectMany実装の核心部分を示しています。

「主線」と「側線」のどちらに進むかという細かな判定をエンコードするには、「裸の」値を目的のTask<Either<L, R>>コンテナでラップします:

Task.FromResult(id.TryParseGuid().OnNull((ActionResult)new NotFoundResult()))

このコードスニペットでは、いくつか導入が必要な小さな部品を利用しています。まず、.NET標準のTryParse APIは合成できません。しかし、これらはMaybe値を返す関数と同型なので、次のようなアダプターを書けます:

public static Guid? TryParseGuid(this string candidate)
{
    if (Guid.TryParse(candidate, out var guid))
        return guid;
    else
        return null;
}

このコードベースでは、Null許容参照型Maybeモナドと同等に扱います。もし言語にその機能がなければ、代わりにMaybeを使えます。

しかし、Putメソッドを実装するには、Null許容(またはMaybe)値ではなく、Either値が必要です。そこで自然変換を導入できます:

public static Either<L, R> OnNull<L, R>(this R? candidate, L left) where R : struct
{
    if (candidate.HasValue)
        return Right<L, R>(candidate.Value);
 
    return Left<L, R>(left);
}

Haskellでは、組み込みのMaybeカタモーフィズムを利用するでしょう:

ghci> maybe (Left "boo!") Right $ Just 123
Right 123
ghci> maybe (Left "boo!") Right $ Nothing
Left "boo!"

MaybeからEitherへのこのような変換はFairbairnしきい値(専用の抽象化を用意する価値があるかの閾値)の是非が問われるところですが、複数回必要になるため、C#コードベースに特化したOnNull変換を追加するのは合理的です。ここで示したものはNull許容値型を処理しますが、コードベースにはNull許容参照型を処理するオーバーロードも含まれており、内容はほぼ同じです。

クエリ構文のサポート

C#でモナド値を扱う方法は複数あります。多くのC#開発者はLINQを好みますが、ほとんどの人はより馴染み深いメソッド呼び出し構文の方を好むようです。つまり、SelectSelectManyWhereメソッドを通常の拡張メソッドとして直接呼び出す方法です。もう一つの選択肢は、クエリ構文を使うことです。ここではImpureimサンドイッチを見つけやすくするため、後者を目指します。

サンドイッチ全体のコードは後ほど示します。その前に、いくつか詳細を抜き出して実装方法を説明します。もしよければ、先に最終結果までスクロールして、後でここに戻ってきても構いません。

サンドイッチ処理は、まず上記の部品を使ってidをGUIDにパースすることから始まります:

var sandwich =
    from rid in Task.FromResult(id.TryParseGuid().OnNull((ActionResult)new NotFoundResult()))

その直後にValidate(実際にはparse)を実行し、dtoを適切なドメインモデルに変換します:

from reservation in dto.Validate(rid).OnNull((ActionResult)new BadRequestResult())

2番目のfrom句が結果をTask.FromResultでラップしていない点に注目してください。これはどうして可能なのでしょうか? dto.Validateの戻り値はすでにTaskなのでしょうか? いいえ、これは「縮退」したSelectManyオーバーロードを追加したことで機能します:

public static Task<Either<L, R1>> SelectMany<L, R, R1>(
    this Task<Either<L, R>> source,
    Func<R, Either<L, R1>> selector)
{
    return source.SelectMany(x => Task.FromResult(selector(x)));
}
 
public static Task<Either<L, R1>> SelectMany<L, U, R, R1>(
    this Task<Either<L, R>> source,
    Func<R, Either<L, U>> k,
    Func<R, U, R1> s)
{
    return source.SelectMany(x => k(x).Select(y => s(x, y)));
}

selectorTask<Either<L, R1>>値ではなくEither<L, R1>値のみを生成する点に注目してください。これにより、クエリ構文は前の値(rid、実際にはTask<Either<ActionResult, Guid>>)を引き継ぎ、TaskではなくEither値だけを生成する関数で処理を続けられます。これら2つのオーバーロードのうち、最初のものはそのEither値を受け取ってTask.FromResultでラップします。2番目のオーバーロードは、クエリ構文を有効にするための、よくある定型的なコードにすぎません。

では、なぜsandwichの処理ではridに同じトリックを使用しないのでしょうか? なぜ明示的にTask.FromResultを呼び出しているのでしょうか?

私の理解では、これは型推論が理由です。C#コンパイラは最初の式からモナドの型を推論するようです。最初の式を次のように変更すると:

from rid in id.TryParseGuid().OnNull((ActionResult)new NotFoundResult())

コンパイラはクエリ式がTask<Either<L, R>>ではなくEither<L, R>に基づいていると解釈してしまいます。これは、最初のTask値に遭遇した時点で、式全体が機能しなくなることを意味します。

最初の式を明示的にTaskでラップすることで、コンパイラはこちらが意図するモナドを正しく推論してくれます。もっと洗練された方法があるかもしれませんが、私にはわかりません。

失敗しない値

サンドイッチ処理は、おなじみのOnNullコンビネータを使い、Null許容値をEither値に変換しながら、様々なデータベースにクエリを発行していきます。

from restaurant in RestaurantDatabase
    .GetRestaurant(restaurantId)
    .OnNull((ActionResult)new NotFoundResult())
from existing in Repository
    .ReadReservation(restaurant.Id, reservation.Id)
    .OnNull((ActionResult)new NotFoundResult())

これは先ほどと同様に機能します。GetRestaurantReadReservationはどちらも値を取得できない可能性があるクエリだからです。ReadReservationのインターフェース定義は次のようになっています:

Task<Reservation?> ReadReservation(int restaurantId, Guid id);

結果がnullになる可能性を示す?(疑問符)に注目してください。

GetRestaurantメソッドも同様です。

しかし、サンドイッチ処理が次に実行するクエリは少し異なります。ReadReservationsメソッドの戻り値の型はTask<IReadOnlyCollection<Reservation>>です。Taskが内包する型がNull許容ではない点に注目してください。データベース接続エラーを除けば、このクエリは失敗しません。データが見つからなければ、空のコレクションを返します。

値がNull許容ではないため、OnNullを使ってTask<Either<L, R>>値に変換できません。代わりにRight作成関数を使えます。

public static Either<L, R> Right<L, R>(R right)
{
    return Either<L, R>.Right(right);
}

これでも機能しますが、少々扱いにくいです:

from reservations in Repository
    .ReadReservations(restaurant.Id, reservation.At)
    .Traverse(rs => Either.Right<ActionResult, IReadOnlyCollection<Reservation>>(rs))

Either.Rightを呼び出す際の問題点は、コンパイラがRにどの型を使用するかを推論できても、L型が何かを知らないことです。そのため、それをコンパイラに教える必要がありますが、Rも一緒に指定しないとLが何か教えられません。たとえそれが既に分かっている場合でもです。

このような場合、F#コンパイラは通常うまく解釈してくれますし、GHCは常に理解できます(コードに特殊な言語拡張を追加しない限り)。C#には、コンパイラが知らない型についてのみ伝え、残りを推論させるという構文がありません。

しかし、諦めるのはまだ早いです。このような場合に使える、ちょっとしたトリックがあります。C#コンパイラにR型を推論させつつ、Lが何かだけを伝えることができます。これは2段階の手順で行います。まず、Rに対して拡張メソッドを定義します:

public static RightBuilder<R> ToRight<R>(this R right)
{
    return new RightBuilder<R>(right);
}

このToRightメソッドの型引数はRだけであり、またrightパラメータがR型なので、C#コンパイラは常にrightの型からRの型を推論できます。

RightBuilder<R>とは何でしょうか?これは小さなヘルパークラスです:

public sealed class RightBuilder<R>
{
    private readonly R right;
 
    public RightBuilder(R right)
    {
        this.right = right;
    }
 
    public Either<L, R> WithLeft<L>()
    {
        return Either.Right<L, R>(right);
    }
}

『脳に収まるコードの書き方』のコードベースは.NET 3.1で書かれましたが、現在ならこれをレコードにすることもできるでしょう。このクラスの唯一の目的は、型推論を2段階に分け、R型を自動的に推論できるようにすることです。こうすることで、L型が何かをコンパイラに伝えるだけで済みます。

from reservations in Repository
    .ReadReservations(restaurant.Id, reservation.At)
    .Traverse(rs => rs.ToRight().WithLeft<ActionResult>())

ご覧のように、このプログラミングスタイル自体は特定の言語に依存しません。このちょっとしたトリックを巧妙だと感じるかもしれませんが、本来はコンパイラ自身に推論させる方がずっと望ましいでしょう。sandwichクエリ式全体は既にTask<Either<ActionResult, R>>を扱うように定義されており、L型はR型のように途中で変えることはできません。関数型言語のコンパイラならこれを理解できます。この記事では、オブジェクト指向プログラマに関数型プログラミングがどのように機能するかを示したいと考えていますが、C#でこのようなコードを書くことが良いアイデアだと考えてほしいわけではありません。その点については既に別の記事で触れています

驚くには値しませんが、ToLeft/WithRightの組み合わせについても同様のものが存在します。

コマンドの扱い

Putメソッドの最終的な目的は、データベースの行を変更することです。それを実行するメソッドのインターフェース定義は次のようになっています:

Task Update(int restaurantId, Reservation reservation);

ジェネリックではないTaskクラスは、「非同期void」と説明すると非C#プログラマにも分かりやすいでしょう。Updateメソッドは非同期のコマンドです。

TaskvoidはLINQクエリ構文では使えないため、この制約に対処する方法を見つける必要があります。今回は、クエリ式の一部に見せかけるために、ローカルヘルパーメソッドを定義しました:

async Task<Reservation> RunUpdate(int restaurantId, Reservation reservation, TransactionScope scope)
{
    await Repository.Update(restaurantId, reservation).ConfigureAwait(false);
    scope.Complete();
    return reservation;
}

これはUpdateが完了した際に、引数のreservationパラメータをそのまま返すだけのものです。これにより、より大きなクエリ式の中で組み合わせることが可能になります。

F#とHaskellでは、どちらもこの状況をもっとスマートに扱え、このような回避策は不要であると言っても、おそらく驚かないでしょう。

完全なサンドイッチ

これで必要な部品はすべて揃いました。以下に、Impureimサンドイッチの例と同様に色分けしたsandwich定義の全体を示します。

Task<Either<ActionResultOkObjectResult>> sandwich =
    from rid in Task.FromResult(
        id.TryParseGuid().OnNull((ActionResult)new NotFoundResult()))
    from reservation in
        dto.Validate(rid).OnNull(
            (ActionResult)new BadRequestResult())
 
    from restaurant in RestaurantDatabase
            .GetRestaurant(restaurantId)
        .OnNull((ActionResult)new NotFoundResult())
    from existing in Repository
        .ReadReservation(restaurant.Id, reservation.Id)
        .OnNull((ActionResult)new NotFoundResult())
    from reservations in Repository
        .ReadReservations(restaurant.Id, reservation.At)
        .Traverse(rs => rs.ToRight().WithLeft<ActionResult>())
    let now = Clock.GetCurrentDateTime()
 
    let reservations2 =
            reservations.Where(r => r.Id != reservation.Id)
    let ok = restaurant.MaitreD.WillAccept(
        now,
        reservations2,
        reservation)
    from reservation2 in
        ok 
            ? reservation.ToRight().WithLeft<ActionResult>()
            : NoTables500InternalServerError().ToLeft().WithRight<Reservation>()
 
    from reservation3 in 
        RunUpdate(restaurant.Id, reservation2, scope)
        .Traverse(r => r.ToRight().WithLeft<ActionResult>())
    select new OkObjectResult(reservation3.ToDto());

色分けからも明らかなように、これは完全なサンドイッチ構造ではありません。構造は、むしろ次のように描くのがより正確でしょう:

緑、赤、緑、赤の水平層を持つボックス。

以前にも述べたように、このメタファーには無理がありますが、これは関数型プログラミングのアーキテクチャとしてはうまく機能します

ここで定義したsandwichは、awaitする必要があるTaskです。

Either<ActionResult, OkObjectResult> either = await sandwich.ConfigureAwait(false);
return either.Match(x => x, x => x);

このタスクをawaitすると、Either値が得られます。一方で、PutメソッドはActionResultを返す必要があります。このEitherオブジェクトを単一のオブジェクトに変換するにはどうすればよいでしょうか?

コードスニペットに示されているように、パターンマッチングを使います。L型(Left側)は既にActionResultなので、そのまま変更せずに返します。C#に組み込みの恒等関数(identity function)があればそれを使えますが、慣用的にはx => xというラムダ式を使います。

R型(Right側)についても同様です。OkObjectResultActionResultを継承しているためです。恒等式(x => x)が自動的に型変換を行ってくれます。

ところで、これはどの言語でも見られる、Either値を扱う際の典型的なパターンです。基本的には、両側が同じ型であるEither<T, T>を計算し、最終的にどちらかの側に含まれるT型の値をそのまま返したい、という状況です。これは非常に一般的なパターンなので、Haskellには気の利いた抽象化があると思うかもしれませんが、Hoogleでさえ、これを行うための一般的に受け入れられている関数を提案できません。どうやら、either id idもFairbairnしきい値以下(つまり、わざわざ専用の関数を作るまでもない)と見なされているようです。

結論

この記事では、単純ではないImpureimサンドイッチの例を紹介しました。このパターンを紹介した際には、いくつかの例を示しました。その際は、考え方の構造を際立たせるために、意図的にシンプルな例を選びました。しかし、そのような教育的な配慮には、例が単純すぎると感じる読者もいるという欠点がありました。そのため、より複雑な例を検討することには価値があると考えます。

『脳に収まるコードの書き方』に付属するコードベースは、現実的な複雑さに近いものです。これは意図的にそのように書かれており、また本の読者にはこのコードベースが馴染み深いと想定されるため、Impureimサンドイッチがどのように見えるかを示す上で良い題材になると考えました。コードベースの中で最も複雑な処理であることから、今回は特にPutメソッドをリファクタリング対象として選びました。

このコードベースの利点は、多くの読者にとって馴染みのあるプログラミング言語で書かれている点です。そのため、関数型プログラミングに興味のある読者にとって、これはいくつかの中級レベルの概念を知る良い機会にもなるだろうと考えました。

途中で述べたように、このようなコードを実運用のC#で書くことを推奨するわけではありません。もしこのように書いても問題ない状況なら、このプログラミングパラダイムにより適した言語を使うべきでしょう。


インデックスへ戻る