種類
Haskellはタイプが主要な言語なので、ストリームを表す適切なタイプを選択することから始めます。 まず、作成したいストリームを単純な言語で示す必要があります。
- ストリームは既存の命令シーケンスを拡張する必要があります
- スレッドは一連の操作をサポートする必要があります:分岐、制御転送、終了
- スレッドは異なるタイプのスケジューラーを許可する必要があります
次に、これらの概念をHaskellに翻訳します。
- 「複数の通訳者/プランナー/バックエンド」と聞いたら、「無料」と考えるべきです(「無料オブジェクト」のように)
- 「コマンドのシーケンス」を聞いたら、「モナド」と考える必要があります。
- 何かを「拡張」したいときは、「トランス」と考えるべきです。
これらの単語を組み合わせると、適切な数学的解決策が得られます:「無料のモナド変換器」。
構文ツリー
「Free Monad Transformer」は、シーケンスが重要な役割を果たす数学的な抽象構文ツリーの空想的な名前です。 一連の命令を提供し、これらの命令から構文ツリーを構築します。
ストリームを分岐、制御の転送、または停止のいずれかにしたいので、分岐、戻り、終了を使用してデータ型を作成しましょう。
{-# LANGUAGE DeriveFunctor #-} data ThreadF next = Fork next next | Yield next | Done deriving (Functor)
ThreadF
は命令セットを導入します。 3つの新しい命令を追加したいので、ThreadFには3つのコンストラクターがあり、各コマンドに1つずつ、
Fork
、
Yield
、および
Done
ます。
ThreadF
タイプは、構文ツリーの単一ノードを表します。 コンストラクターの
next
フィールドは、ノードの子が行くべき場所を表します。
Fork
は2つの実行方法を作成するため、2つの子があります。
Done
は現在の実行パスを完了するため、子はありません。 分岐も終了もしないため、子が1人います。 派生(ファンクター)部分は、フリーモナドトランスフォーマーに、
next
フィールドが子の行くべき場所であることを伝えるだけです。
おおよそ、派生(ファンクター)の実行時に作成されるもの
instance Functor ThreadF where f `fmap` (Fork next next) = Fork (f next) (f next) f `fmap` (Yield next) = Yield (f next) f `fmap` Done = Done
これで、無料のモナド変換器
FreeT
がコマンドの構文ツリーを構築できます。 このツリーをスレッドと呼びます。
-- `free` import Control.Monad.Trans.Free type Thread = FreeT ThreadF
経験豊富なHaskellプログラマーは、「
Thread
は
ThreadF
命令から構築された構文ツリーである」と言って、このコードを読みます
ThreadF
説明書
ここで、プリミティブな命令が必要です。
free
パッケージは
liftF
操作を提供し、1つのコマンドを1つ深いノードの構文ツリーに変換します。
yield :: (Monad m) => Thread m () yield = liftF (Yield ()) done :: (Monad m) => Thread mr done = liftF Done cFork :: (Monad m) => Thread m Bool cFork = liftF (Fork False True)
これがどのように機能するかを完全に理解する必要はありませんが、各コマンドの戻り値が子ノードフィールドに格納するものに対応していることに気付く場合を除きます。
-
yield
コマンドは()
をその子として保存するため、関数の戻り値は()
-
done
コマンドには子がないため、コンパイラーはポリモーフィックな戻り値(つまりr
)があると推測します。つまり、終了しないことを意味します。 -
cFork
コマンドはブール値を子として保存するため、Bool
返します
cFork
は、Cの
fork
関数のように動作するため、その名前が付けられました。つまり、返されたブール値は、分岐後にどの分岐にいるのかを示します。
False
を取得した場合、左ブランチにあり、
True
を取得した場合、右ブランチにあります。
左ブランチを「親」、右ブランチを「子」という規則を使用して、より伝統的なHaskellスタイルで
fork
を実装することにより、
cFork
を組み合わせて
cFork
ことができます。
import Control.Monad fork :: (Monad m) => Thread ma -> Thread m () fork thread = do child <- cFork when child $ do thread done
上記のコードは
cFork
呼び出し、「私が子供の場合、分岐アクションを実行してから停止します。それ以外の場合は通常どおり続行します。」
無料のモナド
最後のコードで異常が発生したことに注目してください。
cFork
をコンパイルし、表記法
do
を使用してプリミティブ
Thread
スレッド命令から関数を実行し、新しい
Thread
を取得しました。 これは、HaskellがMonadインターフェイスを実装する任意の型の
do
記法を使用することを許可
do
、フリーモナドトランスフォーマーが
Thread
instance
モナドの目的の
instance
自動的に決定するためです。 すごい!
実際、私たちの無料のモナド変換子はまったくスマートではありません。
do
記法を使用
do
て無料のモナド変換器を組み立てるとき、行われるのはこれらの原始構文木を1つの深いノード(すなわち命令)に接続してより大きな構文木にすることです。 2つのコマンドのシーケンス:
do yield done
... 2番目のコマンド(つまり
done
)を最初のコマンド(つまり
yield
)の子として保存するために単純化さ
done
ます。
ループフローマネージャー
次に、独自のスレッドスケジューラを作成します。 これは単純な巡回スケジューラになります:
-- O(1) import Data.Sequence roundRobin :: (Monad m) => Thread ma -> m () roundRobin t = go (singleton t) -- where go ts = case (viewl ts) of -- : ! EmptyL -> return () -- : t :< ts' -> do x <- runFreeT t -- case x of -- Free (Fork t1 t2) -> go (t1 <| (ts' |> t2)) -- Free (Yield t') -> go (ts' |> t') -- : Free Done -> go ts' Pure _ -> go ts'
...そして完了! いいえ、本当に、それだけです! これは完全なストリーミング実装です。
カスタムスレッド
新しい勇ましいストリーミングシステムを試してみましょう。 簡単なものから始めましょう。
mainThread :: Thread IO () mainThread = do lift $ putStrLn "Forking thread #1" fork thread1 lift $ putStrLn "Forking thread #1" fork thread2 thread1 :: Thread IO () thread1 = forM_ [1..10] $ \i -> do lift $ print i yield thread2 :: Thread IO () thread2 = replicateM_ 3 $ do lift $ putStrLn "Hello" yield
これらの各スレッドのタイプは
Thread IO ()
です。
Thread
は「モナド変換器」です。つまり、既存のモナドを追加機能で拡張します。 この場合、ユーザースレッドを使用して
IO
モナドを拡張します。つまり、
IO
アクションを呼び出す必要があるたびに、
lift
を使用してこのアクションを
Thread
に挿入し
Thread
。
roundRobin
関数を呼び出すと、スレッドモナドトランスフォーマーが
roundRobin
れ、ストリームプログラムが
IO
の命令の線形シーケンスに崩壊します。
>>> roundRobin mainThread :: IO () Forking thread #1 Forking thread #1 1 Hello 2 Hello 3 Hello 4 5 6 7 8 9 10
さらに、ストリーミングシステムはクリーンです!
IO
だけでなく、他のモナドを展開しても、ストリーム効果を得ることができます! たとえば、ストリーミング
Writer
計算を作成できます。ここで、
Writer
は多くの純粋なモナドの1つです(詳細については、ハブを参照してください)。
import Control.Monad.Trans.Writer logger :: Thread (Writer [String]) () logger = do fork helper lift $ tell ["Abort"] yield lift $ tell ["Fail"] helper :: Thread (Writer [String]) () helper = do lift $ tell ["Retry"] yield lift $ tell ["!"]
今回は
logger
を実行すると、
roundRobin
関数は純粋な
Writer
アクションを生成します。
roundRobin logger :: Writer [String] ()
...また、ロギングコマンドの結果も純粋に抽出できます。
execWriter (roundRobin logger) :: [String]
型が純粋な値、この場合は
String
リストを計算する方法に注目してください。 また、ログに記録された値の実際のストリームを取得できます。
>>> execWriter (roundRobin logger) ["Abort","Retry","Fail","!"]
おわりに
あなたは私が詐欺師であり、主な仕事は
free
ライブラリに行ったと思うかもしれませんが、私が使用したすべての機能は、リサイクル可能な非常に一般的な12行のコードに収まります。
data FreeF fax = Pure a | Free (fx) newtype FreeT fma = FreeT { runFreeT :: m (FreeF fa (FreeT fma)) } instance (Functor f, Monad m) => Monad (FreeT fm) where return a = FreeT (return (Pure a)) FreeT m >>= f = FreeT $ m >>= \v -> case v of Pure a -> runFreeT (fa) Free w -> return (Free (fmap (>>= f) w)) instance MonadTrans (FreeT f) where lift = FreeT . liftM Pure liftF :: (Functor f, Monad m) => fr -> FreeT fmr liftF x = FreeT (return (Free (fmap return x)))
これはHaskellの一般的な傾向です。理論を使用すると、衝撃的な小さなコードで頻繁に使用されるエレガントで強力なソリューションが得られます。
この記事の執筆は、Peng LeeとSteve Zhdantwichの記事「ストリームとイベントを組み合わせるための言語方法」に触発されました。 主な違いは、継続メソッドがフリーモナドの単純なメソッドに置き換えられていることです。