特にデータアクセスのコンテキストで、Interpreterを使用してFree Monadについて話している人々を見てきました。このパターンは何ですか?いつ使用したいですか?どのように機能し、どのように実装しますか?
(このような投稿から)私はそれがモデルをデータアクセスから分離することだと理解しています。よく知られているリポジトリパターンとの違いは何ですか?彼らは同じ動機を持っているように見えます。
特にデータアクセスのコンテキストで、Interpreterを使用してFree Monadについて話している人々を見てきました。このパターンは何ですか?いつ使用したいですか?どのように機能し、どのように実装しますか?
(このような投稿から)私はそれがモデルをデータアクセスから分離することだと理解しています。よく知られているリポジトリパターンとの違いは何ですか?彼らは同じ動機を持っているように見えます。
回答:
実際のパターンは、実際には単なるデータアクセスよりもはるかに一般的です。これは、ASTを提供するドメイン固有の言語を作成する簡単な方法であり、ASTを好きなように「実行」する1つ以上のインタープリターを持っています。
無料のモナド部分は、多くのカスタムコードを記述することなく、Haskellの標準モナド機能(do-notationなど)を使用してアセンブルできるASTを取得する便利な方法です。これにより、DSLが構成可能になります。部品で定義し、構造化された方法で部品を組み合わせて、関数などのHaskellの通常の抽象化を活用できます。
無料のモナドを使用すると、構成可能なDSLの構造が得られます。必要なのは、ピースを指定することだけです。DSLのすべてのアクションを含むデータ型を記述するだけです。これらのアクションは、データアクセスだけでなく、何でも実行できます。ただし、すべてのデータアクセスをアクションとして指定した場合、データストアへのすべてのクエリとコマンドを指定するASTを取得します。次に、これを好きなように解釈できます。ライブデータベースに対して実行する、モックに対して実行する、デバッグ用のコマンドをログに記録する、またはクエリを最適化することもできます。
たとえば、キーバリューストアの非常に単純な例を見てみましょう。今のところ、キーと値の両方を文字列として扱いますが、少し手間をかけてタイプを追加できます。
data DSL next = Get String (String -> next)
| Set String String next
| End
このnext
パラメーターにより、アクションを組み合わせることができます。これを使用して、「foo」を取得し、その値で「bar」を設定するプログラムを作成できます。
p1 = Get "foo" $ \ foo -> Set "bar" foo End
残念ながら、これは意味のあるDSLには十分ではありません。next
合成に使用したため、のタイプはp1
プログラムと同じ長さです(つまり、3つのコマンド)。
p1 :: DSL (DSL (DSL next))
この特定の例では、next
このように使用するのは少し奇妙に思えますが、アクションに異なる型変数を持たせたい場合は重要です。たとえば、型付きのget
、およびset
が必要な場合があります。
next
フィールドがアクションごとに異なることに注意してください。これはDSL
、ファンクターを作成するために使用できることを示しています。
instance Functor DSL where
fmap f (Get name k) = Get name (f . k)
fmap f (Set name value next) = Set name value (f next)
fmap f End = End
実際、これがFunctorにする唯一の有効な方法なのでderiving
、DeriveFunctor
拡張機能を有効にすることでインスタンスを自動的に作成できます。
次のステップは、Free
タイプそのものです。これは、タイプの上に構築するAST 構造を表すために使用するものDSL
です。これは、型レベルのリストのように考えることができます。「cons」は、次のようなファンクターをネストしているだけですDSL
。
-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a = Cons a (List a) | Nil
したがってFree DSL next
、異なるサイズのプログラムに同じタイプを与えるために使用できます。
p2 = Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
どちらがより良いタイプを持っています:
p2 :: Free DSL a
ただし、すべてのコンストラクターを含む実際の式は、使用するのが非常に厄介です!これがモナド部分の出番です。「フリーモナド」という名前が示すようにFree
、モナドはf
(この場合DSL
)ファンクタである限りです:
instance Functor f => Monad (Free f) where
return = Return
Free a >>= f = Free (fmap (>>= f) a)
Return a >>= f = f a
現在、どこかで取得しています:do
表記を使用してDSL式をより良くすることができます。唯一の質問は何を入れるnext
かです。さて、構想はFree
構成に構造を使用することなので、Return
次のフィールドごとに配置し、do表記法ですべての配管を行います。
p3 = do foo <- Free (Get "foo" Return)
Free (Set "bar" foo (Return ()))
Free End
これは良いですが、まだ少し厄介です。我々は持っているFree
とReturn
、すべての場所以上。幸いなことに、私たちが利用することができますパターンがあります:私たちは「リフト」の方法にDSLのアクションはFree
常に同じ-我々がある中でそれをラップFree
し、適用Return
のためにnext
:
liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)
これを使用して、各コマンドの素敵なバージョンを作成し、完全なDSLを作成できます。
get key = liftFree (Get key id)
set key value = liftFree (Set key value ())
end = liftFree End
これを使用して、プログラムを作成する方法を次に示します。
p4 :: Free DSL a
p4 = do foo <- get "foo"
set "bar" foo
end
きちんとしたトリックはp4
、ちょっと命令的なプログラムのように見えますが、実際には値を持つ式です
Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
そのため、パターンの無料のモナド部分は、優れた構文を持つ構文ツリーを生成するDSLを取得しました。を使用しないことでEnd
、構成可能なサブツリーを作成することもできます。たとえば、follow
キーを受け取り、その値を取得し、それをキー自体として使用することができます。
follow :: String -> Free DSL String
follow key = do key' <- get key
get key'
今follow
と同じように、私たちのプログラムで使用することができますget
かset
。
p5 = do foo <- follow "foo"
set "bar" foo
end
そのため、DSLの優れた構成と抽象化も得られます。
ツリーができたので、パターンの後半であるインタープリターに到達します。ツリーをパターンマッチングするだけで好きなように解釈できます。これにより、で実際のデータストアに対してコードを記述できますIO
。架空のデータストアに対する例を次に示します。
runIO :: Free DSL a -> IO ()
runIO (Free (Get key k)) =
do res <- getKey key
runIO $ k res
runIO (Free (Set key value next)) =
do setKey key value
runIO next
runIO (Free End) = close
runIO (Return _) = return ()
これはDSL
、で終わっていないフラグメントも含め、あらゆるフラグメントを喜んで評価しend
ます。幸いなことにend
、入力タイプシグネチャをに設定することで、閉じられたプログラムのみを受け入れる「安全な」バージョンの関数を作成できます(forall a. Free DSL a) -> IO ()
。古い署名が受け入れている間Free DSL a
のために任意の a
(のようなFree DSL String
、Free DSL Int
など)、このバージョンでは唯一の受け入れFree DSL a
のために働くこと、すべての可能なa
我々だけで作成できる-whichをend
。これにより、完了時に接続を閉じることを忘れないことが保証されます。
safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO
(私達はちょうど与えることで起動することはできません。runIO
それが私たちの再帰呼び出しのために正常に動作しませんので、このタイプを。しかし、我々はの定義を移動することができますrunIO
へwhere
のブロックsafeRunIO
と機能の両方のバージョンを公開することなく、同じ効果を得ることができます。)
実行できるのはコードを実行するIO
ことだけではありません。テストのために、State Map
代わりにピュアに対して実行したい場合があります。そのコードを書くことは良い練習です。
したがって、これは無料のモナド+インタプリタパターンです。無料のモナド構造を利用してすべての配管を行うDSLを作成します。DSLでdo記法と標準モナド関数を使用できます。次に、実際に使用するには、なんとか解釈する必要があります。ツリーは最終的には単なるデータ構造であるため、さまざまな目的で好きなように解釈できます。
これを使用して外部データストアへのアクセスを管理する場合、実際にはリポジトリパターンに似ています。データストアとコードの中間にあり、2つを分離します。しかし、いくつかの点で、より具体的です。「リポジトリ」は、明示的なASTを備えたDSLであり、必要に応じて使用できます。
ただし、パターン自体はそれよりも一般的です。これは、外部データベースやストレージを必ずしも必要としない多くのものに使用できます。DSLのエフェクトや複数のターゲットをきめ細かく制御したい場合は、意味があります。
do
法は異なるが実際には「同じ」というプログラムを区別することを不可能にします。
フリーモナドは基本的に、より複雑なことを行うのではなく、計算と同じ「形状」でデータ構造を構築するモナドです。(オンラインで見つけられる例があります。)次に、このデータ構造は、それを消費して操作を実行するコードに渡されます。*私はリポジトリパターンに完全には精通していませんが、読んだところから表示されますより高いレベルのアーキテクチャであり、無料のモナド+インタプリタを使用して実装できます。一方、無料のモナド+インタープリターを使用して、パーサーなどのまったく異なるものを実装することもできます。
*このパターンはモナドだけに限定されず、実際には無料のアプリカティブまたは無料の矢印を使用してより効率的なコードを生成できることに注意してください。(パーサーはこの別の例です。)
repository.Get()
の知識がないところ、それがからドメインオブジェクトを取得しています。