M.Hiroi's Home Page

xyzzy Lisp Programming

[ PrevPage | xyzzy Lisp | NextPage ]

プロセス(1)

Windows や UNIX 系の OS は、複数のプログラムを同時に実行することができます。プログラムの実行は OS によって管理されます。この処理単位をプロセス (process) といいます。通常、ひとつのプログラムがひとつのプロセスに対応します。

そして、あるプロセスから別のプログラムを起動して、新しいプロセスを作ることができます。新しいプロセスをチャイルドプロセス (child process) とか子プロセスと呼び、元のプロセスを親プロセス (parent process) と呼んで区別する場合があります。もちろん、xyzzy Lisp からもプログラムを起動することができます。

●プログラムの起動

xyzzy には、プログラムを起動する基本的な関数に call-process があります。

call-process command-line

引数 command-line には、実行するプログラム名とその引数を文字列で与えます。このほかに、次に示すようなキーワードを指定することができます。

表 1 : call-process の主なキーワード
キーワード機能
:input入力ファイル名を指定
:output出力ファイル名を指定
:directoryプログラム実行時のカレントディレクトリを指定
:show:minimize 最小化, :no-active アクティブにしない
:waitt : 外部プログラムが終了するまで待つ

たとえば、*scratch* で次のプログラムを実行すると、Windows のアプリケーション「メモ帳 (notepad) 」が起動されます。

(call-process "notepad")
(call-process "notepad" :show :minimize)

:show に :minimize を指定すると、notepad は起動されますがウィンドウは最小化の状態になります。また、:wait に t を指定すると、起動したプログラムが終了するまで待ちます。このとき call-process の返り値は、実行したプログラムが返す終了コードとなります。次の例を見てください。

(call-process "perl -e exit(0);" :show :minimize :wait t)
0
(call-process "perl -e exit(10);" :show :minimize :wait t)
10

Perl の場合、関数 exit で終了コードを指定することができます。最初の例では、exit(0) が実行され Perl の終了コードは 0 となり、call-process の返り値が 0 になります。次の例では、Perl の終了コードが 10 となり、call-process の返り値は 10 になります。

プログラムの標準出力を受け取りたい場合は、キーワード :output に出力先のファイル名を指定します。

(call-process "perl -v" :output "test.tmp" :show :minimize :wait t)

perl -v の標準出力は、ファイル test.tmp へ出力されます。同じように、:input にファイル名を指定すると、実行するプログラムの標準入力は、指定されたファイルになります。

●標準出力をバッファへ

プログラムの標準出力をバッファへ表示させたい場合は、関数 execute-shell-command を使うと簡単です。

execute-shell-command command-line &optional infile output environ directory

引数 infile, directory は call-process と同じです。environ ですが、たぶん環境変数だとは思うのですが、詳しい使い方はわかりません。output には、出力先のバッファかバッファ名を指定します。指定した名前のバッファが存在しない場合は、新しいバッファが作成されます。プログラムはシェルを経由して起動されるので、シェルの内部コマンドでも実行できます。たとえば、*scratch* 上で次のプログラムを実行します。

(execute-shell-command "dir" t (selected-buffer))

dir の結果は *scratch* へ出力されます。このとき、バッファの内容はクリアされることに注意してください。

execute-shell-command の返り値は、起動したシェルが返す終了コードとなります。実行したプログラムの終了コードではありません。ご注意くださいませ。


プロセス(2)

プロセスの例題として、Z-MUSIC のコンパイルを取り上げます。Z-MUSIC は X68000 で開発された音源ドライバで、MML (Music Macro Language) で MUSIC データを記述することが特徴です。C/C++ と同じように、エディタでソースファイル(ZMS ファイル)を記述してコンパイルします。コンパイルしたファイルを ZMD ファイルといい、これを音源ドライバに渡すことで音楽が演奏されます。

Windows の場合、Z-MUSIC 互換のコンパイラ ZMC3.EXE と、ZMD ファイルをスタンダード MIDI ファイルへ変換するコンバータ Z2M3.EXE が、やぎ。氏によって開発されています。これらのアプリケーションを使えば、Windows でも Z-MUSIC 互換 MML を使って MIDI データを作成することができます。

今回は、ZMS ファイルのコンパイルと MIDI データへの変換を xyzzy で行うプログラムを作ります。本来ならば Zmusic モードに組み込むコマンドですが、まずはお勉強ということで単独のコマンドとして実装してみましょう。

●コマンドの概要

コマンドを実行して結果をバッファに表示するのであれば、関数 execute-shell-command を使うと簡単です。ところが、今回のように複数のコマンドを実行する場合、最初のコマンドでエラーが発生した場合は、そこで作業を中断してエラーメッセージを表示するべきでしょう。ふつう、コマンドがエラー終了した場合、0 以外の終了コードを返します。execute-shell-command はシェルの終了コードを返すため、返り値でコマンドのエラーを検出することはできません。

そこで、コマンドの実行は call-process を使い、バッファへの表示処理を自前で作ることにします。まあ、execute-shell-command を参考にできるので、それほど難しいプログラムではありません。

●テンポラリファイル

コマンドの標準出力を受け取るファイルは、自分で名前を考えるよりも関数 make-temp-file-name を使って、重複しないファイル名を作成した方が簡単です。

make-temp-file-name &optional prefix suffix directory directory-p

この関数はテンポラリファイルを作成し、ファイル名をフルパスで返します。prifix に文字列を指定すると、ファイル名の先頭に指定した文字列が使われます。また、suffix に文字列を指定すると、今度はファイル名の最後尾に指定した文字列が使われます。簡単な実行例を示します。

(make-temp-file-name)
"C:/TEMP/~xyzlkje.tmp"

(make-temp-file-name "_hiro")
"C:/TEMP/_hirolkjd.tmp"

(make-temp-file-name nil "oop")
"C:/TEMP/~xyzlkjf.oop"

make-temp-file-name を実行するとファイル名を返すだけではなく、実際にファイルが作成されることに注意してください。

●ファイル名の操作

ZMC3.EXE と Z2M3.EXE を実行するときは、ZMS ファイルがあるディレクトリをカレントディレクトリとした方がいいでしょう。編集中の ZMS ファイルは関数 get-buffer-file-name で求めることができます。このとき、ファイル名をフルパスで返すので、ZMS ファイルがあるディレクトリを求めることができます。xyzzy Lisp には、ファイル名を操作する関数が用意されています。

表 2 : ファイル名を操作する主な関数
関数名機能
directory-namestring pathnamepathname のディレクトリ部分を返す
file-namestring pathnamepathname のファイル名部分を返す
merge-pathnames pathname &optional defaultsパスとファイル名を連結する
pathname-host pathnameホスト名を返す
pathname-device pathnameデバイス名を返す
pathname-directory pathnameディレクトリをリストに格納して返す
pathname-name pathnameファイルの名前を返す
pathname-type pathnameファイルの拡張子を返す

簡単な実行例を示しましょう。

(setq *test* "c:/usr/work/test.txt")
=> "c:/usr/work/test.txt"
(directory-namestring *test*)
=> "c:/usr/work/"
(file-namestring *test*)
=> "test.txt"
(pathname-device *test*)
=> "c"
(pathname-directory *test*)
=> ("usr" "work")
(pathname-name *test*)
=> "test"
(pathname-type *test*)
=> "txt"

●プログラムの作成

まずはコマンド本体から作りましょう。プログラムは次のようになります。

List 1 : コンパイル

(defun zmc ()
  (interactive)
  (let* ((zms-file (get-buffer-file-name))
         (zmd-file (substitute-string zms-file ".ZMS" ".ZMD" :case-fold t))
         (dir      (directory-namestring zms-file))
         (tmp-file (make-temp-file-name))
         (output   (find-buffer *zmusic-buffer-name*))
         result)
    (setq result (exec-zmusic-command "ZMC3" zms-file dir tmp-file))
    (when (= 0 result)
      ; 正常終了
      (setq result (exec-zmusic-command "Z2M3" zmd-file dir tmp-file)))
    (unless output
      (setq output (make-zmusic-buffer)))     ; バッファを作る
    (file-to-buffer tmp-file output)          ; 表示する
    (delete-file tmp-file)))

最初に、get-buffer-file-name で ZMS ファイルを求めます。次に、関数 substitute-string で .ZMS を .ZMD に書き換えて、コンパイル後に生成される ZMD ファイル名を作ります。キーワード :case-fold に t が指定されると、英大文字小文字を区別せずに置換します。

ディレクトリ部分は、directory-namestring で取り出して dir にセットし、出力先のファイルは make-temp-file-name で作成します。バッファはグローバル変数 *zmusic-buffer-name* に格納されている名前で作成しますが、その前に関数 find-buffer で以前に作成したバッファを探します。見つからない場合は nil を返すので、そのときは新しくバッファを作成します。

プログラムは exec-zmusic-command で実行します。この関数は、コマンドラインを生成して call-process を呼び出すだけです。ZMC3.EXE の終了コードが 0 であれば、コンパイルは正常に終了しました。次に、Z2M3.EXE を呼び出します。エラーがあれば 0 以外の終了コードを返すので、ファイル output の内容をバッファへ転送します。output が nil の場合は、make-zmusic-buffer で新しくバッファを作成します。

このプログラムでは、Z2M3.EXE の出力結果を無条件にバッファへ転送していますが、正常に終了した場合はバッファを表示しない、ということもできます。最後に delete-file でテンポラリファイル tmp-file を削除します。

次はプログラムを実行する exec-zmusic-command です。

List 2 : コマンドの実行

(defun exec-zmusic-command (command file dir tmp-file)
  (call-process (concat command " " file)
                :output tmp-file
                :exec-directory dir
                :show :minimize
                :wait t))

実行する command と引数 file を関数 concat で連結します。この関数は文字列専用です。Common Lisp には、1 次元配列、文字列、リストを列 (sequence) として操作できる列関数が用意されています。列を連結する関数 concatenate を使うと、次のようになります。

(concatenate 'string command " " file)

このほかにも、Common Lisp には便利な列関数が用意されています。詳細は Common Lisp 入門 列関数 をお読みください。

次はバッファを作成する make-zmusic-buffer を作ります。

List 3 : バッファの作成

(defun make-zmusic-buffer ()
  (let ((output (create-new-buffer *zmusic-buffer-name*)))
    (save-excursion
      (set-buffer output)
      (setq need-not-save t))   ; バッファローカルな変数をセット
    output))

バッファは create-new-buffer で作成します。このバッファはセーブする必要が無いので、バッファローカルな変数 need-not-save に t をセットします。ですが、このままでは ZMS ファイルを格納しているバッファのローカル変数を書き換えてしまいます。そこで、save-ecursion を使って現在の状態を保存し、set-buffer でカレントバッファを output に切り替えます。それから、need-not-save に t をセットしてください。

最後にファイルをバッファへ転送する file-to-buffer を作ります。

List 4 : ファイルをバッファへ

(defun file-to-buffer (file buffer)
  (erase-buffer buffer)
  (pop-to-buffer buffer t)
  (insert-file-contents file)
  (set-buffer-modified-p nil))

まずバッファの内容を erase-buffer で消去します。次に pop-to-buffer で画面を分割して buffer を表示します。このとき、カレントバッファは buffer に切り替わります。ファイルからバッファへの転送は関数 insert-file-contents を使うと簡単です。この関数は指定されたファイルの内容を、カレントバッファのポイントの後ろに挿入します。最後に set-buffer-modified-p でバッファを未変更の状態に設定します。

●とりあえず完成です

これでプログラムは一応完成しました。きちんと動作することは確認しましたが、実際に Zmusic モードに組み込むときには、ユーザーの使い勝手を考慮する必要があります。まず、実行するプログラムを固定していますが、ユーザーが設定できるようにするべきです。このとき、オプションも設定できるようにするといいでしょう。プログラム名やオプションのデフォルト値はグローバル変数に格納しておいて、ユーザーが設定する場合は .xyzzy で書き換えてもらう、という方法が簡単でよいかもしれません。また、MIDI ファイルを演奏するコマンドも作った方がいいですね。

このほかにも、いろいろな問題点があるでしょう。お気づきの点があればご指摘ください。

●追記(2000/10/04)

Zmusic モードに同等のコマンドを組み込みました。


しりとりゲーム

今回はハッシュ表を使ってしりとりゲームを作ってみましょう。プログラムを簡単にするため、入力する単語は「ひらがな」のみとします。たとえば、

りんご  ごりら  らじお  ・・・・

というように、ひらがなで単語を入力します。長音記号「ー」も使用不可としますので、たとえばルビーは「るびぃ」と入力してください。今回は *scratch* 上で実行するプログラムを作ります。

●単語の管理

まず最初に、ゲームで使用する単語の管理から考えましょう。コンピュータが単語を考えるのは難しいので、使用する単語はあらかじめファイル word.dat に格納しておきます。ゲームを始めるときに、このファイルから単語を読み込み、ハッシュ表 *shiri-word* にセットすることにします。ハッシュ表の使い方は Common Lisp 入門 ハッシュ表 をお読みください。

しりとりゲームは同じ単語を二度使うことができないので、未使用の単語はハッシュ表の値を t とし、使用済みの単語はハッシュ表の値にシンボル used をセットすることにしましょう。

コンピュータが単語を返すときは、私達が入力した単語の最後の文字を取り出し、その文字で始まる単語を *shiri-word* の中から探します。この処理はハッシュ表に登録されているキーを調べなければいけません。ハッシュ表のキーを毎回調べるのは大変なので、先頭文字ごとに単語を区分けしておくことにしましょう。この処理にハッシュ表 *first-code* を使います。

ハッシュ表 *first-code* は文字をキーとして使います。たとえば、「あ」で始まる単語が、あさがお、あじさい、あり、だとしましょう。すると、*first-code* の #\あ の値は、これらの単語を格納したリストがセットされます。

(gethash #\あ *first-code*)
=> ("あさがお" "あじさい" "あり")

*first-code* で単語見つからなければ、コンピュータの負けとなります。

それでは、ファイルからデータを読み込むプログラムを作ります。

List 5 : データファイルリード

(defun read-word-data ()
  ; ハッシュ表セット
  (setq *shiri-word* (make-hash-table :test #'equal)
        *first-code* (make-hash-table))
  ; ファイルリード
  (with-open-file (in "./site-lisp/word.dat" :direction :input)
    (let (word)
      (while (setq word (read-line in nil))
        (unless (check-word-p word)
          (setf (gethash word *shiri-word*) t))))))

ハッシュ表を格納する変数 *shiri-word* と *first-code* はスペシャル変数として定義します。ハッシュ表 *first-code* のキーは文字なので、:test はデフォルトの eql のままでかまいません。

ファイルの読み込みですが、プログラムとデータファイル word.dat はディレクトリ site-lisp に格納しておきます。あとは read-line で 1 行ずつ読み込み、check-word-p で単語が「ひらがな」であることを確認してからハッシュ表にセットします。check-word-p は string-match を使えば簡単です。

List 6 : 単語のチェック

(defun check-word-p (word)
  (string-match "[^ぁ-ん]" word))

文字コードは shift-jis を前提としています。ほかの文字コードでは動作しないかもしれません。M.Hiroi はほかのコードでの動作確認はしていないので、ご注意くださいませ。

*first-code* は単語を検索するときにデータをセットします。単語の検索は次のようになります。

List 7 : 単語の検索

(defun search-word (word)
  (let* ((code (get-last-code word))
         (word-list (gethash code *first-code* t)) word)
    (if (eq word-list t)
      (setq word-list (get-word-data code)))
    (while word-list
      (setq word (pop word-list))
      (unless (eq (gethash word *shiri-word*) 'used)
        (return word)))))

get-last-code は最後尾の文字を取り出す関数です。これはあとで説明します。そして、*first-code* から code で始まる単語を取り出します。まだデータがセットされていなければ t を返すこととし、その場合は get-word-data で *shiri-word* から code で始まる単語をすべて求めます。あとは、リストの中から未使用の単語を探します。このとき、使用済みの単語はリストから削除します。

*shiri-word* から単語を取得する処理は maphash を使えば簡単です。

List 8 : 先頭文字 code で始まる単語を取得

(defun get-word-data (code)
  (let (word-list)
    (maphash #'(lambda (key value)
                 (if (char= code (elt key 0))
                     (push key word-list)))
             *shiri-word*)
    word-list))

列関数 elt で先頭文字を取り出し、code と等しければ word-list にセットします。文字の比較関数には char=, char/=, char<, char>, char<=, char>= があります。xyzzy Lisp は日本語も 1 文字として扱えるので、とても便利です。ラムダ式で word-list にデータをセットできるのは、クロージャが働いているからです。ご注意ください。

●単語の入力

次は、私たちが単語を入力する処理を考えましょう。単語の入力にはミニバッファを使います。あとは、入力された単語が「しりとり」の規則に違反していないかチェックします。単語の入力は関数 input-word で行います。

List 9 : 単語の入力

(defun input-word ()
  (loop
    (let ((word (read-string "単語を入力してね > ")))
      (if (check-word-p word)
          (princ "単語は「ひらがな」でお願いね!\n")
          (return word)))))

これは簡単ですね。check-word-p で入力されたデータをチェックをするだけです。次に、規則のチェックを行う関数 rule-check を作ります。

List 10 : 規則のチェック

(defun rule-check (my-word your-word)
  (cond ((char= #\ん (get-last-code your-word))
         (princ "「ん」で終わったのであなたの負けよ!"))
        ((and (stringp my-word)
              (char/= (get-last-code my-word)
                      (elt your-word 0)))
         (princ "最初の文字が違うのであなたの負けよ!"))
        ((eq (gethash your-word *shiri-word*) 'used)
         (princ "その単語は前に使ったのであなたの負けよ!"))
        (t nil)))

引数 my-word がコンピュータ側の単語で、your-word が私たちが入力した単語です。最後の文字が「ん」で終わるチェックは簡単ですね。次に、入力した単語がきちんと「しりとり」になっているかチェックします。このゲームでは、私たちから単語を入力するので、最初は my-word に単語がセットされていません。その場合、my-word に nil が渡されるので、型述語 stringp で文字列がセットされていることを確認します。最後に、使用済みの単語であるかチェックします。これはハッシュ表 *shiri-word* を調べるだけです。

では、単語の最後の文字を取り出す処理を作りましょう。

List 11 : 最後の文字を取り出す

(defun get-last-code (word)
  (change-code (elt word (1- (length word)))))

最後の文字を取り出すことは簡単です。しかし、単語が「ぁぃぅぇぉっゃゅょゎ」などで終わっているときが問題です。これらの文字から始まる単語はないので、この場合は「あいうえおつやゆよわ」に変換することにします。この処理を関数 change-code で行います。

List 12 : 連想リストを使って文字を変換

(defun change-code (code)
  (let ((a-list '((#\ぁ . #\あ) (#\ぃ . #\い) (#\ぅ . #\う)
                  (#\ぇ . #\え) (#\ぉ . #\お) (#\っ . #\つ)
                  (#\ゃ . #\や) (#\ゅ . #\ゆ) (#\ょ . #\よ)
                  (#\ゎ . #\わ)))
         data)
    (if (setq data (assoc code a-list))
        (cdr data)
        code)))

いろいろな方法が考えられますが、Lisp らしく「連想リスト (association list) 」を使ってみました。連想リストの説明は Common Lisp 入門 リストの操作(その2) をお読みください。

●ゲーム本体の作成

では、最後にゲーム本体を作りましょう。

List 13 : しりとりゲーム本体

(defun exec-shiri ()
  (let (your-word my-word)
    (loop
      (setq your-word (input-word))
      (princ your-word)
      (terpri)
      (if (rule-check my-word your-word)
        (return))
      (update-word your-word)
      (princ "うみゅみゅ・・・")
      (unless (setq my-word (search-word your-word))
        (princ "私の負けです")
        (return))
      (format t "「~A」!~%" my-word)
      (goto-char (point-max))
      (update-word my-word))))

私たちが入力した単語は、ファイル word.dat に登録されていない単語もあるでしょう。関数 update-word で単語を使用済みにするとき、それらの単語はハッシュ表 *shiri-word* に登録されます。ゲームが終了したら、ハッシュ表に登録された単語を word.dat に出力することで、私たちが入力した単語を覚えることがでます。つまり、だんだんと強くなるわけですね。むちゃくちゃな単語でも入力することはできますが、次のゲームではその単語を逆に使われることになります。ご自重くださいませ。

update-word はとても簡単です。

List 14 : 単語を使用済みとする

(defun update-word (word)
  (setf (gethash word *shiri-word*) 'used))

シンボル used をセットするだけです。これで新しい単語も登録されます。exec-shiri を呼び出すメインルーチンは、次のようになります。

List 15 : メインルーチン

(defun shiri ()
  (if *shiri-word*
    (change-word-data)
    (read-word-data))
  (exec-shiri)
  (save-word-data))

グローバル変数 *shiri-word* が nil でなければ二回目以降のゲームと判断し、change-word-data を呼び出して使用済みの単語を未使用に戻します。そうでなければ、read-word-data を呼び出してハッシュ表のセットとデータの読み込みを行います。change-word-data は次のようになります。

List 16 : 使用済みの単語を未使用に戻す

(defun change-word-data ()
  (clrhash *first-code*)
  (maphash #'(lambda (key value)
               (if (eq value 'used)
                   (setf (gethash key *shiri-word*) t)))
           *shiri-word*))

まず、ハッシュ表 *first-code* をクリアします。次に、maphash を使って *shiri-word* に登録されているキーで、値が used のものを t に変更します。データのセーブも簡単です。

List 17 : データのセーブ

(defun save-word-data ()
  (with-open-file (out "./site-lisp/word.dat" :direction :output)
    (maphash #'(lambda (key value) (format out "~A~%" key))
             *shiri-word*)))

これも maphash を使って、キーをファイル word.dat に書き込むだけです。これでプログラムは完成です。

●ダウンロード

まあ、とりえのない「しりとりゲーム」ですが、適当な単語を入れた word.dat を用意したので、暇つぶしに遊んでみたり、改造したりしてみてください。圧縮ファイル内の shiri.l と word.dat を site-lisp に展開してください。あとは M-x load-file shiri.l でファイルをロードし、*scratch* で (shiri) を実行するとゲームが始まります。word.dat の単語は少ないので、最初は簡単に勝てるでしょう。

ファイル名
shiri.l : プログラムファイル
word.dat : 単語ファイル
ダウンロード(1,988 byte)

Copyright (C) 2000-2003 Makoto Hiroi
All rights reserved.

[ PrevPage | xyzzy Lisp | NextPage ]