我正在各种平台上尝试Reactive Extensions,有一件让我烦恼的事情就是毛刺。
即使对于UI代码,这些毛刺可能not be that problematic,并且通常可以找到一个围绕它们工作的运算符,我仍然发现在出现毛刺时调试代码更难:中间结果对于调试并不重要,但我的想法不知道结果何时是中间的或“最终的”。
在Haskell和同步数据流系统中使用纯功能FRP,它也“感觉”错了,但这当然是主观的。
但是当将RX连接到非UI执行器(如电机或开关)时,我认为故障更成问题。如何确保只将正确的值发送到外部执行器?
也许这可以通过一些'调度员'来解决,他们知道某些“外部传感器”何时触发了启动事件,因此在将最终结果转发给执行器之前处理所有内部事件。类似于flapjax paper。
中描述的内容我希望得到答案的问题是:
让我举个例子
假设我想要打印一系列元组(a,b)
,其中契约是
a=n b=10 * floor(n/10)
n是自然数字流= 0,1,2 ....
所以我期待以下序列
(a=0, b=0)
(a=1, b=0)
(a=2, b=0)
...
(a=9, b=0)
(a=10, b=10)
(a=11, b=10)
...
在RX中,为了使事情变得更有趣,我将使用过滤器来计算b流
var n = Observable
.Interval(TimeSpan.FromSeconds(1))
.Publish()
.RefCount();
var a = n.Select(t => "a=" + t);
var b = n.Where(t => t % 10 == 0)
.Select(t => "b=" + t);
var ab = a.CombineLatest(b, Tuple.Create);
ab.Subscribe(Console.WriteLine);
这给出了我认为是一个小故障(暂时违反不变/合同):
(a=0, b=0)
(a=1, b=0)
(a=2, b=0)
...
(a=10, b=0) <-- glitch?
(a=10, b=10)
(a=11, b=10)
我意识到这是CombineLatest的正确行为,但我也认为这被称为故障,因为在真正的纯FRP系统中,你没有得到这些中间不变的违规结果。
请注意,在此示例中,我将无法使用Zip,并且WithLatestFrom也会提供不正确的结果。
当然我可以简单地将这个例子简化为一个monadic计算,永远不会对n个流出现进行多播(这意味着无法过滤而只是映射),但这不是重点:RX中的IMO总是得到分裂并重新加入可观察流时出现'故障':
s
/ \
a b
\ /
t
例如,在FlapJAX中,您不会遇到这些问题。
这有什么意义吗?
非常感谢, 彼得
答案 0 :(得分:2)
更新:让我尝试在RX环境中回答我自己的问题。
首先,似乎我对“故障”的理解是错误的。从纯粹的FRP角度来看,RX中看起来像毛刺的故障似乎在RX中实际上是正确的行为。
所以我想在RX中我们需要明确我们期望从传感器中激活值的“时间”。
在我自己的例子中,执行器是控制台,传感器是间隔n
。
所以如果我改变我的代码
ab.Subscribe(Console.WriteLine);
到
ab.Sample(n).Subscribe(Console.WriteLine);
然后只打印“正确”值。
这意味着当我们得到一个组合来自传感器的值的可观察序列时,我们必须知道所有原始传感器,将它们全部合并,然后在将任何值发送给执行器之前用该合并信号对值进行采样...... / p>
因此,另一种方法是将IObservable“提升”为“感知”结构,记住并合并原始传感器,例如:
public struct Sensed<T>
{
public IObservable<T> Values;
public IObservable<Unit> Sensors;
public Sensed(IObservable<T> values, IObservable<Unit> sensors)
{
Values = values;
Sensors = sensors;
}
public IObservable<Unit> MergeSensors(IObservable<Unit> sensors)
{
return sensors == Sensors ? Sensors : Sensors.Merge(sensors);
}
public IObservable<T> MergeValues(IObservable<T> values)
{
return values == Values ? Values : Values.Merge(values);
}
}
然后我们必须将所有RX方法转移到这个“Sensed”结构:
public static class Sensed
{
public static Sensed<T> Sensor<T>(this IObservable<T> source)
{
var hotSource = source.Publish().RefCount();
return new Sensed<T>(hotSource, hotSource.Select(_ => Unit.Default));
}
public static Sensed<long> Interval(TimeSpan period)
{
return Observable.Interval(period).Sensor();
}
public static Sensed<TOut> Lift<TIn, TOut>(this Sensed<TIn> source, Func<IObservable<TIn>, IObservable<TOut>> lifter)
{
return new Sensed<TOut>(lifter(source.Values), source.Sensors);
}
public static Sensed<TOut> Select<TIn, TOut>(this Sensed<TIn> source, Func<TIn, TOut> func)
{
return source.Lift(values => values.Select(func));
}
public static Sensed<T> Where<T>(this Sensed<T> source, Func<T, bool> func)
{
return source.Lift(values => values.Where(func));
}
public static Sensed<T> Merge<T>(this Sensed<T> source1, Sensed<T> source2)
{
return new Sensed<T>(source1.MergeValues(source2.Values), source1.MergeSensors(source2.Sensors));
}
public static Sensed<TOut> CombineLatest<TIn1, TIn2, TOut>(this Sensed<TIn1> source1, Sensed<TIn2> source2, Func<TIn1, TIn2, TOut> func)
{
return new Sensed<TOut>(source1.Values.CombineLatest(source2.Values, func), source1.MergeSensors(source2.Sensors));
}
public static IDisposable Actuate<T>(this Sensed<T> source, Action<T> next)
{
return source.Values.Sample(source.Sensors).Subscribe(next);
}
}
我的例子变成了:
var n = Sensed.Interval(TimeSpan.FromMilliseconds(100));
var a = n.Select(t => "a=" + t);
var b = n.Where(t => t % 10 == 0).Select(t => "b=" + t);
var ab = a.CombineLatest(b, Tuple.Create);
ab.Actuate(Console.WriteLine);
再次只将“所需”值传递给执行器,但通过这种设计,可以在Sensed结构中记住原始传感器。
我不确定这是否有任何“感觉”(双关语),也许我应该放弃对纯粹的FRP的渴望,并与之共存。毕竟,时间是相对的; - )
彼得