概要
下記の投稿記事に超簡単なGHCi用のJupyterのkernelを作りましたが、Restart Kernel
とInterrupt Kernel
に対応する機能が実装していませんでした。今回はそれらを実装した改良版を作ろうと思います。下記の投稿記事を知ってることを前提とします。
実行環境
> 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
ghci-kernel.pyを改良しよう
前回作った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("stack ghci", "ghci> ", None,
continuation_prompt="ghci| ")
def do_execute(self, code, silent, store_history=True,
user_expressions=None, allow_stdin=False):
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)
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
関係の処理を別クラスGHCiKernel
にしてshutdownの簡単な処理を追加。
from ipykernel.kernelbase import Kernel
from pexpect.replwrap import REPLWrapper
import pexpect
import signal
INIT_CMD = """
:set +m
"""
class GHCi:
def __init__(self):
self.start_ghci()
def start_ghci(self):
self.replwrapper = REPLWrapper("ghci", "ghci> ", None,
continuation_prompt="ghci| ")
self.run_command(INIT_CMD)
def run(self, code):
if not code.endswith("\n"):
code += "\n"
return self.run_command(code)
def run_command(self, code):
stdout = self.replwrapper.run_command(code, timeout=None)
stderr = None
return stdout, stderr
def shutdown(self, restart):
self.run_command(":quit")
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'
ghci = GHCi()
def do_execute(self, code, silent, store_history=True,
user_expressions=None, allow_stdin=False):
if not silent:
stdout, stderr = self.ghci.run(code)
if stdout != None:
stream_content = {'name': 'stdout', 'text': stdout}
self.send_response(self.iopub_socket, 'stream', stream_content)
if stderr != None:
stream_content = {'name': 'stderr', 'text': stderr}
self.send_response(self.iopub_socket, 'stream', stream_content)
return {'status': 'ok', 'execution_count': self.execution_count,
'payload': [], 'user_expressions': {}}
def do_shutdown(self, restart):
self.ghci.shutdown(restart)
return super().do_shutdown(restart)
if __name__ == '__main__':
from ipykernel.kernelapp import IPKernelApp
IPKernelApp.launch_instance(kernel_class=GHCiKernel)
-
import
の3行目、4行目は後で使うのであらかじめ書いておきます -
stream_content
の'name':'stderr'
で返すとnotebookの表示の背景がピンクになります。これも後で使います -
do_shutdown
はnotebookのShutdown
またはRestart Kernel
を実行したときに呼ばれるメソッドなのでGHCiをquitします - 参照資料
Interrupt KernelとShutdown Kernelへの対応
class GCHi
にだけ改良を加えるので、class GHCiKernel
等については記載しません。
class GHCi:
def __init__(self):
self.start_ghci()
def start_ghci(self):
self.replwrapper = REPLWrapper("ghci", "ghci> ", None,
continuation_prompt="ghci| ")
self.child = self.replwrapper.child
self.run_command(INIT_CMD)
def run(self, code):
if not code.endswith("\n"):
code += "\n"
return self.run_command(code)
def run_command(self, code):
try:
stderr = None
stdout = self.replwrapper.run_command(code, timeout=None)
except BaseException as e:
stdout, stderr = self.interrupt(e)
return stdout, stderr
def interrupt(self, e):
stderr = "exception: {}".format(type(e).__name__)
# GHCiの起動中かの確認
if self.child.isalive():
# GHCiのSIGINT(CTRL+C)を送る
self.child.sendintr()
# GHCiがSIGINTを受けてプロンプトを表示するまでのデータを受け取る
self.replwrapper._expect_prompt()
# 受け取った内容をstdoutに代入
stdout = self.child.before
else:
# GHCiが起動していないので再起動
self.start_ghci()
stdout = "Restarted GHCi"
return stdout, stderr
def shutdown(self, restart):
try:
self.replwrapper.run_command(":quit", timeout=0.5)
except pexpect.EOF:
return
except BaseException:
pass
if self.child.isalive():
# GHCiが終了していないときは強制終了させる
self.child.kill(signal.SIGKILL)
-
self.replwrapper.child
- REPLWrapperは内部に
pexpect.spawn
のオブジェクト(child)を持っていて、大部分の処理はこのオブジェクトを通して行われているようです - pexpect.spawnのドキュメント
- REPLWrapperは内部に
-
run_command
のtry ~ excpet
-
run_command
が処理中のときにInterrupt Kernel
をしたときなどに例外が発生します- Interrupt Kernel ==>
KeyboardInterrupt
(CTRL+Cです) - GHCiから受取データがEOF ==>
pexpect.EOF
- timeout ==>
pexpect.TIMEOUT
- Interrupt Kernel ==>
- KeyboardInterruptは親がExceptionでなくBaseExceptionなので例外のキャッチをBaseExceptionにしています(Exceptionの親もBaseException)
-
run_command
が処理中にShutdown Kernel
をするとKeyboardInterrupt
の例外が発生してからdo_shutdown
を呼ばれるようです
-
-
shutdown
-
:quit
でGHCiが終了するとプロンプトは表示されないのでずっと待ち続けてtimeoutになると思うがpexpect.EOF
のほうが先に発生して、self.child.isalive()
はFalseになりkill
しなくてよい -
timeout=0.5
にしている理由は、万が一sendintr()
などが効かずGHCiが:quit
を受け取れずに処理を続けているとkill
が実行される前にKernel本体が終了してkill
が出来ずにGHCiプロセスが残ってしまう- "Kernel本体が強制終了" > "timeout" > "quitが成功してEOF例外発生" が成り立つようなtimeoutにしたい
- 通常ならこのケースは起こらない
-
テスト
入力[2]で処理中にStopボタン(Interrupt Kernel)を押すと上記画像のように例外発生して,GHCiにSIGINT(CTRL+C)が送られてInterrupted.
が表示されています。下記のGHCi上での処理と同じになりました。
入力[3]で通常の処理が可能になっています。当然,[2]のCellをfib 10
と書き換えても正常に実行されます。
> ghci
GHCi, version 9.12.2: https://www.haskell.org/ghc/ :? for help
ghci> :set +m
ghci> -- If your code isn't running fast enough, you can just put it into a module.
ghci> module A.B where
ghci| fib 0 = 1
ghci| fib 1 = 1
ghci| fib n = fib (n-1) + fib (n-2)
ghci|
ghci> fib 100
^CInterrupted.
ghci> fib 10
89
ghci>
終わりに
思い違いをして手間取ったのがKeyboardInterrupt
をException
でキャッチできると思い込んで何でキャッチ出来ないんだろうと試行錯誤していたところでしょうか。
ここのところC/C++でJupyter関連の投稿や今回のHaskellの投稿をしていて一番勉強になっているのはPythonかもしれませんが、Haskellの勉強も時間をみながら進めていきたいと思っています。