小谈F#与Functional Reactive Programming

开发 后端
Functional Reactive Programming,直译为函数式被动编程。本文介绍了F#中的Functional Reactive Programming。

本文是老赵在其博客上对F#的又一篇总结,这次讲解的是Functional Reactive Programming相关的一个练习。

最近我们搞了一些把事件当作对象方面的工作。基于事件的编程再常见不过了,尤其是在和UI相关的WinForm,WPF,Silverlight开发方面。把事件作为一等公民看待之后,我们可以实现一些较为“特别”的编程模型,例如Functional Reactive Programming。这是一种较为优雅的基于事件的处理方式,适合一些如交互式动画,自动控制方面的工作。即使平时不太常见,我想作为一个“尝试”或“练习”也是非常合适的。

我是通过F#而了解“事件即对象”以及FRP的相关内容的,而微软的Matthew Podwysocki最近一直在博客上撰写的一系列关于F#事件的文章也给了我很多信息。F#便直接体现了“事件即对象”的概念,它会把.NET类库中的事件转化成IEvent对象,然后便可以对其进行编程。IEvent对象是F#中表示“事件”的标准类型,它的最关键的成员是Add方法,如果使用C#来表示便是:

  1. public interface IEvent<TEventArgs>  
  2. {  
  3.     void Add(Action<TEventArgs> callback);  
  4. }  

当然,其实F#的事件并没有那么简单,不过我们目前只需要关注至此即可(更详细的信息您可以关注Matthew的文章或Anders Cui同学的中文版)。Add方法是为这个事件添加一个回调函数,它自然会在事件触发时被调用。而传入的参数,您可看作是C#中事件的EventArgs对象(即第二个参数)。有了IEvent对象,在F#中便可以使用各种方式来响应一个事件。例如:

  1. #light  
  2.  
  3. open System  
  4. open System.Windows.Forms  
  5.  
  6. let form = new Form(Visible = true, TopMost = true, Text =  
  7. form.MouseDown  
  8.     |> Event.map (fun args -> (args.X, args.Y))  
  9.     |> Event.filter (fun (x, y) -> x > 100 && y > 100)  
  10.     |> Event.listen (fun (x, y) -> printfn "(%d, %d)" x y)  
  11.  

Event.map方法会接受一个IEvent对象,以及一个用于转换事件参数的高阶函数,并返回一个新的事件对象。用户可以监听这个新事件。当原有事件触发时,它的事件参数将被高阶函数转化为新的对象,并以此出发新的事件。F#可以使用|>符号来改变参数传递的顺序,这样代码可以编写得更为流畅。例如下面两行F#代码其实是等价的:

  1. Console.WriteLine "Hello World" 
  2. "Hello World" |> Console.WriteLine  

Event.filter的作用是对事件进行过滤,只有在原事件触发时,其事件参数满足某个条件,才会触发新的事件对象。Event.listen则是简单的调用IEvent对象的Add方法,它只是一个辅助函数。

F#的Event模块还有其他一些方法,例如:

  1. let form = new Form(Visible = true, TopMost = true, Text = "Event Sample")  
  2. form.MouseDown  
  3.     |> Event.choose (fun args ->  
  4.          if args.X > 100 && args.Y > 100 then Some(args.X, args.Y)  
  5.          else None)  
  6.     |> Event.listen (fun (x, y) -> printfn "(%d, %d)" x y)  

Event.choose方法组合了Event.filter和Event.map,即在转化的同时可进行过滤。只有在高接函数的返回值为Some(args)而不是None的时候,才把args作为下一个事件的参数进行触发。还有:

  1. let form = new Form(Visible = true, TopMost = true, Text = "Event Sample")  
  2. form.MouseDown  
  3.     |> Event.merge form.MouseMove  
  4.     |> Event.filter (fun args -> args.Button = MouseButtons.Left)  
  5.     |> Event.map (fun args -> (args.X, args.Y))  
  6.     |> Event.listen (fun (x, y) -> printfn "(%d, %d)" x y)  

Event.merge方法的作用是合并两个(参数相同的)事件。当其中任意一个事件触发时,则会触发新的事件对象。此外:

  1. let form = new Form(Visible = true, TopMost = true, Text = "Event Sample")  
  2. let (overEvent, underEvent) =  
  3.     form.MouseMove  
  4.       |> Event.merge form.MouseDown  
  5.       |> Event.filter (fun args -> args.Button = MouseButtons.Left)  
  6.       |> Event.map (fun args -> (args.X, args.Y))  
  7.       |> Event.partition (fun (x, y) -> x > 100 && y > 100)        
  8. overEvent |> Event.listen (fun (x, y) -> printfn "Over (%d, %d)" x y)  
  9. underEvent |> Event.listen (fun (x, y) -> printfn "Under (%d, %d)" x y)  

Event.partition方法的作用是把原有事件拆分为两个,并在原有事件被触发时,根据高阶函数的返回值来决定触发哪一个新事件。

以上这些都是Matthew在博客中已经介绍过的内容。不过我认为,Event模块下还有两个方法值得一提,那就是Event.pairwise和Event.scan。请看下面的代码:

  1. let (trigger : (int -> unit), event) = Event.create()  
  2. event 
  3.     |> Event.pairwise  
  4.     |> Event.listen (fun (x, y) -> printfn "%d + %d = %d" x y (x + y))  
  5.  [1..10] |> Seq.iter trigger // 使用1到10依次调用trigger

Event.create方法将创建一个事件对象,返回这个事件以及它的触发器。Event.pairwise会根据IEvent<T>对象返回一个IEvent<(T, T)>对象——(T, T)是一个元组,当然在C#中没有这个语言特性时,我们可以使用IEvent<T[]>来代替。当我们使用***次trigger方法来触发event事件时,新的事件对象并不会触发。直到第二次及以后触发event事件时,才会把上一次的事件参数和目前的事件参数“合并”,并以此来触发新的事件。因此,上面的代码会输出以下内容:

  1. 1 + 2 = 3 
  2. 2 + 3 = 5 
  3. 3 + 4 = 7 
  4. 4 + 5 = 9 
  5. 5 + 6 = 11 
  6. 6 + 7 = 13 
  7. 7 + 8 = 15 
  8. 8 + 9 = 17 
  9. 9 + 10 = 19 

而Event.scan方法则可以这样使用:

  1. let (trigger : (int -> unit), event) = Event.create()  
  2. event 
  3.     |> Event.scan (fun acc i -> acc + i) 0  
  4.     |> Event.listen (printfn "%d")  
  5.  
  6. [1..10] |> Seq.iter trigger  
  7.  

Event.scan方法会维护一个累加器(acc),在上面的代码中其初始值为0,每次event事件触发时,则会通过高阶函数,把事件参数计算到当前的累加器中得到新的值,并根据新的值触发新事件。因此上面的代码会输出一下内容(不包括注释):

  1. // +1  
  2. // +2  
  3. // +3  
  4. 10 // ...  
  5. 15  
  6. 21  
  7. 28  
  8. 36  
  9. 45  
  10. 55  

自然,Event.pairwise和Event.scan方法得到的新对象都是有side effect的,需要考虑线程安全的问题。F#的类库不保证事件触发时的线程安全,于是事件在使用或触发时需要自行进行控制。

好,那么这次的“趣味编程”就产生了,您能否设计并实现一套类库,为C#语言提供这样一个类似的功能呢?您需要实现以下7种功能:
◆map
◆filter
◆choose
◆merge
◆partition
◆pairware
◆scan

有些朋友可能会想,为什么不直接使用C#来调用F#的类库呢?原因是“不合适”。F#的类库设计遵循了F#的语言特性,而且如前面所讲,F#本身会对.NET的事件进行一定转变。此外,为C#实现一个合适的API也是个很好的实践过程。例如,这又是一个适合扩展方法特性的场景。在我看来,***的API应该是这样使用的:

  1. someEvent  
  2.     .Map(args => new { args.X, args.Y })  
  3.     .Filter(args => args.X + args.Y > 100)  
  4.     .Scan(0, (acc, args) => acc + args.X, args.Y)  
  5.     .Add(args => Console.WriteLine(args));  

老赵介绍的这个Functional Reactive Programming的练习,您也试试看?

【编辑推荐】

  1. F#中DSL原型设计:语法检查和语义分析
  2. F#入门:基本语法,模式匹配及List
  3. C# Actor的尴尬与F#美丽外表下的遗憾
  4. 函数式编程语言F#:基于CLR的另一个头等编程语言
  5. Visual Studio 2010爆F#二进制兼容性问题
责任编辑:yangsai 来源: 博客园
相关推荐

2010-01-07 10:04:18

F#函数式编程

2010-04-07 16:51:59

F#

2010-01-26 08:25:06

F#语法F#教程

2010-01-15 08:33:13

F#F#类型推断F#教程

2012-03-12 12:34:02

JavaF#

2009-11-09 17:51:51

F#函数式编程

2011-06-09 09:52:41

F#

2009-08-13 17:39:48

F#数据类型Discriminat

2009-08-19 09:42:34

F#并行排序算法

2010-04-06 15:20:56

ASP.NET MVC

2010-03-26 19:22:08

F#代理

2009-12-04 09:16:44

Visual Stud

2012-11-06 10:01:35

ContinuatioF#

2009-12-14 09:04:10

F#运算符

2009-11-16 09:05:46

CodeTimer

2009-08-04 14:23:55

C# Actor

2009-08-13 17:25:21

F#入门

2009-12-11 13:59:35

F#

2010-05-13 09:21:44

F#Visual Stud

2010-04-07 09:46:05

点赞
收藏

51CTO技术栈公众号