SICP 読書会 #1 ― 2008年05月17日 02時45分
書くのが遅くなりましたが、先日開かれた snow-bell さん主催の SICP 勉強会 (計算機プログラムの構造と解釈の読書及び演習) に参加しました。SICP 勉強会は午後からだったのですが、同じ会場で午前中に Gauche 勉強会 (私は不参加) も開かれており、チャットログも合同となっています。他の参加者の記事はSICP勉強会のまとめと異業種交流会 - snow-bellの日記からどうぞ。
組み合わせと合成手続き
Scheme の式 (expression) には組み合わせ (combination) と合成手続き (compound procedure) がある。式を評価したとき、組み合わせならば部分式 (リストの各項) が再帰的に評価されるが、合成手続きでは部分式が組合せとして評価されるとは限らない。
たとえば、手続き定義 (関数定義) (define (square x) (* x x))
は合成手続きであり、この式中の (square x)
は組み合わせとして評価されるわけではない (ここでは関数 square に値 x を適用しているわけではない)。
作用的順序と正規順序
引数をすべて評価してから関数にそれらを適用するのが作用的順序の評価 (applicative-order evaluation)。関数の内容を先に展開し、引数が必要になった時点でその引数を評価するのが正規順序の評価 (normal-order evaluation)。
評価順 | 言語例 | SICP | 別称 |
---|---|---|---|
引数が先 | Scheme、C | 作用的順序の評価 | 正格評価 (strict evaluation)、先行評価 (eager evaluation)、値呼び出し (call-by-value) |
関数が先 | Haskell | 正規順序の評価 | 非正格評価 (non-strict evaluation)、遅延評価 (lazy evaluation)、名前呼び出し (call-by-name) |
それにしても normal、regular、canonical、strict にそれぞれ正規、正則、正準、正格と別個の訳語を割り当てていった人たちはすごいと思う。
1.1.6問題1.3
演習問題三つの数を引数としてとり,大きい二つの数の二乗の和を返す手続きを定義せよ.
、私の解答は以下。
(define (f1.3 x y z)
(cond ((and (>= x z) (>= y z)) (+ (* x x) (* y y)))
((and (>= y x) (>= z x)) (+ (* y y) (* z z)))
((and (>= z y) (>= x y)) (+ (* z z) (* x x)))
(else -1))) ; must not come here
基本的な考え方としては一番小さな数を見つけ、それ以外の二つの数の二乗の和を返すというもの。比較に等号を入れているのは同じ数が複数含まれているときのため。しかし二乗の和を求める部分をまとめてないなど無駄が多い。
雑感
ちょうど書籍を買って積んでいたところだったので、一から読む勉強会はいいタイミングでした。各人の演習問題の解答を見ると、こんな風に書けたのかと思うところが多々あり勉強になります。
第 2 回は 5 月 31 日に Vim 勉強会、Kansai.pm と合同で行われるそうです。また、5 月 24 日には Kanasan.JS JavaScript 第 5 版読書会 #4 も開かれるので、関西の方は是非 (もちろん関西以外からでも) 参加してみてはいかがでしょうか。
本気でやるなら黙読は避けて朗読すべき ― 2008年05月18日 17時40分
「読書百遍義自ら見る」という言葉があります。難解な書物であっても 100 回も読めば自然と意味がわかるようになるという、熟読の大切さを説いた句です。しかし、これは本当のことなのでしょうか? 2000 年もたてば社会も常識もまったく変わってきます。昔の人の言ったことが今も正しいとは限りません。
疑問があれば解明したくなるのが人の性というもの。実際に「読書百遍義自ら見る」は正しいか、確かめて紀要にまとめてくださった方がいます。それによると、女子大生にデカルトの「方法序説」を 30 回読んでもらったところ、ほとんどが内容を理解するにいたったとのこと。この言葉の正しさが見事に証明されたのです。
ただし、一点注意することがあり、それは黙読ではなく朗読するということ。人間は活字を使いだしたのはたかだかこの5千年程度のことであるが、音声を使った情報のやりとりは動物の時代から行ってきたことである
という由緒正しい理由によるものです。
さて、前置きが長くなりましたが、近頃 JavaScript を本気でやるなら何を避けて何をすべきか考えるのが流行っているようです (例 1、例 2、例 3)。ここまでお読みいただいた方にはもうお分かりでしょう。そう、避けるべきは黙読で、すべきは朗読です。文書は声に出して読まなければ身につきません。とはいっても、ソースコードまで音読していたらそれはそれで間抜けです。ソースコードは音読ではなく書き写す、すなわち写経するのがいいでしょう。
では手始めに JavaScript の母体にして標準仕様である ECMAScript Language Specification (HTML 版、日本語版) を朗読し、JavaScript で書かれた JavaScript 実装である Narcissus のソースコードを写経してみましょう。分量を恐れることはありません。前者は中高生でも手軽に読めるライトノベル 1 冊分の約 6 分の 1 のページ数、後者にいたっては 2HD フロッピーディスク 0.047 枚分という驚きの軽さです。30 回も繰り返せば自ずから JavaScript を理解していることでしょう。
それでも分量が多い、とにかく最速コースをという方には Core JavaScript 1.5 ガイドを、こんなのあっという間、もっと書物をという方には、古典として Effective JavaScript と JavaScript 深層 (両者ともインターネットアーカイブより) をお勧めします。
なお、以上はあくまで言語としての JavaScript を本気でやりたいという場合に関してです。クライアントサイドスクリプティングを本気でやりたいのなら、これらに加えてJavaScript 第 5 版、Prototype.js のソースコード、jQuery のソースコードなどはいかがでしょうか。読経と写経の繰り返しが、あなたを JavaScript の悟りの境地へといざなってくれるでしょう。
「本気でやる」ためにはどうすればいいか、はっきりとわかりましたね。この方法にひとつだけ欠点があるとすれば、こんな方法を「本気でや」った人など筆者自身も含めて一人も存在しないであろうことだけです。それでは皆さん功徳を積んで解脱を目指していきましょう。
Narcissus の正規表現 ― 2008年05月22日 13時34分
「前のエントリで書き忘れてた - 最速チュパカブラ研究会」にて、Narcissus で使われている正規表現が参考になるという話が出ています。
- 文字列リテラル
/^"(?:\\.|[^"])*"|^'(?:[^']|\\.)*'/
- 正規表現リテラル
/^\/((?:\\.|[^\/])+)\/([gimy]*)/
- コメント
/^\/(?:\*(?:.|\n)*?\*\/|\/.*)/
一流の人が書いたものを使いましょう
というのに異を唱えるつもりはありませんが、そのままコピー & ペーストしていては意味がありません。ここはやはり一文字一文字心をこめて写経しましょう……ではなく、どうしてその書き方でうまくいくのかをきちんと考えた上で使いましょう。
文字列リテラルにマッチする正規表現
上記の文字列リテラルを表す正規表現から、一重引用符でくくられた文字列にマッチする部分だけを抜き出すと '(?:[^']|\\.)*'
となります。途中 \\.
とあるのはエスケープシーケンスを許容するためですね。ではエスケープシーケンスを含む文字列 'It\'s sunny.' が、この正規表現パターンとどうマッチしていくのか見ていきましょう。
- 文字 ' がパターン
'
とマッチする。 - 文字 I がパターン
[^']
とマッチする。 - 文字 t がパターン
[^']
とマッチする。 - 文字 \ がパターン
[^']
とマッチする。 - 文字 ' がパターン
[^']
とのマッチに失敗する。 - 文字 ' がパターン
\\.
とのマッチに失敗する。 - 文字 ' がパターン
'
とマッチする。 - パターンの終端まで来たので、マッチ成功として終了する。
マッチは成功しましたが、得られた文字列は 'It\' だけです。エスケープシーケンスを適切に処理できていません。
実はこの正規表現、文字列リテラルにマッチするという目的からすると間違っています。すなわち、Narcissus のバグというわけです。エスケープシーケンスを正しく処理するためには、グループ内の選択の順番を入れ替えて '(?:\\.|[^'])*'
としなくてはいけません。
これで大丈夫かと思いきや、まだ落とし穴があります。'It\'s sunny. という終端のない文字列とマッチする場合を考えて見ましょう。この場合、最終的には文字 ' がパターン '
と、文字 I、t、\ がパターン [^']
と、文字 ' がパターン '
とマッチしてマッチが終了します。不正な形式の文字列に対してもマッチ成功となってしまうのです。
しかし、この問題は Narcissus では表面化しません。Narcissus ではマッチした文字列から文字列値を取得するために eval 関数を使っているからです。'It\' という文字列を eval 関数で解析すれば、当然終端のない文字列としてエラーが出ます。そして 'It\' のような文字列がマッチするのはそもそも入力にエラーが含まれていたときだけです。エラーを含む入力があったときにエラーになるという、ごく当然の結果になるのです。
なお、不正な文字列に対してはマッチが失敗してほしいというときは、パターンを '(?:\\.|[^'\\])*'
とする必要があります。さらに、一般的な入力にはエスケープシーケンスでない文字のほうが多く含まれると考えられるので、これは '(?:[^'\\]|\\.)*'
と選択の順番を入れ替えたほうが実行速度が上がるような気がします。しかし、なぜか SpiderMonkey では前者に対して後者のほうが約 60% も遅く、JScript でも両者の実行時間はほぼ同じです。
正規表現リテラルにマッチする正規表現
JavaScript 1.5 までは /[/]/
という入力は /[/
までが正規表現リテラルとみなされ、エラーとなっていました。しかし、JavaScript 1.6 以降では JScript の動作に合わせ、/[/]/
が 1 文字のスラッシュにマッチする正規表現として扱われます。文字クラス内ではスラッシュのエスケープが不要になったのです。上記の正規表現ではこのような入力を扱えないため、以下のように書き換える必要があります。
/^\/((?:\\.|\[(?:\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/
これも文字列リテラルのときと同じく、終端のない文字クラスを含む正規表現リテラルなどにもマッチしてしまう (が、Narcissus では問題が表面化しない) ことに注意してください。
任意の文字にマッチする正規表現
上記のコメントにマッチする正規表現内で、任意の文字にマッチするパターンとして (?:.|\n)
が使われていますが、これは [\s\S]
などとしたほうが速いです (SpiderMonkey、JScript ともに約 20% の高速化)。
ただし、Safari 2 以下で後者のパターンを使うと、ASCII の範囲外の文字 (UTF-8 でのマルチバイト文字) でのマッチに不具合が出るかもしれません。Safari 2 以下では \uxxxx
というパターンに関する不具合もあったりと、ASCII の範囲外の文字と正規表現との組み合わせがいまいち良くないようです (伝聞)。
まとめ
- Narcissus で使われている正規表現は、Narcissus での使用を前提としています。下手にそのまま流用すると思わぬところで引っかかるかもしれません。
- 「文字列リテラルにマッチする正規表現」の内容はすべて「詳説 正規表現」に書かれているはずです (少なくとも第 2 版には書かれていました)。正規表現を語るうえで外せない一冊だと思います。
- 弘法にも筆の誤りはありますが、その数は非常に少ないです。
下手に自分で考えて誤爆するより、一流の人が書いたものを使いましょう
。ただし、それがどういう意味なのかは考えたほうがいいです。
なお、上述した文字列リテラル及び正規表現リテラルにマッチする正規表現の不具合は、すでに修正されています。
「JavaScript の勉強」について個人的に ― 2008年05月23日 08時25分
「JavaScript の『本気』な勉強 - daily dayflower」での質問に横から勝手に答えちゃいます。
- 初学者の入り口として既存のライブラリを使うのは
- どちらかというと望ましい。ブラウザのイベント周りの非互換性など、JavaScript と関係ない部分で悩まずにすむのは便利だと思います。
- (初学者であれ) JavaScript の仕様に沿ったメカニズムについて
- 学習すべきである。いきなり ECMAScript 仕様を読めというつもりはありませんが、体系的な学習はしたほうがいいかと。
- DOM 構築後のスクリプト実行についてどう教える?
- ライブラリを使うならそのライブラリの機能を使って、使わないなら body 要素の内容の終端に script 要素をおくか、もしくは load イベントのイベントリスナで。
- 自分で書く場合
- 自家製ライブラリを利用している。今度このブログのレイアウトを変更するときには jQuery を使ってみようかなとも思っていますが。
書いていて思ったのですが、これはプログラミングとアプリケーション開発という軸からすれば、プログラミングよりの視点ですね。同じ視点からすれば、DOM/BOM は基礎というよりも応用であり、「W3C DOM等(基礎知識を)知りませーん」という人を「JavaScriptを本気で勉強した人」といえるか
と問われれば「いえる」と思います。
Perl ライブラリ jcode.pl は CGI での使用が最も多かったでしょうが、作者の歌代さんは (少なくとも CGI 普及期には) 一度も CGI を書いたことがなかったそうです。Web ブラウザでのクライアントサイドスクリプティングは、JavaScript の最大の利用箇所ではあっても唯一の利用箇所ではありません。「JavaScript の勉強」といった場合、ブラウザにとらわれることはないと私は考えています (dayflower さんの質問とだいぶずれていますが)。
行数の数え方 ― 2008年05月23日 11時56分
行数を数えているのですが、コメント欄他の
str.split(/\n/).length
はかっこいいけどoverkill
404 Blog Not Found:javascript - String.prototype.tr() released
本当でしょうか? 実際に試してみましょう。変数 s が対象文字列を指しているものとします。
// charAt
var lines = 1;
for (var i = 0, n = s.length; i < n; i++)
if (s.charAt(i) == "\n")
lines++;
// Array
var lines = 1;
var chars = s.split("");
for (var i = 0, n = chars.length; i < n; i++)
if (chars[i] == "\n")
lines++;
// split
var lines = s.split("\n").length;
// match
var lines = (s.match(/\n/g) || []).length + 1;
// match (番兵)
var lines = (s + "\n").match(/\n/g).length;
// indexOf
var lines = 1;
var i = -1;
while ((i = s.indexOf("\n", i + 1)) >= 0)
lines++;
// replace
var lines = s.length - s.replace(/\n/g, "").length + 1;
改行を含む文字列 | 改行を含まない文字列 | |||
---|---|---|---|---|
SpiderMonkey 1.8 | JScript 5.7 | SpiderMonkey 1.8 | JScript 5.7 | |
charAt | 468 | 1656 | 389 | 1360 |
Array | 463 | 1047 | 382 | 859 |
split | 108 | 172 | 78 | 63 |
match | 502 | 203 | 73 | 78 |
match (番兵) | 543 | 250 | 185 | 110 |
indexOf | 95 | 250 | 13 | 31 |
replace | 446 | 94 | 82 | 62 |
見ての通り、charAt メソッドで一文字ずつチェックしていくよりも、split メソッドを使ったほうが実行速度は速くなります。特に JScript ではその差が顕著です。
ところで、JavaScript は高級言語であり、一般の JavaScript 処理系ではソースコードの解釈・実行時に多くの最適化がなされます。最適化技術は日々進歩しており、今日一番速かった書き方が明日もそうである保証はどこにもありません。アルゴリズムから変えるとかならともかく、下手に速度を求めて変な書き方をするよりは簡潔な書き方を心がけたほうがいいと思います。
最近のコメント