見出し画像

Retro-gaming and so on

Lispのコードを書いてみようか?

うーん、どうもHTMLタグが自由に使えんし、gooブログって結構ダメなブログだなぁ。JavaScriptも貼り付けられない。って事はプログラミングの話題はコードが貼り付けられない以上、出来ない、って事かぁ。GoogleのBloggerとかに比べると全然ダメなブログです。
と言うわけで、このブログだとプログラミング関係の最後の話題になるかもしれない。ならんかもしれんけどよ(笑)。

最近、ちとLispの話題振ってたんで、Lispのコードをちょっと見せてみようか、って気になった。題材にはC言語でカレンダーを表示するを扱ってみようと思う。
もちろん、題材はこれなんだけど、コードをそのままLispに翻訳しよう、とは思わない。そうすればとてもじゃないけどLisp「らしい」プログラムにはならない。あくまでLisp「らしい」プログラムとして書いてみようと思う。
そしてそれを行うと、「Lisp最強説」はあるんだけど、それとは違って、Lispが得意な事、不得手な事ってのが見えてくるんじゃないか。万能なプログラミング言語は存在しないし、Lispにも不得手な分野はある。配列とかね(笑)。

さて、Lispとは言っても実はたくさん存在している。そして誤解を畏れずに言うと、「Lispには標準が存在しない」。いや、本当はISOで採択されて、JISでも定義されているISLISPと言うのが存在してる。これがLispの「国際標準」だ。ただし、残念ながら殆ど実装が存在しないのだ。沖電気工業の方でOK! ISLISPと言う実装が公開されているけど、どんなモンなのかは知らない。とにかく、フリーで入手出来て気軽に使える処理系の存在を知らない。と言うわけで、「国際標準仕様だけは存在してるけど、実装がほぼない」プログラミング言語はさすがに誰も使う気が起きないだろうし、要するに事実上標準には成りきれていない、と言う事だ。
Common Lispが〜、と言う人もいるけど、あれはANSIと言うアメリカローカルでの規格なので、世界的に見ると、それは結局デファクトスタンダードだ、ってだけの話。ISOでキチンと定義されているC/C++とは格が違うのだ。
同様に、Schemeもアメリカ国内ではIEEE(米国電気電子学会)の方でスタンダードがあるのみ、である。こう書くと「RnRSが〜」とか言うヤツが出てくるんだけど、今はオーソリティの話をしている。通常言われる「Schemeの規格書」と言うのは要するに権威は無いのだ。権威がない、と言う事は強制力もあまりない。だからSchemeの実装と言うのは実装者の方針により結構バラけている。
そんな中で「一番導入がカンタンだ」って理由だけで一応ここではRacketを使う。RacketもLisp方言、もっと言うとScheme方言である。IDEも完備してるし、オールインワンなんで、昔のBASICのように手間無く遊べる。と言うわけでRacketを使っていこうと思う。


ところで、Lispと言うのはList Processor(リスト処理器)の略だと言われる。だからここですぐリストを使う、って思うだろうけど止めておく(笑)。その代わり、ストリームと呼ばれる機能をまずは使おうと思う。ストリームはリストの一種なんだけど、言わば無限長のリストだ。末端が存在しない。
そんなモノが存在するのか?って思うかもしれないけど、ある。今流行りの「遅延評価」で作られたリストがストリームである。
Racket、及びSchemeでストリームを使用する場合、SRFI-41と言うライブラリを使う。SRFI(サーフィ、と読む)と言うのは一応、ライブラリ、と一般には捉えられてるけど、実際は違う。Scheme Request For Implementationの略で、読んだ通り「実装要求」である。つまり「こんな機能があればいいな」集である。しかもこいつは独立した外部ライブラリとして存在しているわけでもない。つまり、各Scheme実装者が好みで選択して自らの実装に取り入れる性質のモノで、実装Aでは実装されていても実装Bでは実装されてるとは限らない、と言う仲々厄介な性質を持ったブツである。従って、SchemeでSRFIを利用してポータブルなコードを書くのは至難の業になるのだ。
話を元に戻す。RacketでSRFI-41を使用する場合、

(require srfi/41)

とファイルの先頭に記述しておく。これでストリームを使い放題、となる。 
まずは次のようなコードを書く。


実はこの時点で、実装の60%以上は終了している。
Schemeの細かい文法の説明は省くが、Lispが超強力だ、と言われる一旦がこのコードには現れている。遅延評価は置いておいて、「条件分岐」が通常のプログラミング用語で言うトコの「文」ではない、ってのがその強力さを支えているのだ。
Lispには「文」が存在しない。従って「地の文」が無い。あるのは常に式のみ。従って、カレンダー用のデータを生成する際に、データ内部に条件分岐(この例だとうるう年の生成)をぶち込める。こんな事は普通のプログラミング言語には出来ない。C言語で配列内部にif文をぶち込めないのは良く知られているだろう。その比較だけでもLispの「柔軟性」と言われるモノが良く分かると思う。
なお、データ名に付いてるアスタリスク(*)は耳あて、と呼ばれていて、Lispで大域変数に命名する際の、ある種緩い命名規約になっている。文法規則ではない。
さて、任意の年を指定すれば*calender-base*からその年の月のリストを取り出す事が出来る。ストリームへのアクセッサには取り敢えずstream-refを使う。

> (stream-ref *calender-base* 2020)
'(31 29 31 30 31 30 31 31 30 31 30 31)
>

2020年はうるう年なので、2月は29日ある、とルール通りのリストを返してくれる。そして*calender-base*は無限長リスト、つまりストリームなので、西暦1万年だろうと西暦10万年だろうと「その中にある」。まあ、後方に行けば行くほど、取り出しにかかる時間は多くなるが、どっちにせよ、「永遠に続く西暦」を取り敢えず入手出来たわけだ。万歳!

さて。Lispの凄さを体験してもらって、次はLispが実は苦手な部分に手を付けようと思う。実はLispは文字列処理が苦手である。これはLispが地の文が無い、と言うのとちょっと関係がある。
プログラミング用語で、地の文を構成する単語や要素をリテラル、等と呼んだりするが、結果Lispにはリテラルが無い。Lispにある「リテラルのように見えるモノ」はシンボルと呼ぶ。そして、Lispのシンボルと言うのは実はれっきとしたデータ型だ。通常のプログラミング言語にはシンボルにあたるデータ型は存在しない。
このシンボルがあまりにも便利なせいで、長い間Lispには文字列が存在しなかった。つまり、変数の代入先にも使えるし、関数名にするために使えるし、あるいはプリントされる対象としても使える。かなり大雑把な使い方をされるのがシンボルだ。最近では「大雑把」をジェネリック、って言ったりするけどね(笑)。
それはさておき。大昔のLispだと、プログラマは次のようなシンボルで構成されたリストをそのまま表示するのを好んだ。

'(This is an old example of Lisp's printing.)

そして、これはリストなので、リスト操作を主機能としてるLispには「イジり甲斐がある」出力なわけだ。これが初期のAIでLispが好まれた1つの大きな理由である。シンボルのリストを自在に操作すれば、「コンピュータが話してるような」プログラムを作る事はワケがない。
一方、それもあって、Lispに「フツーのプログラミング言語が持ってる」文字列と言う「かなり不自由な」機能が入る事が遅れた。殆どLispの中では異端の機能である。ある種のLispでは文字列を自由に扱える機能を盛り込んだ。結果として文字列操作部分はLispからかなり浮いていて、「そこだけ別言語」みたいになってたりする。Lispにとって文字列はかなり悩ましい部分なのだ。
と言う辺りで、まずは「目的の年と月の」カレンダーを文字列を返す関数として実装してみようと思う。その前にまたSRFIの力を借りようと思う。リスト操作関数てんこ盛りのSRFI-1、文字列操作関数てんこ盛りのSRFI-13を追加で使用しようと思う。また、Lispの基礎に戻って、リスト操作を主として文字列を生成してみよう。


*calender-base*に比べるとmessyである。出力要素をリストとして操作して、最後に文字列に変換する、と言うある意味Lispらしいコードにはなってる筈なんだが。そしてこの時点で改行文字(Lispでは"~%")も埋め込んでいる。そのために条件分岐が1つ増えてるんだな。後の操作をシンプルにするため、しょうがねぇだろ。
C言語のバージョンだと、改行しつつ出力、って事をやってるわけだが、どっちが手間なのか、ってのは人に依ると思う。ただし、C言語だと「文字列を自由に生成する」にはパワー不足だろう。Lispは不得手なりに「目的とする文字列の全体像」を、リスト処理を経由して生成する事が可能だ。
全体的には再帰処理をして、目的とする(つまりカレンダーとしての)文字列を一気に生成してる。再帰はLispの反復処理のキモで、言い換えるとモダンな言語でお馴染みのfor文なんかは存在しない。一方、古い言語によくある繰り返しのdoは存在してる。が、Lisp好きは再帰処理も好きなので、再帰を苦にしない。基本的には、ね。
あと、mapとfoldと言う関数が多用されている。この辺はLispのイテレータだ。構文ではない。機能はネットで調べてみてもらいたいんだが、この2つはリスト相手の処理に重宝するイテレータで、愛用されている。再帰よりも下手すればこっちのお世話になる事の方が多いかもしれない。そしてC++のテンプレートなんかに導入されてるのは、このイテレータの「貧弱版」である。
さて、make-calenderを実行してみよう。

> (make-calender 2020 11)
"          1~% 2 3 4 5 6 7 8~% 9 10 11 12 13 14 15~% 16 17 18 19 20 21 22~% 23 24 25 26 27 28 29~% 30"
>

なんかヘナチョコに見えるかもしれないが、C言語版で苦労してる「ズレ」もここで既に解決してるし、7日毎の後はキチンと改行文字(~%)が入ってる。
もうちょっとキチンと整形して「実験結果」を見たい場合は、SRFI-48を利用する。どのみちこの後も使うのでrequireしておこう。Lispでの整形出力用関数はformatと言う。これがC言語で言うprintfで、SRFI-48で定義されている機能だ。


ここまでくればあとは完成だ。(そう名付ける必要はないけど)main関数を組み上げる。また、入力を促すメッセージ等は、本体から分離しておこう。結果、コード全体は次のようになる。


これで完成、だ。入力に対するエラーチェックはやってないが、オリジナルのCコード版の要求した仕様にはなっている。結果は同じだけど、実装方針は随分と違った筈だ。駆け足だったが、Lispの柔軟性の一片でも感じて貰えれば幸いである。



なお、Racketは、ソースコードをスタンドアロンな実行形式にコンパイルしてくれる機能もあるんで、「あまり実用的じゃない」Lispとしては割に実用的な範疇の処理系ではある。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

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

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