DelayCall(window) (延时调用)

From wiki.visual-prolog.com

(以下内容,译自Category:Tutorials中的DelayCall (window)。更多内容可以参看Category:Chinese。)

事件可能发生得过于频繁而超过处理的需要,window::delayCall 提供了一种解决这个问题的方法。

起因

在文本框输入文字时常会出现一个可选项的列表,比如在浏览器输入URL和在搜索引擎输入搜索内容时。但当你快速输入时,就会希望它不要每输入一个字符都给出建议选项。这有两方面的原因,.一是这样太耗资源;二是快速闪动变化的可选项会使人因眼花瞭乱而不舒服。

不要每次击键都更新列表,而是延迟到用户输入有了停顿时才更新。也就是延迟到最后一次击键过了一定时间后再更新。

这里还有两个例子,也可以应用上述同样的解决方案来避免快速闪动及/或性能上的问题:

  • 实际的滚动最好是延迟到滚动事件有暂停时进行。
  • 接收来自外部的数据更新程序,最好等到外部数据有间歇时再对图形用户界面进行更新。

Drawback: 直到超过了某个时间,我们才能知道有了停顿/暂停。所以这种方法引入了一个时延,这个时间并非所有情况都期望: 所以这个延迟时间应在既不要反应太频繁又不要停顿时间太长之间作折衷。

这里 video 演示了三个控件。中间一个每次调整窗口大小事件都重画窗口,另外两个则是在事件后停顿一下再重画。

delayCall

delayCall 可以用来实现“过一会再响应”的方案。

先来看个简单的例子,控件的代码如下:

constants
    delay = 100. % ms
 
clauses
    addData(NewData) :-
        doAddData(NewData),
        delayCall(delay, invalidate).
 
clauses
    removeData(ToRemove) :-
        doRemoveData(ToRemove),
        delayCall(delay, invalidate).

该控件有对自身添加和删除数据的谓词。在数据更新时,控件会失效以便用新数据进行重画。但为了防止对控件大量重复的更新,我们要使失效推迟直到数据稳定了1OOms。这正是delayCall调用做的事:它安排调用invalidatedelay (= 100ms)之后进行,同时删除先前别的安排,如果有的话。因此,它的效果就是在数据更新后停顿100ms产生一次失效。

现在,基本动作都到位了。我们再来看看细节、常见问题及解决方案。

在这个delayCall简单调用中有两个参数:

predicates
    delayCall : (positive Delay, runnable Predicate).

delayCall 的调用将会:

  • 或是在Delay毫秒过后调用 Predicate
  • 或是重启一个新的 delayCall 调用,使用相同的Predicate ,而 Delay 参数重新开始
  • 或是被丢弃,由于Delay到达之前窗口被销毁了。

注意,delayCall是一个窗口一个 Predicate

区分窗口可以避免因窗口已经不存在而执行不相关的延迟谓词。The per window part can be used to avoid executing delayed predicate that is no longer relevant because the window it concerns no longer exist.

各自的Predicate也是十分重要的,因为可能会对许多不相关的事使用delayCall,这些不相关的事不应该彼此取消对方的延迟调用。The per Predicate part is important because you may want to delayCall for many unrelated things, and such things should not cancel each other. 把应用程序窗口用作全局响应是很正常的事,它不是与某个特定窗口关联。 It is very common to use the application window for "global" actions, which are not related to a specific window.例如,在某个元件变化停顿时重新计算全局状态。For example recalculating a global state when there is a pause in atomic changes.

在我们的例子中,两个调用中使用的谓词都是invalidate,因而不管我们调用的addData还是removeData都会再次对invalidate延期。而如果调用是delayCall(17, somethingElse),则不会对invalidate产生影响,不会取消它,也不会对它延期。

如同上例中的情形所示,一般情况下谓词的标识表明了期望delayCall区分的事情。不过对于匿名谓词来说这里容易出错,看下面的例子:

clauses
    setColor(NewColor) :-
       delayCall(10, { :- doSetColor(NewColor) }).

代码的初衷是打算延迟到颜色不再频繁变化时再设置颜色。它使用了一个匿名谓词来捕获新的颜色。通常情况下这种匿名谓词使用方法很值得推荐,不过在这里会产生一个问题,就是:每调用一次setColor就会创建一个新的匿名谓词来捕获NewColor。因而,每调用一次setColor就会用唯一的谓词调用一次delayCall。由于每个谓词都和前一个不是一回事,所以就不会对上一次的延时调用产生影响。如此一来,它就会每延迟10毫秒执行一次。

解决该问题可以这样做:

facts
   newColor : color := color_white. % 没有实际使用的哑赋值
 
clauses
    setColor(NewColor) :-
        newColor := NewColor,
        delayCall(10, setNewColor).
 
clauses
    setNewColor() :-
        doSetColor(newColor).

调用这个版本的setColor时,会更新事实变量newColor,所以这个事实变量中总是包含着NewColor的最新值。现在,对delayCall的调用涉及是的同一个谓词(也就是setNewColor)。因此,这段代码每次调用setColor都会取消前一次的setNewColor计划并重新做延期安排。这样,在setColor的调用中setNewColor的请求就延期在稍有停顿时。

与把新颜色放在匿名谓词中不同,我们把新颜色放在了一个事实变量中。因为这样一来我们就可以调用一个命名的谓词而不是一个匿名谓词。

The overall principle here is to maintain a shaddow (sub-)state which is then merged into the real state in a pause.

上面匿名谓词问题的解决方案是相当复杂和麻烦,需要对匿名和命名谓词都有深入的了解。The solution above to the anonymous predicate problem is rather complex/elaborate, we both need to introduce extra fact(s) and an extra named predicate.

最要命的是,那个事实变量中并没有包含对象所呈现的实际信息,它所存储的只不过是一个算法中的一点到另一点所传递的信息,就是说它是算法的局部信息而不是对象的全局信息。

这样的事实令人非常不满意,常常会用varM或匿名谓词取而代之。在这里,varM也无助于问题的解决,因为与NewColor完全相同的原因,它也需要是对象的全局信息。Such facts are highly undesirable and very often you will use varM's or anonymous predicates to eliminate them. In this case a varM will not help, because it will also have to be made global in the object for exactly the same reasons as for NewColor.

因此,有办法在delayCall中处理好匿名谓词就太好了。问题在于匿名谓词的身份不太适合确定是否需要拖延时间。解决这个问题(及其它一些问题)我们可以用另一版本的delayCall,它显式提供身份值来决定是拖延已有的延时还是要创建一个新的延时。So it would be very nice to have a way to deal with anonymous predicates in delayCall. The problem was that the identity of anonymous predicates is not very suitable for determine whether to prolong a delay. To solve this problem (and others) we can use another version of delayCall which uses an explicit provided identity value to determine whether to prolong an existing delay or to create a new one.

predicates
    delayCall : (positive Delay, runnable Predicate, IdType Id).

这个版本中多了一个Id参数,其类型可以任意确定。该值如果在两次调用中相同,后面的就会替代前面的(当然是前一个延时还没到期);如果值不一样,就认为调用间是不相关的。This version takes an extra Id argumment of any type you like. If you provide the same value in two calls the latter will replace the first (if has not already fired of course), if you provide different values the calls are considered unrelated.

编程人员需要确保选择Id

  • 取消/替换/拖延所指定的调用,并且
  • 不对其它delayCall的使用产生影响

有许多不同的策略可以用于Id,我们后面要讨论其中的一些。

在前面讨论的第一个例子中,我们有两个调用,都属于同样的延迟。在这其中我们(间接地)使用了谓词本身作为ID。前面已经说过,这样意义很明确,在许多情况下是很好的,不过它需要使用命名了的谓词。

对于匿名谓词,当需要更精确控制时就必须使用其它的ID。关键问题在于用某种东西来“碰撞”相同的调用,而不与其它的调用发生关系。

常常与setColor的例子中一样,期望碰撞的调用来自于代码中的特定位置。Very often like in the setColor example the calls you want to collide comes from a specific place in the code.

在这种情况下,将programPoint用作Id最简单。 The is easily obtained using the builtin predicate programPoint/0:

clauses
    setColor(NewColor) :-
       delayCall(10, { :- doSetColor(NewColor) }, programPoint()).

这种源程序行确定的ID调用可以相互取消但不会与其它地方的调用产生冲突(不管它们用什么策略)。

如果需要把调用放在若干行中,或是对同一组不同的命名谓词进行调用,则可以考虑定义一个组ID。同样,仍可以使用programPoint:If you need to put calls from several lines, or calls to different named predicates in the same group you should consider defining a group id. Also here you can use a programPoint:

constants
    myDelayCallId : programPoint = programPoint().
 
   ..., delayCall(10, { :- doSomeThing(...) }, myDelayCallId), ...
 
   ..., delayCall(20, doSomeThingElse, myDelayCallId), ...

如果由于某种原因需要确保源代码变化后这种ID不变(比如一个DLL需要知道作为另一个DLL的相同ID),则可以生成和使用GUID。GUID是一个128比特的数,Windows可以生成这样的数并且很少可能产生冲突。在Visual Prolog集成开发环境中可以生成使用这样的数,只需要在菜单中选择Insert -> New GUID

constants
    myDelayCallId : nativeGuid = % {3363A750-0320-4B2A-BE3A-27F66BD7B875}
        nativeGuid(0x3363A750, 0x0320, 0x4B2A, 0xBE, 0x3A, 0x27, 0xF6, 0x6B, 0xD7, 0xB8, 0x75).
 
   ..., delayCall(10, { :- doSomeThing(...) }, myDelayCallId), ...
 
   ..., delayCall(20, doSomeThingElse, myDelayCallId), ...

最后,我们来看看如何使用“语义”Id,看下面的代码:Finally, let us consider using "semantic" Id's. Consider the following code:

clauses
    onFileChanged(Filename) :-
        doOnFileChanged(Filename).

一个程序监视着磁盘上文件的变化。如果一个(相关的)文件变化了,程序就会执行某个动作。上面代码中每当文件变化就会调用onFileChanged,而doOnFileChanged表示文件变化了我们想要做的事情。

不过文件变化相继来得会很快,最好是等这些更新都消停了再动作。因此,这个过程中自然要用到delayCall。

但上面的解决方案在这里似乎都不适用。因为delayCall应该是针对每个文件的,而不能是针对每次功能或每个程序点的。简单的办法就是用匿名谓词来跟踪文件并使用文件名作为Id

clauses
    onFileChanged(Filename) :-
        delayCall(100, { :- doOnFileChanged(Filename) }, Filename).

现在,只有某个文件最后一个onFileChange在记忆中并被延迟处理。

更广泛的关联中,我们担心使用文件名会与其它地方的某种计划安排产生冲突。一种解决办法是混合使用语义和程序点,像这样:

clauses
    onFileChanged(Filename) :-
        delayCall(100, { :- doOnFileChanged(Filename) }, tuple(Filename, programPoint())).

另一个办法是创建一个唯一的延迟域:

domains
    delayId = fileChangeId(string Filename).
 
clauses
    onFileChanged(Filename) :-
        delayCall(100, { :- doOnFileChanged(Filename) }, fileChangeId(Filename)).

程序点解决方案的好处是不需要额外的定义,是一种“协调”的解决办法。而delayId域的解决方案好处是在代码中它可以使用在若干个地方。