この例をモデル化する方法
これをReaderモナドでどのようにモデル化できますか?
これをReaderでモデル化する必要があるかどうかはわかりませんが、次の方法でモデル化できます。
- コードをReaderでより適切に再生する関数としてクラスをエンコードする
- 理解のためにReaderで関数を構成して使用する
開始の直前に、この回答に有益だと感じた小さなサンプルコードの調整について説明する必要があります。最初の変更はFindUsers.inactive
方法についてです。List[String]
アドレスのリストをUserReminder.emailInactive
メソッドで使用できるように、それを返させます。また、メソッドに簡単な実装を追加しました。最後に、サンプルでは、次の手動ロールバージョンのReaderモナドを使用します。
case class Reader[Conf, T](read: Conf => T) { self =>
def map[U](convert: T => U): Reader[Conf, U] =
Reader(self.read andThen convert)
def flatMap[V](toReader: T => Reader[Conf, V]): Reader[Conf, V] =
Reader[Conf, V](conf => toReader(self.read(conf)).read(conf))
def local[BiggerConf](extractFrom: BiggerConf => Conf): Reader[BiggerConf, T] =
Reader[BiggerConf, T](extractFrom andThen self.read)
}
object Reader {
def pure[C, A](a: A): Reader[C, A] =
Reader(_ => a)
implicit def funToReader[Conf, A](read: Conf => A): Reader[Conf, A] =
Reader(read)
}
モデリングステップ1.クラスを関数としてエンコードする
それはオプションかもしれませんが、よくわかりませんが、後で理解しやすくなります。結果の関数はカレーされていることに注意してください。また、以前のコンストラクター引数を最初のパラメーター(パラメーターリスト)として使用します。そのように
class Foo(dep: Dep) {
def bar(arg: Arg): Res = ???
}
になります
object Foo {
def bar: Dep => Arg => Res = ???
}
それぞれのことを覚えておいてくださいDep
、Arg
、Res
タイプは完全に任意でよい:タプル、関数やシンプルタイプ。
関数に変換された、初期調整後のサンプルコードは次のとおりです。
trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }
object FindUsers {
def inactive: Datastore => () => List[String] =
dataStore => () => dataStore.runQuery("select inactive")
}
object UserReminder {
def emailInactive(inactive: () => List[String]): EmailServer => () => Unit =
emailServer => () => inactive().foreach(emailServer.sendEmail(_, "We miss you"))
}
object CustomerRelations {
def retainUsers(emailInactive: () => Unit): () => Unit =
() => {
println("emailing inactive users")
emailInactive()
}
}
ここで注意すべきことの1つは、特定の関数がオブジェクト全体に依存するのではなく、直接使用される部分にのみ依存することです。OOPバージョンのUserReminder.emailInactive()
インスタンスがuserFinder.inactive()
ここで呼び出す場所はinactive()
、最初のパラメーターで渡された関数です。
コードは、質問から3つの望ましいプロパティを示していることに注意してください。
- 各機能に必要な依存関係の種類は明らかです
- ある機能の依存関係を別の機能から隠します
retainUsers
メソッドはデータストアの依存関係について知る必要はありません
モデリングステップ2.リーダーを使用して関数を作成して実行する
リーダーモナドでは、すべて同じタイプに依存する関数のみを作成できます。多くの場合、これは当てはまりません。この例では
FindUsers.inactive
に依存Datastore
してUserReminder.emailInactive
上EmailServer
。この問題を解決するには、すべての依存関係を含む新しいタイプ(多くの場合、Configと呼ばれます)を導入し、関数を変更して、すべてがそれに依存し、関連するデータのみを取得するようにします。依存関係管理の観点からは、これは明らかに間違っています。これは、これらの関数を、そもそも知らないはずの型にも依存させるためです。
幸いなことに、関数のConfig
一部のみをパラメーターとして受け入れる場合でも、関数を機能させる方法が存在することがわかりました。これlocal
は、Readerで定義されていると呼ばれるメソッドです。から関連する部分を抽出する方法を提供する必要がありますConfig
。
手元の例に適用されるこの知識は、次のようになります。
object Main extends App {
case class Config(dataStore: Datastore, emailServer: EmailServer)
val config = Config(
new Datastore { def runQuery(query: String) = List("john.doe@fizzbuzz.com") },
new EmailServer { def sendEmail(to: String, content: String) = println(s"sending [$content] to $to") }
)
import Reader._
val reader = for {
getAddresses <- FindUsers.inactive.local[Config](_.dataStore)
emailInactive <- UserReminder.emailInactive(getAddresses).local[Config](_.emailServer)
retainUsers <- pure(CustomerRelations.retainUsers(emailInactive))
} yield retainUsers
reader.read(config)()
}
コンストラクターパラメーターを使用するよりも利点
このような「ビジネスアプリケーション」にReaderMonadを使用する方が、コンストラクターパラメーターを使用するよりも優れている点は何でしょうか。
この回答を準備することで、単純なコンストラクターに勝る側面を自分で判断しやすくなることを願っています。しかし、これらを列挙する場合は、ここに私のリストがあります。免責事項:私はOOPのバックグラウンドを持っており、ReaderとKleisliを使用していないため、それらを十分に理解していない可能性があります。
- 均一性-理解のためにどれほど短い/長いかは問題ではありません。それは単なるリーダーであり、別のインスタンスで簡単に構成できます。おそらく、もう1つのConfigタイプを導入し、その
local
上にいくつかの呼び出しを振りかけるだけです。この点はIMOの好みの問題です。コンストラクターを使用する場合、OOPで悪い習慣と見なされているコンストラクターで作業を行うなど、誰かが愚かなことをしない限り、誰もあなたが好きなものを作成することを妨げません。
- Readerはモナドであるため、それに関連するすべての利点が得られます-
sequence
、traverse
メソッドは無料で実装されます。
- 場合によっては、リーダーを1回だけビルドして、さまざまな構成に使用する方が望ましい場合があります。コンストラクターを使用すると、誰もそれを妨げることはありません。着信するすべての構成に対して、オブジェクトグラフ全体を新たに作成する必要があります。私はそれで問題はありませんが(私はアプリケーションへのすべての要求でそれを行うことを好みます)、私が推測するだけの理由から、多くの人にとってそれは明白な考えではありません。
- Readerは、関数をより多く使用するように促します。これは、主にFPスタイルで記述されたアプリケーションでより適切に機能します。
- 読者は懸念を分離します。依存関係を提供せずに、作成、すべての操作、ロジックの定義を行うことができます。実際には後で別々に供給してください。(この点についてはKen Scramblerに感謝します)。これはReaderの利点としてよく耳にしますが、単純なコンストラクターでも可能です。
また、Readerで気に入らない点も教えてください。
- マーケティング。Readerは、セッションCookieであるかデータベースであるかを区別せずに、あらゆる種類の依存関係で販売されているという印象を受けることがあります。私には、この例の電子メールサーバーやリポジトリなど、実質的に一定のオブジェクトにReaderを使用する意味はほとんどありません。このような依存関係については、単純なコンストラクターや部分的に適用された関数の方がはるかに優れていることがわかります。基本的にReaderは柔軟性を備えているため、呼び出しのたびに依存関係を指定できますが、それが本当に必要ない場合は、税金を支払うだけです。
- 暗黙の重さ-暗黙なしでReaderを使用すると、例が読みにくくなります。一方、暗黙的にノイズの多い部分を非表示にしてエラーを発生させると、コンパイラーによってメッセージの解読が困難になる場合があります。
- を使用したセレモニー
pure
、local
および独自のConfigクラスの作成/そのためのタプルの使用。Readerは、問題のあるドメインに関するものではないコードを追加するように強制するため、コードにノイズが発生します。一方、コンストラクターを使用するアプリケーションは、問題ドメインの外部からのファクトリパターンを使用することが多いため、この弱点はそれほど深刻ではありません。
クラスを関数付きのオブジェクトに変換したくない場合はどうなりますか?
あなたが欲しい。技術的にはそれを回避できますが、FindUsers
クラスをオブジェクトに変換しなかった場合にどうなるかを見てください。理解のためのそれぞれの行は次のようになります。
getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
それほど読みにくいですよね?重要なのは、Readerは関数を操作するため、関数をまだ持っていない場合は、インラインで作成する必要がありますが、これはあまり美しくないことがよくあります。