Sublime Text のプラグインのコールバックをジェネレータで「インライン化」する

## 概要 ##

Sublime Text の各種入力インターフェースを利用する処理がコールバック地獄になりがちなので、ジェネレータを使ってすっきり書いてみたよ！　というお話です。
いま書いているプラグインでも使いまくりです。


## 背景 ##

Sublime Text 3 の API は大体テキストエディタを介してやりたい操作は実現できます。メニューアイテムをカスタマイズしたクイックパネルを出したりテキスト入力パネルを出したりと各種の入力受付を出来るわけですが、使っている方はお分かりの通り、各種ユーザからの入力受付をやっている間もエディタの他の操作はブロックされませんよね？　それはもちろんこれらの入力受付インターフェースを操作する関数が新しいスレッドで実行されるからですが、そこから本体のスレッドの処理に干渉したい場合は、予め渡しておいたコールバック関数を入力が行われた後で呼び出します。

例えばクイックパネルの表示であれば以下のような感じ:

```py3
from sublime_plugin import WindowCommand


class ShowMenu(WindowCommand):

    def run(self):
        menu = ["A", "B", "C"]
        def cb(index):
            print(menu[index])
        self.window.show_quick_panel(menu, cb)

```

で要素 "A", "B", "C" を含むメニューが表示でき、`cb()` 関数に選択されたメニューのインデックスが渡されます。まあこれくらいだったらコールバックの内容も少ないので大したことないのですが、コールバックの行数やコールバックに渡す変数の数が増えて行ったりするとまたしんどくなります。
さらに面倒なのはユーザ入力を待つようなコマンドを連続して呼ぶことになった場合で、コールバックの中で次のユーザ入力待ちコマンドを適切なコールバックと共に呼んで……って書いていこうとすると例えば次のようになります。

```py3
class Auth(WindowCommand):

    def run(self):
        self.window.show_input_panel("username", "", cb1)
        # 入力パネルが表示されたら入力を待たずにこちら側の行が続行されるので
        # 入力を使った処理はコールバックに書く


def _cb1(username):
    # show_input_panel()そのままだと入力文字列が表示されて本当はアレなのですが、とりあえずそれは気にしない
    self.window.show_input_panel(
        "password", "", functools.partial(_cb2, username))


def _cb2(username, password):
    _auth(username, password)


def _auth(username, password):
    # username と password を使って認証処理

```

まあ書けますけど読みやすくはないですよね。関数として切り出す単位が適切な訳でもないのに、コールバックとして実行するためだけに関数として分離しないといけないのは面倒で直観的ではない書き方になります。受け渡しをするパラメータの数がたくさんあったりするとまた面倒ですし、メンテナンス性も下がります。

## もっとエレガントに書こう ##

もっとエレガントに書けないのか？……書けます。Python ならね。

原理的に言えばユーザからの入力を待つようになったところで関数の実行を一時停止出来れば良い訳です。――そう、 `yield` 文の出番です。`yield` 文を呼び出せばジェネレータに対して `next()` 関数あるいは　`send()` 関数が呼ばれるまで実行途中の状態を保存できます。ユーザからの入力があったら `send()` 関数を使うことでジェネレータ内の式で用いることができます。なければ単に `next()` を呼べば良い訳です。という訳で、まずはこんな感じのデコレータを用意してやります。

```Py3

def chain_callbacks(
    f: Callable[..., Generator[Callable[Callable[...]], Any, Any]
) -> Callable[..., None]:
    @wraps(f)
    def wrapper(*args, **kwargs):
        chain = f(*args, **kwargs)
        try:
            next_f = next(chain)
        except StopIteration:
            return

        def cb(*args, **kwargs):
            nonlocal next_f
            try:
                if len(args) + len(kwargs) != 0:
                    next_f = chain.send(*args, **kwargs)
                else:
                    next_f = next(chain)
                next_f(cb)
            except StopIteration:
                return
        next_f(cb)
    return wrapper

```

若干いかついですが、こいつを使うことでコールバックを「インライン化」できます。
`yield` 文には「呼ばれたら `yield` 文以降が実行されてほしいコールバック関数一つを引数に取る関数」を渡しましょう。もしそのコールバック関数に何かの値が渡された場合は `yield` の返り値として受け取ることができます。

```py3
from functools import partial

class Auth(WindowCommand):

    @chain_callback
    def run(self):
        # `functools.partial()` を使って `on_done` 引数一つだけを取る
        # 関数の形にして、yield
        username = yield partial(
            self.window.show_input_panel, "username", "",
            on_change=None, on_cancel=None)
        password = yield partial(
            self.window.show_input_panel, "password", "",
            on_change=None, on_cancel=None)
        # ここから username と password を使って認証処理

```

こちら側はスッキリですね！

こういうジェネレータの使い方は `Twisted` や `Tornado` のような ウェブフレームワークでも使われています。