星田さんの記事に対するコメント。
Pythonのは条件分けの列挙で突っ込まれると思ってました(^o^)
うん、間接的には予想通りツッコんでるよね(笑)。
なるほど!この解説は無茶苦茶合点が行きました!名前付きLetの使いどころがちょっと明確になった気がします(←危うい(^_^;)
そうね、まぁ、名前付きletはいつ使ってもいいんですが、明確には
- 関数本体の引数より引数の数が増える場合(例えばどうしてもアキュムレータaccを外部に晒したくない場合)、あるいは関数本体の引数じゃない引数が必要になった場合
- 例示のように、再帰で利用出来るデータを定義して、これが再帰に巻き込まれたくない場合
の2つが主な使用ケースでしょう。
そうじゃなければフツーに末尾再帰書いてOKです。
16進数(以下ならば対応可能)から10進数への変換プログラムを・・テーブルを使って・・とやってみたんですけど、無茶苦茶苦戦しました。
いや、よく出来てます。ついでに言うとfoldlを導入しよう、ってのも素晴らしい。
もう一歩進んじゃいましょう。foldlを使うなら次のようにfoldlを中心に据えて書く事も可能です。
(define (Chex target (m 16))
(let ((table '((A . 10) (B . 11) (C . 12) (D . 13) (E . 14) (F. 15))))
(foldl (lambda (y z x)
(+ x (* z
(if (symbol? y)
(cdr (assv y table))
y)))) 0 target
(map (lambda (x)
(expt m x))
(reverse (range (length target)))))))
まぁね、行数的にはそんなに減った印象はない。
ただ、ポイントとしては、foldlは初期値にリストから受け取った値をガンガン足していく事が可能です。ついでに言うとfoldlは可変長引数でリストを受け取る。
まず、前から懸念していた、foldlが受け取るラムダ式の引数問題。初期値を受け取る引数はパラメータの必ず最後に来ちゃう。
っつーことは、混乱避けるように、x、y、zとか言う引数を想定してたら、xを最後尾にしちゃう、って手があるのね。x、yだったらy、xの順に記述するとか。そうすればアタマが割にサッパリハッキリします。
そうして、初期値xに基本的には (* y z) を加算していく。ここでyはtargetなるリストの要素、そしてzは基数とその桁番号だ。yがシンボルの場合はtableから数値を引っ張ってくる、ってのは星田さんのロジックと同じ。もう1つのトリックはzの計算になる。
例えば、
'(A 1 9 0)
ってリストがある場合、4桁だよね?掛けたいzのリストがあるとすれば、それは
(4096 256 16 1)
と言うリストになる筈だ。16進数の1000は4,096、100は256、10は16、1は1を現している。当然Lispでの計算になると、それぞれ(expt 16 3)、(expt 16 2)、(expt 16 1)、(expt 16 0)となる筈で、これにも規則性がある。
> (range (length '(A 1 9 0)))
'(0 1 2 3)
>
そしてそいつを逆順にする。
> (reverse (range (length '(A 1 9 0))))
'(3 2 1 0)
>
これらは基数のべき乗の指数の役割を果たしてるんで、結果、mapとexptを使えば、
> (map (lambda (x)
(expt 16 x)) (reverse (range (length '(A 1 9 0)))))
'(4096 256 16 1)
>
と望んだリストが得られ、これがfoldlの第二引数になればいい。
とまぁ、そういう発想を繋げていけば、件の関数をfoldl「だけ」で完成させる事が可能です。
最初、実引数の部分を”A190”と文字列で入力するようにしてたんですけど
string->list だとリストには出来るけど (#\A #\1 #\9 #\0) って感じになるもんですから、それを普通のシンボルとか数字に帰る方法がどうしても見つからなかったんです・・
こういう事がしたかったのかな。
> (map (lambda (x)
(if (char-numeric? x)
(string->number (list->string `(,x)))
(string->symbol (list->string `(,x))))) (string->list "A190"))
'(A 1 9 0)
>
こう、char型から何かに直接データ型を変換するのは難しいので、取り敢えずリストで包んでから文字列に変換、その後目的のデータに変換すれば大体のケースは性交成功するでしょう。
この調子で一冊まるごとやったら無意識で避けてるであろう連想リストとかLambdaにMapも使えるようになるんでは・・・と。
なるでしょう。
っつーか避けてたんか(笑)。
結構ビックリしてるんだけど、思い返してみたら、僕も最初そうだったかもしんないなぁ・・・連想リストも異様なスタイルに見えたし(笑)。lambdaって名称も怖いし、mapも避けてたかも・・・。どうだろ。
うん、でも確かに「発達段階」っつーかそういうはあるよねぇ。子供に女の裸見せてもはしゃぐだけ、だとか(笑)。適する年齢に達さないとムラムラせんよな、みたいな(笑)。
そういう事はあるかもしんない。
ちなみに、Gaucheのページでこういうのがあるんですよ。
まぁ、冗談だとは書いてるけど、
mapの便利さに目覚めて、なんでもかんでもリストにしてからmapしまくる。
と言うのはレベル3だそうです。
だから便利さに目覚めるか否か、ってトコでしょうねぇ。
せっかく(他の言語のプログラマが到達しない)末尾再帰を覚えたのに
「末尾再帰をまた書くの?メンドい!」
と思うようになる、っつーのがなんとも皮肉なんですが(笑)。
と言うか、この辺から再帰を書く前に「高階関数(mapとかreduce)を使えないか?」と自問するようになる、って事ですね。
手間はなるたけ減らしたい、ので。
あ!そう言えば・・連想リストってキーと値の逆の検索は出来ないのだろうか・・。Assocで値を検索してCarで取り出せたらキーが出せるならリストの使いまわしが出来るんだけど・・帰宅したら調べてみる!
こういうのはANSI Common Lispならある、んですよねぇ。何度も言うけど、ANSI Common Lispには盲点がない。
これ、なんでフツーないのか、と言うと、一意に決まんないから、なんです。
例えばデータテーブルの設計上、しばしば次のようなテーブルを書く場合があるのね。
(define *table* '((foo . hoge) (bar . hoge)))
キーはユニークなんだけど、値がユニークじゃない場合。こういうケースがままあって、そうすると「逆に検索」するとfooを選んで返すべきなのか、barを選んで返すべきなのか、一意に決まらないんです。
数学的に言うと、このテのテーブルも関数の一種、つまり「1対1対応」が原則なんだけど、「逆関数」が関数のそういった基本性質を満たす保証がないんですよ。
んで、そのテの数学的に曖昧っぽいのってSchemeは少なくとも仕様に含めるのを嫌うんですよ。一方「現実的エンジニアリング言語」ANSI Common Lispは「便利だから」仕様に含める。そして「最初に見つかったパターンで返す」ってのを徹底するわけ。
もっとも自作でrassocの類を作るのは難しくないし、何ならテーブル自体を、例えば
(define *table-a* '((foo . hoge) (bar . fuga) (baz . piyo)))
だった時、
> (define *table-b* (map (lambda (x)
(cons (cdr x) (car x))) *table-a*))
> *table-b*
'((hoge . foo) (fuga . bar) (piyo . baz))
>
とかやってひっくり返しちゃって、別テーブルを生成しても良いと思います。