• 使用Java实现内部领域特定语言
    One Lair and Twenty Ruby DSLs
    Implementing an Internal DSL

    上面几个文章都是关于DSL的,不过,在这里,我并不是太关心DSL的话题,我更感兴趣的是代码的写法。按照这几篇的分类方法,直接用程序设计语言编写的DSL算是内部DSL,也就是说,所谓内部DSL,也就是一种标准的程序代码。

    Kent Beck在他的《Implementation Patterns》的第三章《A Theory Of Programming》中,谈到了编程的价值观(Value):Communication(沟通)、Simplicity(简单)和Flexibility(灵活)。如果说简单和灵活很容易理解的话,那么把沟通放在价值观中,尤其排在所有价值观的第一位,则显现出Kent Beck对于编程的深刻。在这个软件开发越来越需要协作的年代,写代码的时候,多站在让别人理解的角度考虑一下,会极大提升代码的可读性。在ThoughtWorks的招聘流程中,有一个Code Review的环节,拜这个环节所赐,我看过很多人的代码,不在少数的应聘者其代码唯一的优点就是完成了需求。以沟通为标准进行衡量,这显然是不够的。

    同样,以沟通为标准,那么内部DSL显然在这方面做得更好,因为DSL本身就是为了让人更容易理解而存在的。这几篇文章中提供了很多内部DSL的手法,比如Method ChainingExpression Builder等等。抛开DSL这样的BuzzWord,这些方法应该属于增强程序本身表达能力的方法。

    JDK有一个很好的Method Chaining的例子:StringBuffer的append方法。

    StringBuffer sb = new StringBuffer();
    sb.append("log1").append("log2").append("log3");

    这样的写法显然比下面的写法更为简洁,尤其是需要往StringBuffer中添加很多内容的时候。

    StringBuffer sb = new StringBuffer();
    sb.append("log1");
    sb.append("log2");
    sb.append("log3");

    在我看来,这些内部DSL技术为我们打开了一扇窗,它让我们在编写代码,尤其作为API提供的代码时,有了一个新的思考方向。当然,并不是一味的应用这些内部DSL技术就会写出好代码,作为一个有经验的软件开发人员,我们需要一定的鉴别能力,分辨出究竟怎样做才会真正的提高代码的“沟通”能力。

    之前写过两篇关于程序设计语言表达的blog(12),虽然这篇不像那两篇一样讨论语言的差异,但也算是在语言表达能力上的探讨吧!
  • Weka,是一个用Java编写的数据挖掘软件。数据挖掘,从字面上来看,它是一个从数据中找寻有用信息的过程,不过,它涉及的内容很多,所以,这里借用“分类”这一面来说事。

    分类,从名称上来看,再简单不过了,给你一样东西,给它分个类。你如何知道怎么分类呢?显然,这是基于你已有的经验。对于计算机而言,这种经验从何而来呢?只有让人来告诉它,也就是说,我们要拿一批数据训练计算机,经过训练的计算机,便具备了一定的识别能力,就可以完成一些简单的分类工作。现实中,可以用到分类的机会有很多,比如我之前,曾经参与过的一个项目就是用这种方法来做车辆的识别。

    下面便是一段使用Weka完成一段分类程序。

    import weka.classifiers.Classifier;
    import weka.classifiers.bayes.NaiveBayesMultinomial;
    import weka.core.Attribute;
    import weka.core.FastVector;
    import weka.core.Instance;
    import weka.core.Instances;
    import weka.filters.Filter;
    import weka.filters.unsupervised.attribute.StringToWordVector;

    public class Main {
      private static final String GOOD = "G";
      private static final String BAD = "B";
        
      private static final String CATEGORY = "category";
      private static final String TEXT = "text";
        
      private static final int INIT_CAPACITY = 100;
        
      private static final String[][] TRAINING_DATA = {
        {"Good", GOOD},
        {"Wonderful", GOOD},
        {"Cool", GOOD},
        {"Bad", BAD},
        {"Disaster", BAD},
        {"Terrible", BAD}
      };
        
      private static final String TEST_DATA = "Good";
        
      private static Filter filter = new StringToWordVector();
      private static Classifier classifier = new NaiveBayesMultinomial();
        
      public static void main(String[] args) throws Exception {
        FastVector categories = new FastVector();
        categories.addElement(GOOD);
        categories.addElement(BAD);

        FastVector attributes = new FastVector();
        attributes.addElement(new Attribute(TEXT, (FastVector)null));
        attributes.addElement(new Attribute(CATEGORY, categories));

        Instances instances = new Instances("Weka", attributes, INIT_CAPACITY);
        instances.setClassIndex(instances.numAttributes() - 1);
            
        for (String[] pair : TRAINING_DATA) {
          String text = pair[0];
          String category = pair[1];

          Instance instance = createInstanceByText(instances, text);
          instance.setClassValue(category);
          instances.add(instance);
        }
            
        filter.setInputFormat(instances);
        Instances filteredInstances = Filter.useFilter(instances, filter);
        classifier.buildClassifier(filteredInstances);

        // Test
        String testText = TEST_DATA;
        Instance testInstance = createTestInstance(instances.stringFreeStructure(), testText);

        double predicted = classifier.classifyInstance(testInstance);
        String category = instances.classAttribute().value((int)predicted);
        System.out.println(category);
      }
        
      private static Instance createInstanceByText(Instances data, String text) {
        Attribute textAtt = data.attribute(TEXT);
        int index = textAtt.addStringValue(text);

        Instance instance = new Instance(2);
        instance.setValue(textAtt, index);
        instance.setDataset(data);

        return instance;
      }
        
      private static Instance createTestInstance(Instances data, String text) throws Exception {
        Instance testInstance = createInstanceByText(data, text);
        filter.input(testInstance);
        return filter.output();
      }
    }

    这个程序分成两个大部分,前半部分用以训练分类器,后半部分则是测试这个分类器。

    训练分类器,我们要做的包括,选择分类算法和准备训练数据。在Weka中,每一种分类算法都是Classifier的一个子类,这样的话,就可以在不改变其它部分的情况下,很容易的修改分类算法。

    其实,稍微了解一下这方面的知识的人,都会知道,分类算法固然重要,但真正决定一个分类器本事大小的,是用以训练的数据。想要得到一个好的分类器,少不了不断调整训练数据和不断的训练。这同人类认识问题是一样的,经得多,见得广,才有更好的分辨能力。

    在Weka中,用以训练的数据就是Instances,顾名思义,这是Instance的复数,显而易见,单独的一个训练数据就是Instance,而Instances这个类的存在,可以把Instance的一些公共的属性放到一起。在这里,我们可以看到,为了用文本作为训练数据,我们会把文本转换为Instance。同样,测试分类器的时候,我们也会把文本转换为一个Instance,然后再进行分类。

    除此之外,这里还有一个Filter的概念,同常见的filter概念类似,它给了我们一个进行正式处理之前,对数据进行处理的机会。在这里,主要是对Instance做一些相关的变换。

    当我们得到一个分类器之后,就可以利用这个分类器进行分类了,其中,最关键的代码是
        classifier.classifyInstance(testInstance);
    这段代码返回的是根据分类算法计算结果得到的一个相似度,我们可以利用这个值来估计我们测试用的数据应该属于哪个分类。

    从代码上来说,这段代码本身并不复杂。正如前面所说,一个好的分类器是需要让数据帮忙的。所以,换几个测试数据,你就会发现,这段代码中实现的分类器一点都不强大。如果希望它强大起来,扩展训练数据是一个必然的结果。不过,对于这篇blog而言,这不重要,因为我们只是要和Weka问个好,进一步的工作,还需要进一步的努力。

  • 2008-01-07

    Hello, Lucene

    Lucene是什么?下面是官方回答。

    Apache Lucene is a high-performance, full-featured text search engine library written entirely in Java.

    简而言之,它是用来做搜索的库。提及搜索,我们的思绪就会情不自禁飞到串匹配上。没错,串匹配确实是一种搜索,但对于不同的应用,搜索的方法不一样,对于在一篇文档中进行搜索这种小规模应用而言,串匹配足够了,而Lucene为我们向大规模搜索铺上了一条大道。大规模?是不是想到了搜索引擎,事实上,Lucene就是被很多人用来构建搜索引擎。

    关于搜索引擎的实现,很多人或多或少的听说过一些,比如网络爬虫,比如分布式的架构,比如PageRank。抛开其它其它复杂的部分,最关键的步骤便是建立索引,然后进行搜索。不妨让我们Lucene是如何实现这最关键的部分。

    import java.io.File;
    import java.io.FileReader;

    import org.apache.lucene.analysis.standard.StandardAnalyzer;
    import org.apache.lucene.document.Document;
    import org.apache.lucene.document.Field;
    import org.apache.lucene.index.IndexWriter;

    public class Indexer {
        public static void main(String[] args) throws Exception {
            File indexDir = new File("index");
            File dataDir = new File("data");

            IndexWriter indexWriter = null;
            try {
                indexWriter = new IndexWriter(indexDir, new StandardAnalyzer(), true);
                for (File file : dataDir.listFiles()) {
                    if (file.isFile() && file.getName().endsWith(".txt")) {
                        Document document = new Document();
                        Field pathField = new Field("path", file.getCanonicalPath(),
                            Field.Store.YES, Field.Index.TOKENIZED);
                        document.add(pathField);
                        Field contentField = new Field("contents", new FileReader(file));
                        document.add(contentField);
                        indexWriter.addDocument(document);
                    }
                }
                indexWriter.optimize();
            } finally {
                if (indexWriter != null) {
                    indexWriter.close();
                }
            }
        }
    }

    这段代码很容易理解,遍历数据目录下的文本文件,为每个文件生成索引。

    这里有一个Document的概念,它在Lucene表示的是索引和搜索的单位,也就是说,建立索引,是以Document为单位的,搜索也是以Document为单位的。Document中有一堆的Field,我们可以把它们理解为Document中一个一个小节。有了Field,我们可以为Document添加一些属性,比如这里,我们就添加了路径(path)和内容(content)两个属性。这样,搜索之后,我们可以利用这些属性提供更多的信息,比如,告诉别人搜索的词出现在哪个文档中。

    上面的代码中,我们可以清楚看到,建立Document,并向其中插入Field的过程。有了Document,我们就可以把它借助IndexWriter将它们写入索引中,至于最后的optimize,显然是为了让搜索更有效率而存在的。

    有了索引,那就该进行下一步的工作,搜索。

    import org.apache.lucene.document.Document;
    import org.apache.lucene.index.Term;
    import org.apache.lucene.search.Hits;
    import org.apache.lucene.search.IndexSearcher;
    import org.apache.lucene.search.Query;
    import org.apache.lucene.search.TermQuery;

    public class Searcher {
        public static void main(String[] args) throws Exception {
            String type = "contents";
            String key = "game";
            String path = "index";
            
            IndexSearcher searcher = new IndexSearcher(path);
            Term t = new Term(type, key);
            Query query = new TermQuery(t);

            Hits hits = searcher.search(query);
            for(int i = 0; i < hits.length(); i++){
                Document document = hits.doc(i);
                System.out.println("File: " + document.get("path"));
            }
        }
    }

    IndexSearcher是用来在索引中进行搜索主要帮手,前提是我们要告诉它到索引在哪。Term表示文本中的一个词,它说明了我们要在哪个Field(type)中找什么(key)。然后,我们用Term做成一个Query,表示我们要进行搜索了。做好准备,接下来,就是搜索了。搜索的结果叫做Hits。遍历这个Hits,便可以将搜索结果一一展示出来。如前面所说,这里利用路径这个属性报告搜索的结果。

    有了Lucene做基础,能做的事就很多了,比如搭建一个搜索引擎。事实上,已经有了这样的开源项目,比如与Lucene同出一门的Nutch,比如比Nutch年纪更大的Compass
  • 写程序的时候,经常遇到这样的代码。处理之前,先检验一下一些参数,之后再进行处理。下面是一段这样的Java程序。

    public class Service {
      public void approve() {
        Target[] targets = getTargets();
        if (targets.length == 0) {
          reportNoTarget();
          return;
        }

        actualApprove(targets);
      }

      public void reject() {
        Target[] targets = getTargets();
        if (targets.length == 0) {
          reportNoTarget();
          return;
        }

        actualReject(targets);
      }

      ...
    }

    这段代码不够漂亮,这是显然的,因为其中存在重复代码,参数检验的部分是完全相同的,这两个函数的真正的差别只是在检验后的处理。但是在

    Java中,想简单的解决这种重复并不是一件容易的事情。我脑子里想到的方案包括用异常和接口,不过,这两种方法都不够优雅,其主要原因还是因

    为Java的函数并不容易操作。如果你有什么好的解决方案,不妨告诉我。

    如果我们用的是C#,这段代码可以这样来写。

    public class Service
    {
      private delegate void ActualOperationDelegate(Target[] targets);

      public void Approve()
      {
        OperateOnTarget(ActualApprove);
      }

      public virtual void RejectTrades()
      {
        OperateOnTarget(ActualReject);
      }

      private void OperateOnTarget(ActualOperationDelegate operation)
      {
        Target[] targets = GetTargets();
        if (targets.length == 0) {
          ReportNoTarget();
          return;
        }

        operation(trades);
      }

      ...
    }

    这段C#代码用delegate轻松的解决了重复代码的问题。

    如果用Ruby,我们可以让程序更为优雅。
    class Service
      def approve
        operation_on_target { |targets| actual_approve targets }
      end

      def reject
        operation_on_target { |targets| actual_reject targets }
      end

      def operation_on_target
        targets = get_target
        if (targets.length == 0)
          report_no_target
        end

        yield targets
      end

      ...
    end

    之前写过一篇关于程序设计语言表达的blog,这篇算是它的一个例子吧!

  • 我们经常会遇到连接字符串的需求,比如在服务器端记录日志的时候,描述的部分基本上一致,差别在于随运行状态,需要记录变量的内容不一致。

    在Java中,我们可能会这样做:
    StringBuffer sb = new StringBuffer("prefix:");
    sb.append(logRecord);
    sb.append(":postfix");

    String result = sb.toString();

    这里用到了StringBuffer,在Java中,这是众所周知的技巧。

    同样的功能,在Ruby中,可能会这样写:
    result = "prefix:#{logRecord}:postfix"

    看出差别来了吗?Ruby的代码更为简洁。或许这里的代码量不足以说明问题,放开你的想象力,当要连接的内容增多,差异就会非常明显,Java代码中满天遍野的sb.append肯定让人心情不是那么好。事实上,我之前做服务器端应用的时候,这种代码经常可以看到,无论是这里所说的记录日志,还是为传输协议打包。

    造成这种结果的原因是Java语言自身的机制和表达能力。在Java中,对于字符串连接而言,也有相对简单的表达方式:“+”。但字符串的不可变特性决定了与“+”同生的是大量的临时变量,尽管编译器可以在一定程度上进行优化,但治标不治本。这也是StringBuffer广为流传的原因。

    一个简洁的表达方式对于程序设计语言的用户而言,同样重要,这也是提高生产力的重要途径,因为它可以减少代码量(多半也就是工作量),更能减轻维护的负担。同汇编语言相比,高级语言引入的分支、循环等语句,让语言的表达能力大大增强,面向对象语言将数据和函数封装起来,使我们拥有更好的模块化方式,随后的namespace、package等方式在模块化的道路上越走越远。

    在主流程序设计语言中,C++的复杂是让人望而却步的。这主要是C++混杂了太多的东西,有很多复杂的语法只是为那些程序库编写者预备的。如果只是作为一个普通程序员,仅仅利用程序库提供的接口,C++还是挺不错的。遗憾的是,往往是历尽千辛万苦,学遍了各种古怪的用法之后,我们才会知道,哪些在日常开发中基本用不到。所以,Java的一个大动作就是简化过于复杂的C++,它去掉所有它认为复杂的东西,也去掉了一些可以很好提高语言表达能力的东西,比如宏,比如运算符重载。宏是一直颇受争议的东西,但如果只是使用封装好的宏,对于简化代码来说,还是颇有益处的。同样,运算符重载之所以引入,C++之父还是有所考虑的,这在《C++语言的设计与演化》中有详尽的讨论。或许是理念不同,它成为了Java语言中认为复杂的东西,于是被去掉了。除了基本类型之外,唯一的例外是字符串,它拥有一个“+”。但总体上来说,某些机制的缺乏让Java代码看起来很繁琐。

    Ruby越来越受欢迎,因为它对于提高生产力有很大的帮助。相对于Java,Ruby的表达能力让它的代码编写和维护更加容易,前面举的是一个例子。在Ruby中,还有很多简化代码编写的方式,还以字符串连接为例,一种方式是str.concat(other),更为简洁的方式是str << other。类似的例子还有许多,比如正则表达式,比如文件操作。关于使用Ruby作为DSL的讨论越来越多,Ruby的meta programming能力让它可以成为DSL的基础。Ruby On Rails已经为我们展现了Ruby在这方面的优越,DHH为我们展示如何用Ruby进行美的追求,Potian给出了一些对应的解释

    程序设计语言就是搭建在实际问题和计算机之间的桥梁,最初,受到机器性能和人们对程序设计认识的限制,程序设计语言的表达更贴近计算机,而随着软件世界的进步,表达越来越偏向问题领域,所以,程序编写起来也就越来越容易,这样,程序员们就可以把更多的精力放在解决问题上。