Stackless Pythonでは、プログラムがユーザをファンクションコールする！

タイトルは[ロシア的倒置法](https://ja.uncyclopedia.info/wiki/%E3%83%AD%E3%82%B7%E3%82%A2%E7%9A%84%E5%80%92%E7%BD%AE%E6%B3%95)です。

# 環境

[Stackless Python](https://github.com/stackless-dev/stackless/wiki) 3.6.8 Windows 32-bit

# TL;DR

メジャーな言語処理系はすべて、ユーザからの入力待ちをすることができる。Pythonなら`input()`。これは、ユーザに値を渡して値を返してもらう、つまり「ユーザを関数として呼び出ししている」と言える。

ユーザが値の入力を終えるまでのあいだ、スレッドはイベント待ちをする。

似たような状況で、イベント待ちができない場合がある。Webだ。もしメモリ上にコールスタックを広げたままユーザの入力を延々と待ち続けると、すさまじいムダと面倒が発生する。イベント待ちをするかわりに、処理を続けるための情報（セッションデータ）をDBに入れて、プロセスを終了しなければならない。

「ではコールスタックをセッションデータにしてしまえ」というのが自然な発想ではあるまいか。というより、そうしないことには、コールバック地獄の2乗が待っている。少しでも複雑なフローを書くには、なんらかの方法（DSLなど）で同じことをする必要がある。

「なんらかの方法」のひとつが、Stackless Pythonである。

# CPythonの`input()`版

例として、ユーザに生年月日を入力させて、星座占いの星座（サイン）を求めるプログラムを見てみる。なお`pip install zodiac-sign`が必要。

```python:cpython_input.py
from zodiac_sign import get_zodiac_sign
import locale
locale.setlocale(locale.LC_ALL, 'en_US')
from datetime import date
import calendar

def input_num_range(range, msg):
    while True:
        s = input(msg + f' [{range[0]} - {range[1]}]:')
        try:
            i = int(s)
        except ValueError:
            print("Your input wasn't integer. Try again.")
            continue
        if i < range[0]:
            print("Your input was too small number. Try again.")
            continue
        if i > range[1]:
            print("Your input was too big number. Try again.")
            continue
        return i

def main():
    print("This program decides your zodiac sign. Please input your date of birth.")
    today = date.today()
    year = input_num_range([1900, today.year], "The year of birth")
    month = input_num_range([1, 12], "The month of birth")
    max_day = calendar.monthrange(year, month)[1]
    day = input_num_range([1, max_day], "The day of birth")
    d = date(year, month, day)
    z = get_zodiac_sign(d)
    print(f'Your zodiac sign is {z}.')

if __name__ == '__main__':
    main()
```

ではこれを、

- ユーザに入力を求めるたびにプロセスを終了して、次回起動時のコマンドライン引数で入力を受け取るプログラム

に書き換えるには、どうすればいいか。

「`main()`をズタズタに切り裂く」というのが最悪の答えだ。そんな設計でメンテ可能なのは、せいぜいこの例くらいの規模までだろう。もし`main()`から`input()`までのコールスタックの深さが3つを超えれば、おそらくそのプログラムは、バグを1つ潰すごとに常に2つ以上のバグが発生するので、どう頑張ってもバグを減らすことができない。

# Stackless Python版

```python:stackless_python.py
from zodiac_sign import get_zodiac_sign
import locale
locale.setlocale(locale.LC_ALL, 'en_US')
from datetime import date
import calendar

import pickle, os, sys
import stackless

PICKLE_FN = 'stackless_python_tasklet.pickle'
CHANNEL = stackless.channel()

def input_stackless(msg):
    print(msg)
    while True:
        s = CHANNEL.receive()
        if s is not None: # CHANNEL.receive() returns None once when it was unpickled. Bug?
            return s

def input_num_range(range, msg):
    while True:
        s = input_stackless(msg + f' [{range[0]} - {range[1]}]:')
        try:
            i = int(s)
        except ValueError:
            print("Your input wasn't a integer. Try again.")
            continue
        if i < range[0]:
            print("Your input was too small number. Try again.")
            continue
        if i > range[1]:
            print("Your input was too big number. Try again.")
            continue
        return i

def main():
    print("This program decides your zodiac sign. Please input your date of birth.")
    today = date.today()
    year = input_num_range([1900, today.year], "The year of birth")
    month = input_num_range([1, 12], "The month of birth")
    max_day = calendar.monthrange(year, month)[1]
    day = input_num_range([1, max_day], "The day of birth")
    d = date(year, month, day)
    z = get_zodiac_sign(d)
    print(f'Your zodiac sign is {z}.')

def load_task():
    try:
        with open(PICKLE_FN, 'rb') as f:
            task = pickle.load(f)
            task.insert()
            return task
    except:
        rm_task_pkl()
        return None

def dump_task(task):
    with open(PICKLE_FN, 'wb') as f:
        pickle.dump(task, f)
        task.kill()
        task.remove()

def rm_task_pkl():
    if os.path.exists(PICKLE_FN):
        os.remove(PICKLE_FN)

def start_or_resume_main():
    task = load_task()
    if task is None:
        task = stackless.tasklet(main)()
        task.run()
        dump_task(task)
    else:
        if len(sys.argv) > 1:
            CHANNEL.send(sys.argv[1])
            if task.alive:
                dump_task(task)
            else:
                rm_task_pkl()
        else:
            print("Command-line argument is missing.")

if __name__ == '__main__':
    start_or_resume_main()
```

`input_num_range()`は`input()`を差し替えただけ、`main()`は一文字も変えていない。

Stackless Python特有の仕掛けは2つある。スタックレスなコルーチンの`stackless.tasklet`と、`pipe`風のコルーチン間通信ができる`stackless.channel`だ。

`stackless.tasklet(main)()`で`task`インスタンスが生成され、`task.run()`で頭から実行され、`CHANNEL.receive()`で`send()`待ちに入る。これで`task`インスタンスは「ブロックされた」状態になり、他のコルーチンへと実行が切り替わる。この場合、`task.run()`したメインコルーチンしか他に存在しないので、`task.run()`の続きが実行される。

「ブロックされた」状態のコルーチンを`pickle.dump(task, f)`して`task = pickle.load(f)`できる、という機能の持つ意味は深い。Stackless Pythonにおいては、コールスタックがファーストクラスオブジェクトなのだ。これには、関数がファーストクラスオブジェクトなのと同じくらいのインパクトがある。

ラムダ式がなかった時代のC++に感じたのと同じ苛立ちを、私は今、現在のほとんどの言語処理系に感じている。

# 補遺

おそらくStackless Pythonのバグだと思うが、`pickle.load()`した直後、そのインスタンスをブロックしている`channel`に`send()`すると、最初に`None`が`receive()`から返される。検証コード：

```python
import stackless
import pickle

CH = stackless.channel()

def foo(msg):
    while True:
        s = CH.receive()
        cid = str(stackless.getcurrentid())
        print(f'thread_id: {cid}   {msg}: {s}')

t1 = stackless.tasklet(foo)('t1')
stackless.run()
print('before a')
CH.send('a')
print('after a')

pickled = pickle.dumps(t1)
t1.kill()
t1.remove()

t1_reloaded = pickle.loads(pickled)
t1_reloaded.insert()

print('before b')
CH.send('b')
print('after b')

print('before c')
CH.send('c')
print('after c')
```

```
before a
thread_id: 70396336   t1: a
after a
before b
thread_id: 70738224   t1: None
thread_id: 70738224   t1: b
after b
before c
thread_id: 70738224   t1: c
after c
```

コルーチンの永続化ができるらしい（ある程度メジャーな）言語処理系を、私の知るかぎり列挙してみる。

- Lua
    - `pluto`を使う
- Rhino
- Haskell
    - [How to get serializable coroutines?](https://www.reddit.com/r/haskell/comments/4a8wr8/how_to_get_serializable_coroutines/)

多分（当然？）できる：

- Common Lisp、Scheme

できそうでできないらしい：

- Kotlin
    - [Serialization of closures and continuations. #44](https://github.com/Kotlin/kotlinx.serialization/issues/44)