Promise in Tcl

Async http::geturl

proc callback_http_done {token} {
    puts "http geturl done"
}

::http::geturl $url -command [list callback_http_done]

puts "wait http geturl"
vwait forever

恢复顺序

习惯上,我们的思维的习惯是线性的。对于上面的例子,比较贴近人们习惯的流程顺序是下面这样:

  1. 调用 ::http::geturl 访问资源
  2. 访问成功的时候,执行下一步操作

但按照上面的代码结构,读到::http::geturl这一行的时候,思维不是继续到下一行,而是需要跳转到其它地方去。

这个问题似乎可以通过把callback函数的定义挪个位置来解决。

inline 函数的定义

::http::geturl $url -command [lambda {token} {
    puts "http geturl done"  
}]

这样一来,思维的顺序就保留了。

嵌套地调用

如果在收到请求相应之后,需要根据情况开始一个新的异步请求,并且这种嵌套需要好几层。

::http::geturl $url -command [lambda {token} {
    ::http::geturl $new_url -command [lambda {token} {
        ::http::geturl $new_url -command [lambda {token} {
            # ... 
        }] ;# callback 3
    }] ;# callback 2
}] ;# callback 1

这种情况下,形成了一个所谓的调用嵌套,所谓的 callback pyramid of doom.

读代码读到最后一行的时候,这一行实际上对应的是第一个 callback,时间顺序上更靠后的 callback 反而需要往回去找。

用 promise 维持顺序

如果用 promise 的方式考虑这个问题,可以写成下面这个样子

set promise [promise new]
::http::geturl $url -command [list $promise resolve]
$promise done [lambda {token} {
   # ...
}]

为了方便,把上面的代码封装一下

proc http_promise {url} {
    set promise [promise new]
    ::http::geturl $url -command [list $promise resolve]
    return $promise
}

set promise [http_promise $url]
$promise done [lambda {token} {
   # ...
}]

用 promise 绑定多个 callback

用 promise 嵌套调用

set promise [http_promise  $url_1]

set promise [$promise then [lambda {token} {
    return [http_promise $new_url]
}]] ;# callback 1

set promise [$promise then [lambda {token} {
    return [http_promise $new_url]
}]] ;# callback 2

$promise then [lambda {token} {
   # ...
}] ;# callback 3

这下逻辑上,至少维持了时间上的顺序。

promise 的实现

oo::class create promise {
    constructor {} {
       set this(ready)     0
       set this(callback)  ""
    }

    method execute {} {
       if {$this(ready) && $this(callback) ne ""} {
           return [$this(callback) $this(result)]
       }
    }

    method resolve {result} {
       set this(result) $result
       set this(ready)  1

       return [my execute]
    }

    method done {command} {
       set this(callback) $command

       my execute
    }
}

proc http_promise {url} { 
    set promise [promise new]

    ::http::geturl $url -command [list $promise resolve]

    return $promise
}

set promise [http_promise $url]

$promise done [lambda {args} {
   # ...
}]

让 promise 更通用

oo::class create promise {

    method then {command} {
        set this(promise) [promise new]

        my done $command

        return $promise
    }

    method execute {} {
       if {$this(ready) && $this(callback) ne ""} {
           set new_result [$this(callback) $this(result)]

           if {$this(promise) ne ""} {
               $this(promise) resolve $result
           }
       }
    } ;# end method
}
    method execute {} {
       if {$this(ready) && $this(callback) ne ""} {
           set new_result [$this(callback) $this(result)]
           my 
       }
    }

    method process_result {result} {
           if {$result is "promise"} {
               set promise $result
               switch -- [$promise status] "resolved" {
                   my next_promise [$promise result]
               } "pending" {
                   $promise done [list $this(promise) resolve]
               } 
           } else {
               my next_promise $result
           }
    }

    method next_promise {result} {
       if {$this(promise) ne ""} {
               $this(promise) resolve $result
       }
    }

用 promise 实现 callback list

set promise [http_promise  $url_1]

set promise [$promise then [lambda {token} {
    do something 1
}]] ;# callback 1

set promise [$promise then [lambda {token} {
    do something 2
}]] ;# callback 2

$promise then [lambda {token} {
   do something 3
}] ;# callback 3

这下逻辑上,至少维持了时间上的顺序。

Promise is a placeholder

set promise [::http::geturl $url]
$promise done [lambda {token} {
   # ...
}]

promise 的行为像是不管三七二十一先占下一个位子,然后再决定在这个位子上放些什么。

asyn-command -callback $callback

asyn-command -callback [list $promise resolve]

trace add execution $callback-1 leave [list $callback-2] trace add execution $callback-2 leave [list $callback-3]

问题2:callback 的执行顺序

在一切正常的情况下,上面的例子的输出是左边这种情况呢,还是右边这种情况。

虽然较大可能(异步HTTP请求)的情况下,是左边这种情况。

但就普遍意义上来说,callback什么时候执行,是不一定的。比如 http 请求的实现可以是从cache读取数据,进而直接返回结果。

wait http geturl   |  http geturl done
http geturl done   |  wait http geturl

问题3: 代码重用和逻辑分离

回掉函数 callback_http_done 实际上可以分为两部分

  1. 数据的获取和清理
  2. 用户自定义的逻辑

其中“数据的获取和清理”这部分对于每次调用都是一样的,用户的处理逻辑则可能每次都不一样。

::http::geturl $url_1 -command [list callback_http_done_1]

::http::geturl $url_2 -command [list callback_http_done_2]

set promise $url_1 -command [list callback_http_done_1 $promise done callbackhttpdone