「Free Monad + Interpreter」パターンとは何ですか?


95

特にデータアクセスのコンテキストで、Interpreterを使用しFree Monadについて話している人々を見てきました。このパターンは何ですか?いつ使用したいですか?どのように機能し、どのように実装しますか?

このような投稿から)私はそれがモデルをデータアクセスから分離することだと理解しています。よく知られているリポジトリパターンとの違いは何ですか?彼らは同じ動機を持っているように見えます。

回答:


138

実際のパターンは、実際には単なるデータアクセスよりもはるかに一般的です。これは、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にする唯一の有効な方法なのでderivingDeriveFunctor拡張機能を有効にすることでインスタンスを自動的に作成できます。

次のステップは、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

これは良いですが、まだ少し厄介です。我々は持っているFreeReturn、すべての場所以上。幸いなことに、私たちが利用することができますパターンがあります:私たちは「リフト」の方法に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と同じように、私たちのプログラムで使用することができますgetset

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 StringFree DSL Intなど)、このバージョンでは唯一の受け入れFree DSL aのために働くこと、すべての可能なa我々だけで作成できる-whichをend。これにより、完了時に接続を閉じることを忘れないことが保証されます。

safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO

(私達はちょうど与えることで起動することはできません。runIOそれが私たちの再帰呼び出しのために正常に動作しませんので、このタイプを。しかし、我々はの定義を移動することができますrunIOwhereのブロックsafeRunIOと機能の両方のバージョンを公開することなく、同じ効果を得ることができます。)

実行できるのはコードを実行するIOことだけではありません。テストのために、State Map代わりにピュアに対して実行したい場合があります。そのコードを書くことは良い練習です。

したがって、これは無料のモナド+インタプリタパターンです。無料のモナド構造を利用してすべての配管を行うDSLを作成します。DSLでdo記法と標準モナド関数を使用できます。次に、実際に使用するには、なんとか解釈する必要があります。ツリーは最終的には単なるデータ構造であるため、さまざまな目的で好きなように解釈できます。

これを使用して外部データストアへのアクセスを管理する場合、実際にはリポジトリパターンに似ています。データストアとコードの中間にあり、2つを分離します。しかし、いくつかの点で、より具体的です。「リポジトリ」は、明示的なASTを備えたDSLであり、必要に応じて使用できます。

ただし、パターン自体はそれよりも一般的です。これは、外部データベースやストレージを必ずしも必要としない多くのものに使用できます。DSLのエフェクトや複数のターゲットをきめ細かく制御したい場合は、意味があります。


6
なぜ「無料」モナドと呼ばれるのですか?
ベンジャミンホジソン14年

14
「無料」の名前はカテゴリ理論に由来します:ncatlab.org/nlab/show/free+objectしかし、それは一種の「最小」モナドであることを意味します。それは他の構造です。
ボイドスティーブンスミスジュニア14年

3
@BenjaminHodgson:ボイドは完全に正しい。好奇心が強い場合を除き、あまり心配する必要はありません。Dan PiponiはBayHacで「無料」が何を意味するかについて素晴らしい講演をしました。これは一見の価値があります。ビデオのビジュアルはまったく役に立たないので、彼のスライドをフォローしてみてください。
Tikhon Jelvis

3
nitpick:「自由モナド部分があるだけで [私の強調]カスタムコードの多くを記述することなく、Haskellの標準モナド施設(のようなdo記法)を使用して組み立てることができASTを取得するための便利な方法です。」それだけではありません(ご存知のとおり)。フリーモナドは正規化されたプログラム表現でもあり、インタープリターが-表記do法は異なるが実際には「同じ」というプログラムを区別することを不可能にします。
sacundim

5
@sacundim:コメントについて詳しく教えてください。特に、「フリーモナドは正規化されたプログラム表現でもあるため、インタプリタはdo記法は異なるが、実際には「同じ」というプログラムを区別できません。」
ジョルジオ

15

フリーモナドは基本的に、より複雑なことを行うのではなく、計算と同じ「形状」でデータ構造を構築するモナドです。(オンラインで見つけられる例があります。)次に、このデータ構造は、それを消費して操作を実行するコードに渡されます。*私はリポジトリパターンに完全には精通していませんが、読んだところから表示されますより高いレベルのアーキテクチャであり、無料のモナド+インタプリタを使用して実装できます。一方、無料のモナド+インタープリターを使用して、パーサーなどのまったく異なるものを実装することもできます。

*このパターンはモナドだけに限定されず、実際には無料のアプリカティブまたは無料の矢印を使用してより効率的なコードを生成できることに注意してください。(パーサーはこの別の例です。


おApび申し上げますが、リポジトリについてはもっと明確にすべきでした。(誰もがビジネスシステム/ OO / DDDのバックグラウンドを持っているわけではないことを忘れていました!)リポジトリは基本的にデータアクセスをカプセル化し、ドメインオブジェクトを復元します。多くの場合、Dependency Inversionと一緒に使用されます-Repoのさまざまな実装を「プラグイン」できます(テストに役立つか、データベースまたはORMを切り替える必要がある場合)。ドメインコードは、単に呼び出すrepository.Get()の知識がないところ、それがからドメインオブジェクトを取得しています。
ベンジャミンホジソン14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.