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つずつ、 ForkYield 、および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プログラマーは、「 ThreadThreadF命令から構築された構文ツリーである」と言って、このコードを読みます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の記事「ストリームとイベントを組み合わせるための言語方法」に触発されました。 主な違いは、継続メソッドがフリーモナドの単純なメソッドに置き換えられていることです。

Source: https://habr.com/ru/post/J195274/


All Articles