Skip to content

Commit 6d96c87

Browse files
committed
Complete the case of nested pattern match, plus many modifications
1 parent b0c204c commit 6d96c87

File tree

2 files changed

+79
-19
lines changed

2 files changed

+79
-19
lines changed

examples/2020/strict-gotchas/tuple.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
-- The result of this example doesn't change whether with or without Strict.
22

3-
data MyTuple a b = MyTuple a b deriving Show
3+
data MyTuple a b = MyTuple !a !b deriving Show
44

55
main :: IO ()
66
main = do

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

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,27 @@ Haskellは他の多くのプログラミング言語と異なった特徴を持
1515
遅延評価は、適切に扱えば不要な計算を行わず、計算資源を節約してくれるステキな仕組みですが、一歩使い方を間違うと「サンク」という「これから実行する<small>(かも知れない)</small>計算」を表すオブジェクトが大量の作られてしまい、却ってメモリー消費量が増えてしまう、などといった問題を抱えています。
1616
この現象は「スペースリーク」と呼ばれ、かつて[専門のAdvent Calendar](https://qiita.com/advent-calendar/2015/haskell-space-leaks)が作られたことがあるほど、Haskeller達の関心を集めてきました。
1717

18-
そんなHaskeller達の悩みの種を軽減しようと、GHC 8.0以降、[`Strict`](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#strict-by-default-pattern-bindings)[`StrictData`](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#strict-by-default-pattern-bindings)という拡張が搭載されました
18+
そんなHaskeller達の悩みの種を軽減しようと、GHC 8.0以降、[`Strict`](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#strict-by-default-pattern-bindings)[`StrictData`](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#strict-by-default-pattern-bindings)という言語拡張が搭載されました
1919
これらの拡張を大雑把に言うと、
2020

2121
- `StrictData`: 値コンストラクターにおいて、引数の値が弱頭正規形(Weak Head Normal Form。以降慣習に従い「WHNF」と呼びます)まで評価されるようになる
2222
- `Strict`: `StrictData`の効果に加え、あらゆる関数やローカル変数の定義において、パターンマッチで代入した変数の値がWHNFまで評価されるようになる
2323

2424
というものです。
2525

26-
このうち、`StrictData`は比較的リスクが少なく大変有用(もはや標準であって欲しいぐらい)という声をよく聞きますが[^strictdata-sample]`Strict`については様々な問題点があることが知られています。
26+
このうち、`StrictData`は比較的リスクが少なく大変有用<small>(もはや標準であって欲しいぐらい)</small>という声をよく聞きますが[^strictdata-sample]`Strict`については様々な問題点があることが知られています。
2727
今回はその各種問題点をまとめて共有することで、思い切って`Strict`を有効にするときに参考になる情報を提供したいと思います!
2828

29-
[^strictdata-sample]: 例えばfumievalさんによる[この記事](http://fumieval.hatenablog.com/entry/2015/12/10/200630)
29+
[^strictdata-sample]: 例えばfumievalさんによる[この記事](http://fumieval.hatenablog.com/entry/2015/12/10/200630)より:
30+
> もっとも、日常ではここまで気にしなければいけない場面は少ないので、ほとんどの場合は気にせず感嘆符をつけて大丈夫だろう。GHC 8.0からは、全フィールドをデフォルトで正格にする`StrictData`という拡張が入るため、こちらを使おう。
3031
3132
# 前提知識とその参考資料
3233

3334
以下の知識について、概要を理解しているものとして進めます。
3435
参考になりそうな日本語のページも付記したので、ご覧ください。
3536

3637
- Haskellの遅延評価について
37-
- [実装して理解する遅延評価の仕組み 〜 thunkを絵に描いて理解しよう・JavaScriptでHaskellを実装!? - プログラムモグモグ](https://itchyny.hatenablog.com/entry/20130209/1360417348)が分かりやすいでしょう
38+
- [実装して理解する遅延評価の仕組み 〜 thunkを絵に描いて理解しよう・JavaScriptでHaskellを実装!? - プログラムモグモグ](https://itchyny.hatenablog.com/entry/20130209/1360417348)が詳しくて分かりやすいでしょう
3839
- Haskellの正格評価について
3940
- [正格性のすべて (翻訳)](https://haskell.e-bigmoon.com/posts/2018/06-25-all-about-strictness)
4041
- `Strict``StrictData`について
@@ -58,6 +59,8 @@ stack exec runghc -- <これから紹介するコードのファイル>.hs
5859

5960
# Case 1: `where`句だろうとなんだろうと評価
6061

62+
サンプル: [where.hs](https://github.com/haskell-jp/blog/blob/master/examples/2020/strict-gotchas/where.hs)
63+
6164
最初のケースは、遅延評価で当たり前に享受していたメリットが、`Strict`を有効にしている状態では得られなくなってしまう、というものです。
6265
[pfxfncさんのStrict拡張でハマったお話](https://qiita.com/pxfnc/items/a26bda6d11402daba675)という記事でも紹介されてはいますが、まとめ記事なのでここでも改めて取り上げます。
6366

@@ -108,10 +111,12 @@ where.hs: divide by zero
108111

109112
こうなると、`result`を使用しないケース、すなわち`n == 0 = 0`の場合であっても`result`に <small>(WHNFまで評価した)</small>値を代入するのに必要な計算は実行され、結果<code>10 `div` 0</code>が計算されようとして`divide by zero`が発生するのです。
110113

111-
`where`句は関数定義の後ろの方に書くという性格上、見落としがちかも知れません。注意しましょう。
114+
⚠️`where`句は関数定義の後ろの方に書くという性格上、見落としがちかも知れません。注意しましょう。
112115

113116
# Case 2: ポイントフリースタイルかどうかで変わる!
114117

118+
サンプル: [const.hs](https://github.com/haskell-jp/blog/blob/master/examples/2020/strict-gotchas/const.hs)
119+
115120
続いて、Haskellに慣れた方なら誰もが一度は試したくなる、ポイントフリースタイルに関する落とし穴です。
116121
まずは次の二つの関数をご覧ください。
117122

@@ -125,7 +130,7 @@ referArgs x _ = x
125130

126131
この関数、どちらもやっていることは`const`と変わりません。
127132
`dontReferArgs``const`をそのまま使うことでポイントフリースタイルにしていますが、`referArgs`は自前で引数に言及することで`const`と同等の定義となっています。
128-
ポイントフリースタイルに変えると言うことは原則として元の関数の挙動を変えないワケですから、`dontReferArgs``referArgs`の意味は変わらないはず、ですよね[^opt]
133+
ポイントフリースタイルに変えると言うことは原則として元の関数の挙動を変えないワケですから、`dontReferArgs``referArgs`の意味は変わらないはず、ですよね[^opt]
129134

130135
[^opt]: 実際のところ今回紹介するケース以外にも、ポイントフリースタイルにするかしないかで実行効率などが変わる場合があります。例えば、[Evaluation of function calls in Haskell](https://treszkai.github.io/2019/07/13/haskell-eval)をご覧ください。
131136

@@ -163,11 +168,11 @@ CallStack (from HasCallStack):
163168
`Strict`拡張を有効にした結果、意図せず例外を発生させる値<small>(今回の場合`undefined`)</small>が評価されてしまったのです。
164169

165170
例外を発生させた関数はそう、ポイントフリースタイルでない、`referArgs`関数の方です!
166-
なぜ`referArgs`でのみ例外が発生してしまったのかというと、`referArgs``Strict`拡張を有効にしたモジュールで、引数に言及しているからです
167-
`Strict`拡張を有効にした結果「あらゆる関数やローカル変数の定義において、パターンマッチで代入した変数の値」が評価されるようになるとおり`referArgs`の引数`x``_`も必ず評価されるようになり、このような例外が発生したのです。
171+
なぜ`referArgs`でのみ例外が発生してしまったのかというと、`referArgs``Strict`拡張を有効にしたモジュールで、引数に言及<small>(パターンマッチ)</small>しているからです
172+
`Strict`拡張を有効にした結果「あらゆる関数やローカル変数の定義において、パターンマッチで代入した変数の値」が評価されるとおり`referArgs`の引数`x``_`も必ず評価されるようになり、このような例外が発生したのです。
168173
たとえ使用しない変数`_`でも関係ありません!
169174

170-
そのため、原因の本質は引数に言及<small>(してパターンマッチ)</small>しているか否かであり、`Prelude``const`を使用しているか否かではありません。
175+
そのため、原因の本質は引数に言及<small>(パターンマッチ)</small>しているか否かであり、`Prelude``const`を使用しているか否かではありません。
171176
こちら👇のように引数に言及した上で`const`を使っても、結果は同じなのです。
172177

173178
```haskell
@@ -180,11 +185,11 @@ print $ referArgsByConst "referArgsByConst" (undefined :: Int)
180185
```
181186

182187
一方、`dontReferArgs`については、引数に言及せず、`Prelude`にある`const`をそのまま使っています。
183-
`Strict`拡張ではあくまでも「パターンマッチした変数」のみをWHNFまで評価するようになるものであり、あらゆる関数が正格に呼び出されるわけではありません。
188+
`Strict`拡張はあくまでも「パターンマッチした変数」のみをWHNFまで評価するものであり、あらゆる関数が正格に呼び出されるわけではありません。
184189
なので通常の`Prelude`における`const`と同様、`dontReferArgs`も第2引数は評価しないため、`undefined`を渡しても例外は起こらなかったのです。
185190

186-
このことは、「`Strict`拡張を有効にしているモジュールの中でも、`Strict`を有効にしていないモジュールから`import`した関数は、引数を正格に評価しない」という忘れてはならないポイントも示しています。
187-
例えば`const`よりももっと頻繁に使われるであろう、言及する引数を一つ削除する演算子の代表である、関数合成`.`を使ったケースを考えてみてください。
191+
このことは、「`Strict`拡張を有効にしているモジュールの中でも、`Strict`を有効にしていないモジュール<small>(この場合は`Prelude`)</small>から`import`した関数は、引数を正格に評価しない」という忘れてはならないポイントも示しています。
192+
例えば`const`よりももっと頻繁に使われるであろう、言及する引数を一つ削除する演算子の代表、関数合成`.`を使ったケースを考えてみてください。
188193

189194
ポイントフリースタイルに慣れた方なら、関数適用`$`を次👇のように使って定義した`f`を見ると、
190195

@@ -203,10 +208,11 @@ f = map (+ 3) . filter (> 2)
203208

204209
しかし、`Strict`を有効にしたモジュールでこのような書き換えを行うと、`f`の挙動が変わってしまいます。
205210
引数`.`を使って書き換える前は、引数`xs`に言及していたところ`.`を使って引数`xs`に言及しなくなったからです。
206-
こうした書き換えによって、**`Strict`拡張を有効にしていても意図せず遅延評価してしまう**というリスクがあるので、リファクタリングの際はくれぐれも気をつけてください[^list]
211+
`filter``map``Strict`拡張を有効にしたモジュールで定義されているわけではないので、引数を正格に評価しないんですね。
212+
結果、こうした書き換えによって、**`Strict`拡張を有効にしていても意図せず遅延評価してしまう**、というリスクがあるので、リファクタリングの際はくれぐれも気をつけてください[^list]
207213
ざっくりまとめると、`Strict`拡張を有効にしているモジュールでは、「引数や変数を宣言することすなわちWHNFまで評価すること」、あるいは「引数や変数を宣言しなければ、評価されない」と意識しましょう。
208214

209-
[^list]: もっとも、この場合引数はリストでしょうから、WHNFまでのみ正格評価するメリットは少なそうですが。
215+
[^list]: もっとも、この場合引数は多分リストでしょうから、WHNFまでのみ正格評価するメリットは少なそうですが。
210216

211217
ちなみに、`referArgs`における`_`のように「`Strict`拡張を有効にした場合さえ、使用していない引数が評価されてしまうのは困る!」という場合は、引数名の前にチルダ`~`を付けてください。
212218

@@ -215,15 +221,69 @@ referArgs :: a -> b -> a
215221
referArgs x ~_ = x
216222
```
217223

218-
# Case hoge: 内側のパターンはやっぱりダメ
224+
# Case 3: 内側のパターンはやっぱりダメ
219225

220-
hoge
226+
サンプル: [tuple.hs](https://github.com/haskell-jp/blog/blob/master/examples/2020/strict-gotchas/tuple.hs)
227+
228+
続いては、`Strict`拡張のドキュメントでも触れられている、入れ子になったパターンマッチングにおける問題を紹介します。
229+
一言で言うと、`let (a, b) = ...`のような、データ構造<small>(この場合タプルですね)</small>の「内側」に対するパターンマッチは、`Strict`拡張を有効にしていても正格に評価しないよ、という話です。
230+
231+
例えば、下記のコードを`Strict`拡張付きで実行しても、パターンマッチしている`a``b`ともに代入した時点では正格評価されず、`error "a"``error "b"`による例外はいずれも発生しません。
232+
233+
```haskell
234+
(a, b) = (error "a", error "b")
235+
```
236+
237+
これは、タプルの値コンストラクター`(,)`が<small>(`StrictData`やStrictness flagを用いないで定義されているので)</small>各要素を遅延評価することとは**関係なく**、各要素を正格評価する値コンストラクターであっても同様です。
238+
239+
```haskell
240+
data MyTuple a b = MyTuple !a !b deriving Show
241+
242+
MyTuple a b = MyTuple (error "a") (error "b")
243+
```
244+
245+
先ほどの節における「`Strict`拡張を有効にしているモジュールでは、『引数や変数を宣言することすなわちWHNFまで評価すること」』、あるいは『引数や変数を宣言しなければ、評価されない』と意識しましょう」という主張を真に受けてしまうと意図せず遅延評価させてしまい、ハマりそうです😰。
246+
⚠️繰り返しますが「内側のパターンにおける変数は正格評価されない」ということも意識してください。
221247

222-
# Case hoge: `foldl``foldr`
248+
以上のことを試していただくために、サンプルコードでは`Strict`を有効にしてもしなくても、結果が変わらないサンプルを用意しました<small>(コードは👆の例に`main`がくっついたのとあまり変わらないので省きます)</small>。
249+
以下、実行例を。
250+
251+
```bash
252+
> stack exec runhaskell ./tuple.hs
253+
"Default tuple"
254+
"Default tuple in MyTuple1"
255+
"Other value in MyTuple1"
256+
257+
> stack exec -- runghc --ghc-arg=-XStrict .\tuple.hs
258+
"Default tuple"
259+
"Default tuple in MyTuple1"
260+
"Other value in MyTuple1"
261+
```
262+
263+
# Case 4: `foldl``foldr`
264+
265+
ここの話はちょっと難しいので、先に守るべくルールを述べておきます。
266+
「遅延評価する関数を受け取る前提の高階関数に、
267+
268+
より具体的には、`foldr`に<small>(`Strict`拡張などで)</small>引数を正格に評価するよう定義された関数を渡すのは止めましょう、という話です。
269+
`Strict`拡張を有効にした状態ではラムダ式にも注意しないといけないもポイントです。
270+
271+
サンプル1: [stackoverflow-foldl.hs](https://github.com/haskell-jp/blog/blob/master/examples/2020/strict-gotchas/stackoverflow-foldl.hs)
272+
273+
サンプル2: [stackoverflow-foldr.hs](https://github.com/haskell-jp/blog/blob/master/examples/2020/strict-gotchas/stackoverflow-foldr.hs)
223274

224275
hoge
225276

226-
# Case hoge: `undefined`を受け取るメソッド
277+
# Case 5: `undefined`を受け取るメソッド
278+
279+
最後は、ちょっとレアケースではありますが、
280+
281+
[#16810: Use explicit lazy binding around undefined in inlinable functions · Issues · Glasgow Haskell Compiler / GHC · GitLab](https://gitlab.haskell.org/ghc/ghc/issues/16810)
282+
283+
サンプル: [storable.hs](https://github.com/haskell-jp/blog/blob/master/examples/2020/strict-gotchas/storable.hs)
227284

228285
hoge
229286

287+
# おわりに: やっぱり`Strict`は使う?使わない?
288+
289+
hoge

0 commit comments

Comments
 (0)