haskell-jp / questions #99 at 2022-01-11 03:03:42 +0900

IOの正体はRealWorldに対する読み書きを表す(State# RealWorld -> (# State# RealWorld, a #))だと聞いたのですが、RealWorldからの読み出しのみすることを表す(State# RealWorld -> a)に対応する型はありますか?
そういう型の値も作れると思います(有用かどうかは別にして
f :: State# RealWorld -> a
f = const undefined

(`undefined` 使ったらそれは作れるというのか:thinking_face:
んー、 箇条書きにすると、
State# RealWorld -> aRealWorld からの読み出しを表さない
• ので、無意味なので State# RealWorld -> a を表す newtype は普通のライブラリからはエクスポートされてない
• けど、 newtype Bad a = Bad (State# RealWorld -> a) ってやれば自分で定義することはできる
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #)) なのだから、次の ioToBad 関数が書けるので、任意の IO a から Bad a が作れる。読み書きのどっちであるかは関係ない。 そしてこの Bad a の値は(きちんと意味のある I/O 操作をするために I/O操作たちの間で順序を強制するための)役には立たない。
ioToBad :: IO a -> Bad a
ioToBad (IO f) = Bad $ \s0 -> case (f s0) of (# s1, a #) -> a

ってところだと思います。
ちなみになんで「読み」を表せないかというと、 State# RealWorld の値は世界全体を保存している訳ではないので、例えば

newIORef# :: a -> State# RealWorld -> (# State# RealWorld, IORef a #)
newIORef# a = case newIORef a of IO x -> x

writeIORef# :: IORef a -> a -> State# RealWorld -> (# State# RealWorld, () #)
writeIORef# ref a = case writeIORef ref a of IO f -> f

readIORef# :: IORef a -> State# RealWorld -> (# State# RealWorld, a #)
readIORef# ref = case readIORef ref of IO f -> f

badReadIORef# :: IORef a -> State# RealWorld -> a
badReadIORef# ref = case ioToBad (readIORef ref) of Bad f -> f

の状態で
\s0 -> let (# s1, ref1 #) = newIORef# (0::Int) s0
           a              = badReadIORef# ref s1
           (# s2, () #)   = writeIORef# (2::Int) s1
           (# s3, ref2 #) = seq a (newIORef# a) s2
        in readIORef# ref2 s3

みたいな関数を書くと、`a` は s1 のタイミングの ref1 の値 0 ではなく、`a` が評価されたタイミング (`s2` のタイミング) の値 2 になってしまうので
なんかアレで良くないな、という。
指摘されている通り、通常のユーザーが使う型として定義することにほとんど意味はありませんが、unsafePerformIOの定義に使われるrunRW#の引数に出現したりします 
確かに! その o って、現状多くののユースケースで (# State# s, a #) の形をしていますが、 runST やら unsafePerformIO で使うなら別に実際の実装のように

runST (ST f) = case runRW# f of (# !_, a #) -> a

としなくても

runST (ST f) = runRW# $ \s -> case f s of (# !_, a #) -> a

としても構わないですしね(ほんとか…?)。
そうしたら、評価順序の正格性が保証されていないHaskellではうまく「RealWorldからの読み出しのみを行う」を型で表すのはできないということでしょうか
モジュール境界を使えばそういう意味を持つ newtype を作ることはできますが、定義から原理的に読み出ししかできない型っていうのは無理じゃないかなと思います…。
リアルワールドから読み出しをするとその後のリアルワールドは変化しているはずで、変化した後のリアルワールドを使ってその後の IO をしないといけなくなる。
State# RealWorld -> a 単体で実行しちゃうと上記の「変化した後のリアルワールド」を見失ってしまうのでその後はもう IO ができなくなってしまうように思います。
:naruhodo: