Land of Lisp勉強ノート#6

定番のLISP本 オレイリー LISP
定番のLISP本 オレイリー

ユーザインタフェースをこれから学ぶ。取り敢えずコマンドライン・インタフェースからだ。
勿論、Webインタフェースもグラフィカルインタフェースも対応するライブラリは存在している。
第6.6章で学習目標をチェックしておこう。

第6章 世界とのインターフェイス:Lispでのデータの読み書き

外の世界とやり取りするためのユーザーインタフェースが必要になる。
まずはコマンドラインインタフェースからだ。

6.1 テキストの表示と読み込み

printとreadについて。このコマンドたちの間のタクサンの対称性に注意。

スクリーンへの表示

この場合は、REPLの画面と考えてよい。

> (print "foo")
"foo"        ;;こちらはprintの出力
"foo"        ;;こちらはREPLの式の評価結果(こちらは省略していこう)
> (progn (print "this")     ;;printは値を表示した後に空白1個を入れて改行する
         (print "is")
         (print "a")
         (print "test"))
"this"
"is"
"s"
"test"

> (progn (prin1 "this")    ;;prin1はただ表示するだけ 余計なことはしない
         (prin1 "is")      ;;こちらを使う場面も多いのだ
         (prin1 "a")
         (prin1 "test"))
"this""is""s""test"

ユーザに挨拶しよう

> (defun say-hello ()                 ;;関数定義 引数なし
    (print "Please type your name:")  ;;"名前を入れてください"
    (let ((name (read)))              ;;名前をnameへ読み込む
       (print "Nice to meet you, ")   ;;"はじめまして"
       (print name)))                 ;;name

SAI-HELLO
> (say-hello)
"Please type your name:" "bob"        ;;名前を"で囲って入れる

"Nice to meet you, "
"bob"

何が面白くないと言って、入力も出力も”で囲まれているところが。

printとreadから始める

基本はprintとreadだ。まずはこれらを使えないか考えよう。

> (defun add-five ()
    (print "Please enter a number:")
    (let ((num (read)))
      (print "when I add five I get")
      (print (+ num 5))))
ADD-FIVE
> (add-five)
"please enter a number:" 4

"when I add five I get"
9
(print '3)            ;;3 整数
(print '3.4)          ;;3.4 浮動小数点数
(print 'foo)          ;;FOO シンボル シンボルだけはクオートを省略できない
(print '"foo")        ;;"foo" 文字列
(print '#\a)          ;;#\a 文字

最後の例の文字について。文字の前に’#を置けばよい。そして特別なものがこれ。

  • #\newline
  • #\tab
  • #\spave

シンボルで大文字と小文字を区別する方法がある。
シンボル名をパイプ文字|で囲めばよい。
空白があっても良い。
eg, |CaseSensitiveSymbol| |even this is a legal Lisp symbol!|

人に優しいデータの読み書き

Lispには人に優しいコマンドもあるのだ。

ヒューマンインタフェース
ヒューマンインタフェース

princ関数はLispのあらゆるデータ型をとり、できるだけ人がそれを理解しやすいように表示する。

  • 文字列を”で囲まない
  • 見時は生の形で(#\を付けずに)表示する
(print '3)            ;;3 整数
(print '3.4)          ;;3.4 浮動小数点数
(print 'foo)          ;;FOO シンボル シンボルだけはクオートを省略できない
(print '"foo")        ;;foo 文字列
(print '#\a)          ;;a 文字

> (progn (princ "This sentence will be interrupted")
         (princ #\newline)
         (princ "by an annoying newline character."))
This sentence will be interrupted
by an annoying newline character.

printは出力したものをあとから「読み直し」することができる形で出力する。

princは人が読みやすいような形で出力するが「読み直し」はできない。

read-line関数もあり、enterキーが押されるまで一つの文字列とみなすが、賢くない。

> (defun say-hello ()
    (princ ""Please type your name:)
    (let (name (read-line)))
        (princ "Nice to meet you,")
        (princ name))
SAY-HELLO
> (say-hello)
Please type your name: Bob O'Malley
Nice to meet you, Bob O'Malley

どうだろう。少しはマシかな。

6.2 Lispにおけるコードとデータの対称性

Lispの対称性とは、画会から生の文字列を受け取ってLispの内部データに変換したり、その逆を行うこと。

更に深いことには、Lispはプログラムコードとデータを同じように扱えるのだ。こういう言語は同図象性(homoiconic)持つと言われる。

例えばコードモードとデータモードの件だ。あの例ではクオート文字で二つのモードを行き来した。

> '(+ 1 2)    ;;データモード
(+ 1 2)
> (+ 1 2)     ;;コードモード
3

更に準クオートを使ってdescribe-paths関数を定義したのだった。

しかし、更に進めよう。もし任意のLispコードをゼロから組み立て、それを実行できたらどうなるだろう?

> (defparameter *foo* '(+ 1 2))
*FOO*
> (eval *foo*)      ;;*foo*に格納されている文字列をコードとして評価している
3

しかし、eval関数の使いどころには十分注意が必要だ。他の例えばマクロなどで代用できるかを十分に検討することだ。この辺に関連するのは、クオート、準クオート、evalコマンド、マクロだ。

Lispは強力だ!

6.3 ゲームエンジンに専用のインタフェースを追加する

テキストゲームにより適した専用REPLを作り始める。つまりREPLの上にゲームのステージを作るんだ。

専用のREPLの準備

下記のように簡単に作れる。

> (defun game-repl ()
    (loop (print (eval (read)))))
GAME-REPL
> (game-repl)
(look)

(YOU ARE IN THE LIVING-ROOM. A WIZARD IS....
FROM HERE. THERE IS A LADDER GIONG UPSTAIRS ...... ON THE FLOOR.)

この無限ループから抜け出るには、Ctrl-C、:aをタイプする。
そしてもう少しの進化。quitをタイプすればゲームが終わるのだ。
足りない定義はこれから。ソースはこれ。

(defun game-repl ()
  (let ((cmd (game-repl)))
    (unless (eq (car cmd) 'quit)     ;;quitでなければ回り続ける
        (game-print (game-eval cmd))
        (game-repl))))

専用のread関数を書く

game-read関数は標準のread関数の下記の二つの不都合を直したものだ。

  • 括弧無しでコマンドを入力したいので、read-lineで入力した文字列に括弧を付けてやる。
  • クオート文字も付けてやることにする。
(defun game-read ()
  (let ((cmd (read-from-string      ;;cmdに括弧で囲われた文字列が入る
               (concatenate 'string "(" (read-line) ")"))))
     flet ((quote-it (x)            ;;'はquote関数
             (list 'quote x)))
       (cons (car cmd) (mapcar #'quote-it (cdr cmd)))))

> (game-read)
walk east
(WALK 'EAST)

プレーヤーが(walkと入力すると、誤動作を起こすのは目に見えるが、例外処理の定義も行えるんだ。
13章を待て。

game-eval関数を書く

evalコマンドはどんなものでも受け付けてしまうことだ。これを規制してプログラムを守ろう。
game-evalコマンドは予め決められたコマンドだけを受け付けるようにする。

(defparameter *allowed-commands* '(look walk pickup inventory))   ;;コマンド群

(defun game-eval (sexp)
  (if (member (car sexp) *allowd-commands*)  ;;引数のコマンドが既定義のものなら
       (eval sexp)                             ;;true それを評価せよ
       '(ido not know that command.)))         ;;false エラーメッセージを吐く

game-print関数を書く

Lispの生のREPLで不便なのはテキストが全て大文字で帰ってくることだ。
これを解決する。
この手法は例えばHTMLを生成する際に役立つことだろう。
テキストゲームでも文字に彩色したいときもあるだろう。
これはじっくり鑑賞しよう。

(defun tweak-text (lst caps lit)  ;;lstの内容を1文字ずつ変換する
  (when lst
    (let ((item (car lst))     ;;1文字齧る
          (rest (cdr lst)))    ;;残りを取っておく
      (cond ((eql item #\space) (cons item(tweak-text rest caps lit)))
            ((member item '(#\! #\? #\.)) (cons item (tweak-text rest t lit)))
            (eql item #\") (tweak-text rest caps (not lit)))
            (lit (cons item (tweak-text rest nil lit)))
            (caps (cons (char-upcase item) (tweak-text rest nil lit)))
            (t (cons (char-down-case item) (tweak-text rest nil nil))))))

(defun game-print (lst)
  (princ (coerce (tweak-text (coerce (string-trim "() "
                                                       (prin1-to-string lst))
                                      'list)
                               t
                               nil)
                  'string))
  (fresh-line))

実行例はこちらに。

(game-print '(YHIS IS A SENTENCE. WHAT ABOUT THIS? PROBABLY.))
This is a sentence. What about this? Probably.

(game-print '(not only deoes this sentence have a "comma," it also mentions the "iPad."))
Not only does this sentence have a comma, it also mentions the iPad.

このgame-print関数には欠陥もある。\”があるとうまく動作しない。また、英語以外にはうまく動作しないだろう。日本語だとどうなるのか調べないといけないな。

そもそもCommon Lispで日本語は通るんだろうか。
日本語が通ればこんな心配はいらなくなるかもしれないのに。

6.4 さあこの素敵なゲームインタフェースを試してみよう

だいたい部品は揃ったが、17章ではさらにコマンドを追加して行く。写真で結果を示す。どうさとうか、REPL臭は抜けているかな。

実行例
実行例

6.5 readとevalの危険について

readとeval関数には危険性が存在する。悪意あるコマンドを送り込まれる危険があるのだ。
game-eval関数ではこれを定義したコマンド以外は受け付けないようにしている。

しかし、実務でこれらのコマンドは避けるのが懸命だろうと著者は言っている。

リーダマクロに依る攻撃については、変数*read-eval*をnilに設定しておけば無効にできる。”#.”を読んだ時点でLispはエラーを報告するからだ。
要検討事項である。

6.6 本章で学んだこと

  • printとread関数によって、コンソールを通してユーザと直接コミュニケートすることができる。この2つの関数は、「コンピュータに優しい」形式を扱う。
  • 他の入力関数はreadとprintほどエレガントではないけれど、より人間に優しい形式を扱うことができる。printcやread-lineのような関数だ。
  • 同図象性を持つ言語は、プログラムコードとデータをほぼ同じ形式で持つ。Lispのクオート、準クオート、そしてevalとマクロ機能は、Lispの同図象性を強化している。
  • 自分でカスタマイズされたREPLを書くのは簡単だ。
  • Lisp内部のデータ表現を、プログラムのインタフェースにふさわしい形式に変形するのは難しくない。これを知っていれば、プログラム内部で使うデータ構造と、その表示形式とを分けて考えることができる。

第6.5章 Lambda:とても大事な関数なので特別に章を分けて説明しよう

Lispのlambdaコマンドの重要性はいくら強調してもし足りない。実のところ、Lispがそもそも生まれたのはこのコマンドのためなんだ。名前をつけないで関数を実行できるんだ。なんのこっちゃ?

6.5.1 lambdaがすること

lambdaを使えば名前を与えずに関数を作ることができる。つまり動作の定義だけで良くなるんだ。

(defun half (n)     ;;これは数値を半分にするhalf関数
  (/ n 2))

> #'half            ;;functionオペレータの例
#<FUNCTION HALF ...>

> (lambda (n) (/ n 2))   ;;functionオペレータの例
#<FUNCTION :LAMBDA ...>

> (mapcar (lambda (n) (/ n 2)) '(2 4 6))  ;;無名の関数によって半分に計算される
(1 2 3)

lambdaの引数は評価されずにlambdaに渡される。lambdaは本来の関数ではないのだ。
これはマクロと呼ばれているものだ。マクロの詳細は16章にて。
lambdaが返す値は通常のLisp関数だ。ややこしい。
この例では受け取った数値を半分にする関数なんだ。

経験的にマクロ命令とは
スケルトンに対して引数の形で文字列を与える形式。
aaamac(par1=”./” par2=”prt01″ par3=”sample.txt”)
と言うコードが、
現ディレクトリのsample.txtファイルをプリンターprt01へ印刷するコードを展開されるようなものだ。
この作法をLispは日常的に利用しているということ。

他の多くの言語では関数と値の世界をできるだけ分けようとしている。
でもLispでは必要なときにこの2つの世界をつなぐことができる。
例えば他の関数に「その場限りの処理」を渡したいと思ったときに、lambdaはぴったりだ。

Lispにおいてlambdaは多用されている。

6.5.2 lambdaがそんなに大事なわけ

関数を普通のデータのように受け渡しできると言う機能はものすごく便利だ。1

関数を値として渡すことを多用するプログラミングスタイルは高階プログラミングと呼ばれる。詳細は14章だ。

純粋に数学的な意味では、lambdaが唯一のLispコマンドだと言えるのだ。

Lispはラムダ算法と言う数学的な概念から直接導かれたプログラミング言語である。ラムダ算法とはlambdaを唯一のコマンドとする論理的なプログラミング言語のようなものだ。lambda1つだけをコマンドとして特別なコード変換を行うことで、完全なプログラミング言語を作ることができる(実用的ではないだろうが)。

lambdaの基礎を理解したらいよいよlambdaが作る無名関数では書くのが難しいようなプログラミングに取り組んでゆくことになる。

6.5.3 本章で学んだこと

  • lambdaを使って、名前を与えることなしに関数を作れる。
  • Lispの多くの関数は、関数を引数として受け取れる。これらの関数を使うのは、高階プログラミングと言われる。
  1. この辺のツボが身につけばPHP風に動作できるか? ↩︎

コメント

タイトルとURLをコピーしました