Haskellの33行のゼロからの協調スレッド

Haskellは、数学とコンピューターサイエンスに深い文化的ルーツを持っているという点で、ほとんどの関数型言語とは異なります。 しかし、Haskellをよく知れば知るほど、多くの一般的なプログラミングの問題に対する理論が最も実用的な解決策であるという事実に感謝するようになります。 この記事では、利用可能な理論的基盤を組み合わせて、クリーンなユーザーストリームシステムを作成するという事実によって、この観点を強調したいと思います。





種類



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)
      
      





これがどのように機能するかを完全に理解する必要はありませんが、各コマンドの戻り値が子ノードフィールドに格納するものに対応していることに気付く場合を除きます。



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の記事「ストリームとイベントを組み合わせるための言語方法」に触発されました。 主な違いは、継続メソッドがフリーモナドの単純なメソッドに置き換えられていることです。



All Articles