Vim script に Java 8 の Stream API がほしい
ので作ってる。この記事も PR も絶賛更新中。
一言で言うと underscore.vim + Data.LazyList 的なものがほしかった。 Twitter でぼやいた時の会話。
vital.vim の Data.List って、名前からだと https://t.co/423dfKhtl4([1,2,3,4,5]).filter({x -> x % 2 == 1}).fold_left((a,b) -> a+b) みたいなのできるみたいな印象なんだよなぁ
— tyru (@_tyru_) 2017年4月23日
今の Data.List はどちらかというと適切な名前は ListUtils とかな気がする
— tyru (@_tyru_) 2017年4月23日
Vim script でもドットの直前に(空白無しで)バックスラッシュ付ければ改行はいけるし、別にメソッドチェイン前提のインターフェースのライブラリがあってもいいと思うんだけどなぁ
— tyru (@_tyru_) 2017年4月23日
lambda も入ったし Java 8 の Stream API 的なのは結構ほしい。でも Vim script がリスペクトしてる Python 的な考えからすると邪道なんだろうけど。lambda も 2 行以上書けないし…(だったよね?)
— tyru (@_tyru_) 2017年4月23日
@_tyru_ ふと思い出しましたが,Vital.OptionParser は割とそうなってます(破壊的なので別に chain させなくても良いんですが…) https://t.co/hjjvJy5pU1
— ドッグ (@Linda_pp) 2017年4月23日
@Linda_pp @_tyru_ ふと思いましたが、underscore.vim (vital) はメソッドチェーンしまくるインターフェースですね
— はやぶさ (@haya14busa) 2017年4月23日
@_tyru_ コメントを挟み込めれたらなあ
— ujm (@ujm) 2017年4月24日
要件としては
- lambda が扱えること
- 無限ストリームを扱えること
- 最後の値取得までのメソッドチェインの順序から実行計画を組み立て、必要な分だけ map() や filter() を行うこと
- (Java と同じく)二度 Stream が実行されることはないとする
- 要件というか制限な気もするけど「インスタンスを使いまわさないコーディングを強制する」と考える(ことにする)
ちなみに実装としては Java の Stream みたく characteristics を持っていて、Spliterator ライクな内部 API で実装されている。
内部的にも Stream API に似せていて Java 8 の Spliterator クラスのような処理を実装した。ただ Vim script だと Iterator みたいに1つずついてレートしてたら遅くなるんで N 個取れる時はガッと取れるような作りにしてる。
— tyru (@_tyru_) 2017年4月26日
他にも数が少ない時は :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)
- Stream.find(func)
- Java には無し
- Stream.count()
Stream.distinct()
Stream.anyMatch()
- Stream.allMatch()
Stream.noneMatch()
Stream.skip(n)
- Stream.limit(n)
- Stream.take_while(func)
- Java には無し
Stream.drop_while(func)
- Java には無し
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(関数)
- map(F(要素数), 関数)
- Stream.filter(関数)
- filter(F(要素数), 関数)
- Stream.sorted(比較関数)
- sort(F(要素数), 比較関数)
- Stream.limit(新要素数)
- F(新要素数)
- 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 を文字列結合で式を組み立てる。