見出し画像

Retro-gaming and so on

ANSI Common Lispは関数型言語

Schemeでは関数型プログラミングっぽいプログラミング方法が主流だ。
しかし、一方、一般にはCommon Lispユーザーはそういうプログラミング法を好まないし(※1)、本によっては「複数も値を返す関数を持ってる言語のどこが"関数型"なんだ?」(※2)とLispを関数型として捉える事を「間違い」と言っている。
これは一種フレームの元だ。しかし、大本の原因を突き詰め、プログラミング言語の発展の歴史を鑑みると、単純に言えば「Haskell登場以前」と「Haskell登場以降」だと「関数型プログラミング言語」の意味合いが変わっちゃった事にあるんじゃないか。厳密に言って、Haskellを基準とすると、Lisp語族は確かに「関数型言語」ではないのだろう。
しかし、逆に言うと、Haskellより遥かに長い歴史を持つLispに対して、Lispにとっては「後付の」理屈を持ってこられても困るような気もしてる(※3)。
個人的には古典的な、「常に値を返す」ようなプログラムを書き、「引数で返された値を加工する」事を連鎖していくようなプログラミングスタイルを「関数型プログラミング」と呼称して良いと思うし、それで言うと「必ず値を返す」機構になってるANSI Common Lispは古典的にせよ、「関数型プログラミング言語」と言って良いと思ってる。
そして、スタイルの問題はさておき、ANSI Common LispはScheme以上に「関数型」だ。なんせSchemeは「値を返さない」手続きが存在する。ANSI Common Lispではそんな事はない。
そう、大方のLispプログラマのプログラミングスタイルはさておき、ANSI Common LispはScheme以上に「関数型言語」だと思っている。
例えば、これは実は偶然だった模様だが、NIL、つまり空リストがある。
Schemeは真偽値として#t#fがあるが、ANSI Common Lispでは真はTだが、偽はNIL、つまり空リストだ。
最初は僕も、「なんつー大雑把な言語なんだ」とか思った(笑)。
しかしながら、Lispではリストは集合を表す。つまり、数学的に、空リストが空集合を表すとすれば非情に合理的なんだ。
例えば「空集合の部分集合は空集合自身のみ」と言うテーゼを考えると次のANSI Common Lispの動作は非常に数学的かつ合理的だ。


ちなみに、これはSchemeだとエラーになる。


つまり、ANSI Common Lispの「数学性」に比べると、Schemeのそれは格段に落ちる、ってのが事実だ。
従って、フツーのCommon Lispユーザーのプログラミングスタイルの動向に比して、ANSI Common Lispは関数型言語、じゃないとしても割に数学的視点では整合性があるようにデザインされてるんじゃないか、と思う。
逆にSchemeはその辺で劣ってる割には、Schemeプログラマは関数型でプログラミングする事を好んでる、と言う一種逆転現象がここには見える(※4)。

さて、現代的な意味での関数型言語は、「副作用をどうするか」と言う点で苦労して発展してきてる。
ハッキリ言っちゃえば「副作用を無くしたい」わけだが、例えば出力が副作用な以上、副作用を無くすと事実上コンピュータでは何も出来なくなってしまう。
ゲームなんかも言っちゃえば副作用だらけ、なんで、始めたはいいけど、裏で計算ばっかしてて何も映らないようなゲームだと困っちゃうだろう。
つまり、「関数型言語に於ける出力とは何か?」と数学的に整合性が出るように研究してきたわけだな。

一方、僕が思うに、ANSI Common Lispの、古典ながらでも関数型言語「らしい」デザインが何か、と言うと実はformat関数の存在だと思っている。
例えば、ポール・グレアムは次のように書いている

Common Lispの中で、言語道断にLispらしからぬ構文はformat文字列にある。 formatはそれ自身の言語を持っており、それはLispではない。Lispに更に構文を追加するならformat記述子もそれに含めるべきだ。マクロが他のコードを生成するのと同じようにformat記述も生成できたら良い。
ある非常に優れたLispハッカーが、彼のCLtLはformatのところで開く癖がついてしまったと私に語ったことがある。実は私のもそうだ。多分これは改善すべき箇所を暗示している。また、プログラムはI/Oをたくさんするものだということも意味している。 

確かにformatは、軽く使うにはイイが、マジメに使おうとするとメッチャムズい。そしてポール・グレアムが言う通り言語道断にLispらしからぬ構文だ。
ただ、視点を変えてみると、これはHaskellなんかと違った方法で、出力をANSI Common Lisp本体から切り離す試みだったんじゃないか、と思える節があるんだ。
本体から出力、と言う、頻出だけど余計な副作用を完全に切り離そう、としたんじゃないか。分からんけどな。
だから見方を変えると、ANSI Common Lispは関数型言語本体 + 出力用言語、って構成と捉える事が出来る。まるでJavaScript + ブラウザAPIみたいだ(笑)。JavaScriptは入出力を一切持ってない。従って、外部とやり取りするには、何らかのAPIに頼らなきゃならない、って事だ。
ANSI Common Lispの設計は、まるで「こういうプログラミング言語のデザインがあり得る」と未来を予見するようなデザインになってると言える。
そして、formatは使うのがややこしいが、一方、出力に関してはまるで「Lisp本体の手を煩わせない」ように、しつこく、神経症的かつ若干粘着気質的にデザインされている(※5)。
言外にformatはまるで、

「計算は本体に任せるっち!でも出力に関しては丸投げしてくれればオイラの方で完璧に処理して見せるっち!」

とでも言ってるようだ。いや、「っち」って言うかどうかは知らんがな(笑)。
特筆すべきは、formatにはループ機構が内蔵されてる辺りだ。個人的意見だと、他の言語でもこんな機能があればいいのに、って思うほどだ。
例えば、書式指定子ってのはキレイに印字するには非常に役立つ「枠組み」を提供してるのは皆さんご存知だろう。
一方で、仮に出力系の関数が何個か分からんが可変長引数を渡される場合。これ、実際に良くあるケースだろう。要するに印字データ形式は分かってるんだけど、それをいくつ受け取るのかが分からない場合、どーすんの、ってのがしばしば生じるんだ。あるだろ?
そういう時、出力関数にループが内蔵されてればいいのに、とか思うんだ。

いや、まずは通常のケースを考えてみよう。この記事を例としてみる。
例えばPythonで、次のようなデータと関数を書く。



構造体(※6)でアイテムを定義して、アイテムリストを作る。
そしてそのアイテムリストに含まれる各アイテムの(出現)ページスロットを利用してあるページに存在する全アイテムを持ったリストを返す。
ここで、だ。
単純には、page_lst関数を利用して文字列のリストを生成、そしてある文字列[0:持ち物を見る]をそのリストの先頭に付け足して表示したい。
さぁ、どう書くか、ってのがこの記事の議題だったわけだが。
Python慣れしてる、あるいはPythonを良く分かってる人は次のように書くだろう。


あるいは、そんなにPythonに慣れてない人や良く分かってない人は次のように古臭いカンジで書くだろう。


スタイルとしては前者が望ましいが、いずれにせよ(そして前者は返り値があるが)、似たような出力結果になる。


もう一度繰り返すが、スタイルとしては前者が望ましい。速いしな。
ただし、本質的な事を言うと、両者のロジックは変わらないんだ。
まず、リスト内包表記自体がループ構文の抽象化だ。そして出力するが為、その処理を繰り返しの中にぶち込んでる、ってのが流れになっている。

「それってフツーじゃん?」

って思うだろ?
その通り、フツーだ。
これは極めて、各言語に見られるオーソドックスな手、なんだけど、ぶっちゃけこれが副作用(この場合は出力だ)をとにかく、何度も反復させなアカン、って言う例になっている。
もう一度繰り返す。
前者の例だと書式指定子込みの「整形文字列」は

f"[{i}:{j.name} 銀貨{j.cost}枚]"

であって、後者はC言語臭い

"[%d:%s 銀貨%d枚]"

だ。
両者とも前提は3つ引数を取ってるが、その「取ってくる側」がリストである以上、長さが確定していない。
つまり、一種の「可変長引数」の仲間的な現象なんだけど、「いくつオブジェクトがやってくるのか分からない」からこそ、ルーピングが必要になるわけだよな。
そしてこの事情はSchemeでも変わらない。
ところが、「出力の全機能を本体から切り離してるように見える」ANSI Common Lispなら次のように書ける。



まぁ、ANSI Common Lispを知らない人には何やってんだかサッパリかもしんない。
しかし、注目して欲しいのは関数show-zaikoのこの部分だ。

(cons "[0:持ち物を見る]"
   (loop with n = 0
     for i in (page-lst page)
     collect
     (format nil "[~a:~a 銀貨~a枚]"
        (incf n) (item-name i) (item-cost i))))

ここの二行目のloop以降が何やってるか分からんとしても、冒頭でconsしてる以上、loop以降はリストを作成してる。
何のこたぁない。結局この部分はPythonでの

["[0:持ち物を見る]"] + [f"[{i}:{j.name} 銀貨{j.cost}枚]" for i, j in enumerate(page_lst(page), 1)]

とやってるこたぁ全く同じなんだ。
しかし、それにかかってるformatが不思議だろ。と言うか、構文的にループが被っていない。それは一体どういう事だろうか。
いや、結果先に書いた通りなんだ。format自体がループを含んでる
ANSI Common Lispのformat関数での書式指定子~{~}は、データがやってくる以上、そこに挟まれてる書式指定子を使ってループするんだ。だからLisp自体のループの力を借りる必要がない。
つまりcons以降はANSI Common Lispでの関数型言語としての仕事だが(※7)、そこでLispとしての仕事は終わりで、あとは別言語でミニ言語のformatが完璧にその中だけで処理してくれる、わけだ。
だから、formatを「ANSI Common Lispの一部」って考えると腹立つんだけど(笑)、Lispとまた別の言語だと思えば割に納得できる存在なんだよな。繰り返すけど、JavaScriptのDOMみてぇなモンで、むしろANSI Common Lispを関数型にしておくために、余計な、出力に関する一切を詰め込んで本体から切り離しちまった、ってのがformatの正体だと思うんだ。

そもそもC言語のprintf程度の事をするならformatは全然難しくない。
問題はそこじゃなく、ポール・グレアムでさえ「本が割れる程formatを参照した」理由ってのは出力にまつわる機能を全部使いたかったからでしょう。
それくらいANSI Common Lispの副作用関数の代表格、formatは万能なんだ。「出力なら全部任せとけ!」みたいな。
そしてそうでもなければ、Lispでは古典的なprinc辺りを使っときゃイイ、って話になるわけだからな。
ANSI Common Lispは巨大な副作用用の機能を抱えていて、それを切り離しつつ内包している。これがHaskellとはまた違った「古典的関数型言語」の設計の解だと思っている。
だからこそANSI Common Lispは設計上、明らかに「関数型言語」なんだ。
まぁ、個人的な意見だけどな。

なお、本気でSchemeとかRacketにANSI Common Lispばりのformatが欲しい。
そうすりゃ本気で出力にまつわるアレコレを切り離せるのに、とマジで思ってたりする僕がここにいる。

※1: ポール・グレアムとピーター・ノーヴィッグを除く(笑)。
なお、ポール・グレアムは元々はScheme方言であるTのヘヴィーユーザーだったわけで、根はむしろSchemerだ。

※2: これは反証にはならない。数学ではポピュラーではないにせよ、「多価関数」と言う概念が存在する。従って、多値を返す関数がある言語だからと言って「関数型とは言えない」と言うのは論になってはいない。
昔、数学の先生が「複数の値を返す関数と言うのはあり得る」と言った事にビックリして、「なんで?」と訊いてみたら、「そういうのは採用する定義に拠るんだ」と言われた。
なるほど、場合によっては「定義を変更するのはアリなんだ」と感心した事を覚えている。

※3: 歴史上初めて登場したLisp解説書、「Lisp 1.5 Programmer's Manual」の話を何度か書いたが、改めてここに書いておく。
この小冊子では「プログラム」と「関数」がむしろ対立概念のように扱われていて、Lispは「関数を書くための道具」になっていて、Fortran型の「プログラミング」をLispで行いたい層は、マクロ「prog」で実現出来る・・・と言うような記述が見られる。
実際問題、ここで言う「関数記述」が壮大な「関数型プログラミング」を意味してるのではなく、あくまでFortran型の「プログラミング法」(つまり現代に続く「メジャーな」プログラミング法)へのカウンター程度の意味合いだったのかもしれないが、いずれにせよ、間違いなくここで「関数型プログラミング」と言う概念への萌芽が見られると思う。
言っちゃえば、世界最初の高級プログラミング言語、Fortranに対して、世界で2番目に登場した高級プログラミング言語、Lispは、登場時点で既に「フツーのプログラミング手法」に対して反旗を翻していたんだ。

※4: 元々同じ人がデザインしたCommon LispとSchemeだが(両方ともガイ・スティール・Jrと言う人が大きく関わっている)、Schemeコミュニティはそれほどではない気がするが、かなりのCommon Lispコミュニティの人間はSchemeを嫌ってたりする。
割にANSI Common LispもSchemeも両方知ってて使ってます、って人口はCLコミュニティでは少ないのではないか。

※5: ほとんど出力専用の性器正規表現用の某、ってくらい仕組みは近い。
性器正規表現が「欲張り」なら、ANSI Common Lispのformatも対抗出来るほど「欲張り」だ。

※6: 何度も繰り返すが、Pythonに於いては厳密にはクラスだ。

※7: ゴメン、実はウソだ(笑)。
まず、ANSI Common LispにはPythonみたいな便利なrangeもないし、ましてやenumerateもない。この例示の為だけに作るのも何だったんで、loopで誤魔化したが、そもそもANSI Common Lispのloopと言う反復技法自体が関数的じゃないんだ(笑)。
しかも、計数の為だけにnも破壊的にincfで増やしてるんで、破壊的変更をやりまくりだ(笑)。
ごめんなさい、formatの説明だけをしたかったんで、相当端折りました。申し訳ありません。二度としません・・・たぁ言えねぇな(苦笑)。
ちなみに、これもsymbol-valuesの返り値をsetfで破壊的変更・・・とやってるんだけど、これはいつぞや書いたトリックで、星田さんがやってたような、I001、I002、I003、・・・と言うようなシンボルを自動生成して、構造体と結びつけてる、言わばSchemeで言えば「自動define」を行ってる部分だ。


つまり、こういう、プログラミング上、フツーは手書きせねばならぬトコを、ANSI Common Lispなら「自動生成出来る」と言う証明の為に書いたようなモノだ。
だから冒頭の*item-list*にはシンボルに束縛された構造体が格納されてて、シンボルをリスナーで呼び出せば、構造体が束縛されてるが故中身が見れるわけ。



こういう魔術染みたトリックは他のプログラミング言語では行えない。
ANSI Common Lisp最強説、の所以だ。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

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

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