<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Random Taste</title>
  <link rel="alternate" href="http://randomtaste.appspot.com/" />
  <author>
    <name>Yuheng Kuang</name>
    <uri>http://randomtaste.appspot.com/</uri>
  </author>
  
  <entry>
    <title>译者序：社会化Web设计</title>
    <link rel="alternate" href="http://randomtaste.appspot.com/view/page/designing-for-the-social-web"/>
    <summary type="html"><![CDATA[ <p><i>嗯，相当偶然的机会，翻译了这本 <a href="http://www.amazon.com/Designing-Social-Web-Joshua-Porter/dp/0321534921">Designing for the Social Web</a> ，本书将由图灵图书出版，很有趣的书。</i></p>

<p>简而言之,这本书就是——大白话。没什么高深的理论,每个现 象,每个方法看起来都理所当然,似乎都不必大费周章写一本书来说 明。</p>

<p>这就是魔法——它把“产品设计”这个复杂、混乱、充满不确定性 的话题组织成体系,点化成了大白话。这种魔法,我在 Steve Krug 的名 作 Don’t Make Me Think 中就见识过。我想,魔法的秘诀之一在于观察视角:从人自己的心理出发,观察产品设计。读这本书的就像是在读自己 的心声——“啊!没错,我也有这种感觉!”,之后再来看针对这种心理的设计思路,岂有不理所当然的?</p>

<p>我从大学做 BBS 技术站长开始就一直跟社区产品折腾,从版聊到 爆吧到博客互踩、微博互听,见过了各种各样匪夷所思的用户行为。看 得多了,越来越觉得用户不可捉摸。而从本书的视角看,用户行为千奇百怪,其背后的动机却是稳定一致的——互惠心理、荣誉感、群体归属 感...... 这也是我们自己的心理,他们跟我们没什么不同,没那么不可捉 摸。了解了这些深层动机,再看到“奇怪”的用户行为,我们也许能够 会心一笑,理解用户,更好地为他们服务。</p>

<p>所以,对我来说,读这本书是一次重新发现之旅。我以一个新的, 却又是最自然的角度,重新审视过去经历过的产品,回味发现过的每一 个古怪现象,反思每一个设计决策,拥有了许多有趣的新发现。</p>

<p>翻译其实就是最细致的品读,翻译工作也是充满惊喜的发现之旅。 本书引用的资料跨度很大,从 IT 行业经典文章到流行事件,从《影响 力》到《大教堂与市集》。我也跟随这些引用阅读了许多优秀的文章, 接触了许多以前从未接触过的话题,光是纯粹的阅读的快乐就让人回味不已。</p>

<p>Joshua 是在 2008 年写作这本书的。在互联网行业,两年是个很长 的时间,整个行业都发生了巨大的变化。但两年时间没有使这本书落伍,反而让它变得更加有趣:我们可以从行业和书中案例两年来的变 化,审视、验证本书观点,从而有更深的体会。行业瞬息万变,书中的 方法是否依然适用?相信看过本书后,各位读者都能有自己的结论。</p>

<p>听说我在翻译这本书,不少朋友都感到很奇怪:一个开发者来翻译 产品设计的书?我没觉得这有什么不合适,技术、产品与人的互动实在 让人着迷,不信看看本书第 6 章“群体智慧设计”,你的感觉会特别明 显。所以,我也强烈推荐技术开发者来阅读本书。</p>

<p>这本书的翻译也是一个社会化的过程,许多人参与了本书的审阅, 提出了大量宝贵的意见。我要感谢图灵公司为本书译稿把第一道关的李 松峰大哥,以及参与全书各部分审阅的邝圣凯、陈煜、黄嘉欣、黄卓贤 等同学。</p>

<p>我要感谢我的家人,没有你们的理解和支持,我无法完成这个工作。</p>

<p>我还要感谢正在读这本书的你。译者水平有限,错漏难免,还望批 评指正!</p>

<p>2010 年 6 月</p>
 ]]></summary>
  </entry>
  
  <entry>
    <title>特别能战斗，特别能想辙，特别能凑合</title>
    <link rel="alternate" href="http://randomtaste.appspot.com/view/page/page_2010_Jun_29"/>
    <summary type="html"><![CDATA[ <p>前几天我在 twitter 发了<a href="http://twitter.com/kyhpudding/status/17224841680">这么一条</a>: "特别能战斗，特别能想辙，特别能凑合。这用来形容国内互联网技术开发也蛮恰当的。" 其实当时是在看<a href="http://view.ccthere.com/article/2862339">这篇</a>不相干的文章, 一下子就被这句话吸引了.</p>

<ul>
<li>特别能战斗. 女人当男人用, 男人当畜生用, 这个应该算是行规了吧.</li>
<li>特别能想辙. 问题来了, 我们无论如何都能想到解决方法: 再古怪的查询需求, 总能在架构上绕通, 最多不就加个模块么; 开源实现太复杂, 语言不对, 没法定制实现需求, 立马给你山寨一套符合特殊司情的出来. 嘿嘿, 这些事我太有经验了.</li>
<li>特别能凑合. 具有惊人的在恶劣架构环境下生存的能力, 只要代码还能用, 只要架构还能跑, 无论在上面折腾事情多困难, 我们都能折腾下来.</li>
</ul>

<p>想想真的很敬佩我们的 IT 民工. 我们处在世界上发展最快, 也是最野蛮的一个市场, 整体技术水平却又相对较低, 还顺便处在人极其不值钱的国家, 堆人, adhoc 解决方案, 捏着鼻子赶进度, 其实是很合理的选择, 你不得不如此.</p>

<p>这是止痛的吗啡, 你不得不打, 但打多几次, 你就要上瘾了.</p>

<p>不断 adhoc 方案的累积, 使架构越来越混乱, 开发越来越难, 架构改动的成本也越来越大, 所谓积重难返. 那就只好用更勤奋, 更能战斗的民工和更多的 adhoc 方法来搭够了 --- 一个更能战斗, 更能想辙, 更能凑合的循环. 这个过程是有天花板的, 总有一天要下决心推倒重来, 但不是每个人都能过这一关.</p>

<p>这也阻碍了技术水平的提升. 我们有很聪明的技术人才, 他们能想出很好的解决当前问题的点子, 却很难有时间去学习, 总结, 沉淀, 难以有成体系的经验和创新. 我经常提小公司大公司的概念, 在市场上他们是大公司, 但在技术水平上, 他们还是小公司, 一个太大发展太快的市场环境导致两者的不平衡, 开发者被紧逼的市场产品需求压得喘不过气来, 发展到现在, 还是山寨小作坊的模式.</p>

<p>而我认为最可怕的, 是对人心理的影响. 一年年新人进来, 老人带上手, 在"培训"中, 不甚完善修修补补的架构和模式被灌输成了经典设计, 成了教条. 选择 Adhoc 方案不是不得已而是理所当然. 这时候人的忍耐力会极高, 能够让团队以理所当然的, 低效率的方式持续运转下去 --- 吗啡打上瘾了. 技术架构的积重难返, 人技术的难以进步, 心理因素也是重要原因.</p>

<p>特别能战斗, 特别能想辙, 特别能凑合, 技术的低水平山寨式发展, 这是不得已的现实. 但不是不能改变的.</p>

<ul>
<li>保持谦虚, 开阔眼界. 我们不得不承认我们的差距, 在技术上我们鲜有世界领先的成果. 市场巨大成功导致连技术一起过分自信, 高估自己的经验, 等待灌输的应届生, 临时方法成了经典模式, 闭门造车的创新, 我还没想到比这更糟糕的组合.</li>
<li>拥抱变化和魄力. 我承认 adhoc 方法, 不求设计次次完美, 只需始终记得这必定要被抛弃. 人人都在谈拥抱变化, 这需要开发方法, 也需要架构支持, 还需要全团队的魄力. 一个任务, 可以用旧方法有点丑陋但比较保险地完成, 新方法有风险而且工作量更大, 如何选择? 新方法失败, 会不会从此裹足不前?</li>
<li>闲人. 若给每个人 20% 时间不现实, 那至少也要有一点闲人. 外界技术学习, 开源成果研究, 新思路的酝酿, 都是没法在项目压到跟前时完成的, 需要有人不受产品项目约束来来进行这些工作. 只是问题在: 如何衡量闲人的成果, 如何给闲人制定 KPI?</li>
</ul>
 ]]></summary>
  </entry>
  
  <entry>
    <title>态度, 视野和基础</title>
    <link rel="alternate" href="http://randomtaste.appspot.com/view/page/campus"/>
    <summary type="html"><![CDATA[ <p>上星期应邀回中大跟大一大二的信科师弟妹们做了一次交流. 蒙各位老师看得起, 让我这个挨踢民工也有机会在讲台上大放厥词. 大学的这类交流很多, 中大也能请来许多名人, 高管, 成功人士. 讲成功学, 讲各种各样的做人之道, 教导大家 follow your heart, 我显然不是其中一员.</p>

<p>在交流准备时还有些小插曲, 似乎大家都很在乎我的头衔, 一开始联系我的陈书记也以为我是做管理的, 大概已经是经理了, 后来负责组织的同学写的宣传文案, 发现我的 "高工" 和 "基础架构工作" 不知怎么地就变成了 "高管" 和 "高级架构师", 把我吓了一大跳. 让我最安心的, 当然也是最能说明我自己的, 还是我在网站上的这个头衔: Geek, Programmer, Tech Evangelist. 当然我非常理解, 要是文案上这么写, 估计就没人来了.</p>

<p>这个小插曲, 也让我对这个交流更为重视. 这个讲台上不缺成功故事, 不缺高管, 甚至不缺学术和技术大牛, 但似乎从来没人与大家分享从技术工作中获得的纯粹的快乐和满足 --- 或者简单点说, 没有 Geek.</p>

<p>于是这成了我的交流主题, 我讲的不是如何获得成功, 而是如何获得快乐. 我的主题演讲完毕后, 主持师弟给我的演讲做了很好的概括: 态度: 技术热情和独立精神, 视野: 开阔眼界, 兼容并包; 和基础: 本科课程基础.</p>

<p>我给出了认知的四个阶段: <strong>知道, 学习, 研究, 创新</strong>, 以上三点应能在认知的进化中形成良性循环.</p>

<ul>
<li>真正热爱技术的人们, 才会有探索深入的热情. 只需一些技巧和点拨 (我依旧推荐了 "提问的智慧"), 就能帮助他们有效地探索更深, 更广的技术世界.</li>
<li>更多的信息帮助开阔视野, 看得更多, 则愈能发现基础的重要. 我几乎每次都会重点推荐 SICP, 因为它正是让我的眼界豁然开朗, 端正学习态度的关键.</li>
<li>静下心来, 为支持自己的兴趣而不是考试过关来学习学科基础知识. 成体系, 扎实的基础知识能帮助我们更好地探索新技术, 新知识. 各种知识和技术相互印证, 相互启发, 能促进技术的深入研究和创新.</li>
</ul>

<p>但无论从我自己的大学经历, 还是与师弟妹的接触交流. 我看到的往往却是一种恶性循环.</p>

<ul>
<li>高分考上计算机系, 以为就业前途光明, 却发现上了贼船. 大一大二艰难的数学课程把对技术仅有的兴趣打击得干干净净. 陷入了一种常见的 "迷惘" 状态, 一种典型表现是抱怨完 "迷惘, 看不到方向" 之后, 没日没夜地打游戏.</li>
<li>没有靠谱的信息源. 跟一些视野同样狭窄的人学习 "实用技术". 我很吃惊同学们现在就在选择 "技术道路", 是往 linux 发展还是往 windows 发展? 选 Java 还是 C#?</li>
<li>狭窄的视野的直接反应就是鄙视大学课程: 不实用, 太落后. 毫无疑问, 他们没学好基础课程, 那点 "实用技术" 也将永远无法跨过 "学习" 到 "研究" 的门槛.</li>
</ul>

<p>他们的大学生活被提炼成几条很明确的, 可以直接推理的道路. 学 "实用技术", 多做项目积累项目经验, 选对 "技术方向", 找工不难; 搞 ACM, 获奖, 进 BGM (Baidu, Google, Microsoft); 搞好成绩, 保研... 他们用自己的杯具和社会宣传一起勾画挨踢民工的悲惨图景: 技术是碗青春饭, 你永远跟不上技术发展脚步, 不想杯具的话就赶紧在变老前转管理, 做 XXX 经理, 当高管......</p>

<p>于是每年校园招聘, 都要让我这个中大师兄失望一回. 所以我希望做一点事情. 我希望有更多的同学怀着兴趣和热情学习, 夯实基础, 做好信科学生的本分 (我承认我没做好); 我希望大家能看得更多, 看得更广, 能自己把握自己的前途. 我希望大学经历能帮助我们超越模仿和跟随, 成为促进技术发展的力量. 我不怨业界浮躁, 浮躁的业界就是由我们自己构成的, 我们都是这么从大学走过来的.</p>

<p>态度, 视野和基础, 我认为视野是个关键环节. 我在交流中也说到, 这不仅是大学生的问题, 也是整个业界的问题. 从业人员的视野狭窄影响学生 (极端例子如 O(bug) 老师), 视野狭窄的学生走向业界, 这又是一个恶性循环.</p>

<p>我了解到, 中大的信科学生入学第一课 "计算机文化" (好像是类似的东西) 已经改革, 会请校内外教授大牛做讲座, 讲信息科学的各个方面, 讲一些新发展. 但我担心的是, 这些内容是否会对新生太玄乎, 是否会没有相应实践, 听过就算. 在我给出的认知层次里, 那就只能达到 "知道" 的级别. 也许能激起兴趣, 但肯定无法持久.</p>

<p>我对大学第一课的想法, 也许有点不切实际.</p>

<ul>
<li>类似 MIT 6.001 (SICP, 北大有开这门课) 或是现在的 6.00 课程作为入门课程. 使学生在编程实践中, 较全面地看到计算机科学的各个方面, 使学生了解: 为什么要学 XXX 课程 (这是我被问得最多的问题), 产生主动学习这些知识的需求. 并且掌握一些基本能力: 编程和基本的科学方法.</li>
<li>在入门课程中获得独立查找信息, 独立学习研究的习惯和能力. 习惯使用搜索引擎, 习惯阅读英文. 将课外阅读和学习作为必备要求 (例如阅读某几篇 papaer 的读后感). 不去讲什么 "最新技术动态", 让学生自己去接触了解.</li>
</ul>

<p>是的, SICP 很难, 要大一学生读 paper 看起来也很难, 但我没觉得这会比一面惘然地折腾数学分析难. 学生们在一片漆黑中痛苦挣扎, 和看不懂也不知道有什么用的数学符号做斗争; 远处有人在描绘计算机科学的美好图景, 但他们还是什么都看不到; 一旦抱住了哪根树枝, 他们就会抱紧不放, 就此安心.</p>

<p>我们要做的, 就是把灯打开.</p>
 ]]></summary>
  </entry>
  
  <entry>
    <title>并发处理模型, 从 Reactor 到 Coproc</title>
    <link rel="alternate" href="http://randomtaste.appspot.com/view/page/coproc"/>
    <summary type="html"><![CDATA[ <h2>简介</h2> 
 
<p class="first">本文介绍一个正在开发的 C++ 服务框架 coproc 在并发处理模型上的设计思路. coproc 基于 libevent 和基本的 Reactor 模型, 在此之上逐步实现了轻进程以及类似 UNIX fork-wait 的并发模型, 并利用 ucontext coroutine 机制实现了真正的&quot;进程&quot;上下文切换. 从而实现从事件驱动到顺序处理, 从异步到同步等待的模型进化. 在实现 UNIX 传统并发抽象的同时, 保持了相当高的运行效率.</p> 
 
 
<h2>Reactor</h2> 
 
<p><a href="http://en.wikipedia.org/wiki/Reactor_pattern">Reactor</a> 是很常见的并发处理模型, 将业务组织成各种事件驱动的对象. 一个监听接收新 TCP 连接的 Reactor 会像是:</p> 
 
<pre class="example"> 
class ListenReactor : public Reactor
{
  ...
  void OnSockEvent(int listen_fd, int ev) {
    int sock = accept(listen_fd, NULL, 0);
    ...
  }
}
</pre> 
 
<p>利用 libevent 作为事件驱动器, 经过少许包装, 让 Reactor 可以注册到句柄事件上, 回调 OnSockEvent, 这段代码就可以运作了, 非常简单.</p> 
 
<p>NOTE: 在实现中, 我们有任务/消息队列实现异步的多类型消息/事件分发, 下面的事件触发或消息传送或方法调用 (都是一样的东西) 在模型上都是异步的, 同步直接调用只是一个 shortcut, 下面不再重复. 消息队列是支持跨线程协同, 容量控制等的关键, 也是性能优化的重点, 在后面, 它还担负类似 OS 任务调度的工作 (所以它叫 Scheduler), 但与这里讨论的问题无关, 在此略过.</p> 
 
 
<h2>模型改进</h2> 
 
<p class="first">&quot;监听接收新连接&quot; 这个任务从文字上就是事件驱动的. 但 &quot;从 socket 读取 N 个字节&quot; 这个任务不是, 它本来就是过程式的. Web 应用服务所要处理的绝大多数业务属于后者, 从模型上并不与事件驱动模型吻合. 将一个有复杂业务流程和 IO 交互方式的 Web 应用完全组织成事件驱动的 Reactor 方式, 无论设计, 编程, 调试还是后续的维护都是一个极大的挑战.</p> 
 
<p>我们需要在 Reactor 模型之上实现异步的 <strong>调用-返回语义</strong>, 这是过程式处理逻辑中最常见的模式:</p> 
 
<ul> 
<li>一个任务由一个独立 Reactor 处理. 例如不存在管理程序全部 socket, 提供统一 IO 服务的 Reactor. 而是一个 IO 交互任务, 则创建一个专用的 Reactor 来完成.</li> 
<li>引入标准事件: OnInit(), 在 Reactor 任务开始时触发, OnStop(), 在 Reactor 因任何原因停止 (自行停止或他杀) 时触发.</li> 
<li>引入关联 link 机制: 若 A 与 B 关联, 则当 B 停止时, 触发 A 的 OnLinkReturn(B) 事件.</li> 
</ul> 
 
<p>由此, 我们在 Reactor 上实现了与过程式处理中调用-返回对应的模型:</p> 
 
<ul> 
<li>调用: 创建子任务 Reactor, 将其与父任务关联, 并启动之. 我们把这个过程称做 Spawn</li> 
<li>返回: 子任务完成时触发父任务 OnLinkReturn 事件, 在事件处理中, 父任务可获取子任务完成状态等信息, 然后销毁子任务 Reactor, 完成这个 Reactor 生命周期.</li> 
</ul> 
 
<p>在这种模型下, 我们可以用熟悉的思路实现对逻辑的分层封装. 在良好的封装设计下, 除了最底层的 IO 处理 Reactor 外, 其他的 Reactor 只需处理 OnLinkReturn 事件, 根据各种子任务的返回改变状态, 驱动下一步处理, 即可顺利完成任务.</p> 
 
<p>一个 RPC Client 的处理可以实现为 (随手写的, 不用深究细节):</p> 
 
<pre class="example"> 
class RPCClient : public Reactor
{
  int m_stage;
 
  virtual void OnInit() {
    m_stage = STAGE_SEND;
    m_send_reactor.SetRequest(request);
    Spawn(&amp;m_send_reactor);
  }
 
  virtual void OnLinkReturn(Reactor *src) {
    if (m_stage == STAGE_SEND) {
      m_stage = STAGE_RECV;
      Spawn(&amp;m_recv_reactor);
    } else {
      Return(m_recv_reactor.GetResult());
    }
  }
}
</pre> 
 
 
<h2>Proc</h2> 
 
<p class="first">在上例中, 我们的需求实际是等待发送完成, 再启动读取任务, 等待读取完成后再取出结果. 等待是这个任务的天然属性, 我们需要实现支持. 当然, 我们不会让等待任务阻塞在 OS 进程或线程上. 我们做的, 实际上只是对上面代码的一点小改进.</p> 
 
<pre class="example"> 
class RPCClient : public Reactor
{
  int m_next_stage;

  void OnInit() {
    Process(STAGE_INIT);
  }
 
  void Process(int stage) {
    switch (stage) {
    case STAGE_INIT:
      m_send_reactor.SetRequest(request);
      Spawn(&amp;m_send_reactor);
      return SetWait(STAGE_SENT);
    case STAGE_SENT:
      Spawn(&amp;m_recv_reactor);
      return SetWait(STAGE_RECV);
    case STAGE_RECV:
      return Return(m_recv_reactor.GetResult());
    }
  }
 
  void SetWait(int stage) {
    m_next_stage = stage;
  }
 
  void OnLinkReturn(Reactor *src) {
    Process(m_next_stage);
  }
}
</pre> 
 
<p>在这里, 我们把业务处理分成若干个阶段 (stage), 各阶段处理都是非阻塞的, 当它 Spawn 子任务, 并需要等待子任务返回以进入下一阶段时, 它设置等待被唤醒后的状态即可退出. 当它所等待的唯一事件 &mdash; 子 Reactor 返回到来时, OnLinkReturn 执行统一的任务: 根据上次设置的返回状态, 调用 Process 进行下一 stage 处理, 如此一步步下去, 直到任务完成停止.</p> 
 
<p>我们没有引入新功能, 只是对代码结构进行了简单优化. 但我们发现, 在这里, 唯一一个事件处理流程也被统一化, 被框架接管. 处理流程被组织成已经与原本过程式处理非常类似的代码. 在上述简单实现中我们仅实现了对一个子任务的等待, 稍加修改, 即可实现等待所有子任务返回的语义. 这些模式也许不够灵活, 但常用且易用. 虽然只是一些简单的代码改造, 但现在, 我们的模型已经根本不是事件处理模型了.</p> 
 
<p>我把这种模式叫做 <strong>Spawn-Wait</strong> 模式, 或者叫 fork-wait 模式. 这是 UNIX 最简单, 最直接的实现并发的方式: fork 一堆子进程做事, 然后等它们都返回. 我们在 Reactor 引入了进程状态概念: READY, WAIT, DONE, 我们以非常轻量的方法实现了基本的进程语义, 不同的是我们现在能非常高效地创建成千上万的进程, 让它们并发处理, 我把它称作轻进程. 在实现上, 它只是一个很简单的 Reactor 派生类, 可以叫它 Proc.</p> 
 
<p>我们把任务组织成了父子进程/Reactor 的树状关系, 这也让 C++ 下非常头痛的资源管理有了新的出路. 在目前实现中, Proc 带有一个计时器 (啊, 你可以把它理解成发 SIGALRM 的), 在 Proc 超时时会强行终止 Proc, 由于 Proc 维护了所有正在等待的子 Proc/Reactor 列表, Proc 终止时也会终止所有这些子进程, 有效回收所有资源 (内存, 句柄, 在 libevent 上的注册等等).</p> 
 
 
<h2>Coproc</h2> 
 
<p class="first">然则, 上面的处理模式仍然太笨拙. 说直白点, 这种依赖 stage 跳转的模式就是在用 GOTO 写程序, 更复杂的逻辑组织, 或是已有同步业务流程的移植依然是很困难的.</p> 
 
<p>在 Proc 中, stage 的作用实际上是管理处理上下文, 当等待返回时, 通过 stage 回到恰当的处理逻辑中. 实际上, linux 本身就有 userland 的上下文切换管理机制 ucontext. 具体可参考 makecontext(3) 系列 manual.</p> 
 
<p>使用这样的机制, 我们可以实现 Yield, Yield 可以在任何地方切换出当前处理流程, 进入 Proc 的等待状态, 而 OnLinkReturn 唤醒时, 则将 Yield 保存的上下文恢复, 继续 Yield 下面的流程. (NOTE: 这里的 Yield 比 python 的 yield 强, 它能在多层调用栈之上使用, 下文代码就是例子).</p> 
 
<p>RPC Client 的例子太没难度, 下面逻辑实现两两并发调用 DB Reactor, 当它们返回结果总数超过 1000 时退出返回.</p> 
 
<pre class="example"> 
class Searcher : public Coproc {
  const static int NUM_DBS = 256;
 
  int GetDouble(DB *db1, DB *db2) {
    Spawn(db1);
    Spawn(db2);
    Yield();
    return db1-&gt;GetNumResult() + db2-&gt;GetNumResult();
  }
 
  virtual int Main() {
    int total = 0;
    DB dbs[NUM_DBS];
 
    for (int i = 0; i &lt; NUM_DBS; i += 2) {
      total += GetDouble(&amp;dbs[i], &amp;dbs[i + 1]);
      if (total &gt;= 1000) {
        break;
      }
    }
    return total;
  }
}
</pre> 
 
<p>可以看到, 代码已经基本与同步逻辑代码无异了, 我们把这种模型称为 Coproc.</p> 
 
<ul> 
<li>比之 Proc, Coproc 更进一步贴近了普通进程, 当然, 付出了预留 ucontext 栈空间和一点 ucontext 切换代价, 因而也更重了.</li> 
<li>比之 OS 进程或线程,  Coproc 不通用: 切换时机由自己控制, 不到自己 Yield 不会被换出, 一个长的纯 CPU 任务就能把系统堵死; 它不会自动处理会阻塞的 read(2) 等接口等等, 但这换来的就是小得多的资源消耗.</li> 
</ul> 
 
 
<h2>应用</h2> 
 
<p class="first">虽然在 Reactor 上做了那么多事, 但令人吃惊的是, 对底层机制来说, 模型并没有什么改变: Reactor 依然处理事件, 只不过 Proc 和 Coproc 的事件处理被框架接管, 事件处理依然非阻塞. 对 libevent 来说, 整个系统没什么变化. 因此, 在底层, libevent 和 Reactor 模式依然用原来的方法保证系统良好的并发处理能力; 在上层, 从 Reactor 到 Coproc 都是互相兼容, 可以互相调用的. 开发者可以根据需求权衡, 选择最合适的模型.</p> 
 
<p>例如, 在一个 Web 应用服务中. 端口的监听仍由基本 Reactor 实现, 而相对稳定通用的业务框架和数据访问层则由 Proc 实现 &mdash; 再复杂的 IO 模式都不在话下, 而业务逻辑则可以实现为 Coproc, 原同步框架下的代码也能很方便地移植过来. 一个请求到来时, 监听 Reactor 直接创建一个业务框架 Proc 来进行处理 &mdash; 这是 UNIX 最基本的 accept-fork 模型, 我喜欢这种优美自然的模式, 而且现在我可以尽情创建成千上万的 Proc. 业务框架 Proc 指挥整个业务处理流程, 涉及多个 Coproc 编写的业务逻辑模块, 虽然代码逻辑是同步的, 但 IO 能够天然并行.</p> 
 
 
<h2>总结</h2> 
 
<p class="first">从 Reactor 到 Coproc, 我们不断进行权衡, 通过对模型加以恰当的限制和规范, 以及少许额外机制, 我们能从难以设计, 编程和调试的事件驱动模型进化到与普通同步模型相差无几. 在这个过程中, 我们很大程度上重造了轮子: OS, 我们在实现中不断加入了各种 OS 机制. 其实无论 IO 任务还是 OS 本身就是事件驱动的 &mdash; 由硬件中断驱动. 我们只是以稍微不同的, 严重轻量级的方法将事件驱动到顺序处理, 异步到同步阻塞的路重走了一遍而已.</p> 
 
<p>我想, 这样做是有价值的, OS 本身对并行提供了优美, 成熟 &mdash; 更重要的是, 大家很熟悉的抽象. 但作为通用 OS, *nix 不得不考虑更多情况, 实现更完整的封装和抽象, 导致其基本并发模型: 进程严重的 overhead. 以往, 我们要不对其并发模型作折衷: 例如, 放弃每个请求 fork 一个进程的做法, 而是使用进程或线程池, 更不会为并发请求而开一堆进程; 又或是另起炉灶, 完全放弃其模型, 例如采用全事件驱动模型. 而我的思路是, 在特定条件下, 我们可以保持这个成熟的模型, 而根据特定情况, 对 OS 机制的实现作权衡折衷.</p> 
 
<p>这个思路显然受到 exokernel 研究和 erlang 的影响. 这里的轻进程模型显然没有 erlang 的灵活强大, 但相信对 UNIX 程序员来说, 会是一个更熟悉, 更易上手的模型.</p> 
 
<p>本文提出的模型与 <a href="http://blog.csdn.net/xushiweizh/category/556700.aspx">CERL</a> 很类似, 但具体设计差异巨大. 本文的贡献在于, 展现参照 OS 设计, 从有成熟开源实现的 Reactor 模型到 Coproc 模型的思路, 说明模型选择并不是非此即彼, 而是可以根据情况权衡决定, 根据需求灵活定义 (除了 Proc 外, Reactor 还演化出了条件变量等不少模型) 的. CERL 项目远景宏大, 希望造出整套框架或系统, 而 coproc 专注于模型问题, 把 IO 事件处理 (libevent), 服务协议和描述 (protobuf etc) 等交给其他成熟的开源软件处理.</p> 
 
  ]]></summary>
  </entry>
  
  <entry>
    <title>流言终结者与科学精神</title>
    <link rel="alternate" href="http://randomtaste.appspot.com/view/page/mythbusters"/>
    <summary type="html"><![CDATA[ <p>流言终结者  (<a href="http://dsc.discovery.com/fansites/mythbusters/mythbusters.html">Mythbusters</a>) 是我最喜欢的电视节目之一. 看这帮人用实验 --- 往往是大规模的 --- 去证实或证伪各种都市传说, 病毒视频, 让人着迷的不是验证结果, 而是他们的实验方法和过程.</p>

<p>不管验证的流言有多无厘头, 他们都遵从相当严谨的科学研究方法:</p>

<ul>
<li>首先细化流言, 确认流言可被检验, 指出检验标准. 他们并不检验无法用科学方法证实或证伪的东西 --- 比如鬼魂.</li>
<li>研究团队会找寻相关可靠资料, 特别是如蒸汽机关枪等历史相关的流言, 也会拜访专家征询意见. 他们还会事先进行理论计算, 对流言做估计.</li>
<li>这一切都是为了下一步: 更好地设计和执行实验. 他们使用常用受认可的科学方法, 设置对照组, 使用双盲测试, 重复试验... 甚至非常个人化的流言: 酒眼是否真能出西施, 他们也拟定了相当靠谱的测试方案.</li>
<li>公布测试方法和测试过程 --- 这就是他们的节目. 当然他们的测试过程和内容会比电视节目上更多. 虽然大部分的实验由于安全或规模原因不可被一般家庭观众重现 (各种开车省油的流言倒还是可以的, 除了把车子表面高尔夫球化那个之外), 但他们可以给出足够重现实验的信息.</li>
<li>通过测试得出结论, 结论可能与理论计算不符, 这很正常, 因为理论计算可能少考虑了某些因素. 但最终结果应该也要有科学的解释.</li>
<li>最后, 实验接受挑战. 在其官方网站, 人们可以提出意见, 指出漏洞. 他们有旧案新解特集, 根据观众要求用新方法重新检验流言, 并且确实推翻了几个之前的验证.</li>
</ul>

<p>这些都是基本的科学研究方法, 要论实验的可重复性和可靠程度, 这档大众节目要远好于各研究领域中泛滥的灌水和造数据的论文. 流言终结者让我们用科学的方法看待普通生活中的问题, 并且 --- 很有趣!</p>

<p>这是一个很好的科学精神的体现. 科学精神不是让我们崇拜科学, 让我们被一大堆高深的学术名词征服. 正好相反, 科学精神让我们在生活学习工作的方方面面, 具有怀疑和实证精神, 以是否有事实证据, 是否遵循科学推理方法, 而不是以是否出自权威, 是否合自己口味来衡量任何信息的可信性. 我想这样的精神和这样的科学研究方法, 不应该是到要做研究写论文的时候才来学习, 而应该是每个大学生, 甚至每个国民所应该具备的.</p>

<p>这样或许社区会少一些连争论者自己都不知道在争什么的 flamewar.</p>

<p>这样或许社区会多一些可靠的 benchmark.</p>

<p>这样或许我们能少一些疯狂流传却一戳就破的谣言.</p>

<p>这样或许我们能够在领袖狂热中保持清醒, 无需生生饿死那么多人, 无需在十数年后为 "实践是检验真理的唯一标准" 这一显而易见的道理再引发大讨论, 并引为理论界一大成果.</p>

<p>春节的时候我回老家, 看到小侄儿在看的科普读物, 不出所料看到的不是充满神秘主义色彩的 "世界之谜" 就是各种各样孩子绝对看不懂的科学名词. 这让我非常担心. 等到他会看流言终结者的时候, 估计他能在电视上看到的科普节目就是走近科学, 这可就离科学越来越远了.</p>
 ]]></summary>
  </entry>
  
  <entry>
    <title>Web 应用开发的胡思乱想</title>
    <link rel="alternate" href="http://randomtaste.appspot.com/view/page/thinking_about_webapp"/>
    <summary type="html"><![CDATA[ <p>有的想法, 总想等它成体系了, 能出代码了, 再写出来, 结果思维导图话了一大堆还是没出来, 有的想法就这么积压几年, 不用说肯定有的忘了, 虽然我不可能记得我忘了啥. 所以我觉得还是应该把它写出来, 记录下来, 虽然是完全不成体系的流水帐. 也跟这个站的名字相符: Random Taste, 随机的味道, 无论是什么方面, 任何一点的感受, 都应该尽情书写. (事实是, 我想注 randomstate 来着, 没抢着, 不过 taste 和 state 只是字母排列顺序不同而已).</p>

<h2>重新思考浏览器</h2>

<p>还是从浏览器的原本用途说起吧, 浏览器者, 浏览文档之工具也, 这个文档就是 HTML 文档. 然而这个定义正在悄悄改变, 看看 Google Chrome 到 Chrome OS, 浏览器渐渐变成了底层操作系统. 上面跑着的, 是用 HTML, CSS, Javascript 写成的, 越来越接近桌面体验的 web 应用.</p>

<p>我们用尽奇淫技巧利用极受限制的技术实现拖拽, 实现富文本编辑器, 实现服务端消息推送... 结果, HTML5 给他们定义了标准的标签属性和 API, 全部交浏览器实现. 于是我们恍然大悟: 原来我们是在干浏览器干的活!</p>

<p>再看更一般的例子, 譬如这个站, 无论哪个页面, 头上都顶着个 Think different, 旁边也都有个侧边栏, 最下面是个翻页. 这些东西跟这篇文章基本上没有关系 --- 它只是我这个站的框, 一种修饰 (decoration), 这些修饰有的是好看, 也有的是让你更方便的浏览 --- 比如上下翻页, 回到首页 --- 有没有觉得, 这些功能其实就是浏览器工具栏上面那排按钮应该干的; 而且页面上实际显示了多个文档 (在实现上侧边栏就是个普通文档). 我这么简单的页面也不是纯文档, 也有一大堆代码干了控制界面, 干了浏览器该干的活.</p>

<p>有这么个说法, 要实现 HTML 文档的展现与内容分离, 应该用 CSS 去描述 HTML 文档的展现. 这话没错, 但展现不是界面, 界面不仅是视觉效果, 有状态. 理论上, 页面的上下页这些元信息应该用 link 标签放到 head 里, 但浏览器不支持, CSS 也没帮上什么忙, 最后还是要我自己写一坨跟内容无关的 HTML 和 Javascript 抓出上下页信息, 再画到界面上去 --- jQuery 提供了与 CSS 一脉相承的方式实现这一功能.</p>

<p>这让人很自然地想到, 其实 HTML 已经同时承担的两种角色: 表述文档和构建界面, 在 HTML5 中, 我们就显然看到标准们围绕这两大任务: 让文档更语义化, 更丰富; 让 webapp 的功能, 界面交互更好更强大. 而 Web 应用开发会变成这样的两大工作: 提供标准的语义化信息/文档 --- HTML; 以及用各种技术, 无论是 HTML 系列, 还是 Flash, 还是干脆就是传统桌面技术, 开发不同设备上运行的这些文档的浏览器. </p>

<p>绕了一圈, 我们又回到原本: Web 的核心价值还是信息的传播共享, 我们利用浏览器浏览这些信息. 不同的是传统的浏览器软件退居幕后成为 OS, 而我们的界面控制代码成为真正的浏览器. 其实这个想法丝毫不新鲜, 大家天天都在用, 整个 Firefox 的界面就是用 XUL + Javascript 做的. 然而 HTML 跟 XUL 之余界面的不同就在: 实现界面和文档的是同样的 HTML 标准, 我们实在太有动力为了方便, 为了省点流量, 为了操蛋的浏览器, 把界面和文档混在一起了, 概念上好像很美好, 但用 simple and dirty 的方式实现设计图要求的诱惑太大, 这可能是 HTML 的混乱, 尴尬现状的一大原因.</p>

<h2>语义化 HTML?</h2>

<p>文档和浏览界面分离的思路, 再加上现代浏览器的强大支持使我们完全能够剔除 HTML 文档中的展现控制, 实现纯净的语义化 HTML. 不过话又说回来, 为啥?</p>

<p>最直接的需求是设备无关性, 需要展现端根据设备能力定制内容的展现, 而且随着手机上网的流行这愈加重要, 这个很好理解. 而下面的可能更关键: 站点无关性, 信息的开放自由传播是 web 的灵魂, 通过 RSS 也好搜索引擎爬虫也好你的文档必然会出现在别的站点上, 如果你的文档就是一堆 DIV 靠 CSS 撑着的, 人家把 CSS 一换那成什么样儿了? 还有就是机器可理解性, 大家最熟悉的就是搜索引擎, 一个结构标签恰当的文档, 对理解文档, 找出重点极有帮助.</p>

<p>我现在担心的倒不是混着展现的 HTML, 而是另一个极端: 随心所欲的结构化数据. 越来越多的所谓 "纯 AJAX" 应用, 服务端只负责吐 JSON 数据 --- 格式是啥不重要, 关键是这数据的结构, 属性根本就是天书, 只有应用自己认得. 然后客户端负责把他们 "渲染" 成底层浏览器认得的标签和样式.</p>

<p>这直接破坏了 web 的核心: 这些数据文档没法再自由传播了, 因为没人认得. 它变成了一种 lock-in: 只有上这个站才能看到这些信息 (还好这些站点通常比较有良心地提供要了 RSS). 这对搜索引擎而言更是灾难, 遇到这种站点可能最好的方法就是让它在一个 webkit 内核上跑出个 DOM Tree 来, 然后呢? 这个站点可能连个 a 标签都没有, 都是在 element 上加 onclick, 天知道 click 了之后有什么后果, 代表什么意思? PageRank 依赖的最基本的东西都被破坏了.</p>

<p>这是做桌面软件, 不是做 web 的思路. 刚才说我们现在会定制浏览器, 但我们定制的这个浏览器, 必须还是个 web 浏览器. 它可以为特定的站点内容优化, 但决不应该成为站点的 "专属" 浏览器. 我的担心就是这些专属浏览器, 它们好像得到了更好的体验, 却失去了最有价值的东西: 自由流动的信息.</p>

<h2>语义的迷思</h2>

<p>"那你告诉我, HTML 哪个标签能表示人的性别的语义?" 这是我遇到的真实问题.</p>

<p>如果我们认为语义和展现是相对的. 一个论文作者认为它的语义就是它的论点, 推导, 数据, 至于数据是用图还是用表格来说明是纯粹的展现问题. 但一个排版设计师看到的语义就是标题, 分节, 段落, 图表, 他的任务就是研究内容的展现 --- 定行距页边宽, 决定一个图表放哪好. 前面问我这个问题的人显然是个论文作者, 而浏览器和 CSS 就是排版设计师.</p>

<p>我们讲的都是语义, 一个事物在不同上下文, 不同语义系统中的语义. 所以所谓语义 HTML, 在许多人眼里压根不语义.</p>

<p>如果我们要尽量多的人 (其实这里主要是指机器), 尽量充分地理解我们要传播的信息. 就得:</p>

<p>一方面, 使用尽量通用的语义系统作为基础. 例如, 作为文档, 分节分段, 列表, 链接这些必然会有. 有了这些信息, 电脑不见得能理解性别这么高深的东西, 但它至少知道哪些是重点, 信息间的层次关系, 会用链接联系资源, 也会分析进行合理有效的 crawling 和分析. 这是我们现在的 HTML 和 web.</p>

<p>当然这还不够, 另一方面, 我们需要信息载体有扩展性, 能够在基本文档上加入附加的信息或提示标签. 这些信息虽然有的机器不能理解, 但有的却可以根据它们做更多的事情. 这是以往 HTML 规范所欠缺的, XHTML 用 namespace 把 HTML 与扩展语义分离, 也没有实际解决问题, HTML5 则不但加入更多的语义标签 (其实我不 care), 还把 Microformat 换了个 Microdata 的马甲, 以及 RDFa 给加了进来, 让扩展信息的嵌入变得真正可行. 而且, 扩展也不能随意, 道理与 HTML 相同, Microdata 实际上给了一个非常详细的扩展规范.</p>

<p>如果你的文档内容用 Microdata 指明哪里是联系信息, 那么支持的浏览器 --- 或者是你自己的展现控制代码, 会显示界面, 一键加入地址簿或者直接拨号; 虽然可能别的站点并不支持, 而这不影响文本的正常展现; 但要是 Google 支持 (规范是个 Google 的家伙写的), 你的联系信息会以漂亮样式而不是文本摘要显示出来. 你的文档内容也还是可以嵌入只有你的私有格式化数据, 只有你的浏览器代码支持, 但至少不会影响他人获取基本信息.</p>

<p>实还可以注意到的是, 随着 web 传播的信息越来越广泛, 许多信息根本跟 "文章" 没有一毛钱关系, 所以 HTML 也当然不会是仅有的信息传播方式. 最重要的, 还是选择一个最多人理解的语义, 例如 SVG for 图形, 例如 RSS and ATOM for feed, 道理都是一样的. 当然了, 如果你认为你的信息不需要共享传播 --- 那么随你吧...</p>

<p>关于语义化的 Web (Semantic Web), 关于怎么撰写机器可理解的信息, 怎么让机器在不同概念体系上转换, 沟通, 这趟水非常深. 可以参考 W3C 的 Semantic Web 页面, 这里还是打住吧.</p>

<h2>新模式</h2>

<p>自我 05 年真正接触 web 应用开发, 一直都在想两个问题: 如何表达信息? 如何做界面?</p>

<p>如果是在那之前入行, 这两个问题实在没什么可讨论的: 网页即信息, 网页即界面, 服务器脚本把页面拼出来, javascript 玩儿几个特效就完事了. 但 04 年 GMail 震惊了所有人, 05 年正是 AJAX 爆发的时候. 很快, 一种所谓 ``全 AJAX'' 模式开始流行, 所有界面都在客户端使用 Javascript 完成, 真正的信息文档都变成了自定义结构的 JSON, 这也就是百度搜藏的做法. 习惯 GUI 编程的开发者很适应这种模式 (可惜我从来没做过正经的 GUI 应用), 到后来 GWT 推出, 这完全变成了 C/S 模式, GUI 客户端跑在浏览器上, 用 RPC 与服务端交互.</p>

<p>这里面当然也有担忧, 一个最基本的问题是这样的做法破坏了 URL, 也破坏了浏览器的前进后退等机制, 这用一些操作 URL fragment 字段的 hack 解决了, 但当时可能已经有人感到, 这破坏的是整个 web, 信息变成专有格式无法交流, 也无法简单地互相链接. 后台交互也是如此, RPC 风格的交互自然地使用了没有限制的 POST Method, 破坏了 HTTP 语义, 自然也破坏了 Cache 等 HTTP 基础设施.</p>

<p>所以, 我的问题是, 如何标准, 用适合 web 的, 广泛可接受的标准方法表达信息, 促进信息的共享传播; 以及在这前提下, 如何提供丰富的用户体验?</p>

<p>对第一个问题, 简单回答: 语义化 HTML, 这首先会遇到上文的语义迷思. 而更严重的问题: 好吧, 信息你都 "表达" 出来了, 你怎么在浏览器上展现? 正如上文所说, 有了语义化的 HTML 文档, 你用 CSS 是做不出个 RIA 的. 对这个问题的回答, 又回到了本文的开头: 重新思考浏览器.</p>

<p>我们来思考一个类似 friendfeed 的应用: 从不同的数据源收集信息展现, 这些信息源有外部通过 RSS 等标准协议过来的 feed, 也有内部的数据, 例如谁偷了你的菜的消息. 不同源的信息结构可能很不同, 所以无论如何, 信息间的各种转换是必要的. 我们希望尽量理解特殊的信息, 把信息从特殊的语义系统转到更通用的语义系统, 并通过可扩展接口保留特殊信息 --- 即使我们不认得. 第三方的 RSS 中的每一条其实都是 HTML, 包括通过链接爬回来的. 这些 HTML 里可能也会有 microdata 等标准的扩展语义信息, 无论我们是否认得, 我们也把它保存下来. 而来自自己应用的信息, 可能就是五花八门的格式了, 总有一方需要把这样的信息转到更通用, 更可理解的语义系统上来. 如果你的应用能够处理各种不同的数据源, 而不仅仅是自己的数据库 --- 那个信息孤岛, 最后必然要使用这样一个公开, 广泛可理解的语义系统.</p>

<p>我们会有多级的, 多个语义系统之间的转换. 这跟传统的以数据库为中心的应用是不同 --- 那里只有一级转换: 从数据库表到网页. 但方法是相通的 --- 模板. 类似 Smarty 的 web 模板可以满足原来的这种简单需求, 但若我们需要做结构化结构化数据的语义转换, 类似 XSLT 这样的技术是必要的. 我做过不少模板引擎, 最后发现无论它的文本语法如何, 实际上都能变成一个 LISP 的 list, 下面的问题就简单了, 这样的一个表, 只要定义了相应的 macro, 就变成了一个合法的程序. 计算模型很简单: 首先是模式匹配和提取 --- XSLT 用 XPath, CSS 和 jQuery 用 CSS Selector, 接着就是过滤和映射, 这些都是函数式语言所擅长的. 关于更通用意义的模板, 可以再开一篇来讨论.</p>

<p>无论如何, 最终我们得到了一个语义化的 HTML --- 符合 HTML5, 包含各种语义扩展信息. 我们的下一个问题: 如何展现到界面上? 从这里开始, 我们考虑的就是一个浏览器的问题.</p>

<p>我们先考虑普通的桌面应用, 对简单的体验, 就如我这个站, 只要再过一层模板加上修饰, 新的 HTML5 特性已经能提供很不错的体验; 但如果面对的是个不支持 HTML5 的底层浏览器, 你可能需要更复杂的代码: 模板更复杂, 甚至还需要把文档的某些 HTML5 标签属性直接翻译成展现相关的 HTML 和 javascript; 我们还要把浏览器不支持而我们能理解的语义信息, 翻译成展现代码 --- 还是那句, 你在干浏览器干的事.</p>

<p>对于复杂的体验, 我有个纠结了很久的问题 --- 浏览器 (浏览器软件) 与服务端应该如何划分? 我现在对这个问题的回答是: 在新的思考下, 浏览器 vs. 文档与客户端 vs. 服务端是两个不同的问题. 在我们的定义中, 浏览器 --- 关注文档展现的部分有一部分在客户端. 例如, 使用 GWT 的应用, 使用 GWT 的部分完成浏览器的职责: 从后台文档服务, 也或者是从别的第三方 Web 服务中获得标准的 HTML 文档, 再用它的方法展现给用户 --- 用各种奇怪的方法与客户端的浏览器交互, 这已不是着眼信息传播的 web 所关注的问题. 下面的问题是 GWT 与客户端的通信方式没有充分利用 HTTP, 这不是我们现在需要探讨的问题.</p>

<p>HTML5 等新的 Web 技术已经可以使客户端做得更多. 这不仅是 HTML5 本身提供了更多的交互能力 --- HTML5 建议我们应该用说明性标记而不是过程性的 script 去表达交互, 这也使机器理解交互成为可能, 即使是 HTML5 不支持的交互, 我们依然用客户端脚本支持 (直到 HTML6, 哈哈). 例如文档的 a 标签可能用 rel="quote" 表达引用类链接的关系, iPhone 的客户端可以把这样的语义翻译成特定的页面转换样式, 普通桌面浏览器也可以用 jQuery 找到这些标签, 把点击行为定义为自定义的效果, 例如弹窗 (这是找抽的). 一个 table, 客户端可以加上界面, 让用户自由选择是按表格, 饼状还是树状图展现, 可以直接到处 CSV...</p>

<p>这些体验本来就能做, 那这除了更 "语义" 外跟原来有什么不同? 它的关键就在于: 如果资源的请求者是个 crawler 等不关注展现效果的客户端, 我会直接提供裸的 HTML 文档, 让它直接获得最有价值的信息. 如果请求者是个手机浏览器, 有的机型固然可以跟桌面应用一样返回定制界面. 但很多的机器, 我可以让文档直接通过通用的转换器, 做分页折叠, 转换为 WML 等等. 这也是我们使用 HTML 而不是 ATOM 等没那么通用的形式的原因 --- 你能得到更多的开源, 通用支持. 其实, 若文档有恰当的语义标注, 我们甚至不必做 RSS, ATOM 等数据 API 接口, 通用的分析和转换器就可自动完成.</p>

<p>我们考虑链接: 当别的站链接你的内容的时候, 用的是文档而不是哪个界面的 URL. 当一个普通桌面用户点开这个链接时, 你的服务自适应地提供了一个桌面版展现, 移动用户看到的是移动版, 而 crawler 看到的却是最干净完整的内容数据, 方便处理, 各得其所. 客户端和服务端之间究竟怎么配合交互, 方法很多, 却总不会影响文档的共享. 这才是 Web: 相互链接, 充分共享的信息, 而不是一个个华丽而互不相容的孤岛.</p>
 ]]></summary>
  </entry>
  
  <entry>
    <title>珠三角技术沙龙归来</title>
    <link rel="alternate" href="http://randomtaste.appspot.com/view/page/gz-tech-party-12-19"/>
    <summary type="html"><![CDATA[ <p>嗯, 参加了两回技术沙龙, 听着熟悉的粤语味的普通话, 让我感觉算是真回来了 (右边的关于我还没改, 咔咔)</p>

<p>很荣幸能跟大家分享基础架构这个挺虚的主题. 其实这个话题不太好讲, 因为跟工作内容相关度太高, 很难把握分寸. 最后就变成 jjyy 一大堆别人的架构了. 希望能有腾讯和百度的同学对他们自己做的, 非常有趣的事情的分享. 感谢沙龙组织, 感谢 GeekCook 送给我们的邮件抱枕, 敝车间作为一个做邮箱的地方, 真应该每人发一个 :-)</p>

<p>感谢大家的宽容, 经常做 presentation 都是这个毛病, 越讲越快, 特别是对自己喜欢的东西, 还亏大家听懂了. 讲了各种各样的不靠谱和系统组件各种各样的死法. 网络的不靠谱, 我是着手做实际系统才有真切感觉, 也印象最深. 做分布式系统, 考虑多机并行往往不算难, 因为这是算法问题; 考虑机器挂掉也不难; 但往往就忘了结点间交互用的是个异步, 有延迟, 不可靠的网络. 就一个很简单的问题: 向远程服务提个写请求, 没收到应答. 你说这些请求人家做了还是没做? --- 说不好, 也许人家没收到请求, 或者人家做完了应答的时候网络挂了. 那这时候要告诉请求者成功了还是失败了? 这时候你应该, 你可以重试么? 一下子就能引出一大堆问题来. 这些东西确实没法逃避, 也别想用透明 RPC 之类的封装抹平.</p>

<p>对于 CAP 的理解也是. 初中时花了好一段时间做角的尺规三等分, 到实习时就开始做 CAP 兼得的存储. 到后来看到关于 CAP 的极为简单清晰的证明, 就知道权威可以挑战, 但科学不可以, 有的事情就是不行, 不是努力就可以的. 所以也开始从头学习分布式的许多基础知识, 也对流行的系统, 论文有了更深刻的了解. 这实在是一段五味杂陈的经历.</p>

<p>朋友们对 DO NOT Design for Scalability at first 有比较多的讨论. 这个有点偏激的观点背后是一些惨痛的经历. 一般做应用, 发现扩展不上去要迁架构了, 会感觉很痛苦, 然后会后悔为什么当初不把架构搞得好点, 但这个不能如果, 当时要投入太多时间人力做 "好" 架构, 也许就没法撑到现在了. 而我们的经历正好相反, 简单点说就是把东西做大了, 为可扩展性削弱功能, 结果发现应用至少短期内没有这么大的可扩展需求, 却因为功能的削弱而开发困难了. 后来我一直认为产品分布呈现今天大家看到的长尾, 一个公司内部, 一个产品内部也是, 长尾头尾的做法是不一样的. 长尾头部的东西, 数量极少, 规模极大, 要无所不用其极, 用完全定制化的方法, 把可扩展性和单机性能做到极致. 而对长尾尾部, 就要强调功能, 通用性, 强调快速开发, 快速实验. 当一个长尾尾部应用突然热起来移到头部 --- 例如农场, 再去想做定制的优化和架构设计也不迟. </p>

<p>不多说, 上 PPT, 至于最后一段纯意识流, 请忽略...</p>

<div style="width:425px;text-align:left" id="__ss_2749809"><a style="font:14px Helvetica,Arial,Sans-serif;display:block;margin:12px 0 3px 0;text-decoration:underline;" href="http://www.slideshare.net/kyhpudding/dreaming-infrastructure" title="Dreaming Infrastructure">Dreaming Infrastructure</a><object style="margin:0px" width="425" height="355"><param name="movie" value="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=dreaminginfrastructure-091219092117-phpapp02&stripped_title=dreaming-infrastructure" /><param name="allowFullScreen" value="true"/><param name="allowScriptAccess" value="always"/><embed src="http://static.slidesharecdn.com/swf/ssplayer2.swf?doc=dreaminginfrastructure-091219092117-phpapp02&stripped_title=dreaming-infrastructure" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="355"></embed></object><div style="font-size:11px;font-family:tahoma,arial;height:26px;padding-top:2px;">View more <a style="text-decoration:underline;" href="http://www.slideshare.net/">documents</a> from <a style="text-decoration:underline;" href="http://www.slideshare.net/kyhpudding">kyhpudding</a>.</div></div>
 ]]></summary>
  </entry>
  
  <entry>
    <title>在北京吃柳州螺蛳粉</title>
    <link rel="alternate" href="http://randomtaste.appspot.com/view/page/page_2009_Sep_12"/>
    <summary type="html"><![CDATA[ <p>螺蛳粉不算一种有名的食物，名气比桂林米粉可差远了。如果不是因为老妈在柳州住了十几年，如果不是小时候去柳州的时候尝过，并且对它的辣记忆犹新，我可能到现在都不知道这种小吃。</p>

<p>前不久朋友推荐我到这家小店吃螺丝粉。店离我原来住的地方很近，就在万泉庄路十九中对面，离人大西门也很近，店面不大，而且招牌不是对着大路，第一次找要费点心思。店里只卖螺蛳粉和配菜，服务员和顾客也多是桂柳一带的人，他们用柳州话点菜，坐下周围的人说的也是柳州话。周围墙上贴着顾客们的留言，赞不绝口。</p>

<p>虽然已经比小时候能吃辣得多，但由于心有余悸，我点的是不辣的。粉上来，还是有点辣，但确实好吃，螺蛳粉的配料很简单寻常：爽口的米粉，我大爱的酸笋，再加上花生腐竹豆泡青菜，最重要的是那口螺蛳汤，鲜美无比。我后来都要的是微辣，更是鲜辣爽快，吃得挥汗如雨，继续吃。正宗与否，我不清楚，得让老妈亲自来判断。</p>

<p><center><img src="http://farm3.static.flickr.com/2567/3911385721_beca4aa507.jpg" alt="柳州螺蛳粉" /></center></p>

<p>之后就不时跑过来吃。晚上加班回来，身心俱疲，来一碗感觉分外精神。用不着什么理由，就是因为好吃。</p>

<p>喜欢这里，如果还真有别的理由的话。就是这里的服务员总是对你笑脸相迎，这不是那种职业的服务笑容，他们做事麻利爽快，闲下来用柳州话快活地聊天（我基本能听懂），挂着的都是这样的笑容。他们喜欢这里，喜欢这个工作。这让我感受到在这个城市极难感受到的亲切。有一回听小老板在跟人聊天。说 SNS, 说新浪推特，特别有趣。希望饭否早日重开，我一定去忽悠小老板来饭否开个帐号，让我们三天两头@一下.</p>

<p>如果说北京还有哪个吃东西的地方能让我怀念的话，那应该就是这里了。</p>
 ]]></summary>
  </entry>
  
  <entry>
    <title>UNIX 与函数式编程</title>
    <link rel="alternate" href="http://randomtaste.appspot.com/view/page/fp-unix"/>
    <summary type="html"><![CDATA[ <p>我们都喜欢 UNIX, 喜欢它丰富的小工具和万能的管道, 通过他们组成非常复杂的功能. 本文从 UNIX 这种软件构建风格出发, 探讨它与函数式编程风格的高度吻合, 并进一步讨论一种实用的编程方法和它的作用.</p>

<h2>函数式编程, 第一步</h2>

<p>grep, sort 等 UNIX 工具都有一致的行为: 从标准输入接受输入, 输出到标准输出. 一致的行为使他们配合起来非常简单. 我们可以以标准工具的组合实现类似 SQL 的查询</p>

<pre><code># Same as SELECT ... FROM table WHERE ... ORDER BY ... LIMIT ...
# The table content will be provided via standard input
$ (grep regexp | sort ... | head -n ... | cut ...) &lt; table
</code></pre>

<p>我们看 ``grep regexp'' 我们说: 把 regexp 参数<strong>作用</strong>于 grep, 将 <strong>生成</strong> 一个有标准输入输出行为的 <strong>进程</strong>. 这说得有点拗口, 它强调了以下几个方面:</p>

<ul>
<li>grep 是一个程序 <strong>框架</strong>, 用于进行文本检索. 它具有通用性, 它的具体行为 (检索什么), 是通过指定正则表达式参数 --- 其实就是一个小程序来定义的.</li>
<li>对用户而言, 用户只关心要检索的正则表达式, 并不关心 grep 是怎么完成的 --- grep 倒着搜还是分布式搜, 只要最后能得到结果就行.</li>
<li>对 shell 而言, 它只关心调用 grep 后得到的一个有标准输入输出行为的进程, 这样就可以放在管道或输入输出重定向中工作了, 至于用的什么参数调用 grep, grep 到底干什么, 它不关心.</li>
</ul>

<p>这种 seperate of concern, 是构建更大规模应用的基础. 我们可以把上面的命令写成脚本, 加入几个可配置的参数, 但最重要的一点是, 脚本的行为依然是标准的输入输出行为, 这样我们就可以再递归地把这个脚本用于更大规模的构建中, 直至构造宏大的应用.</p>

<p>这样的模式与我们常见的编程习惯是不一样的, 例如 python 的 filter 函数是:</p>

<pre><code>filtered_list = filter(filter_func, source_list)
</code></pre>

<p>写成 UNIX 命令就类似</p>

<pre><code>$ grep filter_regex source_file &gt; filtered_file
</code></pre>

<p>如果要在 python 里做与 UNIX shell 里一样的事情, 我们的描述应该是: 将 filter_func 作用与 filter, 它将 <strong>生成</strong> 一个接受一个数据输入, 返回一个数据输出的 <strong>函数</strong>. 也就是说, 我们需要一个生成函数的函数. 在 python 里面可以简单实现为:</p>

<pre><code>def Filter(filter_func):
    def do_filter(src):
        return filter(filter_func, src)
    return do_filter
</code></pre>

<p>熟悉 python 的同学当然知道可以使用 lambda 简化程序, 但这里我们需要写得更清楚一些. 在这里, 我们做的事情就跟上面叙述的完全一样, 根据一个 filter_func 生成一个拥有标准接口的函数. 在这里, 函数就跟普通对象一样, 是可以动态 "生成" 的, 使用不同参数调用 Filter 的结果是不同的函数, 有不同的过滤行为, 就跟用不同的参数调用 grep 一样. 这就是我们走向函数式编程的第一步 --- 高阶函数 (First-order function). 用 OO 的方法理解也很简单: 所谓函数, 其实就是一个有 "调用" 这么一个标准接口的对象.</p>

<p>PS. 在 lambda 计算理论中, filter(func, data) 只是一个复合函数的糖衣, filter(func) 生成一个 data 的函数. filter(func, data) 和 filter(func)(data) 是等价的. 这就是所谓 curry, 在 haskell 等天然支持 curry 的语言中, 我们是根本不用写上面的 Filter 定义的.</p>

<p>由此, 我们可以做类似:</p>

<pre><code>foo = Filter(filter_func)
bar = Grep(r'regex')
</code></pre>

<p>然后把 foo 和 bar 函数用在任何地方. 而且, 这两个函数有完全一样的接口: 接受一个数据输入 (先忽略数据类型), 输出一个数据. 调用他们的人是不用关心他们是怎么生成的. 基于一致的接口, 函数间的协作, 组合都变得很简单. 例如实现一个 Pipeline:</p>

<pre><code>Pipeline(func1, func2, ...)
</code></pre>

<p>或者重载 python 的或操作</p>

<pre><code>Pipeline() | func1 | func2 ....
</code></pre>

<p>当然, 就跟 UNIX 一样, 合成的结果依然是一个单入单出的函数.</p>

<p>不仅如此, Filter 也是一个单入单出的函数: 接受一个函数, 生成另一个函数. 而它接受这个函数本身也是标准的, 也可以由其他函数工厂生成, 例如有</p>

<pre><code>CheckRegex(r'regex')    # 根据正则表达式生成检查函数, 若输入 match 正则则返回 True, 否则 False
bar = Filter(CheckRegex(r'regex'))
</code></pre>

<p>这个 bar 实现替代了原来的 Grep, 它进一步分离了执行策略和执行框架, 我们只需实现一个简单的正则比较功能, 套进 Filter 框架就成了 Grep, 假如我们实现了分布式的 Filter, 我们就马上获得了一个分布式 Grep!</p>

<p>这类的函数复合在我们的编程中非常常见. 单参数的函数复合其实就是 Pipeline, 例如上面的例子, 我们可以写成</p>

<pre><code>Grep = Pipeline() | CheckRegex | Filter
</code></pre>

<p>当以一个正则表达式为参数调用 Grep 时, 它首先被 CheckRegex 转换为一个输出 True 或 False 的函数, 这个函数再传给 Filter, 就成为了一个真正的 grep 函数.</p>

<p>这是我们迈向函数式编程的第一步: 高阶函数, 把函数当成普通对象, 再把这个普通对象作为处理中的主要对象类型. 你会发现, 在 OO 中熟悉的各种技巧, 抽象, 多态... 能够全部招呼到这个不那么顺眼的对象上. 这可能得花点时间适应, Lennon 说, Imagine there are all functions, it's easy if you try...</p>

<h2>懒惰求值</h2>

<p>考虑 UNIX 命令</p>

<pre><code>grep regexp largefile.txt | head -n 10
</code></pre>

<p>按照最 naive 的想法, 我们的做法应该是列出文件后, 在文件里找出所有符合正则的行, 再将这些行送给 head, 让它选出前 10 行. 这果然很 naive: 反过来想, 只要 head 能够读到 10 行内容, 就可以完成任务了. 而对 grep 来讲, 只要读到的内容中有 10 个匹配, 就不用再做了. 简单点说, 即使 grep regexp <strong>表示</strong> 它要完成对输入的所有处理, 但它<strong>不必</strong>真的完成. 只有当它的下游 (head) 表示需要这个结果时, 它才去做运算.</p>

<p>基于 grep 的按行处理模型, 这是很好处理的事情: grep 每读一行输入, 就进行一次处理, 并将结果输出, 而不是把整个输入读完才进行. 而当下游 head 执行完 --- 表示不再需要输入的时候, head 的推出会导致 grep 收到一个 SIGPIPE, 导致 grep 自然退出, 对 grep 的上游也是如此.</p>

<p>如果我们有脚本:</p>

<pre><code>#!/bin/sh
#largefile_error
grep error largefile.txt
</code></pre>

<p>这是一个无需标准输入, 能够直接输出内容的程序. 假如 largeilfe.txt 是一个只读文件, 那么 "./largefile_query 检索词" 实际上表达的是一个<strong>虚拟文件</strong>: 虽然未经求值, 它的内容是恒定的, 就是 largefile.txt 里含有 error 的行. 我们可以有:</p>

<pre><code>$ ./largefile_error | head -n ...
</code></pre>

<p>只有当我们真正需要对 largefile.txt 进行计算时, 脚本里的 grep 才会被执行, 而且就像上面分析的: 只会被执行真正必要的部分. 这显然能够节省大量资源, 在 largefile.txt 是一个无限长度的文件时, 这也是唯一可行的计算方法.</p>

<p>要在 python 中实现它并不难. 假如设有与 Filter 对应的 Map, 一个类列表数据 list, 很容易知道:</p>

<pre><code>((Pipeline() | Map(func_a) | Map(func_b) ...)(data))[n:m] === Map(Pipeline() | a | b)(data[n:m])
</code></pre>

<p>显然, 若 n 和 m 比 data 的实际长度小很多 (data 可能是无限长), 后面的计算方式能节省大量的计算. 这跟上面 UNIX 的例子是一样的.</p>

<p>我们可以由此实现一个懒惰计算的类 list 类 Stream</p>

<pre><code>Stream(data, pipeline)
Stream(data, pipeline) &amp; func = Stream(data, pipeline | func)
Stream(data, pipeline)[n:m] = map(pipeline, data[n:m])
</code></pre>

<p>我们实现一个 Stream 上的 Map 则是:</p>

<pre><code>def Map(func):
    return lambda stream: stream &amp; func
</code></pre>

<p>我们精确实现了上面的定义:</p>

<pre><code>Pipeline() | Map(func_a) | Map(func_b) ...
</code></pre>

<p>的结果是把各个 map function 加入到 Stream 内部的 pipeline, 将要作用于 Stream 的每一个元素. 但只有当我们明确要求取出数据时, 才会进行真实的计算, 实现了延迟求值.</p>

<p>但是, 它实际上无法实现 Filter, 因为 Filter 的输入和输出不是一对一的. Filter 是通过 python 的 generator 实现的:</p>

<pre><code>def Filter(check_func):
    def do_iter(src):
        for item in src:
            if check_func(item):
                yield item
    def make_stream(src):
        return Stream(do_iter(Stream(src)))
    return make_stream

ret_stream = (Pipeline() | (Filter(CheckRegex(r'...'))) | Map(str.upper))(open('src.txt'))
print ret_stream[:5]
</code></pre>

<p>这里面发生的事情与 UNIX 更为相似. make_stream 创造了一个新的 Stream, 数据源是 do_iter 产生的可迭代的 generator (请自行参考 python 相关文档). 因此, 到 "ret_stream =" 一行, 得到的 stream 是在 make_stream 的基础上加了个 str.upper 操作. 实际上, 任何实际计算都没有进行. 当执行 print 一行时, Stream 机制会驱动 do_iter 产生的 generator 进行 5 次迭代, 这可能另 "for item in src" 一行中从源数据 (可以作为 iterator 使用的 src.txt 文件对象) 读入多于 5 行数据. 当执行完成, GC 把 ret_stream 回收时, 这些东西都浮云了 --- 发生了部分求值.</p>

<p>即使是一些看起来没办法这么做的功能, 例如排序, 也可以做成懒惰和部分求值. 当调用 Sort(...)(src_data) 时, 可以生成一个虚拟的数组 S, 当因为任何的原因 (例如被下游驱动) 发生 S[:n] 操作时, 只需进行一个 TopN 计算即可, 并且可以做个 cache, 当下次做 S[:m], m > n 时, 只需求一次 Top(M - N). 当作为 Iterator 使用时, 为了防止每次求 Top 1 退化为冒泡排序, 只需再做一些预读工作即可. 总之, 没有必要那么乖地在用户调用时真的把它求出来, 无论用什么办法, 最终结果是对的就行.</p>

<h2>无副作用与不可变对象</h2>

<p>我们在这里必须注意懒惰求值的一个重要条件: 我在上文举 largefile_error 的例子时, 很不显眼地提到了一个前提: largefile.txt 必须不发生更改, 否则我们的等式不能成立. 这展现了函数式编程中的非常重要的一面: 对象状态不可改变与函数无副作用. 没有这一点. 对于调用 f(obj), 在任何时候都有相同结果, 因为:</p>

<ul>
<li>f 没有副作用, 它的结果只依赖 obj 的值, 并且不能改变 obj 和其他系统状态.</li>
<li>obj 是一个不可改变内部状态的对象. 作用于 f 显然在任何时候都有相同结果.</li>
</ul>

<p>没有这些条件, 我们上面的讨论都是错误的.</p>

<p>在命令式编程和 OO 中, 我们的全部工作就是在跟各种状态变化周旋, 因此, 上面的要求看起来很难接受. 其实, 我们应该考虑它为数学上的函数: 我说 X = 1, 那不会说它下一刻就等于 2, Y = f(X), 不可能说做多几次它的结果就不同. 我们应该用数学上的 <strong>等式变换</strong> 的方法来考虑它. 这里不涉及状态和执行顺序的问题. 后文很大一部分内容都必须建立在上述约束之上, 我们会渐渐发现这是一个多么自然的事情.</p>

<p>基于这样的前提, 实际执行 f(obj) 的时机对处理结果是不会有影响的, 系统可以自由地选择什么时候才真正执行它. 很自然地:</p>

<ul>
<li>只有当用户显式要求得到 f(obj) 的结果时, 才执行它, 节省宝贵的 CPU 资源.</li>
<li>对 f(obj) 的结果做个 cache, 若在别的地方再次调用, 直接出结果而不用计算.</li>
</ul>

<p>Stream 的实际实现中正是这么做的.</p>

<h2>Curry, 有名管道与 Makefile</h2>

<p>如果我们要让查询的正则可设置, 写成 UNIX 脚本会是:</p>

<pre><code>#/bin/sh
grep $1 | upper
</code></pre>

<p>如果它是一个函数的话, 它是一个需要接受两个参数的函数: 正则和来自标准输入的待 grep 文本. 这样它就不是一个标准接口的函数了. 但是还记得我们关于 curry 的讨论吗? 在 lambda 运算中, 多参数函数只是一个糖衣. 当然 python 没有这个功能, 需要我们自己实现, 我们实现 Curry. 若 func 是一个接受 N 个参数的函数</p>

<pre><code>Curry(func) = Curry(func, N, ())
Curry(func, N, args)(arg) when len(args) + 1 &lt; N = Curry(func, N, args + (arg,))
Curry(func, N, args)(arg) when len(args) + 1 == N = func(*(args + arg))
</code></pre>

<p>另外, Pipeline 对 curry 做一点支持:</p>

<pre><code>func(arg) | others... 若func(arg) 返回一个 Curry 对象 cur, 则 Pipeline 返回
cur | others...

pipeline(Curry(...)) = Pipeline() | Cury(...) | pipeline

pipeline(x, y, ...) = pipeline(x)(y)(...)   # 语法糖衣
</code></pre>

<p>因此, 若以一个参数调用 Pipeline, 而它又返回一个 Pipeline, 意思就是... 你要再提供一个参数了. 所以, 我更习惯把这里的 Pipeline 称为 Option, 而 "|" 会写为 ">>". 上面例子的计算, 我们可以表达为:</p>

<pre><code>opt = Option() &gt;&gt; CheckRegex &gt;&gt; Filter &gt;&gt; Curry &gt;&gt; Map(str.upper)
stdfun = opt(r'...')
print stdfun(Stream(open('src.txt')))[:5]
</code></pre>

<p>我们看到, opt 的前两个操作接受一个正则字符串, 产生一个标准的过滤函数, 而 Curry 接受了这个函数后, 返回了一个 Curry 对象, 表示要更多的参数, Option 在此时也顺水推舟地返回. 因此, 若 Curry 之前得到的函数是 filter_func, stdfun 其实等价于:</p>

<pre><code>Option() | filter_func | Map(str.upper)
</code></pre>

<p>后面的流程大家就非常熟悉了. 这就是利用 Curry 解决多个参数的思路. 在处理流程中的任何一步需要额外参数, 并不需要它的上游为其传递, 实际这个额外参数也无需在处理开始时给出 (由于这个 Option 可能要再与其他的处理组合工作, 你根本无法定义什么叫处理开始). Curry 能非常自然地解决这个问题, 只要默念几次: "多参数是浮云, 单入单出函数是唯一的真函数" 就好了.</p>

<p>对于一些更复杂的情况, 例如带 JOIN 的查询</p>

<pre><code># Same as SELECT * WHERE ... JOIN other-table ....
$ grep ... | join - other-table | grep ... | limit ...
</code></pre>

<p>join 的第一个参数来自标准输入 (grep 的结果), 第二个参数则来自一个文件 other-table. 然则, UNIX 的有强大的虚拟文件系统, 这并不一定是一个实际存储的文件. 例如, 它可能是一个命名管道.</p>

<pre><code>$ mknod other-table p
$ grep ... table-source &gt; other-table &amp;
$ grep ... | join - other-table | grep ... | limit ...
</code></pre>

<p>此时, other-table 是一个对通过 table-source (天知道它是不是也是个命名管道) 进行过滤处理的结果, 再传送到 join 中, 上文提到的懒惰求值等特性在这里依然适用. 上面的功能虽然还是可以利用 Curry 特性实现, 但显然这种写法更为清楚特别是两张表都特别复杂, 需要多个参数生成时. 在 python 中, 基于 Stream 的懒惰求值特性, 实现上述操作是很自然的:</p>

<pre><code>other_table = Grep(...)(Stream(open('other-table-source')))
result_table = Option() &gt;&gt; Grep(...) &gt;&gt; Join() &gt;&gt; Grep(...)(
    Stream(open('table-source'), other-table))
</code></pre>

<p>如果说 pipeline 是一种线状的数据依赖/传递关系, 则若引入多参数/curry, 会构成树状依赖关系. 假设上文中 other_table 参与了 result_table 以外的其他计算, 那我们就得到了一个网状关系. 我们会有这样的断言:</p>

<ul>
<li>A = f(B, C)  => A 依赖 B 和 C</li>
<li>B 依赖 C 和 D</li>
<li>D 依赖 C</li>
</ul>

<p>那么, 若要在一个单处理机上求 A, 我们需要顺序执行 C D B A. 这种依赖关系和相应的处理与 Makefile 很相似: 当依赖关系非常复杂时, 我们可以仅声明对象间的依赖关系, 由类似 make 的框架自动指出执行顺序, 这是我的一个产品级项目的最核心想法. 另外, 若各个函数传递的都是 Stream, 它的懒惰求值特性使得最终都还没有实际逻辑被执行. 当我们真正从 A 拉出数据时, 其他部分会依据依赖关系被驱动, 类似 C 这样的公共依赖会被求值多次 --- 此时, 上文提到的对函数执行结果的 cache 就其作用了.</p>

<h2>表意编程</h2>

<p>从本文第一节开始, 我们就在抽象执行框架, 努力让执行框架与具体策略分离. 例如 Filter 是一个执行框架, 而具体的过滤函数则是它的具体策略. 抽象执行框架与抽象对象有对等的意义, 抽象了执行框架, 使我说 Filter(func) 的时候, 我只关心得到的函数能够在数据集中选出符合我指定的条件 func 的部分, 而它是顺序找, 倒着找还是并行分布式找并不影响我所关心的结果. 当我们无需关心程序真实的执行方式/流程的时候, 我们正在走向一个表意编程的风格.</p>

<p>在这样的风格中, 我们不试图控制流程, 而是组合组件以产生更大的组件. 我们能在 UNIX 中看到这种方法的巨大威力, 一个复杂的应用能很方便地使用小组件复合而成, 由于每个组件都是无副作用的, 处理结果只与标准输入相关, 它的测试和调试都非常简单. 得到的程序很难出 bug, 效率很好. 无论是开发效率还是程序质量都大大高于自己山寨的版本. </p>

<p>当我们把具体控制流程从程序中抽离, 留下的程序可能由于应用特点的不同呈现完全不同的风格. 它更像是另一种语言 (比如我上面应用 Pipeline 等框架的程序就很不像 python), 它成为一种领域特定语言 (Domain specific language), 根据不同的领域, 有完全不同的描述风格. 精确地说, 这叫 internal DSL. 有了这个基础, 我们可以设计一层很薄的语法外壳, 使它更容易开发和被领域专家理解, 这就称为了 extranal DSL. 当然, 如果你可以像 M$ 一样霸道, 你可以像 LINQ 一样把领域特定语法直接集成进宿主语言.</p>

<p>高阶函数的能力是实现这样的风格的重要一环. 有了用函数生成函数的能力, 可以使执行框架, 策略和策略作用于框架的结果都是有一致接口的函数, 他们的复合就成了非常自然的事情. 我们在上面已经给出了很多的例子. CheckRegex 作为框架, 结合正则策略生成的函数可以作为 Filter 框架的策略, 而应用 Filter 框架的结果又自然能嵌入 Pipeline 作为它的一个处理节点 (策略). 这是我们能用组合而不是流程控制的方法编程的实际保证.</p>

<p>表意编程和命令式编程没有明显的界限, 这往往是一个执行框架的抽象程度和粒度的问题. 但表意式编程区别于命令式编程的一个重要特征是: 程序中没有状态维护的内容. 一旦引入了状态维护, 引入了有作用的函数, 系统就不得不严格根据程序指定的执行序列来执行以保证正确结果, 相对的, 程序员必须管理状态和控制流程的责任, 而无法享受抽象执行框架的好处.</p>

<p>但需要注意的是, 上文引入 join 时出现的赋值语句并不是在做状态维护, 因为 other_table, result_table 自始至终都是一个值, 与其说它是个赋值, 还不如说它是一个简单的等式替换, 就像我在上面说的, 引入它只是让程序写得更好看而已. 而且假如 python 本身就是一个懒惰求值的语言, 这些等式的顺序调换是完全没有问题的.</p>

<h2>状态</h2>

<p>读者一定会跟我抬杠, 我要做状态机怎么办? 有的应用天然地需要维护状态. 我们可以看看 python 的 reduce, 作为一个最简单的需求, 如果我要求数组的和.</p>

<pre><code>reduce(lambda curr, x: curr + x, data, 0)
</code></pre>

<p>提供一个状态转换函数, 接受两个参数: 当前状态 (curr) 和真正的参数 (x), 返回下一状态, 然后 reduce 框架会顺序把数据源的每一项和上次执行返回的状态调用这个函数, 返回最后状态. 这里发生的事情其实是, reduce 为我们维护了状态, 而我们的函数仍然是一个很纯净的函数.</p>

<p>状态维护是一件困难的事情, 一旦涉及状态维护, 一切都变得复杂起来. 维护状态的程序很难并行, 维护状态的代码更难调试, 更容易有 bug. 还要处理并发冲突, 状态持久化和崩溃恢复等问题. 这些问题不应该在日常的业务开发中涉及, 我们需要把状态维护抽象到执行框架, 让执行框架自己去维护状态, 处理这些复杂的问题. 这样的结果是:</p>

<ul>
<li>策略依然是纯净无副作用的函数.</li>
<li>策略作用于执行框架产生的依然是纯净无副作用的函数.</li>
<li>执行框架内部在执行过程中维护状态, 做各种各样的肮脏的事情.</li>
</ul>

<p>很多看起来应该没有状态的操作, 其优化实现的执行框架内部都有状态维护. 例如上面多次提到的 cache, 就是一种典型的状态维护 (当然就有很恶心的线程安全问题....). 然而这都是用户不必关心的, 对外来看一切依然纯净, 我们还是能很舒服地做函数式, 表意式编程.</p>

<p>若以有无状态维护来考量表意编程, 则有一个粒度问题. 我们是否需要把所有操作都做成无状态维护的形式? 如果你使用的是 erlang 或 haskell 这样的纯函数式语言, 你没有选择. 但我们这里的考虑是, 如果一个策略没有复杂到需要一个执行框架来组织而可以直接编写函数实现, 只要这个函数本身是无副作用的, 在函数求值过程中是否维护了状态并不重要, 它并没有破坏我们上面的任何一点原则 --- 况且, 究其实际, 在 CPU 里不还就是在捣鼓一堆寄存器状态么.</p>

<p>所以, 总结一下. 编写基础的原子策略时, 不妨使用你熟悉的命令式编程风格. 但进行更大的组件组合时, 使用纯净的表意风格. 大应用中的状态维护问题和相应的控制策略, 则交给专业的执行框架自己解决.</p>

<h2>依赖, 顺序与并行</h2>

<p>多年来我们习惯了顺序编程, 习惯了我们在单处理器上工作的假定. 但这样的假定无论是在多 CPU 或多核环境以及分布式环境下都是不成立的. 而正如我们所展现的, 函数式的编程方法在很多时候并不注重执行顺序, 这给并发优化创造了巨大的空间. 无怪乎在多核 CPU 流行的今天, 在必须使用分布式架构进行超大规模数据处理的领域, 函数式语言和编程方法获得越来越多的重视.</p>

<p>许多计算需求是天然不能并行的, 在这里它被表示为依赖. 管道清晰地表达了组件间的依赖关系. 要求得下游的结果, 必须先进行上游的至少是部分的处理. 我们又从管道的单依赖情况扩展到更复杂的树状依赖乃至网状依赖的情况. 毫无疑问, 没有直接或间接依赖关系的组件可以并行执行.</p>

<p>在我上面提到的产品级项目中, 我们使用了这种方法处理相互依赖的 IO 任务的并发问题. 作为一种更通用的情况, 我们可以看看微软的 Dryad 平台, 当它执行下面这种貌似顺序的程序:</p>

<pre><code>other_table = Grep(...)(Stream(open('other-table-source')))
joint_table = Option() &gt;&gt; Grep(...) &gt;&gt; Join()(
    Stream(open('table-source'), other-table))
result = Grep(...)(joint_table)
</code></pre>

<p>它能够分析处理间的依赖关系, 得到一个优化的执行计划, 使各处理能够并行执行. 这更能说明所谓 "表意" 的意思, 用我们习惯的顺序思维描述需求, 但实际的处理依然可以并行. 一个更实用的例子是 DryadLINQ, 在 Dryad 之上处理 LINQ 程序. LINQ 是另一个函数式编程附体的咚咚, 熟悉的同学可以自行分析 :-)</p>

<h2>实用的函数式编程</h2>

<p>很多时候函数式语言和函数式编程都是很学术的东西, 但我们可以发现很工程的 UNIX 的设计思想与它高度吻合, 这提示了函数式编程思想在实际工程中的应用前景. 在这里, 我们展现了部分函数式的设计思路:</p>

<ul>
<li>以高阶函数作为系统基本构件和通用接口, 使用抽象的执行框架组合基本组件产生更大规模的应用. 这促进重用, 有效管理大型应用的复杂性. 最终得到一种表意式的编程风格. 并由此推出专门表达领域需求的领域特定语言.</li>
<li>无状态, 无副作用组件接口, 使得组件非常容易测试和调错. 非常谨慎地对待状态维护, 使用执行框架对状态维护进行抽象封装, 保证系统绝大部分代码 --- 业务功能不必涉及状态维护. 这将会得到稳定, 高质量, 易测试, 易优化, 可扩展的软件架构.</li>
<li>这种设计方式特别有利于将程序扩展到并行环境.</li>
</ul>

<p>这些设计思路并不需要特定技术或语言来实现. 这也是我在这里利用 python 而不是经典的函数式语言来演示的原因. 基本上, 如果语言能够支持高阶函数和 closure, 这里所述的任何设计都能很方便地实现, 没有 native 支持的语言也可通过一定的封装和限制进行实现. 我们也可以根据实际的项目需求, 以别的, 而不是这种自然的函数方法来实现这类模式.</p>

<p>这样的设计方法也不必运用在所有地方, 我们探讨的内容专注于组件的组合, 专注于基本组件如何构造复杂应用. 而在一个基本组件内部, 可以按照最适合应用特点的方式实现, 而不必使用纯函数式编程的方式. 关键在于我们的基本组件的粒度, 执行框架抽象的程度, 选择这样的粒度和程度是否有利于组件和框架的复用, 是否有利于提高软件开发质量和效率.</p>

<p>我在实际项目中, 实际使用和体会到了这种设计风格的威力. 在一个 C++ 的应用框架设计中, 我们发现, 上述的这些设计点正是保证组件隔离, 处理应用复杂性, 保证复杂性可扩展的关键. 在设计开发中, 虽然我没有使用 "函数式" 这样的说法, 但这种设计思想是很容易理解和贯彻, 并且能与流行的 OO 设计方法有机结合, 在一个传统的静态命令式语言的开发上产生良好的效果.</p>

<p>在 python 上实现上述功能, 如果需要有较好的实际执行效率, 还是有不少的工作. 因此我开发了 <a href="http://code.google.com/p/python-tao/">python-tao</a> 框架, 实做了上面提到的设计. 本文没提到的 python-tao 部分包括其他函数式语言的基本元素: 对 tail call 的支持, 类似 scala 的对象 pattern matching, 以及 partial 机制. 对于 pattern matching, 需要再开另一篇文章来讨论.</p>
 ]]></summary>
  </entry>
  
  <entry>
    <title>emacs 党</title>
    <link rel="alternate" href="http://randomtaste.appspot.com/view/page/page_2009_Aug_01"/>
    <summary type="html"><![CDATA[ <p>发篇超 geek 的.</p>

<p>首先热烈庆祝下 <a href="http://www.gnu.org/software/emacs/#Releases">Emacs Makes A Computer Slow, take 23 正式发布</a></p>

<p>大约是大一的寒假, 因为要玩 scheme, 于是上了 lisp 系语言和 emacs 的贼船, 从此就一直是 emacs 的忠实用户. 用的配置文件都是从那时候一路积累下来的. 在百度 vi 用户占绝对多数, 当时组里就只有 zhangjian 大神和我使用 emacs, 后来大神转去其他部门, 于是我就成为了孤独的 emacs 死忠...</p>

<p>结果今年来了一位新人, 惊喜地发现他用的是 emacs, 本部门 emacs 使用人数直接翻了一番, 泪流满面. 这个星期又来了一位, 我不怀什么期待地问他使用的编辑器 --- 又一个 emacs 用户!</p>

<p>一个部门有三个 emacs 用户, 就可以组织党支部了. 然则正如预料之中的: 只要有多于两个 emacs 用户, 则必然有关于 emacs 不同使用方法的争论. 三个人, 一个是在开发机上用终端 emacs, 一个是用本地 GUI emacs, 用 tramp 远程编辑开发机文件, 还有一个习惯在 emacs 里用鼠标, 到具体的扩展应用上更是五花八门, 这三人中还有一个 windows 用户...... 形势显然相当混乱, 不过, 交流还是王道, 大家又从别人身上学到了各种从来没想过的 emacs 使用方法......</p>

<p>另外, 这两位新人, 使用的都是专用键盘, 虽然不是 emacs 专用, 但是看起来在 emacs 中挺爽的, 有点心动. 不过习惯键位是个很长期且痛苦的过程, 我还在头痛这个问题: 我想买台 macbook, 我那一大堆还有使用 windows 键的配置怎么调过去, 我还要花多久才能适应?</p>
 ]]></summary>
  </entry>
  
</feed>

