| 関数 | 有力な語源説 | 根拠の性格 |
|---|---|---|
terpri | “TERminate PRInt” | Emacs Lispマニュアルに明記 |
print | READに対する “PRINT” | 改行+スペースはTTY時代の名残 |
prin1 | “PRINt 1 Object”? 低水準部品の段階番号、 または「1つのオブジェクトを同一性保持で印字」 | Lisp 1.5の定義と整合。 JonL Whiteの回想 |
princ | “PRINt the Characters” | JonL Whiteの証言。 EXPLODEC/FLATCの命名パターン |
prin1はLisp 1.5(1962年)から存在する出力プリミティブで、当初は「行を終端しない、atomic symbolだけを印字する」低水準な部品でしたprincはMacLisp時代に整備された関数で、関係者の回想では “PRINt the Characters” の略とされており、周辺関数の命名パターンもC = character説を補強しています- どちらも公式仕様に語源の明記はなく、Common Lispの設計者の一人であるKent Pitmanは「
ATAN2と同様、アセンブリ言語の入口名が偶発的に残った可能性がある」と慎重に留保しています
1. print, prin1, princ
Common Lispを使っていると、print、prin1、princという出力関数の名前に気になります。
prin1の「1」やprincの「c」が何なのか、語源がはっきりしません。。
三者の動作の違いは、文字列とシンボルを渡してみるとわかります。
;; 戻り値はいずれも引数そのもの
(prin1 42) ; => 42 (出力も 42)
(princ 42) ; => 42 (出力も 42、数値は同じ)
;; 文字列を渡した場合
(print "hello") ; => 改行してから "hello" 、末尾にスペース
(prin1 "hello") ; => "hello" 引用符つきで出力、READ可能
(princ "hello") ; => hello 引用符なしで出力、人間向け
;; スペースを含むシンボルを渡した場合
(prin1 '|A B|) ; => |A B| 縦棒でエスケープ、READ可能
(princ '|A B|) ; => A B 生の文字列、READ不可Code language: Lisp (lisp)
結果を見ると、printとprin1は、引用符や縦棒エスケープもそのまま出力します。
違いは、先頭の改行のある・なし。
一方、princは、引用符などは出さず、文字列やシンボルの中身を出力します。
1.1. Common Lispのprintは画面表示ではない
Common Lispのprintは、Pythonのprint()やJavaScriptのconsole.log()とは少し設計上の役割に違いがあります。
多くのプログラミング言語のprintは、「画面に文字を出す」関数です。
しかし、Lispのprint関数は「Lispのデータ構造を、Lispが読み戻せる文字列に変換して出力する」という設計になっています。printは、対話環境 REPLの「P」で、評価結果を人間に見せると同時に、評価(eval)した結果を、次にreadできる形(S式)として返すというサイクルが一環です。
このサイクルの基本となる関数は prin1で、その出力はreadでそのまま読み戻せる「Lispの外部表記」ということになります。
一方、人間向けの文字列の出力関数としては、別にprincが用意されました。
名前はわからなくても使えます。
ただ、名前の由来を知ると、この三者がどういう順番で、何のために作られたのかが見えてきます。
語源を辿るには、Lispの歴史を1958年まで遡る必要があります。
1.2. マッカーシーの原論文に出力関数はなかった
1960年4月、John McCarthyは、Communications of the ACMに論文を発表しました。
“Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I”、日本語にすると「記号式の再帰関数と機械による計算(第一部)」。
LISPの出発点となる論文です1。
この論文には、printという関数は登場しません。prin1もprincも、ありません。
それは、論文の目的は「計算の形式的定義」だったからです。
S式(symbol-expression)とは何か。
そして、万能関数applyとevalをどう定義するか。
;; 論文の5つの基本関数(Common Lispで確認)
(car '(a b c)) ; => A 先頭要素
(cdr '(a b c)) ; => (B C) 先頭以外
(cons 'a '(b c)) ; => (A B C) 先頭に追加
(atom 'x) ; => T アトムか
(eq 'a 'a) ; => T 同一かCode language: Lisp (lisp)
論文に登場する基本関数は、car、cdr、cons、atom、eqという5つだけで、これだけで再帰関数が書けることをマッカーシーは示しました。
しかし、そこには出力に関わるものはまだ、何もありませんでした。
マッカーシーは後年の回想(1979年の”History of Lisp”)にこう書いています。
「1950年代後半は、きれいな出力や便利な入力記法は一般的に重要とは思われていなかった。
そういった処理をするプログラムは当時利用できたメモリに入りさえしなかった2」。
出力は「実装上の問題」として、理論とは切り離されていたのです。
2. Lisp 1.5 のREADとPRINT
LISPの実装は1958年秋に始まりました。
MIT人工知能プロジェクトで、マッカーシーとマービン・ミンスキーが立ち上げたチームの、当初の目標はコンパイラでした。
しかし、そのコンパイラを作るための準備として書かれたのが、リスト構造を読み書きするプログラムです。
Lisp 1.5 Programmer’s Manual(1962年)のクレジットによると、READとPRINTは、マッカーシー本人を含む四人によって書かれました。
「print and read programs were written by John McCarthy, Klim Maling, Daniel J. Edwards, and Paul W. Abrahams3」。
2.1. 内部表現と外部記法
このREADとPRINTのプログラムは、予期せぬ副産物をもたらしました。
それは、括弧で表記するS式の記法です。
いまではLispの代名詞となったS式ですが、マッカーシーはもともとFORTRANに近い「M式(M-expression)」という記法を考えていました。
S式は、当初は 単に PRINT の出力する「外部形式」として考案されました。
つまり、Lispの内部表現をそのまま見るための形式です。
マッカーシーはこう書いています。
「READとPRINTのプログラムは、記号情報に対する事実上の標準の外部記法を生み出した。
たとえば x + 3y + z を (PLUS X (TIMES 3 Y) Z) と表記するスタイルだ。
他の記法を使うには特別なプログラムが必要になる。
なぜなら標準的な数学記法は演算子ごとに構文が違うからだ」4。
LISPコードは、処理系内ではコンスセルのリスト構造として表現されています。
データ構造そのものには「見た目」がありません。
ユーザーが式を入力するとき、また結果を表示するとき、どんな文字列として読み書きするかはREADとPRINTの実装が決めます。READが解析できる形式とPRINTが出力する形式が、そのままLISPの「外部記法」になるわけです。
この「外部記法」には、いろんな構文が考えられますが、初期のREADとPRINTの実装では、「括弧付き前置記法で書いてください、そうすれば読めます」という約束事になっていました。
一般的な数式の書き方に比べて、構文解析が容易だからです。
これが、そのままS式としてLispの記法として定着し、「Cambridge Polish」と呼ばれるスタイルと呼ばれました。
Lispにおいては、M式のような数学的な記法は試みられたものの、定着しませんでした。
2.2. READ-PRINT対称性
LispのPRINTには、他言語の「表示する」関数と本質的に異なり、「READ-PRINT対称性」という性質があります。
READとPRINTは対になって設計されていて、PRINTの出力はREADの入力として受け取れる形、つまりLispのデータ構造として読み戻せる文字列でなければならない、という性質です。
;; READ-PRINT対称性の確認
(let ((obj '(a "hello" 42 |A B|)))
(let ((str (prin1-to-string obj))) ; PRINTが出力する文字列を取得
(equal obj (read-from-string str)))) ; => T READで読み戻せるCode language: Lisp (lisp)
もし、文字列"hello"の出力をhelloとしてしまうと、読み戻したときにシンボルと区別できなくなります。
そのために、printやprin1は、引用符や縦棒エスケープを出力します。
そして、この出力された文字列が、readの受け入れるコードです。
つまり、出力プログラムが言語の見た目を決めた、ということです。
3. Lisp 1.5の出力体系とテレタイプ
1962年に刊行されたLisp 1.5 Programmer’s Manualには、出力に関係する関数が整理されています。
- punch
- prin1
- terpri
このマニュアルの関数一覧には、まだPRINCは存在しませんでした。
今では考えにくいことですが、当時のコンピュータには画面(ディスプレイ)がありませんでした。
その主な出力先は、「紙」です。
そのため、printは、まさに紙に印字することで、さらに原始的な方法としてパンチカードに出力するpunchという疑似関数もありました。
当時のコンピュータは、大きな計算機そのものと入出力端末となる「テレタイプ」が分かれていました5。
「テレタイプ」は、キーボードとプリンタが一体になった機械で、印字ヘッドが紙の上を走って文字を打ちつけます。1分間に100文字前後しか出せません6。
printとpunchは、「S式一個」を印刷出力またはパンチカード出力する疑似関数です。
「In each case, the print or punch buffer is written out and cleared so that the next section of output begins on a new record(いずれの場合も、バッファを書き出してクリアし、次の出力が新しいレコードの先頭から始まるようにする)」と説明されています。
一つのS式を出力し終わったら行を終端する、という仕様になっています。
一方、prin1は、単なるシンボル(atomic symbol)を1つ印字する疑似関数です。
「prin1 is a pseudo-function that prints its argument, which must be an atomic symbol, and does not terminate the print line(prin1は疑似関数で引数を印字するが、引数はatomic symbolでなければならず、印字行を終端しない)」。
こちらは、行で終端せず続けて出力でき、複数回呼ぶことで、atomic symbolを行内に並べることができます。
terpriという関数もあります。
「terpri prints what is left in the print buffer, and then clears it」とあります。
この名前は、おそらくEmacs Lispマニュアルに「stands for ‘terminate print’」と書かれているように「terminate print」の略なのでしょう7。
Lisp 1.5での典型的な使い方は、こういうものだったと考えられます。
;; print は一発で行ごと出力する
(print '(a b c))
;; =>
;; (A B C) (改行から始まり、末尾にスペース)
;; prin1 は行を終端しないので、続けて書ける
(prin1 'VALUE)
(prin1 '=)
(prin1 42)
(terpri)
;; => VALUE=42 (改行)Code language: Lisp (lisp)
つまり、printはS式一個を行ごと出力する重い関数、prin1は1つのシンボルを行末なしで出す軽量な部品と、役割が分かれていました。
Lisp 1.5のprin1は低水準な部品でしたが、その後の MacLispの開発の中で汎用化し、現在のCommon Lispではatomic symbol限定という制約はなく、S式全般を受け付けるようになっています。
そのため、printとprin1の違いは、冒頭に改行があることだけになりました。
3.1. 紙に印字する制約下での改行と空白設計
printは、改行が始まるだけでなく、末尾に自動的にスペースを付けます。
(progn (print "abc") (princ "def") (princ "123"))
;;=>
;;"abc" def123 Code language: Lisp (lisp)
これは、「テレタイプで長いシンボルを出力したとき行の途中で改行が入っても、スペースがあればトークン境界になる」という現場の知恵が埋め込まれています。
実は、テレタイプでは、タイプライターと同じ構造で、改行のときに印字ヘッドを行先頭に戻し(キャリッジリターン)、紙を一行送る動作(ラインフィード)をする必要がありました。
この二つは別々の制御信号で、プログラムが明示的に送る必要がありました。
これが、式の終端だけでなく、しばしば途中でも発生します。
シンボルが長いと行の途中でもCRLFが入り、次の行で続きが読まれたのです。
すると、改行がトークンの区切りにしてしまうと、シンボルが分かれてしまいます。
そのため、古いLisp方言では改行はトークン区切りにはしませんでした。
そこで、スペースがトークン区切りを保証することになります。
つまり、printは「Model 33やModel 35 TTYのキャリッジリターンと紙送りが必要な複合オブジェクトにまで考慮した」関数というわけです。
これが、60年以上経った今のCommon Lispにそのまま残っていて、現在のSBCLでも動作は変わっていません。
4. MacLispとPRINCの登場
Lispに PRINCが加わったのは、Lisp 1.5を引き継いだ「MacLisp」で、このときに出力関数の体系が現在に近い形に整理されました。
「MacLisp」を実装したのは、MITの人工知能研究グループです。
「MAC」は、Machine-Aided Cognition(あるいは、Multiple Access Computer)というプロジェクトで、最初のコードベースはRichard Greenblattが書き8、その後Jon L. Whiteがメンテナンスと発展を引き継ぎました9。
Kent PitmanのよるMacLispのリファレンスマニュアル「The Revised Maclisp Manual」には、PRINCとPRIN1の違いがこう説明されています10。
「PRINC will print objects without slashification, but that output will not be suitable for being re-read by READ. The PRIN1 (and PRINT) functions will output objects with any slashification necessary for them to be later re-read by READ」。
ここでのprincのprin1との主な違いは、「スラッシュ化」の有無です。princは、スラッシュ化をしません。
;; MacLispの例(/ がエスケープ文字)
(PRIN1 '|A B|) => /A/ /B/ ; スラッシュあり、READ可能
(PRINC '|A B|) => A B ; スラッシュなし、人間向けCode language: Lisp (lisp)
MacLispでは、シンボルをREADで読み戻せる形にするためには、エスケープ文字として(/を付ける必要(スラッシュ化)がありました。
そのため、スペースを含むシンボル|A B|をPRIN1で出力すると、内部表現に読み戻せる形 /A/ /B/ に変換して出力されます。
これは、人間にとっては読みにくいので、スラッシュ化なしの PRINC が生まれました。
4.1. Common Lispでの仕様の整理
MacLispの体系は、1984年のCommon Lisp(Guy Steele編のCommon Lisp the Language、CLtL1)にほぼそのまま引き継がれました11。
Common Lispの仕様では、prin1、princ、printのそれぞれの動作の違いは、汎用関数writeを使った等価式で定義され、prin1は:escape tの、princは:escape nil :readably nilの、printは改行とスペースを伴うprin1の、
として位置づけられています。
(prin1 object output-stream)
== (write object :stream output-stream :escape t)
(princ object output-stream)
== (write object :stream output-stream :escape nil :readably nil)
(print object output-stream)
== (progn (terpri output-stream)
(write object :stream output-stream :escape t)
(write-char #\space output-stream))Code language: Lisp (lisp)
4.2. Cは character?(JonL Whiteの回想)
動作の違いはわかるものの、PRINCのCが何を意味するかについては、MacLispやCommon Lispのマニュアルには書かれていません。
この疑問について、2001年12月、comp.lang.lispというUsenetグループで、Erik Naggum が Kent Pitman に投げかけた質問の応答が記録されています12。
「Common Lispを友人に教えていて面白い疑問が出た。prin1という名前はどこから来たのか。princの名前はどこから来たのか」。
Kent Pitmanは「(初期のMacLispのコード開発者であった)JonL Whiteに聞いてみる」と答え、JonL Whiteからの返答が投稿されました13。
「昔の話なので print に関する記憶は定かではないが、princ、prin1、printのシリーズは、Lispでテキストを書くための段階的な試みだったと思う」と答えています14。
JonL Whiteによる解釈では、三つの関数は段階的な設計として生まれた、と考えられます。
| 段階 | 関数 | 役割 |
|---|---|---|
| 1 | princ | 文字をそのまま印字(PRINt the Characters) |
| 2 | prin1 | 1つのオブジェクトを、同一性を保てる形で印字 |
| 3 | print | 複合オブジェクトを、TTYの行管理まで含めて印字 |
Lispの歴史的には、prin1、print そして princ の順番で登場しましたが、MacLispの実装においては、princが基盤になっていたのかもしれません。
4.3. 関数名の共通性とアセンブリ言語(Pitmanの解釈)
ただ、この証言に対して、Kent Pitman自身は留保を付けています。
確かに、MacLispにはC = characterと読める命名パターンが複数あります。
たとえば、オブジェクトを文字のリストに変換する EXPLODE、EXPLODEN、EXPLODECという三つの関数です。
;; MacLispの例(/ がエスケープ文字)
(EXPLODE '(|A B|)) => (/( /| A | | B /| /)) ; PRIN1形式、シンボルで
(EXPLODEN '(|A B|)) => (40 65 32 66 41) ; PRINC形式、整数で
(EXPLODEC '(|A B|)) => (/( A | | B /() ; PRINC形式、シンボルでCode language: Lisp (lisp)
EXPLODEは、オブジェクトをPRIN1が出力するであろう文字のリストとして返します。EXPLODENとEXPLODECは、PRINCが出力する文字に対応し、Nは整数のリストで、’C’はシンボルのリストで返します。
ここでは、Nがnumeric(整数)、Cがcharacter(シンボル)という対比になっています。
同じパターンは、文字数を返す FLATSIZEとFLATCにも見られます。
FLATSIZEはPRIN1が出力するであろう文字数を返し、FLATCはPRINCが出力する文字数を返します15。
MacLispのこれらの関数は、Common Lispにはありませんが、prin1-to-stringとprinc-to-stringで同じ情報が得られます。
;; FLATSIZE 相当
(length (prin1-to-string "hello")) ; => 7
;; FLATC 相当
(length (princ-to-string "hello")) ; => 5 Code language: Lisp (lisp)
SIZEとCの対比は、この差をそのまま名前に込めています。
Pitmanもこれらの命名パターンを根拠にして、「Cはcharacterだと思う」と推測していますが、一方で別の可能性にも言及しています。
「かつて、ATAN2という関数の名前を聞いたとき、二引数版のATANだから2がついているのだと思った。
ところが、実際にはATAN1やATAN1Aというアセンブリ言語の入口名があって、ATAN2はその偶発的な続きにすぎなかった」と聞かされたそうです。
初期のLispには、CARやCDRのようにアセンブリ言語に直接由来する関数名も多くあります。
「特別な根拠があるわけではないが、もしかすると PRIN1とPRINCも同じかもしれない」とPitmanは書いています。
5. 【補足】print-object と prin1 の「ねじれ」
Pitmanは、print系の関数名の設計について、もう一つ指摘しています。
「PRINT-OBJECTという名前の関数があるが、これはPRINTではなくPRIN1の挙動を実装している。本当はPRIN1-OBJECTかWRITE-OBJECTと呼ぶべきだった。名前の整理が悪かった」16。
print-objectは、クラス定義されたオブジェクトの出力を決めるメソッドです。
たとえば、pointクラスを作って、表示してみます。
(defclass point ()
((x :initarg :x) (y :initarg :y)))
(defmethod print-object ((p point) stream)
(print-unreadable-object (p stream :type t)
(format stream "~a,~a"
(slot-value p 'x) (slot-value p 'y))))
(defparameter *p* (make-instance 'point :x 1 :y 2))
(print *p*) ; =>
; #<POINT 1,2> (改行から始まり、スペースで終わる)
(prin1 *p*) ; => #<POINT 1,2>
(princ *p*) ; => #<POINT 1,2>Code language: Lisp (lisp)
print関数は、内部で 改行してから、print-objectメソッド を実行し、スペースを付けます。
つまり、print-object は「printに対応するメソッド」ではなく、実際にはprin1の挙動を提供しているのです。
PRIN1という古い名前が引き継がれた結果として生まれた命名のねじれで、歴史の重なりがそのまま残っています。
5.1. princ に刻まれた歴史
もっとも一般的な名前である print は、改行、引用符など、スペースが付く。
基本的な出力は、prin1 と princ という、意味のわからない短縮名になっている。print-objectは、printではなく、prin1 に対応する挙動を実装している。
こういった「ねじれ」を見ると、Common Lispが整合性に欠けると感じる人もいます。
しかし、マッカーシーが1958年秋にチームでサブルーチンを手書きしてから、Lisp 1.5、MacLisp、現在の Common Lisp などに至るまで、使う人が変わり、マシンが変わり、OSが変わってきました。
printが改行とスペースで終わるのは、今は使われないテレタイプ端末のための要件です。prin1の1が何かは、その関数を最初に書いた人も今は確かめられません。
それでもprint関数は動き続けてきました。
自然言語と同じで、語源が忘れられた言葉を日々使い、過去の慣用がそのまま生き残っています。
60年以上にわたって人が書き、動かし、議論し続けた言語には、設計書だけでは生まれない層が積もります。
princとprin1の名前の謎を追うのは、そういう層をたどることでもあるのかもしれません。
- Communications of the ACM, Vol.3, No.4, pp.184-195, DOI: 10.1145/367177.367199。タイトルに「Part I」とあるが、「Part II」はついに書かれなかった。マッカーシーは後に「Part IIは代数式への応用を扱う予定だった」と述べている。 – History of LISP (McCarthy, 1979)
- IBM 704の主記憶容量は最大32,768語(1語36ビット)。初期の搭載量はさらに少なく4,096語からスタートした。記憶容量に対する切実な感覚が、この発言の背景にある。 – IBM 704 – Wikipedia
- Daniel J. Edwardsはガベージコレクタと算術機能も担当。Timothy P. HartとMichael I. Levinがコンパイラとアセンブラを担当した。初期のコンパイラはRobert Braytonが書いた。 – LISP 1.5 Programmer’s Manual (MIT Press, 1962)
- S式が外部表記として定着した。M式は後にいくつかの処理系で試みられたが定着しなかった。 – History of LISP (McCarthy, 1979)
- 1960年代のLisp研究者が使っていたのは、DEC PDP-6、PDP-10、そしてIBM 704などのマシンで、入出力端末はTeletype Model 33やModel 35のようなテレタイプ機器でした。
- Teletype Model 33は1963年に登場したASCII対応のテレタイプ端末で、110ボー(毎秒約10文字)で動作した。1台の価格は1,000ドル前後(当時)で、DEC PDPシリーズと組み合わせて広く使われた。後にModel 33の廉価さとASCII互換性がコンピュータ端末の標準を形成していく。LinuxのTTYという用語の語源でもある。 – Teletype Model 33 – Wikipedia
- Emacs LispではGNU Emacs Reference Manualにこの語源が記されている。
terpriはCommon LispのANSI仕様でも継承され、「output streamへ改行文字を出力する」関数として定義されている。 – GNU Emacs Lisp Reference Manual: Output Functions - Richard Greenblattは後にMIT Lisp Machineプロジェクト(1974年)を創設し、ハードウェアとソフトウェアの両方を設計した。LMI(Lisp Machines Inc.)の共同創業者でもある。 – Maclisp – Wikipedia
- MacLispという名前が使われ始めたのは1970年代初頭で、JonL Whiteが1970年に書いたAIM-190(MIT AI Memo No.190)には「MacLisp refers to the PDP/6 implementation of LISP at the Artificial Intelligence Group of Project MAC」とあります。「MAC」はMassachusetts Institute of TechnologyのProject MACの略で、「Machine-Aided Cognition」または「Multiple Access Computer」の頭字語とされる。AppleのMacintoshとは無関係で、MacLispはMacintoshより十数年早く存在していた。 – Maclisp – Wikipedia
- Pitmanualの正式名称はThe Revised Maclisp Manual(通称はKent Pitmanが書いたことに由来する)。1983年のMIT AIラボ技術報告書として刊行された。現在はmaclisp.infoでオンライン閲覧できる。 – The Pitmanual: New-I/O
- Common Lisp the Language(通称CLtL1)はGuy L. Steele Jr.の編著で1984年に刊行。1986年にANSI標準化委員会X3J13が発足し、1994年12月8日にANSI X3.226:1994として正式な標準となった。CLtL1は標準化前のde facto標準として機能した。
- X3J13 – Wikipedia
- Erik Naggum(1965-2009)はノルウェーのプログラマーで、SGML、Emacs、Common Lispの分野で知られる。comp.lang.lispには14,000件以上の投稿を残し、技術的な知見と挑発的な文体で知られた。2009年に44歳で死去。Pitmanは追悼記事を書いている。 – Erik Naggum – Wikipedia
- Kent M. Pitman(KMP)はX3J13のProject Editorとして、1994年に成立したANSI Common Lisp標準(ANSI X3.226:1994)の文書をまとめた。また自らHTMLに変換してCommon Lisp HyperSpec(CLHS)として公開した、仕様の実質的な主著者でもある。 – Kent Pitman – Wikipedia
- このスレッドはGoogle GroupsのUsenetアーカイブで現在も閲覧できる。投稿日は2001年12月9日〜11日。 – The history of print, prin1, and princ? – comp.lang.lisp
- Pitmanualでは
FLATCとFLATSIZEは「文字列幅を測る」用途で説明されている。GETCHARN(N = numeric)とGETCHAR(シンボル版)にも同じN/Cの対比がある。この一貫したパターンがPitmanのC = character説の根拠になっている。 – The Pitmanual: Character Manipulation - この指摘はcomp.lang.lispの別スレッド(princ & print-readably)でPitmanが行っている。
PRINT-OBJECTはprintが付けるはずの改行やスペースを付けず、prin1と同等の出力をする。命名は歴史的経緯からくる妥協だとPitmanは述べている。 – princ & print-readably – comp.lang.lisp