Dahua 的个人资料笑对人生,傲立寰宇照片日志列表 工具 帮助
6月10日

关于开发效率:Static Typing vs. Dynamic Typing

动态语言的支持者们常说到的关于动态语言的一个重要优点是开发效率高——可以用更少的句子做更多的事情。通常事实也是如此,上一篇文章也通过一个小例子看到了这一点。可是真的是这样么?

我尝到了动态语言的甜头后,开始用它来撰写更大规模的程序,可能涉及到数十上百个类。我发现一些让我很无奈的事情:

1. IDE不会自动提示了。当规模小的时候还好,但是规模见长的时候,我总是记不住某个函数的名字,不得不来回到源定义去翻查——文件多的时候就更加痛苦了。在传统的面向对象语言C++/C#/Java的IDE里,比如VS或者Eclipse,它的intellisence的提示和auto-completion帮了我这种记性不好的人不少忙,而对于像Ruby/Python这种,IDE除了给句子上点颜色,或者提供一个不太到位的函数列表proposal,实在对我爱莫能助了。这也不能怪IDE的vendor,语言本身决定了它很难在Design-time知道一个变量是个什么东西。

2. 设计时的错误检测能力很有限。我是个粗心大意的人,又喜欢一边写程序一边和女孩子聊天——所以常常会敲错变量和函数的名字。在传统的强类型IDE里面,它会马上划个红杠警醒我。不过编写Dynamic Language就没这福分了。我就算把函数拼写错了,它可能还猜想这个家伙是不是打算在运行期动态给这个对象灌一个新方法呢,也就不好意思报警了。

不过也许有人说这些问题可以通过Unit Testing解决,但是怎么说也是运行期的事情——你不运行Test Unit它不会告诉你有问题——就算告诉你有问题,你还得debug一下子去查查是哪写不对了。这个cycle对于诸如小笔误之类的东西是不是太重量级了。

回头想想,static typing虽然有时有些累赘,但是在很多时候它告诉了IDE很多有用的东西,IDE反过来会记住很多你记不住的事情,帮你写很多你不想写的代码,为你及早查出很多没必要留到run-time去debug的小错误,从另一方面节省不少了时间。

要真正减少编码负担,提高开发效率,static typing system不能整个倒掉,而是吸取动态语言的可取之处,让它更简洁灵活而已。而对于动态语言,是不是也考虑让程序员能在必要时加入static typing的自由,其实,很多时候我们还是愿意写几个声明让IDE多帮点忙的。

我在想,matlab能不能也加入optional的static typing呢 ~~~~

6月1日

语言 解决问题的style

几天前写的一篇比较各种语言的blog有点抽象了,今天,我想举一个例子,看看不同的语言是如何解决同一个问题的。大家也许可以看到它们之间非常有趣的差别。

我们要做的事情很简单:数一下一篇文章中各个单词出现的次数,然后按照单词的字典顺序输出结果。(这里单词姑且定义为由字母,数字,连接号,和下划线连续排列成的字符串)

这个问题怎么做呢? 基本思路很简单,先建立一个空字典,对每个读入的单词,如果字典中已经存在它,就把对应的计数加1,否则就建立一个新项,计数设为1。文章全部扫描一遍后,把字典项(包括单词和计数)按照单词顺序输出。

不过,这片文章讨论的不是算法,而是不同语言的表达风格。我选用了5种语言进行了实现:

  • C# (16 statements)
  • Java (22 statements)
  • Ruby (4 statements)
  • MATLAB (2 to 7 statements)
  • C++ (32 statements)

C#Java代表了流行的面向对象高级语言;RubyMATLAB代表了动态解释语言;C++是相对传统的高级语言(把它放在最后,是因为读它的代码需要更多耐性:-))。

由于MSN Blog未能有效嵌入CSS,如果要看着色代码,请到这里

全文比较长,如果你有时间,请您按照顺序读下去。如果您比较忙,可以跳到下面你感兴趣的地方:

如果你想看看C#和Java的实现,和它们之间一些微妙的差别,请到C#和Java

如果你想看看Ruby和MATLAB怎么在几句话就把事情搞定了,请到Ruby和Matlab

如果你想看看,MATLAB怎么做到连文本分析都能向量化,请到MATLAB

如果你想看看C++和STL怎么配合完成这个事情,请到这里C++

C#

C#的实现很清楚明白:

using System;
using System.Collections.Generic;   // for dictionary
using System.Text.RegularExpressions;   // for regex  
using System.IO;  // for output printing

class WordCounter
{
    static void Main(string[] args)
    {
        string filename = args[0];

        // Prepare the Regex and word-count map
        Regex regex = new Regex(@"[-\w]+");
        IDictionary<string, int> map = new SortedDictionary<string , int>();

        // Open the file
        using (StreamReader reader = new StreamReader(filename))
        {
            String line;
            while ((line = reader.ReadLine()) != null)      // read in each line
            {
                foreach (Match m in regex.Matches(line))    // for each matched word
                {
                    // add to the corresponding counter
                    map[m.Value] = map.ContainsKey(m.Value) ? map[m.Value] + 1 : 1;
                }
            }
        }

        // Output
        foreach (KeyValuePair<string, int> e in map)
        {
            Console.Out.WriteLine("{0} {1}", e.Key, e.Value);
        }
    }
}

我们可以看到,C#的代码语义清晰,即使没有学过的人,应该也能明白大概的意思。C#的内建库对于正则表达式和标准数据结构比如字典,以及输入输出格式化都有很好的支持。

在这里,有两个名词简单解释一下。 正则表达式(Regular Expression) 是字符串分析的强大工具,它主要的作用就是能够很简捷地描述一个字符串格式,并且用它找出匹配的子串。在C#里面,它用Regex类表达。我们看到,这里我们采用"[-\w]+"来表达一个单词的格式,这是什么意思呢?\w代表字母数字或者下划线字符,-是连接线。那么[-\w]就代表上述四种字符之一,[-\w]+代表了这些字符连续排成的一串。

另外,字典(Dictionary) 代表一种对应,这里Dictionary代表了从字符串到整数的映射,因而map[str]就会返回和str对应的那个整数。通过这种方式,我们就建立了单词和它的计数的对应关系。主要的高级语言都提供了对字典的支持,在另外一些语言中,它也叫做Map或者Associative Container.

Java

我们再来看看另一种类似的语言Java怎么描述这个事情

import java.io.*;
import java.util.*;
import java.util.regex.*;

public class WordCounter {
    public static void main(String[] args) {	
        try{
            // define the regex pattern
            Pattern pattern = Pattern.compile("[-\\w]+");
			
            // open file
            String filename = args[0];			
            BufferedReader reader = new BufferedReader(
                                         new InputStreamReader(
                                             new FileInputStream(filename)));
			
            // initialize the word-count map (use sorted map here)
            SortedMap<String, Integer> map = new TreeMap<String, Integer>();
			
            // do counting line-by-line
            String line;
            while((line = reader.readLine()) != null){     // parse each line
                Matcher m = pattern.matcher(line); 	
                while(m.find()){  // for each word, add to the counter
                   String word = m.group();
                   int c = map.containsKey(word) ? map.get(word) + 1 : 1;
                   map.put(word, new Integer(c));
                }
            }		
			
            // print the output
            for(Map.Entry<String, Integer> entry : map.entrySet()){
                System.out.println(String.format("%1$s %2$d", 
                    entry.getKey(), entry.getValue().intValue()));
            }						
        }
        catch(Exception exc){
            System.err.println(exc.getMessage());
        }
    }	
}

我们可以看到,Java的实现过程以及所使用的方法和C#没有质的差别,基本也算清晰。但是,相比于C#,它在解决这个问题本身,显示了一些弱点。

  1. 首先,它的输入过程(BufferedReader那部分)需要多重管道包装。这个设计虽然灵活,但在简单问题上显得臃肿。
  2. 其次,其正则表达式类没有直接到容器的输出,需要用while便利,不如C#中的foreach遍历简洁。
  3. 它的容器(Map)不支持以原始的int作为值类型,必须用Integer Object来包裹,因此每次读写计数值都需要装箱和拆箱,效率较低。
总体来说,差别并不显著。

Ruby

虽然C#和Java表现还是不错,但是相对于这个简单问题,编码显得有点累赘。下面,我们将看到动态语言Ruby和MATLAB如何在两三句话把问题给解了。

先看看Ruby吧:

filename = ARGV[0];

# do counting
map = Hash.new;
IO.readlines(filename).join(" ").scan(/[-\w]+/) {|word| map[word] = (map[word] || 0) + 1};

# output
map.sort{|e1, e2| e1[0] <=> e2[0]}.each{|e| puts "#{e[0]} #{e[1]}"}; 

呵呵,是不是很简洁阿。而且,上面的句子就像在说英语,即使不懂Ruby也不难理解上面的代码。不过,我还是解释一下吧:

  1. map = Hash.new 建立一个字典。(字典在Ruby里面用Hash实现)
  2. IO.readlines(filename) 读入文本行,形成一个数组,每个元素是一行
  3. .join(" ")把所有行用空格连成一个长字符串,代表全文
  4. scan(/[-\w]+/) 用Regex /[-\w]+/找出全部单词
  5. 对每个单词word做这个操作:{|word| map[word] = (map[word] || 0) + 1}
  6. map[word] = (map[word] || 0) + 1这句话有点意思:map[word] || 0 表示如果word在字典中存在就返回map[word]当前值(计数值),否则返回0;(map[word] || 0) + 1就是把这个值加1;然后通过map[word] = 赋值update字典
  7. 字典建立之后,map.sort{|e1, e2| e1[0] <=> e2[0]}对字典项根据其中的单词排序,这里{|e1, e2| e1[0] <=> e2[0]}表明排序的规则是根据单词的顺序来排
  8. .each{|e| puts "#{e[0]} #{e[1]}"}; 对排好序的列表的每一项输出(puts)。我们可以看到,变量值可以直接写在字符串表达式中,从而简化了字符串的构造过程。

Ruby是一种我很喜欢的解释语言,特别欣赏它的简洁,灵活而自然的表达方式,虽然有些时候不如Java那么严谨。

MATLAB

接着,看看我们常用的MATLAB。它的强项是什么?Vectorization向量化!不要以为这个只是对矩阵运算适用,其实处理别的数据也可以向量化的哦

function countWords(filename)
%COUNTWORDS counting the occurrences of different words in text

% read the whole text into a string
text = fread(fopen(filename), inf, 'char=>char')';

% find all matched words
words = regexp(text, '[-\w]+', 'match');

% make a unique set and label
[uset, I, J] = unique(words);

% count
n = length(I);
c = hist(J, 1:n);

% compound and output
cs = mat2cell([uset; num2cell(c)], 2, ones(1, n));
cellfun(@(x) fprintf('%s %d\n', x{1}, x{2}), cs);

Matlab也很简洁,而且代码中没有for-loop的出现。这得益于几个方面:

  1. MATLAB内建对于文件IO和正则表达式提供了相当不俗的支持
  2. MATLAB对于几何运算的强大支持,这里一句unique函数,就把之前建字典的事情基本完成了。
  3. hist基于unique给出的label完成了计数的工作
  4. 对数组的灵活重组,num2cell和mat2cell的结合,把字符串cell array和计数的numeric array合并重组了,得到一个新的cell array,每个cell里面放了一个单词和一个计数值。
  5. cellfun和前面的for_each类似,就是对每个cell施行相同的操作。因为Matlab支持lambda表达是,所以输出操作就直接作为参数传禁区了,不用另外写开。

其实,上面的代码稍为合并以下,就变成了两句话:

[U, I, J] = unique( ...
    regexp(fread(fopen(filename), inf, 'char=>char')', '[-\w]+', 'match'));

cellfun(@(x) fprintf('%s %d\n', x{1}, x{2}), ...
    mat2cell([U; num2cell(hist(J, 1:length(I)))], 2, ones(1, length(I))));

不过似乎合并后,代码可读性降低了一些,有点晦涩,呵呵。总体来说,MATLAB以其独特的风格vectorize了文本计数的这件事情。

C++

最后,我提供一个使用传统的C++配合STL的实现:

#include 
#include 
#include 
#include 
#include 

using namespace std;

int main(int argc, char *argv[])
{
	const char *filename = argv[1];	
	const int buf_len = 2048;
	static char line[buf_len];

	// build word-char table
	static bool is_word_char[256];
	for (int i = 0; i < 256; ++i) is_word_char[i] = false;
	for (char ch = '0'; ch <= '9'; ++ch) is_word_char[ch] = true;
	for (char ch = 'a'; ch <= 'z'; ++ch) is_word_char[ch] = true;
	for (char ch = 'A'; ch <= 'Z'; ++ch) is_word_char[ch] = true;
	is_word_char['_'] = is_word_char['-'] = true;


	// build word-count map	
	map<string, int> wcmap;

	ifstream fin(filename);
	while(fin)
	{
		// get a line
		fin.getline(line, buf_len);
		
		// loop for each word
		int si = 0;
		int ei = 0;
		while(line[si])
		{
			// locate the start of next word
			for( ;line[si] && !is_word_char[line[si]]; ++si);
			// locate the corresponding word end 
			for( ei = si; is_word_char[line[ei]]; ++ei); 
			if (!line[si]) break;

			// extract the word to a string
			string word(line+si, ei-si);
			cout << word << endl;
			si = ei;

			// add to the counter	
			wcmap.insert(pair<string, int>(word, 0)).first->second ++;
		}			
	}

	// output
	for(map<string, int>::iterator it = wcmap.begin(); it != wcmap.end(); ++it)
	{
		cout << it->first << " " << it->second << endl;
	}

}

我们看到C++在可读性和简洁性方面明显不如。这主要原因在于它内建缺乏对字符串处理的有效支持,更没有正则表达式。不过STL的存在,使得后面在建字典和计数时还是比较方便的。