The Wayback Machine - https://web.archive.org/web/20201125184723/https://github.com/vim-jp/vital.vim/pull/567
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Review: Async.Promise #567

Merged
merged 20 commits into from Jan 1, 2018
Merged

Review: Async.Promise #567

merged 20 commits into from Jan 1, 2018

Conversation

@rhysd
Copy link
Contributor

@rhysd rhysd commented Dec 24, 2017

This PR takes over #526 because its timeline is too long.

I rebased the branch into 7 commits. Please see #526 for the detail of design of this library.

As described in #526, this PR only implements APIs of ES6 promises. So discussion for adding other (maybe useful) APIs is out of scope. Let's start from small library. After the smallest set of Promise is added to master branch, we can try it in real Vim plugin. And then, let's discuss further rich APIs (like bluebird).

In this PR, I want to

  • brash up implementation/documentation
  • fix bugs
  • add missing test cases

Thank you for your review in advance.

As of #526, either English or Japanese is ok.

@rhysd rhysd added the Review me label Dec 24, 2017
@rhysd rhysd mentioned this pull request Dec 24, 2017
@rhysd rhysd changed the title Promise review Review: Async.Promise Dec 24, 2017
elseif has_callback && success
call s:_resolve(a:promise, Result)
elseif !success
call s:_reject(a:promise, Err)

This comment has been minimized.

@vim-jp-bot

vim-jp-bot Dec 24, 2017

[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err

This comment has been minimized.

@rhysd

rhysd Dec 24, 2017
Author Contributor

この警告は問題ないことを確認済みですが,出ないようにコード直したほうが良いでしょうか?

This comment has been minimized.

@lambdalisue

lambdalisue Dec 25, 2017
Member

提案
lint なので強制力は無いですが、出ないほうが望ましいと思います。
特に Result と合わせる必要もないので Err の代わりに v:exception を直接使えばいいかなという気がします

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

ですね,そうしておきます.ありがとうございます.

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

v:exceptioncatch した節の中でしか使えないのを忘れていました…

This comment has been minimized.

@lambdalisue

lambdalisue Dec 26, 2017
Member

mkj... 間違った情報すみません。

@rhysd rhysd requested review from thinca, tyru, lambdalisue, haya14busa and mattn Dec 25, 2017
@rhysd
Copy link
Contributor Author

@rhysd rhysd commented Dec 25, 2017

#526 からレビューしてくれそうな気配がある人を Reviewers にアサインしてみました.年末で忙しいところすみませんが,レビューいただけると助かります.

@lambdalisue
Copy link
Member

@lambdalisue lambdalisue commented Dec 25, 2017

🎉 来週までには見ます!

Copy link
Member

@lambdalisue lambdalisue left a comment

とりあえず Promise.vim だけ見ました。
いくつか「提案」と「確認」を書いたのでフィードバックお願いします。


" @vimlint(EVL103, 1, a:resolve)
" @vimlint(EVL103, 1, a:reject)
function! s:noop(resolve, reject) abort

This comment has been minimized.

@lambdalisue

lambdalisue Dec 25, 2017
Member

メモ
すでに covimerage 側で修正されているかもしれませんが gina.vim に covimerage を導入した際に空っぽの関数があるとパースエラーが出ました。gina.vim では関数内部にコメントを一行書くことで対処しました。念のため

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

coverage ちゃんと取れてるっぽいです💪

elseif has_callback && success
call s:_resolve(a:promise, Result)
elseif !success
call s:_reject(a:promise, Err)

This comment has been minimized.

@lambdalisue

lambdalisue Dec 25, 2017
Member

提案
lint なので強制力は無いですが、出ないほうが望ましいと思います。
特に Result と合わせる必要もないので Err の代わりに v:exception を直接使えばいいかなという気がします

" expression by **reference**.
call map(
\ copy(a:promises),
\ {i, p -> p.then({v -> wait_group.notify_done(i, v)}, a:reject)},

This comment has been minimized.

@lambdalisue

lambdalisue Dec 25, 2017
Member

確認
maplambda 式を使うと 1.5 倍ほど遅くなりますがよろしいでしょうか?

https://gist.github.com/lambdalisue/7cbaa0ece9496ec8052f5a86d33d8f17#file-test-vim

ただ最速の expr タイプはわかりにくくなるので all で想定する promises 数がそんなに多くないのであれば許容範囲だと思います。

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

知りませんでした… 文字列版も検討してみます.ここは all() の引数なので,そこまで要素数が多くなることを想定していませんが,それでも速いに越したことは無いですし.

function! s:_race(promises, resolve, reject) abort
for p in a:promises
call p.then(a:resolve, a:reject)
endfor

This comment has been minimized.

@lambdalisue

lambdalisue Dec 25, 2017
Member

確認
ここも同様に map(..., 'expr') タイプの方が 1.5 倍ほど早いですがよろしいでしょうか?

https://gist.github.com/lambdalisue/7cbaa0ece9496ec8052f5a86d33d8f17#file-test-vim

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

ベンチマークを測った上で検討してみます.正直,.then のオーバーヘッドのほうが大きくてほぼ影響ないんじゃないかという気もしています.

This comment has been minimized.

@lambdalisue

lambdalisue Dec 26, 2017
Member

上記のベンチマークは 100000 回のループなので無視しても良いと思っています。
そのため「確認」としました

endif
return child
endfunction
let s:PROMISE.then = function('s:_promise_then')

This comment has been minimized.

@lambdalisue

lambdalisue Dec 25, 2017
Member

質問
辞書に対して直接関数定義を行わないで、一度スクリプト関数として定義してから functionfuncref 化して辞書に代入しているのには何か理由があるのでしょうか?

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

ここは以前ツイッターで @haya14busa さんに指摘されたのですが,辞書関数だとエラーが出た時のコールバックの関数名が数字になってしまってデバッグが困難になるためです.

This comment has been minimized.

@lambdalisue

lambdalisue Dec 26, 2017
Member

なるほど納得。でもデバッグの事情の為にこういうハックするのは少しモニョりますね。今回のケースだと非同期なのでデバッグの簡潔さを重要視した方が良いですが‥‥

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

コードが若干複雑になってしまうのは確かにあると思います.

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

実は vital core でもやってます

let s:Vital.import = s:_function('s:import')

書くの面倒だという以外はいいことしかないと思ってる

" States of promise
let s:PENDING = 0
let s:FULFILLED = 1
let s:REJECTED = 2

This comment has been minimized.

@lambdalisue

lambdalisue Dec 25, 2017
Member

提案
ECMA script では Promise.race()で Promise の状態取得が出来ますが Vim script は関数呼び出しのオーバーヘッドが大きいので上記の定数を s:_vital_created(module) を使ってモジュール定数として定義し s:PROMISE._state を公開アトリビュートとする(必要であれば lockvar も?)のはどうでしょうか?

" 使用感イメージ
let p1 = Promise.resolve()
let p2 = Promise.reject('foobar')
let p3 = Promise.new({ resolve -> execute('echo "Do nothing"') })

echo p1.state == Promise.FULFILLED
echo p2.state == Promise.REJECTED
echo p3.state == Promise.PENDING

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

すみません,#526 に1文だけ書いてこの PR には書いていなかったのですが,この PR は ES6 の Promise の API の実装にとどめる形にさせてください.理由は,

  • bluebird のような強力な API を実装することも考えられるため,今入れたい API を言い始めるとキリが無さそう
  • まずは最小の状態で master にマージして実戦投入して,real world で本当に必要な API だけを足したい

ためです.PR の本文にも追記しておきます💪


なのでここからは一応オフトピですが,せっかく提案いただいたので僕の考えを書いておきます.

一般的に,非同期処理で実行の状態を見て処理を分岐するのはバッドプラクティスだと思います.例えば JavaScript には VM 上で他の非同期に走っているコンテキストの状態を取る手段はありませんし,Go ではあえてゴルーチン(coroutine)の状態や ID,今自分がどのゴルーチン(コンテキスト)で実行されているかを取る API を提供していません.
また,飽くまで僕自身の経験としてですが,Promise の状態を取りたいと思ったことが無いというのもあります.

ただ,bluebird のように Promise の 3rd party 実装では Promise の状態を同期的に introspection できるものもあるので,real world で本当に必要なユースケースが発生した時に入れることを考えて議論するのが良いかなと思います.

This comment has been minimized.

@lambdalisue

lambdalisue Dec 26, 2017
Member

ご説明ありがとうございます。納得しかない

call timer_start(0, function('s:_publish', [a:promise]))
endfunction

function! s:_resolve_one(index, value) dict abort

This comment has been minimized.

@lambdalisue

lambdalisue Dec 25, 2017
Member

提案
辞書関数ではなく wait_group インスタンスを第一引数に持つ通常関数にしませんか?以下理由

  1. wait_group の定義位置と離れているので self が何を指しているのかパッと理解できない
  2. Vim script だと . によるアトリビュート検索も結構遅かった気がするので partial で変数化して Notify(i, v) みたいに呼び出したほうが繰り返し時に早そう(パフォーマンス測定はしていません)

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

なるほど.特に 1. はその通りですし,ここで辞書関数にする必要は実際無さそうなので普通の関数にします.ありがとうございます.

endfunction

function! s:_race(promises, resolve, reject) abort
for p in a:promises

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

p は Funcref になり得るので l:P がいいと思います

This comment has been minimized.

@lambdalisue

lambdalisue Dec 26, 2017
Member

あれ ps:PROMISE オブジェクトのコピー(インスタンス)な気がします。

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

@lambdalisue さんのおっしゃる通り,p は dict なので小文字で大丈夫です.

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

あ、確かに。了解です。

endfunction

function! s:_resolve(promise, ...) abort
let Result = a:0 > 0 ? a:1 : v:null

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

同名のグローバル関数が定義されていた場合にエラーにならないように l:Result がいいと思います

let success = 1
if has_callback
try
let Result = a:callback(a:result)

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

同名のグローバル関数が定義されていた場合にエラーにならないように l:Result がいいと思います

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

同名のグローバル関数があってもローカル変数が優先されてエラーにならないようにみえます.7.4 だとそうなるのでしょうか?

https://wandbox.org/permlink/CGv0HxAPsALNt0yk

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

7.4.729 でも試してみましたが結果は上のものと変わりませんでした

try
let Result = a:callback(a:result)
catch
let Err = v:exception

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

同名のグローバル関数が定義されていた場合にエラーにならないように l:Err がいいと思います

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

(コメント書いた diff が outdated で閉じちゃったのでこちらにも)

同名のグローバル関数があってもローカル変数が優先されてエラーにならないようにみえます.7.4.729 までは同じ挙動のようなのですが,もっと古いものだとそうなったりするのでしょうか?

https://wandbox.org/permlink/CGv0HxAPsALNt0yk

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

これだとアウトです(参照ではなく let での定義時に l: が付いてないとエラー)
https://wandbox.org/permlink/vubuQFFNBkoSp47g

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

v:exeption は関数ではないので関係ないのでは? Result は関数になりうりそうなので l: つけておいてもよさそう。

あと関連して Err が funcref にならないなら大文字にする必要はないとおもいます. 逆に funcref なら大文字かつ l: はつけるべき。

This comment has been minimized.

@rhysd

rhysd Dec 27, 2017
Author Contributor

@tyru なるほど…ありがとうございます.そういうことでしたか.

@haya14busa 確かに例外として投げられるのは文字列なので Err のほうは err で大丈夫そうですね.誤解生みそうなので修正します

let parent = self
let state = parent._state
let child = s:new(s:NOOP)
let Res = get(a:000, 0, v:null)

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

同名のグローバル関数が定義されていた場合にエラーにならないように l:Res がいいと思います

\ Promise.new({resolve -> timer_start(100, {-> resolve('second')})}),
\])
\.then({v -> execute('echo ' . v)})
\.catch({e -> execute('echo ' . e)})

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

execute('echo e')execute('echo ' . string(e)) ?

\ Promise.new({resolve -> timer_start(50, {-> execute('throw "ERROR!"')})}),
\ Promise.new({resolve -> timer_start(100, {-> resolve('second')})}),
\])
\.then({v -> execute('echo ' . v)})

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

execute('echo v')execute('echo ' . string(v)) ?

\ Promise.new({resolve -> timer_start(50, {-> resolve('first')})}),
\ Promise.new({resolve -> timer_start(100, {-> resolve('second')})}),
\])
\.then({v -> execute('echo ' . v)})

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

execute('echo v')execute('echo ' . string(v)) ?

" Outputs '42'
Promise.resolve(Promise.reject('ERROR!'))
\.catch({reason -> execute('echo ' . reason)})

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

execute('echo reason')execute('echo ' . string(reason)) ?

\]
\)
\.then({-> exeute('echom "All repositories were successfully cloned!"', '')})
\.catch({err -> execute('echom "Failed to clone: " . ' string(err), '')})

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

. が足らないです。
execute('echom "Failed to clone: " . ' . string(err), '')

これでもいいかも?
execute('echom "Failed to clone:" ' . string(err), '')

@lambdalisue
Copy link
Member

@lambdalisue lambdalisue commented Dec 26, 2017

僕はフィードバックの内容と今後の変更内容(予定)に特に意見はないので Approve にしておきます。

\ code ? reject(buf.err) : resolve(buf.out)
\ },
\ })})
endfunction

This comment has been minimized.

@tyru

tyru Dec 26, 2017
Member

自分も詳しく job の仕様を把握してませんが、試した限りだと exit_cbclose_cb よりも後に実行されるとは限らないようです。
https://gist.github.com/tyru/0ffee5ba2b8cab7390f2652c9a97bc24
上記の結果 (messages) だと exit_cb のが先に実行されてしまっているため、 buf が空になってしまっています。
close_cb のみ指定して s:read_to_buf() に resolve してもらうとかですかね? (それでいいという確証はないです…)

This comment has been minimized.

@rhysd

rhysd Dec 26, 2017
Author Contributor

私の環境だとコールバックの発火順は exit_cb のほうが後なので,どうやらプラットフォーム依存ですね…ちなみに close_cb だけだと exit status が分からないのでコマンドが失敗したかどうか分からないです.

ちゃんとやるならどちらの順序で発火しても動くようにすべきですが,それをやると example としてイマイチになってしまう気がします(本質的でないコードが増えすぎる).なにか良い修正案とかないでしょうか… @lambdalisue @thinca @mattn

This comment has been minimized.

@lambdalisue

lambdalisue Dec 26, 2017
Member

プロセスの終了とチャネルのクローズはそもそも別の物なので例として不適かと思います。
プロセスが終了した後に全ての出力が欲しいのであれば、オプションに 'drop': 'never' を渡してチャネルから ch_read で持ってくるのが良いかと思います

This comment has been minimized.

@lambdalisue

lambdalisue Dec 26, 2017
Member

あと、プロセスの終了はどうでもよくて出力が全部欲しい場合は Vim のヘルプにある通り close_cb でやるべきですかね。
http://vim-jp.org/vimdoc-ja/channel.html#read-in-close-cb

個人的には実用面で言えば「プロセスの出力結果がほしい」が要求であって「プロセスが終了してから出力結果がほしい」ではないと思います。なので例としては、こちらのほうが適切かと思います。

This comment has been minimized.

@mattn

mattn Dec 26, 2017
Member

すでに喋るべき内容は語られていた。

This comment has been minimized.

@rhysd

rhysd Dec 27, 2017
Author Contributor

@lambdalisue @tyru

お手数ですが,これで動いているか確認いただけませんでしょうか?
自分の環境では元のコードも動いてしまっていたので,これで問題が解決したのかどうかが確認できず…

let s:Promise = vital#vital#import('Async.Promise')

function! s:read(chan, part) abort
  let out = ''
  while ch_status(a:chan, {'part' : a:part}) ==# 'buffered'
    let out .= ch_read(a:chan, {'part' : a:part})
  endwhile
  return out
endfunction

function! s:sh(...) abort
  let cmd = join(a:000, ' ')
  let buf = {}
  return s:Promise.new({resolve, reject -> job_start(cmd, {
        \   'drop' : 'never',
        \   'close_cb' : {ch -> 'do nothing' },
        \   'exit_cb' : {ch, code ->
        \     code ?
        \       reject(s:read(ch, 'err')) :
        \       resolve(s:read(ch, 'out'))
        \   },
        \ })})
endfunction

call s:sh('ls', '-l')
      \.then({out -> execute('echom ' . string('Output: ' . out), '')})
      \.catch({err -> execute('echom ' . string('Error: ' . err), '')})

ちなみに,close_cb は指定しないと job の開始に失敗してしまいました

This comment has been minimized.

@tyru

tyru Dec 27, 2017
Member

@rhysd 動きました。ただいくつか修正した方が良さそうな点がありました。

  1. open の場合も考慮する
    • ch_status(a:chan, {'part' : a:part}) =~# 'open\|buffered'
  2. 末尾に改行入れる
    • let out .= ch_read(a:chan, {'part' : a:part}) . "\n"
  3. :echom だと改行を含む文字列の場合つらい感じになるので :echo のがいいかもです (最後の .then().catch() の中)

ちなみに,close_cb は指定しないと job の開始に失敗してしまいました

これ自分の環境 (WSL) でも再現しました。詳しく追ってないですが Error: みたいに err が空文字で渡ってきちゃいますね。

This comment has been minimized.

@tyru

tyru Dec 27, 2017
Member

Twitter でも言いましたが、このコードだとエラーにならなかったです。
もしかして job 以外の所でコケて .catch() に来てるかも?
https://gist.github.com/tyru/4707726873f3e4955f331109f9dc0734

This comment has been minimized.

@rhysd

rhysd Dec 27, 2017
Author Contributor

私の手元で確認した限りでは channel status が fail になっていたので job の開始に失敗しているんじゃないかと思います.他の箇所で例外が起きているなら err が空になるのは(Promise の実装がバグってない限り)おかしいですね

This comment has been minimized.

@lambdalisue

lambdalisue Dec 27, 2017
Member

動きました。修正点としては @tyru さんと同意見で、以下がいいかなと思います。

let s:Promise = vital#vital#import('Async.Promise')

function! s:read(ch, part) abort
  let out = []
  while ch_status(a:ch, {'part' : a:part}) =~# '^\%(open\|buffered\)$'
    call add(out, ch_read(a:ch, {'part': a:part}))
  endwhile
  return join(out, "\n")
endfunction

function! s:sh(...) abort
  let cmd = join(a:000, ' ')
  return s:Promise.new({resolve, reject -> job_start(cmd, {
        \   'drop' : 'never',
        \   'close_cb' : {ch -> 'do nothing' },
        \   'exit_cb' : {ch, code ->
        \     code ?
        \       reject(s:read(ch, 'err')) :
        \       resolve(s:read(ch, 'out'))
        \   },
        \ })})
endfunction

call s:sh('ls', '-l')
      \.then({out -> execute('echo ' . string('Output: ' . out), '')})
      \.catch({err -> execute('echo ' . string('Error: ' . err), '')})

あと s:sh('cat', '/foo/bar') でちゃんと Error: cat: /foo/bar: No such file or directory が出ました。

環境: macOS High Sierra

@rhysd
Copy link
Contributor Author

@rhysd rhysd commented Dec 26, 2017

race()all() の内部実装を map()(述語部を eval 文字列にする)にして,下記のようなスクリプトで1000回実行時間の平均を取ってみました.

let P = vital#vital#import('Async.Promise')

let ps = map(range(1000), {i -> g:P.new({res -> timer_start(i, res)})})

" race()
let amount = 0.0
let total = 10
let i = 0
while i < 10
    let stamp = reltime()
    call P.race(ps)
    let amount += str2float(reltimestr(reltime(stamp)))
    let i += 1
endwhile

echom 'amount: ' . string(amount)
echom 'avg: ' . string(amount / 10.0)

" all()
let amount = 0.0
let total = 10
let i = 0
while i < 10
    let stamp = reltime()
    call P.all(ps)
    let amount += str2float(reltimestr(reltime(stamp)))
    let i += 1
endwhile

echom 'amount: ' . string(amount)
echom 'avg: ' . string(amount / 10.0)

race()

  • 従来実装(for ループ)
amount: 0.487715
avg: 0.048772
  • 新実装(map(copy(...), '...')
amount: 0.469726
avg: 0.046973

Result: 1.04x faster

all()

  • 従来実装(map とラムダ式)
amount: 0.543475
avg: 0.054348
  • 新実装(map と eval 文字列述語)
amount: 0.503912
avg: 0.050391

Result: 1.08x faster


1000要素でこれなので,正直誤差範囲かなと思いますがどうでしょう

@lambdalisue
Copy link
Member

@lambdalisue lambdalisue commented Dec 26, 2017

1000要素でこれなので,正直誤差範囲かなと思いますがどうでしょう

僕のベンチでも 100000 回とかでやっと差が出てる感じなので把握した上で lambda はありだと思います 👍

@rhysd
Copy link
Contributor Author

@rhysd rhysd commented Dec 26, 2017

@lambdalisue 了解です.もし今後実際に使ってみてそこで問題があることが分かったらその時に対処しましょう👍

@rhysd
Copy link
Contributor Author

@rhysd rhysd commented Dec 26, 2017

execute() の第2引数に時間を吸われたので @tyru さんにいただいている残りの指摘箇所は明日修正します.

endif
return child
endfunction
let s:PROMISE.then = function('s:_promise_then')

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

実は vital core でもやってます

let s:Vital.import = s:_function('s:import')

書くの面倒だという以外はいいことしかないと思ってる

try
let Result = a:callback(a:result)
catch
let Err = v:exception

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

v:exeption は関数ではないので関係ないのでは? Result は関数になりうりそうなので l: つけておいてもよさそう。

あと関連して Err が funcref にならないなら大文字にする必要はないとおもいます. 逆に funcref なら大文字かつ l: はつけるべき。

try
let Result = a:callback(a:result)
catch
let Err = v:exception

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

これだとスタックトレースの情報 (v:throwpoint) が消えてしまうのでエラーオブジェクト作って v:exeption と v:throwpoint 両方持たせておいたほうがよさそう?

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

[追記補足] これ"よさそう?"って微妙なカンジで最初は書いてましたが、help の github_issues の例とか実は僕も動かなくてデバックしてて、スタックトレースだしてもムズい(lambdaをおえない...)のにエラーだけだとほんとによくわからないエラーしかでないことがあるので、真剣に検討してもいいかなぁと思います。

This comment has been minimized.

@rhysd

rhysd Dec 27, 2017
Author Contributor

うーん,それはエラーオブジェクトを定義する必要が出てしまいますね…例外が起きたら v:exception だけでなくて,v:throwpoint も結合させた文字列で reject するとかどうでしょう?

This comment has been minimized.

@lambdalisue

lambdalisue Dec 27, 2017
Member

僕は @haya14busa さんのいうエラーオブジェクトのほうが良いかと思います。
分離されたデータをライブラリに勝手に結合されてイラッとした経験結構多いので……(例: stdoutstderr を勝手に結合するやつとか)

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

{ 'message': v:exception [, 'throwpoint': v:throwpoint]} くらいでどうでしょうか. 文字列結合はできるなら避けたい

endfunction

function! s:is_available() abort
return has('nvim') || v:version >= 800

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

nvim よく追えてないですが lambda は割りと最近入ってるはずなのでチェックとしてどれくらい機能してるか微妙かも?
has('lambda') とかのほうがいいかなと思います. あとは v:t_timer_startも使ってはいるので 8 以上っていうのは残してもよいかな?

This comment has been minimized.

@rhysd

rhysd Dec 27, 2017
Author Contributor

うーん,なるほど.確かにそのとおりですね.僕も具体的にどのバージョンかまでは把握していないです… 機能ベースで言うと,has('lambda') && has('timers') のほうが良さそうですね

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

+1 to has('lambda') && has('timers') .

一般的にも機能ベースでチェックして、それでもできないならバージョンでチェックするというのがいいと思います

be |Funcref| and they are guaranteed to be called __asynchronously__.
>
echo 'hi'
Promise.new({resolve -> execute('echo "halo"') || resolve(42)})

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

上下に echoあって実行できそうにみえるし call Promise or call s:Promise にしておいてもよさそう。
この help 全体的に call なしで Promise.new.... としているけどわざとですかね?
せっかくなら call つけてほぼそのまま実行できるくらいにしておいてもよいかなと思いますがどっちでもよさそう

あと execute('...', '') にしないとこの結果を確認できないので直しておいてあげると読者に優しそうですかね。

This comment has been minimized.

@rhysd

rhysd Dec 27, 2017
Author Contributor

はい,第2引数は各所で忘れていたので修正します…ありがとうございます. :call も追加します

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

[補足] 面倒そうなので一度 s:echom() とか定義してもいいかなともちょっと感じましたがおまかせします

\ s:sh('git', 'clone', 'https://github.com/rhysd/clever-f.vim.git'),
\]
\)
\.then({-> exeute('echom "All repositories were successfully cloned!"', '')})

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

[nit] s/exeute/execute/

This comment has been minimized.

@rhysd

rhysd Dec 27, 2017
Author Contributor

nice catch です.misspell さん頼む…

let s:Promise = vital#vital#import('Async.Promise')
function! s:wait(ms)
return s:Promise.new({resolve -> timer_start(a:ms)})

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

return s:Promise.new({resolve -> timer_start(a:ms, resolve)}) ですかね。 resolve できてない

This comment has been minimized.

@rhysd

rhysd Dec 27, 2017
Author Contributor

その通りです.ありがとうございます.

function! s:_notify_done(wg, index, value) abort
let a:wg.done[a:index] = a:value
let a:wg.resolved += 1
if a:wg.resolved == a:wg.total

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

[nit] total じゃなくて remaining にしてデクリメントしていって0ならresolveってすると一つ変数を減らせる説 (total, resolved -> remaining)

(https://github.com/stefanpenner/es6-promise/blob/97478eb6fcd628e0d679438c88bd64b5079a9122/lib/es6-promise/enumerator.js#L95 らへんとかをみた)

This comment has been minimized.

@rhysd

rhysd Dec 27, 2017
Author Contributor

あー,確かに.キーの数は減らしたいのでそうしてみます.

endif

let wait_group = {
\ 'done': repeat([v:null], total),

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

[nit] results とかのほうがわかりやすい気がしますがどうでしょう? done って done フラグ感がある(Go脳かもしれない) これもes6-promise では results だった


function! s:resolve(...) abort
let promise = s:new(s:NOOP)
call s:_resolve(promise, a:0 > 0 ? a:1 : v:null)

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

[nit] get(a:000, 0, v:null) とか他のところで使ってるし書き方統一してもよさそう。

(個人的には get(a:, 1, v:null) が好みだけど統一されてたらなんでもok)

This comment has been minimized.

@rhysd

rhysd Dec 28, 2017
Author Contributor

個人的な好みにより a:0 になりました

This comment has been minimized.

@haya14busa

haya14busa Dec 31, 2017
Member

[感想] 自分がいちばん好みじゃないの来た...! (勿論直してほしいとかではなくて単なる感想で、言い分としてはa:0は考えることが増えるのと(a:0が数でぇ...+1を見に行く...)、ほかの get or default イディオムと合わせたいので自分は get(a:, n, default) を使ってる (g: とかと一緒のイディオムにする)

let a:parent._fulfillments += [ a:on_fulfilled ]
let a:parent._rejections += [ a:on_rejected ]
if is_empty && a:parent._state > s:PENDING
call timer_start(0, function('s:_publish', [a:parent]))

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

[optional] call timer_start(0, ...) っていうイディオム関数化しておいてもよさそう? asap とか next_tick とか?
読者が暗黙に timer_start(0,...) がどういう意味かわかってるという仮定をおいてるけど関数化されててコメントもあったりすると可読性よさそう

This comment has been minimized.

@lambdalisue

lambdalisue Dec 27, 2017
Member

とても気持ちはわかるのですが

  1. ユーザー定義関数のオーバーヘッドが大きい
  2. 無駄にコールスタックを積んでしまう

の二点から今のままが良いと思います。

#567 (comment)

と同じで Vim script の事情から本来不要なハックや最適化が必要になるの少しモニョりますが、仕方ない……

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

その最適化が本当に必要な箇所かどうかが結構あやしいとは思いますが(2はなんかchainしすぎるとダメとか見た気がするのでわからんでもない), まぁoptionalですね。

This comment has been minimized.

@lambdalisue

lambdalisue Dec 27, 2017
Member

はい。ここがユーザーが触る場所ならば僕も関数でラップする事には同意なのですが、ライブラリ内部のコードで(個人の主観としては)ほぼイディオム的な使い方なのでラップする必要が薄いと思います。

This comment has been minimized.

@rhysd

rhysd Dec 27, 2017
Author Contributor

提案ありがとうございます.個人的な感覚では timer_start(0, ...) のままで良いかなと思います.0秒のタイマーでも意味は通じると思いますし,ラップして関数1つ増やす保守コストに見合うほどではないなと思いました.

JavaScript では確かに asap とか next_tick みたいにラップすることが多いのですが,それは環境によって下が setImmediate() だったり setTimeout() だったりするからなのですよね(使えるなら setImmediate のほうが良いため)

elseif settled == s:REJECTED
let l:CB = a:promise._rejections[i]
else
throw 'vital: Async.Promise: Cannot publish a pending promise'

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

このチェックはfor loop の外で settled 変数に入れたあと or 前くらいがよさそう

This comment has been minimized.

@rhysd

rhysd Dec 27, 2017
Author Contributor

publish の呼ばれ方的にこのパスが呼ばれることがない(防衛的に入れてた)ので気づきませんでしたが,確かにそうですね.ループ前にチェックするようにします.


It should create with default value(=v:null) when no argument is given to resolve()/reject()
let l = l:
call P.new({resolve -> resolve()}).then({x -> extend(l, {'done' : x})})

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

[nit][optional] 実装のほうにも似たコメント書きましたが done より result 感がある?

This comment has been minimized.

@rhysd

rhysd Dec 28, 2017
Author Contributor

確かに.フラグをセットするんじゃなくて値をセットしてるとこは result にしといたほうが値の意図が明確で良さそうです

Assert Equals(p._state, PENDING)
let p = p.then({-> extend(l, {'done' : 1})})
Assert Equals(p._state, PENDING)
Assert Equals(done, 0)

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

あってるので別によさそうだけど, call s:wait_has_key(l, 'done') してあげないとフェアじゃないかも?
...と思ったけど無駄にunit test が長くなるのもアレだしムズカシイ

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

(見直すとそもそも wait_has_key は done すでに定義してるので使えなかった.... ので sleep 1ms とかするとフェア感?(本当か?)

This comment has been minimized.

@rhysd

rhysd Dec 28, 2017
Author Contributor

ここは待たなくても timer_start でキューイングされた処理は後のどこかで無害な感じで実行されて終わるだけなので,待たなくて良いかなと

This comment has been minimized.

@rhysd

rhysd Dec 30, 2017
Author Contributor

あーすみません,このコメントの意図を読み違えてました..then がタイマーを挟むようになった今,確かにある程度待たないと .then が発火していないことを確認するのにはこれでは不十分でした.ちなみに .then は永遠に発火しないので,done が定義済みかによらず wait_has_key は使えないです.

let p = P.new({resolve -> resolve(Val)})
Assert Equals(p._state, FULFILLED)
let p2 = p.then({x -> x}).then({r -> extend(l, {'Done' : r})})
call s:wait_has_key(l, 'Done')

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

wait_has_key の前でまだ Done には値入ってないことを確認すると "asynchronously" なことがわかりやすくてよさそう

for l:Val in [42, 'foo', {'foo': 42}, {}, [1, 2, 3], [], function('empty')]
let p = P.new({_, reject -> reject(Val)})
let p2 = p.then({-> extend(l, {'Done' : 'Error: resolved to .then'})}).catch({v -> extend(l, {'Done' : v})})
call s:wait_has_key(l, 'Done')

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

ここも wait_has_key のまえにチェック入れたほうが asynchronouslyのテストであることがわかりやすくてよさそう?

Assert Equals(done, 42)
End

It can take rejection handler at 2nd parameter

This comment has been minimized.

@haya14busa

haya14busa Dec 27, 2017
Member

このテスト catch 使ってないし Describe .then のとこにあったほうがよさそう

This comment has been minimized.

@rhysd

rhysd Dec 28, 2017
Author Contributor

これは .then のとこに置くべきなのを間違えてますね…ありがとうございます!

@lambdalisue
Copy link
Member

@lambdalisue lambdalisue commented Dec 27, 2017

今気が付きましたが 8f6b866 は Neovim を捨てることになります。

Neovim v0.2.3-240-g6ff13d (ほぼ現在の HEAD) で v:t_none はサポートされていません。いかがサポートされている値です。

v:t_bool
v:t_dict
v:t_float
v:t_list
v:t_number
v:t_string

この辺のサポートは結構あとだった気がするので type() を使っていただけないと Neovim では使えなくなってしまいます...

@rhysd
Copy link
Contributor Author

@rhysd rhysd commented Dec 27, 2017

@lambdalisue うお,マジですか.8f6b866 に強い動機があるわけではないため,削除しようと思います.ご指摘ありがとうございます.

@rhysd rhysd force-pushed the rhysd:promise-review branch from 45cea5d to 337e8c4 Dec 27, 2017
@rhysd
Copy link
Contributor Author

@rhysd rhysd commented Dec 27, 2017

8f6b866 を削除するために push -f しました.

たくさんのレビューありがとうございます.2時を回ったので,今日はこのあたり( #567 (comment) )にしてまた明日の夜に修正予定です.

目下のところ,議論すべきは .catchv:throwpoint が取れない点ですね.

endfunction

function! s:_subscribe(parent, child, on_fulfilled, on_rejected) abort
let is_empty = empty(a:parent._children)

This comment has been minimized.

@vim-jp-bot

vim-jp-bot Dec 28, 2017

[vimlint] reported by reviewdog 🐶
EVL102: unused variable l:is_empty

@rhysd
Copy link
Contributor Author

@rhysd rhysd commented Dec 28, 2017

Discussion point: Error Handling

Problem:

call s:P.new({-> execute('throw "oops"')})
  \.catch({err -> execute('echo err', '')})

We can refer v:exception which was thrown in preceding executions, but v:throwpoint was lost. I want to discuss how to solve this problem.

1. Concat v:exception and v:throwpoint

Use v:exception . "\n\n" . v:throwpoint instead of v:exception

Output:

oops

function <SNR>140_new[5]..<lambda>1, line 1

Pros: We don't need to define our own exception object
Cons: Parsing is required to retrieve error message

2. Use exception {'exception' : v:exception, 'throwpoint' : v:throwpoint} object only when exception is thrown

Wrapping v:exception and v:throwpoint with dict to preserve all information

Output:

{'throwpoint': 'function <SNR>183_new[5]..<lambda>3, line 1', 'exception': 'oops'}

Pros: Easy to access to retrieve error message (and throwpoint)
Cons: We need to define our own dict. Learning cost may be a bit up

3. Always use reason {'message' : xxx, 'stack' : ...} object for rejected Promises' values

When any value is rejected, wrap the value with dict (type is {message: any, stack?: string}). When it is an exception thrown in preceding execution, add v:throwpoint as stack field.

call s:P.new({-> execute('throw "oops"')})
  \.catch({err -> execute('echo err', '')})

call s:P.new({_, reject -> reject('hello')})
  \.catch({err -> execute('echo err', '')})

Output:

{'stack': 'function <SNR>183_new[5]..<lambda>3, line 1', 'message': 'oops'}
{'message': 'hello'}

Pros: We can handle rejected Promises' values in persistent way
Cons: We always need to dereference the value by accessing to .message (it may not be string) field

僕的には reject に手軽に好きな値が使えて例外時にも情報量が減らない 2. が良いかなと思うのですが,どうでしょう?

Copy link
Member

@thinca thinca left a comment

テストまで見ました。

be |Funcref| and they are guaranteed to be called __asynchronously__.
>
echo 'hi'
call Promise.new({resolve -> execute('echo "halo"', '') || resolve(42)})

This comment has been minimized.

@thinca

thinca Dec 31, 2017
Member

この書き方たと、例えば echo "halo" の部分が echon "1halo" だと || の左側が TRUE になるので resolve(42) が実行されません。つまりたまたまうまく動くだけのように見えます…。
そうと知らずにこの例をコピーして使う人が出てくることを危惧しています。

- Operation has completed successfully
- Operation has failed with an error

{promise}.then([{onResolved} [, {onRejected}]])

This comment has been minimized.

@thinca

thinca Dec 31, 2017
Member

なるほど。この help 内でも Fulfilled Promise のようなワードがあったので、resolve の方が表記が揺れなくてわかりやすそうという意図なら、これらも Resolved Promise にするのがいいかも?
とは言え私もそこまで強い意志はないです。意図してないうっかりだったらアレなので念のため確認くらいの気持ちでした。

function! s:wait_has_key(obj, name) abort
" if has_key(a:obj, a:name)
" throw printf('s:wait_has_key(): Key "%s" is already in object %s', a:name, a:obj)
" endif

This comment has been minimized.

@thinca

thinca Dec 31, 2017
Member

このコメントは必要なやつでしょうか?

This comment has been minimized.

@rhysd

rhysd Dec 31, 2017
Author Contributor

必要ないやつでした…

let l = l:
call P.new({resolve -> resolve()}).then({x -> extend(l, {'result' : x})})
call s:wait_has_key(l, 'result')
" :Assert Equal() does not support v:null by themis.vim

This comment has been minimized.

@thinca

thinca Dec 31, 2017
Member

一応 master に入ってる最新版 themis.vim ならいけるはずです(が、別に無理にしなくて OK)。

This comment has been minimized.

@rhysd

rhysd Dec 31, 2017
Author Contributor

おっと,そうですね.rebase して最新の themis を使うようにします.

End

It should make settled Promise with v:null when no argument is given to resolve()/reject()
let l = l:

This comment has been minimized.

@thinca

thinca Dec 31, 2017
Member

私が言い出しておいてアレですが、この使い方なら無理に l: 使わなくても新規の辞書 {} でも良さそう…。(その場合は Assert の部分が l.result になる)

This comment has been minimized.

@rhysd

rhysd Dec 31, 2017
Author Contributor

分かる… うーん,ただ l. 付けなくて良い利点はあるんですよね.特に理由が無ければこのままでも良いかなぁと

This comment has been minimized.

@thinca

thinca Jan 1, 2018
Member

ハック感が抜ける(…までは行かなくても薄れる)メリットはありそうですが、全部直すの面倒なのも同意なのでこのままでも OK です 🙆

End
End

Describe .then()

This comment has been minimized.

@thinca

thinca Dec 31, 2017
Member

.then().catch() は Promise オブジェクトのメソッド(モジュール直下の関数ではない)ので、別途 Describe Promise object 的なブロックに入れるのが良さそう。

End
End

Describe .reject()

This comment has been minimized.

@thinca

thinca Dec 31, 2017
Member

.reject() に resolved Promise を渡した場合のテストがほしいなー。

Copy link
Member

@thinca thinca left a comment

間に合わなかった!
些末なコメントをしましたが特筆すべきことは特にありませんでした。
改めて実装ありがとうございます!素晴らしいです 👏

let s:REJECTED = 2

let s:DICT_T = type({})
let s:NULL_T = type(v:null)

This comment has been minimized.

@thinca

thinca Dec 31, 2017
Member

これわざわざ type() 使ってますが、仕様上 v:null を使うことになっているので foo is v:null なチェックでも良さそうな気がします。そうすれば左辺値に type() を適用する必要がなくなるのでほんの少し速くなるかも。

This comment has been minimized.

@rhysd

rhysd Dec 31, 2017
Author Contributor

確かにわざわざ型の ID を見ているのは冗長ですね.ありがとうございます,直します.

call s:_fulfill(a:promise, Result)
elseif a:settled == s:REJECTED
call s:_reject(a:promise, Result)
endif

This comment has been minimized.

@thinca

thinca Dec 31, 2017
Member

💭 [IMO]
ちょっと読んでて混乱しました。
最初の pending の判定を除くと、上2つが has_callbackTRUE の場合、下 2 つが has_callbackFALSE の場合なので、L43 の if の中にそれぞれ書いてしまっても良いかも。
その場合 pending の判定が重複しちゃうのでどちらが良いかむずいですが… 中に入れてしまえば L54 の不要な代入も自然に減らせそうな気がします。

This comment has been minimized.

@rhysd

rhysd Dec 31, 2017
Author Contributor

確かにここはちょっと冗長かつ微妙な実装になっているのですが,意図としてはコールバックの発火処理と次の(子供の)Promise へチェーンする処理を分離する実装になっています.

a:promise._state != s:PENDING をダブらせて上の if の中で判定するというのも考えたのですが,コールバック実行部分とチェーンの続きを発火する部分が混ざると,それはそれで結構読みづらい印象でした.

Copy link
Member

@haya14busa haya14busa left a comment

こちらからは激LGTM 👍

rhysd added 20 commits Jun 10, 2017
Discussion for ES6 Promise:
  promises-aplus/promises-spec#108

Standard ES6 Promise does not check self fulfillment. And in Vim script,
there is no way to refer itself in thenable object because Promise
object is copy of s:PROMISE.
because it is usually shorter and faster. (As long as I measured,
partial is 10% faster than lambda when some variable is captured.
Although previous code has no problem, but linter complains about it.
@rhysd rhysd force-pushed the rhysd:promise-review branch from 23e695d to fbed16a Dec 31, 2017
@rhysd
Copy link
Contributor Author

@rhysd rhysd commented Dec 31, 2017

themis.vim のアップデートの取り込みと fixup になっている箇所の適用のために push -f しました.

@thinca 明らかに直したほうが良さそうなところを修正しました.再度修正箇所とレビューコメントの確認をお願いできますでしょうか

@thinca
thinca approved these changes Jan 1, 2018
Copy link
Member

@thinca thinca left a comment

Approved

End

It should make settled Promise with v:null when no argument is given to resolve()/reject()
let l = l:

This comment has been minimized.

@thinca

thinca Jan 1, 2018
Member

ハック感が抜ける(…までは行かなくても薄れる)メリットはありそうですが、全部直すの面倒なのも同意なのでこのままでも OK です 🙆

@rhysd
Copy link
Contributor Author

@rhysd rhysd commented Jan 1, 2018

では,これでマージしようと思います.細かいところまでチェックいただいた皆さんありがとうございました🙏

@rhysd rhysd merged commit bee84ae into vim-jp:master Jan 1, 2018
5 checks passed
5 checks passed
codecov/patch 98.55% of diff hit (target 0%)
Details
codecov/project 78.89% (target 0%)
Details
continuous-integration/appveyor/pr AppVeyor build succeeded
Details
continuous-integration/drone the build was successful
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
@rhysd rhysd deleted the rhysd:promise-review branch Jan 1, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
7 participants
You can’t perform that action at this time.