写真のモナドコント(Haskell)

続編とモナドContTに関するハブに関する記事がすでにありました 。 この問題についての私の理解を共有し、関連するイラストを提供することにしました。 上記の記事とは異なり、 ContTモナドの内部デバイスにもっと注意を払い、それがどのように機能するかを理解したいと思います。


内部の制御


まず、 ContTの定義を見てください

newtype ContT rma = ContT {runContT :: ((a -> mr) -> mr)} 

タイプを見ると、どういうわけかすぐに不快になります。 また、 callCCのタイプからは、さらに不快になります。

 callCC :: ((a -> mb) -> ma) -> ma 

正しくしましょう。
まず、「(a-> mr)-> m r」型の変数がある場合、これは「a」型の変数がある場合とほぼ同じであることに注意してください。 ここを見て。 タイプが「a」の変数と「a-> mr」の関数があるとします。 片方を他方に適用して、結果「mr」を取得できます。
 a -> mr $ a = mr 

ここで、 "(a-> mr)-> m r"型の変数と "a-> mr"関数があるとします。 同じ方法で一方を他方に適用し、結果を取得できます。
 (a -> mr) -> mr $ (a -> mr) = mr 

写真に移りましょう。
このような矢印付きの円を使用して、タイプ「a」のオブジェクトを示します。


「a-> b」のような関数は、入力と出力がある円のように見えます。


その後、 ContTはリングのようになります。


関数「a-> b」をオブジェクト「(a-> mr)-> m r」に適用すると、次のようになります。


そして今、最も重要なこと。 ContTはモナドです。つまり、 戻り関数とバインド関数が定義されていることを覚えています。 彼らが何をするか見てみましょう。

リターンを使用すると、すべてが非常に簡単になります。 渡された関数にオブジェクトを代入し、結果が直接外部に返されます。
これがバインドの仕組みです。

ご覧のとおり、1つのリングが別のリングに埋め込まれています。 つまり 新しい計算はそれぞれ前の計算に組み込まれます。 考えるべきことがあります。 現在の計算は、後続のすべての計算を関数として受け取ります。 これは、この関数を呼び出して結果を計算できることを意味します。 または、数回呼び出して、結果を結合します。 または、まったくないかもしれませんが、すぐに結果を返します。


ファイルを操作する

ファイルを開き、その中の別のファイルの名前を読み、次にそのファイルを開き、その中の3番目のファイルの名前を読み、3番目のファイルを開いて、画面に内容を表示する必要があるとします。 通常、このようなものを書きます。

 withFileContent :: FilePath -> (String -> IO a) -> IO a withFileContent file fun = withFile file ReadMode $ \h -> hGetContents h >>= fun main = do withFileContent "1.txt" $ \file1_content -> do withFileContent file1_content $ \file2_content -> do withFileContent file2_content $ \file3_content -> do print file3_content 

ここでは、新しいファイルを開くたびにインデントが増加する様子を確認できます。 すべてのファイルが同じモナド内で開くように、この部分を書き直すことは素晴らしいことです。 withFileContent関数を見てみましょう。 その型は、 ContT型に非常に似ていることがわかります。 ContTモナドを使用して例を書き換えましょう。
 doContT cont = runContT cont (const $ return ()) main = doContT $ do file1_content <- ContT $ withFileContent "1.txt" file2_content <- ContT $ withFileContent file1_content file3_content <- ContT $ withFileContent file2_content liftIO $ print file3_content 

とても美しいです。 このプログラムがどのように機能するかを推測しますか? ファイルは最初に指定された順序で開かれ、次に結果が画面に表示され、次にファイルが逆の順序で閉じられます。 それは素晴らしいことではありません!

コンストラクタ、デストラクタ

何らかのオブジェクトを操作するには、まずそのオブジェクトのコンストラクターを呼び出し、作業後にデストラクタを呼び出す必要があるとします。 そのようなオブジェクトの型クラスを作成します。
 class Constructed a where constructor :: a -> IO () destructor :: a -> IO () 

テストオブジェクトを作成してみましょう。
 newtype Obj a = Obj a instance (Show a) => Constructed (Obj a) where constructor (Obj a) = putStr $ "constructor for " ++ (show a) ++ "\n" destructor (Obj a) = putStr $ "destructor for " ++ (show a) ++ "\n" 

ContTでそのようなオブジェクトを操作するための関数withConstructedを作成します。 これで、コンストラクタとデストラクタが自動的に呼び出されます(逆の順序で)。
 withConstructed :: (Constructed a) => a -> ContT r IO a withConstructed obj = ContT $ \fun -> do constructor obj rez <- fun obj destructor obj return rez main = doContT $ do a <- withConstructed $ Obj "ObjectA" b <- withConstructed $ Obj "ObjectB" c <- withConstructed $ Obj "ObjectC" liftIO $ putStr "do something\n" {- : constructor for "ObjectA" constructor for "ObjectB" constructor for "ObjectC" do something destructor for "ObjectC" destructor for "ObjectB" destructor for "ObjectA" -} 


割り込みコンピューティング

前の例は非常に単純で、本質的に同じでした。 そして、計算の終了を待たずにContTを使用して結果を返す方法を教えてください。 例を考えてみましょう。 ContTに何らかの計算があるとします
 calc :: ContT rm Int calc = do x <- return 10 y <- return $ x*2 z <- return $ x + y return z main = runContT calc print -- : 30 

calcを次のように書き換えます
 calc = ContT $ \k -> runContT (do -- <- /  x <- return 10 y <- return $ x*2 z <- return $ x + y return z) k main = runContT calc print -- : 30 

何も変更せず、コンストラクタを削除してから、コンストラクタで再度ラップしました。
次に行を追加します。
 calc = ContT $ \k -> runContT (do x <- return 10 y <- return $ x*2 ContT $ \c -> c 5 -- <-   ( "return 5") z <- return $ x + y return z) k main = runContT calc print -- : 30 

結果はまだ変更されていません。 当然のことながら、「ContT $ \ c-> c 5」という行は「return 5」という行と同等です。
そして今、最も難しい部分です。 「c」を「k」に置き換えましょう。
 calc = ContT $ \k -> runContT (do x <- return 10 y <- return $ x*2 ContT $ \c -> k 5 -- <-    "c"  "k" z <- return $ x + y return z) k main = runContT calc print -- <-  "print"   ContT -- : 5 -- <-   

結果は5になりました。ここで何が起こったのかを説明するのはかなり困難ですが、試してみます。 図を検討してください。

オレンジ色のリングはローカル変数cです。 これらは、「ContT $ \ c-> k 5」行の後に続く計算です。 緑の円はk変数です。 コードをさらに見ると、「k」は印刷機能にすぎないことがわかります。
したがって、値「5」を変数「c」に渡し、変数「c」はprint関数を使用して結果を表示します。 「c」を「k」に置き換えると、次の図が表示されます。

ここで、後続のすべての計算を無視し、すぐに値「5」を「print」関数に渡します。
さらに、プログラムの動作は変更しませんが、同等のコード変換を生成します。 最初に、定数「5」を「括弧」で囲みます。
 calc = ContT $ \k -> runContT (do x <- return 10 y <- return $ x*2 (\a -> ContT $ \c -> ka) 5 -- <-    "a" z <- return $ x + y return z) k 

ラムダ括弧 "(\ a-> ContT $ \ c-> ka)"。
 calc = ContT $ \k -> runContT ((\exit -> do -- <-    "exit" x <- return 10 y <- return $ x*2 exit 5 z <- return $ x + y return z) (\a -> ContT $ \c -> ka)) k 

ここで、「\ exit-> do ... return z」という式全体を取り出します。
 calc = (\f -> ContT $ \k -> runContT (f (\a -> ContT $ \c -> ka)) k) $ \exit -> do -- ^    "f" x <- return 10 y <- return $ x*2 exit 5 z <- return $ x + y return z 

本文に「(\ f-> ContT ... ka))k)」という別の関数を作成する必要があります。 ところで、おめでとうございます! バイク callCC関数を発明しました。
 callCC f = ContT $ \k -> runContT (f (\a -> ContT $ \_ -> ka)) k calc = callCC $ \exit -> do -- <-     "callCC" x <- return 10 y <- return $ x*2 exit 5 z <- return $ x + y return z 

もちろん、プログラムは絶対に愚かであることが判明したため、計算を中断することを学びました。

PS


同じ方法でgetCC関数の本体を推測しようとしましたが、 それに十分な頭脳がないことがわかりました。 たぶんあなたはそれをすることができます。

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


All Articles