懒惰模式

【懒惰模式】"懒惰、傲慢、缺乏耐性是程序员的三大美德"——Larry Wall(Perl's father)
很久以前,《设计模式》是本“红宝书“之类的读物,你要是面试时谈些"模式"总会有加分,同事有时也说,"哦,这里是个singleton模式, 那里是个clone模式"。今天,我们不怎么谈模式,遇到问题时,总结出一些套路,有时套路有用,有时套路却行不通,成为阻碍。
我想,程序设计里“思而不学则die, 学而不思则money",是一个普遍问题,因此,我有点打算,介绍一些更为简单的”模式”,”简单“但能引起"本质”思考,希望这些”简单“的模式能够帮助你多思考。
模式之——懒惰

  • c语言允许这样的语句
    my_assert = (true | (1/0)>millon);
    我们知道(1/0)根本不会发生,这被称为短路求值,
  • 三目运算符,类似
    gain = stupid_test ? million : (1/0);
  • 嫌 if 太啰嗦,三目又不好看,有人希望写一个漂亮的inline函数,
    int my_if(test, trueValue, falseValue) { if(test) trueValue else falseValue; } //usage gain = my_if(stupid_test, 1000000, (1/0) )

    然而,他发现这样根本不行!(1/0)总是被求值!
  • scala的解决方案,
    def my_if(p: boolean, true_value : =>int, false_value : => int) gain = my_if(stupid_test, 1000000, (1/0) ) //perfect

    解释:
    =>int是一个表示这里是函数类型,输入为空,返回为int,
  • c#的办法
    int my_if(bool p, func trueValue, func falseValue)
    {
    if(p) trueValue() else falseValue()
    }
    //丑了点,但将就
    my_if(true, ()=>million, ()=>1/0 )
lazy模式说的是,按需求值,只有在必要时,计算才发生
你可能已经发现,传入函数是实现lazy的关键
上面是些无用的例子,说些实际的例子吧:
  • log 系统
    在c#, java一类代码里面, 我们常常有c语言里不存在的烦恼(因为没有宏),比如说,
    内循环里,做log是一件需要很小心的事,因为我们常常写成这种形式,
    if(log_level > LOG_DEBUG) log.e( string.format("state: %d, %d, %d", getx(), gety(), getz()) )
    在未打开调试开关时,这里只造成一个 if的开销,如果没有if, string.format 总会发生,造成有影响的开销,但写起来还是麻烦的。
    解决办法呢,almost no,一个比较丑陋,但有效的办法是:
    void lazy_log(debug_lv, Func cont)
    {
    if(log_level > LOG_DEBUG) log.w(cont());
    }
    //usage,未开调试前,format不会发生
    lazy_log(log_level, ()=>string.format("state: %d, %d, %d", getx(), gety(), getz()) )
    (真正的办法还是需要DSL上的一些支持)
  • 单例,
    static T _ins = null;
    static T getInstance()
    {
    if(_ins == null) _ins = new T(); //第一次发生
    return _ins;
    }
    首次getInstance时,将实例初始化,这也是一个按需求值的例子,
    我们想象一个有很多module的系统,系统之间存在依赖性,比如说moduleA, 需要moduleB先启动,
有时我们显式地去调用它.
void system_ini() { ModuleA.init() ModuleB.init() ... }

这样的话,我们需要显式维护初始化过程,但使用单例,我们只需要getInstance,让正确的初始化顺序自动发生!
  • 衍生出的 memory 模式
    在求值时,我们总在第一次求值,之后将之存于字典(而不先将之计算存于字典),这是一种空间与时间的均衡的技术,
    int hard_calc(int i)
    {
    if(_cache.find(i) ) return _cache.get(i); //
    int result;
    ////very hard word...
    _cache.add(i, result);
    return result;
    }
    如果你做过游戏物件管理,这种模式到处可见。
  • 懒惰的本质,是按需求值
    在大多数编程语言里,传入函数的参数,是一种严格求值,但并非是所有语言都是严格求值,但,还有一类语言,具有不同的求值策略,比如说 haskell,
    懒惰,但有想象力,你可以定义一个无限长自然数组,
    • from_2 = [2..] #它并不实际发生,当你foreach遍历取值时,真正的求值才发生。
      然后定义过滤器。
    • filter pred #它将一个流转成另一个流,当你foreach遍历时,里面值按需生成
      然后我们滤去这个数组所有被第一个值整除的数
    • filte from_2 (/x -> x / 2 == 0) ,同样,它不是对序列上的所有值立刻发生,
      它得到 [3..]
      重复最后一点,我们得到这样的数列,
      [2..] [3..] [5..] [7..] ...
      很眼熟吧,埃拉托斯特尼筛法, haskell 里算法很直接
      from_2 = [2..]
      sieve list = h : (sieve $ filter (% h) $ t)
      where h = head list
      t = tail list
      first_20_prims = take 20 $ sieve from_2
  • 好吧,我用c#来写一个,
    //[...2,3,4,5,6...] IEnumerable from_n(int n) { int i = n; while(true) yield return i++; }IEnumerable filter(IEnumerable src, Func pred) { foreach(var i in src) if(pred(i)) yield return i; }IEnumerable sieve(IEnumerable src) { var factor = src.First(); yield return factor; var rest = sieve(filter(src, x => x % factor != 0)); foreach (var i in rest) yield return i; }static void Main(string[] args) { Program p = new Program(); var prims = p.sieve(p.from_n(2)); foreach(var i in prims.Take(1100)) { Console.WriteLine(i); } }

    也许你会说,无论是hakell 或是c#实现,都用到了语言的”特别"支持,但,如果经过了深入思考(lazy), 你也 可以在c/c++ 做类似实现,本来我打算写一个,因为懒惰, 这个问题留给你 (注:这题有难度,提示,实现类似IEnumerator接口的iterator,你写得多短,证明你有多(理解)lazy)

    推荐阅读