他の安全でない* 操作とは異なり、のドキュメントでunsafeInterleaveIO
は、考えられる落とし穴についてあまり明確ではありません。では、正確にいつが安全ではないのでしょうか? 並列/同時使用とシングルスレッド使用の両方の条件を知りたいです。
より具体的には、次のコードの 2 つの関数は意味的に同等ですか? そうでない場合、いつ、どのように?
joinIO :: IO a -> (a -> IO b) -> IO b
joinIO a f = do !x <- a
!x' <- f x
return x'
joinIO':: IO a -> (a -> IO b) -> IO b
joinIO' a f = do !x <- unsafeInterleaveIO a
!x' <- unsafeInterleaveIO $ f x
return x'
これを実際に使用する方法は次のとおりです。
data LIO a = LIO {runLIO :: IO a}
instance Functor LIO where
fmap f (LIO a) = LIO (fmap f a)
instance Monad LIO where
return x = LIO $ return x
a >>= f = LIO $ lazily a >>= lazily . f
where
lazily = unsafeInterleaveIO . runLIO
iterateLIO :: (a -> LIO a) -> a -> LIO [a]
iterateLIO f x = do
x' <- f x
xs <- iterateLIO f x' -- IO monad would diverge here
return $ x:xs
limitLIO :: (a -> LIO a) -> a -> (a -> a -> Bool) -> LIO a
limitLIO f a converged = do
xs <- iterateLIO f a
return . snd . head . filter (uncurry converged) $ zip xs (tail xs)
root2 = runLIO $ limitLIO newtonLIO 1 converged
where
newtonLIO x = do () <- LIO $ print x
LIO $ print "lazy io"
return $ x - f x / f' x
f x = x^2 -2
f' x = 2 * x
converged x x' = abs (x-x') < 1E-15
恐ろしいので、深刻なアプリケーションでこのコードを使用することは避けたいと思いますがunsafe*
少なくとも、「収束」が何を意味するかを決定する際に、より厳密な IO モナドで可能になるよりも怠惰になる可能性があり、(私が思うに)より慣用的な Haskell につながります。そして、これは別の疑問を引き起こします: Haskell (または GHC?) の IO モナドのデフォルトのセマンティクスではないのはなぜですか? レイジー IO (GHC は少数の固定されたコマンド セットによってのみ提供される) のリソース管理の問題をいくつか聞いたことがありますが、通常、与えられた例は壊れた makefile のように似ています: リソース X はリソース Y に依存しますが、失敗した場合依存関係を指定すると、X の未定義ステータスが返されます。レイジー IO が本当にこの問題の原因なのでしょうか? (一方で、上記のコードにデッドロックなどの微妙な同時実行バグがある場合、私はそれをより根本的な問題と見なします。)
アップデート
以下の Ben と Dietrich の回答と彼のコメントを読んで、GHC のソース コードを簡単に参照して、IO モナドが GHC でどのように実装されているかを確認しました。ここで、いくつかの発見を要約します。
GHC は、Haskell を不純で参照透過性のない言語として実装しています。GHC のランタイムは、他の関数型言語と同様に、副作用のある不純な関数を連続的に評価することによって動作します。これが、評価順序が重要な理由です。
unsafeInterleaveIO
GHC の Haskell の (通常は) 隠された不純物を公開することにより、単一スレッドのプログラムであっても、あらゆる種類の並行性のバグを導入する可能性があるため、安全ではありません。(iteratee
これは素晴らしくエレガントな解決策のようです。私は確かにそれを使用する方法を学びます。)IO モナドは厳密でなければなりません。なぜなら、安全で怠惰な IO モナドは RealWorld の正確な (リフトされた) 表現を必要とするからです。これは不可能に思えます。
unsafe
安全でないのは IO モナドと関数だけではありません。Haskell 全体 (GHC によって実装されている) は潜在的に安全ではなく、(GHC の) Haskell の「純粋な」関数は、慣習と人々の善意によってのみ純粋です。型が純粋さの証明になることは決してありません。
これを見るために、GHC の Haskell がIO モナドや関数などに関係なく参照透過ではないことを示します。unsafe*
-- An evil example of a function whose result depends on a particular
-- evaluation order without reference to unsafe* functions or even
-- the IO monad.
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE BangPatterns #-}
import GHC.Prim
f :: Int -> Int
f x = let v = myVar 1
-- removing the strictness in the following changes the result
!x' = h v x
in g v x'
g :: MutVar# RealWorld Int -> Int -> Int
g v x = let !y = addMyVar v 1
in x * y
h :: MutVar# RealWorld Int -> Int -> Int
h v x = let !y = readMyVar v
in x + y
myVar :: Int -> MutVar# (RealWorld) Int
myVar x =
case newMutVar# x realWorld# of
(# _ , v #) -> v
readMyVar :: MutVar# (RealWorld) Int -> Int
readMyVar v =
case readMutVar# v realWorld# of
(# _ , x #) -> x
addMyVar :: MutVar# (RealWorld) Int -> Int -> Int
addMyVar v x =
case readMutVar# v realWorld# of
(# s , y #) ->
case writeMutVar# v (x+y) s of
s' -> x + y
main = print $ f 1
簡単に参照できるように、GHC によって実装されている IO モナドに関連する定義をいくつか集めました。(以下のすべてのパスは、ghc のソース リポジトリのトップ ディレクトリからの相対パスです。)
-- Firstly, according to "libraries/base/GHC/IO.hs",
{-
The IO Monad is just an instance of the ST monad, where the state is
the real world. We use the exception mechanism (in GHC.Exception) to
implement IO exceptions.
...
-}
-- And indeed in "libraries/ghc-prim/GHC/Types.hs", We have
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
-- And in "libraries/base/GHC/Base.lhs", we have the Monad instance for IO:
data RealWorld
instance Functor IO where
fmap f x = x >>= (return . f)
instance Monad IO where
m >> k = m >>= \ _ -> k
return = returnIO
(>>=) = bindIO
fail s = failIO s
returnIO :: a -> IO a
returnIO x = IO $ \ s -> (# s, x #)
bindIO :: IO a -> (a -> IO b) -> IO b
bindIO (IO m) k = IO $ \ s -> case m s of (# new_s, a #) -> unIO (k a) new_s
unIO :: IO a -> (State# RealWorld -> (# State# RealWorld, a #))
unIO (IO a) = a
-- Many of the unsafe* functions are defined in "libraries/base/GHC/IO.hs":
unsafePerformIO :: IO a -> a
unsafePerformIO m = unsafeDupablePerformIO (noDuplicate >> m)
unsafeDupablePerformIO :: IO a -> a
unsafeDupablePerformIO (IO m) = lazy (case m realWorld# of (# _, r #) -> r)
unsafeInterleaveIO :: IO a -> IO a
unsafeInterleaveIO m = unsafeDupableInterleaveIO (noDuplicate >> m)
unsafeDupableInterleaveIO :: IO a -> IO a
unsafeDupableInterleaveIO (IO m)
= IO ( \ s -> let
r = case m s of (# _, res #) -> res
in
(# s, r #))
noDuplicate :: IO ()
noDuplicate = IO $ \s -> case noDuplicate# s of s' -> (# s', () #)
-- The auto-generated file "libraries/ghc-prim/dist-install/build/autogen/GHC/Prim.hs"
-- list types of all the primitive impure functions. For example,
data MutVar# s a
data State# s
newMutVar# :: a -> State# s -> (# State# s,MutVar# s a #)
-- The actual implementations are found in "rts/PrimOps.cmm".
したがって、たとえば、コンストラクターを無視して参照透過性を想定すると、次のようになります。
unsafeDupableInterleaveIO m >>= f
==> (let u = unsafeDupableInterleaveIO)
u m >>= f
==> (definition of (>>=) and ignore the constructor)
\s -> case u m s of
(# s',a' #) -> f a' s'
==> (definition of u and let snd# x = case x of (# _,r #) -> r)
\s -> case (let r = snd# (m s)
in (# s,r #)
) of
(# s',a' #) -> f a' s'
==>
\s -> let r = snd# (m s)
in
case (# s, r #) of
(# s', a' #) -> f a' s'
==>
\s -> f (snd# (m s)) s
これは、通常の遅延状態のモナドを束縛することから得られるものではありません。状態変数が何らかの真の意味を持っていると仮定すると(実際にはそうではありません)、通常は「遅延状態モナド」と呼ばれる遅延 IOよりも、並行 IO (または関数が正しく言うようにインターリーブ IOs
) のように見えます。怠惰状態は、連想操作によって適切にスレッド化されます。
私は本当に怠惰な IO モナドを実装しようとしましたが、IO データ型の怠惰なモナド構成を定義するには、RealWorld
. State# s
ただし、との両方のコンストラクターがないため、これは不可能に思えRealWorld
ます。たとえそれが可能であったとしても、現実世界の正確で機能的な表現を表現しなければなりませんが、これもまた不可能です。
しかし、標準の Haskell 2010 が参照の透過性を壊しているのか、それとも遅延 IO 自体が悪いのかはまだわかりません。少なくとも、遅延 IO が完全に安全で予測可能な RealWorld の小さなモデルを構築することは完全に可能と思われます。そして、参照の透明性を壊すことなく、多くの実用的な目的に役立つ十分な近似が存在する可能性があります。