崔鹏飞的Octopress Blog

解释器模式:OOP Versus Functional Decomposition

| Comments

解释器模式

In computer programming, the interpreter pattern is a design pattern that specifies how to evaluate sentences in a language. The basic idea is to have a class for each symbol (terminal or nonterminal) in a specialized computer language. The syntax tree of a sentence in the language is an instance of the composite pattern and is used to evaluate (interpret) the sentence for a client.

以上是wiki对解释器模式的描述。

这是一个学术性稍强的模式,不太好找到生活化的比喻。

就直接上代码吧。

Java

1
2
3
interface Expression {
    int interpret(Map<String, Expression> variables);
}

首先有一个表达式的接口,定义一个求值的方法,该方法接收一个String -> Expression的map。

可以猜到,这个map就是该表达式求值的时候需要用到的context。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Plus implements Expression {
    Expression leftOperand;
    Expression rightOperand;

    public Plus(Expression left, Expression right) {
        leftOperand = left;
        rightOperand = right;
    }

    public int interpret(Map<String, Expression> variables) {
        return leftOperand.interpret(variables) + rightOperand.interpret(variables);
    }
}

class Minus implements Expression {
    Expression leftOperand;
    Expression rightOperand;

    public Minus(Expression left, Expression right) {
        leftOperand = left;
        rightOperand = right;
    }

    public int interpret(Map<String, Expression> variables) {
        return leftOperand.interpret(variables) - rightOperand.interpret(variables);
    }
}

class Number implements Expression {
    private int number;

    public Number(int number) {
        this.number = number;
    }

    public int interpret(Map<String, Expression> variables) {
        return number;
    }
}

class Variable implements Expression {
    private String name;

    public Variable(String name) {
        this.name = name;
    }

    public int interpret(Map<String, Expression> variables) {
        if (null == variables.get(name)) return 0;
        return variables.get(name).interpret(variables);
    }
}

然后有表达式的四个实现类:加法表达式,减法表达式,数字表达式,还有变量。

数字表达式在求值的时候就直接返回它被创建时拿到的数字就好了,这是最简单的。

加法和减法的interpret方法在求值的时候仅仅是把行为委托给了两个子表达式,再对子表达式的求值结果做加减法。

变量在求值的时候则是去context里面寻找其name对应的表达式(也就是它所指向的表达式),然后对其求值。

下面是对它们的结合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InterpreterExample {
    public static void main(String[] args) {
        Map<String, Expression> context = new HashMap<>();
        context.put("w", new Number(5));
        context.put("x", new Number(10));
        context.put("z", new Number(42));

        Plus expr = new Plus(new Variable("w"),
                new Minus(new Variable("x"),
                        new Variable("z")));

        System.out.println(expr.interpret(context));
    }
}

首先构造一个context,里面有w,x,z三个数字。然后计算w+(x-z)的值(看着像不像Lisp?)。

不过再想一下

这些代码实际做的是什么事呢?

实际就是把一个以遇到Number为退出条件的递归算法拆碎了。

如果我们不把它拆碎,就写成递归函数会如何呢?

functions

用Scala试着实现一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
trait Expr

case class Plus(left: Expr, right: Expr) extends Expr

case class Minus(left: Expr, right: Expr) extends Expr

case class Number(n: Int) extends Expr

case class Var(name: String) extends Expr

object ExprEval {
  def eval(expr: Expr, context: Map[String, Expr]): Int = {
    expr match {
      case Plus(l, r) => eval(l, context) + eval(r, context)
      case Minus(l, r) => eval(l, context) - eval(r, context)
      case Var(name) => eval(context(name), context)
      case Number(n) => n
    }
  }

  def main(args: Array[String]) {
    val context = Map("w" -> Number(5), "x" -> Number(10), "z" -> Number(42))
    val expr = Plus(Var("w"), Minus(Var("x"), Var("z")))
    println(eval(expr, context))
  }
}

以上就是全部代码,与Java版等价。 递归函数很容易看懂,其退出条件也很容易看出来。

69行代码变成了26行。

四个case class代表四种表达式,其中并没有什么方法,只是用来作为数据的承载者。

一个eval函数,用pattern match来对四种表达式进行不同的处理。

不过这次我倒不是要宣扬说解释器模式属于是用不合适的工具解决问题。

而是要介绍两种组织代码的方式:按行组织还是按列组织。

按行组织代码与按列组织代码

昨天我在看解释器模式,准备写一个Java实现,再写一个Scala实现,并以此来达到我黑Java的阴暗目的。

但是看了wiki上的示例代码后,马上就想起了去年上过的一门MOOC:《Programming languages》。 (这门课是由华盛顿大学的Dan Grossman教授讲授的,内容极好,强烈推荐。)

这门课里有一节就提到了上面说的两种组织代码的方式:按行组织还是按列组织。 这节课的视频在这里:https://www.youtube.com/watch?v=uEHnI3pq_FM)

比如我们上面的两版代码,Java代码把对表达式的求值分散在每个不同的表达式类里。

而Scala代码把求值代码集中写在一个函数里,pattern match每种表达式类型并求值。

如果要做成一个表格的话,就是这样的:

table

其中的问号代表具体的求值实现。

Java代码横向组织,有一个Plus类,里面有interpret方法,有一个Minus类,里面有interpret方法,等等。这是按照行组织。

而Scala代码则纵向组织,有一个eval函数,纵向把四种表达式的求值都包揽了。这是按列组织。

上面的表格太小,看着不明显,现在假设我们需要打印表达式的功能。那么表格就会变成这样:

table2

可以想象,Java代码里会在每个表达式类里加一个toString函数的实现。横向扩展,一个类把数据和算法组织在一起。

而在Scala代码里则会写一个toString的递归函数,包揽所有字符串打印的工作。纵向扩展,一个函数去分辨数据类型,并据此选择计算策略。

OOP versus Functional Decomposition

那到底哪种组织方式更好呢?

并没有确定的答案,Dan Grossman教授在课程中给出的解释是这样的:

FP and OOP often doing the same thing in exact opposite way: organize the program “by rows” or “by columns”. Which is “most natural” may depend on what you are doing (e.g., an interpreter vs. a GUI) or personal taste.

到底如何组织取决于你想要解决什么样的问题,比如你要做一个GUI库,那么数据与算法放在一起,互相接近是最自然的组织方式。这时选择OOP是最好的设计决策。

而如果你要实现的东西类似于本文中的解释器,那么一个递归的算法来统一处理所有表达式类型则是最自然的。这时选择Functional Decomposition是最好的设计决策。

结语

OOP与Functional Decomposition,这二者并不是完全对立的。

熟练掌握多种抽象与代码组织方式,正确识别应用场景,据此选择合适的范式,或者是选择多种范式结合使用,才是这一系列博文的真实用意。

只不过由于传统的OO设计模式过于盛行,FP范式接受度不够,才会有这一系列博文黑Java,捧Scala的表象。

命令模式的不爽就像用指甲刀刮胡子

| Comments

命令模式

在面向对象程式设计的范畴中,命令模式是一种设计模式,它尝试以物件来代表实际行动。命令物件可以把行动(action) 及其参数封装起来,于是这些行动可以被:

  • 重复多次
  • 取消(如果该物件有实作的话)
  • 取消后又再重做

以上是wiki对命令模式的定义(术语像是台湾的)。

下面是来自《Head first design patterns》的一个例子:

假设你有很多家用电器:电灯泡,电视,音响,还有一个水疗浴缸。(就是没有手电筒)

每个家用电器都有自己的开关装置,处于不同的位置。如果你想把它们都开启,需要一个一个地去按按钮。

现在你想要有一个遥控器,一键开启所有电器,一键关闭所有电器。

或者是一键完成任意的电器操作组合。

每个电器的接口都是不同的,但是又需要和同一个遥控器集成,于是呢,肯定要有一个统一的接口了。

于是就有了下面命令模式的实现代码。

Java

首先是有四大件家用电器。各自之间没有什么关系。

这里面的代码都有点傻,不过没关系,我们就想象这都是些很复杂的硬件通信之类的代码就好了。

然后,定义一个Command接口,其中只有一个execute()方法。

之后我们会用它的实现类来操作各种电器。

这一大坨,就是Command的实现了。

四大件电器,于是便有八个Command,分别负责每个电器的开启和关闭。

有些电器的开启和关闭比别的要复杂一些,不过这没有关系,因为它们的细节都被封装在Command的实现类里面了,我们接下来的代码只要和Command这个接口打交道就好了。

还有一个宏命令,用来组合其他命令。

可以实现遥控器了。

http://elisabethrobson.com/wp-content/uploads/2014/07/Command.jpg

这个遥控器上的按钮都是空白的,我们可以给它置入任意我们想要的命令。

终于可以写一个main函数了:

  • 把家用电器和其对应的Command联系起来
  • 把各种Command组合成开启和关闭两个宏命令
  • 把宏命令置入遥控器

然后,只要按一个按钮,就可以开启所有电器,享受资产阶级奢靡的生活了。

享受够了之后只要再按一个按钮就可以把所有电器关闭掉。

如果再有别的电器,只需要实现几个新的Command,把新的Command组合入宏命令,继续使用遥控器就好了。

换句话说,因为遥控器和电器之间通过Command解耦了,增加新的电器和新的Command对于遥控器没有影响,遥控器的代码是稳定的。这也就是所谓的对扩展开放,对修改关闭。

很好,很符合良好的设计原则,看着就舒服对吧?

不过再想一下

电灯的开启和关闭这两个命令仅仅是对电灯的两个方法的简单代理。

音响的开启和关闭这两个命令仅仅是对音响的两个方法的简单代理。

电视机的关闭也是简单的代理。

这些命令类是否看起来太单薄了呢?它们的方法异常瘦弱,营养不良。

它们除了持有一个需要操作的电器的实例之外,基本没有什么实例级状态。

(电视开机还好,由于需要选择频道,好歹调用了两个方法。 水疗浴缸操作比较复杂,需要调节温度,所以也还稍微好一些。)

每次看到这种贫血的类,我就怀疑它们存在的必要性。

如果我们只是想要给家用电器内的方法构造一个统一个的对外接口,是不是可以用函数式来实现呢?

functions

来试试用Scala实现:

首先是有四大件家用电器,这部分和Java的代码等价。

这一段用来定义各种命令的代码就不同了。

我们对家用电器的各种方法的调用都是只期待其副作用,不期待任何返回值的。所以可以定义一个函数签名Command来涵盖所有这类操作。

和上面的Java代码类似,这里也有一个宏命令,只不过实现简单一些。

电视的开启,水疗浴缸的开和关都有对应的方法来把家用电器的实例封入闭包中。

咦?电灯的开关,音响的开关,以及电视的关闭都跑哪儿去了呢?

由于这几个操作都只涉及到一个方法的调用,它们直接就符合Command的函数签名,所以不用再封入任何闭包了。这一点看下面的代码就明白了。

我们可以定义一个遥控器。其中有开启,和关闭两排按钮。

最后,可以写一个main函数,其中所做的事情和之前Java代码main函数所做的事情是一样的。

只不过,不需要创建各种Command的实例。

而且light.on,stereo.on,light.off,stereo.off,tv.off这几个方法由于符合Command的签名,是可以直接拿来当Command用的。(注意方法名后面没有(),不是调用,而是函数传递)

前后两版代码是等价的。只不过:

  • 247行代码变成了93行代码
  • 16个实体变成了7个

作为一个多按几个按钮都嫌麻烦的好逸恶劳的资产阶级,这个结果是我所乐于见到的。

更少,更紧凑的代码。更少的实体。我终于可以用更小的成本来享受我昂贵的家用电器了。

指甲刀刮胡子

最后回到标题上去:指甲刀刮胡子,意即用不合适的工具解决问题。

命令模式想要做到的事情其实就是给各种不同的操作寻找一个统一的接口,从而实现调用者(遥控器)和被调用者(家用电器)之间的解耦。

给不同的操作寻找一个统一的接口这件事可以通过接口来做,但是我们同时要承担写一堆贫血类的代价。

而如果直接用函数来做的话,则可以得到更紧凑简洁的代码(就像object Commands这个实体内的代码一样)。

该模式提出的时候FP并不如今日盛行,其作者选用了可能会导致贫血类泛滥的解决方案,这无可厚非。传播了解耦和开闭等良好设计的原则也实为功德。

不过今天我们有了剃须刀,就无需一定要用指甲刀来刮胡子了。

职责链模式的别扭就像用门框夹核桃

| Comments

职责链模式

责任链模式在面向对象程式设计里是一种软件设计模式,它包含了一些命令对象和一系列的处理对象。每一个处理对象决定它能处理哪些命令对象,它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。该模式还描述了往该处理链的末尾添加新的处理对象的方法。

以上是wiki对职责链模式的定义。

举个例子来说,我们的系统中需要记录日志的功能。日志需要根据优先级被发送到不同的地方。

低优先级的日志输出到命令行就好了。而高优先级的错误信息则需要通过邮件通知相关人员并且输出到命令行。

这个例子也是来自wiki的。

以下是wiki提供的Java实现:

Java

首先定义一个Logger抽象类。从其setNext和message这两个方法可以看出,我们后面会把多个具有不同writeMessage实现的Logger链到一起,并且依次让它们处理某件需要被记录的事件。

然后有三个Logger的实现,分别为向命令行输出消息,发送邮件(当然是假的),向命令行输出错误。

最后,有一个main函数,创建三个Logger的实例,把它们通过setNext链在一起。 只需要调用一次message就可以让三个Logger依次工作。

如果以后再有更多的Logger呢,还是可以通过同样的方式把它们链接起来协同工作。

很好,很强大,很易于扩展,对吧?

不过再想一下

这三个Logger的实现类看起来都非常的单薄,弱不禁风。

一个接收mask的构造函数,其唯一职责就是把接收到的mask传递给父类的构造函数。

然后父类根据mask和所发生事件优先级的大小关系决定到底要不要调用子类实现的writeMessage方法。

也就是说,子类完全没有定义自己的实例级状态,其实例级方法的行为也就谈不上随着其状态的变化而变化了。

换句话说,这几个子类存在的价值就在于为父类提供writeMessage这个函数。

啊。。。。。。!

一说到提供函数,我就想到了。。。。。。

functions

我想到的自然是FP了,既然需要的是函数,那我们就使用函数好了。

何必用更重的抽象手段:类,去包裹函数呢?

下面就是比较偏函数式的Scala实现:

这个代码已经简短到我不想解释的程度了。不过还是解释一下吧。

三个log的的等级ERR,NOTICE和DEBUG和之前Java的实现是一样的。

一个case class Event,用来包裹需要被log的事件。

type Logger则是声明了一个函数签名,凡是符合这个签名的函数都可以作为logger被使用。

然后便是三个函数实现,它们将mask通过闭包封进函数内。这三个函数共同依赖一个私有handleEvent函数,其作用和Java代码中的message类似,判断mask和正在发生的事件之间优先级大小关系,并以此决定当前logger是否需要处理该事件。

哎?等一下,这个是职责链模式啊,那个啥,链在哪儿呢?就在main函数里。其中的andThen就可以把三个logger链在一起。

这个andThen是个什么东西?何以如此神奇?

欲知详情,请参考我之前的另一篇博客: http://cuipengfei.me/blog/2013/12/30/desugar-scala-9/

而链接之后的结果本身也是一个函数,于是我们就可以调用chain并传入Event了。

这份代码和前面Java版的行为是等价的,输出是一致的。

门框夹核桃

最后回到标题上去:门框夹核桃,意即用不合适的工具解决问题。

职责链模式想要做到的事情其实就是把多个函数链起来调用。

该模式提出的时候FP并不如今日盛行,其作者选用类来包装需要被链接的多个函数,这无可厚非。

无论是class,还是function,都是为程序员提供抽象的手段。当我们想要链接的东西就是多个function,选择直接用function而非class就会显得更加自然,也更加轻量且合适。

当年design pattern的作者广为传播各种patterns,实为功德。

不过今天我们有了核桃夹,就无需一定要用门框了。

最后,依照惯例,羞辱Java一小下下。 以上wiki提供的实现有77行,偏FP风的实现只有38行,只有一个实体Event。

策略模式的尴尬就像用菜刀开啤酒

| Comments

策略模式

策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。

以上是中文wiki中对策略模式的定义。

In computer programming, the strategy pattern (also known as the policy pattern) is a software design pattern that enables an algorithm’s behavior to be selected at runtime. The strategy pattern: - defines a family of algorithms, - encapsulates each algorithm, and - makes the algorithms interchangeable within that family.

Strategy lets the algorithm vary independently from clients that use it.

以上是英文版的。

鸭子

这种偏学术性的描述实在太绕嘴,来思考一个实例:

我们需要创建一些鸭子,鸭子有什么行为呢?

  • 鸭子会飞
  • 会叫
  • 会游泳

不过,是否所有的鸭子都是这样呢?万一是玩具鸭子呢?万一是猎人放在水里的用来勾引公鸭子的木质母鸭子呢?万一是外星来客太空鸭呢?

你已经知道什么意思了。

鸭子的各个子类的飞和叫的行为不尽相同。所以我们想把飞和叫这两种行为独立开来,让它们可以自由组合在鸭子的不同子类中。

以上例子来自著名的《Head first design patterns》。

Java

以下是《Head first design patterns》附带的代码:

飞行的接口,以及两个实现:一个真会飞,一个不会飞。

叫的接口,两个实现,一个真会叫,一个不会叫。

最后,终于到了鸭子。鸭子的顶层抽象类声明两个字段,一个用来飞,一个用来叫。

这样在子类里就可以把这两个字段锁定到某个特定的实现,以实现任意的组合。

可以看到,绿头鸭(mallard)组合了真会飞和真会叫。而诱饵鸭(decoy,猎人用来勾引鸭子上钩的那个)则组合了不会飞和不会叫。

可以想象随着飞和叫这两个家族的扩大,我们可以组合出更多种类的鸭子来。

很好,很灵活,很强大,对吧?

不过再想一下

我们想要的不过是把两个家族的不同行为塞到鸭子的子类里去。是否有更容易的办法来做到呢?

trait

一说到把行为塞到某个类里,就会想到mix in,很自然就想到了Scala的trait。

更多关于Scala的trait的详情请参考我的另一篇博客: http://cuipengfei.me/blog/2013/10/13/scala-trait/

飞行家族。

叫的行为的家族。

最后,鸭子的各种实现。

貌似和Java版的实现差距不大,飞和叫的interface和class变成了trait。

Duck原来是持有Fly和Quack的实例,现在则是变成了混入Fly和Quack这两个trait。

这个代码比Java短一些,紧凑一些,构造函数中的赋值变成了类型声明时的混入。

不过再想一下

我们不过是想要把某种行为塞入到某个类里面去,真的有必要用interface,class,trait来把这些行为包裹起来吗?

行为通常是以哪种形式承载的呢?

functions

行为通常是以函数承载的。

也就是说我们想要做的不过是把符合某个签名的函数塞到鸭子的子类里去而已,而却用interface,class,trait来把这些行为包裹起来了。有些臃肿不是吗?

下面是直接把函数塞入鸭子子类的做法:

Fly和Quack不再是interface或者是trait。而是type aliase。

Scala的type aliase就类似于C#的delegate,用来声明function signature。

更多关于type aliase的更多详情请参考我的另一篇博客: http://cuipengfei.me/blog/2013/12/23/desugar-scala-4/

这样,会飞不会飞,会叫不会叫就无需被class或者trait包裹着了,直接就是一个个的函数。

鸭子的子类通过构造函数接收飞和叫的两个函数作为参数,就能够组合不同的行为了。

如果说之前triat的实现方式与Java实现版相比偏重了inheritance而不是composition,这一版的实现则又回到了纯composition的路上了。

紧凑程度,实体数量都比以上两版有改进。这一点从行数上可以窥见:Java版63行,trait版29行,最后一版21行。

菜刀开啤酒

最后回到标题上去:菜刀开啤酒,意即用不合适的工具解决问题。

strategy patten要解决的问题其实就是如何把一族行为的不同实现注入到某个类里去。

这一点,最开头的wiki定义已经说的很明白了:

Strategy lets the algorithm vary independently from clients that use it.

无论是class,还是function,都是为程序员提供抽象的手段。当我们想要抽象的东西就是一段algorithm(正如wiki所说)的时候,用function来做抽象就是更加轻量且合适的选择。

该模式提出的时候FP并不如今日盛行,其作者选用纯OO的方式解决了问题,并广为传播,实为功德。

不过今天我们有了开瓶器,就无需一定要用菜刀了。

最后是一个Java 8的实现:

看起来比最开始的那一版好一些,但是我还是看它不顺眼。

为什么呢?

一定是由于我强烈的偏见而没有其他任何原因,一定是这样的。

Principles of Reactive Programming Week Two作业导学

| Comments

声明

这系列博文的目标读者仅限于报名参加了这门课并且看完了视频,看完了作业的instruction之后仍有困难的同学。

这系列博文不会公布作业的答案,那是违反Coursera的code of honor的。

我只会试着解释作业中已有的代码,以及应该如何入手。

其实,写这个系列博文对我的帮助比对读者的帮助要大。

这周的作业不太难,主要就是一个观察者模式。

Signal是怎么work的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scala> val a = Var(1)
a: calculator.Var[Int] = calculator.Var@7ca6f5b9

scala> val b = Var(2)
b: calculator.Var[Int] = calculator.Var@2286d26

scala> val c = Signal(a() + b())
c: calculator.Signal[Int] = calculator.Signal@5c60548d

scala> c()
res8: Int = 3

scala> a()=10

scala> c()
res10: Int = 12

scala> b()=20

scala> c()
res12: Int = 30

如果能搞懂上面的代码是如何work的,作业题中需要用到Signal的地方就不会有太大问题了。

a=1,b=2,c=a+b,所以c就是3。

a变成10之后c就变成了12(10+2)。

b再变成20之后,c就变成了30(10+29)。

这个级联的变化是如何发生的呢?

有两个关键点:

  • Signal的constructor
  • Signal的update方法

先看Signal的constructor。

1
2
3
class Signal[T](expr: => T) {
  //......
}

以上是它的签名,关键在于expr的类型签名,expr的类型不是T,而是=>T。

这就意味着expr可以是任何类型为T的表达式,可以是一个字面量,也可以是任意复杂的代码块。

比如Signal(123)是可以的,Signal(complicatedMethodCall())也可以。

最上面那块代码中的val c = Signal(a() + b())就属于后一种。

a() + b()不会被立即求值成3然后传入Signal的constructor,而是整体作为一个可以被反复求值的表达式被记录在Signal的实例中。

constructor的入口参数可以被反复求值是级联变化的基础,那是什么触发了真正的变化呢?

那就是关键点之二:update方法。

update方法的妙处在于,如果一个类A有update方法,那么:

1
2
val x = new A()
x(y)=z

在编译之后会变成这样:

1
2
val x = new A()
x.update(y,z)

详情请见我之前的一篇博客:http://cuipengfei.me/blog/2014/06/12/scala-update-method/

Signal的update方法是protected的,不可访问,所以它只可以从变,不可自变。

而Var把update方法public出来了,这样,在下面这样的代码执行时:

1
2
a()=10
//a.update(10)

a就会通知它的observers去重新求值。 这样就实现了a或者b这样的Var变化的时候,c这样的Signal跟着变化的效果。

搞懂了上面的内容就足以去做作业了。

怎么和html页面结合起来的?

执行instruction里提到的webUI/fastOptJS这个task就会把Scala作业代码编译成js。

这个task是scalajs这个dependency带进来的(在webui.sbt里)。

webui这个项目里有一个CalculatorUI.scala文件,也会被编译成js。其中的代码就把作业代码和html的UI结合起来了。

就是这样了,这周的作业不难懂也不太难做。

Principles of Reactive Programming Week One作业导学

| Comments

前尘

Principles of Reactive Programming在4月13号又开课了。 https://www.coursera.org/course/reactive

上次开课是在2013年的11月,当时我刚第一次上完Functional programming principles in Scala,热情很高于是就报名参加了这门课。 还群发了一个邮件找人一起上课。

但是上了几周发现有点难,于是就放弃了。现在去bitbucket看,最后一次push停留在了2013-11-18。

后来还在上海被8x鄙视于无形之中。

后世

14年做了几个月的Scala开发,后来Functional programming principles in Scala再次开课又上了一遍,拿了个认证证书。

感觉似乎可以再挑战一次。

今生

上课习得的知识放在脑子里是不牢靠的。大脑有遗忘周期。

需要有成文或者成代码的产出,作为日后回忆和做spaced repetition的资料。

于是就有了这个即将成为系列的博文中的第一篇《Principles of Reactive Programming Week One作业导学》。

这系列博文的目标读者仅限于报名参加了这门课并且看完了视频,看完了作业的instruction之后仍有困难的同学。

这系列博文不会公布作业的答案,那是违反Coursera的code of honor的。

我只会试着解释作业中已有的代码,以及应该如何入手。

其实,写这个系列博文对我的帮助比对读者的帮助要大。

正文

Heap.scala

第一周的代码下载下来之后,先来看一下Heap.scala这个文件。

这个文件里定义了很多个trait。现在只需要关注其中一个Heap。

这个就是所有其他trait都会去extend的基类(这个说法合适吗?)。 它定义了所有Heap的实现者都需要实现的方法。

然后BinomialHeap完整实现了Heap定义的所有东西。

Bogus1BinomialHeap到Bogus5BinomialHeap都是继承自BinomialHeap,其中各自覆盖了BinomialHeap的不同方法,以不同的方式引入了bug。 第一周作业的目的就是用ScalaCheck把其中的bug找出来。

这个文件里还有一个IntHeap,这个稍后再说。

实现代码其实就只有这一个文件,接下来看测试代码。

QuickCheckSuite.scala

这个文件里主要定义了QuickCheckSuite这个测试类。

这个测试类继承自FunSuite,这是ScalaTest的测试基类。同时mix in了Checkers,这是ScalaTest为了与ScalaCheck集成而提供的trait。

接下来看测试的case:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def checkBogus(p: Prop) {
  var foundBug = false
  try {
    check(p)
  } catch {
    case e: TestFailedException =>
      foundBug = true
  }
  assert(foundBug, "A bogus heap should NOT satisfy all properties. Try to find the bug!")
}

test("Binomial heap satisfies properties.") {
  check(new QuickCheckHeap with BinomialHeap)
}

test("Bogus (1) binomial heap does not satisfy properties.") {
  checkBogus(new QuickCheckHeap with Bogus1BinomialHeap)
}

test("Bogus (2) binomial heap does not satisfy properties.") {
  checkBogus(new QuickCheckHeap with Bogus2BinomialHeap)
}

可以看到,每个case都调用了check这个方法,或者是check的变体-checkBogus。

checkBogus里面则调用了check,并且assert说一定要出现TestFailedException异常了,测试才算成功。也就是说checkBogus的目的就是要在某些Heap的实现中找到bug。

现在来看check这个方法本身。它接受一个类型为Prop的参数,这些参数从哪儿来呢?这些参数就是:

1
2
3
new QuickCheckHeap with BinomialHeap

new QuickCheckHeap with Bogus1BinomialHeap

这些代码。

这就意味着QuickCheckHeap一定要是一个Prop,是不是这样呢?

QuickCheckHeap.scala

那就到QuickCheckHeap.scala这个文件中来看一下。

可以看到QuickCheckHeap这个抽象类确实是extends了Properties,而properties又extends了Prop。那么,没问题,这个类型是匹配的。

QuickCheckHeap里可以定义任意多个property,这些property将会检查Heap的实现正确与否。

而且它还mix in了IntHeap,就是前面略过的那个trait。它的目的是锁定Heap这个trait里所定义的A这个元素的类型到Int。

全部连起来

第一周作业的已有代码很少,有用的就是这三个文件。

Heap.scala定义了很多个Heap的不同实现。有些是正确的,有些是有bug的。

QuickCheckSuite.scala则是测试的入口点,它由JunitRunner拽着跑起来。 其中的test case使用ScalaCheck去检查对于Heap这种数据结构恒定为true的properties是不是hold住的。

对于Heap这种数据结构恒定为true的properties从哪儿来呢?就来自于QuickCheckHeap.scala。 QuickCheckHeap本身是一个抽象类,不可以被实例化。但是由于有了牛逼的trait,就可以用这种代码:

1
2
3
new QuickCheckHeap with BinomialHeap

new QuickCheckHeap with Bogus1BinomialHeap

创建出实例,进行测试了。

最后,我们的任务就是在QuickCheckHeap.scala添加更多的properties,把所有实现有误的Heap都揪出来。

题外话

有没有发现QuickCheckHeap.scala里面有些奇怪的代码?

1
2
3
4
property("min1") = forAll { (heap: H, a: A) =>
  val min = if (isEmpty(heap)) a else findMin(heap)
  findMin(insert(min, heap)) == min
}

property(“min should be min”),这看起来像是一个方法调用啊。

尼玛,方法调用后面怎么跟着一个等号啊?等号后面还有一个有返回值的表达式啊?

这是啥啊?

这是个乍一看很自然,但是仔细一想很费解的Scala语言特性-update方法。

详情请见我之前的一篇博客:http://cuipengfei.me/blog/2014/06/12/scala-update-method/

职业女性确实处于劣势吗?记一次不甚严谨的考据 – 向胡适之先生的遥远致敬

| Comments

源起

前两天,在一个武汉本地程序员聚集的技术社区微信群里某位群友发了两张图片:

这是某个IT公司的招聘宣传,为程序员提供的鼓励师。

(由于图片出现在愚人节期间,不确定该公司是真的有这样的人员配备,还是恶作剧的,此处暂且存疑)

马上群里就有一位X君跳出来说这种事情就是混蛋啊,怎么女人就得给男人端茶倒水擦汗啊。

另外一位Y君就说没有啊,这就是开个玩笑啊,不要这么较真啊。

X君继续说:!@#¥%……&*啊

Y君回应:*&……%¥#@!啊

于是,你也可以猜到的,这中间X君就说了IT行业对于女性从业者存在歧视,收入不平等之类的话。

这让我颇为感慨:武汉也无非是这样。武大的樱花烂熳的时节,群中却有这样标致极了的讨论。其实,又何止武汉呢?

正当X君与Y君酣战之时,有另外一位群友问声称存在收入歧视的X君是否有数据支持其观点。

恰巧我这周正在看《胡适文选》。胡适之先生反复提醒读者要有怀疑精神,凡事要讲求证据,要用科学的手段得出科学的结论。

胡适之先生之言于我心有戚戚焉,于是我便想要搜罗数据,深入了解一下这个话题,算作是对胡适之先生的遥远致敬。

以上,为源起。

定题

近来我正尝试着自我疏离本性中近似于周树人先生的那一部分。

便不太愿用“歧视”这个颇为尖刻的词汇,因而本文的标题中用了“劣势”这一稍为中性的说法。

既然要考据,就不妨把话题放大些,不独观察IT行业的女性,莫若把视角扩宽到整个职业女性上去。

再加上我不是专业的考据家,并没有投入大量时间精力去搜索资料,交叉引证也不够详备,那就不能怕会过谦,也在标题上加上“不甚严谨”这几个字。

于是便有了这个颇显啰嗦的标题:《职业女性确实处于劣势吗?记一次不甚严谨的考据 – 向胡适之先生的遥远致敬》。

是为题目由来。

开篇

我是一个颇为庸俗的人,也时常会被称为理性的人。

于是,别人看待职业女性的眼光、上司是否会给小鞋穿、同事是否会区别对待等等这些无法量化,难以考量的因素均不采用。

我就只认准了:

在一个行业中具有某种特征的人群占多数就可以被称作是有优势的

在一个行业中具有某种特征的人群挣钱多就可以被称作是有优势的

这两条考校标准。

所以接下来通篇都围绕着人数多寡和挣钱多少展开。

什么行业收入高?

中华人民共和国国家统计局,中国统计年鉴(2014)中有一个条目:按行业分城镇单位就业人员平均工资,以Excel格式提供:

(这一行Excel实在是太长了,我把它分开截了两张图)

其中收入最高的三个行业标记为了红色,分别为金融,IT和科研。

原数据可以在这里找到: http://www.stats.gov.cn/tjsj/ndsj/2014/indexch.htm

点击左侧的“四、就业和工资”然后点击第“4-15”项,里面可以下载Excel。

或者也可以直接通过这个链接下载Excel: http://www.stats.gov.cn/tjsj/ndsj/2014/zk/html/Z0415C.xls

那接下来就按图索骥,考量这三个行业中女性的状况。

(为什么只有三个?笔者精力有限,只求管窥,不求完全覆盖)

金融

搜寻良久,实在是找不到国内的资料,只好拿些英文的资料作为旁证了。

以下是来自美国的公平就业机会委员会(Equal Employment Opportunity Commission,缩写为EEOC)2006年发布的一份报告中关于女性金融从业者比例的图表:

从最后一行的汇总信息可以看出,经理级别的职位,女性占18%左右。

专业从业者中,女性约为26%。

技术与销售类的职位则只有个位数的百分点。

但到了书记员,抄写员(Clerical,表中最后一列)这一类的职位,却有43%是女性。

由此不难观察到,在美国,金融这个高薪行业中女性在做着勤务工作,升到经理职位的甚少。

这份报告的出处: http://www.eeoc.gov/eeoc/statistics/reports/finance/finance.pdf

公平就业机会委员会的wiki页面:http://zh.wikipedia.org/wiki/公平就业机会委员会

而另外一份来自于英国平等与人权委员会(Equality and Human Rights Commission,EHRC)2009年春季发布的报告则有些不同:

从这张男女性别比例的图表可以看出,英国的金融行业男女从业人数基本一比一,差距不大。 从最后一行的汇总数据来看,女性还比男性多一个百分点。

而下面这张出自同一报告的关于收入差距的图表,则显露了另外的信息:

英国金融行业中,全职工作的男性年收入比全职工作的女性多55%。

而在全社会所有行业中,这个数字也有28%。

可见在英国女性虽然以同等的人数参与进了金融行业,但是却没有拿到哪怕是接近同等的薪水。

这份报告的出处: http://www.equalityhumanrights.com/sites/default/files/documents/download__finance_gender_analyis_research.pdf

平等与人权委员会的wiki页面:http://en.wikipedia.org/wiki/Equality_and_Human_Rights_Commission

IT

接下来开始看三大高收入行业中的第二名:IT行业中女性的状况。

我找到了一份来自美国的非营利机构:National Center for Women & Information Technology (NCWIT)在2009年发布的报告。

(没找到这个机构确切的中文翻译,就保留原文吧)

报告中有一张女性IT从业人员比例随年份变化的趋势图

容易看出,从上个世纪八十年代中期到九十年代初期,女性IT从业者比例在攀升,从30%增长到37%左右。

在此之后则一路下降,到2008年已经减少到了25%左右。

出自同一报告的还有另外一张男女收入差距随工作经验变化的趋势图

可以看出,入行初期男女收入没太大区别,但从第三年开始,逐渐拉开差距,由3%增加到12%。

好了,又是一个高薪行业。女性只占其中的四分之一,而且收入还比男性少。

报告出处:http://www.ncwit.org/sites/default/files/legacy/pdf/NCWIT_TheFacts_rev2010.pdf

NCWIT的wiki页面:http://en.wikipedia.org/wiki/National_Center_for_Women_%26_Information_Technology

科研

高薪行业之三,科研。

找到了两份来自欧盟的报告。

第一份报告中有一张科研行业中女性从业者比例的图表,数据采集自1999年:

不难看出,其中希腊和葡萄牙的女性科研工作人员较多,占有41%和43%。

德国和匈牙利则很低,女性只有14%到19%。

其他八个国家大致是落在26%到33%这个区间。

第二份报告发布于2012年,其中有一张男女收入差距的图表:

该图表数据统计于2002年和2006年,从中不难看出,女性在科研行业的各个分支中收入比男性低20%到40%。

由此可见,在欧洲,科研行业作为一个高薪行业,其中女性从业人员较少。 即便进入这个行业的女性,其收入也要较男性低。

第一份报告出处:https://ec.europa.eu/research/swafs/pdf/pub_gender_equality/wir_final.pdf

第二份报告出处:http://ec.europa.eu/research/science-society/document_library/pdf_06/meta-analysis-of-gender-and-science-research-synthesis-report.pdf

发布报告的欧盟网站:http://ec.europa.eu/index_en.htm

小结

以上观察了三个薪水最高的行业:金融,IT和科研,这三个行业中都呈现出了女性从业人员少于男性,且收入低于男性的态势。

如果这条结论和以上干巴巴的数据无法让您获得感性的认知的话,那我们再结合其他数据做个分析。

以下是来源于非营利组织National Association of Colleges and Employers (NACE)的一份报告中关于平均年工资涨幅的数据:

可以从最后一行看出,平均工资涨幅是每年7.5%。

这意味着什么呢?

1
2
3
4
5
6
7
8
[1] pry(main)> Math.log(1.55,1.075)
=> 6.059885534213904
[2] pry(main)> Math.log(1.12,1.075)
=> 1.5670305391527257
[3] pry(main)> Math.log(1.20,1.075)
=> 2.5210161634544224
[4] pry(main)> Math.log(1.40,1.075)
=> 4.652504958776575

如果您不是IT行业的看不懂上面的代码没关系,我来解释一下。

这意味着,如果您是一名金融行业的女性从业者,您旁边座位上是一名和您同时进公司的男同事。 你们的关系很好,他甚至都不介意让您看他的工资单。这给在公司属于珍稀物种的您带来了不少宽慰。 但是经过分析自己历年的工资涨幅,您会发现如果您想要和他赚到一样多的钱的话,您还要再工作六年才行。

而这个数字在IT行业是一年半

在科研行业是两年半四年半

以上引用报告出处:https://www.naceweb.org/uploadedFiles/Content/static-assets/downloads/executive-summary/2014-september-salary-survey-executive-summary.pdf

NACE的wiki页面:http://en.wikipedia.org/wiki/National_Association_of_Colleges_and_Employers

然后呢?

以上仅仅是通过交叉引证来描述了职业女性的状况。是属于实证性的表述(positive statement)。

而关于职业女性应该处于何种状况,那是属于规范性的表述(normative statement),本文就不涉及了。

女性在这些高薪行业中人数少于男性,这是好事吗?这是坏事吗?

女性在这些高薪行业中收入低于男性,应该如何评价这件事呢?

金融,IT和科研,听起来都是理工宅男的专长啊,女的少不是属于正常现象吗?

女性的收入低于男性,那有可能是她们干活不给力啊,那收入低就是应该的吧?

所有这些问题,都属于价值判断。通过上面引用的数据,以及常识的积累,我对这些问题会有确定性的判断。 想来你也能猜到我的判断是什么。但是我不把它说出来,留待读者自己得出结论

最后

如果您觉得这篇博客写的还可以,请用手机支付宝扫描下面的二维码:

我会把收到的巨款用来置装,美容,健身。

然后穿的花枝招展,抹的五彩绚烂,露出两条人鱼线。

站在女程序员们旁边,给她们端茶倒水擦汗。

并且忘掉我也可以是一个独立的个体,也可以通过某种其他的方式体现自我价值。

成为一名雄性鼓励师,从此人生走上巅峰。

最后的后面

最后的后面怎么还有呢?因为标题已经啰嗦了,索性结尾也啰嗦一下。

说是不严谨的考据,但是还是用了十几个番茄钟,五六次git commit,三四次审校。

七易其稿也不过是如此的两倍嘛。

不过从此以后,我要与这样的辛劳说再见了。我要成为一个靠性别,靠脸,靠身材吃饭的男人。

所以,你懂的,趁我还能靠智识谋生,扫码吧。

别跑,说的就是你,别骗我,我知道你余额宝收益好几十块钱一天呢。

难道你就不想为一位志存高远的未来雄性鼓励师提供一些帮助吗?

(胆敢说我最终还是免不了被周树人附体的善款要x2)

现在,请回到页面的最上端,再看一遍那两张图片,请问您现在看到的东西和读本文之前还一样吗?

2015第一季度

| Comments

时间是以何种方式流逝的呢?

三个月瞬间就不见了。

对照着去年的总结和当时对未来的期望写一下2015年第一季度吧。

博客

惨不忍睹。

除了一个应付任务的tech radar的session写在了博客上,其他啥都没写。

去年博客有产出是因为在刷Scala这个主线任务。

今年做的都是支线任务,这一点那一点,难以形成有效的产出。

要形成有效的产出,需要有plan,有execution,有retro。

兴之所至,就把今年的博客主线任务定为OO与FP的比较和结合应用吧。

读书

不错。

第一个季度已经读完10本书。数量达标。

说过要做的笔记也做了。

笔记的作用确实很好,每隔一段时间回顾一次,spaced repetition可以促进和加深记忆。 不会再有“卧槽,这本书我看过吗?作者都讲了些啥啊?”的尴尬事。

看完了《经济学原理》的微观分册,这是最近几年来读过的最大部头的一本书。鉴于之前一直没有耐心读厚书,这可以算作是一个进步。

开始把待读的书按照内容领域和期待得到的效果分类放到豆列里。这样每次没书看了就去想读的豆列里挑一本。不用每次都700选一了。

读书的领域扩张也在按照之前的计划进行,没有风险。

博客需要有主线任务,读书要配合,可以优先选一些讲paradigm的书来读。

MOOC

尴尬。

这三个月一个mooc都没有跟。4月13日,reactive programming课要开,这个一定要跟,而且一定要跟完。

估计完成当时6门课的目标有点悬了。

非常无耻地修改一下mooc的目标吧:配合主线任务,以读书和mooc作为输入,博客作为产出。 数量和领域都不做具体限定。 (果然够无耻)

体重

稳定。

上次写年终总结的时候是61.1公斤,现在是61.0公斤。

当时写的是:

明年没有太多改进的目标,维持就ok了。

没有了减重目标,没有了改进方向,效果果然是不会自动出现的啊。 这是很有意思的一个现象,没有了改进的意愿,或许潜意识里对热量摄入和消耗都没有那么敏感和严格。

还好胸在变大,肱三在变大,腹肌在浮出水面。

最后

写个阶段性总结还是有用,发掘出了主线任务。