タイトルはロシア的倒置法です。
環境
Stackless Python 3.6.8 Windows 32-bit
TL;DR
メジャーな言語処理系はすべて、ユーザからの入力待ちをすることができる。Pythonならinput()
。これは、ユーザに値を渡して値を返してもらう、つまり「ユーザを関数として呼び出ししている」と言える。
ユーザが値の入力を終えるまでのあいだ、スレッドはイベント待ちをする。
似たような状況で、イベント待ちができない場合がある。Webだ。もしメモリ上にコールスタックを広げたままユーザの入力を延々と待ち続けると、すさまじいムダと面倒が発生する。イベント待ちをするかわりに、処理を続けるための情報(セッションデータ)をDBに入れて、プロセスを終了しなければならない。
「ではコールスタックをセッションデータにしてしまえ」というのが自然な発想ではあるまいか。というより、そうしないことには、コールバック地獄の2乗が待っている。少しでも複雑なフローを書くには、なんらかの方法(DSLなど)で同じことをする必要がある。
「なんらかの方法」のひとつが、Stackless Pythonである。
CPythonのinput()
版
例として、ユーザに生年月日を入力させて、星座占いの星座(サイン)を求めるプログラムを見てみる。なおpip install zodiac-sign
が必要。
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版
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()
から返される。検証コード:
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
多分(当然?)できる:
- Common Lisp、Scheme
できそうでできないらしい: