phel-lang の recur のエラーメッセージが分かり難いというIssueがある。
これ自体は、末尾再帰の最適化が行なわれる場所にしかrecurを置けないという雑な理解をしている。
試しにphel-langでfactorialを末尾再帰の形で書いてみようとするとコンパイルエラーになる。
こうゆうときは、まずclojureで試すのが良さそう。
(defn fact [n & [acc]]
(if (<= n 0)
acc
(fact (dec n) (* (or acc 1) n))))
(println (fact 5))
=> 120
まずはrecurを使わずに書いた。
(defn fact [n & [acc]]
(if (<= n 0)
acc
(recur (dec n) (* (or acc 1) n))))
(println (fact 5))
Syntax error (UnsupportedOperationException) compiling at (Main.clj:8:1). nth not supported on this type: Long
? 末尾再帰の最適化が行なわれる箇所に書いているつもりなんだけど、recurを使うとエラーになる。
(defn fact [n acc]
(if (<= n 0)
acc
(recur (dec n) (* (or acc 1) n))))
(println (fact 5 1))
=> 120
省略可能引数じゃない形だと、エラーが無くなるのか。
定義を見てみる
https://clojure.org/reference/special_forms
そもそも recur とは何か
(recur expr*)
再帰ポイントのバインディングをexprの値に再バインドします。
式recurは再帰点のアリティと正確に一致する必要があります。(The recur expression must match the arity of the recursion point exactly.)
fn のところの記述
(fn name? [params* ] expr*)
params ⇒ positional-param* , or positional-param* & rest-param
positional-param ⇒ binding-form
rest-param ⇒ binding-form
name ⇒ symbol
loop のところの記述
(loop [binding* ] expr*)
loopletは、ループの先頭に再帰ポイントを確立し、アリティがバインディングの数に等しい点を除いて、 とまったく同じです。
binding の定義を求めて、letのところの記述
(let [ binding* ] expr*)
binding ⇒ binding-form init-expr
このあたりを見てわかることは
loop と fn は再帰ポイントを設定する。
recur は直前の再帰ポイントに、引数を再バインドする。(再帰処理の動作イメージ)
「式recurは再帰点のアリティと正確に一致する必要があります。」
fn の params* と loop の binding* は同じではないので、fn に対してrecurを使った場合、再帰点のアリティと正確に一致しない場合がある。
アリティ(arity)というのが何かを理解していない。→アリティは、引数とbodyの組合せを複数定義できる、その一つのことか。
「式recurは再帰点のアリティと正確に一致する必要があります。」というのは、ちょっと曖昧で、可変長引数等の定義が混じっていたらrecurを使えません、という風に解釈をしてしまっていいのだろうか?
loop の時の recur は問題ないが、fn の時に recurを使えない場面があるので注意が必要。
そもそも同じコードの場合に、Clojureではどうなのか
(defn factorial [n]
(if (<= n 1)
n
(* n (recur (dec n)))))
(println (factorial 5))
Syntax error (UnsupportedOperationException) compiling recur at (Main.clj:4:10). Can only recur from tail position
Can only recur from tail position というエラーが発生した。あまり親切なメッセージとは言えないけど、状況を把握するための調査を行なうためのキーワードは与えられている。
(続く)