Humanity

Edit the world by your favorite way

Vim script に ECMAScript の Observable がほしい

タイトルは前記事からの流用。

最近 Vim script で Java 8 の Stream API を実装する、ということをしている。

tyru.hatenablog.com

github.com

Vim 8 でも lambda が入ったので、メソッドチェインでどんどん処理を繋げるスタイルの書き方ができると嬉しいとつくづく思っていたからだ(例:JS の Promise)。 ちなみにこの Vital.Stream モジュールを使うと以下のように書けるようになる(適当な乱数を10個生成するコード)。

というわけで開発の方も段々落ち着いてドキュメントを整備したりしているので、 ふと前から作りたかった ECMAScript の Observable を Vim script で実装するということについて考えてみた。

  • 非同期インターフェースを強制することになるので、Vim 8 の +timer 必須になるからおそらく Vim 8 以降限定のライブラリになるはず *1
  • あと async / await なんてものは Vim script に絶対入ることはないので、意地でもメソッドチェインで処理しないといけない

とつらつらと考えた所で、 Stream と Observable の相互変換ってできるんだろうか?という疑問がふと浮かび上がった。 filter や map でメソッドチェインする辺り似たようなインターフェースなのでできれば協調させたい… と考えた所やはりそれは無理という結論に至った。

Java の Stream は filter や map などの中間処理(intermediate operation)は非同期に実行されうるが、Stream から値を取り出す終端処理(terminal operation)は同期的なインターフェースである。 しかし Promise 型や Observable 型の値は持ち回った結果受け取るのも非同期である必要がある。 その点 Stream は出口が同期的なので、既存コードへの導入も楽だろう。 しかし本質的に非同期でないと処理できない類の問題がある。イベント処理だ。

Stream から要素を取ってくるために終端処理を実行するという一連の動作は能動的なものだが、プログラムのユーザーがクリックするなどのイベントをプログラムが事前に知ることは不可能だ。 どうしてもイベント駆動にならざるを得ない。 そもそも Promise や Observable はそのために導入されたものだ。 async / await で同期処理の様に書ける、と言ってもそれはトランスパイラやブラウザが読み替えているだけであって、非同期であることを意識して扱う必要があることに変わりはない。 この様に async / await は単なる糖衣構文であることを念頭に置く必要がある。


と、いうように当たり前だけど Observable と Stream は別物である。 RxJava に限らず各言語の Rxほげ なライブラリの Observable はなんだかリスト処理とかもできちゃったりするらしい(どうやら同期的な処理用のメソッドもある?)ので勘違いしそうになるので、ここでは ECMAScript の Observable のことを考えた方が良さそうだ。

ただ今考えるだけでも Vim script だと困る部分が色々ある。 例えば Vim でイベントって言ったら auto command だけど autocmd コマンドだと受け取ったコマンドを「文字列的に」実行するので、ローカル変数のキャプチャができない。 Vim script にはこのように副作用を強制するインターフェースがあちこちにある。 これを回避するにはスクリプトローカル変数や、グローバル変数、とにかくローカル変数以外のスコープの変数に代入する必要がある。 これはとても醜いのでできれば Vital.Observable.Autocmd みたいなモジュールで吸収できるものなら吸収したい。 そして listen(event) みたいな関数をポーンと提供したい。 しかし本当に実現可能だろうか。 恐らく追加で呼び出し元がどいつかを関数の引数として与えてやらなければいけない気がする。 できればそれは避けたいけど。


そういえばりんだんさん(id:rhysd)に「(Vital.Stream モジュールに)from_channel メソッドが欲しいです」って言われてて今回は量多くなるので見送ったのだけど、Vim の channel はソケット通信のためのオブジェクトで、非同期なので、Vital.Stream モジュールでは扱えませんというのが正しそう。

どうしても扱いたい場合は終端処理を実行した時点までに来た要素なら受け取れるけど、非同期で受け取った要素をバッファに持っておく必要があって逐次的に処理ができないため余計なメモリを食うし、そもそも途中までの要素がほしいってあんまり意味がない気がする。*2 なのでそういうのは Vital.Observable みたいな非同期用インターフェースで解決するのが筋じゃないだろうか。


と、記事を書いてから無限ストリームにイベントを流して take_while で中断するという用途はありそうだと思った。 ただ要素を取得中はブロッキングしてしまうのでやっぱり非同期で受け取りたいところ。

*1:それ以前の Vim では任意の処理を非同期にする方法は中々難しかった。この記事ではあまり関係ないので割愛

*2:りんだんさんもおそらくそういうつもりで言ったんじゃなさそう

Vim script に Java 8 の Stream API がほしい

ので作ってる。この記事も PR も絶賛更新中。

github.com

一言で言うと underscore.vim + Data.LazyList 的なものがほしかった。 Twitter でぼやいた時の会話。

要件としては

  • lambda が扱えること
  • 無限ストリームを扱えること
    • 最後の値取得までのメソッドチェインの順序から実行計画を組み立て、必要な分だけ map() や filter() を行うこと
  • Java と同じく)二度 Stream が実行されることはないとする
    • 要件というか制限な気もするけど「インスタンスを使いまわさないコーディングを強制する」と考える(ことにする)

ちなみに実装としては Java の Stream みたく characteristics を持っていて、Spliterator ライクな内部 API で実装されている。

他にも数が少ない時は :for ループではなく map() を使ったりとか色々最適化したりしたい。

大体これさえあれば困らないよねリスト

主に

こちらの記事も参照。

具体的な関数は以下の通り。

  • of(list)
  • empty()
  • Stream.concat(another)

  • iterate(init, func)

  • generate(generator)
  • range(from, to)
  • rangeClosed(from, to)

  • Stream.peek()

  • Stream.map(func)

  • Stream.flatmap(func)
  • Stream.filter(func)
  • Stream.foreach(func)

  • Stream.max(func)

  • Stream.min(func)

  • Stream.find_first(func)

  • Stream.find_any(func)
    • Java だと parallel stream の場合に Stream.find_first(func) と結果が異なる場合がある?Vim script ではスレッドは使えないので同じ
  • Stream.find(func)
  • Stream.count()
  • Stream.distinct()

  • Stream.anyMatch()

  • Stream.allMatch()
  • Stream.noneMatch()

  • Stream.skip(n)

  • Stream.limit(n)
  • Stream.take_while(func)
  • Stream.drop_while(func)

  • Stream.sorted()

  • comparing(prop, is_func)
  • comparing(prop, is_func).thenComparing(prop, is_func)
  • comparing(prop, is_func).reverse()

  • Stream.collect(collector)

  • Stream.collect(supplier, accumulator, combiner)
  • Stream.reduce(func, init)
  • Stream.average()
  • Stream.sum()

  • Collectors.to_list()

    • これわざわざ別のモジュールにしなくてもいいような気がする
  • zip()

    • なんで Java 8 には入らなかったんや
  • Dictionary or List から Stream への変換関数

  • Stream から Dictionary or List への変換(畳込み)関数

Stream の実装

終端処理が実行されたら、遡るように map(), filter(), sorted() 等のいずれかの処理を行う。 上記3つの処理は内部的な private 関数で、リスト、(比較)関数、処理する要素数を引数に受け取る。

リストは計算済みのものが下位(終端処理から遠い方)、(比較)関数はユーザーからの引数、処理する要素数は上位(終端処理に近い方)から limit() 等が呼ばれ、個数が判明している場合はその個数が、判明していない場合は 1/0 が渡される(Java の Spliterator#estimateSize() と同じ)。

任意の中間処理とその前の処理の間のデータの受け渡しは、合成された親の関数を要求する最大*1の要素数の引数とともに呼ぶことで行われる。

上位から F(3) のように要素を求められるとその分を都度下位の関数を再帰的に読んで計算し、上位に返す。 個数が分からない場合は F(1/0) のように 1/0 が指定される。

ここまでの話を踏まえ、中間処理としてそれぞれ以下のコードが実行される。 (TODO: 随時中間処理を追記)

  • Stream.map(関数)
  • Stream.filter(関数)
    • filter(F(要素数), 関数)
  • Stream.sorted(比較関数)
    • sort(F(要素数), 比較関数)
  • Stream.limit(新要素数)
  • Stream.skip(スキップ数)
    • F(要素数)[スキップ数 :]

ちなみに Vim 7 には Partial(部分適用)や lambda がない。 よって関数 F を生成することはできない。 代わりに Dictionary を使って擬似的に実現する。

let F = {'list' [1,2,3]}
function! F.take(n)
  return self.list[: a:n - 1]
endfunction

vital は Vim 7 もサポートするので、やっていくしかない。

Comparator の実装

  • Q. .thenComparing() や .reversed() などの比較関数の合成をどうするか?
  • A. Vim 7 でも使えるようにするために、受け取った expr を文字列結合で式を組み立てる。

*1:filter() があるため、必ず要求された要素数分のリストが返ってくるとは限らない