“我真是受够了Scala 的语法,太TM灵活了!学了一周了,十行代码还有八行代码看不懂!” by 资深Java程序员&野生数学家-Garfield

为什么总有一些无聊的人发明新的语言,为什么所有的程序员不能只用一种语言?新的语言要浪费多少程序员的精力?不同的编程语言有着不同的哲学,它们对同一个问题有着不同的解决之道。新的语言带给你新的思考问题的方式,即使你学完Scala之后回过头再使用Java,也会带给你新的思考。

如今采用Scala的项目和公司很多:

  1. Spark 如今主流的大数据处理平台
  2. Kafka 目前流行的分布式流数据处理平台
  3. Play 一个响应式的web框架
  4. akka 一个构建响应式,高并发,分布式,应用的工具集
  5. Twitter,IBM,Linkedin 等公司已经采用了Scala作为编程语言

可以看到Scala在分布式,大数据领域非常抢眼,这和它函数式编程的特性有很大关系。这一章我们就来看一下什么是函数式编程思维,在看完这一章节后我希望你能够理解为什么函数式编程在分布式和大数据项目中应用如此广泛。

假如您是位伐木工人,如果您拥有最好的斧子,那您将会是林场中最能干的伐木工人。忽然有一天,有人向您展示并推荐一个新型伐木工具,电锯。这个销售人员很善于推销,于是您购买了电锯,但不知道如何使用。于是您就参照以前的模式来用,举起电锯用力地挥向树木。您很快就会得出结论,电锯这种新工具只不过是一种时尚,最后还是改用斧子来砍树

所以花点时间学习什么是函数式编程,和它背后的逻辑是值得的。

从一个例子开始

我们还是从一个实际的例子来看吧,一组单词构成了一句话,我们要写一个Sentence 类,它是由一组单词构成的。对于一个Sentense对象,我们可以增加,修改,删除单词,同时可以统计这个句子的字数。好了,我们开始实现吧。

public class Sentence {
   List words=null;
   public void setLines(List words){
      this.words = words;
   }
   public void removeWord(int index){
      words.remove(index);
   }
   public void addWord(String word){
      words.add(word);
   }
   public void modifyWord(String word,int index){
      words.set(index, word);
   }
   public int getWordsCount(){
      return words.size();
   }
   @Override
   public String toString(){
      StringBuilder sentence=new StringBuilder();
      for(String word:words){
         sentence.append(word+" ");
      }
      return sentence.toString();
   }
}

我很高兴我能写出这么好的一个例子,因为它充满了问题。

问题一

调用这个类的方法我们需要了解它是如何构造的。

       Sentence sentence =new Sentence();
        List words = new ArrayList(Arrays.asList("I","am","happy"));
        sentence.setLines(words);
        sentence.addWord("today");
        System.out.println(sentence.toString());

如果我这样写是没有问题的,我可以得到一个期望的输出:I am happy today

       Sentence sentence =new Sentence();
        sentence.addWord("today");
        System.out.println(sentence.toString());

但是如果我这样写,就会得到一个空指针异常,因为列表words没有被初始化。同样输入的一句调用

sentence.addWord("today");

,在不同情况下,有时成功,有时失败。这很让人受挫,这意味着你使用一个类的方法时,你必须了解更多的关于这个类的状态的信息。

问题二

对这个类的方法不是引用透明

       Sentence sentence =new Sentence();
        List words = new ArrayList(Arrays.asList("I","am","happy"));
        sentence.setLines(words);
        String printSentence = sentence.toString();
        sentence.addWord("today");
        System.out.println(printSentence);

注意最后一句我们打印printSentence这个变量。如果我们重构代码时,不想用这个变量而是用当时得到这个变量的方法

sentence.toString()

来替换这个变量,把最后一句改成这样:

       Sentence sentence =new Sentence();
        List words = new ArrayList(Arrays.asList("I","am","happy"));
        sentence.setLines(words);
        String printSentence = sentence.toString();
        sentence.addWord("today");
        System.out.println(sentence.toString());

那么修改前和修改后结果是不一样的。因为在获得printSentence 之后,我们通过addWord方法改变了sentence的内部状态。

问题三

这个类并不好测试

       Sentence sentence =new Sentence();
        List words = new ArrayList(Arrays.asList("I","am","happy"));
        sentence.setLines(words);
        sentence.addWord("today");
        assertEquals("I am happy today ",sentence.toString());

因为很多方法没有返回值,我们必须调用其他并不想被测试的方法。比如我们只想测试addWord方法,但是必须调用toString来间接测试,出了问题我们无法确定是addWord的问题还是toString的问题。

问题四

这个类并不是线程安全的

如果多个线程同时对这个类的同一个对象进行修改,那么结果就不能保证正确了。因为它们共享了words这个list。

上边这些问题是我们在面向对象编程中常遇到的,当然这个锅不能让面向对象来背。引起这些问题的原因主要是状态的变化引起的。在函数式编程中是要尽量避免变化,所以Scala中极力推荐你用常量。就连默认的List,在scala里也是不可变的。如果要增加元素,你会得到一个新的List。那么我们用函数式编程的思想来改造一下我们的Sentence类

    final public class FunctionalSentence {
        final ArrayList words;
        final int size;
   
        public FunctionalSentence(ArrayList words) {
            if (words == null) {
                this.words = new ArrayList();
            } else {
                this.words = (ArrayList) words.clone();
            }
            this.size = words.size();
        }
   
        public FunctionalSentence removeWord(int index) {
            ArrayList newWords = (ArrayList) words.clone();
            newWords.remove(index);
            return new FunctionalSentence(newWords);
        }
   
        public FunctionalSentence addWord(String word) {
            ArrayList newWords = (ArrayList) words.clone();
            newWords.add(word);
            return new FunctionalSentence(newWords);
        }
   
        public FunctionalSentence modifyWord(String word, int index) {
            ArrayList newWords = (ArrayList) words.clone();
            newWords.set(index, word);
            return new FunctionalSentence(newWords);
        }
   
        public int getWordsCount() {
            return size;
        }
   
        @Override
        public String toString() {
            StringBuilder sentence = new StringBuilder();
            for (String word : words) {
                sentence.append(word + " ");
            }
            return sentence.toString();
        }
    }

我们对之前的类进行了修改。首先,我们把构造函数进行了修改,一旦传入words我们立即将它clone。这样就保证了用来构造FunctionalSentence 实例的words在外边如何变化都不会影响我们我们已近构造好的FunctionalSentence。因为这个FunctionalSentence 只有在构造函数的时候可以设置状态,而且一旦设置,不能再被改变。所以我们可以放心的在构造函数里来计算这个句子的单词数。因为这个Sentence一旦创建,它不能再被改变。所以单词数不会再改变。

那么我们如何来完成对sentence的修改呢?答案是创建新的sentence对象,就像我们在removeWord,addWord和modifyWord方法里做的那样。我们并不改变当前的words list。而是clone一份。修改clone的list,然后去构造一个新的不可被改变的sentence。

通过上边的修改,给我们带来以下的益处:

  1. 一个sentence对象一旦创建,它的状态就是固定的。
  2. 一个sentence对象的方法是引用透明的,它的方法和方法的调用次序,调用次数都没有关系。一个方法,给定输入,输出永远一样。并且没有副作用。没有副作用是纯函数的一大特征,所谓副作用就是函数改变了外部状态。
  3. 我们给每个方法都加上了返回值。你如果实现了equals方法。那么就可以进行比较来测试了。
  4. 这个类是线程安全的,并且是无锁的,对分布式计算是友好的。在如今CPU频率很难增长,但是CPU内核数量在增加,以及分布式计算的流行。线程安全是如此重要,并且线程锁会极大的限制利用硬件的性能。

这个类的修改有什么坏处吗?

当然,它的坏处就是我们clone了很多list来构造不同状态的sentence对象。但是如果我们是要做一个版本管理软件,需要记录每次的修改。那么这个就不是什么坏处了。看到这里你也许就不难理解Scala里list默认是不可变的,每次修改都给我们生成一个新的list。它只是帮我们省去了clone罢了。

通过这个例子你可能看到了一些函数式编程的特点,当然这还远远不是全部。我也不想在这里展开。在以后的章节里我们还会多次讨论函数式编程思想。下一章就让我们来开始学习Scala吧。

尾巴

什么是函数

一般的,在一个变化过程中,假设有两个变量x、y,如果对于任意一个x都有唯一确定的一个y和它对应。那么这个函数就是y=f(x) x称为自变量,y称为因变量。

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注

%d 博主赞过: