Skip to content

Commit d4f6bd5

Browse files
committed
Complete the case of Storable
1 parent c4e7aa5 commit d4f6bd5

File tree

1 file changed

+75
-4
lines changed

1 file changed

+75
-4
lines changed

preprocessed-site/posts/2020/strict-gotchas.md

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ stack exec runghc -- <これから紹介するコードのファイル>.hs
6565
[pfxfncさんのStrict拡張でハマったお話](https://qiita.com/pxfnc/items/a26bda6d11402daba675)という記事でも紹介されてはいますが、まとめ記事なのでここでも改めて取り上げます。
6666

6767
```haskell
68-
main :: IO ()
6968
main = print $ div10 0
7069

7170
div10 :: Int -> Int
@@ -276,13 +275,85 @@ hoge
276275

277276
# Case 5: `undefined`を受け取るメソッド
278277

279-
最後は、ちょっとレアケースではありますが、
278+
サンプル: [storable.hs](https://github.com/haskell-jp/blog/blob/master/examples/2020/strict-gotchas/storable.hs)
279+
280+
最後はちょっとレアケースではありますが、こちら👇のIssueで発覚した問題を解説しましょう。
280281

281282
[#16810: Use explicit lazy binding around undefined in inlinable functions · Issues · Glasgow Haskell Compiler / GHC · GitLab](https://gitlab.haskell.org/ghc/ghc/issues/16810)
282283

283-
サンプル: [storable.hs](https://github.com/haskell-jp/blog/blob/master/examples/2020/strict-gotchas/storable.hs)
284+
問題を簡単に再現するために、次のサンプルコードを用意しました。
284285

285-
hoge
286+
```haskell
287+
-- importなどは当然省略!
288+
data Test = Test Int Int deriving Show
289+
290+
instance Storable Test where
291+
sizeOf _ = sizeOf (1 :: Int) * 2
292+
alignment _ = 8
293+
peek = error "This should not be called in this program"
294+
poke = error "This should not be called in this program"
295+
296+
main = alloca $ \(_ :: Ptr Test) -> putStrLn "This won't be printed when Strict is enabled"
297+
```
298+
299+
はい、適当な型を定義して[`Storable`](https://downloads.haskell.org/~ghc/8.10.1/docs/html/libraries/base-4.14.0.0/Foreign-Storable.html#t:Storable)のインスタンスにして、それに対して[`alloca`](https://downloads.haskell.org/~ghc/8.10.1/docs/html/libraries/base-4.14.0.0/Foreign-Marshal-Alloc.html#v:alloca)を呼ぶだけのコードです。
300+
インスタンス定義がかなり手抜きな感じになっちゃってますが、まぁ今回の問題を再現するのにはこれで十分なので、ご了承ください🙏。
301+
302+
このコード、残念ながら`Strict`拡張を有効にした状態で実行すると、`undefined`による例外が発生してしまいます。
303+
304+
```bash
305+
> stack exec -- runghc --ghc-arg=-XStrict storable.hs
306+
storable.hs: Prelude.undefined
307+
CallStack (from HasCallStack):
308+
error, called at libraries\base\GHC\Err.hs:79:14 in base:GHC.Err
309+
undefined, called at libraries\base\Foreign\Marshal\Alloc.hs:117:31 in base:Foreign.Marshal.Alloc
310+
```
311+
312+
こちらは`Strict`を有効にしなかった場合。やはり例外は起きてませんね😌。
313+
314+
```bash
315+
> stack exec -- runghc storable.hs
316+
This won't be printed when Strict is enabled
317+
```
318+
319+
さてこの、`Strict`拡張を有効にした場合に発生した、`undefined`による例外はどこからやってきたのでしょう?
320+
上記のコードにはいくつか`error`関数を使用している箇所がありますが、発生した例外はあくまでも`undefined`です。見た限り上記のコードそのものから発生した例外ではなさそうですね...🤔。
321+
322+
その答えはなんと、`main`関数で呼んでいる[`alloca`の定義](https://downloads.haskell.org/~ghc/8.10.1/docs/html/libraries/base-4.14.0.0/src/Foreign-Marshal-Alloc.html#alloca)にありました!
323+
324+
```haskell
325+
alloca :: forall a b . Storable a => (Ptr a -> IO b) -> IO b
326+
alloca =
327+
allocaBytesAligned (sizeOf (undefined :: a)) (alignment (undefined :: a))
328+
```
329+
330+
確かに、`sizeOf`メソッドや`alignment`メソッドに`undefined`を渡しています。
331+
これらはいずれも`Storable`型クラスのメソッドなので、上記の`Test`型でももちろん実装しています。
332+
そう、実はこの`sizeOf`メソッドと`alignment`メソッドの実装で、下👇のように引数`_`を定義しているのが問題なのです!
333+
334+
```haskell
335+
instance Storable Test where
336+
sizeOf _ = sizeOf (1 :: Int) * 2
337+
alignment _ = 8
338+
-- ...
339+
```
340+
341+
[「Case 2: ポイントフリースタイルかどうかで変わる!」の節](#TODO)で、「`Strict`拡張を有効にしているモジュールでは、『引数や変数を宣言することすなわちWHNFまで評価すること」』、あるいは『引数や変数を宣言しなければ、評価されない』」と述べたことを再び思い出してください。
342+
こちらの`sizeOf`・`alignment`の定義でも同様に、引数`_`に言及しているため、引数を必ずWHNFまで評価することになっています。
343+
結果、`alloca`関数がそれぞれを呼ぶ際`undefined`を渡しているため、`undefined`を評価してしまい、`undefined`による例外が発生してしまうのです💥。
344+
345+
なぜこのように、`alloca`関数では`sizeOf`や`alignment`に`undefined`をわざわざ渡しているのでしょう?
346+
それは、これらのメソッドがそもそも`undefined`を渡して使うことを前提に設計されているからです。
347+
`sizeOf`・`alignment`はともに`Storable a => a -> Int`という型の関数なので、第一引数に`Storable`のインスタンスである型`a`の値を受け取るのですが、このとき**渡される`a`型の値は、使わない**こととなっています。
348+
[それぞれのメソッドの説明](https://downloads.haskell.org/~ghc/8.10.1/docs/html/libraries/base-4.14.0.0/Foreign-Storable.html#v:sizeOf)にも「The value of the argument is not used.」と書かれていますね。
349+
これは、`sizeOf`も`alignment`も、型毎に一意な値として定まる<small>(引数の値によって`sizeOf`や`alignment`の結果が変わることがない)</small>ので、第一引数の`a`は、単に「この型の`sizeOf`を呼んでくださいね」という**型の**情報を渡すためのものでしかないからなのです。
350+
だから値には関心がないので`undefined`を渡しているわけです。そもそも、`alloca`関数のように引数として`Storable a => a`型の値をとらない関数では、`a`型の値を用意することができませんし。
351+
352+
現代では通常、このように「値に関心がなく、何の型であるかという情報だけを受け取りたい」という場合は、[`Proxy`](https://downloads.haskell.org/~ghc/8.10.1/docs/html/libraries/base-4.14.0.0/Data-Proxy.html#t:Proxy)型を使うのが一般的です。
353+
`Storable`は恐らく`Proxy`が発明される前に生まれたため、`undefined`を渡すことになってしまっているのでしょう。なので、`Storable`型クラスのインスタンスを自前で定義したりしない限り、こうしたケースに出遭うことはまれだと思います。
354+
ただ、それでも`Proxy`を`import`するのを面倒くさがって`undefined`を代わりに渡す、なんてケースもありえるので、`Proxy`を使って定義した型クラスでも同じ問題にハマることはあるかも知れません...。
355+
356+
⚠️結論として、`Storable`型クラスや、`Proxy`を受け取るメソッドを持つ型クラスのインスタンスを、`Strict`拡張を有効にした状態で定義する場合は、`Proxy`にあたる引数を評価しないよう、`~_`などを使って定義しましょう。
286357
287358
# おわりに: やっぱり`Strict`は使う?使わない?
288359

0 commit comments

Comments
 (0)