haskell-jp / questions #103 at 2023-03-30 23:01:14 +0900

https://haskell-jp.slack.com/archives/CL3AXB1AL/p1680155886899719 の関係で strict-wrapper のコード () を読んでたんですが,ここの strictunstrict 関数が怖いことをしていると思うので,皆様のご意見を伺いたいです.参照先のコードは大体

data SPair a b = SPair !a !b

strict :: (a, b) -> SPair a b
strict x = case x of
  (!_, !_) -> unsafeCoerce x

unstrict :: SPair a b -> (a, b)
unstrict = unsafeCoerce

のようなことをしており,これって SPair a b(a, b) の runtime representation が完全に一致しているということを主張している気がするんですが,そんな保証ってありましたっけ…?
データコンストラクターの SPair を export せずに SPair a b 型の値は必ず strict によって生成されるようにすることで、内部表現が (a, b) と同じだという保証を得ているんじゃないでしょうか :thinking_face:

スマートコンストラクターパターンですかね。
https://wiki.haskell.org/Smart_constructors
そっちだけのexportなら私も不審には思わなかったんですけど、

constructStrict (x, y) = SPair x y

もexportしているので…
検証するのに便利な関数をやっと見つけたので,解決(?)しました! 結論をいうと,一致 してない ですね.

{-# LANGUAGE UnboxedTuples, MagicHash, BangPatterns #-}
import Data.Void (Void)
import GHC.Exts (unpackClosure#)
import GHC.Ptr (Ptr(..))

tagPtr :: a -> Ptr Void
tagPtr x = case unpackClosure# x of
  (# !addr#, !_, !_ #) -> Ptr addr#

を準備して ghci -fobject-code で見てやると:

ghci> tagPtr $! ((0,1) :: (Int,Int))
0x00000001026870e0
ghci> tagPtr $! ((1,1) :: (Int,Int))
0x00000001026870e0
ghci> tagPtr $! ((2,1) :: (Int,Int))
0x00000001026870e0
ghci> tagPtr $! ((2,2) :: (Int,Int))
0x00000001026870e0
ghci> tagPtr $! ((2,0) :: (Int,Int))
0x00000001026870e0

ghci> data T a b = T !a !b
ghci> tagPtr $! (T 0 1 :: T Int Int)
0x0000000100745008
ghci> tagPtr $! (T 1 1 :: T Int Int)
0x0000000100745008
ghci> tagPtr $! (T 2 1 :: T Int Int)
0x0000000100745008
ghci> tagPtr $! (T 2 2 :: T Int Int)
0x0000000100745008
ghci> tagPtr $! (T 2 0 :: T Int Int)
0x0000000100745008

ghci> data U a b = U a b
ghci> tagPtr $! (U 0 1 :: U Int Int)
0x0000000100746008
ghci> tagPtr $! (U 1 1 :: U Int Int)
0x0000000100746008
ghci> tagPtr $! (U 2 1 :: U Int Int)
0x0000000100746008
ghci> tagPtr $! (U 2 2 :: U Int Int)
0x0000000100746008
ghci> tagPtr $! (U 2 0 :: U Int Int)
0x0000000100746008

と全然違います.ただ,この状態で unsafeCoerce# すると,まるで全てがうまくいったかのように見えますが:

ghci> t0 = T 1 0 :: T Int Int
ghci> tup = unsafeCoerce t0 :: (Int, Int)
ghci> tup
(1,0)

その実,tag は正しくない値を指しています:

ghci> tagPtr tup
0x0000000100745008

これ,いわゆる, undefined behavior が問題を起こしていないだけ,のケースに見えますね.
面白いことに, これを Either 及びそれと同形状の定義を持つ型

data E a b = L a | R b

でやった時にも「tagは違うのに,`L 2 :: L Int Int` を unsafeCoerce することで Either Int Int を作ったあと,それを show させると Left 2 が出てくる」という現象が起きます.多分 case式での照合の時に特定のビットしか見てないんですね (GHC9.4.4).