-
Notifications
You must be signed in to change notification settings - Fork 64
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
Conversation
elseif has_callback && success | ||
call s:_resolve(a:promise, Result) | ||
elseif !success | ||
call s:_reject(a:promise, Err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[vimlint] reported by reviewdog 🐶
EVL104: variable may not be initialized on some execution path: l:Err
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
この警告は問題ないことを確認済みですが,出ないようにコード直したほうが良いでしょうか?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
提案
lint なので強制力は無いですが、出ないほうが望ましいと思います。
特に Result
と合わせる必要もないので Err
の代わりに v:exception
を直接使えばいいかなという気がします
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ですね,そうしておきます.ありがとうございます.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
v:exception
は catch
した節の中でしか使えないのを忘れていました…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mkj... 間違った情報すみません。
#526 からレビューしてくれそうな気配がある人を Reviewers にアサインしてみました.年末で忙しいところすみませんが,レビューいただけると助かります. |
🎉 来週までには見ます! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
とりあえず Promise.vim
だけ見ました。
いくつか「提案」と「確認」を書いたのでフィードバックお願いします。
|
||
" @vimlint(EVL103, 1, a:resolve) | ||
" @vimlint(EVL103, 1, a:reject) | ||
function! s:noop(resolve, reject) abort |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
メモ
すでに covimerage 側で修正されているかもしれませんが gina.vim に covimerage を導入した際に空っぽの関数があるとパースエラーが出ました。gina.vim では関数内部にコメントを一行書くことで対処しました。念のため
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coverage ちゃんと取れてるっぽいです💪
elseif has_callback && success | ||
call s:_resolve(a:promise, Result) | ||
elseif !success | ||
call s:_reject(a:promise, Err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
提案
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)}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確認
map
に lambda
式を使うと 1.5 倍ほど遅くなりますがよろしいでしょうか?
https://gist.github.com/lambdalisue/7cbaa0ece9496ec8052f5a86d33d8f17#file-test-vim
ただ最速の expr
タイプはわかりにくくなるので all
で想定する promises
数がそんなに多くないのであれば許容範囲だと思います。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
知りませんでした… 文字列版も検討してみます.ここは all()
の引数なので,そこまで要素数が多くなることを想定していませんが,それでも速いに越したことは無いですし.
function! s:_race(promises, resolve, reject) abort | ||
for p in a:promises | ||
call p.then(a:resolve, a:reject) | ||
endfor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確認
ここも同様に map(..., 'expr')
タイプの方が 1.5 倍ほど早いですがよろしいでしょうか?
https://gist.github.com/lambdalisue/7cbaa0ece9496ec8052f5a86d33d8f17#file-test-vim
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ベンチマークを測った上で検討してみます.正直,.then
のオーバーヘッドのほうが大きくてほぼ影響ないんじゃないかという気もしています.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
上記のベンチマークは 100000 回のループなので無視しても良いと思っています。
そのため「確認」としました
endif | ||
return child | ||
endfunction | ||
let s:PROMISE.then = function('s:_promise_then') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
質問
辞書に対して直接関数定義を行わないで、一度スクリプト関数として定義してから function
で funcref
化して辞書に代入しているのには何か理由があるのでしょうか?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ここは以前ツイッターで @haya14busa さんに指摘されたのですが,辞書関数だとエラーが出た時のコールバックの関数名が数字になってしまってデバッグが困難になるためです.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
なるほど納得。でもデバッグの事情の為にこういうハックするのは少しモニョりますね。今回のケースだと非同期なのでデバッグの簡潔さを重要視した方が良いですが‥‥
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
コードが若干複雑になってしまうのは確かにあると思います.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
実は vital core でもやってます
vital.vim/autoload/vital/vital.vim
Line 76 in e553b10
let s:Vital.import = s:_function('s:import') |
書くの面倒だという以外はいいことしかないと思ってる
" States of promise | ||
let s:PENDING = 0 | ||
let s:FULFILLED = 1 | ||
let s:REJECTED = 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
提案
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
すみません,#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 で本当に必要なユースケースが発生した時に入れることを考えて議論するのが良いかなと思います.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ご説明ありがとうございます。納得しかない
call timer_start(0, function('s:_publish', [a:promise])) | ||
endfunction | ||
|
||
function! s:_resolve_one(index, value) dict abort |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
提案
辞書関数ではなく wait_group
インスタンスを第一引数に持つ通常関数にしませんか?以下理由
wait_group
の定義位置と離れているのでself
が何を指しているのかパッと理解できない- Vim script だと
.
によるアトリビュート検索も結構遅かった気がするのでpartial
で変数化してNotify(i, v)
みたいに呼び出したほうが繰り返し時に早そう(パフォーマンス測定はしていません)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
なるほど.特に 1. はその通りですし,ここで辞書関数にする必要は実際無さそうなので普通の関数にします.ありがとうございます.
endfunction | ||
|
||
function! s:_race(promises, resolve, reject) abort | ||
for p in a:promises |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
p
は Funcref になり得るので l:P
がいいと思います
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
あれ p
は s:PROMISE
オブジェクトのコピー(インスタンス)な気がします。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lambdalisue さんのおっしゃる通り,p は dict なので小文字で大丈夫です.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
あ、確かに。了解です。
endfunction | ||
|
||
function! s:_resolve(promise, ...) abort | ||
let Result = a:0 > 0 ? a:1 : v:null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
同名のグローバル関数が定義されていた場合にエラーにならないように l:Result
がいいと思います
let success = 1 | ||
if has_callback | ||
try | ||
let Result = a:callback(a:result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
同名のグローバル関数が定義されていた場合にエラーにならないように l:Result
がいいと思います
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
同名のグローバル関数があってもローカル変数が優先されてエラーにならないようにみえます.7.4 だとそうなるのでしょうか?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
7.4.729 でも試してみましたが結果は上のものと変わりませんでした
try | ||
let Result = a:callback(a:result) | ||
catch | ||
let Err = v:exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
同名のグローバル関数が定義されていた場合にエラーにならないように l:Err
がいいと思います
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(コメント書いた diff が outdated で閉じちゃったのでこちらにも)
同名のグローバル関数があってもローカル変数が優先されてエラーにならないようにみえます.7.4.729 までは同じ挙動のようなのですが,もっと古いものだとそうなったりするのでしょうか?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
これだとアウトです(参照ではなく let
での定義時に l:
が付いてないとエラー)
https://wandbox.org/permlink/vubuQFFNBkoSp47g
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
v:exeption は関数ではないので関係ないのでは? Result は関数になりうりそうなので l:
つけておいてもよさそう。
あと関連して Err が funcref にならないなら大文字にする必要はないとおもいます. 逆に funcref なら大文字かつ l:
はつけるべき。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
同名のグローバル関数が定義されていた場合にエラーにならないように l:Res
がいいと思います
doc/vital/Async/Promise.txt
Outdated
\ Promise.new({resolve -> timer_start(100, {-> resolve('second')})}), | ||
\]) | ||
\.then({v -> execute('echo ' . v)}) | ||
\.catch({e -> execute('echo ' . e)}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
execute('echo e')
か execute('echo ' . string(e))
?
doc/vital/Async/Promise.txt
Outdated
\ Promise.new({resolve -> timer_start(50, {-> execute('throw "ERROR!"')})}), | ||
\ Promise.new({resolve -> timer_start(100, {-> resolve('second')})}), | ||
\]) | ||
\.then({v -> execute('echo ' . v)}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
execute('echo v')
か execute('echo ' . string(v))
?
doc/vital/Async/Promise.txt
Outdated
\ Promise.new({resolve -> timer_start(50, {-> resolve('first')})}), | ||
\ Promise.new({resolve -> timer_start(100, {-> resolve('second')})}), | ||
\]) | ||
\.then({v -> execute('echo ' . v)}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
execute('echo v')
か execute('echo ' . string(v))
?
doc/vital/Async/Promise.txt
Outdated
" Outputs '42' | ||
|
||
Promise.resolve(Promise.reject('ERROR!')) | ||
\.catch({reason -> execute('echo ' . reason)}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
execute('echo reason')
か execute('echo ' . string(reason))
?
doc/vital/Async/Promise.txt
Outdated
\] | ||
\) | ||
\.then({-> exeute('echom "All repositories were successfully cloned!"', '')}) | ||
\.catch({err -> execute('echom "Failed to clone: " . ' string(err), '')}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.
が足らないです。
execute('echom "Failed to clone: " . ' . string(err), '')
これでもいいかも?
execute('echom "Failed to clone:" ' . string(err), '')
僕はフィードバックの内容と今後の変更内容(予定)に特に意見はないので Approve にしておきます。 |
doc/vital/Async/Promise.txt
Outdated
\ code ? reject(buf.err) : resolve(buf.out) | ||
\ }, | ||
\ })}) | ||
endfunction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
自分も詳しく job の仕様を把握してませんが、試した限りだと exit_cb
が close_cb
よりも後に実行されるとは限らないようです。
https://gist.github.com/tyru/0ffee5ba2b8cab7390f2652c9a97bc24
上記の結果 (messages) だと exit_cb
のが先に実行されてしまっているため、 buf
が空になってしまっています。
close_cb
のみ指定して s:read_to_buf()
に resolve してもらうとかですかね? (それでいいという確証はないです…)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
私の環境だとコールバックの発火順は exit_cb
のほうが後なので,どうやらプラットフォーム依存ですね…ちなみに close_cb
だけだと exit status が分からないのでコマンドが失敗したかどうか分からないです.
ちゃんとやるならどちらの順序で発火しても動くようにすべきですが,それをやると example としてイマイチになってしまう気がします(本質的でないコードが増えすぎる).なにか良い修正案とかないでしょうか… @lambdalisue @thinca @mattn
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
プロセスの終了とチャネルのクローズはそもそも別の物なので例として不適かと思います。
プロセスが終了した後に全ての出力が欲しいのであれば、オプションに 'drop': 'never'
を渡してチャネルから ch_read
で持ってくるのが良いかと思います
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
あと、プロセスの終了はどうでもよくて出力が全部欲しい場合は Vim のヘルプにある通り close_cb でやるべきですかね。
http://vim-jp.org/vimdoc-ja/channel.html#read-in-close-cb
個人的には実用面で言えば「プロセスの出力結果がほしい」が要求であって「プロセスが終了してから出力結果がほしい」ではないと思います。なので例としては、こちらのほうが適切かと思います。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
すでに喋るべき内容は語られていた。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
お手数ですが,これで動いているか確認いただけませんでしょうか?
自分の環境では元のコードも動いてしまっていたので,これで問題が解決したのかどうかが確認できず…
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 の開始に失敗してしまいました
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rhysd 動きました。ただいくつか修正した方が良さそうな点がありました。
open
の場合も考慮するch_status(a:chan, {'part' : a:part}) =~# 'open\|buffered'
- 末尾に改行入れる
let out .= ch_read(a:chan, {'part' : a:part}) . "\n"
:echom
だと改行を含む文字列の場合つらい感じになるので:echo
のがいいかもです (最後の.then()
と.catch()
の中)
ちなみに,
close_cb
は指定しないと job の開始に失敗してしまいました
これ自分の環境 (WSL) でも再現しました。詳しく追ってないですが Error:
みたいに err
が空文字で渡ってきちゃいますね。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Twitter でも言いましたが、このコードだとエラーにならなかったです。
もしかして job 以外の所でコケて .catch()
に来てるかも?
https://gist.github.com/tyru/4707726873f3e4955f331109f9dc0734
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
私の手元で確認した限りでは channel status が fail になっていたので job の開始に失敗しているんじゃないかと思います.他の箇所で例外が起きているなら err
が空になるのは(Promise の実装がバグってない限り)おかしいですね
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
動きました。修正点としては @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
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)
|
僕のベンチでも 100000 回とかでやっと差が出てる感じなので把握した上で |
@lambdalisue 了解です.もし今後実際に使ってみてそこで問題があることが分かったらその時に対処しましょう:+1: |
|
endif | ||
return child | ||
endfunction | ||
let s:PROMISE.then = function('s:_promise_then') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
実は vital core でもやってます
vital.vim/autoload/vital/vital.vim
Line 76 in e553b10
let s:Vital.import = s:_function('s:import') |
書くの面倒だという以外はいいことしかないと思ってる
try | ||
let Result = a:callback(a:result) | ||
catch | ||
let Err = v:exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
v:exeption は関数ではないので関係ないのでは? Result は関数になりうりそうなので l:
つけておいてもよさそう。
あと関連して Err が funcref にならないなら大文字にする必要はないとおもいます. 逆に funcref なら大文字かつ l:
はつけるべき。
try | ||
let Result = a:callback(a:result) | ||
catch | ||
let Err = v:exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
これだとスタックトレースの情報 (v:throwpoint
) が消えてしまうのでエラーオブジェクト作って v:exeption と v:throwpoint 両方持たせておいたほうがよさそう?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[追記補足] これ"よさそう?"って微妙なカンジで最初は書いてましたが、help の github_issues の例とか実は僕も動かなくてデバックしてて、スタックトレースだしてもムズい(lambdaをおえない...)のにエラーだけだとほんとによくわからないエラーしかでないことがあるので、真剣に検討してもいいかなぁと思います。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
うーん,それはエラーオブジェクトを定義する必要が出てしまいますね…例外が起きたら v:exception
だけでなくて,v:throwpoint
も結合させた文字列で reject するとかどうでしょう?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
僕は @haya14busa さんのいうエラーオブジェクトのほうが良いかと思います。
分離されたデータをライブラリに勝手に結合されてイラッとした経験結構多いので……(例: stdout
と stderr
を勝手に結合するやつとか)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ 'message': v:exception [, 'throwpoint': v:throwpoint]}
くらいでどうでしょうか. 文字列結合はできるなら避けたい
endfunction | ||
|
||
function! s:is_available() abort | ||
return has('nvim') || v:version >= 800 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nvim よく追えてないですが lambda は割りと最近入ってるはずなのでチェックとしてどれくらい機能してるか微妙かも?
has('lambda')
とかのほうがいいかなと思います. あとは v:t_
や timer_start
も使ってはいるので 8 以上っていうのは残してもよいかな?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
うーん,なるほど.確かにそのとおりですね.僕も具体的にどのバージョンかまでは把握していないです… 機能ベースで言うと,has('lambda') && has('timers')
のほうが良さそうですね
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to has('lambda') && has('timers')
.
一般的にも機能ベースでチェックして、それでもできないならバージョンでチェックするというのがいいと思います
doc/vital/Async/Promise.txt
Outdated
be |Funcref| and they are guaranteed to be called __asynchronously__. | ||
> | ||
echo 'hi' | ||
Promise.new({resolve -> execute('echo "halo"') || resolve(42)}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
上下に echoあって実行できそうにみえるし call Promise
or call s:Promise
にしておいてもよさそう。
この help 全体的に call なしで Promise.new....
としているけどわざとですかね?
せっかくなら call
つけてほぼそのまま実行できるくらいにしておいてもよいかなと思いますがどっちでもよさそう
あと execute('...', '')
にしないとこの結果を確認できないので直しておいてあげると読者に優しそうですかね。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
はい,第2引数は各所で忘れていたので修正します…ありがとうございます. :call
も追加します
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[補足] 面倒そうなので一度 s:echom()
とか定義してもいいかなともちょっと感じましたがおまかせします
doc/vital/Async/Promise.txt
Outdated
\ s:sh('git', 'clone', 'https://github.com/rhysd/clever-f.vim.git'), | ||
\] | ||
\) | ||
\.then({-> exeute('echom "All repositories were successfully cloned!"', '')}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] s/exeute/execute/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice catch です.misspell
さん頼む…
doc/vital/Async/Promise.txt
Outdated
let s:Promise = vital#vital#import('Async.Promise') | ||
|
||
function! s:wait(ms) | ||
return s:Promise.new({resolve -> timer_start(a:ms)}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return s:Promise.new({resolve -> timer_start(a:ms, resolve)})
ですかね。 resolve できてない
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
その通りです.ありがとうございます.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] total じゃなくて remaining
にしてデクリメントしていって0ならresolveってすると一つ変数を減らせる説 (total, resolved -> remaining)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
あー,確かに.キーの数は減らしたいのでそうしてみます.
endif | ||
|
||
let wait_group = { | ||
\ 'done': repeat([v:null], total), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] get(a:000, 0, v:null)
とか他のところで使ってるし書き方統一してもよさそう。
(個人的には get(a:, 1, v:null)
が好みだけど統一されてたらなんでもok)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
個人的な好みにより a:0 になりました
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[感想] 自分がいちばん好みじゃないの来た...! (勿論直してほしいとかではなくて単なる感想で、言い分としては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])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[optional] call timer_start(0, ...)
っていうイディオム関数化しておいてもよさそう? asap とか next_tick とか?
読者が暗黙に timer_start(0,...)
がどういう意味かわかってるという仮定をおいてるけど関数化されててコメントもあったりすると可読性よさそう
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
とても気持ちはわかるのですが
- ユーザー定義関数のオーバーヘッドが大きい
- 無駄にコールスタックを積んでしまう
の二点から今のままが良いと思います。
と同じで Vim script の事情から本来不要なハックや最適化が必要になるの少しモニョりますが、仕方ない……
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
その最適化が本当に必要な箇所かどうかが結構あやしいとは思いますが(2はなんかchainしすぎるとダメとか見た気がするのでわからんでもない), まぁoptionalですね。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
はい。ここがユーザーが触る場所ならば僕も関数でラップする事には同意なのですが、ライブラリ内部のコードで(個人の主観としては)ほぼイディオム的な使い方なのでラップする必要が薄いと思います。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
提案ありがとうございます.個人的な感覚では 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' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
このチェックはfor loop の外で settled 変数に入れたあと or 前くらいがよさそう
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
publish の呼ばれ方的にこのパスが呼ばれることがない(防衛的に入れてた)ので気づきませんでしたが,確かにそうですね.ループ前にチェックするようにします.
test/Async/Promise.vimspec
Outdated
|
||
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})}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit][optional] 実装のほうにも似たコメント書きましたが done より result 感がある?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確かに.フラグをセットするんじゃなくて値をセットしてるとこは result にしといたほうが値の意図が明確で良さそうです
test/Async/Promise.vimspec
Outdated
Assert Equals(p._state, PENDING) | ||
let p = p.then({-> extend(l, {'done' : 1})}) | ||
Assert Equals(p._state, PENDING) | ||
Assert Equals(done, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
あってるので別によさそうだけど, call s:wait_has_key(l, 'done')
してあげないとフェアじゃないかも?
...と思ったけど無駄にunit test が長くなるのもアレだしムズカシイ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(見直すとそもそも wait_has_key は done すでに定義してるので使えなかった.... ので sleep 1ms とかするとフェア感?(本当か?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ここは待たなくても timer_start でキューイングされた処理は後のどこかで無害な感じで実行されて終わるだけなので,待たなくて良いかなと
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
あーすみません,このコメントの意図を読み違えてました..then
がタイマーを挟むようになった今,確かにある程度待たないと .then
が発火していないことを確認するのにはこれでは不十分でした.ちなみに .then
は永遠に発火しないので,done
が定義済みかによらず wait_has_key
は使えないです.
test/Async/Promise.vimspec
Outdated
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') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wait_has_key の前でまだ Done には値入ってないことを確認すると "asynchronously" なことがわかりやすくてよさそう
test/Async/Promise.vimspec
Outdated
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') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ここも wait_has_key のまえにチェック入れたほうが asynchronouslyのテストであることがわかりやすくてよさそう?
test/Async/Promise.vimspec
Outdated
Assert Equals(done, 42) | ||
End | ||
|
||
It can take rejection handler at 2nd parameter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
このテスト catch 使ってないし Describe .then
のとこにあったほうがよさそう
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
これは .then
のとこに置くべきなのを間違えてますね…ありがとうございます!
今気が付きましたが 8f6b866 は Neovim を捨てることになります。 Neovim v0.2.3-240-g6ff13d (ほぼ現在の HEAD) で
この辺のサポートは結構あとだった気がするので |
@lambdalisue うお,マジですか.8f6b866 に強い動機があるわけではないため,削除しようと思います.ご指摘ありがとうございます. |
45cea5d
to
337e8c4
Compare
8f6b866 を削除するために たくさんのレビューありがとうございます.2時を回ったので,今日はこのあたり( #567 (comment) )にしてまた明日の夜に修正予定です. 目下のところ,議論すべきは |
endfunction | ||
|
||
function! s:_subscribe(parent, child, on_fulfilled, on_rejected) abort | ||
let is_empty = empty(a:parent._children) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[vimlint] reported by reviewdog 🐶
EVL102: unused variable l:is_empty
Discussion point: Error HandlingProblem: call s:P.new({-> execute('throw "oops"')})
\.catch({err -> execute('echo err', '')}) We can refer 1. Concat
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 💪
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
テストまで見ました。
doc/vital/Async/Promise.txt
Outdated
be |Funcref| and they are guaranteed to be called __asynchronously__. | ||
> | ||
echo 'hi' | ||
call Promise.new({resolve -> execute('echo "halo"', '') || resolve(42)}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
この書き方たと、例えば echo "halo"
の部分が echon "1halo"
だと ||
の左側が TRUE
になるので resolve(42)
が実行されません。つまりたまたまうまく動くだけのように見えます…。
そうと知らずにこの例をコピーして使う人が出てくることを危惧しています。
doc/vital/Async/Promise.txt
Outdated
- Operation has completed successfully | ||
- Operation has failed with an error | ||
|
||
{promise}.then([{onResolved} [, {onRejected}]]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
なるほど。この help 内でも Fulfilled Promise
のようなワードがあったので、resolve の方が表記が揺れなくてわかりやすそうという意図なら、これらも Resolved Promise
にするのがいいかも?
とは言え私もそこまで強い意志はないです。意図してないうっかりだったらアレなので念のため確認くらいの気持ちでした。
test/Async/Promise.vimspec
Outdated
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
このコメントは必要なやつでしょうか?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
必要ないやつでした…
test/Async/Promise.vimspec
Outdated
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
一応 master に入ってる最新版 themis.vim ならいけるはずです(が、別に無理にしなくて OK)。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
おっと,そうですね.rebase して最新の themis を使うようにします.
End | ||
|
||
It should make settled Promise with v:null when no argument is given to resolve()/reject() | ||
let l = l: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
私が言い出しておいてアレですが、この使い方なら無理に l:
使わなくても新規の辞書 {}
でも良さそう…。(その場合は Assert の部分が l.result
になる)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
分かる… うーん,ただ l.
付けなくて良い利点はあるんですよね.特に理由が無ければこのままでも良いかなぁと
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ハック感が抜ける(…までは行かなくても薄れる)メリットはありそうですが、全部直すの面倒なのも同意なのでこのままでも OK です 🙆
test/Async/Promise.vimspec
Outdated
End | ||
End | ||
|
||
Describe .then() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.then()
と .catch()
は Promise オブジェクトのメソッド(モジュール直下の関数ではない)ので、別途 Describe Promise object
的なブロックに入れるのが良さそう。
End | ||
End | ||
|
||
Describe .reject() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.reject()
に resolved Promise を渡した場合のテストがほしいなー。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
間に合わなかった!
些末なコメントをしましたが特筆すべきことは特にありませんでした。
改めて実装ありがとうございます!素晴らしいです 👏
let s:REJECTED = 2 | ||
|
||
let s:DICT_T = type({}) | ||
let s:NULL_T = type(v:null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
これわざわざ type()
使ってますが、仕様上 v:null
を使うことになっているので foo is v:null
なチェックでも良さそうな気がします。そうすれば左辺値に type()
を適用する必要がなくなるのでほんの少し速くなるかも。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確かにわざわざ型の ID を見ているのは冗長ですね.ありがとうございます,直します.
call s:_fulfill(a:promise, Result) | ||
elseif a:settled == s:REJECTED | ||
call s:_reject(a:promise, Result) | ||
endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 [IMO]
ちょっと読んでて混乱しました。
最初の pending の判定を除くと、上2つが has_callback
が TRUE
の場合、下 2 つが has_callback
が FALSE
の場合なので、L43 の if の中にそれぞれ書いてしまっても良いかも。
その場合 pending の判定が重複しちゃうのでどちらが良いかむずいですが… 中に入れてしまえば L54 の不要な代入も自然に減らせそうな気がします。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
確かにここはちょっと冗長かつ微妙な実装になっているのですが,意図としてはコールバックの発火処理と次の(子供の)Promise へチェーンする処理を分離する実装になっています.
a:promise._state != s:PENDING
をダブらせて上の if
の中で判定するというのも考えたのですが,コールバック実行部分とチェーンの続きを発火する部分が混ざると,それはそれで結構読みづらい印象でした.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
こちらからは激LGTM 👍
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.
Thank you @lambdalisue and @tyru!
nit festival! Thank you @haya14busa
23e695d
to
fbed16a
Compare
themis.vim のアップデートの取り込みと fixup になっている箇所の適用のために push -f しました. @thinca 明らかに直したほうが良さそうなところを修正しました.再度修正箇所とレビューコメントの確認をお願いできますでしょうか |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
End | ||
|
||
It should make settled Promise with v:null when no argument is given to resolve()/reject() | ||
let l = l: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ハック感が抜ける(…までは行かなくても薄れる)メリットはありそうですが、全部直すの面倒なのも同意なのでこのままでも OK です 🙆
では,これでマージしようと思います.細かいところまでチェックいただいた皆さんありがとうございました🙏 |
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
Thank you for your review in advance.
As of #526, either English or Japanese is ok.