<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Todd | 酷 壳 - CoolShell</title>
	<atom:link href="https://coolshell.cn/articles/author/todd/feed" rel="self" type="application/rss+xml" />
	<link>https://coolshell.cn</link>
	<description>享受编程和技术所带来的快乐 - Coding Your Ambition</description>
	<lastBuildDate>Tue, 31 Mar 2015 12:49:40 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.2</generator>
	<item>
		<title>程序的本质复杂性和元语言抽象</title>
		<link>https://coolshell.cn/articles/10652.html</link>
					<comments>https://coolshell.cn/articles/10652.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Thu, 31 Oct 2013 00:00:09 +0000</pubDate>
				<category><![CDATA[编程语言]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=10652</guid>

					<description><![CDATA[<p>（感谢 @文艺复兴记（todd） 投递此文） 组件复用技术的局限性 常听到有人讲“我写代码很讲究，一直严格遵循DRY原则，把重复使用的功能都封装成可复用的组件，...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/10652.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/10652.html">程序的本质复杂性和元语言抽象</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢 <a href="http://weibo.com/weidagang" target="_blank">@文艺复兴记</a>（todd） 投递此文）</strong></p>
<h4>组件复用技术的局限性</h4>
<p>常听到有人讲“我写代码很讲究，一直严格遵循<a href="http://en.wikipedia.org/wiki/Don't_repeat_yourself">DRY原则</a>，把重复使用的功能都封装成可复用的组件，使得代码简短优雅，同时也易于理解和维护”。显然，DRY原则和组件复用技术是最常见的改善代码质量的方法，不过，在我看来以这类方法为指导，能帮助我们写出“不错的程序”，但还不足以帮助我们写出简短、优雅、易理解、易维护的“好程序”。对于熟悉Martin Fowler《重构》和GoF《设计模式》的程序员，我常常提出这样一个问题帮助他们进一步加深对程序的理解：</p>
<blockquote><p>如果目标是代码“简短、优雅、易理解、易维护”，组件复用技术是最好的方法吗？这种方法有没有根本性的局限？</p></blockquote>
<p>虽然基于函数、类等形式的组件复用技术从一定程度上消除了冗余，提升了代码的抽象层次，但是这种技术却有着本质的局限性，其根源在于 <strong>每种组件形式都代表了特定的抽象维度，组件复用只能在其维度上进行抽象层次的提升</strong>。比如，我们可以把常用的HashMap等功能封装为类库，但是不管怎么封装复用类永远是类，封装虽然提升了代码的抽象层次，但是它永远不会变成Lambda，而实际问题所代表的抽象维度往往与之并不匹配。</p>
<p>以常见的二进制消息的解析为例，组件复用技术所能做到的只是把读取字节，检查约束，计算CRC等功能封装成函数，这是远远不够的。比如，下面的表格定义了二进制消息X的格式：</p>
<p><span id="more-10652"></span></p>
<pre>Message X:
--------------------------------------------------------
| ID |  Name           | Type    | Size | Constraints  |
--------------------------------------------------------
| 1  | message type    | int     | 1    | = 0x01       |
--------------------------------------------------------
| 2  | payload size    | int     | 2    | &gt; 0          |
--------------------------------------------------------
| 3  | payload         | bytes   | &lt;2&gt;  |              |
--------------------------------------------------------
| 4  | CRC             | int     | 4    |              |
--------------------------------------------------------</pre>
<p>它的解析函数大概是这个样子：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
bool parse_message_x(char* data, int32 size, MessageX&amp; x) {
    char *ptr = data;
    if (ptr + sizeof(int8) &lt;= data + size) {
        x.message_type = read_int8(ptr);
        if (0x01 != x.message_type) return false;
        ptr += sizeof(int8);
    } else {
        return false;
    }
    if (ptr + sizeof(int16) &lt;= data + size) {
        x.payload_size = read_int16(ptr);
        ptr += sizeof(int16);
    } else {
        return false;
    }
    if (ptr + x.payload_size &lt;= data + size) {
        x.payload = new int8[x.payload_size];
        read(ptr, x.payload, x.payload_size);
        ptr += x.payload_size;
    } else {
        return false;
    }
    if (ptr + sizeof(int32) &lt;= data + size) {
        x.crc = read_int32(ptr);
        ptr += sizeof(int32);
    } else {
        delete x.payload;
        return false;
    }
    if (crc(data, sizeof(int8) + sizeof(int16) + x.payload_size) != x.crc) {
        delete x.payload;
        return false;
    }
    return true;
}
</pre>
<p>很明显，虽然消息X的定义非常简单，但是它的解析函数却显得很繁琐，需要小心翼翼地处理很多细节。在处理其他消息Y时，虽然虽然Y和X很相似，但是却不得不再次在解析过程中处理这些细节，就是组件复用方法的局限性，它只能帮我们按照函数或者类的语义把功能封装成可复用的组件，但是消息的结构特征既不是函数也不是类，这就是抽象维度的失配。</p>
<h4><a href="http://www.cnblogs.com/weidagang2046/p/the-nature-of-meta.html#-2" name="-2"></a>程序的本质复杂性</h4>
<p>上面分析了组件复用技术有着根本性的局限性，现在我们要进一步思考：</p>
<blockquote><p>如果目标还是代码“简短、优雅、易理解、易维护”，那么代码优化是否有一个理论极限？这个极限是由什么决定的？普通代码比起最优代码多出来的“冗余部分”到底干了些什么事情？</p></blockquote>
<p>回答这个问题要从程序的本质说起。Pascal语言之父Niklaus Wirth在70年代提出：Program = Data Structure + Algorithm，随后逻辑学家和计算机科学家R Kowalski进一步提出：Algorithm = Logic + Control。谁更深刻更有启发性？当然是后者！而且我认为数据结构和算法都属于控制策略，综合二位的观点，加上我自己的理解，程序的本质是：Program = Logic + Control。换句话说，程序包含了逻辑和控制两个维度。</p>
<p>逻辑就是问题的定义，比如，对于排序问题来讲，逻辑就是“什么叫做有序，什么叫大于，什么叫小于，什么叫相等”？控制就是如何合理地安排时间和空间资源去实现逻辑。逻辑是程序的灵魂，它定义了程序的本质；控制是为逻辑服务的，是非本质的，可以变化的，如同排序有几十种不同的方法，时间空间效率各不相同，可以根据需要采用不同的实现。</p>
<p>程序的复杂性包含了本质复杂性和非本质复杂性两个方面。套用这里的术语， <strong>程序的本质复杂性就是逻辑，非本质复杂性就是控制</strong>。逻辑决定了代码复杂性的下限，也就是说不管怎么做代码优化，Office程序永远比Notepad程序复杂，这是因为前者的逻辑就更为复杂。如果要代码简洁优雅，任何语言和技术所能做的只是尽量接近这个本质复杂性，而不可能超越这个理论下限。</p>
<p>理解&#8221;程序的本质复杂性是由逻辑决定的&#8221;从理论上为我们指明了代码优化的方向：让逻辑和控制这两个维度保持正交关系。来看Java的Collections.sort方法的例子：</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW">
interface Comparator&lt;T&gt; {
    int compare(T o1, T o2);
}
public static &lt;T&gt; void sort(List&lt;T&gt; list, Comparator&lt;? super T&gt; comparator)
</pre>
<p>使用者只关心逻辑部份，即提供一个Comparator对象表明序在类型T上的定义；控制的部分完全交给方法实现者，可以有多种不同的实现，这就是逻辑和控制解耦。同时，我们也可以断定，这个设计已经达到了代码优化的理论极限，不会有本质上比它更简洁的设计（忽略相同语义的语法差异），为什么？因为逻辑决定了它的本质复杂度，Comparator和Collections.sort的定义完全是逻辑的体现，不包含任何非本质的控制部分。</p>
<p>另外需要强调的是，上面讲的“控制是非本质复杂性”并不是说控制不重要，控制往往直接决定了程序的性能，当我们因为性能等原因必须采用某种控制的时候，实际上被固化的控制策略也是一种逻辑。比如，当你的需求是“从进程虚拟地址ptr1拷贝1024个字节到地址ptr2“，那么它就是问题的定义，它就是逻辑，这时，提供进程虚拟地址直接访问语义的底层语言就与之完全匹配，反而是更高层次的语言对这个需求无能为力。</p>
<p>介绍了逻辑和控制的关系，可能很多朋友已经开始意识到了上面二进制文件解析实现的问题在哪里，其实这也是 <strong>绝大多数程序不够简洁优雅的根本原因：逻辑与控制耦合</strong>。上面那个消息定义表格就是不包含控制的纯逻辑，我相信即使不是程序员也能读懂它；而相应的代码把逻辑和控制搅在一起之后就不那么容易读懂了。</p>
<p>熟悉OOP和GoF设计模式的朋友可能会把“逻辑与控制解耦”与经常听说的“接口和实现解耦”联系在一起，他们是不是一回事呢？其实，把这里所说的逻辑和OOP中的接口划等号是似是而非的， 而GoF设计模式最大的问题就在于有意无意地让人们以为“what就是interface, interface就是what”，很多朋友一想到要表达what，要抽象，马上写个接口出来，这就是潜移默化的惯性思维，自己根本意识不到问题在哪里。其实，接口和前面提到的组件复用技术一样，同样受限于特定的抽象维度，它不是表达逻辑的通用方法，比如，我们无法把二进制文件格式特征用接口来表示。</p>
<p>另外，我们熟悉的许多GoF模式以“逻辑与控制解耦”的观点来看，都不是最优的。比如，很多时候Observer模式都是典型的以控制代逻辑，来看一个例子：</p>
<blockquote><p>对于某网页的超链接，要求其颜色随着状态不同而变化，点击之前的颜色是#FF0000，点击后颜色变成#00FF00。</p></blockquote>
<p>基于Observer模式的实现是这样的：</p>
<p>[javascript]<br />
$(a).css(&#8216;color&#8217;, &#8216;#FF0000&#8217;);</p>
<p>$(a).click(function() {<br />
    $(this).css(&#8216;color&#8217;, &#8216;#00FF00&#8217;);<br />
});<br />
[/javascript]</p>
<p>而基于纯CSS的实现是这样的：</p>
<pre data-enlighter-language="css" class="EnlighterJSRAW">
a:link {color: #FF0000}
a:visited {color: #00FF00}
</pre>
<p>通过对比，您看出二者的差别了吗？显然，Observer模式包含了非本质的控制，而CSS是只包含逻辑。理论上讲，CSS能做的事情，JavaScript都能通过控制做到，那么为什么浏览器的设计者要引入CSS呢，这对我们有何启发呢？</p>
<h4><a href="http://www.cnblogs.com/weidagang2046/p/the-nature-of-meta.html#-3" name="-3"></a>元语言抽象</h4>
<p>好的，我们继续思考下面这个问题：</p>
<blockquote><p>
逻辑决定了程序的本质复杂性，但接口不是表达逻辑的通用方式，那么是否存在表达逻辑的通用方式呢？</p></blockquote>
<p>答案是：有！这就是元(Meta)，包括元语言(Meta Language)和元数据(Meta Data)两个方面。元并不神秘，我们通常所说的配置就是元，元语言就是配置的语法和语义，元数据就是具体的配置，它们之间的关系就是C语言和C程序之间的关系；但是，同时元又非常神奇，因为元既是数据也是代码，在表达逻辑和语义方面具有无与伦比的灵活性。至此，我们终于找到了让代码变得简洁、优雅、易理解、易维护的终极方法，这就是： <strong>通过元语言抽象让逻辑和控制彻底解耦</strong>！</p>
<p>比如，对于二进制消息解析，经典的做法是类似Google的<a href="http://code.google.com/p/protobuf/">Protocol Buffers</a>，把消息结构特征抽象出来，定义消息描述元语言，再通过元数据描述消息结构。下面是Protocol Buffers元数据的例子，这个元数据是纯逻辑的表达，它的复杂度体现的是消息结构的本质复杂度，而如何序列化和解析这些控制相关的部分被Protocol Buffers编译器隐藏起来了。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
message Person {
  required int32 id = 1;
  required string name = 2;
  optional string email = 3;
}
</pre>
<p>元语言解决了逻辑表达问题，但是最终要与控制相结合成为具体实现，这就是元语言到目标语言的映射问题。通常有这两种方法：</p>
<p>1) 元编程(Meta Programming)，开发从元语言到目标语言的编译器，将元数据编译为目标程序代码；</p>
<p>2) 元驱动编程(Meta Driven Programming)，直接在目标语言中实现元语言的解释器。</p>
<p>这两种方法各有优势，元编程由于有静态编译阶段，一般产生的目标程序代码性能更好，但是这种方式混合了两个层次的代码，增加了代码配置管理的难度，一般还需要同时配备Build脚本把整个代码生成自动集成到Build过程中，此外，和IDE的集成也是问题；元驱动编程则相反，没有静态编译过程，元语言代码是动态解析的，所以性能上有损失，但是更加灵活，开发和代码配置管理的难度也更小。除非是性能要求非常高的场合，我推荐的是元驱动编程，因为它更轻量，更易于与目标语言结合。</p>
<p>下面是用元驱动编程解决二进制消息解析问题的例子，meta_message_x是元数据，parse_message是解释器：</p>
<p>[javascript]<br />
var meta_message_x = {<br />
    id: &#8216;x&#8217;,<br />
    fields: [<br />
        { name: &#8216;message_type&#8217;, type: int8, value: 0x01 },<br />
        { name: &#8216;payload_size&#8217;, type: int16 },<br />
        { name: &#8216;payload&#8217;, type: bytes, size: &#8216;$payload_size&#8217; },<br />
        { name: &#8216;crc&#8217;, type: crc32, source: [&#8216;message_type&#8217;, &#8216;payload_size&#8217;, &#8216;payload&#8217;] }<br />
    ]<br />
}</p>
<p>var message_x = parse_message(meta_message_x, data, size);<br />
[/javascript]</p>
<p>这段代码我用的是JavaScript语法，因为对于支持Literal的类似JSON对象表示的语言中，实现元驱动编程最为简单。如果是Java或C++语言，语法上稍微繁琐一点，不过本质上是一样的，或者引入JSON配置文件，然后解析配置，或者定义MessageConfig类，直接把这个类对象作为配置信息。</p>
<p>二进制文件解析问题是一个经典问题，有Protocol Buffers、Android AIDL等大量的实例，所以很多人能想到引入消息定义元语言，但是如果我们把问题稍微变换，能想到采用这种方法的人就不多了。来看下面这个问题：</p>
<blockquote><p>某网站有新用户注册、用户信息更新，和个性设置等Web表单。出于性能和用户体验的考虑，在用户点击提交表单时，会先进行浏览器端的验证，比如：name字段至少3个字符，password字段至少8个字符，并且和repeat password要一致，email要符合邮箱格式；通过浏览器端验证以后才通过HTTP请求提交到服务器。</p></blockquote>
<p>普通的实现是这个样子的：</p>
<p>[javascript]<br />
function check_form_x() {<br />
    var name = $(&#8216;#name&#8217;).val();<br />
    if (null == name || name.length &lt;= 3) {<br />
        return { status : 1, message: &#8216;Invalid name&#8217; };<br />
    }</p>
<p>    var password = $(&#8216;#password&#8217;).val();<br />
    if (null == password || password.length &lt;= 8) {<br />
        return { status : 2, message: &#8216;Invalid password&#8217; };<br />
    }</p>
<p>    var repeat_password = $(&#8216;#repeat_password&#8217;).val();<br />
    if (repeat_password != password.length) {<br />
        return { status : 3, message: &#8216;Password and repeat password mismatch&#8217; };<br />
    }</p>
<p>    var email = $(&#8216;#email&#8217;).val();<br />
    if (check_email_format(email)) {<br />
        return { status : 4, message: &#8216;Invalid email&#8217; };<br />
    }</p>
<p>    &#8230;</p>
<p>    return { status : 0, message: &#8216;OK&#8217; };</p>
<p>}<br />
[/javascript]</p>
<p>上面的实现就是按照组建复用的思想封装了一下检测email格式之类的通用函数，这和刚才的二进制消息解析非常相似，没法在不同的表单之间进行大规模复用，很多细节都必须被重复编写。下面是用元语言抽象改进后的做法：</p>
<p>[javascript]<br />
var meta_create_user = {<br />
    form_id : &#8216;create_user&#8217;,<br />
    fields : [<br />
        { id : &#8216;name&#8217;, type : &#8216;text&#8217;, min_length : 3 },<br />
        { id : &#8216;password&#8217;, type : &#8216;password&#8217;, min_length : 8 },<br />
        { id : &#8216;repeat-password&#8217;, type : &#8216;password&#8217;, min_length : 8 },<br />
        { id : &#8217;email&#8217;, type : &#8217;email&#8217; }<br />
    ]<br />
};</p>
<p>var r = check_form(meta_create_user);<br />
[/javascript]</p>
<p>通过定义表单属性元语言，整个逻辑顿时清晰了，细节的处理只需要在check_form中编写一次，完全实现了“简短、优雅、易理解、以维护”的目标。其实，不仅Web表单验证可以通过元语言描述，整个Web页面从布局到功能全部都可以通过一个元对象描述，完全将逻辑和控制解耦。此外，我编写的用于解析命令行参数的<a href="https://github.com/weidagang/line-parser-js">lineparser.js</a>库也是基于元语言的，有兴趣的朋友可以参考并对比它和其他命令行解析库的设计差异。</p>
<p>最后，我们再来从代码长度的角度来分析一下元驱动编程和普通方法之间的差异。假设一个功能在系统中出现了n次，对于普通方法来讲，由于逻辑和控制的耦合，它的代码量是n * (L + C)，而元驱动编程只需要实现一次控制，代码长度是C + n * L，其中L表示逻辑相关的代码量，C表示控制相关的代码量。通常情况下L部分都是一些配置，不容易引入bug，复杂的主要是C的部分，普通方法中C被重复了n次，引入bug的可能性大大增加，同时修改一个bug也可能要改n个地方。所以，对于重复出现的功能，元驱动编程大大减少了代码量，减小了引入bug的可能，并且提高了可维护性。</p>
<h4><a href="http://www.cnblogs.com/weidagang2046/p/the-nature-of-meta.html#-4" name="-4"></a>总结</h4>
<p>《人月神话》的作者Fred Brooks曾在80年代阐述了它对于软件复杂性的看法，即著名的<a href="http://en.wikipedia.org/wiki/No_Silver_Bullet">No Silver Bullet</a>。他认为不存在一种技术能使得软件开发在生产力、可靠性、简洁性方面提高一个数量级。我不清楚Brooks这一论断详细的背景，但是就个人的开发经验而言，元驱动编程和普通编程方法相比在生产力、可靠性和简洁性方面的确是数量级的提升,在我看来它就是软件开发的银弹！<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" id="wp_rp_first"><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/3980.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/11.jpg" alt="程序员那些悲催的事儿" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3980.html" class="wp_rp_title">程序员那些悲催的事儿</a></li><li ><a href="https://coolshell.cn/articles/3136.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/3.jpg" alt="chmod -x chmod的N种解法" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3136.html" class="wp_rp_title">chmod -x chmod的N种解法</a></li><li ><a href="https://coolshell.cn/articles/2507.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2010/06/5176XS40F9L._SL500_AA300_-150x150.jpg" alt="2000年的iMac和2010年的iPhone" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2507.html" class="wp_rp_title">2000年的iMac和2010年的iPhone</a></li><li ><a href="https://coolshell.cn/articles/7.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2009/03/1-150x150.png" alt="你应该知道的20个Ajax技术(01-10)" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7.html" class="wp_rp_title">你应该知道的20个Ajax技术(01-10)</a></li><li ><a href="https://coolshell.cn/articles/2529.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/6.jpg" alt="StackOverflow的404错误页" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2529.html" class="wp_rp_title">StackOverflow的404错误页</a></li><li ><a href="https://coolshell.cn/articles/3311.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/23.jpg" alt="几篇技术文章" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3311.html" class="wp_rp_title">几篇技术文章</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/10652.html">程序的本质复杂性和元语言抽象</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/10652.html/feed</wfw:commentRss>
			<slash:comments>97</slash:comments>
		
		
			</item>
		<item>
		<title>C++模板”&#062;&#062;”编译问题与词法消歧设计</title>
		<link>https://coolshell.cn/articles/10449.html</link>
					<comments>https://coolshell.cn/articles/10449.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Wed, 02 Oct 2013 10:47:36 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[C++]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=10449</guid>

					<description><![CDATA[<p>（感谢 @文艺复兴记（todd） 投递此文） 在编译理论中，通常将编译过程抽象为5个主要阶段：词法分析(Lexical Analysis)，语法分析(Parsi...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/10449.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/10449.html">C++模板”>>”编译问题与词法消歧设计</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢 <a href="http://weibo.com/weidagang" target="_blank">@文艺复兴记</a>（todd） 投递此文）</strong></p>
<p>在编译理论中，通常将编译过程抽象为5个主要阶段：词法分析(Lexical Analysis)，语法分析(Parsing)，语义分析(Semantic Analysis)，优化(Optimization)，代码生成(Code Generation)。这5个阶段类似Unix管道模型，上一个阶段的输出作为下一个阶段的输入。其中，词法分析是根据输入源代码文本流，分割出词，识别类别，产生词法元素(Token)流，如：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
int a = 10;
</pre>
<p>​经过词法分析会得到[(Type, &#8220;int&#8221;), (Identifier, &#8220;a&#8221;), (AssignOperator, &#8220;=&#8221;), (IntLiteral, 10)]，在后续的语法分析阶段，就会根据这些词法元素匹配相应的语法规则。在我学习编译原理时，教科书中对于词法分析的介绍主要是基于正则表达式的，言下之意就是普通语言的词法规则是可以通过正则表达式描述的。比如，C语言的变量名规则是“包含字母、数字或下划线，并且以字母或下划线开头”，这就可以用正则表达式<code>[a-zA-Z_][a-zA-Z0-9_]*</code>表达。但是，在实践中我发现不管是主流语言，还是自己设计的DSL都大量存在不能简单通过正则表达式进行词法分析的例子。来看C++98的模版例子：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
map&lt;int, vector&lt;int&gt;&gt;
</pre>
<p>上面这段代码会被C++98编译器中报语法错误，原因在于它把“&gt;&gt;”识别成了位右移运算符而不是两个模版右括号，在C++98中必须在两个括号中间加空格，写成</p>
<p><span id="more-10449"></span></p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
map&lt;int, vector&lt;int&gt; &gt;
</pre>
<p>除此了C++模版，据我所知，经典的FORTRAN语言的语法规则更是大量存在词法歧义。</p>
<p>我认为从本质上讲，这类问题的根源在于词法分析的依据只是简单的词法规则，并不具备所有的语法信息，而词法歧义必须提升一层在语法规则中消除。所以，在我自己设计一些DSL的时候干脆就把词法分析和语法分析合二为一了，相当于让语法分析在字符层次上去进行，而不是经典的词法元素层次上，这就是所谓的<a title="Scannerless Parsing" href="http://en.wikipedia.org/wiki/Scannerless_parsing">Scannerless Parsing</a>。采用这种方法的例子并不少见，TeX, Wiki, Makefile和Perl 6等语言的语法分析器都属此类。</p>
<p>Scannerless Parsing方法弥补了词法规则无法消歧的问题，但是同时也破坏了词法和语法分析简单清晰的管道结构，总体上增加了实现和理解的复杂度。另外，像C++这样大型的语言，如果开始是有词法分析的，稍微碰到一个歧义就整个转成Scannerless Parsing未免也显得太夸张了。这个问题困扰了我很久，直到最近才找到了一个满意的解决方案。还是以上面&#8221;&gt;&gt;&#8221;为例，我们知道现在C++11已经允许不加空格了，那么C++11编译器是如何处理这个词法歧义的呢？答案是：词法分析阶段既然分析不好&#8221;&gt;&gt;&#8221;，干脆就不分析了，直接把&#8221;&gt;&#8221; &#8220;&gt;&#8221;交给语法分析器来分析，其他没有词法歧义的照旧。当我知道这个方案的时候不由得感叹：妙！理论上，词法分析是可以什么也不做的，全部把字符一一交给语法分析器也没有问题，所以，干脆让词法分析只做有把握的部分，解决不了的交给语法分析器，这样就既保留了管道结构，又解决了词法歧义。</p>
<p>下面我们再来看看C++11规范关于这个问题的定义：</p>
<blockquote><p>14.2 Names of template specializations [temp.names] ###</p>
<p>After name lookup (3.4) finds that a name is a template-name or that an operator-function-id or a literal-operator-id refers to a set of overloaded functions any member of which is a function template if this is followed by a &lt;, the &lt; is always taken as the delimiter of a template-argument-list and never as the less-than operator. When parsing a template-argument-list, the first non-nested &gt; is taken as the ending delimiter rather than a greater-than operator. Similarly, the first non-nested &gt;&gt; is treated as two consecutive but distinct &gt; tokens, the first of which is taken as the end of the template-argument-list and completes the template-id. [ Note: The second &gt; token produced by this replacement rule may terminate an enclosing template-id construct or it may be part of a different construct (e.g. a cast).—end note ]</p></blockquote>
<p>可见，在C++11中，词法分析器是把&#8221;&gt;&gt;&#8221;直接当成两个&#8221;&gt;&#8221;传给了语法分析器，然后在语法分析中如果匹配了template-argument-lis语法，第一个&#8221;&gt;&#8221;符号会被直接认为是模版结束符，而不是大于，也不是位移符号。根据这个定义，我构造了一个例子：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
template&lt;int N&gt;
class Foo {
};

Foo&lt;3&gt;&gt;1&gt; foo;
</pre>
<p>这个例子在C++98中是能正确编译的，&#8221;&gt;&gt;&#8221;被解释成了位移运算，但是它反而不能在C++11中编译了，因为根据规范第一个&#8221;&gt;&#8221;被解释成了模版参数结束符。如果要在C++11中编译，需要显式地加上括号：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
Foo&lt;(3&gt;&gt;1)&gt; foo;
</pre>
<p><!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-150x150.jpg" alt="Rust语言的编程范式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_title">Rust语言的编程范式</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li><li ><a href="https://coolshell.cn/articles/12012.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/10/edsm-150x150.gif" alt="State Threads 回调终结者" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12012.html" class="wp_rp_title">State Threads 回调终结者</a></li><li ><a href="https://coolshell.cn/articles/11466.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/04/c99-150x150.jpg" alt="C语言的整型溢出问题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11466.html" class="wp_rp_title">C语言的整型溢出问题</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/10449.html">C++模板”>>”编译问题与词法消歧设计</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/10449.html/feed</wfw:commentRss>
			<slash:comments>17</slash:comments>
		
		
			</item>
		<item>
		<title>数据即代码：元驱动编程</title>
		<link>https://coolshell.cn/articles/10337.html</link>
					<comments>https://coolshell.cn/articles/10337.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Fri, 09 Aug 2013 02:18:31 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Javascript]]></category>
		<category><![CDATA[Lisp]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=10337</guid>

					<description><![CDATA[<p>（感谢 @文艺复兴记（todd） 投递此文） 几个小伙伴在考虑下面这个各个语言都会遇到的问题： 问题：设计一个命令行参数解析API 一个好的命令行参数解析库一般...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/10337.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/10337.html">数据即代码：元驱动编程</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢 <a href="http://weibo.com/weidagang" target="_blank">@文艺复兴记</a>（todd） 投递此文）</strong></p>
<p>几个小伙伴在考虑下面这个各个语言都会遇到的问题：</p>
<p><strong>问题：设计一个命令行参数解析API</strong></p>
<p>一个好的命令行参数解析库一般涉及到这几个常见的方面：</p>
<p>1) 支持方便地生成帮助信息</p>
<p>2) 支持子命令，比如：git包含了push, pull, commit等多种子命令</p>
<p>3) 支持单字符选项、多字符选项、标志选项、参数选项等多种选项和位置参数</p>
<p>4) 支持选项默认值，比如：&#8211;port选项若未指定认为5037</p>
<p>5) 支持使用模式，比如：tar命令的-c和-x是互斥选项，属于不同的使用模式</p>
<p>经过一番考察，小伙伴们发现了这个几个有代表性的API设计：</p>
<p><strong>1. getopt()：</strong></p>
<p><a href="http://www.gnu.org/software/libc/manual/html_node/Getopt.html">getopt()</a>是libc的标准函数，很多语言中都能找到它的移植版本。</p>
<p><span id="more-10337"></span></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
//C
while ((c = getopt(argc, argv, &quot;ac:d:&quot;)) != -1) {
    int this_option_optind = optind ? optind : 1;
    switch (c) {
    case &#039;a&#039;:
        printf (&quot;option a&quot;);
        aopt = 1;
        break;
    case &#039;c&#039;:
        printf (&quot;option c with value &#039;%s&#039;&quot;, optarg);
        copt = optarg;
        break;
    case &#039;d&#039;:
        printf (&quot;option d with value &#039;%s&#039;&quot;, optarg);
        dopt = optarg;
        break;
    case &#039;?&#039;:
        break;
    default:
        printf (&quot;?? getopt returned character code 0%o ??&quot;, c);
    }
}
</pre>
<p>getopt()的核心是一个类似printf的格式字符串的命令行参数描述串，如上面的&#8221;ac:d:&#8221;定义了&#8221;a&#8221;, &#8220;c&#8221;，&#8221;d&#8221;3个命令行参数，其中，a是一个标志符不需要参数，&#8221;c&#8221;和&#8221;d&#8221;需要跟参数。getopt()功能非常弱，只支持单个字符的标志选项和参数选项。如果按上面的5点来比对，基本上只能说是勉强支持第3点，其他几项只能靠程序自己来实现了，所以，想直接基于getopt()实现一个像git这样复杂的命令行参数是不可能的，只有自己来做很多的解析工作。小伙伴们看过getopt()之后一致的评价是:图样图森破。</p>
<p><strong>2. Google gflags</strong></p>
<p>接着，小伙伴们又发现了<a href="https://code.google.com/p/gflags/">gflags</a>这个Google出品C++命令行参数解析库。</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
//C++
DEFINE_bool(memory_pool, false, &quot;If use memory pool&quot;);
DEFINE_bool(daemon, true, &quot;If started as daemon&quot;);
DEFINE_string(module_id, &quot;&quot;, &quot;Server module id&quot;);
DEFINE_int32(http_port, 80, &quot;HTTP listen port&quot;);
DEFINE_int32(https_port, 443, &quot;HTTPS listen port&quot;);

int main(int argc, char** argv) {
    ::google::ParseCommandLineFlags(&amp;argc, &amp;argv, true);

    printf(&quot;Server module id: %s&quot;, FLAGS_module_id.c_str());

    if (FLAGS_daemon) {
      printf(&quot;Run as daemon: %d&quot;, FLAGS_daemon);
    }
    if (FLAGS_memory_pool) {
      printf(&quot;Use memory pool: %d&quot;, FLAGS_daemon);
    }

    Server server;

    return 0;
}
</pre>
<p>小伙伴们看了后不由得感叹“真心好用啊”！的确，gflags简单地通过几个宏就定义了命令行选项，基本上很好的支持了上面提到的1，3，4这几项，比起getopt()来强多了。对于类似cp这样的小命令，gflags应该是够用了，但要达到git这种级别就显得有些单薄了。</p>
<p><strong>3. Ruby Commander</strong></p>
<p>接下来小伙伴们又发现了Ruby Commander库：</p>
<pre data-enlighter-language="ruby" class="EnlighterJSRAW">
//Ruby
# :name is optional, otherwise uses the basename of this executable
program :name, &#039;Foo Bar&#039;
program :version, &#039;1.0.0&#039;
program :description, &#039;Stupid command that prints foo or bar.&#039;
command :bar do |c|
  c.syntax = &#039;foobar bar [options]&#039;
  c.description = &#039;Display bar with optional prefix and suffix&#039;
  c.option &#039;--prefix STRING&#039;, String, &#039;Adds a prefix to bar&#039;
  c.option &#039;--suffix STRING&#039;, String, &#039;Adds a suffix to bar&#039;
  c.action do |args, options|
    options.default :prefix =&gt; &#039;(&#039;, :suffix =&gt; &#039;)&#039;
    say &quot;#{options.prefix}bar#{options.suffix}&quot;
  end
end
$ foobar bar
# =&gt; (bar)
$ foobar bar --suffix &#039;}&#039; --prefix &#039;{&#039;
# =&gt; {bar}
</pre>
<p>Commander库利用Ruby酷炫的语法定义了一种描述命令行参数的内部DSL，看起来相当高端大气上档次。除了上面的第5项之外，其他几项都有很好的支持，可以说Commander库的设计基本达到了git这种级别命令行参数解析的要求。只是，要搞懂Ruby这么炫的语法和这个库的使用方法恐怕就不如getopt()和gflags容易了。有小伙伴当场表示想要学习Ruby，但是也有小伙伴表示再看看其他库再说。</p>
<p><strong>4. Lisp cmdline库</strong></p>
<p>接下来，小伙伴们发现了Lisp方言Racket的<a href="http://docs.racket-lang.org/reference/Command-Line_Parsing.html">cmdline库</a>。</p>
<pre data-enlighter-language="ruby" class="EnlighterJSRAW">
//Lisp
(parse-command-line &quot;compile&quot; (current-command-line-arguments)
  `((once-each
     [(&quot;-v&quot; &quot;--verbose&quot;)
      ,(lambda (flag) (verbose-mode #t))
      (&quot;Compile with verbose messages&quot;)]
     [(&quot;-p&quot; &quot;--profile&quot;)
      ,(lambda (flag) (profiling-on #t))
      (&quot;Compile with profiling&quot;)])
    (once-any
     [(&quot;-o&quot; &quot;--optimize-1&quot;)
      ,(lambda (flag) (optimize-level 1))
      (&quot;Compile with optimization level 1&quot;)]
     [(&quot;--optimize-2&quot;)
      ,(lambda (flag) (optimize-level 2))
      ((&quot;Compile with optimization level 2,&quot;
        &quot;which implies all optimizations of level 1&quot;))])
    (multi
     [(&quot;-l&quot; &quot;--link-flags&quot;)
      ,(lambda (flag lf) (link-flags (cons lf (link-flags))))
      (&quot;Add a flag &lt;lf&gt; for the linker&quot; &quot;lf&quot;)]))
   (lambda (flag-accum file) file)
   &#039;(&quot;filename&quot;))
</pre>
<p>这是神马浮云啊?括号套括号，看起来很厉害的样子，但又不是很明白。看到这样的设计，有的小伙伴连评价都懒得评价了，但也有的小伙伴对Lisp越发崇拜，表示Lisp就是所谓的终极语言了，没有哪门语言能写出这么不明觉历的代码来！小伙伴们正准备打完收工，突然&#8230;</p>
<p><strong>5. Node.js的LineParser库</strong></p>
<p>发现了Node.js的<a href="https://github.com/weidagang/line-parser-js">LineParser库</a>:</p>
<p>[javascript]<br />
//JavaScript<br />
var meta = {<br />
    program : &#8216;adb&#8217;,<br />
    name : &#8216;Android Debug Bridge&#8217;,<br />
    version : &#8216;1.0.3&#8217;,<br />
    subcommands : [ &#8216;connect&#8217;, &#8216;disconnect&#8217;, &#8216;install&#8217; ],<br />
    options : {<br />
        flags : [<br />
            [ &#8216;h&#8217;, &#8216;help&#8217;, &#8216;print program usage&#8217; ],<br />
            [ &#8216;r&#8217;, &#8216;reinstall&#8217;, &#8216;reinstall package&#8217; ],<br />
            [ &#8216;l&#8217;, &#8216;localhost&#8217;, &#8216;localhost&#8217; ]<br />
        ],<br />
        parameters : [<br />
            [ null, &#8216;host&#8217;, &#8216;adb server hostname or IP address&#8217;, null ],<br />
            [ &#8216;p&#8217;, &#8216;port&#8217;, &#8216;adb server port&#8217;, 5037 ]<br />
        ]<br />
    },<br />
    usages : [<br />
        [ &#8216;connect&#8217;, [&#8216;host&#8217;, &#8216;[port]&#8217;], null, &#8216;connect to adb server&#8217;, adb_connect ],<br />
        [ &#8216;connect&#8217;, [ &#8216;l&#8217; ], null, &#8216;connect to the local adb server&#8217;, adb_connect ],<br />
        [ &#8216;disconnect&#8217;, null, null, &#8216;disconnect from adb server&#8217;, adb_disconnect ],<br />
        [ &#8216;install&#8217;, [&#8216;r&#8217;], [&#8216;package&#8217;], &#8216;install package&#8217;, adb_install ],<br />
        [ null, [&#8216;h&#8217;], null, &#8216;help&#8217;, adb_help ],<br />
    ]<br />
};</p>
<p>try {<br />
    var lineparser = require(&#8216;lineparser&#8217;);<br />
    var parser = lineparser.init(meta);<br />
    // adb_install will be invoked<br />
    parser.parse([&#8216;install&#8217;, &#8216;-r&#8217;, &#8216;/pkgs/bird.apk&#8217;]);<br />
}<br />
catch (e) {<br />
    console.error(e);<br />
}<br />
[/javascript]</p>
<p>天啊！？这是什么？我和小伙伴们彻底惊呆了！短短十几行代码就获得了上面5点的全面支持，重要的是小伙伴们居然一下子就看懂了，没有任何的遮遮掩掩和故弄玄虚。本来以为Ruby和Lisp很酷，小伙伴们都想马上去学Ruby和Lisp了，看到这个代码之后怎么感觉前面全是在装呢？有个小伙伴居然激动得哭着表示：我写代码多年，以为再也没有什么代码可以让我感动，没想到这段代码如此精妙，我不由得要赞叹了，实在是太漂亮了！</p>
<p>小伙伴们的故事讲完了，您看懂了吗？如果没有看懂的话，正题开始了：</p>
<p>在绝大多数语言中数据和代码可以说是泾渭分明，习惯C++、Java等主流语言的程序员很少去思考数据和代码之间的关系。与多数语言不同的是Lisp以“数据即代码，代码即数据”著称，Lisp用S表达式统一了数据和代码的形式而独树一帜。Lisp奇怪的S表达式和复杂的宏系统让许多人都感到Lisp很神秘，而多数Lisp教程要么强调函数式编程，要么鼓吹宏如何强大，反而掩盖了Lisp真正本质的东西，为此我曾写过一篇<a href="http://www.cnblogs.com/weidagang2046/archive/2012/06/03/tao_of_lisp.html">《Lisp的永恒之道》</a>介绍Lisp思想。</p>
<p>设计思想和具体技术的区别在于前者往往可以在不同的环境中以不同的形式展现出来。比如，熟悉函数式编程的程序员在理解了纯函数的优点后即使是用C语言也会更倾向于写出无副作用的函数来，这就是函数式思想在命令式环境的应用。所以，理解Lisp思想一定要能在非Lisp环境应用，才算是融汇贯通。</p>
<p>如果真正理解了Lisp的本质，那所谓的“数据即代码，代码即数据”一点儿也不神秘，这不就是我们每天打交道的配置文件吗！？如果你还不是很理解的话，我们通过下面几个问题慢慢分析：</p>
<p>1) 配置的本质是什么？为什么要在程序中使用配置文件？</p>
<p>不知道你是否意识到了，我们每天都在使用的各种各样的<strong>配置本质上是一种元数据也是一种DSL</strong>，这和Lisp基于S表达式的“数据即代码，代码即数据”没有本质区别。在C++、Java等程序中引入配置文件的目的正是用DSL弥补通用语言表达能力和灵活性的不足。我知道不少人喜欢从计算的角度来看到程序和语言，似乎只有图灵完备的语言如C++、Java、Python等才叫程序设计语言，而类似CSS和HTML这样的东西根本不能叫做程序设计语言。其实，在我看来这种观点过于狭隘，<strong>程序的本质是语义的表达</strong>，而语义表达不一定要是计算。</p>
<p>2) 配置是数据还是代码？</p>
<p>很明显，Both!说配置是数据，因为它是声明式的描述，能方便地修改和传输；说配置是代码，因为它在表达逻辑，你的程序实际上就是配置的解释器。</p>
<p>3) 配置的格式是什么？</p>
<p>配置的格式是任意的，可以自己定义语法，只要配以相应的解释器就行。不过更简单通用的做法是基于XML、JSON、或S表达式等标准结构，在此之上进一步定义schema。甚至完全不必是文件，在我们的项目中配置经常是放到用关系数据库中的。另外，下面我们还会看到用语言的Literal数据作为配置。</p>
<p>4) 业务逻辑都可以放到配置中吗？</p>
<p>这个问题的答案显然是：Yes！我没有遇到过不可以放入配置的逻辑，只是问题在于这样做是否值得，能达到什么效果。对于需要灵活变化，重复出现，有复用价值的东西放入作为配置是明智的选择。这篇文章的主要目的就在于介绍把<strong>主要业务逻辑都放到配置中，再通过程序解释执行配置的设计方法，我称之为：元驱动编程(Meta Driven Programming)</strong>。</p>
<p><!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/5202.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/14.jpg" alt="对象的消息模型" width="150" height="150" /></a><a href="https://coolshell.cn/articles/5202.html" class="wp_rp_title">对象的消息模型</a></li><li ><a href="https://coolshell.cn/articles/1839.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2009/11/oscar-meyer-wienermobile-150x150.jpg" alt="编程语言汽车" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1839.html" class="wp_rp_title">编程语言汽车</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/10739.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/12/lua-150x150.gif" alt="Lua简明教程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10739.html" class="wp_rp_title">Lua简明教程</a></li><li ><a href="https://coolshell.cn/articles/10169.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/5.jpg" alt="类型的本质和函数式实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10169.html" class="wp_rp_title">类型的本质和函数式实现</a></li><li ><a href="https://coolshell.cn/articles/5709.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="API设计：用流畅接口构造内部DSL" width="150" height="150" /></a><a href="https://coolshell.cn/articles/5709.html" class="wp_rp_title">API设计：用流畅接口构造内部DSL</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/10337.html">数据即代码：元驱动编程</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/10337.html/feed</wfw:commentRss>
			<slash:comments>77</slash:comments>
		
		
			</item>
		<item>
		<title>类型的本质和函数式实现</title>
		<link>https://coolshell.cn/articles/10169.html</link>
					<comments>https://coolshell.cn/articles/10169.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Mon, 22 Jul 2013 11:46:00 +0000</pubDate>
				<category><![CDATA[编程语言]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Design]]></category>
		<category><![CDATA[Javascript]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=10169</guid>

					<description><![CDATA[<p>（感谢 @文艺复兴记（todd） 投递此文） 在上一篇文章《二叉树迭代器算法》中，我介绍了一种基于栈的二叉树迭代器实现。程序设计语言和Haskell大牛@九瓜 ...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/10169.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/10169.html">类型的本质和函数式实现</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢 </strong><a href="http://weibo.com/weidagang" target="_blank">@文艺复兴记</a><strong>（todd） 投递此文）</strong></p>
<p>在上一篇文章<a href="https://coolshell.cn/articles/9886.html">《二叉树迭代器算法》</a>中，我介绍了一种基于栈的二叉树迭代器实现。程序设计语言和Haskell大牛<a href="http://weibo.com/u/1684815495">@九瓜</a> 在看过之后评论到：</p>
<blockquote><p>这里用了 stack 来做，有点偷懒，所以错失了一个抽象思考机会。如果我们能够理解二叉树到线性表的转换过程，完全可以把 Iterator 当作抽象的线性表来看，只要定义了关于 Iterator 的 empty, singleton, 还有 append 操作，实现二叉树的 Iterator 就变得非常直观。</p></blockquote>
<p>“错失了一个抽象思考机会”是什么意思呢？我理解九瓜的意思是基于栈的实现虽然是正确的，但它缺乏对于迭代器类型本质的理解，不具有通用性。如果能对迭代器进行合适地抽象就可以像二叉树递归遍历一样自然地得出二叉树迭代器，甚至其他更复杂的数据结构，只要我们能写出它的遍历算法，迭代器算法都可以自然推出。</p>
<h4>类型的本质</h4>
<p>九瓜提到了通过empty, singleton和append操作对Iterator进行抽象，我本来打算直接根据这个思路介绍函数式的二叉树迭代器实现，但是考虑到其实首要的问题在于理解类型的本质，而并不是所有人都具备这个基础，不如先普及一下类型基础再进入具体实现。那么下面我们就先来认识一下类型到底是什么？我们先以来看看表示元素对的Pair类型，可能有人一提到Pair类型马上就会在脑海中浮现出下面的结构：</p>
<p><span id="more-10169"></span></p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
struct Pair {
    int left;
    int right;
}
</pre>
<p>其实，这种理解是非本质的，Pair完全可以用2个元素的数组来表示，第一个元素表示left，第二个元素表示right：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
struct Pair {
    int elements[2];
}
</pre>
<p>上面的两种不同表示是类型的不同实现，而<strong>类型的本质是由操作(Operation)和操作间的关系或不变式(Invariant)所定义的</strong>，我们称之为类型规范(Type Specification)。比如，Pair类型是这样定义的：</p>
<pre data-enlighter-language="python" class="EnlighterJSRAW">
Type Pair:
    Operations:
        Pair make_pair(int x, int y)
        int get_left(Pair pair)
        int get_right(Pair pair)
    Invariants:
        get_left(make_pair(x, y)) == x  //对x, y构造的Pair取左元素等于x
        get_right(make_pair(x, y)) == y  //对x, y构造的Pair取右元素等于y
</pre>
<p>也就是说只要是满足Pair类型规范，即定义了make_pair，get_left, get_right这3种操作，并且这些操作满足上面两个不变式，那么它这就是Pair类型。我们再来看看稍微复杂一点的Stack类型：</p>
<pre data-enlighter-language="python" class="EnlighterJSRAW">
Type Stack:
    Operations:
        Stack make_stack()  //构造空栈
        Stack push(Stack stack, int x)  //将元素x压栈，返回新栈
        int top(stack)  //获取栈顶元素
        Stack pop(Stack stack)  //将栈顶元素弹栈，返回新栈
    Invariants:
        top(push(stack, x)) == x  //栈顶元素为最后一次压栈值
        pop(push(stack, x)) == stack  //对stack压栈后再弹栈等于原来的stack
</pre>
<p>Stack类型规范简言之就是FILO（先入后出），如果要形式化就是上面的不变式。为了加深理解，我们现在切换到测试视角来看一看，如果请你来编写一个Stack类的单元测试用例，你应该怎么写呢？许多朋友都不清楚单元测试到底测什么？怎么测？我见过不少人用一个测试用例单独对push做测试，用另一个测试用例对pop单独做测试，其主要原因就在于缺乏对类型本质的理解。其实，只要理解了类型的本质我们就知道孤立地看push或pop是没有任何意义的，它们的意义是在FILO关系下相互解释的，所以测试当然是基于类型规范来测试FILO不变式！这种基于类型规范的测试是一种黑盒测试，与类型的内部实现细节无关，只要单元测试覆盖了类型所定义的所有操作和不变式，那么不管内部怎么实现或优化，测试用例都不需要调整。反之，如果深入到了类型的内部实现做白盒测试，那这样的测试用例实际上就不再是反映其类型规范了，它会随着实现细节的调整而失效。</p>
<p>更深一层来看，不仅是在Pair，Stack这样的微观层面，在一个系统的宏观层面同样可以采用类型视角，即考察系统定义了哪些操作？这些操作之间有什么样的关系或不变式？比如，你如何从类型的角度来看待MySQL这样一套数据库系统？MySQL系统定义了哪些操作？这些操作之间必须满足怎样的关系和不变式？不仅如此，类型视角除了可以应用于计算机系统，甚至还可以应用于生活中的事物，比如，你到超市购物可以寄存自己的包，在寄包的时候会获得一张密码条，取包时可以通过密码条打开箱子。你能从超市寄包这个例子中看出类型来吗？如果你看出来了，说明你对类型的理解真正融会贯通了！</p>
<h4>类型的函数式实现</h4>
<p>上面我们介绍了类型的本质在于操作和操作间的关系，下面我们要关注的是类型的实现。在上面的例子中，Pair的内部结构到底是什么，是一个left和一个right成员？还是一个两元素的数组？没有讲，也没关系，就好像Windows的Handle和Linux的FileDescriptor一样，它们都是一个标识，你并不需要关心它的值本身，你只需要用几个相关的函数创建和操作它就行了（上面超市寄包例子中的密码条和Windows中的Handle是什么关系，你看出来了吗？你需要理解密码条的内容吗？）。换句话说，只要满足类型规范，具体实现是可变的，使用者<strong>只依赖于类型规范而不依赖于其具体实现</strong>。这在面向对象语言中意味着接口保持不变而具体实现可以变化（这里把public方法视为一种广义的接口）。</p>
<p>下面，我们还会看到的是不仅类型的内部实现可以变化，而且可以根本没有什么所谓的内部实现。这是什么意思呢？让我们来思考一下，是不是Pair内部一定要有什么东西来保存构造函数传入的left和right？我们能跳出这个定势吗？在函数式编程中，我们能做到：</p>
<p>[javascript]<br />
//Javascript<br />
function make_pair(x, y) {<br />
    // 返回一个支持get_left和get_right操作的闭包(Closure)<br />
    return {<br />
        get_left : function() { return x },<br />
        get_right : function() { return y }<br />
    }<br />
}<br />
function get_left(pair) {<br />
    return pair.get_left();<br />
}<br />
function get_right(pair) {<br />
    return pair.get_right();<br />
}<br />
// Test case<br />
console.log(get_left(make_pair(1, 2))) //1<br />
console.log(get_right(make_pair(1, 2))) //2<br />
[/javascript]</p>
<p>上面的关键代码在于make_pair的内部返回的不是一种具体的数据结构，而是一个支持get_left和get_right操作的闭包(Closure)，将来可以通过get_left和get_right来提取x, y。这种基于闭包的实现和我们通常采用的基于数据结构的实现的本质区别在哪里呢？不难发现，<strong>基于闭包的实现和类型规范是直接对应的</strong>，它并没有引入类型规范之外的东西，而基于数据结构的实现则隐藏了实现的细节。换句话说，如果要验证实现代码的正确性，对于前者只需要比对着类型规范，对于后者我们可能需要去仔细理解推敲其所采用的数据结构。对于Pair这样简单的结构二者差别不大，甚至基于数据结构的实现更简单，但是对于复杂的类型就容易体现出闭包实现的优势了。为了加深理解，我们再来看一个Stack的函数式实现：</p>
<p>[javascript]<br />
//Javascript<br />
function make_stack() {<br />
    return null<br />
}<br />
function push(stack, x) {<br />
    return {<br />
        top : function() { return x },<br />
        pop : function() { return stack }<br />
    }<br />
}<br />
function top(stack) {<br />
    return stack.top()<br />
}<br />
function pop(stack) {<br />
    return stack.pop()<br />
}<br />
// Test case<br />
var stack = make_stack()<br />
stack = push(stack, 1)<br />
stack = push(stack, 2)<br />
stack = push(stack, 3)<br />
console.log(top(stack)) //3<br />
stack = pop(stack)<br />
console.log(top(stack)) //2<br />
stack = push(stack, 4)<br />
console.log(top(stack)) //4<br />
[/javascript]</p>
<p>上面的所有函数都是采用了无副作用的纯函数式设计，可能习惯面向对象编程的朋友不是很习惯，不过这不影响我们对类型的讨论，而且它也很容易改造成面向对象的风格，感兴趣的朋友可以自己尝试对上面的代码进行简单的包装让它看起来像面向对象的样子。</p>
<h4>函数式二叉树迭代器</h4>
<p>上面我们介绍了类型的本质和函数式实现，下面我们再来看看Iterator类型又该如何定义和实现呢？ 思路当然还是从操作入手，考虑Iterator类型对应了哪些操作，它们的关系是什么？上面九瓜提示了Iterator类型可以抽象为线性表List类型，或者说Iterator本质上是一个List。为什么呢？其实，只要跳出“如何表示数据结构”的思维，从类型角度思考就很容易理解，因为Iterator和List都定义了相同的操作，Iterator的使用者完全不关心也不知道它背后到底是链表还是二叉树，你对Iterator的操作和一个List的操作完全一样。正是这个原因，STL等范型库才能通过Iterator将算法和数据结构解耦。</p>
<p>怎么定义一个List类型呢？九瓜提到的empty(), singleton()和append()实际上就是和List打交道最多的Lisp语言的经典定义方式。Lisp是基于s-expression的，s-expression既可以视为线性表又可以视为树，本质上Lisp为List类型了构造、取首元素和取剩余元素等几种操作：</p>
<pre data-enlighter-language="python" class="EnlighterJSRAW">
Type List:
    Operations:
        List empty()  //构造空表，通常由()这个文字量表示
        List singleton(Element e)  //构造包含一个元素的表，通常由(e)这个文字量表示
        Element first(List list)   //取list的第一个元素，通常又叫car操作
        List rest(List list)  //取list除第一个元素外的剩余部分，通常又叫cdr操作
        List append(List list1, List list2) //连接两个表
    Invariants:
        append(empty(), list) == list  //空表和表list连接后等于表list
        append(list, empty()) == list  //空表和表list连接后等于表list
        first(singleton(e)) == e  //对singleton(e)取首元素等于e
        rest(singleton(e)) == empty()  //对singleton(e)取首元素外的剩余部分的结果为空表
        append(first(list), rest(list)) == list  //对list的首尾两部分进行连接等于list本身
        if list1 is not empty then
            first(append(list1, list2)) == first(list1)  //对非空表list1于表list2的连接取首元素等于对非空表list1取首元素
        if list1 is not empty then
            rest(append(list1, list2)) == append(rest(list1), list2)  //对非空表list1于表list2的连接取首元素等于对非空表list1取首元素
</pre>
<p>有了上面的分析，我们相应地写出下面的List实现：</p>
<p>[javascript]<br />
//Javascript<br />
function empty() {<br />
    return null<br />
}<br />
function singleton(e) {<br />
    return {<br />
        first: function() { return e },<br />
        rest: function() { return null }<br />
    }<br />
}<br />
function first(list) {<br />
    return list.first()<br />
}<br />
function rest(list) {<br />
    return list.rest()<br />
}<br />
function append(list1, list2) {<br />
    if (null == list1) return list2<br />
    if (null == list2) return list1</p>
<p>    return {<br />
        first : function() { return first(list1) },<br />
        rest : function() { return append(rest(list1), list2) }<br />
    }<br />
}<br />
[/javascript]</p>
<p>在此基础上可以进一步实现二叉树迭代器：</p>
<p>[javascript]<br />
function make_binary_tree_iterator(node) {<br />
    return {<br />
        first : function() {<br />
            return null != node.left ? first(make_binary_tree_iterator(node.left)) : node<br />
        },<br />
        rest : function() {<br />
            var left_it = (null == node.left ? null : make_binary_tree_iterator(node.left))<br />
            var root_it = singleton(node)<br />
            var right_it = (null == node.right ? null : make_binary_tree_iterator(node.right))<br />
            var it = append(append(left_it, root_it), right_it)<br />
            return rest(it)<br />
        }<br />
    }<br />
}<br />
//======== Test case ========<br />
var tree = {<br />
    value : 1,<br />
        left : {<br />
            value : 2,<br />
            left : { value : 4, left : null, right : null },<br />
            right : null<br />
        },<br />
        right : {<br />
            value : 3,<br />
            left : null,<br />
            right : { value : 7, left : null, right : null }<br />
    }<br />
}<br />
for (var it = make_binary_tree_iterator(tree); null != it; it = rest(it)) {<br />
    console.log(first(it).value)<br />
}<br />
[/javascript]</p>
<p>上面的make_binary_tree_iterator在List类型的基础上按照二叉树遍历过程构造了一个List。不知道你是否注意到了，为什么它不像下面这个例子一样直接返回一个List，而要构造一个闭包呢？</p>
<p>[javascript]<br />
function make_binary_tree_iterator(node) {<br />
    var left_it = (null == node.left ? null : make_binary_tree_iterator(node.left))<br />
    var root_it = singleton(node)<br />
    var right_it = (null == node.right ? null : make_binary_tree_iterator(node.right))<br />
    return append(append(left_it, root_it), right_it)<br />
}<br />
[/javascript]</p>
<p>这里关键的区别在于闭包是惰性求值的，也就是说只有当真正开始迭代遍历的时候才会逐渐对各个函数进行求值，而上面的函数递归调用是非惰性的，会从一开始就把所有结点展开成线性表。如果你对这一点还不能很好地理解，可以尝试在各个函数中加log跟踪函数的调用过程。</p>
<h4>总结</h4>
<p>本文介绍了类型的本质在于它所定义的操作以及操作之间的关系和不变式。类型的实现关键在于满足类型规范的要求，而具体实现是可以变化的，使用者和测试用例都应该只依赖于类型规范而不依赖于具体实现。函数式的类型实现往往和类型规范是直接对应的，简单通用，但可能有性能问题，而命令式的类型实现往往会引入复杂的内部数据结构，但是其优点是高效。这两种实现并不是完全互斥的，有时候可以将二者相结合达到简单与高效的结合。<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li><li ><a href="https://coolshell.cn/articles/10822.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/12/yoda-lambda-150x150.png" alt="函数式编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10822.html" class="wp_rp_title">函数式编程</a></li><li ><a href="https://coolshell.cn/articles/10739.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/12/lua-150x150.gif" alt="Lua简明教程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10739.html" class="wp_rp_title">Lua简明教程</a></li><li ><a href="https://coolshell.cn/articles/10337.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/24.jpg" alt="数据即代码：元驱动编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10337.html" class="wp_rp_title">数据即代码：元驱动编程</a></li><li ><a href="https://coolshell.cn/articles/7886.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/07/muxnt-150x150.jpg" alt="代码执行的效率" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7886.html" class="wp_rp_title">代码执行的效率</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/10169.html">类型的本质和函数式实现</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/10169.html/feed</wfw:commentRss>
			<slash:comments>32</slash:comments>
		
		
			</item>
		<item>
		<title>二叉树迭代器算法</title>
		<link>https://coolshell.cn/articles/9886.html</link>
					<comments>https://coolshell.cn/articles/9886.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Sun, 14 Jul 2013 03:08:23 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[Algorithm]]></category>
		<category><![CDATA[C++]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=9886</guid>

					<description><![CDATA[<p>（感谢 @文艺复兴记（todd） 投递此文） 二叉树(Binary Tree)的前序、中序和后续遍历是算法和数据结构中的基本问题，基于递归的二叉树遍历算法更是递...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/9886.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/9886.html">二叉树迭代器算法</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢 </strong><a href="http://weibo.com/weidagang" target="_blank">@文艺复兴记</a><strong>（todd） 投递此文）</strong></p>
<p>二叉树(Binary Tree)的前序、中序和后续遍历是算法和数据结构中的基本问题，基于递归的二叉树遍历算法更是递归的经典应用。</p>
<p>假设二叉树结点定义如下：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
// C++
struct Node {
    int value;
    Node *left;
    Node *right;
}
</pre>
<p>中序递归遍历算法：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
// C++
void inorder_traverse(Node *node) {
    if (NULL != node-&gt;left) {
        inorder_traverse(node-&gt;left);
    }
    do_something(node);
    if (NULL != node-&gt;right) {
        inorder_traverse(node-&gt;right);
    }
}
</pre>
<p>前序和后序遍历算法类似。</p>
<p>但是，仅有遍历算法是不够的，在许多应用中，我们还需要对遍历本身进行抽象。假如有一个求和的函数sum，我们希望它能应用于链表，数组，二叉树等等不同的数据结构。这时，我们可以抽象出迭代器(Iterator)的概念，通过<strong>迭代器把算法和数据结构解耦了</strong>，使得通用算法能应用于不同类型的数据结构。我们可以把sum函数定义为：</p>
<p><span id="more-9886"></span></p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
int sum(Iterator it)
</pre>
<p>链表作为一种线性结构，它的迭代器实现非常简单和直观，而二叉树的迭代器实现则不那么容易，我们不能直接将递归遍历转换为迭代器。究其原因，这是因为二叉树递归遍历过程是编译器在调用栈上自动进行的，程序员对这个过程缺乏足够的控制。既然如此，那么我们如果可以自己来控制整个调用栈的进栈和出栈不是就达到控制的目的了吗？我们先来看看二叉树遍历的非递归算法：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
// C++
void inorder_traverse_nonrecursive(Node *node) {
    Stack stack;
    do {
        // node代表当前准备处理的子树，层层向下把左孩子压栈，对应递归算法的左子树递归
        while (NULL != node) {
            stack.push(node);
            node = node-&gt;left;
        }
        do {
            Node *top = stack.top();
            stack.pop(); //弹出栈顶，对应递归算法的函数返回
            do_something(top);
            if (NULL != top-&gt;right) {
                node = top-&gt;right; //将当前子树置为刚刚遍历过的结点的右孩子，对应递归算法的右子树递归
                break;
            }
        }
        while (!stack.empty());
    }
    while (!stack.empty());
}
</pre>
<p>通过基于栈的非递归算法我们获得了对于遍历过程的控制，下面我们考虑如何将其封装为迭代器呢？ 这里关键在于理解遍历的过程是由栈的状态来表示的，所以显然迭代器内部应该包含一个栈结构，每次迭代的过程就是对栈的操作。假设迭代器的接口为：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
// C++
class Iterator {
    public:
        virtual Node* next() = 0;
};
</pre>
<p>下面是一个二叉树中序遍历迭代器的实现：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">
//C++
class InorderIterator : public Iterator {
    public:
        InorderIterator(Node *node) {
            Node *current = node;
            while (NULL != current) {
                mStack.push(current);
                current = current-&gt;left;
            }
        }
        virtual Node* next() {
            if (mStack.empty()) {
                return NULL;
            }
            Node *top = mStack.top();
            mStack.pop();
            if (NULL != top-&gt;right) {
                Node *current = top-&gt;right;
                while (NULL != current) {
                    mStack.push(current);
                    current = current-&gt;left;
                }
            }
            return top;
         }
    private:
        std::stack&lt;Node*&gt; mStack;
};
</pre>
<p>下面我们再来考察一下这个迭代器实现的时间和空间复杂度。很显然，由于栈中最多需要保存所有的结点，所以其空间复杂度是O(n)的。那么时间复杂度呢？一次next()调用也最多会进行n次栈操作，而整个遍历过程需要调用n次next()，那么是不是整个迭代器的时间复杂度就是O(n^2)呢？答案是否定的！因为每个结点只会进栈和出栈一次，所以整个迭代过程的时间复杂度依然为O(n)。其实，这和递归遍历的时空复杂度完全一样。</p>
<p>除了上面显式利用栈控制代码执行顺序外，在支持yield语义的语言（C#, Python等)中，还有更为直接的做法。下面基于yield的二叉树中序遍历的Python实现：</p>
<pre data-enlighter-language="python" class="EnlighterJSRAW">
// Python
def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x
        yield t.label
        for x in inorder(t.right):
            yield x
</pre>
<p>yield与return区别的一种通俗解释是yield返回时系统会保留函数调用的状态，下次该函数被调用时会接着从上次的执行点继续执行，这是一种与栈语义所完全不同的流程控制语义。我们知道Python的解释器是C写的，但是C并不支持yield语义，那么解释器是如何做到对yield的支持的呢？ 有了上面把递归遍历变换为迭代遍历的经验，相信你已经猜到Python解释器一定是对yield代码进行了某种变换。如果你已经能够实现递归变非递归，不妨尝试一下能否写一段编译程序将yield代码变换为非yield代码。<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li><li ><a href="https://coolshell.cn/articles/8239.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/09/lock_free_bicycle-150x150.jpg" alt="无锁队列的实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8239.html" class="wp_rp_title">无锁队列的实现</a></li><li ><a href="https://coolshell.cn/articles/6010.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/23.jpg" alt="一些有意思的算法代码" width="150" height="150" /></a><a href="https://coolshell.cn/articles/6010.html" class="wp_rp_title">一些有意思的算法代码</a></li><li ><a href="https://coolshell.cn/articles/4220.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/0.jpg" alt="一些有意思的文章和资源" width="150" height="150" /></a><a href="https://coolshell.cn/articles/4220.html" class="wp_rp_title">一些有意思的文章和资源</a></li><li ><a href="https://coolshell.cn/articles/3738.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/26.jpg" alt="打印质数的各种算法" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3738.html" class="wp_rp_title">打印质数的各种算法</a></li><li ><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-150x150.jpg" alt="Rust语言的编程范式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_title">Rust语言的编程范式</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/9886.html">二叉树迭代器算法</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/9886.html/feed</wfw:commentRss>
			<slash:comments>54</slash:comments>
		
		
			</item>
		<item>
		<title>TF-IDF模型的概率解释</title>
		<link>https://coolshell.cn/articles/8422.html</link>
					<comments>https://coolshell.cn/articles/8422.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Wed, 24 Oct 2012 01:05:54 +0000</pubDate>
				<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Algorithm]]></category>
		<category><![CDATA[search]]></category>
		<category><![CDATA[TF-IDF]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=8422</guid>

					<description><![CDATA[<p>（感谢 @猫叔shiro（以前的todd） 投递此文） 信息检索概述 信息检索是当前应用十分广泛的一种技术，论文检索、搜索引擎都属于信息检索的范畴。通常，人们把...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/8422.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/8422.html">TF-IDF模型的概率解释</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong><span style="color: #cc0000">（感谢 </span></strong><a href="http://weibo.com/weidagang" target="_blank">@猫叔shiro</a><strong><span style="color: #cc0000">（以前的todd） 投递此文）</span></strong></p>
<h4>信息检索概述</h4>
<p>信息检索是当前应用十分广泛的一种技术，论文检索、搜索引擎都属于信息检索的范畴。通常，人们把信息检索问题抽象为：在文档集合D上，对于由关键词w[1] &#8230; w[k]组成的查询串q，返回一个按查询q和文档d匹配度relevance(q, d)排序的相关文档列表D&#8217;。</p>
<p>对于这一问题，先后出现了布尔模型、向量模型等各种经典的信息检索模型，它们从不同的角度提出了自己的一套解决方案。布尔模型以集合的布尔运算为基础，查询效率高，但模型过于简单，无法有效地对不同文档进行排序，查询效果不佳。向量模型把文档和查询串都视为词所构成的多维向量，而文档与查询的相关性即对应于向量间的夹角。不过，由于通常词的数量巨大，向量维度非常高，而大量的维度都是0，计算向量夹角的效果并不好。另外，庞大的计算量也使得向量模型几乎不具有在互联网搜索引擎这样海量数据集上实施的可行性。</p>
<h4>tf-idf模型</h4>
<p>目前，真正在搜索引擎等实际应用中广泛使用的是tf-idf模型。tf-idf模型的主要思想是：如果词w在一篇文档d中出现的频率高，并且在其他文档中很少出现，则认为词w具有很好的区分能力，适合用来把文章d和其他文章区分开来。该模型主要包含了两个因素：</p>
<p><span id="more-8422"></span></p>
<p>1) 词w在文档d中的词频tf (Term Frequency)，即词w在文档d中出现次数count(w, d)和文档d中总词数size(d)的比值：</p>
<pre><code>tf(w,d) = count(w, d) / size(d) </code></pre>
<p>2) 词w在整个文档集合中的逆向文档频率idf (Inverse Document Frequency)，即文档总数n与词w所出现文件数docs(w, D)比值的对数:</p>
<pre><code>idf = log(n / docs(w, D)) </code></pre>
<p>tf-idf模型根据tf和idf为每一个文档d和由关键词w[1]&#8230;w[k]组成的查询串q计算一个权值，用于表示查询串q与文档d的匹配度：</p>
<pre><code>
tf-idf(q, d) 
= sum { i = 1..k | tf-idf(w[i], d) } 
= sum { i = 1..k | tf(w[i], d) * idf(w[i]) } 
</code></pre>
<h4>信息检索问题的概率视角</h4>
<p>直观上看，tf描述的是文档中词出现的频率；而idf是和词出现文档数相关的权重。我们比较容易定性地理解tf-idf的基本思想，但具体到tf-idf的一些细节却并不是那么容易说清楚为什么。比如：</p>
<p>1) 为什么tf是count(w, d) / size(d)？能不能是log(count(w, d) / size(d))等其他形式？</p>
<p>2) 为什么idf是一个log形式？</p>
<p>3) 为什么tf和idf之间是乘积关系，而不是加法或指数关系？</p>
<p>4) 为什么多个关键词的tf-idf值是加法关系，而不是乘法或者指数关系？</p>
<p>5) 除了tf-idf值，Google还会计算网页的PageRank值，二者相乘得到最后的权值，为什么是乘法，而不是加法或指数？</p>
<p>据说，最初甚至tf-idf的提出者自己也没有对诸如“为什么idf是log形式”这个问题给出有力的解释，虽然后来有人从信息论的角度对idf的log形式给出了令人信服的解释，但是剩下的其他一些疑问仍然存在。在我了解的范围内，对于tf-idf模型还没有一个真正统一完整的理论解释。在试图为tf-idf找到更好的理论解释的过程中，我意识到对tf-idf模型种种疑问的根源在于tf-idf试图表达的“查询q和文档的匹配度”本身就有一定的模糊性，什么叫做“匹配度”，这就有很大的自由发挥空间。如果说向量模型的用向量夹角来表示匹配度概念还有一定的理论基础，那么用tf-idf来表达匹配度就有点“与其说是科学，不如说是艺术”的味道。</p>
<p>更进一步，其实，信息检索问题的抽象方式“在文档集合D上，对于给定查询串q，返回一个按查询q和文档d匹配度relevance(q, d)排序的相关文档列表D&#8217;”本身是值得反思的。我们应当考虑抛弃“匹配度”这种模糊的目标，从根源上寻求一种具有明确数学意义的目标。如果我们从概率视角来看，<strong>把“查询串q和文档d的匹配度”问题转换为“当查询串是q时，用户期望获得文档d的概率”问题</strong>，信息检索问题就清晰多了。一方面这个概率描述是站在人的角度来看待信息检索问题的，更加贴近实际的用户体验；另一方面，概率本身是有明确数学意义的，这样我们就首先从目标上对问题进行了严格化。</p>
<p>下面，我将通过一个模型，从概率的视角，一边解释tf-idf的概率意义，一边指出其不合理之处。</p>
<h4>盒子小球模型</h4>
<p>为了分析“当查询串是q时，用户期望获得文档d的概率”问题，我首先建立了一种称为“盒子小球模型”的简化模型。盒子小球模型把词想象成各种不同颜色的小球，文档想象成装有若干小球的盒子，把“当查询串是q时，用户期望获得文档d的概率“转换为下面的问题：</p>
<p><strong>有n个盒子d[1], d[2], &#8230; d[n]，每个盒子中有若干不同颜色的小球，有人随机地选择了一个盒子，并从盒子中随机地拿出了一个颜色为w[j]的小球，那么这个小球来自于盒子d[i]的概率是多少？</strong></p>
<p>其实，这就是经典的条件概率问题P(d[i] | w[j])，采用贝叶斯推断将其转化为：</p>
<pre><code>
P(d[i] | w[j]) 
= P(d[i], w[j]) / P(w[j]) 
= P(d[i]) * P(w[j] | d[i]) / P(w[j]) 
</code></pre>
<p>我们注意到这个条件概率包括几个部分，P(d[i])是盒子d[i]被选中的先验概率，p(w[j])是w[j]颜色小球被选中的先验概率，P(w[j] | d[i])是在盒子d[i]中选中颜色w[j]小球的条件概率。</p>
<h4>文档先验概率P(d)与PageRank</h4>
<p>首先，我们来看盒子d[i]被选中的先验概率P(d[i])是什么。P(d[i])的意义是：当用户什么也没有输入的时候，它可能对文档d[i]感兴趣的概率。在没有更多信息的情况下，我们可以认为每个盒子被选中的先验概率P(d[i])是相等的，都等于1 / m，其中m表示总文档数（总盒子数），这时P(d[i])作为公共系数可被忽略。不过，在实际应用中，我们通常可以根据其他知识获得各文档的先验概率，比如，学术文献和网页通常可以基于引用度模型计算其先验概率，这些经典论文和热门网页是多数人乐于见到的。说到这里，你可能已经发现，Google PageRank本质上就是这个先验概率P(d[i])乘以某个系数！所以，PageRank实际上也被纳入这个条件概率模型中来了，这就不难解释为什么在Google的排序算法中PageRank权重和tf-idf权重是一种乘积关系而不是加或者指数关系。另一方面，在理解了文档先验概率对整个搜索结果概率的影响后，当搜索引擎中针对PageRank出现各种假链接SEO时，我们可以不拘泥于基于链接引用模型的PageRank，只要是以网页先验概率为目标，不论是采用基于链接引用的PageRank，还是基于搜索结果点击数模型，或是其他模型，都是可以的。这就是“变通”，从原理上“通”了，就可以在方法上“变”。</p>
<h4>词的先验概率P(w)</h4>
<p>下面我们来考察词w[j]的先验概率P(w[j])。P(w[j])的意义是：在整个文档集合中，w[j]被作为搜索关键词的概率，比如：“iPhone 5”，“青花瓷”这类词被用作搜索关键词的概率较高，而“的”，“什么”，“我们”这类高频词不大可能成为搜索关键词。那么，我们如何来定量计算P(w[j])呢？一种思路就是把w[j]在文档集中出现的频率作为其先验概率。不过，显然存在更好的方案：在大量的搜索查询中进行统计，统计方法得出P(w[j])的方法很接近P(w[j])本质的，不需要引入额外的假设。比如，一段时间内某搜索引擎的搜索总次数为10^10次，“公积金”这个词出现了100次，那么，我们可以认为先验概率P(&#8220;公积金&#8221;)就是100 / 10^10 = 10^-8。</p>
<h4>词代表文档主题的条件概率P(w | d)</h4>
<p>最后，我们来看条件概率P(w[j] | d[i])。P(w[j] | d[i])的意义是在文档d[i]中，人们用关键词w[j]来搜索它的概率。那么，什么样的词是人们会用来搜索一篇文档的呢？多数情况下，是那些代表一篇文档主题的词。比如，有一篇新闻是关于iPhone 5发布会的，那么“iPhone5”， “发布会”，“库克”，“苹果”这些词基本上就构成了文章的主题；那么，反过来说，如果用户想搜索这篇关于iPhone 5发布会的新闻，他就有很大的可能通过这几个词来进行搜索。我们应当注意分辨P(w[j] | d[i])与P(w[j])的区别，后者可以通过大量的查询统计得来，而前者不能与后者直接划等号，因为前者的意义是w[j]代表d[i]主题的概率。如果非要引入统计方法，那么P(w[j] | d[i])对应的统计是：当搜索关键词是w[j]且搜索结果包含d[i]时，用户点击（满意）d[i]作为搜索结果的频率。比如，用“iPhone5 发布会”的搜索，在结果中有都10000次出现了网页x，其中，用户8000次点击了网页x，那么，可以认为有80%的概率网页x的主题是关于“iPhone5 发布会”的。</p>
<h4>词的信息量和idf</h4>
<p>上面谈到了对P(w[j] | d[i])的计算的统计方法，但该方法有一定的局限，比如，要能进行统计首先需要文档出现在足够多的搜索结果中，需要时间和量的积累。除了统计方法外，我们可以考虑其他方法计算词w[j]代表文档d[i]主题的概率。可能有人立刻会想到要对文章进行语义分析提取关键词，给这些关键词高权重，给其他词低权重。这种想法有一定的合理性，但实现上涉及语义分析，没有成熟高效的方法。实际上，信息论为我们提供了另一条高效方案。上面谈到“的”，“什么”，“我们”这类高频词不会成为文档主题和搜索关键词的原因是它们不能提供足够的信息，而“iPhone 5”，“发布会”这样的词汇则信息量丰富。所谓信息是指对不确定性（熵）的减小程度，信息的单位是比特(bit)，信息量越大对于不确定性的减小程度越大。比如，外面可能在下雨也可能没有下雨，可能性空间大小为2，如果我们看一眼窗外，可能性空间就变成了1，那么“看见窗外在下雨”所提供的信息量就和熵的减小程度成正比，具体来讲等于log(2/1)=1。如果要用二进制编码是否下雨，需要1个bit，0代表没有下雨，1代表下雨。</p>
<p>但在很多场景下，各个可能性的概率并不相同，比如：欧洲杯16只球队都可能夺冠，赛前它们夺冠的先验概率并不相同，那么结果的不确定性程度实际上是小于log(16)=4。如果你没有看比赛，有人告诉你西班牙夺冠了，你可能会觉得很正常，但如果有人告诉你瑞士夺冠了，你通常会非常惊讶。这一现象的理论解释是，如果赛前西班牙夺冠概率是1/4，而瑞士夺冠概率是1/32，那么，“西班牙夺冠”的信息量为log(4)=2，即把不确定性减小为原来的1/4，而“瑞士夺冠”的信息量为log(32)=5，不确定性减小为原来的1/32，一下子接受比前者大了两倍以上的信息量，当然你会吃惊。</p>
<p>回到信息检索，比如，“2012美国大选”这个查询串包含了“2012”，“美国”和“大选”3个关键词，我们应该如何定量计算它们的信息量呢？根据信息的定义，词的信息量等于它对不确定性的缩小程度。如果文档总数为2^30，其中2^14篇文档出现了“美国”，那么“美国”这个词就把文档的不确定性从2^30缩小为2^14，它所包含的信息量为log(2^30/2^14)=16；而只有2^10篇文档出现了“大选”，那么大选的信息量就是log(2^30/2^10)=20，比“美国”多了4个bit。而“的”，“什么”，“我们”这些高频词对减小文档不确定性几乎没有帮助，因而信息量为0。相信你已经发现，上面idf(w)公式中的log(n / docs(w, D))实际上就是词w的信息量了。</p>
<p>如果我们考虑词的信息量对条件概率P(w[j] | d[i])的影响，假设“词w在文档中被选中的概率与其在文档中的出现频率和其信息量的乘积成正比”，那么上面的条件概率模型就变成：</p>
<pre><code>
P(d[i] | w[j]) 
= P(d[i], w[j]) / P(w[j]) 
= P(d[i]) * P(w[j] | d[i]) / P(w[j]) 
= P(d[i]) * (tf(w[j], d[i]) * idf(w[j] / sum { k = 1..size(d[i]), tf(w[k], d[i]) * idf(w[k]) }) / p(w[j]) 
= P(d[i]) * (tf-idf(w[j], d[i]) / sum { k = 1..size(d[i]), tf-idf(w[k], d[i]) }) / p(w[j]) 
= P(d[i]) * (tf-idf(w[j], d[i]) / tf-idf(d[i])) / p(w[j]) 
</code></pre>
<p>我们看到tf-idf已经被纳入框架内了，但是还多出文档先验概率P(d[i])，关键词先验概率P(w[j])和文档各词的总tf-idf(d[i])。普通搜索引擎是基于PageRank和tf-idf的，那么，根据这个概率模型，我们可以看出，它没有考虑文档总tf-idf(d[i])和关键词先验概率p(w[j])。如果考虑这两个因素，相信搜索效果会更好。</p>
<h4>多关键词</h4>
<p>上面的条件概率模型主要是针对单个关键词的情况，下面我们进一步将其扩展到多关键词情况。我们知道，在tf-idf中，多个关键词的所产生的tf-idf值是一种叠加关系，那么这是否符合条件概率模型呢？答案是否定的。在两个关键字情况下，条件概率问题转化为“如果有人从一个盒子中同时摸出颜色w[x]的小球和颜色w[y]的小球，这两个小球来自于盒子d[i]的概率是多少？”。假设从盒子中摸出各个小球事件是相互独立的情况下，即</p>
<pre><code>
P(w[x], w[y]) 
= P(w[x]) * P(w[y]) P(w[x], w[y] | d[i]) 
= P(w[x] | d[i]) * P(w[y] | d[i]) 
</code></pre>
<p>我们可以推导出条件概率：</p>
<pre><code>
P(d[i] | w[x], w[y]) 
= P(d[i], w[x], w[y]) / P(w[x], w[y]) 
= P(d[i]) * P(w[x], w[y] | d[i]) / P(w[x], w[y]) 
= P(d[i]) * P(w[x] | d[i]) * P(w[y] | d[i]) / (P(w[x] * P(w[y])) 
= P(d[i]) * (tf-idf(w[x], d[i]) / tf-idf(d[i])) * ((tf-idf(w[y], d[i]) / tf-idf(d[i]))) / (p(w[x]) * P(w[y])) 
</code></pre>
<p>可见，概率模型所得出的各个关键词的tf-idf值之间是乘积关系，这是与tf-idf模型的加法关系是不同的。这一点可能与二者是否要求“文档必须包含所有查询关键词”的基本假设有关系。在文档不包含所有关键字的这种情况下，tf-idf模型可能得出一个非0的匹配度，但条件概率模型得出的概率肯定为0。不过，如果考虑一般查询关键词数量不多（3个以内），而大量文档都同时包含这些关键词，概率模型的乘积关系是比tf-idf模型的加法关系更有理论基础。从根本上讲，这是因为tf-idf的“匹配度”是一个模棱两可的概念，而条件概率有坚实的理论基础。</p>
<h4>总结</h4>
<p>TF-IDF模型是搜索引擎中广泛使用的信息检索模型，但对于TF-IDF模型一直存在各种疑问。本文为信息检索问题一种基于条件概率的盒子小球模型，其核心思想是把“查询串q和文档d的匹配度问题”转化为“查询串q来自于文档d的条件概率问题”。它从概率的视角为信息检索问题定义了比TF-IDF模型所表达的匹配度更为清晰的目标。从概率模型中，我们看到查询串q来自于文档d的条件概率主要包含以下几个因素：1) 文档的先验概率P(d[i])，这与PageRank对应；2) 词w被作为搜索关键词的先验概率P(w)，这可以通过统计方法获得；3) 关键词w代表文档d主题，或以词w搜索文档d的概率，P(w | d)，除了统计方法，这可以通过tf-idf来计算。<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/17391.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2016/07/Community-150x150.jpg" alt="为什么我不在微信公众号上写文章" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17391.html" class="wp_rp_title">为什么我不在微信公众号上写文章</a></li><li ><a href="https://coolshell.cn/articles/17225.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/cuckoo-150x150.jpg" alt="Cuckoo Filter：设计与实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17225.html" class="wp_rp_title">Cuckoo Filter：设计与实现</a></li><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li><li ><a href="https://coolshell.cn/articles/11847.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/08/puzzle-150x150.png" alt="谜题的答案和活动的心得体会" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11847.html" class="wp_rp_title">谜题的答案和活动的心得体会</a></li><li ><a href="https://coolshell.cn/articles/11832.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/08/538efefbgw1eiz9cvx78fj20rm0fmdi8-150x150.jpg" alt="【活动】解迷题送礼物" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11832.html" class="wp_rp_title">【活动】解迷题送礼物</a></li><li ><a href="https://coolshell.cn/articles/10590.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/10/QR-Code-Overview-150x150.jpeg" alt="二维码的生成细节和原理" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10590.html" class="wp_rp_title">二维码的生成细节和原理</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/8422.html">TF-IDF模型的概率解释</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/8422.html/feed</wfw:commentRss>
			<slash:comments>51</slash:comments>
		
		
			</item>
		<item>
		<title>Lisp的永恒之道</title>
		<link>https://coolshell.cn/articles/7526.html</link>
					<comments>https://coolshell.cn/articles/7526.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Mon, 04 Jun 2012 00:58:46 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Lisp]]></category>
		<category><![CDATA[面向语言编程]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=7526</guid>

					<description><![CDATA[<p>【感谢 Todd投递本文 – 微博帐号：weidagang 】 Lisp之魅 长久以来，Lisp一直被许多人视为史上最非凡的编程语言。它不仅在50多年前诞生的时...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/7526.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/7526.html">Lisp的永恒之道</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script>【<span style="color: #cc0000;">感谢 Todd投递本文 – 微博帐号：</span><a title="weidagang" href="http://weibo.com/weidagang" target="_blank">weidagang</a> 】</p>
<h4>Lisp之魅</h4>
<p>长久以来，Lisp一直被许多人视为史上最非凡的编程语言。它不仅在50多年前诞生的时候带来了诸多革命性的创新并极大地影响了后来编程语言的发展，即使在一大批现代语言不断涌现的今天，Lisp的诸多特性仍然未被超越。当各式各样的编程语言摆在面前，我们可以从运行效率、学习曲线、社区活跃度、厂商支持等多种不同的角度进行评判和选择，但我特别看中的一点在于语言能否有效地表达编程者的设计思想。学习C意味着学习如何用过程来表达设计思想，学习Java意味着学习如何用对象来表达设计思想，而虽然Lisp与函数式编程有很大的关系，但学习Lisp绝不仅仅是学习如何用函数表达设计思想。实际上，<strong>函数式编程并非Lisp的本质</strong>，在已经掌握了lambda、高阶函数、闭包、惰性求值等函数式编程概念之后，学习Lisp仍然大大加深了我对编程的理解。<strong>学习Lisp所收获的是如何“自由地”表达你的思想</strong>，这正是Lisp最大的魅力所在，也是这门古老的语言仍然具有很强的生命力的根本原因。</p>
<h4>Lisp之源</h4>
<p>Lisp意为表处理(List Processing)，源自设计者John McCarthy于1960年发表的一篇论文《符号表达式的递归函数及其机器计算》。McCarthy在这篇论文中向我们展示了用一种简单的数据结构S表达式(S-expression)来表示代码和数据，并在此基础上构建一种完整的语言。Lisp语言形式简单、内涵深刻，Paul Graham在《Lisp之根源》中将其对编程的贡献与欧几里德对几何的贡献相提并论。</p>
<h4>Lisp之形</h4>
<p>然而，与数学世界中简单易懂的欧氏几何形成鲜明对比，程序世界中的Lisp却一直是一种古老而又神秘的存在，真正理解其精妙的人还是少数。从表面上看，Lisp最明显的特征是它“古怪”的S表达式语法。S表达式是一个原子(atom)，或者若干S表达式组成的列表(list)，表达式之间用空格分开，放入一对括号中。“列表“这个术语可能会容易让人联想到数据结构中的链表之类的线形结构，实际上，Lisp的列表是一种可嵌套的树形结构。下面是一些S表达式的例子:</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
foo

()

(a b (c d) e)

(+ (* 2 3) 5)

(defun factorial (N)
    (if (= N 1)
        1
        (* N (factorial (- N 1)))
    )
)
</pre>
<p><span id="more-7526"></span></p>
<p>据说，这个古怪的S表达式是McCarthy在发明Lisp时候所采用的一种临时语法，他实际上是准备为Lisp加上一种被称为M表达式(M-expression)的语法，然后再把M表达式编译为S表达式。用一个通俗的类比，S表达式相当于是JVM的字节码，而M表达式相当于Java语言，但是后来Lisp的使用者都熟悉并喜欢上了直接用S表达式编写程序，并且他们发现S表达式有许多独特的优点，所以M表达式的引入也就被无限期延迟了。</p>
<p>许多Lisp的入门文章都比较强调Lisp的函数式特性，而我认为这是一种误导。真正的Lisp之门不在函数式编程，而在S表达式本身，Lisp最大的奥秘就藏在S表达式后面。S表达式是Lisp的语法基础，语法是语义的载体，形式是实质的寄托。<strong>“S表达式”是程序的一种形，正如“七言”是诗的一种形，“微博”是信息的一种形</strong>。正是形的不同，让微博与博客有了质的差异，同样的道理，正是S表达式让Lisp与C、Java、SQL等语言有了天壤之别。</p>
<h4>Lisp之道</h4>
<p>一门语言能否有效地表达编程者的设计思想取决于其抽象机制的语义表达能力。根据抽象机制的不同，语言的抽象机制形成了面向过程、面向对象、函数式、并发式等不同的范式。当你采用某一种语言，基本上就表示你已经“面向XXX“了，你的思维方式和解决问题的手段就会依赖于语言所提供的抽象方式。比如，采用Java语言通常意味着采用面向对象分析设计；采用Erlang通常意味着按Actor模型对并发任务进行建模。</p>
<p>有经验的程序员都知道，无论是面向XXX编程，程序设计都有一条“抽象原则“：What与How解耦。但是，<strong>普通语言的问题就在于表达What的手段非常有限</strong>，无非是过程、类、接口、函数等几种方式，而诸多领域问题是无法直接抽象为函数或接口的。比如，你完全可以在C语言中定义若干函数来做到make file所做的事情，但C代码很难像make file那样声明式地体现出target、depends等语义，它们只会作为实现细节被淹没在一个个的C函数之中。采用OOP或是FP等其它范式也会遇到同样的困难，也就是说make file语言所代表的抽象维度与面向过程、OOP以及FP的抽象维度是正交的，使得各种范式无法直接表达出make file的语义。这就是普通语言的“刚性”特征，它要求我们必须以语言的抽象维度去分析和解决问题，把问题映射到语言的基本语法和语义。</p>
<p>更进一步，如果仔细探究这种刚性的根源，我们会发现正是由于普通语言<strong>语法和语义的紧耦合</strong>造成了这种刚性。比如，C语言中printf(&#8220;hello %s&#8221;, name)符合函数调用语法，它表达了函数调用语义，除此之外别无他义；Java中interface IRunnable { &#8230; }符合接口定义语法，它表达了接口定义语义，除此之外别无他义。如果你认为“语法和语义紧耦合“是理所当然的，看不出这有什么问题，那么理解Lisp就会让你对此产生更深的认识。</p>
<p>当你看到Lisp的(f a (b c))的时候，你会想到什么？会不会马上联想到函数求值或是宏扩展？就像在C语言里看到gcd(10, 15)马上想到函数调用，或者在Java里看到class A马上想到类定义一样。如果真是这样，那它就是你理解Lisp的一道障碍，因为你已经习惯了顺着语言去思考，总是在想这一句话机器怎么解释执行？那一句话又对应语言的哪个特性？理解Lisp要反过来，让语言顺着你，Lisp的(f a (b c))可以是任何语义，完全由你来定，它可以是函数定义、类定义、数据库查询、文件依赖关系，异步任务的执行关系，业务规则 &#8230;</p>
<p>下面我准备先通过几个具体的例子逐步展示Lisp的本质。需要说明的是，由于Lisp的S表达式和XML的语法形式都是一种树形结构，在语义表达方面二者并无本质的差别。所以，为了理解方便，下面我暂且用多数人更为熟悉的XML来写代码，请记住我们可以很轻易地把XML代码和Lisp代码相互转换。</p>
<p>首先，我们可以轻易地用XML来定义一个求两个数最大公约数的函数：</p>
<pre data-enlighter-language="xml" class="EnlighterJSRAW">
    &lt;func name=&#039;gcd&#039; return_type=&#039;int&#039;&gt;
        &lt;params&gt;
            &lt;a type=&#039;int&#039;/&gt;
            &lt;b type=&#039;int&#039;/&gt;
        &lt;/params&gt;
        &lt;body&gt;
            &lt;if&gt;
               &lt;equals&gt;
                   &lt;a/&gt;
                   &lt;int&gt;0&lt;/int&gt;
               &lt;/equals&gt;
            &lt;/if&gt;
            &lt;then&gt;
                &lt;return&gt;&lt;b/&gt;&lt;/return&gt;
            &lt;/then&gt;
            &lt;else&gt;
                &lt;return&gt;
                    &lt;gcd&gt;
                        &lt;modulo&gt;&lt;b/&gt;&lt;a/&gt;&lt;/modulo&gt;
                        &lt;a/&gt;
                    &lt;/gcd&gt;
                &lt;/return&gt;
            &lt;/else&gt;
        &lt;/body&gt;
    &lt;/func&gt;
</pre>
<p>其次，我们可以用它来定义类：</p>
<pre data-enlighter-language="xml" class="EnlighterJSRAW">
    &lt;class name=&quot;Computer&quot;&gt;
        &lt;field access=&quot;private&quot; type=&quot;MainBoard&quot; name=&quot;main-board&quot; /&gt;
        &lt;field access=&quot;private&quot; type=&quot;CPU&quot; name=&quot;cpu&quot; /&gt;
        &lt;field access=&quot;private&quot; type=&quot;Memory&quot; name=&quot;memory&quot; /&gt;

        &lt;method access=&quot;public&quot; return_type=&quot;boolean&quot; name=&quot;powerOn&quot; /&gt;
            &lt;params&gt;...&lt;/params&gt;
            &lt;body&gt;...&lt;/body&gt;
        &lt;/method&gt;

        &lt;method access=&quot;public&quot; return_type=&quot;boolean&quot; name=&quot;powerOff&quot; /&gt;
            &lt;params&gt;...&lt;/params&gt;
            &lt;body&gt;...&lt;/body&gt;
        &lt;/method&gt;
    &lt;/class&gt;
</pre>
<p>还可以轻易地用它来编写关系查询：</p>
<pre data-enlighter-language="xml" class="EnlighterJSRAW">
&lt;sql&gt;
    &lt;select&gt;
        &lt;column name=&quot;employees.id&quot; /&gt;
        &lt;column name=&quot;bonus.amount&quot; /&gt;
    &lt;/select&gt;
    &lt;from&gt;
        &lt;table name=&quot;employees&quot; /&gt;
        &lt;table name=&quot;bonus&quot; /&gt;
    &lt;/from&gt;
    &lt;where&gt;
        &lt;equals&gt;
            &lt;column name=&quot;employees.id&quot; /&gt;
            &lt;column name=&quot;bonus.employee_id&quot; /&gt;
        &lt;/equals&gt;
    &lt;/where&gt;
&lt;/sql&gt;
</pre>
<p>还可以用它来实现类似make file的自动化构建(语法取自ant)：</p>
<pre data-enlighter-language="xml" class="EnlighterJSRAW">
    &lt;project name=&quot;MyProject&quot; default=&quot;dist&quot; basedir=&quot;.&quot;&gt;
        &lt;property name=&quot;src&quot; location=&quot;src&quot;/&gt;
        &lt;property name=&quot;build&quot; location=&quot;build&quot;/&gt;
        &lt;property name=&quot;dist&quot;  location=&quot;dist&quot;/&gt;

        &lt;target name=&quot;init&quot;&gt;
            &lt;mkdir dir=&quot;${build}&quot;/&gt;
        &lt;/target&gt;

        &lt;target name=&quot;compile&quot; depends=&quot;init&quot; description=&quot;compile the source &quot; &gt;
            &lt;javac srcdir=&quot;${src}&quot; destdir=&quot;${build}&quot;/&gt;
        &lt;/target&gt;

        &lt;target name=&quot;dist&quot; depends=&quot;compile&quot; description=&quot;generate the distribution&quot; &gt;
            &lt;mkdir dir=&quot;${dist}/lib&quot;/&gt;
            &lt;jar jarfile=&quot;${dist}/lib/MyProject-${DSTAMP}.jar&quot; basedir=&quot;${build}&quot;/&gt;
        &lt;/target&gt;

        &lt;target name=&quot;clean&quot; description=&quot;clean up&quot; &gt;
            &lt;delete dir=&quot;${build}&quot;/&gt;
            &lt;delete dir=&quot;${dist}&quot;/&gt;
        &lt;/target&gt;
    &lt;/project&gt;
</pre>
<p>一口气举了这么多个例子，目的在于用XML这种树形结构来说明Lisp的S表达式所能够描述的语义。不知道你是否发现了S表达式和XML这种树形语法在语义构造方面有着特别的“柔性”？我们可以轻易地用它构造出函数、变量、条件判断语义；类、属性、方法语义；可以轻易地构造出关系模型的select、where语义；可以轻易地构造出make的target、depends语义，等等数不清的语义。在普通语言里，你可以定义一个函数、一个类，但你无法为C语言增加匿名函数特性，也没法给Java语言加上RAII语义，甚至连自己创造一个foreach循环都不行，而自定义语义意味着在Lisp之上<strong>你创造了一门语言</strong>！不管是面向过程，面向对象，函数式，还是关系模型，在Lisp里统统都变成了一种DSL，而Lisp本身也就成了一种定义语言的语言，即元语言(Meta Language)。</p>
<p>Lisp的柔性与S表达式有着密切的关系。Lisp并不限制你用S表达式来表达什么语义，同样的S表达式语法可以表达各种不同领域的语义，这就是<strong>语法和语义解耦</strong>。如果说普通语言的刚性源于“语法和语义紧耦合”，那么Lisp的柔性正是源于“语法和语义解耦”！“语法和语义解耦”使得Lisp可以随意地构造各种领域的DSL，而不强制用某一种范式或是领域视角去分析和解决问题。本质上，Lisp编程是一种超越了普通编程范式的范式，这就是<strong>Lisp之道：面向语言编程(LOP, Language Oriented Programming)</strong>。Wikipedia上是这样描述LOP的：</p>
<blockquote><p>Language oriented programming (LOP) is a style of computer programming in which, rather than solving problems in general-purpose programming languages, the programmer creates one or more domain-specific languages for the problem first, and solves the problem in those languages &#8230; The concept of Language Oriented Programming takes the approach to capture requirements in the user&#8217;s terms, and then to try to create an implementation language as isomorphic as possible to the user&#8217;s descriptions, so that the mapping between requirements and implementation is as direct as possible.</p></blockquote>
<p>LOP范式的基本思想是从问题出发，先创建一门描述领域模型的DSL，再用DSL去解决问题，它具有高度的声明性和抽象性。SQL、make file、CSS等DSL都可以被认为是LOP的具体实例，下面我们再通过两个常见的例子来理解LOP的优势。</p>
<p>例1：在股票交易系统中，交易协议定义若干二进制的消息格式，交易所和客户端需要对消息进行编码和解码。</p>
<p>消息格式是一种抽象的规范，本身不对语言做任何的限制，你可以用C，C++，Java，或者Python。普通的实现方式是按照消息格式规范，在相应的语言中定义消息结构，并编写相应的编解码函数。假设为一个消息定义结构和实现编解码函数的工作量为M，不同消息类型的数量为N，这种方式的工作量大致为M*N。也就是说每增加一种消息类型，就需要为该消息定义结构，实现编解码函数，引入bug的可能性当然也和M*N成正比。如果仔细观察不难发现，各个消息结构其实是高度类似的，编解码函数也大同小异，但是普通语言却找不到一种抽象机制能表达这种共性，比如，我们无法通过面向对象的方法定义一个基类把消息结构的共性抽象出来，然后让具体的消息去继承它，达到复用的目的。这正是由于普通语言的抽象维度限制所致，在普通语言中，你只能从函数、接口等维度对事物进行抽象，而恰好消息格式共性所在的维度与这些抽象维度并不匹配。</p>
<p>其实，不同消息类型的<strong>共性在于它们都具有相同的领域语义</strong>，比如：“某字段内容是另一个字段内容的md5码”就是一种消息格式的领域语义，这种领域语义是OOP的抽象机制无法描述的。LOP的思路是先创建一门消息定义DSL，比如，类似Google的Protocol Buffer，Android的AIDL。然后，通过DSL编写消息定义文件，直接声明式地描述消息的结构特征，比如，我们可以声明式地描述“某字段内容是另一个字段内容的md5码”。我们还需要为DSL开发编译器用于生成C、Java等通用语言的消息定义和编解码函数。</p>
<p>有了消息定义DSL和编译器之后，由于DSL编写消息定义是一种高度声明式的编程方法，每增加一种消息的只需要多编写一个消息定义文件而已，工作量几乎可以忽略不计。所有的工作量都集中在编译器的开发上，工作量是一个常数C，与消息的数量没有关系；质量保证方面也只需要关注编译器这一点，不会因为增加新的消息类型而引入bug。</p>
<p>例2：在图书管理系统中，需要支持在管理界面上对书籍、学生、班级等各种实体进行管理操作。</p>
<p>如果按传统的三层架构，一般需要在后端程序中为每一种实体定义一个类，并定义相应的方法实现CRUD操作，与之相应的，还需要在前端页面中为每一个实体编写相应的管理页面。这些实体类的CRUD操作都是大同小异的，但细节又各不相同，虽然我们很想复用某些共同的设计实现，但OOP所提供的封装、继承、多态等抽象机制不足以有效捕获实体之间的共性，大量的代码还是必须放在子类中来完成。比如，Student和Book实体类的实现非常相似，但是如果要通过OOP的方式去抽象它们的共性，得出的结果多半是Entity这样的大而空的基类，很难起到复用的效果。</p>
<p>其实，不同实体之间的共性还是在于它们具有相同的领域语义，比如：实体具有属性，属性具有类型，属性具有取值范围，属性具有可读取、可编辑等访问属性，实体之间有关联关系等。LOP方法正是直接面向这种领域语义的。采用LOP方法，我们并不需要为每一个实体类单独编写CRUD方法，也不需要单独编写管理页面，只需要定义一种DSL并实现其编译器；然后，用DSL声明式地编写实体描述文件，去描述实体的属性列表，属性的类型、取值范围，属性所支持的操作，属性之间的关系和约束条件等；最后，通过这个实体描述文件自动生成后端的实体类和前端管理页面。采用LOP，不论前后端采用何种技术，Java也好，C#也好，JSP也好，ASP.NET也好，都可以自动生成它们的代码。采用LOP的工作量和质量都集中在DSL的设计和编译器的开发，与实体的数量无关，也就是说，越是庞大的系统，实体类越多越是能体现LOP的优势。</p>
<p>通过上面两个小例子我们可以感受到，LOP是一种面向领域的，高度声明式的编程方式，它的抽象维度与领域模型的维度完全一致。LOP能让程序员从复杂的实现细节中解脱出来，把关注点集中在问题的本质上，从而提高编程的效率和质量。</p>
<p>接下来的问题是如果需要为某领域设计DSL，我们是应该发明一门类似SQL这样的专用DSL呢，还是用XML或S表达式去定义DSL呢？它们各有何优缺点呢？</p>
<p>我认为采用XML或S表达式定义DSL的优点主要有：1) SQL、make file、CSS等专用DSL都只能面向各自的领域，而一个实际的领域问题通常是跨越多个领域的，有时我们需要将不同领域融合在一起，但是由于普通语言的刚性，多语言融合通常会是一件非常困难的事情，而XML和S表达式语法结构的单一性和“代码及数据”的特点使得跨领域融合毫无障碍。2) 在为DSL开发编译器或解释器的方面，二者难度不同。对XML和S表达式定义的DSL进行语法分析非常简单，相比之下，对SQL这样的专用DSL进行语法分析，虽然可以借助Lex、Yacc、ANTLR等代码生成工具，但总的来讲复杂度还是要明显高一些。</p>
<p>当然，XML和S表达式的优点也正好是其缺点，由于XML和S表达式的语法形式是固定的，不能像专用DSL那样自由地设计语法。所以，一般来讲专用DSL的语法显得更加简洁。换句话说，XML和Lisp其实是在语法和语义间做了一个交换，用语法的限制换来了语义的灵活。</p>
<h4>Lisp之器</h4>
<p>接下来我们继续探讨DSL的解释执行问题。DSL代码的解释执行一般分为3种典型的方式：1) 通过专门的解释器解释执行；2) 编译生成其他语言的代码，再通过其他语言的解释器解释执行(或编译运行)；3) 自解释。比如，第1类的代表是SQL，上一节举的两个例子都属于第2类，而第3类自解释正是Lisp的特色。</p>
<p>为了理解自解释，我们可以先从内部DSL的解释执行说起。内部DSL是指嵌入在宿主语言中的DSL，比如，Google Test单元测试框架定义了一套基于流畅接口(Fluent Interface)的C++单元测试DSL。从语义构造的角度看，内部DSL直接借用宿主语言的语法定义了自己的领域语义，是一种语法和语义解耦；从解释执行的角度看，内部DSL是随宿主语言的解释器而自动解释的，不需要像外部DSL一样开发专门的解释器，因而实现的代价很低。当然，并不是说设计内部DSL不用关心任何的解释实现，实际上，还是需要熟悉宿主语言的特性，并利用该特性使得DSL能随着宿主语言的解释器得到解释执行。</p>
<p>Lisp拥有强大的自解释特性，这得益于独一无二的<strong>Lisp之器：宏 (macro)</strong>。宏使得Lisp编写的DSL可以被Lisp解释器直接解释执行，这在原理上与内部DSL是相通的，只是内部DSL一般是利用宿主语言的链式调用等特性，通常形式简陋，功能有限，而Lisp的宏则要强大和灵活得多。</p>
<p>C语言中也有宏的概念，不过Lisp的宏与C语言的宏完全不同，C语言的宏是简单的字符串替换。比如，下面的宏定义：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
#define square(x) (x*x)
</pre>
<p>square(1+1)的期望结果是4，而实际上它会被替换成(1+1*1+1)，结果是3。这个例子说明，C语言的宏只在预编译阶段进行简单的字符串替换，对程序语法结构缺乏理解，非常脆弱。Lisp的宏不是简单的字符串替换，而是一套完整的代码生成系统，它是在语法解析的基础上把Lisp代码从一种形式转换为另一种形式，本质上起到了普通语言编译器的作用。不同的是，普通编译器是把一种语言的代码转换为另一种语言的代码，比如，Java编译器把Java代码转换成Java字节码；而Lisp宏的输入和输出都是S表达式，它本质上是把一种DSL转换为另一种DSL。下面的例子是宏的一个典型用法。</p>
<p>例3：假设Lisp解释器已经具备解释执行面向过程DSL的能力，需要实现类似ant的自动化构建工具。</p>
<p>我们可以基于宏构建一门类ant的DSL，宏的作用是把类ant DSL通过宏展开变成面向过程的DSL，最后被Lisp解释器所解释执行。这样用Lisp编写的ant DSL就不需要被编译为其他语言，也不需要像XML的ant一样依赖于专门的解释器了。</p>
<p>当然，和开发专门的解释器/编译器相比，Lisp的宏也并非没有缺点，宏难以理解，开发和调试更加困难。到底是开发专门的解释器/编译器还是直接采用宏应该视具体情况而定。</p>
<h4>总结</h4>
<p>Lisp采用单一的S表达式语法表达不同的语义，实现了语法和语义解耦。这使得Lisp具有强大的语义构造能力，擅长于构造DSL实现面向语言编程，而宏使得Lisp具有自解释能力，让不同DSL之间的转换游刃有余。进入Lisp的世界应当从理解面向语言编程入门，这是Lisp之道，而函数式编程和宏皆为Lisp之器，以道驭器方为正途。</p>
<h4>后记</h4>
<p>本文是我学习Lisp的一个总结，也是写给有兴趣学习Lisp的程序员的入门资料。必须说明，我还是一个标准的Lisp初学者，几乎没有写过像样的Lisp程序，文中的错误和不足在所难免，希望读者批评指正，感谢！</p>
<h4>参考</h4>
<p><a href="http://www.paulgraham.com/rootsoflisp.html">The Roots of Lisp</a></p>
<p><a href="http://www.defmacro.org/ramblings/lisp.html">The Nature of Lisp</a></p>
<p><a href="http://lists.warhead.org.uk/pipermail/iwe/2005-July/000130.html">Why Lisp macros are cool, a Perl perspective</a></p>
<p><a href="http://en.wikipedia.org/wiki/Language-oriented_programming">Wikipedia: Language-oriented programming</a></p>
<p><a href="http://book.douban.com/subject/6859720/">《实用Common Lisp编程》</a></p>
<p><a href="http://book.douban.com/subject/4031906/">《冒号课堂 &#8211; 编程范式与OOP思想》</a><!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/10337.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/24.jpg" alt="数据即代码：元驱动编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10337.html" class="wp_rp_title">数据即代码：元驱动编程</a></li><li ><a href="https://coolshell.cn/articles/1839.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2009/11/oscar-meyer-wienermobile-150x150.jpg" alt="编程语言汽车" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1839.html" class="wp_rp_title">编程语言汽车</a></li><li ><a href="https://coolshell.cn/articles/559.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/1.jpg" alt="菜鸟学PHP之Smarty入门" width="150" height="150" /></a><a href="https://coolshell.cn/articles/559.html" class="wp_rp_title">菜鸟学PHP之Smarty入门</a></li><li ><a href="https://coolshell.cn/articles/8460.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/11/go2-150x150.jpg" alt="Go 语言简介（上）— 语法" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8460.html" class="wp_rp_title">Go 语言简介（上）— 语法</a></li><li ><a href="https://coolshell.cn/articles/2492.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/21.jpg" alt="WTF Javascript" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2492.html" class="wp_rp_title">WTF Javascript</a></li><li ><a href="https://coolshell.cn/articles/2011.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/27.jpg" alt="推荐几个镜像站点" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2011.html" class="wp_rp_title">推荐几个镜像站点</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/7526.html">Lisp的永恒之道</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/7526.html/feed</wfw:commentRss>
			<slash:comments>93</slash:comments>
		
		
			</item>
		<item>
		<title>需求变化与IoC</title>
		<link>https://coolshell.cn/articles/6950.html</link>
					<comments>https://coolshell.cn/articles/6950.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Mon, 26 Mar 2012 03:01:07 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[系统架构]]></category>
		<category><![CDATA[Design]]></category>
		<category><![CDATA[design pattern]]></category>
		<category><![CDATA[IoC]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=6950</guid>

					<description><![CDATA[<p>【感谢 Todd投递本文 – 微博帐号：@weidagang 】 需求又变了，怎么办？ 先上一个轻松的段子： 程序员XX遭遇车祸成植物人，医生说活下来的希望只有...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/6950.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/6950.html">需求变化与IoC</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script>【<span style="color: #cc0000;">感谢 Todd投递本文 – 微博帐号</span>：@<a title="weidagang" onclick="pageTracker._trackPageview('/outgoing/weibo.com/weidagang?referer=http%3A%2F%2Fcoolshell.cn%2F');" href="http://weibo.com/weidagang" target="_blank">weidagang</a> 】</p>
<h4>需求又变了，怎么办？</h4>
<p>先上一个轻松的段子：</p>
<blockquote><p>程序员XX遭遇车祸成植物人，医生说活下来的希望只有万分之一，唤醒更为渺茫。可他的Lead和亲人没有放弃，他们根据XX工作如命的作风，每天都在他身边念：“XX，需求又改了，该干活了，你快来呀！”，奇迹终于发生了，XX醒来了，第一句话：“需求又改了？”。</p></blockquote>
<p>这个段子用幽默的方式反映了需求变化是每一个程序员、架构师或项目经理都会经常遇到的问题。面对这个问题，不同的人有不同的应对之道，最近微博上有一段关于需求变化的讨论：</p>
<blockquote><p>@假装刺猬的猪：我们在软件开发过程中，会持续碰到客户需求变更的情况。如果没有领域建模，我们单纯将问题使用直觉将问题解决，那么等到客户需求变更或者有新的需求时，就会面临一个僵硬的前设计！无法在以前的设计上持续深入的优化模型，导致需求变更无法及时深化。设计实现均滞后与变更！</p>
<p>@高煥堂: &lt;碰到客户需求变更的情况&gt;是合理的；但&lt;领域建模&gt;不是美好的手段!!!</p>
<p>@weidagang: 要不被客户牵着鼻子走，需要自己有很强的设计能力，<strong>反过来</strong>让客户跟着你的设计来满足你的要求。能做到这点的公司很少，但这是软件行业唯一有希望的出路。</p>
<p>@高煥堂: &lt;这是软件行业唯一有希望的出路&gt;。 Great!!</p></blockquote>
<p>如何应对需求变化？ @假装刺猬的猪 的答案是领域建模，并持续优化模型，适应需求的变化。@高煥堂 则认为领域建模不是美好的手段。我进一步补充，应该<strong>“反过来”</strong>让自己在需求变化中处于主导地位，而不是被动地适应。</p>
<p><span id="more-6950"></span></p>
<h4>控制反转 (IoC)</h4>
<p>什么样就算是“反过来”了呢？举个例子：</p>
<blockquote><p>用户想购买一台普通PC，他只想电脑能流畅运行魔兽世界，他根本不想知道什么叫主板，什么叫内存，什么叫CPU；但他不得不接受必须购买主板、CPU、内存的事实，因为PC架构是产业标准，而不是由用户定的。客户有选择的权利，但没有设计的权利，客户的需求必须在设计框架下得到满足。</p></blockquote>
<p>这里我们要问PC架构是保护了谁的利益？显然，直接的受益者是厂商。如果没有PC架构的保护，厂商就会直接面对客户，客户说我需要功能A，我马上分析设计实现功能A；客户说我要功能B，我马上分析设计实现功能B &#8230; 有了PC架构的保护，厂商就变得更加强势，用户的一切需求都必须在PC架构下来谈。厂商可以倾听用户的声音，不断改进产品，但设计主导权永远在自己手中。我们IT行业常常用“做产品”和“做项目”的视角来区分不同的公司，但很少有人用“做设计”的视角来看。实际上，关键的问题在于设计主导权是厂商还是在客户。如果设计主导权在客户，不管是做产品、做服务还是做项目，其命运必然是疲于奔命应付客户，最后获得微薄的利润；如果设计主导权在厂商，不管做产品、做服务还是做项目都能有更多的话语权和更高的利润。</p>
<p>当然，光有设计还不够，必须客户接受才能起到通过设计掌握主导权的作用。这一方面需要自己具有很强的设计能力，如苹果就是以设计能力著称的公司；另一方面，和其他厂商结盟壮大阵营也是一种方法，如最著名的Wintel联盟(Windows+Intel)，以及现在的日益壮大的Android阵营都属于此类。假如有厂商不遵守PC产业标准，说我的PC就没有主板，没有显卡，因为用户更不不需要这些东西；那么，它要么像苹果一样独树一帜成为一种新的标准，要么无人问津。</p>
<p>我所谈到的“反过来”本质上就是软件设计中的控制反转 (Inversion of Control, IoC)思想。IoC是每一个初级程序员向高级进阶所需要了解的<strong>最重要</strong>的设计思想。由于Spring等开发框架的流行，知道IoC概念的程序员不在少数，但不少人对于IoC的理解仅仅停留在通过依赖注入 (Dependency Injection)实现解耦这个层面。实际上，IoC的应用不仅包括解耦，它还是框架的基本原理，在非计算机领域，IoC也是无处不在，如果你能从上面的例子中体会到IoC，这才算是融会贯通了。</p>
<p>软件开发中一种最常见的模式是“以用户为出发点，以需求分析为核心”。该模式提倡从用户需求中分析推导出设计和实现，比如，TDD式的设计正是这类典型。而IoC式的软件设计与此截然相反，IoC的设计是一种“以愿景（自身利益是愿景的重要方面）为出发点，以架构为核心”的模式。如果用户的需求是一台电脑，我们如何能通过第一种模式分析需求推导出“主板-CPU-内存-外设”的PC架构呢？恐怕很难。IoC式的设计是以用户看不见摸不着的架构为核心，自己主导设计，用户需求是设计的约束条件和验证手段，而不是出发点和目标。我们想要掌握主动，不被需求变化搞得疲于奔命，就必须熟练使用第二种模式。</p>
<p>我们的人生都被环境和各种客观条件所束缚，多数人只能随波逐流，听从命运的安排。你有没有想过要拥有人生的主导权呢？既然你是程序员，你懂IoC，你能否设计自己的人生框架呢？Yes，you can!<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/9949.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/07/inverted-bookshelf_thumb-150x150.jpg" alt="IoC/DIP其实是一种管理思想" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9949.html" class="wp_rp_title">IoC/DIP其实是一种管理思想</a></li><li ><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2016/07/cache-150x150.png" alt="缓存更新的套路" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_title">缓存更新的套路</a></li><li ><a href="https://coolshell.cn/articles/8961.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/01/kiss-150x150.png" alt="从面向对象的设计模式看软件设计" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8961.html" class="wp_rp_title">从面向对象的设计模式看软件设计</a></li><li ><a href="https://coolshell.cn/articles/7236.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/05/Bannière-Unix-linux-150x150.jpg" alt="用Unix的设计思想来应对多变的需求" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7236.html" class="wp_rp_title">用Unix的设计思想来应对多变的需求</a></li><li ><a href="https://coolshell.cn/articles/21672.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/12/bachelor-mechanical-eng-icon@72x-150x150.png" alt="我做系统架构的一些原则" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21672.html" class="wp_rp_title">我做系统架构的一些原则</a></li><li ><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/12/go.k8s-150x150.png" alt="Go 编程模式：k8s Visitor 模式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21263.html" class="wp_rp_title">Go 编程模式：k8s Visitor 模式</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/6950.html">需求变化与IoC</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/6950.html/feed</wfw:commentRss>
			<slash:comments>73</slash:comments>
		
		
			</item>
		<item>
		<title>多版本并发控制(MVCC)在分布式系统中的应用</title>
		<link>https://coolshell.cn/articles/6790.html</link>
					<comments>https://coolshell.cn/articles/6790.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Tue, 13 Mar 2012 00:36:53 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[系统架构]]></category>
		<category><![CDATA[cas]]></category>
		<category><![CDATA[Design]]></category>
		<category><![CDATA[lock-free]]></category>
		<category><![CDATA[mvcc]]></category>
		<category><![CDATA[Performance]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=6790</guid>

					<description><![CDATA[<p>【感谢 Todd投递本文 – 微博帐号：weidagang 】 问题 最近项目中遇到了一个分布式系统的并发控制问题。该问题可以抽象为：某分布式系统由一个数据中心...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/6790.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/6790.html">多版本并发控制(MVCC)在分布式系统中的应用</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script>【<span style="color: #cc0000;">感谢 Todd投递本文 – 微博帐号：</span><a title="weidagang" href="http://weibo.com/weidagang" target="_blank">weidagang</a> 】</p>
<h4>问题</h4>
<p>最近项目中遇到了一个分布式系统的并发控制问题。该问题可以抽象为：某分布式系统由一个数据中心D和若干业务处理中心L1，L2 &#8230; Ln组成；D本质上是一个key-value存储，它对外提供基于HTTP协议的CRUD操作接口。L的业务逻辑可以抽象为下面3个步骤：</p>
<ol>
<li>read: 根据keySet {k1, &#8230; kn}从D获取keyValueSet {k1:v1, &#8230; kn:vn}</li>
<li>do: 根据keyValueSet进行业务处理，得到需要更新的数据集keyValueSet&#8217; {k1&#8242;:v1&#8242;, &#8230; km&#8217;:vm&#8217;} (<strong>注</strong>：读取的keySet和更新的keySet&#8217;可能不同)</li>
<li>update: 把keyValueSet&#8217;更新到D （<strong>注</strong>：D保证在一次调用更新多个key的原子性）</li>
</ol>
<p>在没有事务支持的情况下，多个L进行并发处理可能会导致数据一致性问题。比如，考虑L1和L2的如下执行顺序：</p>
<ol>
<li>L1从D读取key:123对应的值100</li>
<li>L2从D读取key:123对应的100</li>
<li>L1将key:123更新为100 + 1</li>
<li>L2将key:123更新为100 + 2</li>
</ol>
<p>如果L1和L2串行执行，key:123对应的值将为103，但上面并发执行中L1的执行效果完全被L2所覆盖，实际key:123所对应的值变成了102。</p>
<p><span id="more-6790"></span></p>
<h4>解决方案1：基于锁的事务</h4>
<p>为了让L的处理具有可串行化特性(Serializability)，一种最直接的解决方案就是考虑为D加上基于锁的简单事务。让L在进行业务处理前先锁定D，完成以后释放锁。另外，为了防止持有锁的L由于某种原因长时间未提交事务，D还需要具有超时机制，当L尝试提交一个已超时的事务时会得到一个错误响应。</p>
<p><img decoding="async" src="http://images.cnblogs.com/cnblogs_com/weidagang2046/362318/o_conditional_update_1.PNG" alt="" /><img decoding="async" loading="lazy" class="size-full wp-image-16991 aligncenter" src="https://coolshell.cn/wp-content/uploads/2012/03/0915536496-0.png" alt="0915536496-0" width="769" height="749" srcset="https://coolshell.cn/wp-content/uploads/2012/03/0915536496-0.png 769w, https://coolshell.cn/wp-content/uploads/2012/03/0915536496-0-300x292.png 300w" sizes="(max-width: 769px) 100vw, 769px" /></p>
<p>本方案的优点是实现简单，缺点是锁定了整个数据集，粒度太大；时间上包含了L的整个处理时间，跨度太长。虽然我们可以考虑把锁定粒度降低到数据项级别，按key进行锁定，但这又会带来其他的问题。由于更新的keySet&#8217;可能是事先不确定的，所以可能无法在开始事务时锁定所有的key；如果分阶段来锁定需要的key，又可能出现死锁(Deadlock)问题。另外，按key锁定在有锁争用的情况下并不能解决锁定时间太长的问题。所以，按key锁定仍然存在重要的不足之处。</p>
<h4>解决方案2：多版本并发控制</h4>
<p>为了实现可串行化，同时避免锁机制存在的各种问题，我们可以采用基于多版本并发控制（Multiversion concurrency control，MVCC）思想的无锁事务机制。人们一般把基于锁的并发控制机制称成为悲观机制，而把MVCC机制称为乐观机制。这是因为锁机制是一种预防性的，读会阻塞写，写也会阻塞读，当锁定粒度较大，时间较长时并发性能就不会太好；而MVCC是一种后验性的，读不阻塞写，写也不阻塞读，等到提交的时候才检验是否有冲突，由于没有锁，所以读写不会相互阻塞，从而大大提升了并发性能。我们可以借用源代码版本控制来理解MVCC，每个人都可以自由地阅读和修改本地的代码，相互之间不会阻塞，只在提交的时候版本控制器会检查冲突，并提示merge。目前，Oracle、PostgreSQL和MySQL都已支持基于MVCC的并发机制，但具体实现各有不同。</p>
<p>MVCC的一种简单实现是基于CAS（Compare-and-swap）思想的有条件更新（Conditional Update）。普通的update参数只包含了一个keyValueSet&#8217;，Conditional Update在此基础上加上了一组更新条件conditionSet { &#8230; data[keyx]=valuex, &#8230; }，即只有在D满足更新条件的情况下才将数据更新为keyValueSet&#8217;；否则，返回错误信息。这样，L就形成了如下图所示的Try/Conditional Update/(Try again)的处理模式：</p>
<p><img decoding="async" src="http://images.cnblogs.com/cnblogs_com/weidagang2046/362318/o_mvcc_2.png" alt="" /><img decoding="async" loading="lazy" class="aligncenter  wp-image-16989" src="https://coolshell.cn/wp-content/uploads/2012/03/0915535U3-1.png" alt="0915535U3-1" width="746" height="483" srcset="https://coolshell.cn/wp-content/uploads/2012/03/0915535U3-1.png 826w, https://coolshell.cn/wp-content/uploads/2012/03/0915535U3-1-300x194.png 300w" sizes="(max-width: 746px) 100vw, 746px" /></p>
<p>虽然对单个L来讲不能保证每次都成功更新，但从整个系统来看，总是有任务能够顺利进行。这种方案利用Conditional Update避免了大粒度和长时间的锁定，当各个业务之间资源争用不大的情况下，并发性能很好。不过，由于Conditional Update需要更多的参数，如果condition中value的长度很长，那么每次网络传送的数据量就会比较大，从而导致性能下降。特别是当需要更新的keyValueSet&#8217;很小，而condition很大时，就显得非常不经济。</p>
<p>为了避免condition太大所带来的性能问题，可以为每条数据项增加一个int型的版本号字段，由D维护该版本号，每次数据有更新就增加版本号；L在进行Conditional Update时，通过版本号取代具体的值。</p>
<p><img decoding="async" src="http://images.cnblogs.com/cnblogs_com/weidagang2046/362318/o_mvcc_3.png" alt="" /><img decoding="async" loading="lazy" class="aligncenter  wp-image-16990" src="https://coolshell.cn/wp-content/uploads/2012/03/0915533324-2.png" alt="0915533324-2" width="706" height="265" srcset="https://coolshell.cn/wp-content/uploads/2012/03/0915533324-2.png 912w, https://coolshell.cn/wp-content/uploads/2012/03/0915533324-2-300x113.png 300w" sizes="(max-width: 706px) 100vw, 706px" /></p>
<p>另一个问题是上面的解决方案假设了D是可以支持Conditional Update的；那么，如果D是一个不支持Conditional Update的第三方的key-value存储怎么办呢？这时，我们可以在L和D之间增加一个P作为代理，所有的CRUD操作都必须经过P，让P来进行条件检查，而实际的数据操作放在D。这种方式实现了条件检查和数据操作的分离，但同时降低了性能，需要在P中增加cache，提升性能。由于P是D的唯一客户端；所以，P的cache管理是非常简单的，不必像多客户端情形担心缓存的失效。不过，实际上，据我所知redis和Amazon SimpleDB都已经有了Conditional Update的支持。</p>
<h4>悲观锁和MVCC对比</h4>
<p>上面介绍了悲观锁和MVCC的基本原理，但是对于它们分别适用于什么场合，不同的场合下两种机制优劣具体表现在什么地方还不是很清楚。这里我就对一些典型的应用场景进行简单的分析。需要注意的是下面的分析不针对分布式，悲观锁和MVCC两种机制在分布式系统、单数据库系统、甚至到内存变量各个层次都存在。</p>
<p>### 场景1：对读的响应速度要求高</p>
<p>有一类系统更新特别频繁，并且对读的响应速度要求很高，如股票交易系统。在悲观锁机制下，写会阻塞读，那么当有写操作时，读操作的响应速度就会受到影响；而MVCC不存在读写锁，读操作是不受任何阻塞的，所以读的响应速度会更快更稳定。</p>
<p>### 场景2：读远多于写</p>
<p>对于许多系统来讲，读操作的比例往往远大于写操作，特别是某些海量并发读的系统。在悲观锁机制下，当有写操作占用锁，就会有大量的读操作被阻塞，影响并发性能；而MVCC可以保持比较高且稳定的读并发能力。</p>
<p>### 场景3：写操作冲突频繁</p>
<p>如果系统中写操作的比例很高，且冲突频繁，这时就需要仔细评估。假设两个有冲突的业务L1和L2，它们在单独执行是分别耗时t1，t2。在悲观锁机制下，它们的总时间大约等于串行执行的时间：</p>
<p>T = t1 + t2</p>
<p>而在MVCC下，假设L1在L2之前更新，L2需要retry一次，它们的总时间大约等于L2执行两次的时间（这里假设L2的两次执行耗时相等，更好的情况是，如果第1次能缓存下部分有效结果，第二次执行L2耗时是可能减小的）：</p>
<p>T&#8217; = 2 * t2</p>
<p>这时关键是要评估retry的代价，如果retry的代价很低，比如，对某个计数器递增，又或者第二次执行可以比第一次快很多，这时采用MVCC机制就比较适合。反之，如果retry的代价很大，比如，报表统计运算需要算几小时甚至一天那就应该采用锁机制避免retry。</p>
<p>从上面的分析，我们可以简单的得出这样的结论：对读的响应速度和并发性要求比较高的场景适合MVCC；而retry代价越大的场景越适合悲观锁机制。</p>
<h4>总结</h4>
<p>本文介绍了一种基于多版本并发控制（MVCC）思想的Conditional Update解决分布式系统并发控制问题的方法。和基于悲观锁的方法相比，该方法避免了大粒度和长时间的锁定，能更好地适应对读的响应速度和并发性要求高的场景。</p>
<h4>参考</h4>
<ul>
<li><a href="http://en.wikipedia.org/wiki/Serializability">Wikipedia &#8211; Serializability</a></li>
<li><a href="http://en.wikipedia.org/wiki/Compare-and-swap">Wikipedia &#8211; Compare-and-swap</a></li>
<li><a href="http://en.wikipedia.org/wiki/Multiversion_concurrency_control">Wikipedia &#8211; Multiversion concurrency control</a></li>
<li><a href="http://blogs.msdn.com/b/oldnewthing/archive/2011/04/12/10152296.aspx">Lock-free algorithms: The try/commit/(try again) pattern</a></li>
<li><a href="http://aws.amazon.com/simpledb/faqs/#Does_Amazon_SimpleDB_support_transactions">Amazon SimpleDB FAQs &#8211; Does Amazon SimpleDB support transactions?</a></li>
<li><a href="http://redis.io/topics/transactions">redis &#8211; Transactions</a></li>
<li><a href="http://simpledbm.googlecode.com/files/mvcc-survey-1.0.pdf">A Quick Survey of MultiVersion Concurrency Algorithms</a></li>
<li><a href="http://www.cnblogs.com/jobs/archive/2007/11/13/957446.html">非阻塞算法思想在关系数据库应用程序开发中的使用</a></li>
</ul>
<h4>友情推荐</h4>
<p>本文的图是用我自己开发的<a href="http://textdiagram.sinaapp.com">TextDiagram</a>工具画的，欢迎试用！如果您喜欢，请推荐给朋友，谢谢！<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/11454.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/17.jpg" alt="从LongAdder看更高效的无锁实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11454.html" class="wp_rp_title">从LongAdder看更高效的无锁实现</a></li><li ><a href="https://coolshell.cn/articles/10910.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/01/trade-off-150x150.jpg" alt="分布式系统的事务处理" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10910.html" class="wp_rp_title">分布式系统的事务处理</a></li><li ><a href="https://coolshell.cn/articles/6470.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/01/12306-150x150.png" alt="由12306.cn谈谈网站性能技术 " width="150" height="150" /></a><a href="https://coolshell.cn/articles/6470.html" class="wp_rp_title">由12306.cn谈谈网站性能技术 </a></li><li ><a href="https://coolshell.cn/articles/22242.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/05/etcd-150x150.png" alt="ETCD的内存问题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22242.html" class="wp_rp_title">ETCD的内存问题</a></li><li ><a href="https://coolshell.cn/articles/21672.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/12/bachelor-mechanical-eng-icon@72x-150x150.png" alt="我做系统架构的一些原则" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21672.html" class="wp_rp_title">我做系统架构的一些原则</a></li><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/6790.html">多版本并发控制(MVCC)在分布式系统中的应用</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/6790.html/feed</wfw:commentRss>
			<slash:comments>98</slash:comments>
		
		
			</item>
		<item>
		<title>API设计：用流畅接口构造内部DSL</title>
		<link>https://coolshell.cn/articles/5709.html</link>
					<comments>https://coolshell.cn/articles/5709.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Mon, 31 Oct 2011 00:28:47 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Javascript]]></category>
		<category><![CDATA[Ruby]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=5709</guid>

					<description><![CDATA[<p>感谢@weidagang （Todd）向酷壳投递本文。 程序设计语言的抽象机制包含了两个最基本的方面：一是语言关注的基本元素/语义；另一个是从基本元素/语义到复...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/5709.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/5709.html">API设计：用流畅接口构造内部DSL</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>感谢<a href="http://weibo.com/n/weidagang">@weidagang</a> （Todd）向酷壳投递本文。</strong></p>
<p>程序设计语言的抽象机制包含了两个最基本的方面：一是语言关注的基本元素/语义；另一个是从基本元素/语义到复合元素/语义的构造规则。在C、C++、Java、C#、Python等通用语言中，语言的基本元素/语义往往离问题域较远，通过API库的形式进行层层抽象是降低问题难度最常用的方法。比如，在C语言中最常见的方式是提供函数库来封装复杂逻辑，方便外部调用。</p>
<p>不过普通的API设计方法存在一种天然的陷阱，那就是不管怎样封装，大过程虽然比小过程抽象层次更高，但本质上还是过程，受到过程语义的制约。也就是说，通过基本元素/语义构造更高级抽象元素/语义的时候，语言的构造规则很大程度上限制了抽象的维度，我们很难跳出这个维度去，甚至可能根本意识不到这个限制。而SQL、HTML、CSS、make等DSL（领域特定语言）的抽象维度是为特定领域量身定做的，从这些抽象角度看问题往往最为简单，所以DSL在解决其特定领域的问题时比通用程序设计语言更加方便。通常，SQL等非通用语言被称为外部DSL（External DSL）；在通用语言中，我们其实也可以在一定程度上突破语言构造规则的抽象维度限制，定义内部DSL（Internal DSL）。</p>
<p>本文将介绍一种被称为流畅接口（Fluent Interface）的内部DSL设计方法。Wikipedia上<a title="Fluent Interface" href="http://en.wikipedia.org/wiki/Fluent_interface">Fluent Interface</a>的定义是：</p>
<blockquote><p>A fluent interface (as first coined by Eric Evans and Martin Fowler) is an implementation of an object oriented API that aims to provide for more readable code. A fluent interface is normally implemented by using method chaining to relay the instruction context of a subsequent call (but a fluent interface entails more than just method chaining).</p></blockquote>
<div>
<p>下面将分4个部分来逐步说明流畅接口在构造内部DSL中的典型应用。</p>
</div>
<h4><strong>1. 基本语义抽象</strong></h4>
<p>如果要输出0..4这5个数，我们一般会首先想到类似这样的代码：</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW">
//Java
for (int i = 0; i &lt; 5; ++i) {
    system.out.println(i);
}</pre>
<p><span id="more-5709"></span></p>
<p>而Ruby虽然也支持类似的for循环，但最简单的是下面这样的实现：</p>
<pre data-enlighter-language="ruby" class="EnlighterJSRAW">
//Ruby
5.times {|i| puts i}</pre>
<p>Ruby中一切皆对象，5是Fixnum类的实例，times是Fixnum的一个方法，它接受一个block参数。相比for循环实现，Ruby的times方式更简洁，可读性更强，但熟悉OOP的朋友可能会有疑问，times是否应该作为整型类的方法呢？在OOP中，方法调用通常代表了向对象发送消息，改变或查询对象的状态，times方法显然不是对整型对象状态的查询和修改。如果你是Ruby的设计者，你会把times方法放入Fixnum类吗？如果答案是否定的，那么Ruby的这种设计本质上代表了什么呢？实际上，这里的times虽然只是一个普通的类方法，但它的目的却与普通意义上的类方法不同，它的语义实际上类似于for循环这样的语言基本语义，可以被视为一种自定义的基本语义。times的语义从一定程度上跳出了类方法的框框，向问题域迈进了一步！</p>
<p>另一个例子来自Eric Evans的“用两个时间点构造一个时间段对象”，普通设计：</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW">
//Java
TimePoint fiveOClock, sixOClock;
TimeInterval meetingTime = new TimeInterval(fiveOClock, sixOClock);</pre>
<p>另一种Evans的设计是这样：</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW">
//Java
TimeInterval meetingTime = fiveOClock.until(sixOClock);</pre>
<p>按传统OO设计，until方法本不应出现在TimePoint类中，这里TimePoint类的until方法同样代表了一种自定义的基本语义，使得表达时间域的问题更加自然。</p>
<p>虽然上面的两个简单例子和普通设计相比看不出太大的优势，但它却为我们理解流畅接口打下了基础。重要的是应该体会到它们从一定程度上跳出了语言基本抽象机制的束缚，我们不应该再用类职责划分、迪米特法则（Law of Demeter）等OO设计原则来看待它们。</p>
<h4><strong>2. 管道抽象</strong></h4>
<p>在Shell中，我们可以通过管道将一系列的小命令组合在一起实现复杂的功能。管道中流动的是单一类型的文本流，计算过程就是从输入流到输出流的变换过程，每个命令是对文本流的一次变换作用，通过管道将作用叠加起来。在Shell中，很多时候我们只需要一句话就能完成log统计这样的中小规模问题。和其他抽象机制相比，管道的优美在于无嵌套。比如下面这段C程序，由于嵌套层次较深，不容易一下子理解清楚：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
//C
min(max(min(max(a,b),c),d),e)
</pre>
<p>而用管道来表达同样的功能则清晰得多：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
#!/bin/bash
max a b | min c | max d | min e
</pre>
<p>我们很容易理解这段程序表达的意思是：先求a, b的最大值；再把结果和c取最小值；再把结果和d求最大值；再把结果和e求最小值。</p>
<p>jQuery的链式调用设计也具有管道的风格，方法链上流动的是同一类型的jQuery对象，每一步方法调用是对对象的一次作用，整个方法链将各个方法的作用叠加起来。</p>
<pre data-enlighter-language="js" class="EnlighterJSRAW">
//Javascript
$(&#039;li&#039;).filter(&#039;:event&#039;).css(&#039;background-color&#039;, &#039;red&#039;);
</pre>
<h4>3. 层次结构抽象</h4>
<p>除了管道这种“线性”结构外，流畅接口还可用于构造层次结构抽象。比如，用Javascript动态创建创建下面的HTML片段：</p>
<pre data-enlighter-language="html" class="EnlighterJSRAW">
&lt;div id=&quot;’product_123’&quot; class=&quot;’product’&quot;&gt;
&lt;img src=&quot;’preview_123.jpg’&quot; alt=&quot;&quot; /&gt;
&lt;ul&gt;
	&lt;li&gt;Name: iPad2 32G&lt;/li&gt;
	&lt;li&gt;Price: 3600&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;

</pre>
<p>若采用Javascript的DOM API：</p>
<pre data-enlighter-language="js" class="EnlighterJSRAW">
//Javascript
var div = document.createElement(&#039;div&#039;);
div.setAttribute(‘id’, ‘product_123’);
div.setAttribute(‘class’, ‘product’);

var img = document.createElement(&#039;img&#039;);
img.setAttribute(‘src’, ‘preview_123.jpg’);
div.appendChild(img);

var ul = document.createElement(&#039;ul&#039;);
var li1 = document.createElement(&#039;li&#039;);
var txt1 = document.createTextNode(&quot;Name: iPad2 32G&quot;);
li1.appendChild(txt1);
…
div.appendChild(ul);</pre>
<p>而下面流畅接口API则要有表现力得多：</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW">
//Javascript
var obj =
$.div({id:’product_123’, class:’product’})
    .img({src:’preview_123.jpg’})
    .ul()
        .li().text(‘Name: iPad2 32G’)._li()
        .li().text(‘Price: 3600’)._li()
    ._ul()
 ._div();</pre>
<div>和Javascript的标准DOM API相比，上面的API设计不再局限于孤立地看待某一个方法，而是考虑了它们在解决问题时的组合使用，所以代码的表现形式特别贴近问题的本质。这样的代码是自解释的（self-explanatory）在可读性方面要明显胜于DOM API，这相当于定义了一种类似于HTML的内部DSL，它拥有自己的语义和语法。需要特别注意的是，上面的层次结构抽象和管道抽象有着本质的不同，管道抽象的方法链上通常是同一对象的连续传递，而层次抽象中方法链上的对象却在随着层次的变化而变化。此为，我们可以把业务规则也表达在流畅接口中，比如上面的例子中，body()不能包含在div()返回的对象中，div().body()将抛出&#8221;body方法不存在”异常。</div>
<h4><strong>4. 异步抽象</strong></h4>
<div>流畅接口不仅可以构造复杂的层次抽象，还可以用于构造异步抽象。在基于回调机制的异步模式中，多个异步调用的同步和嵌套问题是使用异步的难点所在。有时一个稍复杂的调用和同步关系会导致代码充满了复杂的同步检查和层层回调，难以理解和维护。这个问题从本质上讲和上面HTML的例子一样，是由于多数通用语言并未把异步作为基本元素/语义，许多异步实现模式是向语言的妥协。针对这个问题，我用Javascript编写了一个基于流畅接口的异步DSL，示例代码如下：</div>
<div>[javascript]<br />
//Javascript<br />
$.begin()<br />
    .async(newTask(&#8216;task1&#8217;), &#8216;task1&#8217;)<br />
    .async(newTask(&#8216;task2&#8217;), &#8216;task2&#8217;)<br />
    .async(newTask(&#8216;task3&#8217;), &#8216;task3&#8217;)<br />
.when()<br />
    .each_done(function(name, result) {<br />
        console.log(name + &#8216;: &#8216; + result);})<br />
    .all_done(function(){ console.log(&#8216;good, all completed&#8217;); })<br />
    .timeout(function(){<br />
        console.log(&#8216;timeout!!&#8217;);<br />
        $.begin()<br />
            .async(newTask(&#8216;task4&#8217;), &#8216;task4&#8217;)<br />
        .when()<br />
            .each_done(function(name, result) {<br />
                console.log(name + &#8216;: &#8216; + result); })<br />
        .end();}<br />
        , 3000)<br />
.end();[/javascript]</p>
</div>
<div>上面的代码只是一句Javascript调用，但从另一个角度看它却像一段描述异步调用的DSL程序。它通过流畅接口定义了begin when end的语法结构，begin后面跟的是启动异步调用的代码；when后面是异步结果处理，可以选择each_done, all_done, timeout中的一种或多种。而begin when end结构本身是可以嵌套的，比如上面的代码在timeout处理分支中就包含了另一个begin when end结构。通过这个DSL，我们可以比基于回调的方式更好地表达异步调用的同步和嵌套关系。</div>
<p>上面介绍了用流畅接口构造的4种典型抽象，出此之外还有很多其他的抽象和应用场合，比如：不少单元测试框架就通过流畅接口定义了单元测试的DSL。虽然上面的例子以Javascript等动态语言居多，但其实流畅接口所依赖的语法基础并不苛刻，即使在Java这样的静态语言中，同样可以轻松地使用。流畅接口不同于传统的API设计，理解和使用流畅接口关键是要突破语言抽象机制带来的定势思维，根据问题域选取适当的抽象维度，利用语言的基本语法构造领域特定的语义和语法。</p>
<p><strong>参考</strong></p>
<ul>
<li><a title="Wikipedia: Fluent Interface" href="http://en.wikipedia.org/wiki/Fluent_interface">Wikipedia: Fluent Interface</a></li>
<li><a title="Martin Fowler: Fluent Interface" href="http://www.martinfowler.com/bliki/FluentInterface.html">Martin Fowler: Fluent Interface</a></li>
<li><a title="jQuery is DSL" href="http://www.cnblogs.com/cathsfz/archive/2009/08/10/1543266.html">jQuery is DSL</a></li>
<li><a title="An Approach to Internal Domain-Specific Languages in Java" href="http://www.infoq.com/articles/internal-dsls-java">An Approach to Internal Domain-Specific Languages in Java</a></li>
</ul>
<p><!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/10337.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/24.jpg" alt="数据即代码：元驱动编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10337.html" class="wp_rp_title">数据即代码：元驱动编程</a></li><li ><a href="https://coolshell.cn/articles/5202.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/14.jpg" alt="对象的消息模型" width="150" height="150" /></a><a href="https://coolshell.cn/articles/5202.html" class="wp_rp_title">对象的消息模型</a></li><li ><a href="https://coolshell.cn/articles/3437.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2010/12/ediff-small-150x150.png" alt="一些杂项资源" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3437.html" class="wp_rp_title">一些杂项资源</a></li><li ><a href="https://coolshell.cn/articles/2053.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/20.jpg" alt="最为奇怪的程序语言的特性" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2053.html" class="wp_rp_title">最为奇怪的程序语言的特性</a></li><li ><a href="https://coolshell.cn/articles/1992.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2009/12/language-fanboys-150x150.jpg" alt="程序员眼中的编程语言" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1992.html" class="wp_rp_title">程序员眼中的编程语言</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/5709.html">API设计：用流畅接口构造内部DSL</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/5709.html/feed</wfw:commentRss>
			<slash:comments>32</slash:comments>
		
		
			</item>
		<item>
		<title>“品质在于构建过程”吗？</title>
		<link>https://coolshell.cn/articles/5625.html</link>
					<comments>https://coolshell.cn/articles/5625.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Sun, 16 Oct 2011 05:16:55 +0000</pubDate>
				<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[agile]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=5625</guid>

					<description><![CDATA[<p>感谢@weidagang （Todd）向酷壳投递的这篇精彩的文章。原文 今天在微博上看到几位敏捷爱好者探讨敏捷测试和质量保证问题，我忍不住也加入了讨论： Z先生...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/5625.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/5625.html">“品质在于构建过程”吗？</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>感谢<a href="http://weibo.com/n/weidagang">@weidagang</a> （Todd）向酷壳投递的这篇精彩的文章。<a href="http://www.cnblogs.com/weidagang2046/archive/2011/10/15/2213672.html" target="_blank">原文</a></strong></p>
<p>今天在微博上看到几位敏捷爱好者探讨敏捷测试和质量保证问题，我忍不住也加入了讨论：</p>
<blockquote><p><span style="color: #800040;"><strong>Z先生原帖：</strong>我刚才看到一个大会演讲稿，谈到敏捷测试六大指导原则：1.仅靠测试人员不可能获得高质量的软件，质量是整个研发团队的责任；2. 场景是不可穷举的，测试活动必须是风险驱动的，关注于高风险的场景；3.分层自动化测试是唯一出路;4.在正确的位置进行恰当的测试是自动化的关键；【待续】</span></p>
<p><span style="color: #800040;"><strong>S先生回复：</strong>品质在于构建过程。检验贯穿构建过程，提供及时反馈。</span></p>
<p><span style="color: #800040;"><strong>我回复：</strong>什么样的构建过程才能出Unix这样的品质呢？迭代？快速反馈？TDD?</span></p>
<p><span style="color: #800040;"><strong>S先生回复：</strong>据说stroustrup听到重构时的反应是，我们从七十年代就这样做了。推荐《UNIX编程环境》，了解大师的编程方式。</span></p>
<p><span style="color: #800040;"><strong>我回复：</strong>您偷换了概念。不能说大师用了重构，C++和UNIX的品质就是靠重构或某种构建过程得来的。厨师做菜用到了勺子，不等于菜好吃是因为勺子。</span></p>
<p><span style="color: #800040;"><strong>S先生回复：</strong>我没有概念。我们看到一个果，就问因是什么。其实是泛因果，无因果，一切是机缘凑巧。</span></p>
<p><span style="color: #800040;"><strong>我回复：</strong>“品质在于构建过程”难道不是一个明白的因果描述吗？</span></p>
<p><span style="color: #800040;"><strong>S先生回复：</strong>品质在于构建的人。我说话时没因果，你看到了因果。</span></p>
<p><span style="color: #800040;"><strong>我回复：</strong>欢迎敏捷爱好者围观！</span></p></blockquote>
<p>很高兴几个回合讨论下来S先生修正了先前“品质在于构建过程”的观点。什么重构、TDD、迭代、快速反馈等等构建过程都不是Unix品质的核心要素。我不但不认同“品质在于构建过程”、“测试是最好的设计方法”这类机械式的观点，而且也不满意把软件优劣归结于“人是根本”的简单回答。我们需要探索一个既非机械式，也非简单地归结为某种理念的答案。</p>
<p><span id="more-5625"></span></p>
<p>像Unix这样优秀的软件，真正的核心要素到底是什么呢？我的答案是：模型，即人心中的软件。在看得见、摸得着之前，Unix的品质就已经存在于设计者的心中了，他们不会在Unix诞生后惊讶：“哇，Unix的稳定性这么好，7&#215;24小时运行，从来不蓝屏”。模型一定是设计者心中最美的东西，为什么我们阅读操作系统源代码会像进入迷宫一般理不清头绪，而作者自己却觉得头头是道呢？因为作者早已“胸有成竹”，我们以为他几十万行代码敲很辛苦，实际上在他自己看来是按部就班一步步向目标靠近。</p>
<p>模型是软件的灵魂，存在于设计者的心中，而软件的构建过程正是心中的世界向现实世界逐渐投影。模型可以是完美的，而现实却非完美，或许有时候我们很幸运地到达了，或许有时候我们不得不向现实妥协，改变心中的世界。试图制造灯泡的爱迪生可能会一时找不到熔点极高的发光金属而止步不前，企图制造永动机的人则根本无法实现。在不完美的现实中，我们明明想的是a+b，却敲成了a-b；我们以为某个API可以很快返回，没想到却等了5秒钟，为了不阻塞用户不得不改成了异步。Review、测试等构建过程在一定程度上弥补了现实的不完美，并对模型给予了反馈，但它却无法决定软件的特质。如果设计者心中没有Unix，即使每个实现环节都层层检验，拥有光速般的反馈，他有怎么能构建出Unix呢？Windows NT内核和Windows 3.1内核的品质差别不在于微软采用了两种不同的构建过程，而在于它们采用了不同的内核模型。灵魂与躯体的差别就在于此！虽然对于普通的软件开发通常有不少成熟的模型供选择，并不需要总是创造自己的模型，但理解模型间的差异，并在设计时选用恰当的模型仍然比采用某种构建过程更加重要。服务器架构采用Nginx似的异步IO模型，还是采用Apache似的每个请求一个线程的模型远比开发是否采用了TDD更为重要。</p>
<p>模型的产生是柔性的，主要源于灵感；过程的执行是刚性的，主要源于逻辑。苹果砸在牛顿的脑袋上能砸出万有引力模型，砸在我们脑袋上却只是“哎呦”一声；但一个苹果3元钱，两个苹果2*3=6元钱却在牛顿和我们面前是平等的。迷信灵感和迷信逻辑是两个错误的极端，孔子讲“天下国家可均也，爵禄可辞也，白刃可蹈也，中庸不可能也”，任何一项技能的高级阶段都是关于“度”的艺术。如同光具有波粒二象性，软件开发也具有艺术创作和工业生产的二象性，它包含了柔性的设计和刚性的过程。越是不成熟的前沿领域越表现出柔性特征；越是成熟的一般领域越表现出工业生产的特征。因此，一个以新产品为主的创业型公司应当更注重设计，更需要画家、诗人般的创造型人才；而业务成熟产品稳定的大公司应当更注重过程，更需要踏踏实实的生产线工人似的人才。但在当今这个瞬息万变的信息时代，即使是世界500强的大公司也越来越不稳定，越来越需要创新才能适应，所以即使大公司也不可忽视软件开发的柔性特征。同时，我们也不能迷信模型，过程同样可以成为企业的核心竞争力，比如：富士康。虚虚实实，实实虚虚，其妙无穷。老外做Nike品牌（虚），我们做代工生产（实），高额利润被老外拿走了；我们经营航空公司（虚），老外生产波音飞机（实）高价卖给我们，高额利润又被老外拿走了。靠虚取胜还是靠实取胜？这是个问题^_^</p>
<p>或许我对于模型柔性的描述不太让人满意，人们多习惯于有章可循的感觉，即便不是死板的知识，起码要找个“在某某思想的指导下”才觉得心里有着落。或许还有人说，模型的确重要，那么我们能不能有一个过程、模式或套路来推导出模型呢？比如，现在非常流行的从用户需求出发的分析模式，即“分析需求，抽象出共性，共性是本质的，本质是稳定的”，这类模式的特点符合人们希望找到套路的心理，一看就明白，容易操作，有成就感。我不否认这类模式的确可以得出可用的软件设计，沿用成熟的模型也未尝不可。但我们应该明白，心中的世界远比现实的世界更广大更美妙。世界是多元的，用户需求、成熟模型等直接可见的东西只代表了某几个维度的视图，设计者心中应当有更多的维度！用户需要一个文本编辑器，是设计者心中的世界决定了他交出的作品是Vi，还是Emacs，亦或是Notepad。亨利·福特说：“如果你问用户需要什么，他会告诉你一匹更快的马”。汽车源于福特心中的世界，这是一个比只有马的世界更多彩的世界。乔布斯是一个不重视市场调研的人，iPod，iPhone，iPad都不是发个问卷，做个市场调查看看用户需要什么的结果。Apple是乔布斯心中的世界在现实中的投影！所以，请打破“从用户需求出发”，“从模式出发”的迷信，释放你的想象力，让自己心中的世界去包容现实的世界吧！</p>
<p>每个人心中都有一个属于自己的世界，牛顿运动定律是牛顿心中的世界，相对论是爱因斯坦心中的世界。哪一个才是本来的世界呢？有没有本来的世界呢？本来的世界是什么样子呢？… 老子给我们启示“道可道，非常道”，说得清，道得明，想得到的都不是永恒的真理，所以真理不可言说，对真理的探索永远没有止境……<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/8209.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/09/fight-150x150.jpg" alt="“单元测试要做多细？”" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8209.html" class="wp_rp_title">“单元测试要做多细？”</a></li><li ><a href="https://coolshell.cn/articles/7657.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/06/hudsonCI2-150x150.jpg" alt="持续部署，并不简单！" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7657.html" class="wp_rp_title">持续部署，并不简单！</a></li><li ><a href="https://coolshell.cn/articles/5531.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/14.jpg" alt="Test-Driven Development？别逗了" width="150" height="150" /></a><a href="https://coolshell.cn/articles/5531.html" class="wp_rp_title">Test-Driven Development？别逗了</a></li><li ><a href="https://coolshell.cn/articles/5143.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/10.jpg" alt="在新浪微博上关于敏捷的一些讨论" width="150" height="150" /></a><a href="https://coolshell.cn/articles/5143.html" class="wp_rp_title">在新浪微博上关于敏捷的一些讨论</a></li><li ><a href="https://coolshell.cn/articles/5044.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2011/07/hat-150x150.jpeg" alt="为什么Scrum不行？" width="150" height="150" /></a><a href="https://coolshell.cn/articles/5044.html" class="wp_rp_title">为什么Scrum不行？</a></li><li ><a href="https://coolshell.cn/articles/4891.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/17.jpg" alt="Bob大叔和Jim Coplien对TDD的论战" width="150" height="150" /></a><a href="https://coolshell.cn/articles/4891.html" class="wp_rp_title">Bob大叔和Jim Coplien对TDD的论战</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/5625.html">“品质在于构建过程”吗？</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/5625.html/feed</wfw:commentRss>
			<slash:comments>66</slash:comments>
		
		
			</item>
		<item>
		<title>对象的消息模型</title>
		<link>https://coolshell.cn/articles/5202.html</link>
					<comments>https://coolshell.cn/articles/5202.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Mon, 15 Aug 2011 02:37:13 +0000</pubDate>
				<category><![CDATA[编程语言]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Javascript]]></category>
		<category><![CDATA[OOP]]></category>
		<category><![CDATA[Ruby]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=5202</guid>

					<description><![CDATA[<p>[ ———— 感谢 Todd 同学 投递本文，原文链接 ———— ] C++对象模型 话题从下面这段C++程序说起，你认为它可以顺利执行吗？ //C++ cla...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/5202.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/5202.html">对象的消息模型</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong><span style="color: #cc0000;">[ ———— 感谢</span> <a href="http://www.cnblogs.com/weidagang2046/" target="_blank">Todd 同学</a> <span style="color: #cc0000;">投递本文，<a href="http://www.cnblogs.com/weidagang2046/archive/2011/08/14/2138059.html" target="_blank">原文链接</a> ———— ]</span></strong></p>
<h4><strong>C++对象模型</strong></h4>
<p>话题从下面这段C++程序说起，你认为它可以顺利执行吗？</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//C++
class A {
    public:
        void Hello(const std::string&amp; name) {
           std::cout &lt;&lt; &quot;hello &quot; &lt;&lt; name;
         }
};
int main(int argc, char** argv)
{
    A* pa = NULL; //!!
    pa-&gt;Hello(&quot;world&quot;);
    return 0;
}</pre>
<p>试试的确可以顺利运行输出hello world，奇怪吗？其实并不奇怪，根据C++对象模型，类的非虚方法并不会存在于对象内存布局中，实际上编译器是把Hello方法转化成了类似这样的全局函数：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">void A_Hello_xxx(A * const this, const std::string&amp; name) {
    std::cout &lt;&lt; “hello “ &lt;&lt; name;
}</pre>
<p>对象指针其实是作为第一个参数被隐式传递的，pa-&gt;Hello(“world”)实际上是调用的A_Hello_xxx(pa, “world”)，而恰好A_Hello_xxx内部没有使用pa，所以这段代码得以顺利运行。</p>
<h4><strong>对象的消息模型</strong></h4>
<p>如果是研究C++对象模型，上面的讨论可以到此为止，不过这里我想从另一个层面来继续探讨这个问题。OOP的先驱人物Alan Kay在总结Smalltalk的OO特征时强调：</p>
<p><span id="more-5202"></span></p>
<blockquote><p>Smalltalk is not only NOT its syntax or the class library, it is not even about classes. I&#8217;m sorry that I long ago coined the term &#8220;objects&#8221; for this topic because it gets many people to focus on the lesser idea. The big idea is &#8220;messaging&#8221;.</p></blockquote>
<p>也就是说相比类和对象的概念来讲，他认为对象交互的消息模型是OOP更为本质的特征，因为消息关注的是对象间的接口和交互，在构建大的系统的时候重要的不是对象/模块的内部状态，而是它们的交互。根据消息模型，牛.吃(草) 的语义是发送一条消息给“牛”，消息的类型是“吃”，消息的内容是“草”。如果按照严格的消息模型，那么上面那段C++代码应解释为向一个NULL对象发送Hello消息，这显然是不应该顺利执行的。类似的代码如果是在Java或C#中则会抛出空引用异常，所以Java和C#的设计更符合消息模型。</p>
<p>不过，Java和C#中也并非完全符合消息模型，来看一个经典的封装问题：</p>
<pre data-enlighter-language="csharp" class="EnlighterJSRAW">//C#

public class Account {
    private int _amount;

    public void Transfer(Account acc, int delta) {
        acc._amount += delta;
        this._amount -= delta;
    }
    …
}</pre>
<p>上面定义了一个Account类，问题在于为什么在这个类的Transfer方法中可以直接访问另一个对象acc的私有成员_amount呢？这是不是有破坏封装的嫌疑呢？这个问题经典的答案是：并不破坏封装，封装是划分了基于类的静态的代码边界，使得类的private代码修改不影响外界，而不是对于动态对象的保护。这个解释当然是合理的，不过正如上面C++代码的解释属于C++对象模型范畴，这个解释则属于基于类的静态类型OOP语言的范畴。消息模型强调了对象内部状态的保护，只能通过消息改变其状态，而对象内部是否真的具有_amout这样一个私有成员对其他任何对象（即使同类对象）都是未知的。</p>
<p>如果要严格遵守消息模型实现对象内部状态的保护应该怎么做呢？我们来看一个例子，定义一个集合类，包括：1.集合对象的构造函数；2.In方法：判断元素是否存在；3.Join方法：对两个集合做交集；4.Union方法：对两个集合做并集。下面是一种Javascript实现：</p>
<pre data-enlighter-language="js" class="EnlighterJSRAW">//Javascript

//集合类Set的构造函数
function Set() {
    var _elements = arguments;
    //In方法：判断元素e是否在集合中
    this.In = function(e) {
        for (var i = 0; i &lt; _elements.length; ++i) {
            if (_elements[i] == e) return true;
        }
        return false;
    };
}

//Join方法：对两个集合求交集
Set.prototype.Join = function(s2) {
    var s1 = this;
    var s = new Set();
    s.In = function(e) { return s1.In(e) &amp;&amp; s2.In(e); }
    return s;
};

//Union方法：对两个集合求并集
Set.prototype.Union = function(s2) {
    var s1 = this;
    var s = new Set();
    s.In = function(e) { return s1.In(e) || s2.In(e); }
    return s;
};

var s1 = new Set(1, 2, 3, 4, 5);
var s2 = new Set(2, 3, 4, 5, 6);
var s3 = new Set(3, 4, 5, 6, 7);
assert(false == s1.Join(s2).Join(s3).In(2));
assert(true == s1.Join(s2).Uion(s3).In(7));</pre>
<p>如果是在静态类型OOP语言中，要实现集合类的Join或Union，我们多半会像上面Account的例子一样直接对s2内部的_elements进行操作，而上面这段Javascript定义的Set关于对象s2的访问完全是符合消息模型的基于接口的访问。要实现消息模型Javascript的prototype机制并非必须的，真正的关键在于函数式的高级函数和闭包特性。从这个例子我们也可以体会到函数式的优点不仅在于无副作用，函数的可组合性也是函数式编程强大的原因。</p>
<h4><strong>Method Missing</strong></h4>
<p>接下来我们还要进行深度历险，让我们思考一下如果发送一条对象不能识别的消息会怎样？这种情况在C++、Java、C#等静态类型语言中会得到一个方法未定义的编译错误，如果是在Javascript中则会产生运行时异常。比如，s1.count()会产生一个运行时异常：Object #&lt;Set&gt; has no method &#8216;count&#8217;。</p>
<p>在静态类型语言这个问题很少受到重视，但在动态类型语言中却大有文章，来看下面的例子：<br />
//Ruby</p>
<pre data-enlighter-language="ruby" class="EnlighterJSRAW">
builder = Builder::XmlMarkup.new
xml = builder.books {|b|
    b.book :isbn =&gt; &quot;14134&quot; do
        b.title &quot;Revelation Space&quot;
        b.author &quot;Alastair Reynolds&quot;
    end
    b.book :isbn =&gt; &quot;53534&quot; do
        b.title &quot;Accelerando&quot;
        b.author &quot;Charles Stross&quot;
    end
}</pre>
<p>上面这段很DSL的Ruby代码创建了这样一个XML文件对象：</p>
<pre data-enlighter-language="xml" class="EnlighterJSRAW">

&lt;books&gt;
    &lt;book isbn=&quot;14134&quot;&gt;
        &lt;title&gt;Revelation Space&lt;/title&gt;
        &lt;author&gt;Alastair Reynolds&lt;/author&gt;
    &lt;/book&gt;
    &lt;book isbn=&quot;53534&quot;&gt;
        &lt;title&gt;Accelerando&lt;/title&gt;
        &lt;author&gt;Charles Stross&lt;/author&gt;
    &lt;/book&gt;
&lt;/books&gt;

</pre>
<p>builder.books, b.book, b.title都是对象方法调用，由于XML的元素名是任意的，所以不可能事先定义这些方法，类似的代码如果是在Javascript中就是no method异常。那为什么上面的Ruby代码可以正确执行呢？其实只要理解了消息模型就很容易想明白，只需要定义一个通用的消息处理方法，所有未明确定义的消息都交给它来处理就行了，这就是所谓的Method Missing模式：</p>
<pre data-enlighter-language="ruby" class="EnlighterJSRAW">
class Foo
    def method_missing(method, *args, &amp;block)
        …
    end
end
</pre>
<p>Method Missing除了对实现DSL很重要外，还可用于产生更好地调试和错误信息，把参数嵌入到方法名中等场合。目前，Ruby、Python、Groovy几种语言对Method Missing都有很好的支持，甚至在C# 4.0中也可以利用动态特性实现。</p>
<h4>总结</h4>
<p>本文主要介绍了对象的消息模型的特征，并比较了C++对象模型，Java、C#等基于类的静态类型语言中的对象模型与严格消息模型的差异，最后探讨了Method Missing相关话题。</p>
<h4>参考</h4>
<ul>
<li><a href="http://book.douban.com/subject/1484262/" target="_blank">Inside the C++ Object Model</a></li>
<li><a href="http://book.douban.com/subject/4031906/" target="_blank">冒号课堂 &#8211; 编程范式与OOP思想</a></li>
<li><a href="http://c2.com/cgi/wiki?AlanKaysDefinitionOfObjectOriented" target="_blank">Alan Kays Definition Of Object Oriented</a></li>
<li><a href="http://fitzgeraldnick.com/weblog/39/" target="_blank">OOP The Good Parts: Message Passing, Duck Typing, Object Composition, and not Inheritance</a></li>
<li><a href="http://olabini.com/blog/2010/04/patterns-of-method-missing/">Patterns of Method Missing</a></li>
<li><a href="http://haacked.com/archive/2009/08/26/method-missing-csharp-4.aspx">Fun With Method Missing and C# 4</a></li>
</ul>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/10337.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/24.jpg" alt="数据即代码：元驱动编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10337.html" class="wp_rp_title">数据即代码：元驱动编程</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/10739.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/12/lua-150x150.gif" alt="Lua简明教程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10739.html" class="wp_rp_title">Lua简明教程</a></li><li ><a href="https://coolshell.cn/articles/10169.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/5.jpg" alt="类型的本质和函数式实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10169.html" class="wp_rp_title">类型的本质和函数式实现</a></li><li ><a href="https://coolshell.cn/articles/6731.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/03/closure-150x150.png" alt="理解Javascript的闭包" width="150" height="150" /></a><a href="https://coolshell.cn/articles/6731.html" class="wp_rp_title">理解Javascript的闭包</a></li><li ><a href="https://coolshell.cn/articles/6668.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/02/joo_1-150x150.png" alt="再谈javascript面向对象编程 " width="150" height="150" /></a><a href="https://coolshell.cn/articles/6668.html" class="wp_rp_title">再谈javascript面向对象编程 </a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/5202.html">对象的消息模型</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/5202.html/feed</wfw:commentRss>
			<slash:comments>42</slash:comments>
		
		
			</item>
		<item>
		<title>语言的数据亲和力</title>
		<link>https://coolshell.cn/articles/4905.html</link>
					<comments>https://coolshell.cn/articles/4905.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Wed, 29 Jun 2011 00:10:44 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Groovy]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[JSON]]></category>
		<category><![CDATA[XML]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=4905</guid>

					<description><![CDATA[<p>[ 感谢 Todd 同学投递本文 ] 目前，程序设计语言似乎进入了一个蓬勃发展的时期，Javascript、Perl、Python、Ruby、Groovy等一批...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/4905.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/4905.html">语言的数据亲和力</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>[ 感谢 <a href="http://www.cnblogs.com/weidagang2046/" target="_blank">Todd 同学</a>投递本文 ]</strong></p>
<p><span style="font-family: 'Lucida Console';">目前，程序设计语言似乎进入了一个蓬勃发展的时期，Javascript、Perl、Python、Ruby、Groovy等一批较新的语言正越来越多地被熟悉和使用，而C++、C#、Java等主流语言也在不断地融入函数式和动态性特征。程序员的百宝箱中可供选择的宝贝是越来多了，而社区中关于语言间的比较和争论也更为热烈，我们常常见到关于“面向过程和面向对象的比较”、“动态语言和静态语言的比较”、“命令式和函数式范式的比较”等比较。我注意到这类讨论的关注点多集中于设计相关话题，如“动态语言的Duck typing多态和静态语言的继承多态的比较”，“Prototype based和Class based的比较”等。但我认为还有一个十分重要的方面值得关注，这就是数据处理。</span></p>
<p><span style="font-family: 'Lucida Console';">数据处理之所以重要是因为不论是本地信息存储还是系统间信息交换都需要建立在一定的数据格式基础上。另外，不管语言属于那种范式，设计上采用什么模式，在微观层次上程序很大一部分工作都是在做数据处理。所以，从数据处理角度比较和理解语言间的差异有重要的现实意义。虽然数据通常是平台和语言无关的，但不同的语言在处理某种格式的数据时会表现出不同的难度，甚至某些数据格式只能采用特定的语言才能实现，这就是数据亲和力的不同。</span></p>
<p><span style="font-family: 'Lucida Console';">语言的数据亲和力(Data Affinity)指的是语言的数据模型与某种数据格式之间的匹配程度。语言对某种数据格式亲和力越强，则操作某类数据越容易。</span></p>
<p>&nbsp;</p>
<h4><strong><span style="font-family: 'Lucida Console';">二进制字节块格式</span></strong></h4>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><span style="font-family: 'Lucida Console';">在偏底层的操作系统、嵌入式和通信系统中，二进制的字节块是最常见的一种数据格式。二进制数据布局紧凑和接近机器的特点使得它常常作为系统间通信或系统文件的数据格式，但一般高级语言都不方便直接和0101打交道，而是基于记录、结构体和类等结构化表示操作数据，这就存在着在底层的二进制字节块和高层的结构化数据直接的转换问题。</span></p>
<p><span style="font-family: 'Lucida Console';"><span id="more-4905"></span><br />
</span></p>
<p><span style="font-family: 'Lucida Console';">C语言作为最主要的系统语言具有很高的字节块数据亲和力。这不仅因为C语言具有指针可以直接访问内存以外，还因为C的结构体(struct)可以和字节块建立起直接的映射关系。例如，在基于Socket连接的分布式系统中服务器端和客户端通过二进制的字节数据进行通信，通信双方只要事先定义共用的结构体，发送方先创建相应的结构体变量并填充字段，然后把变量对应的内存块copy到Socket，接收方从Socket读取字节块，然后把字节块强制类型转换为相应的结构体指针即可读取个字段信息。整个过程中通信的双方都没有复杂的信息编码和解码的过程。示例代码如下：</span></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">struct t_data {
    int version;
    char type[10];
    float value;
};

//发送方
struct t_data data;
data.version = 1;
strcpy(data.type,  “degree”);
data.value = 189.0;
send(socket,  &amp;data,  sizeof(data));

//接收方
struct t_data data;
read(socket,  &amp;data,  sizeof(data));
printf(“%d, %s, %f”, data.version,  data.type, data.value);</pre>
<p>&nbsp;</p>
<p><span style="font-family: 'Lucida Console';">上面的方法在实际应用中还需要注意内存对齐问题和大小端问题。内存对齐问题可以通过编译器预处理命令来进行控制，保证内存中struct结构与传输的字节块具有相同的对齐方式；大小端问题需要通信的双方采用同样的大小端方式，否则就需要进行转换。</span></p>
<p><span style="font-family: 'Lucida Console';">C++可以完全兼容C的结构体，但C++的类(包括class和struct)中如果定义了虚函数，则会丧失结构的字节块数据亲和力，这是C++编程时需要权衡的一个因素。而除了C/C++，其他语言中则难以见到字节块数据亲和力，其原因在于C/C++允许控制结构体/对象的内存布局，并允许对指针进行非类型安全的强制类型转换，这都是在Java，C#等语言中不允许的。所以，在Java、C#中进行字节块的编码解码就只能按照协议一个字段一个字段地按偏移量和长度进行解析。C/C++的指针以及结构体和内存的直接映射带来了对字节块数据的亲和力，但同时也留下了内存访问和类型安全的隐患；而Java、C#在拥有引用安全和类型安全的同时也失去了对字节块数据的亲和力。</span></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h4><span style="font-family: 'Lucida Console';"><strong>文本格式</strong> </span></h4>
<p><span style="font-family: 'Lucida Console';">文本格式是另一种十分常见的数据格式。《Unix编程艺术》中是这样描述文本格式的：&#8221;Text streams are a valuable universal format because they&#8217;re easy for human beings to read, write, and edit without specialized tools ”。基于文本流的管道处理是一种备受赞誉的Unix风格。Shell可以通过管道把各种功能单一的命令串联起来，让文本流在管道上流动，因而Shell语言具有很好的文本数据亲和力。许多文本数据处理任务Bash都可以一行搞定，这就是Hacker们酷爱的One Liner风格。</span></p>
<p>&nbsp;</p>
<p><span style="font-family: 'Lucida Console';">下面我们来看两个用Bash进行文本处理的例子：</span></p>
<p><span style="font-family: 'Lucida Console';">1. 统计当前目录下的gz文件数目：</span></p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">ls –l *.gz | wc –l</code></p>
<p>&nbsp;</p>
<p><span style="font-family: 'Lucida Console';">2. 在Web服务器日子service.log中统计2011年6月26和27两天中每天中各页面的PV</span></p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">cat service.log | grep  ^2011-06-2[6-7] | cut –d ‘ ‘ –f 1, 3 | sort | uniq –c</code></p>
<p>&nbsp;</p>
<p>service.log:</p>
<p style="padding-left: 30px;"><span style="font-family: 'Lucida Console';">2011-06-25 13:00:55 /music/c.htm Safari<br />
…<br />
2011-06-26 08:01:23 /main.htm IE<br />
2011-06-26 08:03:01 /sports/b.htm Chrome<br />
…<br />
2011-06-27 11:41:06 /main.htm IE<br />
2011-06-27 11:52:41 /news/a.htm Firefox</span></p>
<p>&nbsp;</p>
<p><span style="font-family: 'Lucida Console';">输出:</span></p>
<p style="padding-left: 30px;"><span style="font-family: 'Lucida Console';">210 2011-06-26 /main.htm<br />
231 2011-06-26 /news/a.htm<br />
155 2011-06-26 /sports/b.htm<br />
288 2011-06-27 /main.htm<br />
292 2011-06-27 /news/a.htm<br />
161 2011-06-27 /sports/b.htm</span></p>
<p>&nbsp;</p>
<p><span style="font-family: 'Lucida Console';">上面的两个简单文本数据处理任务如果是在C或C++下实现则要麻烦得多，代码量至少是十几行或者数十行，加上编译调试，整个开发效率可能比Shell低一个数量级。除了Shell外，Perl也是以强大的文本数据处理而闻名的。我们来看一个Perl正则表达式的例子：</span></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">while (&lt;STDIN&gt;) {
    if (/hello\s(\w+)/i)  {
        print “say hello to $1“
     }
     elsif (/goodbye\s(\w+)/i)  {
         print “say goodbye to  $1”
    }
}</pre>
<p><span style="font-family: 'Lucida Console';">输入：</span></p>
<p style="padding-left: 30px;">HeLLo world</p>
<p style="padding-left: 30px;">Goodbye bug</p>
<p><span style="font-family: 'Lucida Console';">输出：</span></p>
<p style="padding-left: 30px;">say hello to world</p>
<p style="padding-left: 30px;">say goodbye to bug</p>
<p><span style="font-family: 'Lucida Console';">上面的例子中我们看到Perl直接进行字符串匹配并进行数据提取的强大威力。Perl基于正则表达式的字符串处理不仅比C/C++等系统语言更强大，甚至比Python这样的动态语言也更强大和更方便，这是因为正则表达式是Perl语言的“一等公民”，这就使得Perl比其他以库的方式支持正则表达式功能的语言具有更好的文本数据亲和力。后来的Ruby也学习Perl把直接在语言上支持正则表达式。</span></p>
<p>&nbsp;</p>
<h4><strong><span style="font-family: 'Lucida Console';">结构化文本格式</span></strong></h4>
<p><span style="font-family: 'Lucida Console';">XML是最近十几年来流行起来的一种通用（半）结构化的文本数据交换格式。XML除具有一般文本格式的优点外，还具有表达复杂的层次信息的优势，所以它至诞生以来就被大量用于配置文件和各种Web Service中。现代程序设计基本都少不了了XML打交道，不过在C++、Java和C#集中静态类型语言中处理XML却并不是一件十分轻松的事情。我们先来看一个Java解析和构建下面这个XML的例子：</span></p>
<pre data-enlighter-language="xml" class="EnlighterJSRAW">&lt;langs type=&quot;current&quot;&gt;
  &lt;language&gt;Java&lt;/language&gt;
  &lt;language&gt;Groovy&lt;/language&gt;
  &lt;language&gt;JavaScript&lt;/language&gt;
&lt;/langs&gt;</pre>
<p>&nbsp;</p>
<pre data-enlighter-language="java" class="EnlighterJSRAW">//Java解析XML
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
    DocumentBuilder db = dbf.newDocumentBuilder();
    Document doc = db.parse(&quot;src/languages.xml&quot;);
    Element langs = doc.getDocumentElement();
    System.out.println(&quot;type = &quot; + langs.getAttribute(&quot;type&quot;));
    NodeList list = langs.getElementsByTagName(&quot;language&quot;);
    for(int i = 0 ; i &amp;lt; list.getLength();i++) {
        Element language = (Element) list.item(i);
        System.out.println(language.getTextContent());
    }
}catch(Exception e) {
    e.printStackTrace();
}

//Java创建XML
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
    DocumentBuilder db = dbf.newDocumentBuilder();
    Document doc = db.newDocument();
    Element langs = doc.createElement(&quot;langs&quot;);
    langs.setAttribute(&quot;type&quot;, &quot;current&quot;);
    doc.appendChild(langs);

    Element language1 = doc.createElement(&quot;language&quot;);
    Text text1 = doc.createTextNode(&quot;Java&quot;);
    language1.appendChild(text1);
    langs.appendChild(language1);

    Element language2 = doc.createElement(&quot;language&quot;);
    Text text2 = doc.createTextNode(&quot;Groovy&quot;);
    language2.appendChild(text2);
    langs.appendChild(language2);
    Element language3 = doc.createElement(&quot;language&quot;);
    Text text3 = doc.createTextNode(&quot;JavaScript&quot;);
    language3.appendChild(text3);
    langs.appendChild(language3);
} catch (Exception e) {
    e.printStackTrace();
}</pre>
<p><span style="color: #800040; font-family: 'Lucida Console';"><br />
</span> 为了解析和创建小小的一段XML代码需要编写如此冗长的Java代码，而实现同样的功能动态语言Groovy则十分简洁：</p>
<pre data-enlighter-language="groovy" class="EnlighterJSRAW">//Groovy解析XML
def langs = new XmlParser().parse(&quot;languages.xml&quot;)
println &quot;type = ${langs.attribute(&quot;type&quot;)}&quot;
langs.language.each{
    println it.text()
}
//Groovy创建XML
def xml = new groovy.xml.MarkupBuilder()
xml.langs(type:&quot;current&quot;){
   language(&quot;Java&quot;)
   language(&quot;Groovy&quot;)
   language(&quot;JavaScript&quot;)
}</pre>
<p>&nbsp;</p>
<p><span style="font-family: 'Lucida Console';">上面Groovy操作XML的代码简洁而富有表达力，代码与XML几乎是一一对应的，如同直接在XML上进行操作的DSL一样，而相应的Java代码则看不到XML的影子。这说明Groovy具有很高的XML数据的亲和力。为什么Java和Groovy在XML亲和力方面有这样的差异呢？原因在于Java要求所有的方法和属性都必须先定义再调用，严格的静态类型检查使得Java只能把XML元素作为“二等公民”来表达；而Groovy则没有静态类型检查的限制，可以自由地使用方法和属性来表达XML结构。上面用Groovy创建XML的例子中，groovy.xml.MarkupBuilder类中实际上并没有langs, language这些方法，但会在调用的时候自动创建相应的XML结构。</span></p>
<p>&nbsp;</p>
<p><span style="font-family: 'Lucida Console';">除了XML外，JSON是另一种通用的半结构化的纯文本数据交换格式，它常被视为轻量级的XML。JSON的本意是Javascript的对象表示(Javascript Object Notation)，它属于Javascript的语法子集，所以Javascript对JSON有原生的支持。下面就是一个在Javascript中创建JSON对象的例子：</span></p>
<p>[javascript]var json = { “langs” :<br />
    {<br />
        &quot;type” : &quot;current”,<br />
       &quot;language” :  [&quot;Java”, &quot;Groovy”, &quot;Javascript”]<br />
    }<br />
}[/javascript]</p>
<p><span style="font-family: 'Lucida Console';">许多Javascript程序都会通过AJAX都从服务器获取JSON字符串，然后把字符串解析为JSON对象。由于Javascript对JSON的原生支持，所以，在Javascript中解析JSON字符串可以采用通用的eval方式，如：</span></p>
<p>[javascript]var json = eval(“(&quot; +  jsonStr + “)&quot;);</p>
<p>alert(json.langs.type);[/javascript]</p>
<p><span style="font-family: 'Lucida Console';">甚至可以：</span></p>
<p>[javascript]eval(“var json = ” +  jsonStr);</p>
<p>alert(json.langs.type);[/javascript]</p>
<p>&nbsp;</p>
<p><span style="font-family: 'Lucida Console';">不过eval的通用性带来了一定的安全隐患，所以一般只建议对受信任的数据源采用eval方式解析JSON，对于不受信任的数据源可以采用专门的JSON解析库。无论如何Javascript对JSON的原生支持都使得Javascript创建和解析JSON数据十分的简单，也就是说Javascript具有很高的JSON数据亲和力。另外，Groovy 1.8也加入了对JSON的原生支持，操作JSON与Javascript一样方便。</span></p>
<h4><strong><span style="font-family: 'Lucida Console';">总结</span></strong></h4>
<p><span style="font-family: 'Lucida Console';">到这里为止本文篇幅已经很长了，只能列举二进制字节块格式、文本格式和结构化文本格式3种典型的数据格式。实际上，数据亲和力的话题还有很多值得探讨的，比如C#的Linq。本文的探讨算是抛砖引玉，目的在于引起大家注意在比较语言的时候不要忽略了数据亲和力这样一个重要方面。本文的错误或不足，敬请指正，谢谢！</span><!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-150x150.jpg" alt="Rust语言的编程范式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_title">Rust语言的编程范式</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/8088.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/7.jpg" alt="对技术的态度" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8088.html" class="wp_rp_title">对技术的态度</a></li><li ><a href="https://coolshell.cn/articles/7992.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/08/cpp_small-150x150.jpg" alt="C++的坑真的多吗？" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7992.html" class="wp_rp_title">C++的坑真的多吗？</a></li><li ><a href="https://coolshell.cn/articles/5576.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2011/10/00.QuickBasic_PDS_IDE-150x150.png" alt="那些曾伴我走过编程之路的软件" width="150" height="150" /></a><a href="https://coolshell.cn/articles/5576.html" class="wp_rp_title">那些曾伴我走过编程之路的软件</a></li><li ><a href="https://coolshell.cn/articles/4990.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2011/07/programmer-150x150.png" alt="程序员技术练级攻略" width="150" height="150" /></a><a href="https://coolshell.cn/articles/4990.html" class="wp_rp_title">程序员技术练级攻略</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/4905.html">语言的数据亲和力</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/4905.html/feed</wfw:commentRss>
			<slash:comments>31</slash:comments>
		
		
			</item>
		<item>
		<title>HTTP幂等性概念和应用</title>
		<link>https://coolshell.cn/articles/4787.html</link>
					<comments>https://coolshell.cn/articles/4787.html#comments</comments>
		
		<dc:creator><![CDATA[Todd]]></dc:creator>
		<pubDate>Tue, 07 Jun 2011 00:49:01 +0000</pubDate>
				<category><![CDATA[Web开发]]></category>
		<category><![CDATA[Windows]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[HTTP]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=4787</guid>

					<description><![CDATA[<p>[ 感谢 Todd 同学投递本文 ] 基于HTTP协议的Web API是时下最为流行的一种分布式服务提供方式。无论是在大型互联网应用还是企业级架构中，我们都见到...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/4787.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/4787.html">HTTP幂等性概念和应用</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>[ 感谢 <a href="http://www.cnblogs.com/weidagang2046/" target="_blank">Todd 同学</a>投递本文 ]</strong></p>
<p>基于HTTP协议的Web API是时下最为流行的一种分布式服务提供方式。无论是在大型互联网应用还是企业级架构中，我们都见到了越来越多的SOA或RESTful的Web API。为什么Web API如此流行呢？我认为很大程度上应归功于简单有效的HTTP协议。HTTP协议是一种分布式的面向资源的网络应用层协议，无论是服务器端提供Web服务，还是客户端消费Web服务都非常简单。再加上浏览器、Javascript、AJAX、JSON以及HTML5等技术和工具的发展，互联网应用架构设计表现出了从传统的PHP、JSP、ASP.NET等服务器端动态网页向Web API + RIA（富互联网应用）过渡的趋势。Web API专注于提供业务服务，RIA专注于用户界面和交互设计，从此两个领域的分工更加明晰。在这种趋势下，Web API设计将成为服务器端程序员的必修课。然而，正如简单的Java语言并不意味着高质量的Java程序，简单的HTTP协议也不意味着高质量的Web API。要想设计出高质量的Web API，还需要深入理解分布式系统及HTTP协议的特性。</p>
<p><strong> </strong></p>
<p><strong> </strong></p>
<p><strong>幂等性定义</strong></p>
<p>本文所要探讨的正是HTTP协议涉及到的一种重要性质：幂等性(Idempotence)。在HTTP/1.1规范中幂等性的定义是：</p>
<blockquote><p><em><span style="color: #800040;">Methods can also have the property of &#8220;idempotence&#8221; in that (aside from error or expiration issues) the side-effects of N &gt; 0 identical requests is the same as for a single request.</span></em></p></blockquote>
<p>从定义上看，HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。幂等性属于语义范畴，正如编译器只能帮助检查语法错误一样，HTTP规范也没有办法通过消息格式等语法手段来定义它，这可能是它不太受到重视的原因之一。但实际上，幂等性是分布式系统设计中十分重要的概念，而HTTP的分布式本质也决定了它在HTTP中具有重要地位。</p>
<p><span id="more-4787"></span></p>
<p><strong> </strong></p>
<p><strong>分布式事务 vs 幂等设计</strong></p>
<p>为什么需要幂等性呢？我们先从一个例子说起，假设有一个从账户取钱的远程API（可以是HTTP的，也可以不是），我们暂时用类函数的方式记为</p>
<p><code data-enlighter-language="c" class="EnlighterJSRAW">bool withdraw(account_id, amount); </code></p>
<p>withdraw的语义是从account_id对应的账户中扣除amount数额的钱；如果扣除成功则返回true，账户余额减少amount；如果扣除失败则返回false，账户余额不变。值得注意的是：和本地环境相比，我们不能轻易假设分布式环境的可靠性。一种典型的情况是withdraw请求已经被服务器端正确处理，但服务器端的返回结果由于网络等原因被掉丢了，导致客户端无法得知处理结果。如果是在网页上，一些不恰当的设计可能会使用户认为上一次操作失败了，然后刷新页面，这就导致了withdraw被调用两次，账户也被多扣了一次钱。如图1所示：</p>
<p style="text-align: center;"><a href="http://images.cnblogs.com/cnblogs_com/weidagang2046/201106/201106042051027575.png"><img decoding="async" loading="lazy" src="http://images.cnblogs.com/cnblogs_com/weidagang2046/201106/201106042051039636.png" border="0" alt="image" width="458" height="394" /></a></p>
<p style="text-align: center;"><a href="http://images.cnblogs.com/cnblogs_com/weidagang2046/201106/201106042051027575.png"></a>图1</p>
<p>这个问题的解决方案一是采用分布式事务，通过引入支持分布式事务的中间件来保证withdraw功能的事务性。分布式事务的优点是对于调用者很简单，复杂性都交给了中间件来管理。缺点则是一方面架构太重量级，容易被绑在特定的中间件上，不利于异构系统的集成；另一方面分布式事务虽然能保证事务的ACID性质，而但却无法提供性能和可用性的保证。</p>
<p>另一种更轻量级的解决方案是幂等设计。上面的withdraw显然不满足幂等性，但我们可以一些技巧将它变成幂等的，比如：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int create_ticket();

bool idempotent_withdraw(ticket_id, account_id, amount);</pre>
<p>create_ticket的语义是获取一个服务器端生成的唯一的处理号ticket_id，它将用于标识后续的操作。idempotent_withdraw和withdraw的区别在于关联了一个ticket_id，一个ticket_id表示的操作至多只会被处理一次，每次调用都将返回第一次调用时的处理结果。这样，idempotent_withdraw就符合幂等性了，客户端就可以放心地多次调用。</p>
<p>基于幂等性的解决方案中一个完整的取钱流程被分解成了两个步骤：1.调用create_ticket()获取ticket_id；2.调用idempotent_withdraw(ticket_id, account_id, amount)。虽然create_ticket不是幂等的，但在这种设计下，它对系统状态的影响可以忽略，加上idempotent_withdraw是幂等的，所以任何一步由于网络等原因失败或超时，客户端都可以重试，直到获得结果。如图2所示：</p>
<p><a href="http://images.cnblogs.com/cnblogs_com/weidagang2046/201106/201106042051059820.png"><img decoding="async" loading="lazy" class="aligncenter" src="http://images.cnblogs.com/cnblogs_com/weidagang2046/201106/201106042051069339.png" border="0" alt="image" width="610" height="578" /></a></p>
<p style="text-align: center;">图2</p>
<p><strong> </strong></p>
<p>和分布式事务相比，幂等设计的优势在于它的轻量级，容易适应异构环境，以及性能和可用性方面。在某些性能要求比较高的应用，幂等设计往往是唯一的选择。</p>
<p><strong> </strong></p>
<p><strong>HTTP的幂等性</strong></p>
<p>HTTP协议本身是一种面向资源的应用层协议，但对HTTP协议的使用实际上存在着两种不同的方式：一种是RESTful的，它把HTTP当成应用层协议，比较忠实地遵守了HTTP协议的各种规定；另一种是SOA的，它并没有完全把HTTP当成应用层协议，而是把HTTP协议作为了传输层协议，然后在HTTP之上建立了自己的应用层协议。本文所讨论的HTTP幂等性主要针对RESTful风格的，不过正如上一节所看到的那样，幂等性并不属于特点的协议，它是分布式系统的一种特性；所以，不论是SOA还是RESTful的Web API设计都应该考虑幂等性。下面将介绍HTTP GET、DELETE、PUT、POST四种主要方法的语义和幂等性。</p>
<p>HTTP GET方法用于获取资源，不应有副作用，所以是幂等的。比如：GET http://www.bank.com/account/123456，不会改变资源的状态，不论调用一次还是N次都没有副作用。请注意，这里强调的是一次和N次具有相同的副作用，而不是每次GET的结果相同。GET http://www.news.com/latest-news这个HTTP请求可能会每次得到不同的结果，但它本身并没有产生任何副作用，因而是满足幂等性的。</p>
<p>HTTP DELETE方法用于删除资源，有副作用，但它应该满足幂等性。比如：DELETE http://www.forum.com/article/4231，调用一次和N次对系统产生的副作用是相同的，即删掉id为4231的帖子；因此，调用者可以多次调用或刷新页面而不必担心引入错误。</p>
<p>比较容易混淆的是HTTP POST和PUT。POST和PUT的区别容易被简单地误认为“POST表示创建资源，PUT表示更新资源”；而实际上，二者均可用于创建资源，更为本质的差别是在幂等性方面。在HTTP规范中对POST和PUT是这样定义的：</p>
<blockquote><p><span style="color: #800040;"><em>The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line. &#8230;&#8230; </em><em>If a resource has been created on the origin server, the response SHOULD be 201 (Created) and contain an entity which describes the status of the request and refers to the new resource, and a Location header.</em></span></p>
<p><em><span style="color: #800040;">The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server. If the Request-URI does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI.</span></em></p></blockquote>
<p>POST所对应的URI并非创建的资源本身，而是资源的接收者。比如：POST http://www.forum.com/articles的语义是在http://www.forum.com/articles下创建一篇帖子，HTTP响应中应包含帖子的创建状态以及帖子的URI。两次相同的POST请求会在服务器端创建两份资源，它们具有不同的URI；所以，POST方法不具备幂等性。</p>
<p>而PUT所对应的URI是要创建或更新的资源本身。比如：PUT http://www.forum/articles/4231的语义是创建或更新ID为4231的帖子。对同一URI进行多次PUT的副作用和一次PUT是相同的；因此，PUT方法具有幂等性。</p>
<p>在介绍了几种操作的语义和幂等性之后，我们来看看如何通过Web API的形式实现前面所提到的取款功能。很简单，用POST /tickets来实现create_ticket；用PUT /accounts/account_id/ticket_id&amp;amount=xxx来实现idempotent_withdraw。值得注意的是严格来讲amount参数不应该作为URI的一部分，真正的URI应该是/accounts/account_id/ticket_id，而amount应该放在请求的body中。这种模式可以应用于很多场合，比如：论坛网站中防止意外的重复发帖。</p>
<p><strong> </strong></p>
<p><strong>总结</strong></p>
<p>上面简单介绍了幂等性的概念，用幂等设计取代分布式事务的方法，以及HTTP主要方法的语义和幂等性特征。其实，如果要追根溯源，幂等性是数学中的一个概念，表达的是N次变换与1次变换的结果相同，有兴趣的读者可以从<a href="http://en.wikipedia.org/wiki/Idempotence">Wikipedia</a>上进一步了解。</p>
<p><strong> </strong></p>
<p><strong>参考</strong></p>
<p><a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html">RFC 2616, Hypertext Transfer Protocol &#8212; HTTP/1.1, Method Definitions</a></p>
<p><a href="http://devhawk.net/2007/11/09/the-importance-of-idempotence/">The Importance of Idempotence</a><!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/22173.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/02/http_method-150x150.png" alt="“一把梭：REST API 全用 POST”" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22173.html" class="wp_rp_title">“一把梭：REST API 全用 POST”</a></li><li ><a href="https://coolshell.cn/articles/19840.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/10/HTTP-770x513-300x200-1-150x150.jpg" alt="HTTP的前世今生" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19840.html" class="wp_rp_title">HTTP的前世今生</a></li><li ><a href="https://coolshell.cn/articles/18094.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/08/enable-https-banner-150x150.png" alt="如何免费的让网站启用HTTPS" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18094.html" class="wp_rp_title">如何免费的让网站启用HTTPS</a></li><li ><a href="https://coolshell.cn/articles/8767.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/12/webtoolbox-150x150.jpg" alt="Web工程师的工具箱" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8767.html" class="wp_rp_title">Web工程师的工具箱</a></li><li ><a href="https://coolshell.cn/articles/2367.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2010/04/URL-BAR-150x150.png" alt="谷歌Chrome取消&#8221;http://&#8221;" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2367.html" class="wp_rp_title">谷歌Chrome取消&#8221;http://&#8221;</a></li><li ><a href="https://coolshell.cn/articles/1480.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/27.jpg" alt="非常简单的Python HTTP服务" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1480.html" class="wp_rp_title">非常简单的Python HTTP服务</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/4787.html">HTTP幂等性概念和应用</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/4787.html/feed</wfw:commentRss>
			<slash:comments>37</slash:comments>
		
		
			</item>
	</channel>
</rss>
