1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

概要

下記の投稿記事に超簡単なGHCi用のJupyterのkernelを作りましたが、Restart KernelInterrupt 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

前回作った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の簡単な処理を追加。

ghci_kernel.py
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を実行したときに呼ばれるメソッドなのでGHCiquitします
  • 参照資料

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のドキュメント
  • run_commandtry ~ excpet
    • run_commandが処理中のときにInterrupt Kernelをしたときなどに例外が発生します
      • Interrupt Kernel ==> KeyboardInterrupt (CTRL+Cです)
      • GHCiから受取データがEOF ==> pexpect.EOF
      • timeout ==> pexpect.TIMEOUT
    • KeyboardInterruptは親がExceptionでなくBaseExceptionなので例外のキャッチをBaseExceptionにしています(Exceptionの親もBaseException)
    • run_commandが処理中にShutdown KernelをするとKeyboardInterruptの例外が発生してからdo_shutdownを呼ばれるようです
  • shutdown
    • :quitGHCiが終了するとプロンプトは表示されないのでずっと待ち続けてtimeoutになると思うがpexpect.EOFのほうが先に発生して、self.child.isalive()Falseになりkillしなくてよい
    • timeout=0.5にしている理由は、万が一sendintr()などが効かずGHCi:quitを受け取れずに処理を続けているとkillが実行される前にKernel本体が終了してkillが出来ずにGHCiプロセスが残ってしまう
      • "Kernel本体が強制終了" > "timeout" > "quitが成功してEOF例外発生" が成り立つようなtimeoutにしたい
      • 通常ならこのケースは起こらない

テスト

haskell06.png

入力[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> 

終わりに

思い違いをして手間取ったのがKeyboardInterruptExceptionでキャッチできると思い込んで何でキャッチ出来ないんだろうと試行錯誤していたところでしょうか。
ここのところC/C++でJupyter関連の投稿や今回のHaskellの投稿をしていて一番勉強になっているのはPythonかもしれませんが、Haskellの勉強も時間をみながら進めていきたいと思っています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?