haskell-jp / questions #102 at 2022-09-19 16:01:28 +0900

はじめて質問させていただきます。
Haskell学習中の者で、理解が進まない箇所があるのでお力添えいただきたく思います:man-bowing:
(続く...)
少し長いのでスレッドにて続きを書かせていただきます。

末尾再起最適化について学習をしているのですが、
①「Haskellは遅延評価なので末尾再起をしてもうまみがない」と言っている記事がある
②「末尾再起として挙げられているコードがそもそも末尾再起じゃない」という指摘をされている記事がある
ため、混乱を極めております。

関数の処理の末尾が再起呼び出しで終了している
末尾再起最適化することによって関数呼び出しごとにコールスタックを生成しなくなるのでスタックオーバーフローしない
というのが今のところの理解です。

そこで質問なのですが、
①以下のb,cの例は末尾再起最適化されている例といえるでしょうか?
②末尾再起最適化されている場合、実行時には実際にコールスタックの生成が抑制されているのでしょうか?
a. よく見る末尾再起じゃない定義
fib :: Integer -> Integer
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)
=======================================================
b. 末尾再起?
fibResult :: (Integer, Integer, Integer) -> Integer
fibResult (x, _, 0) = x
fibResult (x, y, idx) = fibResult (y, x + y, idx - 1)

fib :: Integer -> Integer
fib x = fibResult (0, 1, x)
=======================================================
c. 末尾再起?
fib :: Integer -> Integer
fib n = fib' n 1 0
    where
        fib' m x y
            | m == 0 = 1
            | m == 1 = x
            | otherwise = fib' (m - 1) (x + y) (x)
「Haskellは遅延評価なので末尾再起をしてもうまみがない」だと思ってますが他の人の意見も聞きたい
あと、「式」の関数に「末尾再帰」は当てはまらない気がしますがどうなんでしょう
「末尾再帰」は「手続き」に対してのみ「末尾再帰かどうかが言える」んじゃないかなと思うんですが?
質問に質問を付け加えてしまってすみません
かりんとう
間違ってたらすみませんがコールスタックはHaskellにないような気が。
「Haskellは末尾再帰をしてもうまみがない」はその通りだと思います、コールスタックがなく、サンク(予定されている計算を記憶)があります。
また、Haskellでは再帰を末尾にすることによる最適化もされないと認識しています。

その例だとサンクが潰れていませんので、計算が実際に実行される前にはサンクによりメモリが使用されます。
もしサンクを潰したければ、正格評価にする必要があります。(代わりに無限リストなどができなくなるデメリットはありますが、それが問題なければ)
ただし、Haskellは全体を全て正格評価する方法はないはずで(自分は知らない)、部分部分を正格評価にすることはできますが、
弱冠頭正規形の制約や評価のタイミングを把握しなければ「したつもりでなってない」ということになる可能性があります。
しかし、上手く行けばいわゆるコールスタックもサンクも使用しない状態にできるとは思われます。

ただ一般的には、Haskellでメモリを気にするのはかなり難しい気がします、
余程のこだわりがなければ、メモリを気にしないか、メモリを気にできる言語を使用するかを選んだ方が良いと思います。

自分も熟練者とは言えないので、間違ってるところがあるかもしれません。
かりんとう
質問に沿って答えると、
①以下のb,cの例は末尾再起最適化されている例といえるでしょうか?
→なっていません、Haskellでそもそもその最適化はされません
②末尾再起最適化されている場合、実行時には実際にコールスタックの生成が抑制されているのでしょうか?
→なっていませんが、コールスタックはありません、ただし実行前にサンクがあります

と自分は認識してます。
@kakkun61
ご確認ありがとうございます。
「末尾再帰」は「手続き」に対してのみ「末尾再帰かどうかが言える」んじゃないかなと思うんですが?
すみません、そういった観点は持っておりませんでした。
例が適切ではなかったでしょうか?
末尾再起については学習中のため、ご容赦ください。
@かりんとう
ありがとうございます。
周辺知識も交えてご回答いただけて、勉強になります。
もしサンクを潰したければ、正格評価にする必要があります。
ちなみに、正格評価にするとサンクを使用しなくなると思いますが、この場合はコールスタックを使うことになりますか?
かりんとう
訂正させて下さい。
また、Haskellでは再帰を末尾にすることによる最適化もされないと認識しています
これは違ったようです。
Haskellでの末尾再帰は、
遅延評価では最適化自体はあるが結局サンクが作られる
正格評価では最適化され、サンクもコールスタックもない
ということっぽいです。

以下を参考にしました
https://opaupafz2.hatenablog.com/entry/2021/09/18/230521#fn-30fc4e27

上記によると、Haskellにはそもそもコールスタックがないとのことですので、接している情報が正しければ使われません。
気にするのはサンクだけです。
@かりんとう
ご追記、リンクの添付ありがとうございます:man-bowing:
Haskellの評価の仕方についてはもっと理解を深めなくてはいけないですね、、参考にさせていただきます!
かりんとう
訂正:
①以下のb,cの例は末尾再起最適化されている例といえるでしょうか?
→最適化自体は働くように見えます
②末尾再起最適化されている場合、実行時には実際にコールスタックの生成が抑制されているのでしょうか?
→コールスタックはありませんが、遅延評価である限りサンクがあります

自分もまだまだ勉強しないといけませんね
再帰定義が生成する計算プロセス進行の「かたち」を考えると判りやすくなると思います。
SICPに判りやすく正確な(個人の感想です)説明があります。
https://sicp.iijlab.net/fulltext/x121.html
注釈も含め端的に説明されています。参考になると思います。
Haskellの場合は蓄積(アキュミュレーション)が遅延されるので、
結局、反復プロセスになるように書いても蓄積部分に遅延演算(deffered operation)がのこって
しまうので、うまみがないと言われてしまうわけです。
@nobsun
ありがとうございます!
参考にいたします:man-bowing: