Build. Translate. Understand.

部分適用は依存関係の注入である

2025-06-17 10:00:25

原文: Partial application is dependency injection by Mark Seemann

F#における依存関係の注入に相当するのは部分関数適用ですが、それは関数型的ではありません。

この記事は、依存関係の注入から依存関係の排除へという短い連載記事の2番目の記事です。

F#で依存関係の注入をどのように行うのか、とよく質問されます。私が数年前にDependency Injection in .NETを執筆し、また近年F#や他の関数型プログラミング言語にますます力を注いでいることを考えれば、それはごく自然なことでしょう。

長年にわたり、他のF#のエキスパートがその質問に答えるのを見てきましたが、その答えはしばしば、部分関数適用がF#流の依存関係の注入の方法である、というものでした。私も数年間はそのように考えていました。それはある意味では正しく、また別の意味では正しくないことが分かりました。部分適用は、依存関係の注入と等価です。ただ、それは依存関係を扱うための関数型的な解決策ではないのです。

(誤解のないようにはっきりさせておきますが、私は部分適用が関数型的でないと主張しているわけではありません。私が主張しているのは、依存関係の注入のために使われる部分適用は関数型的ではない、ということです。)

関数を使った依存関係の注入の試み

前の記事の例に戻り、MaitreD.TryAcceptを関数として書き直してみましょう。

// int -> (DateTimeOffset -> Reservation list) -> (Reservation -> int) -> Reservation
// -> int option
let tryAccept capacity readReservations createReservation reservation =
    let reservedSeats =
        readReservations reservation.Date |> List.sumBy (fun x -> x.Quantity)
    if reservedSeats + reservation.Quantity <= capacity
    then createReservation { reservation with IsAccepted = true } |> Some
    else None

例をできるだけ等価に保つために、このtryAccept関数はMaitreDというモジュールの一部だと考えてください。

この関数は4つの引数を取ります。1つ目は、対象となるレストランの収容人数を表すプリミティブな整数です。次の2つの引数、readReservationscreateReservationは、前の記事で注入されたIReservationsRepositoryの役割を果たします。オブジェクト指向の例では、TryAcceptメソッドはリポジトリ上の2つのメソッド、ReadReservationsCreateを使用していました。F#の関数ではインターフェースを使う代わりに、2つの独立した関数を受け取るようにしています。これらの型は、C#の対応するものと(ほぼ)同じです。

最初の3つの引数が、前のMaitreDクラスで注入された依存関係に対応します。4つ目の引数はReservation型の値で、これは前のTryAcceptメソッドへの入力に対応します。

このF#バージョンは、null許容整数を返す代わりに、int optionを返します。

実装もC#の例と等価です。readReservations関数の引数を使ってデータベースから関連する予約を読み込み、その数量を合計します。すでに予約されている席数に基づいて、予約を受け入れるかどうかを決定します。予約を受け入れられる場合は、IsAcceptedtrueに設定し、createReservation関数の引数を呼び出し、返されたID(整数)をSomeにパイプします。予約を受け入れられない場合は、Noneを返します。

最初の3つの引数が「依存関係」であり、最後の引数が、いわば「実際の入力」であることに注目してください。これは、部分関数適用を使ってこの関数を合成できることを意味します。

適用

前のIMaitreDインターフェースの定義を思い出してみると、TryAcceptメソッドは次のように定義されていました(C#コードスニペット)。

int? TryAccept(Reservation reservation);

Reservation -> int optionという型を持つ、同様の関数を定義してみましょう。通常はアプリケーションの境界に近い部分でこれを行いたいところですが、次の例では、実際のデータベース操作を関数に「注入」する方法を示します。

DBというモジュールに、次のような関数があると想像してみてください。

module DB =
    // string -> DateTimeOffset -> Reservation list
    let readReservations connectionString date = // ..
    // string -> Reservation -> int
    let createReservation connectionString reservation = // ..

readReservations関数は、接続文字列と日付を引数に取り、その日付の予約リストを返します。createReservation関数もまた、接続文字列と予約情報を引数に取ります。呼び出されると、その予約の新しいレコードを作成し、新しく作成された行のIDを返します。(この種のAPIはCQSに違反するため、代替案を検討するべきです。)

これらの関数に有効な接続文字列を部分適用すると、両方ともtryAccept内での役割に求められる型を持ちます。つまり、これらの要素から関数を作成できるということです。

// Reservation -> int option
let tryAcceptComposition =
    let read   = DB.readReservations  connectionString
    let create = DB.createReservation connectionString
    tryAccept 10 read create

tryAccept自体がどのように部分適用されているかに注目してください。C#の依存関係に対応する引数だけが渡されているため、戻り値は、最後の引数、つまり予約情報を「待っている」関数になります。上記のコードコメントで示そうとしたように、これはReservation -> int optionという、求められる型を持っています。

等価性

このように使われる部分適用は、依存関係の注入と等価です。その仕組みを理解するために、生成された中間言語(IL)を見てみましょう。

F#は.NET言語なので、ILにコンパイルされます。そのILをC#にデコンパイルすれば、何が起きているのかを把握できます。上記のtryAcceptCompositionでそれを実行すると、次のようになります。

internal class tryAcceptComposition@17 : FSharpFunc<Reservation, FSharpOption<int>>
{
    public int capacity;
    public FSharpFunc<Reservation, int> createReservation;
    public FSharpFunc<DateTimeOffset, FSharpList<Reservation>> readReservations;
 
    internal tryAcceptComposition@17(
        int capacity,
        FSharpFunc<DateTimeOffset, FSharpList<Reservation>> readReservations,
        FSharpFunc<Reservation, int> createReservation)
    {
        this.capacity = capacity;
        this.readReservations = readReservations;
        this.createReservation = createReservation;
    }
 
    public override FSharpOption<int> Invoke(Reservation reservation)
    {
        return MaîtreD.tryAccept<int>(
            this.capacity, this.readReservations, this.createReservation, reservation);
    }
}

主に様々な要素からすべての属性を削除するなど、少しクリーンアップしました。これがクラスであり、クラスフィールドを持ち、フィールドの値を受け取って代入するコンストラクタを持っていることに注目してください。これはコンストラクタインジェクションです!

部分適用は依存関係の注入なのです。

コンパイルも通り、期待通りに動作しますが、はたしてこれは関数型的なのでしょうか?

評価

時々、こう尋ねられます。どうすれば自分のF#コードが関数型的だとわかるのですか?

私自身も時々そのことを考えますが、残念ながら、F#は素晴らしい言語であるものの、その点についてはあまり助けになりません。F#は関数型プログラミングに重点を置いていますが、ミューテーション(可変状態)、オブジェクト指向プログラミング、さらには手続き型プログラミングも許容します。フレンドリーで寛容な言語なのです。(この性質により、関数型の概念を少しずつ学べるため、F#は「初心者向け」の関数型言語としても優れています。)

一方、Haskellは厳密に関数型な言語です。Haskellでは、コードを関数型的な方法でしか書くことができません。

幸いなことに、F#とHaskellは十分に似ているため、そのF#コードがすでに「十分に関数型的」である限りは、F#のコードをHaskellに移植するのは簡単です。自分のF#コードが適切に関数型的であるかを評価するために、私は時々それをHaskellに移植してみます。Haskellでコンパイルして実行できれば、私はそれを、自分のコードが関数型的であることの証左としています。

以前、これと似た例を示したことがありますが、ここでその実験を繰り返してみましょう。tryAccepttryAcceptCompositionをHaskellに移植することはうまくいくでしょうか?

tryAcceptの移植は簡単です。

tryAccept :: Int -> (ZonedTime -> [Reservation]) -> (Reservation -> Int) -> Reservation
             -> Maybe Int
tryAccept capacity readReservations createReservation reservation =
  let reservedSeats = sum $ map quantity $ readReservations $ date reservation
  in  if reservedSeats + quantity reservation <= capacity
      then Just $ createReservation $ reservation { isAccepted = True }
      else Nothing

もちろん違いはありますが、類似点も見いだせるはずです。この関数の最も重要な特徴は、純粋であることです。Haskellの関数は、不純であると明示的に宣言されない限り、デフォルトで純粋であり、この関数もそうです。この関数は純粋であり、readReservationscreateReservationも同様に純粋です。

Haskell版のtryAcceptはコンパイルできますが、tryAcceptCompositionはどうでしょうか?

F#のコードと同様に、この実験の目的は、実際にデータベースを操作する関数を「注入」することが可能かどうかを確認することです。F#の例と等価なものとして、次のようなDBモジュールがあると想像してみてください。

readReservations :: ConnectionString -> ZonedTime -> IO [Reservation]
readReservations connectionString date = -- ..

createReservation :: ConnectionString -> Reservation -> IO Int
createReservation connectionString reservation = -- ..

データベース操作は、定義上、不純であり、Haskellはそのことを型システムで見事に表現しています。両方の関数がIO値を返すことに注目してください。

両方の関数に有効な接続文字列を部分適用しても、IOコンテキストは残ったままです。DB.readReservations connectionStringの型はZonedTime -> IO [Reservation]であり、DB.createReservation connectionStringの型はReservation -> IO Intです。これらをtryAcceptに渡そうとしても、型が一致しません。

tryAcceptComposition :: Reservation -> IO (Maybe Int)
tryAcceptComposition reservation =
  let read   = DB.readReservations  connectionString
      create = DB.createReservation connectionString
  in tryAccept 10 read create reservation

これはコンパイルできません。

データベース操作が不純であり、tryAcceptが純粋関数を求めているため、コンパイルできないのです。

要するに、依存関係の注入のために使われる部分適用は、関数型的ではないのです。

まとめ

F#の部分適用は、依存関係の注入と等価な結果を得るために使用できます。コンパイルも通り、期待通りに動作しますが、それは関数型的ではありません。関数型的ではない理由は、(ほとんどの)依存関係が、その性質上、本質的に不純だからです。それらは非決定的であったり、副作用を持っていたり、あるいはその両方であったりします。そしてそれこそが、そもそもそれらの処理が依存関係として切り出される根本的な理由であることが多いのです。

しかし、純粋関数は不純な関数を呼び出すことはできません。もし呼び出せてしまえば、それ自身も不純になってしまうからです。このルールはHaskellでは強制されますが、F#では強制されません。

不純な操作をF#の関数に注入すると、その関数もまた不純になります。依存関係の注入はすべてを不純にしてしまう、これが関数型的ではない理由です。

関数型プログラミングは、(副作用のような)作用をプログラムロジックから切り離す問題を、別の方法で解決します。それが次の記事のテーマです。

次へ: 依存関係の排除


インデックスへ戻る