19

他の安全でない* 操作とは異なり、のドキュメント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 でどのように実装されているかを確認しました。ここで、いくつかの発見を要約します。

  1. GHC は、Haskell を不純で参照透過性のない言語として実装しています。GHC のランタイムは、他の関数型言語と同様に、副作用のある不純な関数を連続的に評価することによって動作します。これが、評価順序が重要な理由です。

  2. unsafeInterleaveIOGHC の Haskell の (通常は) 隠された不純物を公開することにより、単一スレッドのプログラムであっても、あらゆる種類の並行性のバグを導入する可能性があるため、安全ではありません。(iterateeこれは素晴らしくエレガントな解決策のようです。私は確かにそれを使用する方法を学びます。)

  3. IO モナドは厳密でなければなりません。なぜなら、安全で怠惰な IO モナドは RealWorld の正確な (リフトされた) 表現を必要とするからです。これは不可能に思えます。

  4. 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 の小さなモデルを構築することは完全に可能と思われます。そして、参照の透明性を壊すことなく、多くの実用的な目的に役立つ十分な近似が存在する可能性があります。

4

4 に答える 4

18

上部にある 2 つの機能は常に同じです。

v1 = do !a <- x
        y

v2 = do !a <- unsafeInterleaveIO x
        y

結果が強制されるまで操作を延期することに注意してください。ただし、厳密なパターン一致を使用してすぐに強制しているためunsafeInterleaveIO、操作はまったく延期されません。だからとはまったく同じです。IO!av1v2

一般に

unsafeInterleaveIO一般に、 の使用が安全であることを証明するのはあなた次第です。を呼び出す場合は、いつでも呼び出して同じ出力を生成できることunsafeInterleaveIO xを証明する必要があります。x

Lazy IO に関する最新の感情

...Lazy IO は危険であり、99% の確率で悪い考えです。

それが解決しようとしている主な問題は、IO がIOモナドで行われなければならないということですが、インクリメンタル IO を実行できるようにしたいので、より多くを取得するために IO コールバックを呼び出すためにすべての純粋な関数を書き直したくないということです。データ。インクリメンタル IO はメモリの使用量が少なく、アルゴリズムをあまり変更せずにメモリに収まらないデータ セットを操作できるため、重要です。

Lazy IO の解決策は、IOモナドの外で IO を行うことです。これは一般的に安全ではありません。

今日、人々はConduitPipesなどのライブラリを使用して、さまざまな方法でインクリメンタル IO の問題を解決しています。コンジットとパイプは、Lazy IO よりもはるかに決定論的で行儀がよく、同じ問題を解決し、安全でない構造を必要としません。

unsafeInterleaveIOそれは本当にunsafePerformIO別のタイプであることを忘れないでください。

遅延 IO が原因で壊れたプログラムの例を次に示します。

rot13 :: Char -> Char
rot13 x 
  | (x >= 'a' && x <= 'm') || (x >= 'A' && x <= 'M') = toEnum (fromEnum x + 13)
  | (x >= 'n' && x <= 'z') || (x >= 'N' && x <= 'Z') = toEnum (fromEnum x - 13)
  | otherwise = x 

rot13file :: FilePath -> IO ()
rot13file path = do
  x <- readFile path
  let y = map rot13 x
  writeFile path y

main = rot13file "test.txt"

このプログラムは動作しません。 lazy IO を strict IO に置き換えると、機能するようになります。

リンク

Haskell メーリング リストの Oleg Kiselyov によるLazy IO breakpurityから:

遅延 IO が参照透過性をどのように壊すかを示します。型の純関数は、Int->Int->Int引数の評価順序に応じて異なる整数を返します。私たちの Haskell98 コードは、標準入力しか使用しません。Haskell の純度を称賛することと遅延 IO を宣伝することは矛盾していると結論付けます。

...

Lazy IO は良いスタイルとは見なされません。純粋性の一般的な定義の 1 つは、評価順序に関係なく、純粋な式は同じ結果に評価される必要がある、または equals を equals に置き換えることができるというものです。Int 型の式が 1 に評価された場合、結果やその他の観測値を変更することなく、式のすべての出現箇所を 1 に置き換えることができるはずです。

Haskell メーリング リストの Oleg Kiselyov によるLazy vs correct IOから:

結局のところ、観察可能な副作用を伴う「純粋な」関数ほど、Haskell の精神に反するものはありません。Lazy IO では、正しさとパフォーマンスのどちらかを選択する必要があります。このようなコードの出現は、Lazy IO によるデッドロックの証拠がこのリストに掲載されてから 1 か月も経たないうちに特に奇妙です。予測不可能なリソースの使用やファイルを閉じるためのファイナライザーへの依存は言うまでもありません (GHC はファイナライザーがまったく実行されることを保証しないことを忘れてしまいます)。

Kiselyov は、遅延 IO の最初の真の代替手段であるIterateeライブラリを作成しました。

于 2012-11-07T05:39:58.157 に答える
11

遅延性とは、正確に計算が実際に実行されるタイミング (および実行されるかどうか) は、ランタイム実装が値が必要であると判断するタイミング (および実行されるかどうか) に依存することを意味します。Haskell プログラマーとして、評価順序に対する制御を完全に放棄します (ただし、コードに固有のデータ依存関係と、ランタイムに特定の選択を強制するために厳密に遊び始める場合を除きます)。

純粋な計算の結果は、いつ実行してもまったく同じになるため、これは純粋な計算には最適です(ただし、実際には必要のない計算を実行すると、エラーが発生したり、別の評価を実行したときに終了に失敗したりする可能性があります)。順序によってプログラムが正常に終了する可能性がありますが、評価順序によって計算されたすべての非底値は同じになります)。

しかし、IO 依存のコードを書いているときは、評価順序が重要です。の全体的なポイントはIO、ステップがプログラムの外の世界に依存し、影響を与える計算を構築するためのメカニズムを提供することであり、その重要な部分は、それらのステップが明示的に順序付けられることです。を使用unsafeInterleaveIOすると、その明示的な順序付けが破棄され、IO操作が実際にいつ実行されるか (および実行されるかどうか) の制御がランタイム システムに渡されます。

これは一般に IO 操作では安全ではありません。なぜなら、プログラム内のデータ依存関係から推測できない副作用間に依存関係がある可能性があるためです。たとえば、あるIOアクションがデータを含むファイルを作成し、別のIOアクションが同じファイルを読み取る場合があります。両方とも「遅延」して実行される場合、結果の Haskell 値が必要な場合にのみ実行されます。ファイルの作成はおそらくですが、決してそうではないIO ()可能性は十分にあります()必要です。これは、読み取り操作が最初に実行され、失敗するか、ファイルに既に存在するデータを読み取るが、他の操作によってそこに置かれるべきであったデータではないことを意味する可能性があります。ランタイム システムがそれらを正しい順序で実行するという保証はありません。常にこれを行うシステムで正しくプログラミングするには、Haskell ランタイムがさまざまなアクションIOを実行するために選択する順序を正確に予測できなければなりません。IO

アクションがいつ実行されるか、または完全に省略されるかどうかは問題ではないというコンパイラーへの約束として扱いunsafeInterlaveIOます (コンパイラーは検証できず、ただあなたを信頼するだけです) 。これがすべての機能です。一般的には安全ではなく、安全性を自動的にチェックすることはできませんが、特定の場合には安全になる施設を提供します。それらの使用が実際に安全であることを確認する責任はあなたにあります。しかし、コンパイラに対して約束をし、その約束が間違っていた場合、不快なバグが発生する可能性があります。名前の「安全でない」とは、特定のケースについて考えさせ、コンパイラーに対して本当に約束できるかどうかを判断させることです。IOunsafe*

于 2012-11-07T06:01:04.833 に答える
3

基本的に、質問の「更新」の下にあるものはすべて混乱しているので、間違っているわけではないので、私の答えを理解しようとしているときは忘れるようにしてください.

この関数を見てください:

badLazyReadlines :: Handle -> IO [String]
badLazyReadlines h = do
  l <- unsafeInterleaveIO $ hGetLine h
  r <- unsafeInterleaveIO $ badLazyReadlines h
  return (l:r)

私が説明しようとしていることに加えて、上記の関数はファイルの最後に到達することも処理しません。しかし、今はそれを無視してください。

main = do
  h <- openFile "example.txt" ReadMode
  lns <- badLazyReadlines h
  putStrLn $ lns ! 4

これにより、「example.txt」の最初の行が出力されます。これは、リストの 5 番目の要素が実際にはファイルから読み取られる最初の行であるためです。

于 2015-01-19T10:25:33.733 に答える
2

あなたのjoinIOjoinIO'は意味的に同等ではありません。通常は同じですが、微妙な点があります。強打パターンは値を厳密にしますが、それだけです。Bang パターンは を使用して実装されseq、特定の評価順序を強制しません。特に、次の 2 つは意味的に同等です。

a `seq` b `seq` c
b `seq` a `seq` c

GHC は c を返す前に b または a を最初に評価できます。実際、最初に c を評価し、次に a と b を評価し、次に c を返すことができます。または、a または b が底でないこと、または c底であることを静的に証明できる場合、a または b を評価する必要はまったくありません。いくつかの最適化では、この事実を真に利用していますが、実際にはあまり頻繁には使用されません。

unsafeInterleaveIOは対照的に、これらの変更のすべてまたは一部に敏感です。関数がどれほど厳密であるかというセマンティック プロパティに依存するのではなく、何かが評価されるときの操作プロパティに依存します。したがって、上記の変換はすべて目に見えるためunsafeInterleaveIO、IO を非決定論的に実行していると見なすのが妥当である理由は、多かれ少なかれ、適切と思われる場合に限られます。

本質的に、これが が安全でない理由です。これは、unsafeInterleaveIO通常の使用において、意味を維持するはずの変換を検出できる唯一のメカニズムです。これは、評価を検出できる唯一の方法ですが、本来は不可能なはずです。

余談ですが、 のunsafeすべての関数GHC.Prim、およびおそらく他のいくつかのGHC.モジュールの前に精神的に追加することはおそらく公平です。それらは確かに普通の Haskell ではありません。

于 2012-11-09T21:56:11.987 に答える