Hello World
Spiga

趣味编程:Functional Reactive Programming(参考答案)

2009-09-23 10:45 by 老赵, 13158 visits

原题在此。初见Functinal Reactive Programming时,它的编程方式让我大开了眼界,居然可以用这种方式来操作和控制事件。虽然从技术角度来说,要实现这种方式并不是非常困难,甚至颇为有趣。因此我把它当作一次“趣味编程”。不过,这次的结果似乎让我对API设计有了一些新的体会,我打算明天再来总结一下。今天,我们先关注原题的解决方式。

在接下来的代码中,我们只关注主体逻辑实现,而参数交验无非是一些判断是否为null的操作,就暂且放一边吧。

提取EventBase及NativeEvent改造

之前的文章中,我们提到了如何从DelegateEvent中创建第一个IEvent对象:NativeEvent。从中我们可以得出IEvent对象的一些普遍规律:

  1. 在对象内部保留多个Callback
  2. 仅在第一次添加Callback时才注册一个触发器
  3. 在自身事件触发时,执行所有Callback

由于一个IEvent对象的触发,一般都是由另一个“事件”引起的,因此这里的“触发器”指的便是沟通原始事件与自身事件触发的“桥梁”。根据这一特性,我们可以总结出一个通用的EventBase类型:

public abstract class EventBase<TEventArgs> : IEvent<TEventArgs>
{
    private List<Action<TEventArgs>> m_callbacks = new List<Action<TEventArgs>>();

    public void Add(Action<TEventArgs> callback)
    {
        if (this.m_callbacks.Count == 0)
        {
            this.RegisterTrigger();
        }

        this.m_callbacks.Add(callback);
    }

    protected abstract void RegisterTrigger();

    protected void FireEvent(TEventArgs args)
    {
        foreach (var callback in this.m_callbacks) callback(args);
    }
}

对于子类来说,需要实现RegisterTrigger方法,提供“搭桥”的逻辑,并且在自身事件触发时调用FireEvent方法。由此,原文中的NativeEvent便可修改为:

private class NativeEvent<TEventArgs> : EventBase<TEventArgs>
    where TEventArgs : EventArgs
{
    private DelegateEvent<TDelegate> m_delegateEvent;
    private Type m_eventArgsType;

    public NativeEvent(DelegateEvent<TDelegate> delegateEvent)
    {
        this.m_delegateEvent = delegateEvent;
        this.m_eventArgsType = this.GetEventArgsType();
    }

    private Type GetEventArgsType() { ... }

    protected override void RegisterTrigger()
    {
        // sender
        var senderExpr = Expression.Parameter(typeof(object), "sender");
        // eventArgs
        var eventArgsExpr = Expression.Parameter(this.m_eventArgsType, "args");
        // (TEventArgs)eventArgs
        var castExpr = typeof(TEventArgs) == this.m_eventArgsType ? eventArgsExpr :
            (Expression)Expression.Convert(eventArgsExpr, typeof(TEventArgs));
        // this
        var thisExpr = Expression.Constant(this);
        // this.FireEvent((TEventArgs)eventArgs)
        var bodyExpr = Expression.Call(thisExpr, s_fireEventMethod, castExpr);
        // (sender, eventArgs) => this.FireEvent((TEventArgs)eventArgs)
        var lambdaExpr = Expression.Lambda<TDelegate>(bodyExpr, senderExpr, eventArgsExpr);
        
        this.m_delegateEvent += lambdaExpr.Compile();
    }

    ...
}

提取EventBase,其实遵循的是DRY原则。虽然有人说,最好等到出现第三次重复时才重构。不过我们既然已经总结出一套“统一结构”,又能预料到它的复用性,不如现在就提取出基类吧。

MergeEvent与扩展方法

F#中Event.merge方法的作用是“捆绑”起大量同类型的事件,再任意一个事件触发时便引发自身。实现如下:

public class MergeEvent<TEventArgs> : EventBase<TEventArgs>
{
    private List<IEvent<TEventArgs>> m_events;

    public MergeEvent(params IEvent<TEventArgs>[] events)
    {
        this.m_events = events.ToList();
    }

    protected override void RegisterTrigger()
    {
        this.m_events.ForEach(e => e.Add(this.FireEvent));
    }
}

F#中有Event.merge辅助方法,在C#中的形式自然是扩展方法:

public static class EventExtensions
{
    public static MergeEvent<TEventArgs> Merge<TEventArgs>(
        this IEvent<TEventArgs> ev, params IEvent<TEventArgs>[] events)
    {
        var all = new IEvent<TEventArgs>[] { ev }.Union(events);
        return new MergeEvent<TEventArgs>(all.ToArray());
    }
}

我们的Merge方法返回的是MergeEvent而不是IEvent,这是因为对于一个函数来说,它的返回类型最好尽可能的具体,而参数类型最好尽可能的抽象,这样它的复用性则会更好一些。

InOutEventBase及Map/FilterEvent

除了如MergeEvent这样的事件之外,还有一类事件对象是形成一个“代理”,将输入事件触发时的参数经过某种转化,或者过滤,由此再来触发自身。为此,我们可以再定义一个InOutEventBase基类:

public abstract class InOutEventBase<TIn, TOut> : EventBase<TOut>
{
    protected IEvent<TIn> InEvent { get; private set; }

    public InOutEventBase(IEvent<TIn> inEvent)
    {
        this.InEvent = inEvent;
    }

    protected override void RegisterTrigger()
    {
        this.InEvent.Add(this.OnInEventFire);
    }

    protected abstract void OnInEventFire(TIn inArgs);
}

InOutEventBase有两个泛型参数,TIn为输出事件的类型,TOut为输出事件的类型。由于EventBase关心的是自身事件的输出,因此它继承的是EventBase<TOut>。InOutEventBase会负责保留输入事件对象,并提供了RegisterTrigger的实现,让子类只需要把注意力集中在输入事件触发时传入的参数即可。例如MapEvent实现的则是最经典的“转换”操作:

public class MapEvent<TIn, TOut> : InOutEventBase<TIn, TOut>
{
    private Func<TIn, TOut> m_mapper;

    public MapEvent(Func<TIn, TOut> mapper, IEvent<TIn> inEvent)
        : base(inEvent)
    {
        this.m_mapper = mapper;
    }

    protected override void OnInEventFire(TIn inArgs)
    {
        this.FireEvent(this.m_mapper(inArgs));
    }
}

同样,FilterEvent实现的则是“过滤”。很显然,它的的输出参数和输出参数是同一类型:

public class FilterEvent<TEventArgs> : InOutEventBase<TEventArgs, TEventArgs>
{
    private Func<TEventArgs, bool> m_predicate;

    public FilterEvent(Func<TEventArgs, bool> predicate, IEvent<TEventArgs> inEvent)
        : base(inEvent)
    {
        this.m_predicate = predicate;
    }

    protected override void OnInEventFire(TEventArgs inArgs)
    {
        if (this.m_predicate(inArgs)) this.FireEvent(inArgs);
    }
}

至于它们对应的扩展方法,相信对您来说一定不是问题,这里就不多重复了吧。此外还有F#中的Event.choose方法,它结合了Map和Filter,您亲自试试看?

ScanEvent

F#中的Event.scan方法会维护一个累加器(acc),每次输入事件触发时,则会通过高阶函数,把事件参数计算到当前的累加器中得到新的值,并根据新的值触发新事件:

public class ScanEvent<TIn, TOut> : InOutEventBase<TIn, TOut>
{
    private Func<TOut, TIn, TOut> m_scan;
    private TOut m_curr;

    public ScanEvent(Func<TOut, TIn, TOut> scan, TOut seed, IEvent<TIn> inEvent)
        : base(inEvent)
    {
        this.m_scan = scan;
        this.m_curr = seed;
    }

    protected override void OnInEventFire(TIn inArgs)
    {
        this.m_curr = this.m_scan(this.m_curr, inArgs);
        this.FireEvent(this.m_curr);
    }
}

至于F#中的Event.pairware,它的作用是将一个输入事件的参数以两两一组的方式呈现为输出事件(具体可观察原题的描述)。因此,它其实也是一个InOutEventBase:

public class PairwareEvent<TEventArgs> : InOutEventBase<TEventArgs, TEventArgs[]>
{ 
    ...
}

至于它的实现,哪位朋友可以给出一个正确的结果呢?

Partition操作

在F#中还提供了一个Event.partition操作,它的作用是将一个事件拆分为两个,拆分依据则为一个从输入参数返回布尔值的高阶函数。拆分后的事件,一个在高阶函数返回true时触发,另一个则正好想法。很显然,Partition操作与其它操作不同,它并不需要返回一个新的事件,它完全可以通过FilterEvent来组合生成:

public static class EventExtensions
{
    public static IEvent<TEventArgs>[] Partition<TEventArgs>(
        this IEvent<TEventArgs> ev, Func<TEventArgs, bool> predicate)
    {
        return new IEvent<TEventArgs>[]
        {
            new FilterEvent<TEventArgs>(predicate, ev), // true event
            new FilterEvent<TEventArgs>(x => !predicate(x), ev) // false event
        };
    }
}

为什么这里生成的是IEvent<TEventArgs>数组而不是FilterEvent<TEventArgs>数组呢?因为这里的实现其实是一种较为偷懒的做法,把过滤操作完全交给FilterEvent去负责了。因此从理论上说,这个做法的效率不是最好的——虽然完全可行。因此,在需要优化的时候,我们完全可以把它换成另一种实现方式,返回另外的IEvent实现。因此这里我把它当作抽象类型返回,保留了修改的余地。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

7 条回复

  1. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-09-23 11:02:00

    沙发

  2. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-09-23 12:42:00



    在接下来的代码中,我们只关注主体逻辑实现,而参数交验无非是一些判断是否为null的操作,就暂且放一边吧。
    ====================================
    校验

    ML是函数式语言吗?在书店里看了一会,好像通篇都是用来解数学问题。

  3. egmkang
    *.*.*.*
    链接

    egmkang 2009-09-23 13:00:00

    @DiggingDeeply
    ML是函数式编程语言的一个分支.

  4. xingjiao[未注册用户]
    *.*.*.*
    链接

    xingjiao[未注册用户] 2009-09-23 17:22:00

    ScanEvent<TEventArgs, T> 怎么转换成 IEvent< TEventArgs >呢

  5. 老赵
    admin
    链接

    老赵 2009-09-23 17:25:00

    @xingjiao
    再仔细看看文章吧。

  6. vczh[未注册用户]
    *.*.*.*
    链接

    vczh[未注册用户] 2009-09-24 12:58:00

    凑这种东西总是很爽的,这让我想起前几天在C++封装了delegate,然后写的一个Curry函数,最后把linq搬进C++……

  7. richie
    210.74.149.*
    链接

    richie 2015-11-03 10:27:08

    不得不来膜拜一把,这玩意我现在才开始研究,没想到大神早在6年前就有了自己的见解。国内.net界真正的大神。只有看老赵的文章才能吸取营养,其他文章只能当作教程来看

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我