HaskellのJupyter用IHaskellは高機能だけどインストールが面倒なので超簡単なGHCi用のkernelを作ろう

# 概要

Haskellの勉強用にIHaskellを検討したがなんだか面倒そうなので、超簡単なGHCi用のkernelを自分で作ってHaskellの勉強をしようと考えた。
簡単なkernelなので機能は限定されるかもしれませんし、まだHaskellの勉強を始めたばかりなのでどんな機能が必要かもわかっていませんが作ってみました。

# 実行環境

```terminal
> sw_vers
ProductName:		macOS
ProductVersion:		15.4.1
BuildVersion:		24E263

> ghci --version
The Glorious Glasgow Haskell Compilation System, version 9.12.2

> python --version
Python 3.12.7

> jupyter --version
Selected Jupyter core packages...
IPython          : 8.27.0
ipykernel        : 6.28.0
ipywidgets       : 7.8.1
jupyter_client   : 8.6.0
jupyter_core     : 5.7.2
jupyter_server   : 2.14.1
jupyterlab       : 4.2.5
nbclient         : 0.8.0
nbconvert        : 7.16.4
nbformat         : 5.10.4
notebook         : 7.2.2
qtconsole        : 5.5.1
traitlets        : 5.14.3
```




# 超簡単なkernel作成

## kernel作成のドキュメントからサンプルをコピー

https://ipython.readthedocs.io/en/stable/development/wrapperkernels.html#example

上記からEcho Kernelのソースをコピーします。

```echokernel.py
from ipykernel.kernelbase import Kernel

class EchoKernel(Kernel):
    implementation = 'Echo'
    implementation_version = '1.0'
    language = 'no-op'
    language_version = '0.1'
    language_info = {'mimetype': 'text/plain'}
    banner = "Echo kernel - as useful as a parrot"

    def do_execute(self, code, silent, store_history=True, user_expressions=None,
                   allow_stdin=False):
        if not silent:
            stream_content = {'name': 'stdout', 'text': code}
            self.send_response(self.iopub_socket, 'stream', stream_content)

        return {'status': 'ok',
                # The base class increments the execution count
                'execution_count': self.execution_count,
                'payload': [],
                'user_expressions': {},
               }

if __name__ == '__main__':
    from ipykernel.kernelapp import IPKernelApp
    IPKernelApp.launch_instance(kernel_class=EchoKernel)
```

```kernel.json
{"argv":["python","-m","echokernel", "-f", "{connection_file}"],
 "display_name":"Echo"
}
```

ファイル構成

```
echo/
└─ kernel.json
```
kernelのインストール。echoはディレクトリ名
```bash
> jupyter kernelspec install echo --user
```
これらに若干の変更を加えてGHCi用のkernelを作ります。

## echo kernelを変更してGHCi用のkernel作成

名前は**ghci**にします。

### ファイル構成
```
ghci/
├─ kernel.json 
└─ logo-64x64.png
```

```kernel.json
{
    "argv": ["python", "-m",
             "ghci_kernel", "-f",
             "{connection_file}"],
    "display_name": "GHCi",
    "language": "haskell",
    "env": {
        "PYTHONPATH": "/Users/xxxx/yyyy"
    }
}
```
- `ghci_kernel`はこれから作るkernelのソース名
- `display_name`はJupyter notebookのkernel名に表示される
- `PYTHONPATH`はこれから作る`ghci_kernel.py`があるディレクトリ名で、もしPathがすでに通っているなら`env`は不要


**logo-64x64.png**はJupyter Labでlauncherに表示される画像で、64x64ピクセルの画像です。無くてもOK。

##### kernel情報のインストール

```bash
> jupyter kernelspec install ghci --user
[InstallKernelSpec] Installed kernelspec ghci in /Users/xxxx/Library/Jupyter/kernels/ghci
```

### kernel作成

```ghci_kernel.py
from ipykernel.kernelbase import Kernel
from pexpect.replwrap import REPLWrapper

class GHCiKernel(Kernel):
    implementation = 'GHCi'
    implementation_version = '0.0.1'
    language_info = {
        'name': 'haskell',
        'mimetype': 'text/x-haskell',
        'file_extension': '.hs'
    }

    language_version = '9.12.2'
    banner =  'Simple GHCi Kernel'

    replwrapper = REPLWrapper("ghci", "ghci> ", None,
                              continuation_prompt="ghci| ")

    def do_execute(self, code, silent, store_history=True, 
                   user_expressions=None, allow_stdin=False):        
        if not silent:
            stdout = self.replwrapper.run_command(code, timeout=None)
            stream_content = {'name': 'stdout', 'text': stdout}
            self.send_response(self.iopub_socket, 'stream', stream_content)

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

if __name__ == '__main__':
    from ipykernel.kernelapp import IPKernelApp
    IPKernelApp.launch_instance(kernel_class=GHCiKernel)
```

基本的には`REPLWrapper`を使って、Jupyterの入力Cellの内容を`run_command`に渡し、その結果をJupyterに渡すだけです。echo kernelとそんなに変わりません。以下が各プロパティに加えて追加したものです。

```py
from pexpect.replwrap import REPLWrapper
```
```py
    replwrapper = REPLWrapper("ghci", "ghci> ", None,
                              continuation_prompt="ghci| ")
```
```py
            stdout = self.replwrapper.run_command(code, timeout=None)
            stream_content = {'name': 'stdout', 'text': stdout}
```

次のURLが`REPLWrapper`のドキュメントです。

https://pexpect.readthedocs.io/en/stable/api/replwrap.html

`REPLWrapper`の引数
- "ghci": 起動する対話型Haskellインタプリタ
    - stackを使っているときは"stack ghci"または"stack repl"でもOK
- "ghci> ": プロンプト  
- continuation_prompt="ghci| " : 継続入力のプロンプト

以下のGHCiを起動した時のプロンプトを上記の設定に使いました。
```zsh
> ghci
GHCi, version 9.12.2: https://www.haskell.org/ghc/  :? for help
ghci> :set +m
ghci> let f :: Int -> Int
ghci|     f x = x + 10
ghci| 
ghci> f 1
11
```

## テスト

まだ、勉強始めたばかりなので、以下のIHaskellにあるコードでエラー起きないように一部変更して実行しました。特にブロック(複数行)にしないとエラーになるコードは`:set +m`を設定して`let`または`:{ :}`を使いました。

https://github.com/IHaskell/IHaskell/blob/master/notebooks/IHaskell.ipynb


![haskell01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/336132a9-8015-4f12-88b0-fbef75a5542e.png)

ちゃんと**Haskell**のロゴが表示されています
また、下記の画像の右上にkernel名**GHCi**が表示されています。

![haskell02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/abafc861-bd89-4696-8250-06867a0822f0.png)


![haskell03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/f008e34f-7dfc-4c16-9e14-5071e5a21f7c.png)

![haskell04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/3ded905f-95f2-460f-bd15-d2a04d51bb9a.png)


入力の[11],[12],[13]は最後に空行を追加しています。これはブロックを終了させるためです。



:::note warn
追記)
フィボナッチ数列は`fib 0 = 0`のようですが、上記のようにIHaskellのサンプルコードは`fib 0 = 1`となっています。画像の変更はしません。以降のテストも同様です。
:::

# 改良

```diff_python
         if not silent:
+            if self.execution_count == 1:
+                code = ":set +m\n" + code 
+            if not code.endswith("\n"):
+                code += "\n"
             stdout = self.replwrapper.run_command(code, timeout=None)
```
毎回最初に`:set +m`をCellから実行するのは面倒なので、プログラムで実行します。入力の最後が改行でないとき改行を追加していますが、ブロックの最後は空行が必要なためです。`:{ :}`を使う場合は関係ないですが。

## 改良版のテスト

![haskell05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/90bb66e1-7aa4-4890-8604-f361622f0222.png)

空行を追加しなくても、ちゃんと動作しました。

# 終わりに

たった10行にも満たないコードの追加で簡単なGHCi用のkernelが出来ました。しかし、問題もあります。`fib 100`を実行したら全然終わらなかったので`Restart Kernel`を実行しても`fib 100`を実行しているGHCiは終了せずもう1個起動してたので、結果GHCiが2個動いている状態でした。よって、`fib 100`を実行していたprocessを`kill -KILL pid`で強制終了させました。
次回の投稿はこれに対処して、`Interrupt Kernel`にも対応する予定ですが、上記のコードだけでも入門レベルの学習用には使えると思います。


# 改良版と機能アップ版
改良版

https://qiita.com/h6591/items/cca23b1c8841850104bf

改良版をさらに機能アップ

https://qiita.com/h6591/items/5611ce9684189eb608a3