函数式和面向对象编程有什么区别?

函数式编程 (Functional Programming) 和 面向对象编程 (Object Oriented Programming) 是两个主流的编程范式,他们有各自独特的闪光点,比如函数式编程的数据不可变惰性求值,面向对象编程的继承多态等。这些语言特性上的区别,可以参考之前的文章,这篇文章主要从实现相同功能的角度,来对比这两种编程范式,他们在实现上的逻辑是截然相反的

初步实现

在函数式编程中,代码逻辑通常是按照要做什么。而在面向对象编程中,通常是把代码逻辑抽象成 class,然后给这些 class 一些操作。这么说起来很抽象,用下面这个例子来详细说明。

假设我们要用 函数式编程 和 面向对象编程 来分别实现下面这些功能:

eval toString hasZero
Int
Add
Negate

表格左列 Int, Add, Negate 是三个变式 (Variant),eval, toString, hasZero 是三种操作,这里要做的是填满这个表格,分别实现三个变式的三种操作。

函数式编程实现

这里用 ML 来做函数式编程的实现,即使没用过这门语言,应该也能读懂大概意思。

datatype exp =
    Int    of int
  | Negate of exp
  | Add    of exp * exp

exception BadResult of string

fun add_values (v1,v2) =
    case (v1,v2) of
            (Int i, Int j) => Int (i+j)
      | _ => raise BadResult "non-values passed to add_values"

fun eval e =
    case e of
            Int _       => e
      | Negate e1   => (case eval e1 of
                          Int i => Int (~i)
      | _ => raise BadResult "non-int in negation")
      | Add(e1,e2)  => add_values (eval e1, eval e2)

fun toString e =
    case e of
        Int i           => Int.toString i
      | Negate e1   => "-(" ^ (toString e1) ^ ")"
      | Add(e1,e2)  => "("  ^ (toString e1) ^ " + " ^ (toString e2) ^ ")"

fun hasZero e =
    case e of
        Int i           => i=0
      | Negate e1   => hasZero e1
      | Add(e1,e2)  => (hasZero e1) orelse (hasZero e2)

在函数式编程中,先定义了一个数据类型 (datatype) 来表示 Int, Negate, Add,这样定义的目的是什么呢?举个表达式的例子:

  • Int 代表一个 int 的数据,比如 Int(2)
  • Negate 代表 Int 的负数,比如 Negate(Int(2)))
  • Add 代表两个 Int 相加,比如 Add((Int(2), Int(3))

然后再分别实现三个操作 eval, toString, hasZero:

  • eval 是给一个表达式求值,比如给 Negate 求值,eval(Negate(Int(2))) = Int(-2) ,给 Add 求值,eval(Add(Int(2), Int(3))) = Int(5)
  • toString 是把这个表达式输出成字符串,比如 toString(Add(Int(2), Int(3))) = "2 + 3"
  • hasZero 是判断表达式有没有 0。

再看刚刚这句话函数式编程的代码逻辑通常是按照要做什么,这里的主体是三个操作,eval, toString 和 hasZero,所以三个分别是一个函数,在函数里去实现三种变式怎么操作。

可以说,函数式编程式纵向的填满了上面的表格。

面向对象编程

这里用 Ruby 来实现。

class Exp
end

class Value < Exp
end

class Int < Value
  attr_reader :i
  def initialize i
    @i = i
  end
  def eval # no argument because no environment
    self
  end
  def toString
    @i.to_s
  end
  def hasZero
    i==0
  end
end

class Negate < Exp
  attr_reader :e
  def initialize e
    @e = e
  end
  def eval
    Int.new(-e.eval.i) # error if e.eval has no i method
  end
  def toString
    "-(" + e.toString + ")"
  end
  def hasZero
    e.hasZero
  end
end

class Add < Exp
  attr_reader :e1, :e2
  def initialize(e1,e2)
    @e1 = e1
    @e2 = e2
  end
  def eval
    Int.new(e1.eval.i + e2.eval.i) # error if e1.eval or e2.eval has no i method
  end
  def toString
    "(" + e1.toString + " + " + e2.toString + ")"
  end
  def hasZero
    e1.hasZero || e2.hasZero
  end
end

< 在 Ruby 里是继承的意思,class Int < Value 表示 Int 继承了 Value,Int 是 Value 的 Subclass。

可以看到面向对象编程组织代码的方式和之前的完全不一样。这里把 Int, Negate, Add 抽象成了三个 class,然后分别给每个 class 加上 eval, toString, hasZero 三个方法。这也是刚刚那句话的说法 面向对象编程把代码逻辑抽象成 class,然后给这些 class 一些操作,这里的主体是 Int, Negate, Add 这三个 class。

可以说,面向对象编程是横向的填满了上的表格。

通过这个对比,可以知道 函数式编程 和 面向对象编程 是两种相反的思维模式和实现方式。这两种方式对代码的扩展性有什么影响呢?

扩展实现

eval toString hasZero absolute
Int
Negate
Add
Multi

在上面那个例子的基础上,我们再加一行一列,增加 Multi 这个变式,表示乘法,增加 absolute 这个操作,作用是求绝对值。这会怎么影响我们的代码呢?

函数式编程

在函数式编程中,要增加一个操作 absolute 很简单,只要添加一个新的函数,不用修改之前的代码。但是要增加 Multi 比较麻烦,要修改之前的所有函数。

面向对象编程

和函数式编程相反的,在这里增加一个 Multi 简单,只要添加一个新的 class,但是增加 absolute 这个操作就要在之前的每一个 class 做更改。

选择用 函数式编程 还是 面向对象编程 的一个考量因素是以后将会如何扩展代码,对之前代码的更改越少,出错的概率越小。

Binary Methods

前面的对比,操作都是在一个数据类型上进行的,这里进行最后一个对比,一个函数对多个数据类型进行操作时,函数式和面向对象分别怎么实现。

Int String Rational
Int
String
Rational

这里要实现的是一个 add_values(x, y) 的操作,把两个数据相加,但是 x, y 可能是不同的类型的。

函数式编程

函数式编程的实现相对简单:

datatype exp =
    Int    of int
  | String of string
  | Rational of real

fun add_values (v1,v2) =
    case (v1,v2) of
                (Int i,  Int j)         => Int (i+j)
      | (Int i,  String s)      => String(Int.toString i ^ s)
      | (Int i,  Rational(j,k)) => Rational(i*k+j,k)
      | (String s,  Int i)      => String(s ^ Int.toString i) (* not commutative *)
      | (String s1, String s2)  => String(s1 ^ s2)
      | (String s,  Rational(i,j)) => String(s ^ Int.toString i ^ "/" ^ Int.toString j)
      | (Rational _, Int _)        => add_values(v2,v1)
      | (Rational(i,j), String s)  => String(Int.toString i ^ "/" ^ Int.toString j ^ s)
      | (Rational(a,b), Rational(c,d)) => Rational(a*d+b*c,b*d)
      | _ => raise BadResult "non-values passed to add_values"

这里的操作是 add_values,所以只要把所有可能的数据类型(总共9种)都列出来,就可以了。

面向对象编程:二次分派

按照上面面向对象编程的例子,我们可以这么做:

class Int < Value
    ...
  def add_values v
    if v.is_a? Int
      i + v.i
    elsif v.is_a? MyString
      i.to_s + v.i
    else
      ...
    end
  end
end

class MyString < Value
  ...
end

在 add_values 这个方法里面去做判断,看传入参数的类型,去做相应的操作。这种做法不是那么的 面向对象,可以有另外一种写法:

class Int < Value
    ...
  # double-dispatch for adding values
  def add_values v # first dispatch
    v.addInt self
  end
  def addInt v # second dispatch: other is Int
    Int.new(v.i + i)
  end
  def addString v # second dispatch: other is MyString (notice order flipped)
    MyString.new(v.s + i.to_s)
  end
  def addRational v # second dispatch: other is MyRational
    MyRational.new(v.i+v.j*i,v.j)
  end
end

class MyString < Value
  ...
  # double-dispatch for adding values
  def add_values v # first dispatch
    v.addString self
  end
  def addInt v # second dispatch: other is Int (notice order is flipped)
    MyString.new(v.i.to_s + s)
  end
  def addString v # second dispatch: other is MyString (notice order flipped)
    MyString.new(v.s + s)
  end
  def addRational v # second dispatch: other is MyRational (notice order flipped)
    MyString.new(v.i.to_s + "/" + v.j.to_s + s)
  end
end
...

这里涉及到了一个概念 二次分派 (Double Dispatch),在一次方法的调用过程中,做了两次 动态分派 (Dynamic Dispatch) 。用例子来说明

i = Int.new(1)
s = MyString.new("string")
i.add_values(s)

i.add_values(s)在调用这个方法时,实现了一次 dispatch,到 add_values 这个方法里后,做的其实是 s.addInt i,也就是去调用了 MyString 里的 addInt 这个方法,这是第二次 dispatch,所以叫做 double dispatch。

总结

函数式编程 和 面向对象编程 对比下来,我们并不能说哪一种模式更好。但是可以看出它们在思维上是截然不同的。函数式编程中侧重要做什么,面向对象编程侧重对象的抽象化,在有些编程语言里,比如 Java,是都可以实现的,但是要用哪种还要根据需求具体考虑。如果要了解更多 函数式编程 和 面向对象编程 的基础概念的话,可以看看之前的这三篇文章。

推荐阅读:
编程语言的一些基础概念(一):静态函数式编程
编程语言的一些基础概念(二):动态函数式编程
编程语言的一些基础概念(三):面向对象

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容