Skip to content

Commit b0c204c

Browse files
committed
Complete the case of point free style
1 parent 166eaae commit b0c204c

File tree

2 files changed

+109
-12
lines changed

2 files changed

+109
-12
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
dontReferArgs :: a -> a -> a
1+
dontReferArgs :: a -> b -> a
22
dontReferArgs = const
33

4-
referArgs :: a -> a -> a
4+
referArgs :: a -> b -> a
55
referArgs x _ = x
66

77
main :: IO ()
88
main = do
9-
print $ dontReferArgs "dontReferArgs" undefined
10-
print $ referArgs "referArgs" undefined
9+
print $ dontReferArgs "dontReferArgs" (undefined :: Int)
10+
print $ referArgs "referArgs" (undefined :: Int)

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

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ 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」と呼びます)まで評価されるようになる
22-
- `Strict`: 値コンストラクターを含めたあらゆる関数やローカル変数の定義において、パターンマッチで代入した変数の値がWHNFまで評価されるようになる
22+
- `Strict`: `StrictData`の効果に加え、あらゆる関数やローカル変数の定義において、パターンマッチで代入した変数の値がWHNFまで評価されるようになる
2323

2424
というものです。
2525

@@ -54,7 +54,7 @@ stack exec runghc -- <これから紹介するコードのファイル>.hs
5454

5555
実際に試すときは`-XStrict`というオプションを`runghc`に付けた場合と付けなかった場合両方で実行して、違いを確かめてみてください。
5656

57-
なお、使用したGHCのバージョンは8.6.5で、OSはWindows 10 ver. 1909です。
57+
なお、使用したGHCのバージョンは8.8.3で、OSはWindows 10 ver. 1909です。
5858

5959
# Case 1: `where`句だろうとなんだろうと評価
6060

@@ -110,15 +110,112 @@ where.hs: divide by zero
110110

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

113-
# Case hoge: ポイントフリースタイルかどうかで変わる!
113+
# Case 2: ポイントフリースタイルかどうかで変わる!
114114

115-
hoge
115+
続いて、Haskellに慣れた方なら誰もが一度は試したくなる、ポイントフリースタイルに関する落とし穴です。
116+
まずは次の二つの関数をご覧ください。
116117

117-
# Case hoge: 内側のパターンはやっぱりダメ
118+
```haskell
119+
dontReferArgs :: a -> b -> a
120+
dontReferArgs = const
118121

119-
hoge
122+
referArgs :: a -> b -> a
123+
referArgs x _ = x
124+
```
125+
126+
この関数、どちらもやっていることは`const`と変わりません。
127+
`dontReferArgs``const`をそのまま使うことでポイントフリースタイルにしていますが、`referArgs`は自前で引数に言及することで`const`と同等の定義となっています。
128+
ポイントフリースタイルに変えると言うことは原則として元の関数の挙動を変えないワケですから、`dontReferArgs``referArgs`の意味は変わらないはず、ですよね[^opt]
129+
130+
[^opt]: 実際のところ今回紹介するケース以外にも、ポイントフリースタイルにするかしないかで実行効率などが変わる場合があります。例えば、[Evaluation of function calls in Haskell](https://treszkai.github.io/2019/07/13/haskell-eval)をご覧ください。
131+
132+
ところがこれらの関数を`Strict`拡張を有効にした上で定義すると、なんと挙動が異なってしまいます!
133+
134+
使用例:
135+
136+
```haskell
137+
main :: IO ()
138+
main = do
139+
print $ dontReferArgs "dontReferArgs" (undefined :: Int)
140+
print $ referArgs "referArgs" (undefined :: Int)
141+
```
142+
143+
実行結果(Strict拡張を有効にしなかった場合):
144+
145+
```bash
146+
> stack exec runghc const.hs
147+
"dontReferArgs"
148+
"referArgs"
149+
```
150+
151+
実行結果(Strict拡張を有効にした場合):
152+
153+
```bash
154+
> stack exec -- runghc --ghc-arg=-XStrict const.hs
155+
"dontReferArgs"
156+
const.hs: Prelude.undefined
157+
CallStack (from HasCallStack):
158+
error, called at libraries\base\GHC\Err.hs:80:14 in base:GHC.Err
159+
undefined, called at const.hs:10:34 in main:Main
160+
```
161+
162+
はい、`where`句のケースと同様、`Strict`拡張を有効にした場合、例外が発生してしまいました❗️
163+
`Strict`拡張を有効にした結果、意図せず例外を発生させる値<small>(今回の場合`undefined`)</small>が評価されてしまったのです。
164+
165+
例外を発生させた関数はそう、ポイントフリースタイルでない、`referArgs`関数の方です!
166+
なぜ`referArgs`でのみ例外が発生してしまったのかというと、`referArgs``Strict`拡張を有効にしたモジュールで、引数に言及しているからです。
167+
`Strict`拡張を有効にした結果「あらゆる関数やローカル変数の定義において、パターンマッチで代入した変数の値」が評価されるようになるとおり、`referArgs`の引数`x``_`も必ず評価されるようになり、このような例外が発生したのです。
168+
たとえ使用しない変数`_`でも関係ありません!
169+
170+
そのため、原因の本質は引数に言及<small>(してパターンマッチ)</small>しているか否かであり、`Prelude``const`を使用しているか否かではありません。
171+
こちら👇のように引数に言及した上で`const`を使っても、結果は同じなのです。
120172

121-
## [strict-types](https://github.com/pepeiborra/strict-types)が使えるかも
173+
```haskell
174+
referArgsByConst :: a -> b -> a
175+
referArgsByConst x y = const x y
176+
```
177+
178+
```haskell
179+
print $ referArgsByConst "referArgsByConst" (undefined :: Int)
180+
```
181+
182+
一方、`dontReferArgs`については、引数に言及せず、`Prelude`にある`const`をそのまま使っています。
183+
`Strict`拡張ではあくまでも「パターンマッチした変数」のみをWHNFまで評価するようになるものであり、あらゆる関数が正格に呼び出されるわけではありません。
184+
なので通常の`Prelude`における`const`と同様、`dontReferArgs`も第2引数は評価しないため、`undefined`を渡しても例外は起こらなかったのです。
185+
186+
このことは、「`Strict`拡張を有効にしているモジュールの中でも、`Strict`を有効にしていないモジュールから`import`した関数は、引数を正格に評価しない」という忘れてはならないポイントも示しています。
187+
例えば`const`よりももっと頻繁に使われるであろう、言及する引数を一つ削除する演算子の代表である、関数合成`.`を使ったケースを考えてみてください。
188+
189+
ポイントフリースタイルに慣れた方なら、関数適用`$`を次👇のように使って定義した`f`を見ると、
190+
191+
```haskell
192+
f xs = map (+ 3) $ filter (> 2) xs
193+
194+
-- あるいは、`$`を使わないでこのように書いた場合も:
195+
f xs = map (+ 3) (filter (> 2) xs)
196+
```
197+
198+
こちら👇のように書き換えたくなってうずうずするでしょう。
199+
200+
```haskell
201+
f = map (+ 3) . filter (> 2)
202+
```
203+
204+
しかし、`Strict`を有効にしたモジュールでこのような書き換えを行うと、`f`の挙動が変わってしまいます。
205+
引数`.`を使って書き換える前は、引数`xs`に言及していたところ`.`を使って引数`xs`に言及しなくなったからです。
206+
こうした書き換えによって、**`Strict`拡張を有効にしていても意図せず遅延評価してしまう**というリスクがあるので、リファクタリングの際はくれぐれも気をつけてください[^list]
207+
ざっくりまとめると、`Strict`拡張を有効にしているモジュールでは、「引数や変数を宣言することすなわちWHNFまで評価すること」、あるいは「引数や変数を宣言しなければ、評価されない」と意識しましょう。
208+
209+
[^list]: もっとも、この場合引数はリストでしょうから、WHNFまでのみ正格評価するメリットは少なそうですが。
210+
211+
ちなみに、`referArgs`における`_`のように「`Strict`拡張を有効にした場合さえ、使用していない引数が評価されてしまうのは困る!」という場合は、引数名の前にチルダ`~`を付けてください。
212+
213+
```haskell
214+
referArgs :: a -> b -> a
215+
referArgs x ~_ = x
216+
```
217+
218+
# Case hoge: 内側のパターンはやっぱりダメ
122219

123220
hoge
124221

0 commit comments

Comments
 (0)