ちょっとした実験の話だ。
Schemeでは繰り返しを書く場合何でも再帰だ。
これはSchemeプログラマが末尾再帰最適化に絶大な信頼を寄せてる、って事だ。
そして、インタプリタあるいはコンパイラレベルで末尾再帰構文は単なるジャンプ構文もしくはgoto構文に変換される。
そしてジャンプ構文やgoto構文に変換される、と言う事は、Schemeにおいて末尾再帰は「非効率」と言う事にはならない。
ところで、プログラミングをやった事がある人はgoto有害論、ってのを聞いた事があるだろう。
しかし、昨今のモダンなプログラミング言語だと、そもそもgotoを持ってないケースが多いんで、ひょっとしてgoto自体を見たことがないかもしれない。
C言語なんかだとgotoを積んでるんで、例えば「繰り返し」をgotoで書く事を試してみる事が出来る。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 3;
char ls[3];
TAG:
if (n == 0) {
puts(ls);
return EXIT_SUCCESS;
}
ls[n-1] = 'a';
n--;
goto TAG;
}
コード内にラベルを貼っ付けておき(この例だとTAGとしてる)、それ以降何らかの処理をして、goto文でソースコード上のTAGの部分に戻る。そしてまたそこから処理を行って・・・とやってくと、脱出条件に達するまで延々と繰り返しを行うことが出来るんだ。
Schemeだと内部的には、ザックリ言うと「末尾再帰」はこのシステムに変換される。これにより効率的に末尾再帰を扱えるようになってるんだ。
じゃあ、Schemeの場合、全部の「末尾再帰構文」でこのC言語的なトコでのgoto文へ変換してるのか、っつーとさに非ず。さすがにそんなメンドイ事はしていない。
Schemeにはマクロ、と呼ばれる構文定義機能がある。つまり、1つ末尾再帰に変換出来る何かを実装しておけば、そいつを利用してガンガン派生構文を作り出す事が可能だ。直接「面倒臭く書かれた何か」を弄る必要はない。
Schemeの仕様書を見る限り、何が基本で何がそうじゃないのか、ってのはパッと見分からない。「拡張した構文」はどれも同じ効率で動いてくれるのが原則、だからだ。
例えば古き良きLispの繰り返し、do構文、なんかは名前付きletに変換出来る(※1)。
(define-syntax my-do
(syntax-rules ()
((_ ((var init step) ...) ;; do の構文を
(test e ...)
command ...)
(let loop ((var init) ...) ;; 名前付き let を使った式へと変換
(if test
(begin e ...)
(begin command ... (loop step ...)))))))
つまり、Schemeでは反復構文さえ内情は再帰処理されてる、って事だ。
お陰様で、SchemeのdoはANSI Common Lispのdoと違った動きをする場合がある(※2)。
;; ANSI Common LispCL-USER> (defparameter *sample* (do ((i 0 (1+ i))
(l '() (cons (lambda () i) l)))
((>= i 5) l)))
*SAMPLE*
CL-USER> (mapcar #'funcall *sample*)
(5 5 5 5 5)
CL-USER>
;; Scheme> (define *sample* (do ((i 0 (+ i 1))
(l '() (cons (lambda () i) l)))
((>= i 5) l)))> (map (lambda (x) (x)) *sample*)
'(4 3 2 1 0)
>
とまぁSchemeでは「末尾再帰最適化」を武器として、何でも再帰で繰り返しを書くようになっている。
ところで、昨今のモダンな言語はgotoを持ってない、と書いた。
Schemeもモダンな言語だ。だからgotoは持ってない。
gotoを持ってないから結局言語仕様上、ソースコードでは再帰に頼らざるを得ない。
従って何でも再帰だ、と。
いや、ゴメン、実はSchemeはgotoを持ってるんだ(笑)。名前は違うけど。
従って、上で書いたC言語でのソースのような「ジャンプ/gotoによる繰り返し」を実はやろうと思えば出来る。
Scheme版ジャンプ構文を「継続」と言う。またはその関数call/ccは「引数付きgoto」と言うアダ名がある。
まずちょっと前提を話しておこう。
通常、プログラミング言語では、「繰り返し」あるいは「反復」は構文だ。
構文ってのはユーザーが弄れない。C言語やPythonでforやwhileの動作をユーザーの都合で変更する事は不可能だ。
従ってこれらの機能は必ず「組み込み」じゃないとならない。
一方、Schemeではcall/ccは単なる関数だ。これどういう意味か分かるだろうか?
関数で繰り返しを定義する事が出来る、なんつーのは他の言語じゃご法度なんだよ。
そういう恐らく意図しなかった機能をcall/ccは持ってしまった。実は歴史的に言うと、「偶発的に実装出来てしまった」call/ccは実装者側もビックリだったんだろう。
ちと実験してみよう。
例えば"hoge"と延々と印字したいプログラムを書きたい、としよう。
call/ccを使えば次のようにすれば「Cのgotoと殆ど全く同じような」プログラムを書く事が出来る。
(let ((goto #f))
(call/cc ;; ここらでCで言うトコのラベルを設定してる。
(lambda (tag)
(set! goto tag)))
(display 'hoge)
(newline)
(goto 'tag)) ;; goto でタグに飛ぶ。
このプログラムを走らせると無限ループするから気をつけて。
ただ、どっちにせよ、「継続で反復が可能だ」と言う証明にはなるだろう(※3)。
なお、上のコードの「コメント」は、Cのコードのどの部分に対応してるか、と言う対比の為に付けたコードで、だから内情は端折ってる。
と言うのも引数付きgoto、call/ccはちと説明が厄介な機能を持ってるからだ。
Schemeの大まかな基本はラムダ式だ。従ってラムダ式が形成するブロック(カッコの内側)を基準に考えることが出来る。
一方、call/ccが厄介なのはこのブロックがひっくり返ってるからだ。表裏が逆になってる。
と言われても分かりづらいだろ。
例えばだな。
;; ラムダ式(lambda (x) ///)
(水色で網掛けられた)斜線3つがラムダ式が形作る有効範囲を示している。そして仮引数xを通じて情報がその中身に与えられる。
ところがcall/ccはこんなカンジなんだ。
;; call/cc...//////(call/cc (lambda (x) 何とやら))//////...
極端な言い方をすると、call/cc自体はブロックと言えるブロックを形成しない。実はcall/ccが認識してる「ブロック」と言うのはcall/ccの「外側」なんだ。
これがcall/ccで初見、混乱する理由だ。「内側」を考えると全く意味が分からない。その「外側」がcall/ccでは大事なんだ。
じゃあ、内側のラムダ式、そしてその引数は何か。と言うのは、ここのラムダ式をフツーのラムダ式と考えちゃダメなんだ。
上のケースだとxと言う引数はcall/ccの外側の全情報を勝手に吸い込む窓口として機能している。そしてcall/ccの外側の全情報はcall/ccのラムダ式の内側に流れ込むわけだ。
これがcall/ccで悩む理由だ。説明見てみると「すべて確かにcall/ccではひっくり返ってる」だろ?だから分かりづらい。
もう一回上のSchemeのコードを見てみようか。
(let ((goto #f))
(call/cc
(lambda (tag)
(set! goto tag)))
(display 'hoge)
(newline)
(goto 'tag))
call/ccの認識する対象のブロックに何があるか、と言うと、つまり直接その外側で、要するにset!より逐次順としては後ろにある未実行のこの3つだ。
(display 'hoge)
(newline)
(goto 'tag)
この3つが「存在する」と言う情報が、call/ccの内側のラムダ式のtagと名付けられた引数を通じてラムダ式内部に流れ込む(※4)。と言うより、この場合、tagと名付けられた変数そのものの中身が
(display 'hoge)
(newline)
(goto 'tag)
と言う情報になった、と言って良い。
そしてその内部で、外部の変数gotoに
(display 'hoge)
(newline)
(goto 'tag)
が(未実行の状態で)ある、って情報が、call/cc内側のset!行為と合わせて束縛される(※5)。
従って3行目の(goto 'tag)が実行されると「今まで保存されたcall/ccの外側の情報」(つまりそれが「継続」だ)が再び実行され・・・と繰り返しが成立するようになるわけだ。
繰り返すと、
- call/ccがcall/ccの外側にある情報を取得、内側のラムダ式に送る。
- 送られた情報を遥か外側にある変数に登録する(ちなみに、この時点で自然にset!の「貼り付ける」と言う行為自体も貼り付けられる)。
- 情報の中に「変数を実行する」情報があればそれを行う(そしてまたこの過程でset!により新たな「call/ccの外側」の情報がgotoにset!込みで貼り付けられる)。
結果、情報取得 -> 貼り付け -> 実行 -> 情報取得 -> 貼り付け -> 実行 ...... と言うルーピングプロセスが成立するわけだ。
ところで、一応曲がりなりにも、call/ccでループを書く事が出来た。しかし無限ループである。これはイカンわな。
テキトーになんかのサイン、例えばreturnなんがあれば通常の言語だとループから脱出可能だ。しかし最悪な事にSchemeにはループから脱出しようにもreturnがない。さてどうすんべぇ、と言う話になる。
大丈夫だ。Schemeにはキチンと脱出機構がある。どうやって脱出するんだ。
それにはまたもやcall/ccを使う(爆
つまり、call/ccでループを作り、その外側でcall/ccを使い脱出を試みるんだ。
例えば最初に見せたようなCコードがあるだろ。もう一回見てみるか。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 3;
char ls[3];
TAG:
if (n == 0) {
puts(ls);
return EXIT_SUCCESS;
}
ls[n-1] = 'a';
n--;
goto TAG;
}
これをScheme的にcall/ccを使って書き換えるとこう言うカンジになる。
(call/cc
(lambda (return) ;; ここの call/cc は脱出用
(let ((n 3) (ls '()))
(let ((goto #f))
(call/cc
(lambda (tag) ;; ここの call/cc は繰り返し用
(set! goto tag)))
(if (zero? n)
(return ls)
(begin
(set! ls (cons 'a ls))
(set! n (- n 1))
(goto 'tag)))))))
まぁ、これはset!多用だしとてもじゃないけど「望ましいSchemeコード」にはなってない。
なってないが、Cのgotoによる繰り返し、そして脱出はSchemeではcall/ccによって実現出来る、と言う例だ。
加えると「制御構文」と言われるウチの反復と脱出が、Schemeでは「関数」として実現出来ている、と言う話だった。そして反復も脱出も実は統一的に扱う事が可能なんだ、と言う事をSchemeは語っている。
なお、この場合、脱出用のcall/ccの外側には何も情報がない。と言う事は「何も情報がない」と言う情報がいきなりreturnと言う「変数」を通じて流れ込む。
よって、繰り返しのcall/cc内部の脱出条件を満たした時、「何も情報がない」と言う情報、つまり「特に操作が必要ありません」と言う情報(※6)がlsと言う引数に適用される・・・見方によっては「変数returnがlsを言う情報を持って外側に脱出した」と見えるだろう。
いずれにせよ、それにより「繰り返し」は終わってしまうのだ。視点を変えると外側のcall/ccが内側のcall/ccが作り出す「繰り返し」を強制終了させたように見える。
さて、上のような理屈を見ると、なんかANSI Common Lispの古き良きloopが実装出来そうに見える。
ANSI Common Lispには・・・言い方はマズいが、事実上2つのloopが搭載されている。1つは古典的でシンプルなloop、もう1つは複雑な拡張loop。
CLerは一般に拡張loopが大好きだが(複雑で分かりづらいんで・笑)、ここでは前者を取り上げる。
ANSI Common Lispのシンプルなloopと言うのはANSI Common Lispにおける至極基本的な反復構文だ。特殊な用法は何もない。ただ単に本体に書かれた文を延々と反復するだけだ。ただ、returnが使われれば反復行為から脱出する、と。それだけの繰り返し構文だ。
これをSchemeに移植するにはどうするか。単純にマクロを使えば良い、って事になりそうだ。
そこでsyntax-rulesと上のcall/ccで作った反復機構を使ってマクロを組み立てる事に挑戦してみる。
(define-syntax loop
(syntax-rules ()
((_ e ...)
(call/cc (lambda (return)
(let ((goto #f))
(call/cc (lambda (tag)
(set! goto tag)))
e ...
(goto 'tag)))))))
ロジック的にはこれで良さそうだが残念ながらこれじゃ上手く動かない。
例えばANSI Common Lispなら次のようになる。
CL-USER> (loop (return 'hoge))
HOGE
CL-USER>
この場合loopはreturnを見かけた途端即座に脱出する。なるほど、returnは確かにloopにおける脱出キーワードになっている。
ところが、上で書いてみたScheme版loopは上手くreturnが効かないんだ。
> (loop (return 'hoge))
. . return: undefined;
cannot reference an identifier before its definition
>
なんと「returnは定義されてない」と文句を言ってくる。
一体これはどうなってんだろう。
もう一回復習すると、マクロがやってるのはある記述式を別の式に変換する事だ。もっと砕けた言い方をすると、式Aを式Bに変換する。あるいは「マクロの記述形式」を「その意味するトコロ」に変換する、って考えても良いだろう。
ここでsyntax-rulesで記述されたloopの「記述形式」は次のようなものだ。
(loop 本体...)
これがsyntax-rulesでは以下のように記述されている。
(_ e ...)
_はマクロ名(この場合はloop)、eはexpression(式)、...は以下続く、で式が連続して存在する「場合」に相当してる。
ここまではいいだろう。
問題は変換先だ。
こうなってる。
(call/cc (lambda (return)
(let ((goto #f))
(call/cc (lambda (tag)
(set! goto tag)))
e ...
(goto 'tag)))))))
書いてるこたぁ散々、上の「call/ccの利用方法」で散々っぱら書いた例そのものだ、ってのは分かるだろう。式表現のe ...ってのがマクロ独特の表現だが、ここは先の「記述形式」でのe ...に対応してるのは分かるだろう。
しかし問題は、だ。ここで僕が選んでツッコんだ「変数」は僕が読むためのモノであって、コンパイラやインタプリタが読んだ時は「全く違うモノになってる」って辺りなのだ。
要するにコンパイラやインタプリタは僕が書いた「return」や「goto」を「return」や「goto」として認識していない。
call/ccやlambda、let、set!や#fはSchemeが元々持ってるものだからコンパイラやインタプリタは「そいつらがなにか」知っている。
よって上で書いたマクロの「変換先」の部分は一種穴あきなコードとしてSchemeに認識されてるわけだ。例えば次のように。
(call/cc (lambda (g279266)
(let ((g279533 #f))
(call/cc (lambda (g279633)
(set! g279533 g279633)))
e ...
(g279533 'g279633)))))))
全くデタラメに自動生成されたシンボルが「ユーザーが作ったシンボル」に埋め込まれてるようなカタチだ。
あるいはこんなカンジか。
(call/cc (lambda (くぁwせdrftgyふじこlp)
(let ((うわなにをするくぁwせdrftgyふじこlp #f))
(call/cc (lambda (あqwせdrftgyふじこlp;@:「」)
(set! うわなにをするくぁwせdrftgyふじこlp あqwせdrftgyふじこlp;@:「」)))
e ...
(うわなにをするくぁwせdrftgyふじこlp 'あqwせdrftgyふじこlp;@:「」)))))))
いずれにせよ、それは、書かれた通り、あるシンボルは別の場所にあっても等しいようになってるけど、じゃあ「一体どういうシンボルなのか」と言うのは全くこちらから伺い知る事はできない。すべてコンパイラ/インタプリタの内部ではこうなってる、ってだけで、意味を持ってた「return」「goto」「tag」等と言った「名称」は、どことどこが等しい、と言う情報だけを遺して消失しまっているんだ。
これは困るだろう。つまり「return」は脱出機能だけ遺してその「名前」はどっかに消えてしまった、と言う事だ。そして「一体何になったのか」は我々には知る術がない。
これが「returnは定義されていない」と文句を言われた理由だ。
マクロを書いたソースコード上の「goto」と「tag」はコンパイラ/インタプリタ上で名称が「消失」しても構わないが、returnは困る。何故ならそいつがなければ、これまで見てきた通り、「繰り返しを止めて脱出する事が不可能」になるんだ。
そして「無限ループしかしない」ループ構文なぞ悪質な冗談以外の何物でもない。
さて、ここまで見てきた人は分かるだろう。ソースコード上に書いた記述上のシンボルを「勝手に何か別のモノに変換してる」のはsyntax-rulesがやってる事だ。「何故に名前を勝手に別のモノに置き換えるのか」、にはキチンとした理由があるんだけど、今回のreturnのように「それを置き換えちゃダメだってば」ってモノさえ自動で置き換えてしまう。
要するに、このsimple-loopを成立させるためにはsyntax-rulesを使っちゃならない、って事になる。
そこでsyntax-rulesに代わって登場するのがsyntax-case(※7)だ。後者はsyntax-rulesが暗黙に色々やってくれてた事を「明確に」書かなきゃいけない、と言うような性質がある。
結果色々とややこしい部分が出るんだけど、ポイントは大まかに言うとたった3つくらいだ、と言えば3つくらいである。
- syntax-caseはマクロもラムダ式である、と言う事を明示する。
- syntax-rulesではA式 -> B式と言う「等レベル変換」をするように誤魔化していたが(笑)、syntax-caseは「記述形式」が「構文」に変換される、と明確化しないとならない。そして「記述形式」と言うルールと生成される「構文」は当然同レベルにはない。
- syntax-caseもsyntax-rulesと同様にプログラマ/ユーザーが「構文」上記述したシンボルを勝手に自動生成した「何か」に置き換えるが、置き換えたくないシンボルをwith-syntaxとdatum->syntaxを組み合わせて指定可能だ。
この3つを把握してsimple-loopを定義すると次のようになる。
(define-syntax loop;; 1. のルールから、マクロ loop の本体もラムダ式で、;; 引数 expr が loop マクロ本体に記述される式のカタマリを意味する。
(lambda (expr);; syntax-case は case に似てるが、;; 1つ引数が多い(このケースだと () になってる)。
(syntax-case expr ();; 記述形式定義部。;; ここはsyntax-rulesと基本同じだが;; 定義マクロ名をハッキリ書かないとならない。
((loop e0 ...);; ここで「マクロ外部から使える」シンボルを定義する(3.)。;; with-syntax は letに近い構文で、事実上「外部から参照出来る」;; シンボル名と「何か」を束縛する。;; そして datum->syntax は「マクロ☓☓内で△△と言うシンボルを;; 構文へと変換します」と言う意味になる。;; => (datum->syntax #'xx '△△);; 結果、目出度くreturn と言うシンボルは外部からも認識される;; loop マクロの構文の一部となった。
(with-syntax ((return (datum->syntax #'loop 'return)));; #' と言うのは「ここから全部構文」と言う意味(2.)。
#'(call/cc
(lambda (return)
(let ((goto #f))
(call/cc
(lambda (tag)
(set! goto tag)))
e0 ...
(goto 'tag)))))))))
syntax-rulesよりは記述量は明らかに増えるが、syntax-caseの最初の一歩、としては悪くない例になってるだろう。
もう一度確認するが、
「シンボルreturnをloopマクロを利用したコード内で使った時に、それをloopマクロに脱出用キーワードとしてキチンと認識させたい」
と言うのが目的だった。
その目的に対するダイレクトな手段としてはwith-syntaxとdatum->syntaxを使って、シンボルreturnをloopマクロで認識させるようにしないとならないんだけど、syntax-rulesじゃそれらは使えない。
結果、syntax-caseを使わないとどうしようもない、って事になる。
さて、出来上がったANSI Common Lisp型のsimple-loopマクロを試してみようか。
> (let ((n 3) (ls '()))
(loop (if (zero? n) (return ls) #f)
(set! ls (cons 'a ls))
(set! n (- n 1))))
'(a a a) ;; 返り値
>
冒頭のCによるgoto文の例をScheme + ANSI Common Lisp型simple-loopマクロで書くと上のようになり、計算結果も殆ど同じとなる。
※1: ホントはSchemeのsyntax-ruleで書けるのはdoの基礎的な式の変換であって、実際のdoのように、必要なパラメータが省略された時の挙動さえ正確に実装するとしたら「それは無理だ」と言う事を一応言っておく。
言い換えると、何故にdoが仕様に含まれてるのか、と言うのは、「ユーザーが直接書けない」からであって、むしろ別のマクロを作る際の材料としては強力だからだ、と言う言い方が出来る。
ここでは実際のdoの実装方法を紹介してるわけではなく、Schemeで提供されてる「繰り返し」は、基本的にはすべて再帰処理に還元されてる、と言いたかっただけだ。
※2: なおこれは消えた「某所」に掲げられていた例の改題だが、この例をもって「ANSI Common Lispが正しい」にはならない。逆にクロージャの性質から言うとANSI Common Lispの「仕様上のバグ」と思える例である。
いずれにせよ、「反復」は、思わぬ挙動をする、って言い方の方がしっくり来る例と言える。 => Pythonでの例
※3: なお、このコードのジャンプ部分で(goto 'tag)と書いてるのはあくまでC言語のgoto文との対比の為であり、視認性を良くする為、だ。
実際は変数gotoに保存された「継続」は一引数関数になるが、与える引数は何でも良い。つまり、gotoは与えられた引数が#fだろうが0だろうが1だろうが構わずループを形成する。
言い換えると、「与える引数は何でも良いので」視認性を重視してクオートしたtagを与えたらさもC言語的なコードに見えて、そういう意味では分かりやすいだろ、ってだけだ。
※4: letは?(つまりgoto #f)と思う向きもあるだろうが、これは関係ない。と言うか、call/ccの「認識範囲」はcall/ccの外側1個まで、って考えて良い。
let自体もラムダ式で表現可能なので、件のコードは
((lambda (goto)
(call/cc
(lambda (tag)
(set! goto tag)))
(display 'hoge)
(newline)
(goto 'tag)) #f)
と等価で、こう見ればcall/ccの認識範囲はやはり
(display 'hoge)
(newline)
(goto 'tag)
の3つだ、と言う事が分かるだろう。
ちなみに、call/cc内部のラムダ式の引数は、僕の中ではブラックホールとか風呂場の排水口のイメージになっていて(笑)、ラムダ式の本体で使われてるその引数(変数)は、ホワイトホールとか、あるいは噴水のイメージになっている(笑)。
※5: つまりこの時点で、コードの意味は
(let ((goto #f))
(set! goto (lambda () (begin (display 'hoge)
(newline)
(goto 'tag)))))
に限りなく近くなってる。
限りなく近いけど、断言出来ない、のは、実は表裏を戻した状態だと、
(let ((goto #f))
(set! goto (lambda () (begin (set! goto (lambda () ... ;; ここだ(display 'hoge)
(newline)
(goto 'tag)))))
と内部的かつ再帰的にラムダ式をset!してるのが「延々と続いてる」ように見えるから、だ。
確かに理論上は、これが繰り返しだ!と強弁出来そうだが、現実的にはあまり上手くないだろう。
要はこの場合、call/ccがブロックの表裏をひっくり返してくれたお陰で、プログラム上は「凄くマトモな範疇に見える」ようになってる、って言い方が出来る。
※6:「何もないと言う情報が引数lsに適用される」と言うのはなかなかバチっとハマらない説明だ。しかし、「何もないと言う情報」が関数だとすれば、それは(lamda (x) x)しかあり得ない。つまり、引数に何かを受け取って引数をそのまま返す関数である。
ちなみに、なーんも加工もクソもせず、受け取った引数をそのまま返す事をidentity関数等と呼んだりする。
結果、外側のcall/ccはreturnと言う変数名でidentity関数の役割を作り出し、脱出条件が揃った時にその名称(return)をlsに適用し、見事lsを返り値としてループからの脱出を性交成功させるのだ。
※7: なお、厳密な話をすると、現行(2022年現在)のSchemeの仕様(R7RS)ではsyntax-caseは存在しない。今はR7RS-smallと言う仕様しかなく、そこでsyntax-caseが定義されてない以上、ここで書いた話はほぼ画餅になっている。
一般には、R6RSを実装した事がある処理系が現時点では独自拡張としてR6RSのsyntax-caseを利用したマクロを流用してる、って程度の話になっている。
最初にR7RSの話を聞いた時、「R7RS-smallと言う基礎(言語のコア部分)を作ってlargeで拡張するのか?」とか思ったが、さに非ず、R7RS-smallは「未だ未定義のlargeのサブセット」と言うワケの分からん状態で、実際smallが発行されてからかなり経つのにlargeの出版はサッパリだ。
今回「Scheme」と書いたのは、既にRacketがマクロそのものを独自拡張していく路線に入っていってるし、R7RS-largeがどうなるのか、ユーザーとしては「分からん」状態だと今後の展開についてはなんとも言えないからだ。
そして実際問題、ANSI Common Lispの長年愛されてきたマクロに比べると、Schemeのマクロは、健全ではあれど仕様としては安定してねぇぞ、と言うカンジではある。