プログラミング言語 emacs lispnobulign.way-nifty.com/lisp/krel2-01.pdf ·...
TRANSCRIPT
プログラミング言語プログラミング言語 Emacs LispEmacs Lisp
私は、私の先輩または同年配の、青春の大半を C言語によるシステム開発に捧げてしまったが、生涯プログラマを目指
す方々に向けて、この書を記すこととする。ただし、私は過去に正規かつ包括的なソフトウェア工学、システム工学の
教育や研修を受けた者ではなく、独学と現場経験のみを糧としてこれまでしのいできたので、これが高度な技術解説や
専門書の類となることはあり得ない。よって、これは小賢しく C++や Javaなどのプログラミング言語にスキルシフト出
来なかった Cプログラマが、楽しく余生を過ごすためのヒントを提供するものと考えていただきたい。
LISP(LISt Processing language)は、私が個人的に、Cプログラマが楽しめるプログラミング言語ではないかと
考えている。その理由は、この 2つのプログラミング言語が見た目に反してよく似ており、Cプログラマがその経験から
LISPの実装についてあれこれ類推可能であろうと考えられるからである。また、C言語を使っていて感じたさまざまな
疑問や問題点、あるいは、こういうときにはこうふうに出来ないものか、といったことが LISPではごく自然に実現出来
ていて、思わず「そうそう、やっぱりプログラミングはこうでなきゃ!」と口に出してしまうことがたびたびあった。
さらに、Cプログラマの強力な味方である Emacsを好んで使っていれば、その環境設定やカスタマイズにおいてイヤで
も Emacs Lisp(以降 elispと記す)というプログラミング言語に触れることになる(最近は Customizeによって多
くの設定変更が出来るようになっている)。しかし、これほど身近なところにあるプログラミング言語でありながら、
それで積極的にプログラムを書こうという人は意外に少ないのは、いったい elispで何が実現可能なのかを知らないか
らに他ならない。
これは非常に勿体ない話ではないだろうか。GNU Emacsが動くコンピュータ環境さえあれば(2015年現在ほとんどの
パーソナルコンピュータで動作可能)、それをインストールすることにより、elispの言語仕様書、ファンクションリ
ファレンス、編集環境、デバグ環境が手に入ったも同然となる。まさに elispオールインワン環境と言っても過言では
ない。このようなプログラミング環境は他にはないし、しかも GNU Emacsはフリーソフトウェアなのである。まさに、
プログラマにとって宝物であり、これを活用しない手はない。この書には記さないが、実際の活用例として、私は職場
でサイボウズデヂエのメール通知を elispで書いて、バッチ処理として動かしており、かなり動作が安定していて、非
常に満足している。264が 18446744073709551616(1844京 6744兆...)という数値であることを、自然数として
サッと表示してくれるのも elisp(GNU計算機)の面白いところだ。また、近々 Emacsはバージョン 25がリリースさ
れるはずだが、elispを含め、今も日々進化を続けている。
さて、前書きでいつまでも elispの有効性を力説していてもしょうがないので、本題に入ることにする。この書は、C
プログラマには K&R(ブライアン・カーニハン&デニス・リッチー)としてお馴染みの「プログラミング言語 C 第 2版
ANSI規格準拠」(以降、原著と記す)という書籍の流れに沿って、Cプログラムは elispで書くとこんな感じになると
いう事例を、出来るだけ数多く示しながら書いていくことにした。最初のうちは、文章もそのまま使えるものは流用し
ようと考えていたが、参考程度にとどめることにした。実は原著を最初から最後まできっちり読んだわけではないし、C
言語の説明を elispの説明に読み替えるには、いろいろと無理が生じてくることに気付いたのだ。場合によっては、一
つの章節まるまる elispにまったく該当しないこともある。よって、章節以外は原著にあまりこだわらずに書いていく
ので、その辺は悪しからず。
念のため繰り返すが、本書はプログラミングの専門書ではないので、ここに記載された文章やプログラムによって何か
不都合や問題が発生しても、私は責任を負うことが出来ない。もちろん、何か問い合わせを受けた場合は誠実に対応す
るが、それは問題の解決を保証するものではない。容易に想像がつくのは、ファイルを開き、誤ってその中身を変更し
保存してしまった、など。くれぐれも注意していただきたい。
1
– 第1章 やさしい入門 – 第1章 やさしい入門
この章では、まず第 2章以降で詳細に記す内容を予習する。C言語のプログラムは elispで書くとこうなるというのを
基本とするが、ときには別のプログラミング言語のプログラムとの比較を載せることもあり得る。というのは、何でも
かんでも elispでプログラムを書こうと考えるのも、その言語のポテンシャルを検証する意味で重要であるが、反対に
他の言語で書いた方がよりスマートであると考えることも、同様に重要であると考えるからである。
原著には「経験豊かなプログラマは、本章の材料から自分のプログラミングに必要なものをすぐに応用出来るはずであ
る。」と書かれている。原著を読む経験豊かなプログラマというのは、ちょっと想像しがたいが、どうやら Fortranや
Pascalのプログラマを指しているようだ。まあとりあえず、「経験豊かな Cプログラマは、本章の材料から自分の
elispプログラミングに必要なものをすぐに応用出来るはずである。」ということくらいは言えるのだろう。
1.1 1.1 手始めに手始めに
新しいプログラミング言語を学ぶための唯一の方法は、その言語でプログラムを書いてみることであり、書いてみるべ
き最初のプログラムはすべての言語で同じである。語句をプリントしてみよう。
hello, world
今や、これはプログラミング言語解説書の決まり文句のようなものかもしれないが、elispで最初に書くプログラムが
hello, worldだったという人は、おそらく自発的に elispを学び始めた人ではないだろう。Emacsの Infoに搭載さ
れている GNU Emacs Lisp Reference Manualの最初の章は「Lisp Data Types」であり、read表現や print
表現のことが書かれている。Emacsから入った人は、Emacsの動作を決定する変数のカスタマイズや、キーマップの設
定について知りたがるのであろう。
───────────────────────────────────────────────────
/* C 言語の場合 hello.c */#include <stdio.h>
main(){ printf("hello, world\n");}
$ make hello # hello.cから実行ファイル helloを作成する。cc hello.c o hello
$ ./hello # 実行ファイル helloを起動する。hello, world # 実行結果が表示される。
───────────────────────────────────────────────────
;; elisp の場合 hello;; * Mode: emacslisp *#!/usr/local/bin/emacs –script
(princ "hello, world\n")
$ ./hellohello, world
2
;; elispのプログラム例ではインデントにタブが混入した場合、スペースに置き換えて;; 使用しているワープロ上でもインデント表示幅が狂わないようにしている。;; この書面上からプログラムをコピペして実行する場合は、indentregionなどにより;; インデントし直してから実行することをお勧めする。;; そうしなかった場合、実行結果が本書と合わない場合があり得る。
───────────────────────────────────────────────────
このプログラムをどこに書き、どうやって実行するかは、使用しているシステムに依存するが、本書では、Linux上で
の実施例を示すことにする。Windowsにおいてもほぼ同様のことが可能だが、Windowsの実行例は示さないことにする。
それは、Linux上の実行方法を理解すれば、Windows上での方法に応用可能であるからだ。
$ make hellocc hello.c o hello
$ cc hello.c
意外に知られていないかもしれないが、hello.cから helloという a.out形式の実行ファイルを作成するのに、上記
のように makeコマンドを実行する。これは.cルールという暗黙の生成ルール(.cファイルから拡張子のないファイル
を生成するルール)を使ったものである。別に Makefileをいちいち書いているわけではない。ccコマンドだと、
a.outという名前の実行ファイルになってしまい、この先 Cプログラムをたくさん書いていくとその都度コンパイルす
ることになる。また、コンパイルは Emacsの中で Mx compileにより実行する。そうすると、コンパイルエラーが出
たときにエラー行へ簡単にジャンプ出来る(Mx nexterror, Cx `)ので修正するのが非常に楽だ。
さて、この 2つプログラムの違いについて検討してみよう。
C言語の方は mainという関数に printf文を 1個だけ書いている。C言語で実行可能なプログラムは、必ず mainとい
う名前の関数に処理を書く。main関数の無いプログラムからは実行可能な helloは作成されない。printfという関数
名は print formatという意味であり、書式を指定して文字列を標準出力に出力する。ここでは書式を指定していない
のに、なぜ printfを使っているのであろうか。なぜ putsではいけないのか...おそらく次節で、すぐに書式を指定
した出力をするので、あえて printfを使ったのであろうと思われる。printfについて、あるいはこの最初のプログラ
ムについて、これ以上くどくど説明する気は無い。その気がある人は main関数に printf文だけ書いてあれこれ試すだ
ろうし、書式指定といっても、頻繁に使用するのは表示幅と左右寄せを指定した%dと%sくらいで、それ以上 printfに
ついて深堀りするメリットは経験上ほとんどない。
elispの方は princを用いて文字列を出力している。Emacsは搭載するファンクションのヘルプを参照することが出来
るので、それがどんな機能なのかを確認してみるとよいが、ここでは princという関数が文字列 hello, world\nを
標準出力に出力するということより、このように記述した helloというファイルを、まるでシェルスクリプトのように
実行出来るという点に注目しておいた方がよい。これは Emacsバージョン 22で実現された仕様である。
───────────────────────────────────────────────────
#!/usr/local/bin/emacs –script
(defun main () (princ "hello, world\n"))
(main)
$ ./hellohello, world
3
───────────────────────────────────────────────────
もちろん、上記のように mainという関数に処理を書くことも可能だが、この例ではその必要性をあまり感じない。あえ
てそうする場合は main関数を実行する式も書く必要がある。
defunは関数を定義するマクロである...こう書くと Cプログラマは非常に奇異に感じるであろう。C言語において
マクロはプリプロセッサが解釈するものであり、コンパイラはマクロの存在を意識しない。しかし、elispではインタ
プリタがマクロを解釈する。defunが一体何をしているのかは、現時点ではおおよそ以下のようにイメージしておれば
良い。今はまだあまり厳密に考える段階ではない。
(fset 'main (function (lambda () (princ "hello, world\n"))))
これを簡単に説明しておくと、lambda式を mainという名のシンボルのファンクションセルに設定し、mainという名
の関数として動作するようにする...という意味になる。
1.2 1.2 変数と算術式変数と算術式
次のプログラムは、℃=(5/9)( 32)℉ という公式を使って、華氏の温度と摂氏の温度との次のような対応表を印字する
プログラムである。
0 1720 640 460 1580 26100 37120 48140 60160 71180 82200 93220 104240 115260 126280 137300 148
プログラムは、やはり mainという名の一つの関数の定義からなる。これは hello, worldを印字するプログラムより
は長いが、この例にはコメント、宣言、変数、算術式、ループ、書式付き出力といった、いくつかの新しいアイデアが
使われている。
───────────────────────────────────────────────────
#include <stdio.h>
/* fahr = 0, 20, ..., 300に対して、摂氏 華氏対応表を印字する */main(){ int fahr, celsius; int lower, upper, step;
lower = 0; /* lower limit of temperature scale */ upper = 300; /* upper limit */ step = 20; /* step size */
4
fahr = lower; while (fahr <= upper) { celsius = 5 * (fahr 32) / 9; printf("%d\t%d\n", fahr, celsius); fahr = fahr + step; }}
$ ./fahr0 1720 640 4...
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
;; fahr = 0, 20, ..., 300に対して、摂氏華氏対応表を印字する(let ((fahr 0)) (while (<= fahr 300) (princ (format "%d\t%d\n" fahr (/ (* 5 ( fahr 32)) 9))) (setq fahr (+ fahr 20))))
$ ./fahr0 1720 640 4...
───────────────────────────────────────────────────
プログラム中のコメント(コンパイラやインタプリタが無視する部分)の形式は、C言語は/*と*/自身とそれに囲まれ
た部分を指すが、このような形式はかなり珍しい。elispのように決まった文字から、それ以降行末までをコメントと
する言語が多くなっている。以前職場で、C言語ソースにおいてコメントがネストしている部分がないかを確認するコマ
ンドを書いてくれと頼まれたことがあったが、やがて多くのテキストエディタがコメントを色付けしてくれるように
なって、その必要はなくなった。
この華氏摂氏対応表プログラムでは、最初の hello worldプログラムに比べると、局所変数の宣言が目立っているが、
elisp版では不要な変数を使用せず、直接数値を書いている。これは、余分な変数は使用しない方が良いという、私個
人の経験に基づくものである。華氏の最小値 lower、華氏の最大値 upper、きざみの step、摂氏の計算結果 celcius
に変数を使用する必要は無い。このことは、この程度の小さいプログラムにおいてはどうでも良いことであるが、大き
なプログラム、またはシステムを構成するプログラムにおいては、重要な意味を持つ。
elispでは、こういう局所変数を使用するのに letというフォームを使用する。
(setq value 1)1 ; このように表示した場合は上の行の式の評価結果を表す。
(let ((value 2)) value)2
value1
上記は letの性質を示す最も簡潔な例である。setqを使用して valueに 1をバインドすると、それはグローバルな変
5
数 valueになる。しかし、elispにはデフォルトでどんなグローバル変数が有効になっているか分からないので、let
フォーム内でのみ有効となる変数を宣言して使用する。もちろん、letフォーム内でグローバル変数の値を意図的に変
更して処理することも可能になる。
どちらの言語においても、局所変数を活用することにより、処理を単純な演算式や代入文に分解することが可能になる。
しかし、単純なものが分かりやすく誤解を避けるとは限らない。C言語においてはソースレベルデバガ(dbxや gdbな
ど)がライン単位にしか追えないために、式の単純化を推奨されているのかもしれないが、elispにおいてその心配は
無用であり、どのように複雑にネストした式であっても、評価する単位でステップ実行が可能(edebug)になっている。
原著において、C言語で扱える数値の範囲は少し変わった定義が記載されている(付録 B)。通常の整数型 intは
16bit定義(32,767~32,767)、longは 32bit定義(2,147,483,647~2,147,483,647)である。しかし使
用している Linux(Ubuntu15)では、intは 32bit定義、longはワードサイズによって 32bit定義か 64bit定義
(9,223,372,036,854,775,807~9,223,372,036,854,775,807)になっている。私は個人的に、int型という
のは普通に演算に使用する CPUレジスタ(i8086で言えば Aレジスタとか)がナチュラルに扱える数値範囲になるもの
と思っていたが、どうもそうではないらしい。なぜ、64bitCPUなのに整数を 16bitで演算するのか。そもそも 64bit
のコンピュータにおいて 64bitの演算はナチュラルな処理なのか。リトルエンディアンの CPUは 64bit数値のロード
/ストアに 8クロック必要とか、そういうウソのような話になっているのかもしれない。
elispでは shortだの intだの longだのというデータ型が存在しないので、扱える数値の範囲はインタプリタの実装
に依存する。バージョン 18の頃の Emacs Lisp Manualには8,388,608~8,388,607(24bit)と記載されている
が、今私が使用しているバージョン 25の Emacsでは 30bitになっているようだ。
mostpositivefixnum536870911
mostnegativefixnum536870912
このように変数で定義されている。いつからこうなったかは不明(これはただ単に私が知らないだけ)。各バージョン
に付属している elispの texinfoをこまめに見ていけば分かるのかもしれない。扱える数値の範囲が知らないうちに
変わっているというのは Cプログラマには驚きかもしれないが、elispは元々 Emacs上の編集機能を書くための言語な
ので、誰も気にしていないのかもしれない。気にするのであれば GNU計算機を使えば良い。
(calceval "2^29")"536870912"
(calceval "2^29")"536870912"
(1+ mostpositivefixnum)536870912
数値の場合、オーバーフローは無視されるので、上記のように意図しない結果になることがある。
(stringtonumber (calceval "2^29"))536870912.0
ちなみにこういうことも起きる。上記の評価結果は fixnumではなく明らかに floatになっている。elispの数値がい
つから 30bitになったのか不明なのと同様に、floatを扱えるようになったのもいつからなのか不明。これは推測であ
るが、stringtonumberというファンクションは、数値の大きさを判断して、作成する数値オブジェクトを決定して
6
いるらしい。
おそらく多くの Cプログラマは、プログラム上の変数を「データの器」と考えよ、と習ってきている。そして、C言語で
書くプログラムの設計においても、そのように意識して設計書を書き、それがあたかもすべてのコンピュータプログラ
ムに通じる普遍的な法則であるかのように認識するようになる。しかし、elispではそうではない。
(defun main (a b) (+ a b))main
(setq main (main 1 2))3
(main main 4)7
Cプログラマは「変数とはデータの器である」の呪縛から解放されないと、上記のような elispの振る舞いは理解出来
ないかもしれない。当然であるが、こういう変数の使い方を推奨しているわけではない。変数については「第 2 章 デー
タ型・演算子・式」や、「第 5 章 ポインタと配列」でさらに詳細に検討することになると思われるので、ここまでとす
る。この段階ではまだ頭の中が混乱するのも無理はない。
次にループを制御する whileについて。偶然かどうかは知らないが、Cと elispの whileはほぼ同様のスタイルに
なっている。whileに続く最初の式が、ループを継続してよいかどうかの判定式であり、それに続いて繰り返すべき処
理が配置されている。判定式が偽になる場合は、ループ処理を実行しないのも同様である。では、この判定式とは何な
のか。
fahr <= upper /* C */
これが華氏摂氏対応表プログラムの C言語における判定式である。数値的に fahrが upper以下であれば、whileは
ループを実行するが、さらに正確に言えば、上記の式の計算結果が偽でない場合に、whileはループを実行する。判定
式の計算結果はどんな値だろうか。
───────────────────────────────────────────────────
#include <stdio.h>
main(){ int fahr = 0, result;
while (result = fahr <= 300) { printf("%d\t%d\t%d\n", fahr, 5 * (fahr 32) / 9, result); fahr = fahr + 20; }
printf("%d\n", result);}
$ ./fahr0 17 120 6 140 4 1...0
7
───────────────────────────────────────────────────
上記の実行結果によれば、判定式の結果は 1という値であり、その場合はそれを真とみなすらしい。また whileを終了
する時の判定結果は 0と表示された。つまり、この実行結果の範囲では真は 1で偽は 0ということになる。また、int
の変数 resultに代入出来、printfの%dでエラー無く表示されたので、この値は数値であるということになる。実際
には、CPUのレジスタは自身の値が 0かどうかを判断する(ゼロの場合にフラグが立つ)ので、C言語でも 0が偽で、そ
れ以外を真と扱うようになっている。これは違和感があっても受け入れるべき重要事項である。
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(let ((fahr 0) result) (while (setq result (<= fahr 300)) (princ (format "%d\t%d\t%S\n" fahr (/ (* 5 ( fahr 32)) 9) result)) (setq fahr (+ fahr 20))) (princ (format "%S\n" result)))
$ ./fahr0 17 t20 6 t40 4 t...nil
───────────────────────────────────────────────────
elispの方は、真が tで偽が nilであると分かる。formatの%Sはシンボルを表示する指示子である。こちらも、C言
語と同様に tだからループを実行するのではなく、nil以外ならループを実行する。
算術式については、C言語は数学の式と同様の記述であり、四則演算子のそれぞれの優先度も数学の計算と同様である。
その方が分かりやすいからであろう。
celcius = 5 * (fahr – 32) / 9;
(setq fahr (/ (* 5 ( fahr 32)) 9))
この 2つの式に関して、とくにどちらが優れているということはないが、下の elispの方は、評価順、計算順に曖昧さ
が無いというくらいだろうか。それよりも、C言語の方は演算子であるのに対し、elispの方はそれがファンクション
であるということの方が大きいだろう。
(+ 1 2 3 4 5)15
( 1 2 3 4 5)14
(* 1 2 3 4 5)120
(/ 1 2 3 4 5)0
elispにおける+、、*の 3つは引数を何個指定しても良い。また引数が無くてもエラーではない。/だけは 1個だけ固
定の引数を要する。
8
(+)0
()0
(*)1
(/)error: Wrong number of arguments
(/ 1)1
(setq list1 '(1 2 3 4 5))(1 2 3 4 5)
(apply #'+ list1)15
(setq list2 [1 2 3 4 5])[1 2 3 4 5]
(apply #'+ (mapcar #'identity list2))15
最後の方の applyを使用した 2例は、elisp特有の呼び出し方法である。リストでもベクトルでも変数で指定して、あ
たかもそのシーケンス内の要素をすべて引数として呼び出したかのように処理する。ただし、ベクトルの要素を合計し
なければならないケースはほとんどないと思っていて良い。華氏摂氏対応表プログラムで使用した formatも書式指示
子の数分だけ引数をしているするので、上の例と同様の性質を使用している。これの詳細も後の章であらためて検討す
るところが出てくるはずである。
原著では、printfの書式指示子で印字幅を指定したり、float表示するプログラムを載せているが、C言語と elisp
を比較しても、とくに新しい要素はないので elisp版のみ載せておく。
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(let ((fahr 0)) (while (setq result (<= fahr 300)) (princ (format "%3d\t%7.3f\n" fahr (/ (* 5.0 ( fahr 32)) 9.0))) (setq fahr (+ fahr 20))))
$ ./fahr 0 17.778 20 6.667 40 4.444...300 148.889
───────────────────────────────────────────────────
ちなみに、数値を 5.0とか 9.0というふうに少数点付きにしているのは、インタプリタに float演算させるためである。
整数表記のままだと、インタプリタは整数演算する。原著では摂氏温度を%6.1fで表示していたが、上のプログラムで
は少し変えている。
9
1.3 For1.3 For文文
特定の仕事をするためのプログラムを書くにも、いろいろのやり方がある。温度換算プログラムを別の形に書いてみよ
う。
───────────────────────────────────────────────────
#include <stdio.h>
/* 摂氏 華氏対応表を印字する */main(){ int fahr;
for (fahr = 0; fahr <= 300; fahr = fahr + 20) printf("%3d\t%6.1f\n", fahr, (5.0 / 9.0) * (fahr 32));}
$ ./fahr2 0 17.8 20 6.7 40 4.4...300 148.9
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'cl)
(do ((fahr 0 (+ fahr 20))) ((< 300 fahr)) (princ (format "%3d\t%6.1f\n" fahr (/ (* 5.0 ( fahr 32)) 9.0))))
$ ./fahr 0 17.8 20 6.7 40 4.4...300 148.9
───────────────────────────────────────────────────
本節は for文を説明するためにあるが、これに一節設ける必要があるかどうかは疑問だ。そもそも実際の C言語の開発
現場でこんな単純なループを書くことはないし、forにしても whileにしても、継続条件を検査する書式でありながら、
ループの本体で breakしたり、returnしたりするのが日常茶飯事である。まあしかし、華氏を 20ずつ上げていって摂
氏を求めるという、この例題には forがピッタリだということは言える。
elispの方では doというループ構文を使用している。もともと elispには while以外にループのプリミティブがない
ので、その他はすべて whileを使ったマクロということになる。また、elispの whileには途中でループを抜ける
breakや、ループの先頭に戻る continueのような制御は出来ない。上のプログラムで使用している doは Common
Lisp互換のマクロであり、ループの本体から returnで抜け出すことを可能にしている。しかし、これは doを抜ける
だけであり、実行中の関数から returnするものではない。
elispでは breakや continueが出来ないため、C言語よりはもう少しちゃんと考えてループを書くようになるだろう。
10
まあ、この程度のループであれば下記のように書くことも検討すべきである。これであれば、ループを制御する変数の
初期化や再初期化、終了判定などに気を回す必要がなくなる。
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(defconst fahrlist '(0 20 40 60 80 100 120 140 160 180 200 220 240 260 280 300))
(dolist (fahr fahrlist) (princ (format "%3d\t%6.1f\n" fahr (/ (* 5.0 ( fahr 32)) 9.0))))
$ ./fahr 0 17.8 20 6.7 40 4.4...300 148.9
───────────────────────────────────────────────────
ちなみに、C言語では上のような発想でプログラムを書くことが出来ない。それは C言語のシーケンスデータに終端とい
う概念がないからである。
1.4 1.4 記号定数記号定数
───────────────────────────────────────────────────
#include <stdio.h>
#define LOWER 0 /* 表の下限 */#define UPPER 300 /* 上限 */#define STEP 20 /* ステップ・サイズ */
/* 摂氏 華氏の対応表を印字する */main(){ int fahr;
for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP) printf("%3d\t%6.1f\n", fahr, (5.0 / 9.0) * (fahr 32));}
$ ./fahr 0 17.8 20 6.7 40 4.4...300 148.9
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'cl)
(defconst LOWER 0) ; 表の下限(defconst UPPER 300) ; 上限
11
(defconst STEP 20) ; ステップ・サイズ
;; 摂氏華氏の対応表を印字する(do ((fahr LOWER (+ fahr STEP))) ((> fahr UPPER)) (princ (format "%3d\t%6.1f\n" fahr (/ (* 5.0 ( fahr 32)) 9.0))))
$ ./fahr 0 17.8 20 6.7 40 4.4...300 148.9
───────────────────────────────────────────────────
本節は、プログラムに数値を直接書かないで記号定数として定義しよう、という内容である。原著には「プログラムの
中に 300とか 20とかいう、魔法の数(マジックナンバー)を埋め込むのは悪い習慣である。」と書かれている。たしか
にプログラムを書き始めて間もない頃、原著を読み、「ああ、そうするのが良いんだ」と思ったことがある。なので、
自分の書いたプログラムを人前で実行したり、プログラムレビュしたりしたときに「プログラムにマジック・ナンバー
を埋め込むな!」などと叱られたことなどない。実際には、プログラムに固定値を書かなければそれで良いわけではな
く、誰がどういう名前を付けるのか、それをどういうケースで使用しなければならないか、既に標準的なヘッダファイ
ルで定義済みではないか、定義として正しいか、同様の定義を周囲でバラバラに行なっていないか、といったことの方
がより重要である。
上のプログラムでは、fahr変数に関連する、数値の意味が明らかなものだけ定義されていて、5.0、32、9.0といった
換算公式内の数値や、printf内の書式指示子で指定している幅や小数点以下の有効桁数については、定義していないの
で、それらはマジックナンバーのままである。これはどう考えれば良いのか。
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'cl)
(defconst LOWER 0) ; 表の下限(defconst UPPER 300) ; 上限(defconst STEP 20) ; ステップ・サイズ
(defconst FWIDTH 3) ; 華氏印字幅(defconst CWIDTH 6) ; 摂氏印字幅(defconst FLOATW 1) ; 摂氏小数有効桁
(defconst CEX1 5.0) ; 換算公式 1(defconst CEX2 32) ; 換算公式 2(defconst CEX3 9.0) ; 換算公式 3
(defvar f2cformat (format "%%%dd\t%%%d.%df\n" FWIDTH CWIDTH FLOATW))
(defmacro f2c (fahr) `(/ (* CEX1 ( ,fahr CEX2)) CEX3))
;; 摂氏華氏の対応表を印字する(do ((fahr LOWER (+ fahr STEP))) ((> fahr UPPER)) (princ (format f2cformat fahr (f2c fahr))))
$ ./fahr
12
0 17.8 20 6.7 40 4.4...300 148.9
───────────────────────────────────────────────────
ホンの一例に過ぎないが、杓子定規にやれば、こんなふうになるのであろう。そして、周囲の固定値嫌悪派を黙らせる
ことは出来るのかもしれない。しかし、換算公式内の意味が分からない数値を含め、ここまでやる必要があるのだろう
か。
摂氏の表示幅 CWIDTHは、小数有効桁 FLOATWを変更する際に見直す必要があるので、そういう関係があることをコメ
ントに記す必要がある。当然ながら、LOWERを UPPERよりも大きくしてもダメである。
原著には「記号定数にすると系統的な変更が容易になる」と書かれているが、その記号定数を使用したプログラムがど
こまでの変更を許容するよう作られているかが問題になる。そういう意味で、換算公式内の数値は変更不可であり、記
号定数にするメリットはなく、かえって公式を分かりにくくしてしまう。
一般ユーザが使用する業務システムの場合、変更可能な数値には二通りの意味がある。数値を変更してリコンパイルし、
再起動すれば反映されることを求めている場合と、システム動作中にその数値を変更したい場合である。動作中に変更
したい場合は、その数値を記号定数にするのではなく、コンフィグファイルなどに記述して、それを常に読み込んで処
理するようになっていなければならない。C言語においては記号定数にしたとしても、結果的には実行ファイルに埋め込
まれる数値だ、ということに注意する必要がある。
1.5 1.5 文字入出力文字入出力
タイトルどおり、文字の入出力について検討する。原著では、文字入力に getchar(標準入力から 1文字取ってくる)、
文字出力に putchar(標準出力に 1文字書き出す)という、有名ではあるが実際の開発現場ではほとんど使う機会のな
い関数を用いている。C言語においては、そういう便利な標準ライブラリ関数が用意されているが、elispでは C言語
のように、まるで呼吸するかのように標準入力を扱うことが出来ないので、ちょっとした小細工が必要になる。
───────────────────────────────────────────────────
;;; Standard I/O Library
(defvar clib**stdin** (getbuffercreate "**Standard I/O**"))
(defvar clibmarkerstdin (withcurrentbuffer clib**stdin** (pointmarker)))
(defconst EOF 'EOF)
(when noninteractive (withcurrentbuffer clib**stdin** (let (line) (while (setq line (ignoreerrors (readstring ""))) (insert line "\n")))))
(defmacro printf (&rest args) `(princ (format ,@args)))
13
(defun getchar () (withcurrentbuffer clib**stdin** (gotochar (markerposition clibmarkerstdin)) (if (eobp) EOF (prog1 (followingchar) (forwardchar) (setmarker clibmarkerstdin (point))))))
(defmacro putchar (c) `(printf (chartostring ,c)))
(provide 'stdio)
───────────────────────────────────────────────────
こういう、かなりインチキくさいコードを何の説明もなく用意し、これに stdio.elというファイル名を付けて、使用
中の Emacsの sitelispディレクトリに置くことにする。そうしないと、この先の話が出来ないからだ。この先まだ
あれこれ追加するかもしれないので、sitelispに置くのはシンボリックリンクの方が良い。注意事項として、この
stdioは本書に載せた elispプログラムを実行する目的で書いたものであり、それ以上のことは考えていないので、別
の目的で使用すればバグの 1つや 2つあっても何ら不思議ではない。自己責任で使用するとともに、問題が発生した場
合は自己解決していただきたい。
ちなみに、この stdioは elispプログラムがスクリプト起動される(バッチモードで動く)ことを想定しているが、そ
うでない通常のインタラクティブな elispプログラムでも使用出来ないわけではない。そういう場合は、標準入力を受
け取らないので、**Standard I/O**という名前のバッファに手動で任意の文字列を書き込めば、それを getcharで
読み出すことが出来る。ただし、clibmarkerstdinが指しているポイントに注意する必要がある。
1.5.1 ファイルの複写
getcharと putcharがあれば、入出力についてそれ以外に何も知らなくても、役に立つプログラムをかなりたくさん
書くことが出来る...ということらしい。
───────────────────────────────────────────────────
#include <stdio.h>
/* 入力を出力に複写;第 1 版 */main(){ int c;
c = getchar(); while (c != EOF) { putchar(c); c = getchar(); }}
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'stdio)
14
(let ((c (getchar))) (while (not (eq c EOF)) (putchar c) (setq c (getchar))))
───────────────────────────────────────────────────
さきほどの載せた elisp版の stdioにより、C言語とそっくりの構造になり、実行結果も全く同じになることを確認出
来る。elispには!=のような等しくないかどうかを判断するものがないので、等しいかどうかを判断した結果を反転さ
せる必要がある。notは nullという関数の別名で、引数が tの場合に nilを返す。
tや nilというのは、elisp上の変更不可能な特殊なシンボルである。現段階では、tは trueを、nilは falseを意
味すると考えていても良い。それ以上のことは、シンボルのことを詳細に理解する必要があるが、私自身もそれをちゃ
んと理解出来ているわけではない。
getcharで取ってきた文字が EOFかどうかを whileの先頭で調べている。stdioの中では EOFを EOFというシンボル
として定義したので、取ってきた文字が EOFと等しいかどうかを調べることが出来る。Cプログラマは文字なのにシン
ボルかどうかを判定出来るのか、と疑問に思うかもしれない。しかし、elispにおいて cという変数は、文字が設定さ
れれば文字になり、EOFが設定されれば EOFになるのだ。C言語では void *cという変数を使って同様のことが出来る
ような気もするが、変数の型が決まっているというのが邪魔になって仕方がない。elispの場合は、変数に型はなく、
変数にバインドされるオブジェクトの方に型があるのだ。
複写を行なうプログラムは、経験のある Cプログラマならば実際にもっと短く書けるであろう。
───────────────────────────────────────────────────
#include <stdio.h>
/* 入力を出力に複写;第 2 版 */main(){ int c; while ((c = getchar()) != EOF) putchar(c);}
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'stdio)
(let (c) (while (not (eq (setq c (getchar)) EOF)) (putchar c)))
───────────────────────────────────────────────────
さて、これに関して何か解説が必要だろうか。要するにこのようなプログラムはどのプログラミング言語で書いても大
差なく、まして優劣を論じるポイントもない。C言語の方は第 1版も第 2版も実行ファイルのサイズは同じだ(7376バ
イト)。つまり a.out形式の配置で丸められる部分はあるにせよ、プログラムの書き方が違っても同じような実行ファ
イルが作られるということではないか。比べる意味は無いが、elisp版は 121バイトである。
15
原著では、ほんのりと演算子の結合優先度に触れている。
c = getchar() != EOF;
これだと、getchar() != EOFの結果(真偽)が cに代入されてしまうのである。c = getchar()の部分を括弧で囲
み、先に評価することで意図した動作をさせる必要がある。しかし、プログラムの目的を除いて考えると、上の式は、
変数 cにその右側の演算結果を代入するのが自然ではないだろうか。
EOF != (c = getchar());
むかし開発現場にいた賢いリーダは、上記のように記号定数を最左辺に持ってくる方法をコーディング規約に記してい
た。これにより c = getchar()に括弧を付け忘れたり、==を=と間違えたりすると、コンパイルエラーになるので、原
因が分かりにくいバグを後々の試験工程まで潜在させずに済む、と考えたわけだ。「俺がそんな間違いするかよ!」と
思って反抗したが、結局このアイデアは受け入れた記憶がある。
1.5.2 1.5.2 文字のカウント文字のカウント
次のプログラムは文字数を数えるためのものである。これは複写のプログラムに少し手を加えたものになっている。
───────────────────────────────────────────────────
#include <stdio.h>
/* 入力される文字をカウント;第 1 版 */main(){ long nc;
nc = 0; while (getchar() != EOF) ++nc; printf("%ld\n", nc);}
$ time ./ccount < ccount.c166
real 0m0.002suser 0m0.000ssys 0m0.000s
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'stdio)
(defmacro ++ (n) `(setq ,n (1+ ,n)))
(let ((nc 0)) (while (not (eq EOF (getchar))) (++ nc)) (printf "%d\n" nc))
$ time ./ccounte1 < ccounte1 174
16
real 0m0.073suser 0m0.032ssys 0m0.012s
───────────────────────────────────────────────────
大したことではないが、++という新しく出てきた演算子に触れる必要があるようだ。
C言語には++(インクレメント)、(デクレメント)という、変数の前後にくっ付けて変数の値をインクレメントし
たり、デクレメントしたりする演算子がある。1を足す/引くというのは CPUのレジスタにもある機能なので、C言語に
おいてはそのまんまの機能としてコンパイラがそういうインストラクションを出力するのかもしれない。しかし、開発
現場ではあまり評判の良い演算子でなかった記憶がある。原著には、++nc(前置演算子)も nc++(後置演算子)も、
どちらも ncを 1増加させると、さらっと書かれているが、実際にはかなり意味が違うものだ。これについては、後の章
で触れることがあれば、そこで確認する。ここでは nc = nc + 1よりも++ncの方が短くて良いではないか、というこ
とが言いたいらしい。
elispには、そもそも++やと同機能のファンクションはないし、前置演算や後置演算という概念もない。だが、それ
ではちょっと悔しいので、elisp版では++というマクロを書いてみた。誰もやらないとは思うが(nc ++)とは書けない
ので悪しからず。elispのマクロの書き方については、まだ関数定義の詳細も説明していない段階であり、いろいろと
注意事項もあるので、後の章で書いた方が良いと思われる。
ここまで、C言語と elispのプログラムを同じ動作になるように書いてきたが、その性能については比較してこなかっ
た。上の例では一応 timeコマンドを使って実行時間を計測している。使用している環境は 2スレ 2GBメモリの仮想マ
シン上で動く Ubuntu Linuxであるが、C言語の方が elispよりも約 40倍速いことが分かる。まあ、elispの処理を
実行する度に、20MBもある Emacsの実行ファイルをローディングして(スティッキービットを立ててみたがあまり変
わらなかった)、いかがわしい stdioを動かし、さらにそれを使った処理を実行するのであるから、遅いのは当たり前
だが、やはりそういう無駄がほとんどない C言語のプログラムは速いの一言に尽きる。
Cプログラムではここで long変数を使用している。原著では intの最大値が 32767なので、簡単にオーバーフローす
るかもしれない、ということのようだが、このプログラムを実行するのにそんな巨大な入力を与えるとは思えない。
printfの long変数に対応する書式指示子は%ldなのだそうだ。この l(エル)のことを長さ修飾子と呼び、他に
h(shortまたは unsigned short)や L(long double)というのもあるらしい。こんなのは全く知らなかったが、
開発の現場で困ったことは一度もない。こういうことは事細かに記憶するよりも、手元の言語解説書のどこを探せば書
いてあるかを、把握しておくだけで良いのではないかと考えている。
ところで、これよりさらに大きな数を扱うには、double(倍長の float)が使える。ループを表現するもう一つの別の
やり方を示すために、次に whileの代わりに for文を使ってみよう。
───────────────────────────────────────────────────
#include <stdio.h>
/* 入力される文字をカウント;第 2 版 */main(){ double nc;
for (nc = 0; getchar() != EOF; ++nc) ;
17
printf("%.0f\n", nc);}
$ ./ccountc2 < ccountc2.c167$ wc ccountc2.c 11 21 167 ccountc2.c
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'stdio)(require 'cl)
(do ((nc 0.0 (1+ nc)) (c (getchar) (getchar))) ((eq c EOF) (printf "%.0f\n" nc)))
$ ./ccounte2 < ccounte2156$ wc ccounte2 8 20 156 ccounte2$ ./ccounte2 < ccountc2.c137
───────────────────────────────────────────────────
doubleを使ってカウントするにふさわしい標準入力はちょっと見当が付かないのであきらめて、今更ではあるが、プロ
グラムがカウントする文字数が正しいかどうかを本職の wcコマンドを使って確かめてみた。見て分かるとおり、Cプロ
グラムの方は C言語と wcコマンドが 167とカウントし、elispは 137とカウントした。これは Cプログラムの方に日
本語の全角文字 15文字あるからである。
$ echo $LANGja_JP.UTF8
$ nkf g ccountc1.cUTF8
使用している環境の文字コードが UTF8であることから、日本語の全角文字が 3バイトエンコードになっており、
elispの文字カウントは wcコマンドの結果よりも 30バイト少なくなっているのである。つまり elispでは 1個の日本
語全角文字をちゃんと 1文字と認識するが、C言語の getcharや wcコマンドは文字コードを気にせずに処理する(と
思われる)ので、結果はファイルサイズと同じになるというわけだ。文字のカウントとして、どちらが正しいかはプロ
グラムの利用目的によって変わる。
87654321 0011 2233 4455 6677 8899 aabb ccdd eeff 0123456789abcdef00000000: 2369 6e63 6c75 6465 203c 7374 6469 6f2e #include <stdio.00000010: 683e 0a0a 2f2a 20e5 85a5 e58a 9be3 8195 h>../* .........00000020: e382 8ce3 828b e696 87e5 ad97 e382 92e3 ................00000030: 82ab e382 a6e3 83b3 e383 88ef bc9b e7ac ................00000040: ac32 e789 8820 2a2f 0a6d 6169 6e28 290a .2... */.main().00000050: 7b0a 2020 646f 7562 6c65 206e 633b 0a0a {. double nc;..00000060: 2020 666f 7220 286e 6320 3d20 303b 2067 for (nc = 0; g00000070: 6574 6368 6172 2829 2021 3d20 454f 463b etchar() != EOF;00000080: 202b 2b6e 6329 0a20 2020 203b 0a20 2070 ++nc). ;. p00000090: 7269 6e74 6628 2225 2e30 665c 6e22 2c20 rintf("%.0f\n", 000000a0: 6e63 293b 0a7d 0a nc);.}.
18
これは Emacsの hexlmodeを使って実際に Cプログラムをダンプ表示したものである。右側のデコード表示内に/*と
*/で囲まれている部分があり、その中で 2という文字だけが表示されているのが分かるだろうか。これは「第 2版」の
2だけが表示されており、その後ろにピリオドが 3つ続いて、空白、*/となっている。そのピリオド 3つが「版」とい
う文字であろうことは明白であり、全角文字 1文字に 3バイト使用していることが分かる。
原著では「forループの本体は空である」という言い方をしているが、繰り返しが何もないわけではない。明らかに
getcharを EOFになるまで繰り返し呼び出している。これは forの継続条件を書くべきところで、何をどこまで書いて
良いのかという問題を招く。同様に、再初期化を書くべき所に何をどこまで書いて良いかという問題もある。
───────────────────────────────────────────────────
#include <stdio.h>
/* 入力される文字をカウント;第 2.1 版 */main(){ int c; double nc;
for (nc = 0, c = getchar(); c != EOF; ++nc, c = getchar()) ; printf("%.0f\n", nc);}
───────────────────────────────────────────────────
Cプログラムの方は、このように書いても同じ動作をする。実は elispの do文はこの forの書き方に近い構文になっ
ている。別に邪悪なコード大会をしたいわけではない。プログラムのコーディングルールは開発中の視認性、運用段階
での保守性だけでなく、プログラム規模の定量化、さらには品質管理にも影響する。もちろん、どのように書き換えて
も規模が変わらない、優秀なプログラムカウンタを用意出来れば良いが、そんなものを研究するよりはコーディング
ルールを徹底する方が明らかに利口だ。要するにそこそこにコーディングルールを決めて、スタイル的にも規模的にも
同様に揃えば良いのだ。しかし、にわかに集められたプロジェクトメンバでそうなるとは思えないし、メンバ全員が
ルールどおりに同じようなプログラムを書いてくるのは却って気持ちが悪いし、別の心配を生んでしまう。現場のプロ
ジェクトリーダはいつもそんなことで悩んでいるのである。
elispも do文もループの本体は空であるが、こちらはあまりわざとらしさを感じない。もともとそういうふうに書ける
ようになっているフォームだからである。プログラムを短く書きたい人は elispのような LISP系の言語が向いている
のかもしれない。ループはやはり、どの変数をどう変化させ、どういう場合に繰り返すのか(または終了するのか)、
何を繰り返しているのかが分かりやすい書き方が望ましいのではないかと考える。
最後に原著では、標準入力がなかった場合に結果が 0になるかどうかについて書いている。これを確認するのは非常に
簡単だ。
$ echo n | ./ccountc20
$ echo n | ./ccounte20
C言語(ccountc2)でも elisp(ccounte2)でも結果は 0になる。もちろん echoのnオプションを外すと結果
は 1になる。
19
1.5.3 1.5.3 行数のカウント行数のカウント
次のプログラムは入力の行数を数えるものである。上に述べたように、標準ライブラリでは、各入力ストリームが、改
行で区切られた一連の行の形になることが保証されている。したがって、行を数えるには、改行記号の数を数えれば良
い。
───────────────────────────────────────────────────
#include <stdio.h>
/* 入力の行数をカウント */main(){ int c, nl = 0;
while (EOF != (c = getchar())) if (c == '\n') ++nl; else ; printf("%d\n", nl);}
$ ./ccountc < ccountc.c14
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'stdio)
(defmacro ++ (n) `(setq ,n (1+ ,n)))
(let (c (nl 0)) (while (not (eq EOF (setq c (getchar)))) (if (= c ?\n) (++ nl) t)) (printf "%d\n" nl))
$ ./ccounte.el < ccounte.el13
$ ./ccounte.el < ccountc.c14
───────────────────────────────────────────────────
ここでは新たに if文が登場した。
C言語においては、if(判定文){ 判定文が偽でない場合の処理 } else { 判定文が偽の場合の処理 }という形の構
文である。{}に囲まれたところには式をいくつかいても良く、式が一つの場合は{}を省略できる。これは elseについ
ても同様だ。C言語の場合、Bourne Shellと違って elseとは書いても thenとは書かない。
elispの ifには thenも elseもないが、ちゃんと thenと elseを区別している。ifの引数の最初の式が判定文で、
20
それに続く式が thenである。それ以降式がいくつ続いても良く、それらはすべて elseになる。
上のプログラムではどちらも elseは何もすることがないが、それがあることを明示的に書いた(原著に elseは書かれ
ていない)。もちろんどちらも NOPになるようにしなければならない。実際にプログラムをいくつか書くと、thenのみ
処理がある if文を無意識のうちに書くことが多くなる。これはプログラムを設計する際に、こういう場合はこうする必
要があると、ポジティブに考えて処理を組み立てている証拠だ。そういう傾向が特に強いプログラマは、こういう場合
はこうする、こうでない場合はこうするというのを、分岐でなく別々の if文で書いてしまっていることがある。そうい
う状況に陥ってしまうような複雑な条件を伴う処理は、本人以外の客観的な目で if文の妥当性を見てやる必要がある。
つまりレビュアが、ここの elseは気にしなくて良いか?と一つ一つ聞いてやるのだ。
C言語では、取ってきた文字が改行かどうかを==という演算子で検査する。なので、慣れないうちは等しくない場合に
なぜ!==ではないのかと疑問を持つかもしれない。実は!自体が演算子の一つであり、右側の式にのみ結合して、その式
の結果を真偽反転(論理値反転)する。なので==の左側に!がくるとコンパイラはシンタックスエラーにしてしまう。!
=はこれで一つの演算子であり!と=が結合したものではないのだ。
if (c == '\n') { ...
if (!(c != '\n')) { ...
こういう話をすると、(こんな簡単な例ではないにせよ)上の 2つはどっちでも同じなのかと考える輩が必ず出てくる
が、基本的に二重否定の判断文は書かない方が良い(書いてはいけない)。
また、C言語では型が違うもの同士を==や!=で比較するとエラーやワーニングを出してくれるはずだ。しかし、elisp
ではそういうチェックがかかる場合とかからない場合がある。最近の Emacsは、たとえば eq: (OBJ1 OBJ1)とか=:
(NUMBERORMARKER &rest NUMBERORMARKER)というガイダンスをミニバッファに表示してくれるようになった
ので、実はちょっと驚いているのだが、ちょっと前までは Infoを見たり、ファンクションヘルプを見ながらプログラム
を書いたものだ。=は数値的に等しいかどうかを検査するのに使用するが、eqは引数で指定された 2つのオブジェクト
が同じものかどうかを検査する。似たようなファンクションに equalというのがあるが、こちらは 2つのオブジェクト
が同じものでなくても良く、等しい値と構造をしているかどうかを検査する。なので、elispのプログラムを書き慣れ
ないうちは安易に eqを使用しない方が良い。もちろん、上のプログラムでは eqを equalに置き換えても同様に動作す
る。ある程度 elispに慣れてきて、処理スピードが気になるようになったら eq系のファンクションを使ってみよう。
その他、getcharで取ってきた文字が特定の文字かどうかを調べるときに、C言語の場合はシングルクォートで文字を
囲んで文字コードであることを表す。elispの場合は文字の前に?を付けることで同様の意味になる。これは見たままで
あり、大した話ではないので、ここではこれ以上は説明しない。ただ、Emacsの場合は M:で evalexpressionを起
動し、?Aを評価すると、65 (#o101, #x41, ?A)とミニバッファに表示してくれるので便利である(#oはオクタル表
示、#xはヘキサ表示)。
1.5.4 1.5.4 単語のカウント単語のカウント
次に、第四の有用なプログラムとして、行と単語と文字の個数を数えるプログラムを考えてみよう。ただし、ここでは
単語は、空白やタブや改行を含まない任意の文字の並びであるとゆるやかに定義しておく。このプログラムは、UNIXの
ユーティリティ・プログラム wcの裸の骨格部分である。
───────────────────────────────────────────────────
21
#include <stdio.h>
#define IN 1#define OUT 0
main(){ int c, nl, nw, nc, state;
state = OUT; nl = nw = nc = 0; while (EOF != (c = getchar())) { ++nc; if (c == '\n') ++nl; if (c == ' ' || c == '\n' || c == '\t') state = OUT; else if (OUT == state) { state = IN; ++nw; } else ; } printf("%d %d %d\n", nl, nw, nc);}
$ ./countword < countword.c25 76 381
$ wc countword.c 25 76 381 countword.c
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'stdio)(require 'cl)
(defconst IN 1)(defconst OUT 0)
(defmacro ++ (n) `(setq ,n (1+ ,n)))
(let (c (nl 0) (nw 0) (nc 0) (state OUT)) (while (not (eq EOF (setq c (getchar)))) (++ nc) (when (= c ?\n) (++ nl)) (if (or (= c ? ) (= c ?\n) (= c ?\t)) (setq state OUT) (when (= state OUT) (setq state IN) (++ nw)))) (printf "%d %d %d\n" nl nw nc))
$ ./countword.el < countword.el22 75 421
$ wc countword.el 22 75 421 countword.el
22
───────────────────────────────────────────────────
このプログラムの建て付けは、標準入力から 1文字ずつ取ってきて文字数をカウントし、改行だったら行数をカウント
する、空白文字が改行だったら状態を OUTに変更、空白文字以外だったら状態が OUTの時だけ状態を INに変更し単語
数をカウントする、つまり単語が始まる文字と判断したらカウントする、という処理を入力が EOFになるまで繰り返し
ている。
elispのプログラムはほぼ C言語のとおりに変換して書いたが、これは 1文字ずつ処理するためのロジックであり、通
常の elispであれば、もう少し違った書き方も出来る。
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(defmacro ++ (n) `(setq ,n (1+ ,n)))
(withtempbuffer (let ((file (car commandlineargsleft)) (nw 0) (nl 0)) (insertfilecontents file) (gotochar (pointmin)) (while (searchforward "\n" nil t) (++ nl)) (gotochar (pointmin)) (while (researchforward "[^ \n\t]+" nil t) (++ nw)) (princ (format "%d %d %d\n" nl nw (length (bufferstring))))))
$ ./countword2.el countword2.el15 48 423
$ wc countword2.el 15 48 423 countword2.el
───────────────────────────────────────────────────
このプログラムでは stdioを使用せず、コマンドラインで指定されたファイルを開いて行数、単語数、文字数を表示し
ている。わざわざテンポラリなバッファにファイルを読み込んでいるのは、ファイルを編集バッファとして開くのと処
理的にあまり変わらないし、ファイルの内容を変更してしまうリスクがないからである。
処理的には、ファイルを読み込んだバッファにおいて、改行をサーチしてカウントする、空白文字以外で構成される文
字列をサーチしてカウントする、文字数がバッファ全体の文字列を length関数でカウントするという、単語の内か外
かを表す状態を使うよりは分かりやすいのではないかと思われる。おそらく C言語のプログラムで正規表現を使うのは
稀なので(やる場合はレキシカルアナライザを使う?)、たまにはこういう例も載せた方が良いと考えた。
C言語の方で気になるのは、else ifの使い方である。私は C言語のプログラムを書くときに else ifを使ったことが
ない。とくに例のように、ifの判定文ではどういう文字かを調べ、else ifの判定文ではそれとは違う stateを調べ
るような場合には、必ず以下のように書く。
if (c == ' ' || c == '\n' || c == '\t') state = OUT;else { if (OUT == state) { state = IN;
23
++nw; }}
もっと言えば、以下のように書きたいくらいなのだ。
if (c == ' ' || c == '\n' || c == '\t') state = OUT;if (c != ' ' && c != '\n' && c != '\t' && OUT == state) { state = IN; ++nw;}
上記のような単純な例ではどちらでもよいかもしれないが、複雑な条件になると else ifが延々と続く if文はチェッ
クするのにウンザリしてくるからだ。しかし、原著には複雑な条件の場合に else ifが重要になると書かれている。つ
まりその方が簡潔に書けるから良いということなのであろう。このへんが、この言語を作った人と、その言語を使って
苦しんだ人(というより他人が書いた分かりにくいプログラムをたくさん読まされた人?)との違いだろうか。
1.6 1.6 配列配列
個々の数字と空白文字(ブランク、タブ、改行文字)とその他すべての文字の出現する回数を数えるプログラムを書い
てみよう。これは少しわざとらしい例だが、この一つのプログラムで Cのいくつかの面を示すことが出来るからである。
入力に関してここでは 12のカテゴリーがあるとするから、それぞれの数字の出現する回数を把握するには 10個の異
なった変数を使うより、配列を使った方が便利であろう。ここに一つのプログラム形式を示す。
───────────────────────────────────────────────────
#include <stdio.h>
/* 数字、空白文字、その他をカウント */main(){ int c, i, nwhite, nother; int ndigit[10];
nwhite = nother = 0; for (i = 0; i < 10; ++i) ndigit[i] = 0;
while (EOF != (c = getchar())) if (c >= '0' && c <= '9') ++ndigit[c '0']; else if (c == ' ' || c == '\n' || c == '\t') ++nwhite; else ++nother;
printf("digit ="); for (i = 0; i < 10; ++i) printf(" %d", ndigit[i]); printf(", whites space = %d, other = %d\n", nwhite, nother);}
$ ./array < array.cdigit = 9 3 0 0 0 0 0 0 0 1, whites space = 146, other = 364
───────────────────────────────────────────────────
24
#!/usr/local/bin/emacs script
(require 'stdio)
(defmacro ++ (n) `(setq ,n (1+ ,n)))
(let (c (nwhite 0) (nother 0) (ndigit (makevector 10 0))) (while (not (eq EOF (setq c (getchar)))) (cond ((and (>= c ?0) (<= c ?9)) (aset ndigit ( c ?0) (1+ (aref ndigit ( c ?0))))) ((or (= c ? ) (= c ?\n) (= c ?\t)) (++ nwhite)) (t (++ nother)))) (printf "digits = %s, whites space = %d, other = %d\n" (mapconcat 'numbertostring ndigit " ") nwhite nother))
$ ./array.el < array.eldigits = 7 3 0 0 0 0 0 0 0 1, whites space = 115, other = 357
$ ./array.el < array.cdigits = 9 3 0 0 0 0 0 0 0 1, whites space = 146, other = 332
───────────────────────────────────────────────────
また例によって、array.cに対する C言語と elispの結果が違っているのは UTF8日本語文字のバイト数の差である。
配列については、総じて LISPは配列を扱うのが苦手なので...というのを以前何かの本で読んだことがあるのだが、
別に問題ないようだ。これは最近のバージョンで改善されたのかもしれないが、いつどのバージョンでというのは分か
らない。
C言語の配列は宣言において、データ型と配列の要素数を指定するが、配列の終端は言語によって管理されていないので、
宣言した要素数を越えてアクセス出来てしまう。そのため、配列アクセスが宣言した範囲を越えないよう、添字の指定
には注意を要する。Cプログラムではこれを for文で、変数 iにより行なっている。これは C言語の配列が、データ型
のサイズ×要素数で求められる大きさの連続領域になっていて、各要素毎に添字でアクセス出来るというより、データサ
イズ×添字により目的のデータのアドレスを求めていると理解した方が分かりやすく、間違いを防ぐことが出来る。こう
考えれば、C言語の配列がゼロオリジン(配列の先頭要素の添字がゼロ)であることも自然なことであり、容易に理解出
来るのではないだろうか。
elispにおいては、配列の初期化、各要素の印字に C言語と違って添字変数を使わないで処理している。これは文字列、
リスト、配列といったシーケンスは、言語によって終端が管理されており、一括処理するファンクションが用意されて
いるからである。配列要素内の参照、変更は arefや asetを使って行なう。とくに配列要素の変更はリストよりもやり
やすい。
(let ((array [1 2 3 4 5])) (aset array 1 10) array)[1 10 3 4 5]
(let ((list '(1 2 3 4 5))) (setcar (nthcdr 1 list) 10) list)(1 10 3 4 5)
25
上の 2つを比べると、配列の方は指定した要素に直接値を設定しているように見え、リストの方は指定した要素が car
になるようリストを取り出してから変更している。このあたりは、後の章でさらに詳細に触れるかもしれない。
原著では、配列の章節であるにもかかわらず、それ以上に if~else ifについて説明しているが、これは switch~
caseへの布石であり、邪悪な判定の羅列を生みやすいと思われるので、ここでは割愛する。
1.7 1.7 関数関数
原著には「Cの関数は、Fortranのサブルーチンあるいは関数、Pascalの手続きあるいは関数に対応する。」と書か
れている。これからプログラミング言語を学ぼうとして、C言語のバイブルと名高い原著を手に取った人にとっては、他
の言語ではこうであるとか、ここは他の言語と似ているとか、書かれてもおそらくよく分からないだろう。もしそう書
きたいのなら、本書のように両論併記すべきだろうが、そのような言語仕様書や言語解説書は混乱を招くだけなのでほ
とんど存在しない。また、私のような素人が専門家の監修なしに本を出版するのは不可能だ。
しかし、ソフトウェア工学や言語開発の専門家は、プログラミング言語の系譜というのをかなり強く意識している。と
いうのも、彼らの研究において生み出されるかもしれない新たなプログラミング言語は、現存するプログラミング言語
と全く無関係というわけにはいかないのだ。もし、過去もしくは現在あるプログラミング言語と全く無関係で、真に新
しい発想と原理の下に作られたプログラミング言語が完成したとして、誰がそれを学び、使おうとするのか。C C +→
+ Java→ の流れは、明らかにプログラミング言語の系譜を意識していると思われる。
数学は苦手だが、関数とはやはり y=f(x)の世界ではないかと思う。elispで言えば(setq y (f x))である。
───────────────────────────────────────────────────
#include <stdio.h>
int power(int base, int n){ int i, p;
p = 1; for (i = 1; i <= n; ++i) p = p * base; return p;}
int main(){ int i;
for (i = 0; i < 10; ++i) printf("%d %d %d\n", i, power(2, i), power(3, i)); return 0;}
$ ./power0 1 11 2 32 4 93 8 274 16 815 32 2436 64 7297 128 2187
26
8 256 65619 512 19683
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'cl)
(defun power (base n) (do ((p 1 (* p base)) (i 1 (1+ i))) ((< n i) p)))
(do ((i 0 (1+ i))) ((<= 10 i)) (princ (format "%d %d %d\n" i (power 2 i) (power 3 I))))
$ ./power.el0 1 11 2 32 4 93 8 274 16 815 32 2436 64 7297 128 21878 256 65619 512 19683
───────────────────────────────────────────────────
これは y=xnを計算する power関数を用いて、2と3のべき乗を 0乗から 9乗まで行なうプログラムである。ここまで
書き進めてきて、やっと main以外の関数が出てきたことになる。正確に言えば、始めて呼び出される側の関数を書くプ
ログラムが出てきたと言うべきか。そして今回から突然 main関数の型が省略されずに intと表記され、処理の最後で 0
をリターンしている。変数にデータ型があるように、関数にもデータ型がある。y=f(x)であるから、f(x)は値を返し、
その値が yに代入される。また、関数の引数にもデータ型が表記されるようになった。これは原著が ANSI Cに合わせ
て初版から変わった部分である。
int power(base, n)int base;int n;{ ...}
ANSI C以前は上記のように表記していた。何のためにこうした表記方法の変更が行なわれたのかは不明である。引数の
型チェックを行なうようになったからだ、という人がいるが、そのために表記を変える必要があるとは思えない。ANSI
C以前からあった lintコマンドは、関数呼び出し時の引数の個数や型の違いを、ゆらぎとして検出出来ていたからであ
る。詳しいことはコンパイラ屋に聞かないと分からないかもしれない。
elispの方では、power関数のみ定義し、main関数に当たる部分は直接実行するように書いている。変数 iは両方の
関数で使用しているが、この場合は両方でそれぞれローカル変数として宣言しているので、別の変数として機能する。
(defun test1 (a) (+ a i))test1
27
(let ((i 100)) (test1 7))107
elispでは変数 iを上記のように使用することも不可能ではないようだが、このように大域的に使用したい変数にはそ
れなりの変数名を付けて使用することを推奨する。iのような如何にもローカル変数のような名前の変数を大域的に使用
すれば、必ず誤りの元となる。
(let ((i 100)) ((lambda (a) (+ a i)) 7))107
elispにおいて、変数 iを呼び出し先の関数の中でも使用出来るのは、test1の呼び出しがインタプリタには上記のよ
うに解釈されるからではないかという気もする。C言語では、ローカル変数をスタック上に静的に配置するので、呼び出
された側でその変数を見つけるには、現在のフレームポインタからのオフセットが必要になるが、関数呼び出しをまた
ぐオフセットは、その関数の呼び出され方によって不定となるため、そもそも許されていない(未定義の変数と判断さ
れコンパイルエラーとなる)。もちろん、ポインタ変数を使用すれば呼び出された側でも参照可能だし、値の変更だっ
て出来る。elispの場合は、letによるローカルバインドをスタック上に配置するときに、マーカーを付与するので、
呼び出された側で直近のマーカーを探索することでその変数を見つけるようだ。
このあたりの話は、あまり突き詰めると lambda式愛好家などを敵にまわしそうな気がするのでやめる。
1.8 1.8 引数ーー値による呼び出し引数ーー値による呼び出し(call by value)(call by value)
この節では、C言語において関数を呼び出す際の引数は呼び出された側でローカル変数のように使用できる、ということ
を書いている。さっき書いたように、呼び出し側が関数の引数として渡すデータは、それがローカル変数であってもコ
ピーされてスタックに積まれるので、呼び出された側はそれを変数として使用しても悪影響は出ない。ただし、引数が
ポインタ変数の場合は注意が必要である。
前の節で余計なことを書いたために、ここでは書くことがなくなってしまった。
1.9 1.9 文字配列文字配列
C言語における配列の最も一般的な形は、文字の配列である。
───────────────────────────────────────────────────
#include <stdio.h>#define MAXLINE 1000
int get_line(char s[], int lim){ int c, i;
for (i = 0; i < lim 1 && EOF != (c = getchar()) && c != '\n'; i++) s[i] = c; if (c == '\n') { s[i] = c; ++i; } s[i] = '\0'; return i;
28
}
void copy(char to[], char from[]){ int i;
for (i = 0; '\0' != (to[i] = from[i]); ++i);}
main(){ int len, max; char line[MAXLINE], longest[MAXLINE];
max = 0; while (0 < (len = get_line(line, MAXLINE))) if (len > max) { max = len; copy(longest, line); } if (0 < max) printf("%s", longest); return 0;}
$ ./maxline < maxline.c for (i = 0; i < lim && EOF != (c = getchar()) && c != '\n'; i++)
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'stdio)(require 'cl)
(defconst MAXLINE 1000)
(defun getline (s lim) (let (c (i 0)) (while (and (< i (1 lim)) (not (eq EOF (setq c (getchar)))) (not (= c ?\n))) (aset s i c) (setq i (1+ i))) (when (= c ?\n) (aset s i c) (setq i (1+ i))) (aset s i 0) i))
(defun copy (to from) (do ((i 0 (1+ i))) ((= 0 (aref from i))) (aset to i (aref from i))))
(let (len (max 0) (line (makevector MAXLINE 0)) (longest (makevector MAXLINE 0))) (while (< 0 (setq len (getline line MAXLINE))) (when (> len max) (setq max len) (copy longest line))) (when (< 0 max) (printf "%s" (mapconcat (function (lambda (c) (if (zerop c) "" (chartostring c))))
29
longest ""))) 0)
$ ./maxline.el < maxline.el(let (len (max 0) (line (makevector MAXLINE 0)) (longest (makevector MAXLINE 0)))
───────────────────────────────────────────────────
文字配列の節のプログラム例は、標準入力から行単位に文字列を読み出して、最も長い行を印字するものである。Cプロ
グラムでは、main関数を先頭に書くと呼び出す関数の宣言が要るので、呼び出す関数を先に配置している。これは原著
の主旨と合わないかもしれないが、あまり重要なこととは思わない。elispの方は、ほぼ C言語版に忠実に書いたが、
配列だと結果出力のときに多少面倒なことになる。
C言語の方では、getlineという関数名が stdio.hで定義されているものとぶつかるので、get_lineの名前に変更し
ている。
これは前節で書くべきだったことかもしれないが、適切なプログラム例がなかったのでこちらに書く。C言語では line
変数を文字配列の先頭アドレスとして渡し、getline関数では変数 sとして受け取っているが、これにより main関数
内の line変数の領域を使用することになる。なので、変数 s自体を変更してはいけない。しかし、elispでは何も気
にせずに生成したベクトル変数 lineを渡して、getline関数の中で中身を変更している。elispにはポインタなどと
いう概念はないが、逆に言えばすべてがポインタ変数のようなものとも言える。C言語においては文字配列はつまり文字
列であり、printfの%s書式で普通に印字出来る。
指定されたファイルを開いて、最も長い行を表示するだけなら非常に簡単なのだが、標準入力から 1文字ずつ読み出し
て処理しなければならないので、プログラムが無駄に長くなる。
───────────────────────────────────────────────────
#!/usr/local/bin/emacs script
(require 'stdio)(require 'cl)
(defun getline () (do* ((c 0 (getchar)) (s "" (concat s (if (characterp c) (chartostring c) "")))) ((or (eq c EOF) (= c ?\n)) (if (eq c EOF) EOF s))))
(do* ((line "START" (getline)) (len 0 (length line)) (longest "" (if (< max len) line longest)) (max 0 (if (< max len) len max))) ((string= "" line) (printf "%s" longest)))
$ ./maxline2.el < maxline2.el(s "" (concat s (if (characterp c) (chartostring c) ""))))
───────────────────────────────────────────────────
elispでは文字配列などという面倒なものを使う必要がないので、文字列の長さを気にする必要がない。そう考えて書
き直すと、一例ではあるが上のプログラムのようになる。かなり短くなったが、元々大した処理じゃないので、見通し
という点でもこのくらいが妥当ではないだろうか。
30
大きく違うのは copyという関数をなくしたこと。より長い行が見つかったからといってべたコピーする必要はなく、
longestが指す文字列オブジェクトを変更するだけで良いはずだ。次に main処理と getline関数ともに、bodyのな
い do文だけで処理するようになっている。結果的に getline関数は引数がなくなり、文字列を返す仕様に変わってし
まった。do文は慣れないと分かりにくいかもしれないが、変数の初期化をダミーにして素通りさせ、終了条件を判定さ
せ、再初期化後から本来の処理をするように書くとあまり苦労しないようだ。しかし、どうしても上手く制御出来ない
ようなら、素直に whileを使って書いた方が良い。
1.10 1.10 外部変数と通用範囲外部変数と通用範囲
外部変数とは、ローカル変数と違って実行するコードと同様の扱いでメモリ上にローディングされる領域である。これ
が必要となるのは、通常ソースファイルを分割しなければならない規模になったときと考えるのが普通であり、まして
原著で取り上げているような、tinyプログラムでは外部変数を使わないで書いた方が良い。外部変数を使うと、関数の
引数が不要になるので便利だと言う人がいるのかもしれないが、端的に言って論外だ。
原著では、さっきのプログラム(最も長い行を表示する)の特別版として外部変数を使用した例を載せている。しかし、
なぜ外部変数を使用することで特別版なのか。あまりに下らないので、勝手ながら別の特別版を載せることにする。
elisp版はさっき書いたので省略。
───────────────────────────────────────────────────
#include <stdio.h>#include <malloc.h>#include <string.h>
#define APPENDSIZE 16
char *get_line(){ int c, i = 0; char *line = (char *)malloc(APPENDSIZE);
line[0] = '\0'; while (EOF != (c = getchar())) { if (c == '\n') break; line[i++] = c; if (0 == i % APPENDSIZE) line = (char *)realloc(line, i + APPENDSIZE + 2); } if (c == '\n') line[i++] = '\n'; line[i] = '\0'; return line;}
main(){ int len, max = 0; char *line, *longest;
while (1) { line = get_line(); len = strlen(line); if (len > max) {
31
max = len; longest = line; } else free(line); if (0 == len) break; } if (0 < max) printf("%s", longest); return 0;}
$ ./maxline2 < ./maxline2.c line = (char *)realloc(line, i + APPENDSIZE + 2);
───────────────────────────────────────────────────
このプログラムのこだわりとして、get_line関数は標準入力から 1行分の文字列を取ってきて返すという仕様でブ
ラックボックス化したこと、main関数において文字列領域のサイズを意識する必要がないこと、最長行を保存しておく
のにいちいちコピーしないこと、くらいだろうか。ちなみに環境依存かもしれないが、(c == '\n')を('\n' == c)
と書くと判定を誤ることが分かった(これのために gdbまで使わされた)。まあ、原著のプログラムより少しはマシと
いった程度か。
この章の elispプログラムで使用したインチキ stdioは、結果的に何も追加することなく終わってしまった。getsく
らい追加しておけば、getline関数で何も悩む必要がなかったかもしれないが、それはさっきのプログラムを書くこと
によって、既に出来ているようなものだ。ちなみに、使っていれば気付かれることではあるが、標準入力から入力しな
いプログラムで、この stdioを使用するとプログラムが端末からの入力待ちになる。これは、Emacs上で言えばミニ
バッファからの入力待ちと同じ状態になるということだ。そして、その入力に関しては終了条件がないので、control
cでブレイクするしかない。もちろんプログラムはそこで終了となってしまう。入力の終了を判定する文字列を決めて、
それを使って入力を終わらせることも出来るが、それをどんな文字列にすれば良いのか悩むより、標準入力を使わなけ
れば stdioを使用しなければ良いだけだと気付き、そのままとした。その他、パイプを読むとどういう挙動になるか興
味があるが、面倒なので試していない。
24個ある演習問題は割愛させていただいた。この章に載せた elispプログラムを咀嚼出来ていれば、悩むような問題は
ないであろうと楽観しているが、どうだろうか。
まだ第 1章が終わったばかりであるが、既に何度も C言語の悪口や、原著の書きっぷりに対して批判めいたことをいく
つか書いてしまっていることについて、謹んでお詫びする。
32