@@ -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
215221referArgs 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 .\t uple.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
224275hoge
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
228285hoge
229286
287+ # おわりに: やっぱり` Strict ` は使う?使わない?
288+
289+ hoge
0 commit comments