函數式和面向對象編程有什么區別?

函數式編程 (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,是都可以實現的,但是要用哪種還要根據需求具體考慮。如果要了解更多 函數式編程 和 面向對象編程 的基礎概念的話,可以看看之前的這三篇文章。

推薦閱讀:
編程語言的一些基礎概念(一):靜態函數式編程
編程語言的一些基礎概念(二):動態函數式編程
編程語言的一些基礎概念(三):面向對象

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容