現行の各種 Python への L2 Lisp の移植

2008.6.10 (鈴)

1. はじめに

前回報告した Python による L2 Lisp (Little Lazy Lisp) インタープリタを 7.2 版に改訂し, 対話セッションでの行編集を可能にするとともに, Python 2.5, 3.0 (3.0a5) に加え,Python 2.3, 2.4, 2.6 (2.6a3) でも動作するようにした。

まだ広く使われている Mac OS X 10.4.* は Python 2.3.5 を標準で用意している。 .NET Framework 上の互換実装 IronPython 1.1.1 は,一部 Python 2.5 の仕様を実現しつつ,基本的に Python 2.4 の仕様に基づいている。 ターゲットを Python 2.3 にまで広げることにより,現在使われている妥当な Python 処理系の大部分をカバーできる。

2. 行編集機能の実現

第1回の標準 Pascal による実装からこれまで Lisp の対話セッションでは単に標準入力から Lisp 式を読み取っていた。 快適なセッションのためには,入力履歴の再利用を含む行編集機能があることが望ましい。

Python では,次のように readline モジュールを import しておいてから,関数 raw_input (Python 3.0 では input) を呼び出すことによって,行編集機能付きの1行入力ができる。 引数の '> ' は行入力時に表示されるプロンプトである。

import readline

s = raw_input('> ')

標準入力オブジェクト sys.stdin から1行を読み取るかわりに,この raw_input を使えばよい。 ただし,readline モジュールはオプションであり,どこでも利用できるとは限らないから,下記のようにして import が失敗した時でもプログラムが停止しないようにする。 import しないときは,今までと同じく行編集機能はない。

try:
    import readline             # raw_input での行編集を可能にする
except ImportError:
    pass

通常の読取り用ファイルや,文字列に StringIO を適用した疑似的な読取り用ファイルと同じように raw_input を扱うため,下記のクラスを定義する。これも疑似的な読取り用ファイルのクラスである。

class InteractiveInput (object):
    "プロンプト付き標準入力 (可能ならば行編集機能付き)"
    __slots__ = '__ps1', '__ps2', '__primary'

    def __init__(self, ps1, ps2):
        "引数は1次プロンプトと2次プロンプト"
        self.__ps1 = ps1
        self.__ps2 = ps2
        self.__primary = True

    def readline(self):
        "初回は1次プロンプト,次回以降は2次プロンプトで1行を入力する"
        try:
            if self.__primary:
                prompt = self.__ps1
                self.__primary = False
            else:
                prompt = self.__ps2
            return raw_input(prompt) + '\n'
        except EOFError:
            return ''

    def reset(self):
        "プロンプトを1次に戻す"
        self.__primary = True

    def close(self):
        pass

Python 3.0 では関数名が raw_input から input に変更されている。 raw_input という名前は未使用だから,下記のようにすればよい。

raw_input = input

実際には,型オブジェクト long の有無で Python 3.0 とそれ以外を切り分ける。

try:
    _NUMS = (int, float, complex, long) # for Python 2.3-2.6
    from cStringIO import StringIO
except NameError:
    _NUMS = (int, float, complex) # for Python 3.0
    from io import StringIO
    raw_input = input

_NUMS は,Lisp 述語 numberp で数値型の判定に使うタプルである。 Python 3.0 では,無限多倍長整数と固定長整数が統合されており,long という型オブジェクトはない。 L2 Lisp 7.1 では from io import StringIO の成否によって Python 3.0 とそれ以外を切り分けていたが,その方法では Python 2.6a3 が誤判定される。

long の有無と StringIO の所在と raw_input の三つは,特定の Python のバージョンでその組み合わせになっているというだけで,必然的な関係はありませんから, 三つを別々に場合分けするか,それとも陽に sys.version_info の値で切り分けるのが妥当だったかもしれません。

3. Python 2.3, 2.4 への対応

L2 Lisp 7.1 に使われている Python 2.5 の新しい仕様は次の2項目である。

  1. 条件式 (PEP 308: Conditional Expressions)
  2. 新しいスタイルのクラスとしての例外 (PEP 352: Exceptions as New-Style Classes)

「条件式」については,Python 2.4 への対応として if 文に展開すればよい。 ただし,1行のラムダ式 lambda x: NIL if x is NIL else x.car のかわりに下記のような関数を定義する必要があるなど,実際には,この書き換えはいくらか大がかりになった。

def car(x):
    if x is NIL:
        return NIL
    else:
        return x.car

「新しいスタイルのクラスとしての例外」については,特に対応は必要ない。 評価時例外クラス EvalError とその派生クラス Thrown__slots__ 属性の定義が,Python 2.4 以前に対して無駄になるが,大勢には影響しない。 個々のインスタンスではなくクラスに対する属性だから, プログラム全体で二つだけ無駄に大域変数を定義したのとほぼ等価である。 Python 2.5 で新しく導入された BaseException は元々使っていなかった。

L2 Lisp 7.1 に使われている Python 2.4 の新しい仕様は次の1項目である。

  1. 組込みの集合オブジェクト (PEP218: Built-In Set Objects)

Python 2.3 では set は未定義だから,次のようにして代用の定義を与えることができる。

try:
    set
except NameError:
    import sets                 # for Python 2.3
    set = sets.Set

4. その他の変更

Interp#eval メソッドから Lisp 組込み関数を呼び出したとき,7.1 版では,発生した例外が EvalError ならばそのまま再送出し,それ以外ならば EvalError に変換して送出していた。 Python モジュールとして,より礼儀正しい振舞にするため, 7.2 版では,EvalError に加えて, (Control-C の打鍵などによる) KeyboardInterrupt と (sys.exit(n) などによる) SystemExit もそのまま再送出するようにした。

                            args = self.__get_args(x.cdr, fn not in self.lazy)
                            try:
                                return fn(*args)
                            except (EvalError, KeyboardInterrupt, SystemExit):
                                raise
                            except:
                                info = sys.exc_info()
                                raise EvalError('%s: %s -- %s %s' % (
                                        info[0], info[1], fn, args))
Python 2.5 以降専用になりますが,上記の except 節で KeyboardInterrupt と SystemExit を明示しないようにもできます (What's New in Python 2.5 - PEP 352)。

さらに,Lisp インタープリタの read-eval-print ループを実現する Interp#run では,KeyboardInterrupt を他の例外とは別に扱うようにした。 この二つの変更により,キーボードからの中断が確実にキーボードからの中断として扱われるようになった。 今までは,Lisp 組込み関数で発生した KeyboardInterrupt が EvalError に変換されてエラー扱いされていたか, または,それ以外で発生した KeyboardInterrupt が捕捉されずに伝播し Python 自体が終了していた。

今までは,Python など多くの対話型インタープリタと同じく,EOF の打鍵で Lisp の対話セッションを終了していた。 しかし,IronPython 1.1.1 の raw_input 関数では EOF (Windows では Control-Z) の打鍵が無視される。さしあたりの回避策として PRELUDE に下記のような Lisp 関数を定義した。

(defun exit (n)
  (python-apply (python-eval "sys.exit") (list n) nil))

対話セッションで EOF の打鍵が無視されるときでも,(exit 0) を評価すれば,Lisp インタープリタを終了コード 0 で終わらせることができる。 この関数は Lisp でスクリプトを記述するときにも有用である。

_SYMBOL_PAT の正規表現で文字列の先頭にマッチする \A と末尾にマッチする \Z が IronPython 1.1.1 で機能しなかったため,それぞれ ^$ に替えた。 現在の使い方では複数行に正規表現を適用することはないから,問題はない。

just-in-time コンパイラ Psyco (前回 付録) が利用できるときは,それを使うようにした。

if __name__ == '__main__':      # 主ルーチン
    try:
        import psyco
        psyco.full()
    except ImportError:
        pass
    interp = Interp()
    file_names = sys.argv[1:]
    if file_names:
        for name in file_names:
            if name == '-':
                interp.run()
            else:
                interp.run(open(name))
    else:
        interp.run()

PRELUDE の Lisp 関数 equal の定義を訂正し,((atom y) nil)cond 節を追加した。 この誤りは L2 Lisp 1.0 版 に紛れ込んでから今まで見過ごされていた。

(defun equal (x y)
  (cond ((atom x) (eql x y))
        ((atom y) nil)
        ((equal (car x) (car y)) (equal (cdr x) (cdr y)))))
今までの版の L2 Lisp を使うときは, equal の定義を同様にそれぞれ訂正してください。

5. Jython 2.2.1 への移植

Java VM 上の Python 言語の実装である Jython 2.2.1 は,基本的に Python 2.2 互換だが,enumerate 関数など Python 2.3 の新しい仕様のいくつかを実現している。 そのため,L2Lisp 7.2 版は,下記の2箇所の変更だけで Jython 2.2.1 でも動作する。 このバリアントを 7.2j 版とする。

$ diff -c L2Lisp.py~ L2Lisp.py
*** L2Lisp.py~  2008-06-10 16:57:20.000000000 +0900
--- L2Lisp.py   2008-06-10 17:29:26.000000000 +0900
***************
*** 92,97 ****
--- 92,99 ----
  URL: http://www.oki-osk.jp/esc/llsp/
  """
  
+ from __future__ import generators
+ 
  COPYRIGHT = """
  Copyright (c) 2007, 2008 OKI Software Co., Ltd.
  
***************
*** 816,822 ****
          return x
  
      def __python_exec(self, x):
!         exec(x, self.python_env)
          return NIL
  

      def eval(self, x, can_lose_current_env=False):
--- 818,824 ----
          return x
  
      def __python_exec(self, x):
!         exec x in self.python_env
          return NIL
  

      def eval(self, x, can_lose_current_env=False):

この exec の変更は Python 3.0a5 でエラーを引き起こす。 たとえ実行経路が __python_exec 関数を経由しなくても, 実行前のソース・ファイルから中間コードへのバイト・コンパイル時に SyntaxError が発生する。 Python 3.0 との互換性を採る限り,7.2 版と 7.2j 版を無理なく一本化することはできない。

6. おわりに

現在の妥当な Python 処理系のほとんどで L2 Lisp を動作させることに成功した。 ただし,日本語文字 (および一般に非 ASCII 文字) の扱いについては,問題がないわけではない。

例えば,Windows 上の IronPython 1.1.1 を Python & IronPython 入門 - 導入と設定 - IronPython にしたがって設定した場合,前々回 §2farmer-and-wolf-etc1.l を動かすと奇妙なエラーに悩まされる。

$ ipy L2Lisp.py farmer-and-wolf-etc1.l
Traceback (most recent call last):
  File C:\llsp\L2Lisp.py, line 1490, in Initialize
  File C:\llsp\L2Lisp.py, line 1133, in run
__main__.EvalError: *** void variable: _search
  0: (_search tree nil)
  1: (nreverse (_search tree nil))
$

その原因は,UTF-8 で書かれた

;; 解となる状態の列をどれか一つ深さ優先探索する。
(defun search (tree)
  (nreverse (_search tree nil)))

が,Shift JIS として解釈すると

;; 隗」縺ィ縺ェ繧狗憾諷九・蛻励r縺ゥ繧後°荳■縺、豺ア縺募━蜈域爾邏「縺吶k縲・(defun search (tree)
  (nreverse (_search tree nil)))

のように見えるからである。 この現象は Windows のコマンドプロンプトの TYPE や Unix の nkf -S でファイルの内容を出力して確かめることができる。 farmer-and-wolf-etc1.l の文字コードを Shift JIS にすれば,さしあたり,この問題は解決する。

無設定のままの IronPython で L2Lisp.py を実行すると "ImportError: No module named threading" というエラーになります。 IronPython は threading モジュールに必要な下請けモジュールを用意していますから, IronPython から Python 標準ライブラリを利用するように設定すれば問題ありません。
ファイルを Shift JIS として解釈するのは sys.setdefaultencoding('cp932') の設定によります。 省略した場合,上記の例では改行が食われなくなりますから正常に動作するようになりますが, 一般の用途にはおそらく不便でしょう。 できれば,いろいろ実際に試して各自の用途・用法に適した設定を決定するのが最善です。 例えば,sys.setdefaultencoding('utf-8') はその良い候補です。

他の問題については,例えば,Tiny Prolog in Python §2 のドキュメンテーション文字列の議論を参照されたい。


次回へつづく


Copyright (c) 2008 OKI Software Co., Ltd.