私が上で見てきたhttps://www.fpcomplete.com/blog/2017/06/tale-of-two-brackets一部をスキミングけれども、私はまだかなり問題の核心は、「理解していないStateT
悪い、IO
ですOK」、Haskellが悪いStateT
モナドを書くことを漠然と許可しているという感覚を得る以外に(または、記事の最終的な例では、MonadBaseControl
ではなくStateT
)。
ハドックでは、次の法則を満たす必要があります。
askUnliftIO >>= (\u -> liftIO (unliftIO u m)) = m
したがって、これはm
、を使用するときにモナドで状態が変更されないことを示しているように見えますaskUnliftIO
。しかし、私の考えではIO
、では、全世界が国家になることができます。たとえば、ディスク上のテキストファイルの読み取りと書き込みを行うことができます。
Michaelによる別の記事を引用するには、
偽りの純度WriterTとStateTは純粋であり、技術的には純粋であると言います。ただし、正直に言うと、StateT内に完全に存在するアプリケーションがある場合、純粋なコードから必要な抑制されたミューテーションのメリットは得られません。スペードをスペードと呼び、可変変数があることを受け入れることもできます。
これは確かにそうだと私に思わせます。IOでは私たちは正直でありStateT
、では、可変性について正直ではありません...しかし、それは上記の法律が示していることとは別の問題のようです。結局のところ、MonadUnliftIO
を想定していIO
ます。IO
他のものよりも制限がどのようになっているのかを概念的に理解するのに苦労しています。
アップデート1
寝た後(一部)、私はまだ混乱していますが、日が経つにつれて徐々に少なくなっています。の法定証明を作成しましたIO
。id
READMEにその存在を実感しました。特に、
instance MonadUnliftIO IO where
askUnliftIO = return (UnliftIO id)
したがってaskUnliftIO
、IO (IO a)
にを返すように見えUnliftIO m
ます。
Prelude> fooIO = print 5
Prelude> :t fooIO
fooIO :: IO ()
Prelude> let barIO :: IO(IO ()); barIO = return fooIO
Prelude> :t barIO
barIO :: IO (IO ())
法則に戻ると、m
変換されたモナド(askUnliftIO
)でラウンドトリップを実行すると、モナドでは状態が変化しないと言っているように見えます。ここで、ラウンドトリップはunLiftIO
->liftIO
です。
、上記の例を再開barIO :: IO ()
私たちはそうならば、barIO >>= (u -> liftIO (unliftIO u m))
そして、u :: IO ()
そしてunliftIO u == IO ()
、その後、liftIO (IO ()) == IO ()
。**基本的にすべてが内部でのアプリケーションid
であるため、を使用していても状態が変更されていないことがわかりますIO
。重要なのは、をa
使用した結果として、の値が実行されたり、他の状態が変更されたりしないことですaskUnliftIO
。もしそうなら、の場合のように、それrandomIO :: IO a
を実行askUnliftIO
しなければ同じ値を取得することはできません。(以下の検証試行1)
しかし、他のモナドが状態を維持していても、同じことができるようです。しかし、一部のモナドでは、そうすることができない場合があることもわかります。不自然な例を考えてみましょう。a
ステートフルモナドに含まれるタイプの値にアクセスするたびに、一部の内部状態が変更されます。
検証の試み1
> fooIO >> askUnliftIO
5
> fooIOunlift = fooIO >> askUnliftIO
> :t fooIOunlift
fooIOunlift :: IO (UnliftIO IO)
> fooIOunlift
5
これまでのところ良好ですが、次のことが発生する理由について混乱しています。
> fooIOunlift >>= (\u -> unliftIO u)
<interactive>:50:24: error:
* Couldn't match expected type `IO b'
with actual type `IO a0 -> IO a0'
* Probable cause: `unliftIO' is applied to too few arguments
In the expression: unliftIO u
In the second argument of `(>>=)', namely `(\ u -> unliftIO u)'
In the expression: fooIOunlift >>= (\ u -> unliftIO u)
* Relevant bindings include
it :: IO b (bound at <interactive>:50:1)
「StateTは悪いです、IOはOKです」
それは実際には記事のポイントではありません。この考え方はMonadBaseControl
、並行性と例外が存在する場合に、ステートフルモナド変換子との紛らわしい(そしてしばしば望ましくない)動作を許可するというものです。
finally :: StateT s IO a -> StateT s IO a -> StateT s IO a
良い例です。「StateT
タイプの可変変数をs
モナドにアタッチしているm
」というメタファーを使用するs
場合、例外がスローされたときにファイナライザーアクションが最新の値にアクセスすることを期待できます。
forkState :: StateT s IO a -> StateT s IO ThreadId
別のものです。入力からの状態の変更が元のスレッドに反映されることを期待するかもしれません。
lol :: StateT Int IO [ThreadId]
lol = do
for [1..10] $ \i -> do
forkState $ modify (+i)
これlol
は(モジュロパフォーマンス)として書き直すことができると思われるかもしれませんmodify (+ sum [1..10])
。しかし、それは正しくありません。の実装はforkState
、初期状態をフォークされたスレッドに渡すだけで、状態の変更を取得することはできません。の簡単で一般的な理解はStateT
ここでは失敗します。
代わりに、StateT s m a
「s
計算によって暗黙的にスレッド化されるタイプのスレッドローカル不変変数を提供するトランスフォーマーとして、より微妙な見方を採用する必要があります。そのローカル変数を同じタイプの新しい値に置き換えることができます。計算の将来のステップのために。」(多かれ少なかれ、の冗長な英語の言い回しs -> m (a, s)
)この理解により、の動作はfinally
もう少し明確になります。これはローカル変数であるため、例外に耐えることはできません。同様に、forkState
より明確になります。これはスレッドローカル変数であるため、別のスレッドに変更しても他のスレッドには影響しません。
これは時々あなたが望むものです。しかし、それは通常、人々がコードIRLを書く方法ではなく、しばしば人々を混乱させます。
長い間、この「下げる」操作を実行するためのエコシステムのデフォルトの選択はでしたがMonadBaseControl
、これには多くの欠点がありました。タイプがわかりにくい、インスタンスを実装するのが難しい、インスタンスを導出できない、動作がわかりにくい場合があります。素晴らしい状況ではありません。
MonadUnliftIO
物事をより単純なモナド変換子のセットに制限し、比較的単純な型、派生可能なインスタンス、および常に予測可能な動作を提供できます。コストはつまりExceptT
、StateT
など変圧器はそれを使用することはできません。
基本的な原則は、可能なことを制限することにより、何が起こるかを理解しやすくすることです。MonadBaseControl
は非常に強力で一般的であり、その結果、使用が非常に難しく、混乱を招きます。MonadUnliftIO
それほど強力ではなく一般的ですが、はるかに使いやすいです。
したがって、これは、askUnliftIOを使用するときに、モナドmで状態が変更されないことを示しているように見えます。
これは真実ではありません-法律はunliftIO
、モナド変換子をに下げる以外は何もしてはならないと述べていますIO
。これがその法則に違反するものです:
newtype WithInt a = WithInt (ReaderT Int IO a)
deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader Int)
instance MonadUnliftIO WithInt where
askUnliftIO = pure (UnliftIO (\(WithInt readerAction) -> runReaderT 0 readerAction))
これが与えられた法則に違反していることを確認しましょう:askUnliftIO >>= (\u -> liftIO (unliftIO u m)) = m
。
test :: WithInt Int
test = do
int <- ask
print int
pure int
checkLaw :: WithInt ()
checkLaw = do
first <- test
second <- askUnliftIO >>= (\u -> liftIO (unliftIO u test))
when (first /= second) $
putStrLn "Law violation!!!"
によって返される値test
とaskUnliftIO ...
下げ/持ち上げが異なるため、法律に違反しています。さらに、観察された効果は異なり、それも素晴らしいことではありません。
この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。
侵害の場合は、連絡してください[email protected]
コメントを追加