見出し画像

Retro-gaming and so on

なんでも遅延評価(たぁイカンが・苦笑

今回は小ネタだ。

最初に遅延評価に出会ったのはSICP第3章5節だったように思う。
その時感じた印象は、

「随分と理屈っぽい話だよな。大体これって役に立つんか?」

だった。
いや、プログラミング言語の機能を説明する際に、「数字の処理」ってのは定番ネタではある。元々、プログラミング自体は数値計算の為の作業として始まった、って事なんで、当然と言えば当然なんだけど、そもそも「プログラミングをしたい」って人は文字列処理や画像処理をしたい、ってぇんで入ってきてるわけで、「数式を処理する」例には大して魅力を感じない、って人が多いだろう。
僕もご多分に漏れずそうだったんだ。
「数式対象」って事は「規則性がある」って事だ。しかしながら、現実のプログラミングに於いては、必ずしも「規則性がある」データ対象にプログラムを書く、って場面はむしろ少ないんじゃないか。
そう、遅延評価も「規則性がある」データ構成に対しては、SICPに書かれてる通り、極めて効果的、だとは思うんだけど、じゃあ「規則性がない」ブツに関してはどうなんだ、っつーとイマイチ説得力に欠ける「技術」だとしか思えなかったんだよな。
しかもSICPを見ると、最終的にはオチがどーなんだ、ってカンジになっている。



馬鹿野郎、知りたいのはその「統合」法なんだよ、と(笑)。途中で止めてぶん投げてるんじゃねぇよ、と(笑)。画餅か
これが僕がSICPを全然評価しない理由なんだ。途中で止めるな、と(笑)。この本、こういうトコがあるんだよな〜(※1)。
だから人には薦めない、んだわ。いくら「名作」とか言われても、マジメに読んでると「そりゃねえだろ」って話になる。

しかしながら同時にこうも思った。

「このプログラミング法はプログラミング始めた際にやりたかった事じゃね?」

と。
例えばプログラミング初心者向けに良くある繰り返しの問題。C言語なんかではこう書いたりする。



Racketで言うとこんなカンジか。



良くあんだろ?こういう練習問題。誰でもやった筈だ。
そして、初心者向けの簡単な練習問題レベルでも間違えるんだ(笑)。大体のトコ、終了条件(ベースケース、とも言う)の設定ミスなんかは良くやる。いや、プログラミングに慣れてでさえも良く間違える箇所、っつって良い。
間違えて無限ループする、なんつーのも良くやるわな。結果、人は繰り返しが嫌いになる
んでこんな事考えた事無い?

ベースケースなんざ書きたくない

とか(笑)。
あるいは、

無限ループすんなよ。無限大もヘーキで扱えよ。

とか(笑)。
いや、俺は思ったね(笑)。
いつぞやも書いたが、元々プログラミング言語自体が完璧だ、って事はないし、ハッキリ言うと「素人が不満に思う点」ってのは大方実はその通り、なんだ。
プログラミングを覚える、ってのは「発想の自由さを制限して」型にハメる、ってのが本質的なトコで、実のトコ殆ど洗脳っつっていいと思う。「出来ない事を出来ない」と教え込む、って言った方がより事実に沿っている。
しかし、遅延評価なら無限大を簡単に扱える。これがプログラミング初心者時に欲しかった機能じゃないか、って問われれば、答えは明らかにYESだ。
つまり、事実上、一番最初に教えて構わん機能なのが遅延評価なんだよ。何故なら初心者こそ「そうしたい」って簡単に思えるからだ。そこにはベースケース、要するに「終了条件」がない。簡単に無限大を扱える。
Racketで言うと上のお題はこんな風に書けるだろう。

;; 無限長の整数列を設定
(define s
 (stream-cons 0 (stream-map add1 s)))

;; 無限長の整数列から冒頭10個を引っこ抜いてくる
(for-each (lambda (x)
     (printf "~a~%" x)) (stream->list (stream-take s 10)))

簡単だ。プログラミング初心者でも理解出来る範疇だろう。
ところが、「フツーのプログラミングに慣れれば慣れる程」この記述を難しく思い、まるで「プログラミングの黒魔術」であるとか、あるいは秘術みたいに見るようになるんだ。
でもそれは誤解で間違ってるんだ。
何度か書いてるけど、そもそもプログラミングの新機能的なモノってのは記述を簡単にする為に生まれてるんだ。言い換えると新しいモノ程使うのは容易だ、ってのが原則なんだ。
当たり前だろ?これも何度も言ってるが使うのをややこしくするブツをわざわざ開発したりしない。要は、殆どのプログラミング言語開発者は親切心で新機能を搭載するわけだ。決してユーザーを混乱させようとか言う嫌がらせで新機能を開発/搭載するわけじゃないんだよ。
逆説的に言えば、新機能の部分、こそが初心者ユーザー向けになり得る、って事なんだ。
高度な機能だから使い方が高度、たぁ限らない、と肝に銘じよう。
しかしながら、既存のプログラミング作法に慣れてると、こういった「新しい観点でのプログラミング法」に抵抗を覚える。ある言語の「熟練者になればなるほど」他の言語の「その言語に無い機能」に抵抗を覚えるんだ。そしてそのテの「新しい機能」を認めず、それどころか、「言語によって記述法が変わりうる」と言う事実から目を逸らしたくなる。

例えば次のような問題を考えよう。



さて、この問題をどう解く?
ハッキリ言えば既存の、遅延評価無しの言語だと解けない問題だ。何故なら、nの初期値は関数の引数として与えられても、終了条件が分からない。前提としてnいくらでも大きな値になり得る
こういう場合、遅延評価ナシの手続き言語だと、「自分で最大値を決めて」何とかする、しかなくなるが、それは単に問題に沿ってない解法だ、って事だ。
仮に100歩譲って「自分で上限値を決める」と言う事を許容したとしよう。
で?
どう解く?
原則この問題は一種の一次変換の問題だが、いわゆる「手続き型言語」だとどう解くだろうか。一々数式の要素を「分解」してfor文で回す?
ここでまず、普通の「手続き型言語」の人間とLispユーザーの「発想の違い」が分かるだろう。Lispユーザーはそもそもこの問題を見た時点で、「あ、これは再帰の問題だからすぐ解けるな」と分かる。何故ならこの問題は漸化式の問題で、漸化式とはすなわち再帰だ、と言う事を良く知ってるから、だ。
従って、Lispユーザーにとってはこの問題は単なる相互再帰を書け、って言ってるだけの問題、って事になる。殆ど初心者向けの簡単な問題だ。
次に「どう解く?」の問題としてはLispユーザーは「特に何も考えない」。数式のままそれをLispに翻訳すれば充分だ、って考えるから、だ。Schemerなんかだと、「最大値がない」時点で遅延評価技法を選択するだろう。
例えばSICPに書かれてる基本技法を使えば、これだけ書けば題意は満たせる。

(define (seq p q x0 y0)
 ;; a を定義する
 (let ((a (* 2 (cos (* 2 pi (/ p q))))))
  ;; 無限長の数列 s を得る
  (define s
   (stream-cons (vector x0 y0)
         (stream-map (lambda (z)
                (match-let (((vector x y) z))
                 (vector (+ (* a x) y)
                    (- x)))) s)))
  ;; 無限長数列 s を返す
  s))

これで終わり、だ。
数列sを使って数列sを定義する、ってのがなんともトリッキーと言えばトリッキーかもしれんが(※2)、別段再帰慣れしてれば不思議でも何でもない。ここではベクタを利用してるが、要は

  • 数列sはある計算を適用した数列sの先頭にベクタ(x0, y0)を付け足したモノである。
と言ってるだけ、だ。
これだけ、で上記の問題は「全て解けた」。わざわざ数式を分解してforで回そう、なんざ身構えなくて良い。
なお、一応上では「日本語で意味を書いてみた」わけだが、遅延評価を使うScheme及びRacketプログラミングに慣れてる層はそこまで考えていない。殆ど直感的に書き下す。マジでほぼ「何も考えていない」んだ。
と言うのも、パターンがほぼ決まってるから、なんだよ。

(define s (stream-cons 初期値 (stream-map 遷移 s)))

最初に書いた単純な例も上の「一次変換」的な例も実はこの形式に則っている。
ここで言う遷移、とは、敢えて難しく言うとnの状態をn+1の状態へと変更する過程の事、だ。



でも中身は簡単じゃん。ラムダ式でラップしてっけど、単に与えられた数式を記述してるだけだ。
xax+yになり、y-xになる。そのまんま、だ。
つまり難しい事は何も考えていないと言う事だ。
そして何も考えなくてO.K.、ってことはプログラミングの難易度が極端に下がってる、ってのと同義だ。
だから言っただろ、むしろプログラミング初心者向け、だって(笑)。
繰り返すが、こんなモン、別にプログラミングの奥義でも何でもない。誰でも使える簡単なブツなんだ。

ところで、p = 1q = 20(x, y) = (1, 0) と言う値を与えてみようか。

> (seq 1 20 1 0)
#<stream>

何かヘンなのが返ってくる。
そう、関数seqは無限長の数列sを返す。しかし「無限長」と言うのはそのままプログラムで扱うにはちとマズいわけだな。無限ループの悪夢再び、となる。
しかし、無限長の数列から「任意の長さの数列」を取ってくる事は出来る。それがstream-take だ。

> (stream-take (seq 1 20 1 0) 10)
#<stream>

うん、まだヘンなのが返ってくる(苦笑)。
実はこの時点ではまだデータが実体化してないんだ。いまだ実体化してないデータをコンピュータ・サイエンス用語でプロミス、と呼ぶ。必要なのはプロミスを「解凍」してデータを実体化する作業だ。
通常、Racketではこの数列のプロミス(ストリーム)をstream->list で解凍する。

> (stream->list (stream-take (seq 1 20 1 0) 10))
'(#(1 0)
#(1.902113032590307 -1)
#(2.6180339887498945 -1.902113032590307)
#(3.0776835371752522 -2.6180339887498945)
#(3.2360679774997876 -3.0776835371752522)
#(3.0776835371752504 -3.2360679774997876)
#(2.618033988749891 -3.0776835371752504)
#(1.902113032590302 -2.618033988749891)
#(0.9999999999999938 -1.902113032590302)
#(-6.661338147750939e-15 -0.9999999999999938))

これで、実データをリストとして入手する事が出来る。
「計算が合ってるのかどうか」検算したい人はしてみればいいと思う。が、y = -xにより、前のベクタの第0要素にマイナスを掛けた数値が今のベクタの第1要素になってる、と言う事実に気づくだろう。
いずれにせよ、無限長の数列を相手にする際は、リスト化(実体化)する前に一旦stream-takeで「有限化する」作業を忘れないようにしよう。

なお、同じ計算をもうちょっとScheme/Racket慣れしてる人ならSRFI-41stream-iterateを使って実現するだろう。stream-iteraterange の無限長版だ(※3)。

(define (seq p q x0 y0)
 (let ((a (* 2 (cos (* 2 pi (/ p q))))))
  (stream-iterate (lambda (z)
          (match-let (((vector x y) z))
           (vector (+ (* a x) y)
              (- x)))) (vector x0 y0))))

stream-iterateは初期値(vector x0 y0)からラムダ式で与えられた条件に従って、粛々とvectorを生成していく。上で見せたstream-consstream-mapの合わせ技よりコードは短くなってて見やすくなってるだろう。

なお、このお題で得たストリームを先頭から60個取ってきて、Racketのplotで、x-y座標で作図、そしてyのデータを捨ててn-xで作図すると以下のようになる。




x-yのプロットでは楕円軌道を生成し、n-xのプロットではxは三角関数的な振動を行ってる事が分かる。
結果として、x-y-nでプロットしてみると次のようになっている。



楕円軌道、かつ螺旋状に、回転しつつ上昇してる、と言うのがかの一次変換的な数列が表す軌道になる。
ここに二次元プロットを含むソースコードを置いておく。三次元は自分で工夫して描いてくれ(大して難しくもない・笑)。

と、本当はここで終わろう、とか思ってたんだけど、教えて!gooの方で気になる事を書いてた人がいたんでその文を紹介しよう。

アルゴリズムを学ぶには、何か具体的な言語でコーディングしながら
でないと身につきません。アルゴリズムを学ぶのに適した言語は
ふた昔くらい前ならPASCALでしたが、今だと未だにCかなあ...
Pythonとかで学ぶと、普通の手続き型言語とはちょっと違ったクセ
がつきそうだから、CかC++が無難でしょう。

いい事書いてる?
いや、僕の観点ではダメだこりゃ、とか思ってる(笑)。
「アルゴリズムを学ぶ」重要性はさておき、

Pythonとかで学ぶと、普通の手続き型言語とはちょっと違ったクセ
がつきそう

これはダメだ。全然お話にならない。
ここで分かるのは、「普通の手続き型言語」と言うのが一体何を指し示しているのか全く判断がつかん、と言う事だ。
恐らくこの人が想定してるのは、C、C++、Java、Pascal、良くてFortranくらいだろう。BASICやCobolを含んでもいいけどさ。所詮4〜7つの言語くらいしか「想定」してない。「この範疇で」役立つ考え方、しか思い浮かんでないんだよ。
しかし、それらの言語が形作るサークルの外側には膨大な数のプログラミング言語が存在する。そして、既に「外側の言語」は必ずしも「サークル内の言語の考え方」に従ってはいない。
まさにこの考え方が「井の中の蛙大海を知らず」って事なんだ。「ちょっと違ったクセがつく」なんてアホな事を言ってる時点でダメだ。そう、外に目を向けると、いわば「普通の手続き型言語」なんて既に少数派、なんだ。
繰り返すが、「プログラミング言語毎に考え方が違う」と言う事をまずは認めよう。そして外部の言語が「違う発想で使う」言語な以上、サークル内のプログラミング言語は「考え方の基盤」を提供しない、ってこった。言わずとしれた「C言語はプログラミングの基礎」って妄言だな。
こう言う人は結局、単にモノを知らんだけ、なんだ。サークルの外側の世界を知らない。だからこういったバカな事が書けるし、若者をマウントする事に余念がない(笑)。
結局、「俺は勉強をする気はない」「若者は俺が理解出来るようなコードを書け」って言ってるだけなんだよ。発言をオブラートに包んでいるように見えるが、結局やってる事は「若者へのマウンティング」だ。

ポール・グレアムの「ほげ言語」のパラドックスを思い出そう。

このプログラマ氏が反対の方向に目を転じた時、彼は自分が見上げているのだということに 気付かないのだ。彼が目にするのは、変てこりんな言語ばかり。 多分、それらは「ほげ」と同じくらいパワフルなんだろうけど、 どういうわけかふわふわしたおまけがいろいろついているんだ、と思うだろう。 彼にとっては「ほげ」で十分なのだ。何故なら彼は「ほげ」で考えているから。

これが実例だ。彼はPythonが「どういうわけかふわふわしたおまけがいろいろついている」と思っている。それが「ちょっと違ったクセ」の正体だ。
Cで考えてる人にはPythonのパワフルさが理解出来ない。単にそれだけ、の話だ。

もちろん、上の問題を解いたRacketのコードは「アルゴリズム」なんつー高尚なモンでも何でもない。あるのは単なる数式のRacketへの転写でしかない。
そして「代表的な手続き型言語」ではその「数式の転写」が出来ない、っつー話なだけなんだ。

Pythonは既に初心者向けプログラミング言語ではなくなってる、と言う話は何度かした。一つの大きな理由は、イテレータがあっちこっちにある、って辺りなんだけど、そのイテレータ自身が「プログラミング初心者には理解がし難い」奥義的な扱いになってるから、なんだよな。
言わば「自分が作れないモノ」が頻出する。一体何だこれは、って話になるだろう。
一方、ある程度Pythonに明るい人から見れば、上で出てきたRacketのストリーム、が性質的には極めてPythonのイテレータに近い、って事に勘付くだろう。
もちろん、完全に同じではない。しかし似てるこたぁ似てるんだ。
そう、Pythonのイテレータ及びジェネレータは「遅延評価」のPython流解釈に於ける「部分的実装」なんだ。ハッキリ言ってScheme/Racketでの扱いの自由度よりは低いけど、「使いどころ」に関して言うとほぼ互角の能力を持っている。
それはこのブログで何度か指摘している。
そして上の論、つまり「遅延評価自体はプログラミング初心者にでさえ扱いやすい機能」だとすれば、当然Pythonのイテレータも「最初に教えても構わん機能」って事にならないか?
そう、確かにその通りなんだ。問題はやっぱりPythonを使った「プログラミング入門書」の質の悪さ、と言う類の話となり、そもそも総初期にyield を用いた関数、つまりジェネレータをガンガン扱う、って構成にすれば大した問題にはならなくなる。
言い換えると、もはや「プログラミング初心者向けのプログラミング入門書の章構成」でさえ「変えなアカン」段階に来ている。
要は「C言語入門書」のような「章構成」にする、って事でさえ害毒を撒き散らす段階に入ってる、と言える。
繰り返すが、本当は「遅延評価」自体はプログラミング初心者にも扱いやすい代物なんだ。

例えば、上の与題のPythonに依る単純な解題は次のような記述になるだろう。

def seq(p, q, x0, y0):
 a = 2 * cos(2 * pi * p / q)
 z = (x0, y0)
 while True:
  yield z
  x, y = z
  z = ((a * x) + y, -x)

個人的には「手続き的記述」なんで気に食わないが(笑)、一方、手続き型に慣れてる人にとっては分かりやすい記述だとは思う。
しかも、実質的にはRacketでのstream-iterateのロジックとほぼ同じ、なんだ。初期値z = (x0, y0)をまずは作る。あとはループを回す。最初にzyieldしといて、あとは数式に従ってzを再計算しとけばいい。
当然、ベースケース、つまり終了条件なんぞは無い、んでこいつは無限長のタプル列を生成する。結果、これは「プログラミング初心者でさえ」欲しがるブツとなる。

Racketと同様に適当な初期値を与えて実行してみよう。

>>> seq(1, 20, 1, 0)
<generator object seq at 0x7fbf1004c4a0>

これもRacketの結果と同じだ。Racketは#<stream>と言う記述を返したが、Pythonも返り値がgenerator objectなるモノだ、と言っている。
要はこれもコンピュータ・サイエンス的にはプロミスなんだ。いまだデータとしては実体化していない。
理屈はRacketの場合と全く同じなんで、無限長のデータ、generator objectから任意の長さのデータを切り取ってきて、リスト化すれば実体化する。
Pythonでの「データの切り取り」を行うのがitertools.takewhile だ。
ただし、Pythonはちと厄介なトコがあって(と言うかライブラリ関数の不備)、itertools.takewhile はRacketのstream-takeとは違って「任意の長さのデータを切り取ってくる」関数じゃないんだ(※4)。条件を満たしてる部分列を返す関数だ。
従って、「長さでデータを切り取りたい」と言った場合、enumerate を使ってgenerator objectにインデックスを付加して、そのインデックス対象に条件を記述せなアカンだろう。
from itertools import takewhileしてるとすると、次のようにリスト内包表記を使えばデータを実体化可能だ。



計算結果自体はRacket版と同じになってる事が分かるだろう。確かめて欲しい。
しかし、リスト内包表記を使ってでも計算式は冗長に見える。
ただし、これは「初心者に使えないくらいPythonでの遅延評価(イテレータ/ジェネレータ)が難しい」から、じゃないんだ。単にBattery Includedな筈のPythonのitertoolsのユーティリティの数が少ないから、なんだ。
ハッキリ言って、PythonのitertoolsとRacket/SchemeのSRFI-41を比べてみると、圧倒的にitertoolsのユーティリティ数は貧弱だ。Python 3.xはあっちこっちにイテレータがある割には、貧弱なitertoolsしか備えてない。
これがある種Pythonの抱えてる問題で、ハッキリ言っちゃえばSchemeの実装者陣に比べると、単にPythonの実装者達は遅延評価を良く分かってない、って事だろう。良く分かってないからどんなユーティリティが必要になるのか、ピンと来てない、って事だと思う。
翻って言うと、実はPython実装者もイテレータ自体の活用には慣れてない、って事じゃないか。あくまで基本機能の実装だけやってて、ついでにここまで来てしまった。
多分そんなトコで、これが現在のPython 3.xが抱えてる問題だと思う。イテレータだらけな割にはitertoolsには「使える」ユーティリティが極端に少ない。
結果、ユーザーが思ったような結果を得るには、ユーザー側が「工夫」しないとならない。そしてその「工夫」の部分は単に「面倒くさい」だけで、別に遅延評価が難しいから、ではない。

さて、Python慣れしてて、かつitertools慣れしてれば、上記のコードを次のように書くかもしんない。

def seq(p, q, x0, y0):
 a = 2 * cos(2 * pi * p / q)
 def foo(z, w):
  nonlocal a
  n, x, y = z
  return (w, (a * x) + y, -x)
 return accumulate(count(1), foo, initial = (0, x0, y0))

itertools.accumulateを用いて、最初からインデックス付きの数列を作る。
ここでitertools.countは言わば無限長版rangeだ。ここでは1から始まる無限長の整数列を作っている。
itertools.accumulateは無限長版reduceだ。itertools.accumulatereduceと若干違うのは、itertools.accumulateは初期値への計算結果の追加機能を持ってるのがデフォルト動作だ、って辺りだ。よって、コールバック関数を書く際にはその辺を念頭に置いてないとならない。
ここでは、コールバック関数をローカル関数fooとして定義している。Pythonのラムダ式は貧弱なんで、初期値を分解して加工、なんかをラムダ式内で行えないから、だ。
なお、実体化はこんなカンジになる。



あとは、matplotlib.pyplotでも使えばRacketと同様に作図が出来るだろう。




Pythonのコードはここに置いておこう(※5)。

最後にもう一度書いておくが、別に遅延評価は難しくはない。しかし「フツーの手続き型言語のやり方に慣れれば慣れる程」理解し難い機能となる。つまり、なるたけ初心者に近い時点で慣れておいた方がいい機能だと言う事だ。
もし、あなたが使うデータが「規則性がある」モノだとしたら、遅延評価技法を使って「生成」すれば色々とラクになるだろう。

以上。

※1: SICPは「部分部分で」役立つ話は書いてある。そこは否定せんが、一冊全部にわたって・・・となると「そりゃねぇだろ」ってのが結論だ。
なお、人によっては「MITなんかではチューターが付いて教えてくれるのが前提で〜」とか言ってるけど、逆に言えば独学用の教科書としてはサイテーだ、と言う事に他ならない(笑)。そもそもそれだと教科書の体を成していない
なお、厳密に言うと、この節で「辿り着けなかった」結論へは、第4章2節が導いてくれる・・・つまり、遅延評価版Schemeと言う全く別のプログラミング言語の実装により、だ。
言い換えると、遅延評価でのプログラミングの真骨頂は、Haskellみたいな遅延評価前提言語じゃないと成し得ない、って意味であり、部分的に遅延評価を持ってます、ってぇんじゃ実は意味がないんだ。
結果、遅延評価とは、部分的に実装すれば何とかなる、ってモンじゃなく、プログラミング言語の「基本機能」(式の評価規則)として実装されてないと本来は活かせないアイディアだ、と言う事だ。画餅ここに極まれリ、だ(笑)。
なお、Racketに付属してるLazy Racketと言う言語処理系(Racketとはまるで別物だ)が、その「言語の基礎部分から遅延評価を用いてる」Racketの変種だ。要はHaskellのように式の評価規則が根本からして違う
いずれにせよ、真の遅延評価は言語ユーザー側がどうにか出来るようなブツじゃない、と言う事を示している。
加えると、SICPのこの節にはもっと重大な欠陥がある。そもそも読者はこの節のお題を全くプログラミング出来なかった
それはこういう事だ。SICPの第二版の出版年は1996年だ。この時あったSchemeはIEEE Scheme及びR4RSと言う仕様で、R5RS以降を使うのがフツーになってる我々には信じられないが、この頃のSchemeにはマクロがない。正確に言うとR4RSにもマクロは載ってるが、それはAppendix(おまけ)なんだ。
そしてSICPにはこんな事が書いてある。


R4RSにはforce delay があるが、SICPで取り上げられてるcons-streamはない。
cons-streamが無い、って事は作らなアカン、って事だが、生憎マクロがない。
つまり手詰まりだ、って事だ。
もちろん、現代の我々にとっては、SICPでのcons-streamの定義式とは言えない定義式を見て、

(define-syntax cons-stream
 (syntax-rules ()
  ((_ a b) (cons a (delay b)))))

って実装すればいい、とすぐ分かるだろう。でもそれは僕らがScheme経験者、だからだ。
一方、SICPはMITでは今からプログラミングを始める新一年生が使う前提の教科書であり、そんな教科書にプログラム出来ないプログラムが載ってる、なんつーのはどー考えても致命的としか言いようがない。
そういう事もあって、世間一般のSICP評が高い(とは言っても、正直なトコ、権威主義的評価だと思ってる)とか言っても僕は全然評価してないわけ。
SICPの日本語訳はヒドい、ってのは有名だけど、他にも色々とヒドいんだよ(笑)。
なお、SICPがLisp本の割に全くマクロに触れてない、と言うのは、当時のScheme(IEEE Scheme及びR4RS)にマクロが無かったからだ、と言う単純な理由に依る。

※2: なお、ここでローカル変数sletを使わずdefineで定義してる理由は、letは再帰的な変数定義が出来ない、からだ。
一方、defineはローカルだろうとグローバルだろうと再帰定義が可能な機能を持ってるので、その機能目当てでここでローカル変数sを定義するのに使ってるわけだ。

※3: 厳密に言うと、以前見せた余再帰のストリーム版、となる。
なお、SRFI-41にはストリーム用余再帰が二種類あって、有限長のストリームを生成するのがstream-unfold、無限長のストリームを生成するのがstream-iterateだ。

※4: 実際、Pythonのitertools.takewhile はSchemeのSRFI-41のstream-take-whileに近い。

※5: 「なんでsの定義式でitertools.takewhileを無引数ラムダ式でラップしてんの?」って不思議がる人がいるかもしんないが、これはいつぞや書いた通り、Pythonのイテレータは、実は破壊的変更をする構造になってて、一旦使うと「消費されて消えてしまう」からだ。つまり、そのまま使うと一回使っただけで消えて再利用が出来ない。
ここがRacket/Schemeのストリームと違う辺りなんだ。
従って、ラムダ式でラップしてるのは「単純に消費するのを止めるため」で、こういうやり方をサンクを作る、と呼ぶ。言い換えると、無引数の関数に書き換える事で「使いまわしを可能にする」んだ。
結果、sからgenerator objectを取り出す際には、関数実行すれば良く、s()generator objectを取り出している。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事