Jupyter の Egison 簡易カーネルを自作してみた。

## 前置き

[Egison](http://www.egison.org/ "The Egison Programming Language") の可能性を色々探っています。
取り敢えず動作確認するのに、標準のよりもう少し使い勝手の良い REPL があると良いな、と前から思ってました。

一方で、[Jupyter](http://jupyter.org/ "Jupyter Project") の可能性を色々探っています。
その中で、REPL が提供されている環境なら、それをラップしたカーネルが（Python で）比較的簡単に作れる、ということを発見。

ということで、Egison の REPL をラップした Jupyter カーネルを自作してみました。
他の言語/環境のコンソールベース REPL でも同様に構築できると思うので、何かの参考になればと、晒してみます。

あと、ちょっとだけ躓いたのでその情報共有も兼ねて、動作確認→カーネル作成の2段階に分けました。手っ取り早くカーネルの作り方だけ知りたい方は STEP.1 の途中は飛ばして [STEP.2](#step2-%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB%E4%BD%9C%E6%88%90 "STEP.2 カーネル作成") からどうぞ。

## 環境

+ Mac OSX 10.9.5
+ Python 2.7.9
+ IPython 3.1.0
+ Egison 3.5.6

※ Jupyter (IPython)・Egison のインストールは省略。[私の過去記事](http://qiita.com/antimon2/items "antimon2の投稿 - Qiita") 等を参照してください。

## STEP.1 `pexpect.replwrap` で動作確認

### Pexpect のインストール

REPL をラップしたカーネルを作るには、外部ライブラリ [Pexpect](http://pexpect.readthedocs.org/ "Pexpect documentation") の [replwrap](http://pexpect.readthedocs.org/en/latest/api/replwrap.html "replwrap - Control read-eval-print-loops &mdash; Pexpect 3.3 documentation") モジュールを利用する（と便利）。とのことで。

```bash:pexpect_install.sh
pip install pexpect
```

※ Pexpect を 利用した IPython カーネル（例：[bash_kernel](https://github.com/takluyver/bash_kernel "takluyver/bash_kernel")）が既にインストールされていれば、Pexpect もインストールされているので、この作業は不要です。

### Egison の REPL をラップして動作確認

まずは結果から。

```py:replwrap_egison_sample.py
# replwrap インポート
from pexpect import replwrap

# プロンプト作成 (※1)
import uuid
prompt = uuid.uuid4().hex + ">"

# Egison ラッパー作成 (※2)
egison = replwrap.REPLWrapper("egison --prompt " + prompt, unicode(prompt), None)

# 簡単な式で動作確認 (※3)
egison.run_command("(test (+ 1 2 3))")
# => u'6\r\n'

# 関数定義・実行
cmd = "(define $sample (lambda [$xs] (nth (pure-rand 1 (length xs)) xs)))"
egison.run_command(cmd)
egison.run_command("(sample {1 2 3 4 5 6 7 8 9 10})")
# => u'4\r\n'（実行するたびに結果は異なります）

# 複数行にわたる関数定義の場合 (※4)
define_fib_fast = """
(define $fib-fast
  (match-lambda integer
    {[,0 0]
     [(& ?(lt? $ 0) $n)
       (if (even? n)
         (neg(fib-fast (neg n)))
         (fib-fast (neg n)))]
     [$n
       (letrec {[$fib-fast-iter (lambda [$a $b $p $q $c]
          (if (lt? c 2)
            (+ (* a p) (* b q))
            (let {[$a' (if (odd? c) (+ (* a p) (* b q)) a)]
                  [$b' (if (odd? c) (+ (* a q) (* b q) (* b p)) b)]
                  [$p' (+ (* p p) (* q q))]
                  [$q' (* (+ (* 2 p) q) q)]
                  [$c' (quotient c 2)]}
              (fib-fast-iter a' b' p' q' c'))))]}
        (fib-fast-iter 0 1 0 1 n))]}))
"""
import re
define_fib_fast = re.sub(r'[\r\n]+', ' ', define_fib_fast.strip())
egison.run_command(define_fib_fast)

egison.run_command("(test (map fib-fast (between -10 10)))")
# => u'{-55 34 -21 13 -8 5 -3 2 -1 1 0 1 1 2 3 5 8 13 21 34 55}\r\n'

egison.run_command("(test (fib-fast 100))")
# => u'354224848179261915075\r\n'

# 再定義（REPL なら可能）
cmd = "(define $sample #f)"
egison.run_command(cmd)
egison.run_command("(sample {1 2 3 4 5 6 7 8 9 10})")
# => u'Expected function, but found: #f\r\n'
```

簡単に解説：

+ (※1) `replwrap.REPLWrapper` は、REPL のプロンプトを指定して、それを区切りにして出力結果を取得する仕組み。  
ところが Egison repl のようにプロンプトが単純な `'>'` だったりする場合、出力結果にそれが含まれているだけで誤動作してしまいます（例：`(test '>')`）。  
そこで、ランダムで十分な長さの文字列を前に付けたプロンプトを作成。
+ (※2) ↑で作ったプロンプト文字列を指定して egison を実行。  
なお第3引数は「プロンプトを変更するコマンド」の指定。これを指定した上で第4引数で「変更後の新しいプロンプト」を指定することもできるそうですが、Egison REPL はそういうのに対応していない代わりにコマンドライン引数でプロンプトを指定できるので、そのようにしています。  
あと、`egison` コマンドにはパスが通っている前提です。
+ (※3) `REPLWrapper.run_command()` メソッドで実行。第1引数が REPL に引き渡すコマンド、第2引数はタイムアウト秒数（省略可、省略時のデフォルトは30秒）。  
戻り値は、そのコマンドを実行してから次のプロンプトが現れるまでの出力全体（Unicode 文字列）。
+ (※4) コマンド文字列に改行文字が含まれる場合。  
REPL でも 普通のコードでも問題ないはず[^1]なんですけれど、`run_command()` に渡したらなぜかうまく動作しませんでした。  
文法的には問題は無いはず（ホワイトスペース扱い）なので、半角空白に置換してしまえばOK。  
ということで、`re.sub(r'[\r\n]+', ' ', cmd.strip())` は常套句、ですね。

[^1]: 少し前の Egison REPL は、改行するとそこまででコードを解釈して処理しようとしてた記憶があるのですが、最近のはカッコの対応が取れていなければまだコードが続いていると判定してくれるようです。

ということで、ちゃんと動くことと、動かし方のコツが分かったです。

## STEP.2 カーネル作成

### カーネル本体

[公式の開発ドキュメント](https://ipython.org/ipython-doc/dev/development/wrapperkernels.html "Making simple Python wrapper kernels &mdash; IPython 3.0.0-b1 documentation") や、[実際の実装例（bash_kernel）](https://github.com/takluyver/bash_kernel/blob/master/bash_kernel/kernel.py "bash_kernel/kernel.py at master · takluyver/bash_kernel") を参考に。

```py:egison-kernel.py
# coding: utf-8

# ===== DEFINITIONS =====

from IPython.kernel.zmq.kernelbase import Kernel
from pexpect import replwrap, EOF
from subprocess import check_output

import re
import signal
import uuid

__version__ = '0.0.1'

version_pat = re.compile(r'(\d+(\.\d+)+)')
crlf_pat = re.compile(r'[\r\n]+')

class EgisonKernel(Kernel):
    implementation = 'egison_kernel'
    implementation_version = __version__

    _language_version = None

    @property
    def language_version(self):
        if self._language_version is None:
            m = version_pat.search(check_output(['egison', '--version']).decode('utf-8'))
            self._language_version = m.group(1)
        return self._language_version


    @property
    def banner(self):
        return u'Simple Egison Kernel (Egison v%s)' % self.language_version


    language_info = {'name': 'egison',
                     'codemirror_mode': 'scheme',
                     'mimetype': 'text/plain',
                     'file_extension': '.egi'}


    def __init__(self, **kwargs):
        Kernel.__init__(self, **kwargs)
        self._start_egison()


    def _start_egison(self):
        # Signal handlers are inherited by forked processes, and we can't easily
        # reset it from the subprocess. Since kernelapp ignores SIGINT except in
        # message handlers, we need to temporarily reset the SIGINT handler here
        # so that Egison is interruptible.
        sig = signal.signal(signal.SIGINT, signal.SIG_DFL)
        prompt = uuid.uuid4().hex + ">"
        try:
            self.egisonwrapper = replwrap.REPLWrapper("egison --prompt " + prompt, 
                unicode(prompt), None)
        finally:
            signal.signal(signal.SIGINT, sig)


    def do_execute(self, code, silent, store_history=True,
                   user_expressions=None, allow_stdin=False):
        code = crlf_pat.sub(' ', code.strip())
        if not code:
            return {'status': 'ok', 'execution_count': self.execution_count,
                    'payload': [], 'user_expressions': {}}

        interrupted = False
        try:
            output = self.egisonwrapper.run_command(code, timeout=None)
        except KeyboardInterrupt:
            self.egisonwrapper.child.sendintr()
            interrupted = True
            self.egisonwrapper._expect_prompt()
            output = self.egisonwrapper.child.before
        except EOF:
            output = self.egisonwrapper.child.before + 'Restarting Egison'
            self._start_egison()

        if not silent:
            # Send standard output
            stream_content = {'name': 'stdout', 'text': output}
            self.send_response(self.iopub_socket, 'stream', stream_content)

        if interrupted:
            return {'status': 'abort', 'execution_count': self.execution_count}

        return {'status': 'ok', 'execution_count': self.execution_count,
                'payload': [], 'user_expressions': {}}


# ===== MAIN =====
if __name__ == '__main__':
    from IPython.kernel.zmq.kernelapp import IPKernelApp
    IPKernelApp.launch_instance(kernel_class=EgisonKernel)

```

簡単に解説：

+ 18行目と96行目。`IPython.kernel.zmq.kernelbase.Kernel` クラスを継承したクラスを作って、`IPKernelApp.launch_instance()` メソッドの引数にそのクラスを渡して起動する。これが（Python 実装による）IPython カーネルの基本。
+ 62行目。`Kernel` クラスで最低限継承するべきメソッドは `do_execute()` のみ。  
セルに入力された文字列（＝`code`）を受け取って、解釈・実行して、その結果を `send_response()` メソッドで送信してステータスを所定の Dictionary で返す。  
`try:〜except:〜` で `KeyboardInterrupt` とか `pexpect.EOF` とかの例外ハンドリングしている記述は、bash_kernel からそのまま拝借しましたが、Egison ではひょっとしたら `KeyboardInterrupt` は不要かも（未確認）。
+ 前後しますが、48〜59行目。`replwrap.REPLWrapper` の初期化を実施。  
こちらも bash_kernel からほぼそのまま拝借しましたが、signal の処理はひょっとしたら不要かも（未検証）。  
`prompt` の処理は [前節](#egison-%E3%81%AE-repl-%E3%82%92%E3%83%A9%E3%83%83%E3%83%97%E3%81%97%E3%81%A6%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D "STEP.1 pexpect.replwrap で動作確認 - Egison の REPL をラップして動作確認") の解説 (※1)、(※2) 参照。
+ さらに前に戻って、38行目。`codemirror_mode` は、主にセルに入力したコードのシンタックスハイライトに使用されます。でも [CodeMirror](http://codemirror.net/ "CodeMirror") に Egison モードが用意されていないので、文法や予約語が一番近い `'scheme'` に設定。

その他、bash_kernel では画像の表示に対応していたり、`do_complete()`（コード補完）メソッドもオーバーライドしてたりしますが、Egison には不要（もしくは実現困難）なので省いています。

### KernelSpec ファイル

`~/.ipython/kernels/` ディレクトリ内に、↑で作った egison-kernel.py ファイルを配置した上で、同ディレクトリ内に↓を作成：

```json:kernel.json
{
  "display_name": "Egison",
  "language": "egison", 
  "argv": [
    "python", 
    "/path/to/user_home/.ipython/kernels/egison/egison-kernel.py", 
    "-f", "{connection_file}"
  ], 
  "codemirror_mode": "scheme"
}
```

`codemirror_mode` に先ほどと同じ指定をしている以外は、まぁ見ていただければ分かると思いますので詳細略。

## 動作確認

これで Jupyter を（再）起動すれば、Egison カーネルが反映されます[^2]。

![JupyterKernels20150618.png](https://qiita-image-store.s3.amazonaws.com/0/30400/abf22ebc-c95a-e517-14dd-6fa69d524010.png)

[^2]: [前回の記事](http://qiita.com/antimon2/items/a8cd98257219773b9ef3 "Jupyter 環境設定補足 #pythontokai - Qiita")から、さらに色々 Kernel が増えてます。この記事でも触れている bash_kernel とか、まだ触れていない [IHaskell](https://github.com/gibiansky/IHaskell "gibiansky/IHaskell") とか。

実際に動かしてみたのがこちら↓

![Egison_1st.png](https://qiita-image-store.s3.amazonaws.com/0/30400/b288547e-12c1-87f0-7ef3-253c96aac212.png)

結果は↓こちらから閲覧・ダウンロードできます（nbviewer）。
[Egison_1st.ipynb](https://nbviewer.jupyter.org/gist/antimon2/498557764d0403eefc4c)

### 雑感

+ ○：最低限のシンタックスハイライトと、カッコ対応のハイライトはしてくれるので、便利。
+ ○：式ごとに結果を確認できるし、再定義も許可されているから試行錯誤できる。
+ △：コード補完は利用できない（そこまで自作するのはつらい…）。
+ △：カッコの対応が取れていない状態（閉じ括弧が足りない状態）で実行すると、BUSY のまま返ってこなくなる。  
→ 慌てず焦らず、メニューの [Kernel] → [Interrupt] を選択して中断すれば OK。
+ ×：エラーがエラーと分かりにくい（`replwrap`が通常の出力かエラーかの区別をしてくれないので仕方が無い）

ま、REPL をラップしただけのカーネルでここまでの使い勝手なら良い方ですね。
ちょっと使い方に気をつければ充分に使えるレベルです(^-^)

## 参考

+ [Making simple Python wrapper kernels](https://ipython.org/ipython-doc/dev/development/wrapperkernels.html "Making simple Python wrapper kernels &mdash; IPython 3.0.0-b1 documentation")
+ [bash_kernel](https://github.com/takluyver/bash_kernel "takluyver/bash_kernel")
+ [replwrap - Control read-eval-print-loops](http://pexpect.readthedocs.org/en/latest/api/replwrap.html "replwrap - Control read-eval-print-loops &mdash; Pexpect 3.3 documentation") in [Pexpect](http://pexpect.readthedocs.org/ "Pexpect documentation")