16

以前に何かをハッキングしているときに、次のコードを作成しました。

newtype Callback a = Callback { unCallback :: a -> IO (Callback a) }

liftCallback :: (a -> IO ()) -> Callback a
liftCallback f = let cb = Callback $ \x -> (f x >> return cb) in cb

runCallback :: Callback a -> IO (a -> IO ())
runCallback cb =
    do ref <- newIORef cb
       return $ \x -> readIORef ref >>= ($ x) . unCallback >>= writeIORef ref

Callback a一部のデータを処理し、次の通知に使用する必要がある新しいコールバックを返す関数を表します。いわば、基本的に自分自身を置き換えることができるコールバック。liftCallback通常の関数を自分の型に持ち上げるだけで、runCallbackan を使用して a を単純な関数IORefに変換します。Callback

タイプの一般的な構造は次のとおりです。

data T m a = T (a -> m (T m a))

これは、圏論からのよく知られた数学的構造と同形である可能性が非常に高いように見えます。

しかし、それは何ですか?モナドか何かですか?アプリカティブファンクター?変形モナド?矢でも?このような一般的なパターンを検索できる Hoogle に似た検索エンジンはありますか?

4

4 に答える 4

14

あなたが探している用語はフリーモナドトランスフォーマーです。これらがどのように機能するかを学ぶには、The Monad Reader の第 19 号の「コルーチン パイプライン」の記事を読むのが最適です。Mario Blazevic は、このタイプがどのように機能するかを非常に明快に説明していますが、彼はそれを「コルーチン」タイプと呼んでいます。

彼のタイプをパッケージに書き留めた後、新しい公式ホームであるパッケージにtransformers-freeマージされました。free

あなたのCallback型は以下と同型です:

type Callback a = forall r . FreeT ((->) a) IO r

フリー モナド トランスフォーマーを理解するには、まずフリー モナド を理解する必要があります。フリー モナドは単なる抽象構文木です。free モナドに、構文ツリーの単一のステップを定義するファンクターを与えると、基本的にそれらのタイプのステップのリストであるMonadfrom が作成されます。Functorあなたが持っていた場合:

Free ((->) a) r

これは、ゼロ個以上aの s を入力として受け取り、 value を返す構文ツリーになりますr

ただし、通常、効果を埋め込むか、構文ツリーの次のステップを何らかの効果に依存させたいと考えています。これを行うには、単純に自由モナドを自由モナド変換子に昇格させます。これは、構文ツリーのステップ間でベース モナドをインターリーブします。あなたのCallbackタイプの場合、各入力ステップの間にインターリーブIOしているので、ベースモナドはIO次のとおりです。

FreeT ((->) a) IO r

フリーモナドの良いところは、それらが自動的に任意のファンクターのモナドになることです。そのため、これを利用してdo記法を使用して構文ツリーを組み立てることができます。たとえばawait、モナド内で入力をバインドするコマンドを定義できます。

import Control.Monad.Trans.Free

await :: (Monad m) => FreeT ((->) a) m a
await = liftF id

Callbackこれで、 sを書くための DSL ができました。

import Control.Monad
import Control.Monad.Trans.Free

printer :: (Show a) => FreeT ((->) a) IO r
printer = forever $ do
    a <- await
    lift $ print a

Monad必要なインスタンスを定義する必要がないことに注意してください。FreeT fとはどちらも functor に対してFree f自動的にMonads でfあり、この場合((->) a)は functor であるため、自動的に正しいことを行います。それが圏論の魔法です!

MonadTransまた、 を使用するためにインスタンスを定義する必要はありませんでしたliftFreeT ffunctor が与えられると、自動的にモナド変換子にfなるので、それも処理してくれます。

私たちのプリンターは適しCallbackた です。

feed :: [a] -> FreeT ((->) a) IO r -> IO ()
feed as callback = do
    x <- runFreeT callback
    case x of
        Pure _ -> return ()
        Free k -> case as of
            []   -> return ()
            b:bs -> feed bs (k b)

をバインドすると実際の出力が行わrunFreeT callbackれ、リストの次の要素をフィードする構文ツリーの次のステップが得られます。

試してみよう:

>>> feed [1..5] printer
1
2
3
4
5

ただし、これらすべてを自分で書く必要さえありません。Petr が指摘したように、私のpipesライブラリは、このような一般的なストリーミング パターンを抽象化します。コールバックは次のとおりです。

forall r . Consumer a IO r

printerusing を定義する方法pipesは次のとおりです。

printer = forever $ do
    a <- await
    lift $ print a

...そして、次のように値のリストをフィードできます。

>>> runEffect $ each [1..5] >-> printer
1
2
3
4
5

pipes私は、これらのような非常に広範囲のストリーミング抽象化を包含するように設計し、do各ストリーミング コンポーネントを構築するために常に表記法を使用できるようにしました。 pipesには、状態やエラーの処理、情報の双方向フローなどのさまざまな洗練されたソリューションも付属しているため、 のCallback観点から抽象化を定式化するpipesと、大量の便利な機構を無料で利用できます。

について詳しく知りたい場合は、チュートリアルを読むことpipesをお勧めします。

于 2013-02-06T00:09:53.283 に答える
8

タイプの一般的な構造は私には似ています

data T (~>) a = T (a ~> T (~>) a)

あなたの用語のどこ(~>) = Kleisli mに(矢印)。


Callbackそれ自体は、私が考えることができる標準的な Haskell 型クラスのインスタンスのようには見えませんが、Contravariant Functor (Cofunctor としても知られていますが、誤解を招く可能性があります) です。GHC に付属するどのライブラリにも含まれていないため、Hackage にはいくつかの定義が存在しますが (これを使用してください)、それらはすべて次のようになります。

class Contravariant f where
    contramap :: (b -> a) -> f a -> f b
 -- c.f. fmap :: (a -> b) -> f a -> f b

それで

instance Contravariant Callback where
    contramap f (Callback k) = Callback ((fmap . liftM . contramap) f (f . k))

圏論から持っているよりエキゾチックな構造はありますCallbackか? 知らない。

于 2013-02-05T20:21:17.583 に答える
6

このタイプは、私が聞いた「回路」と呼ばれる矢印のタイプに非常に近いと思います。IO部分を少し無視すると(クライスリの矢印を変換するだけでこれを実現できるため)、回路トランスは次のようになります。

newtype CircuitT a b c = CircuitT { unCircuitT :: a b (c, CircuitT a b c) }

これは基本的に、毎回次の入力に使用する新しい矢印を返すすべての矢印です。ベース矢印がそれらをサポートしている限り、すべての一般的な矢印クラス(ループを含む)をこの矢印トランスフォーマーに実装できます。さて、あなたが言及したタイプと概念的に同じにするために私たちがしなければならないのは、その余分な出力を取り除くことです。これは簡単に実行できるため、次のことがわかります。

Callback a ~=~ CircuitT (Kleisli IO) a ()

右側を見るように:

CircuitT (Kleisli IO) a () ~=~
  (Kliesli IO) a ((), CircuitT (Kleisli IO) a ()) ~=~
  a -> IO ((), CircuitT (Kliesli IO) a ())

ここから、単位値も出力することを除けば、これがコールバックaとどのように似ているかがわかります。とにかく単位値は他の何かとタプルになっているので、これは実際にはあまりわかりません。したがって、基本的に同じであると言えます。

NB私は、何らかの理由で、類似しているが完全に同等ではないために〜=〜を使用しました。ただし、これらは非常によく似ています。特に、aCallback aをaに、CircuitT (Kleisli IO) a ()またはその逆に変換できることに注意してください。

編集:私はまた、これがA)モナディックコストリーム(無限の数の値を消費するモナディック操作、これは意味すると思います)およびB)消費専用パイプ(多くの点で非常に似ている)であるという考えに完全に同意します出力のない回路タイプ、または出力が()に設定されている回路タイプ。このようなパイプも出力を持っている可能性があります)。

于 2013-02-05T21:40:38.540 に答える
3

ただの観察ですが、あなたのタイプはパイプライブラリ(そしておそらく他の同様のライブラリ)Consumer p a mに表示されることにかなり関連しているようです:

type Consumer p a = p () a () C
-- A Pipe that consumes values
-- Consumers never respond.

ここCで、は空のデータ型であり、は型クラスpのインスタンスです。Proxyタイプの値を消費しa、生成することはありません(出力タイプが空であるため)。

たとえば、CallbackaをConsumer:に変換できます。

import Control.Proxy
import Control.Proxy.Synonym

newtype Callback m a = Callback { unCallback :: a -> m (Callback m a) }

-- No values produced, hence the polymorphic return type `r`.
-- We could replace `r` with `C` as well.
consumer :: (Proxy p, Monad m) => Callback m a -> () -> Consumer p a m r
consumer c () = runIdentityP (run c)
  where
    run (Callback c) = request () >>= lift . c >>= run

チュートリアルを参照してください。

(これはかなりコメントであるはずですが、少し長すぎます。)

于 2013-02-05T21:39:35.847 に答える