【懒惰模式】"懒惰、傲慢、缺乏耐性是程序员的三大美德"——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, functrueValue, func falseValue)
{
if(p) trueValue() else falseValue()
}
//丑了点,但将就
my_if(true, ()=>million, ()=>1/0 )
你可能已经发现,传入函数是实现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
- from_2 = [2..] #它并不实际发生,当你foreach遍历取值时,真正的求值才发生。
- 好吧,我用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)