Nim 語言程式教學:多型 (Polymorphism)

PUBLISHED ON APR 3, 2018 — PROGRAMMING
FacebookTwitter LinkedIn LINE Skype EverNote GMail Email Email

    由於 Nim 既不支援多重繼承 (multiple inheritance) 也不支援介面 (interface),Nim 對於多型的支援相對薄弱。不支援多型的話,很多設計模式 (design patterns) 會難以實作,希望 Nim 以後可以改善這方面的議題。

    Duck Type

    Duck Type 是指專注在物件的外在行為上,不用過度地檢查物件實際的型別;動態型別語言,像是 Python 或 Ruby,可自動實現這個想法,靜態型別語言則要透過一些額外的手法。現階段,可以使用繼承來達成子類型 (subtyping) 的效果,如下例:

    type
      Animal* = ref object of RootObj
    
    method speak*(a: Animal) {.base.} =
      quit "Unimplemented"
    
    type
      Duck* = ref object of Animal
    
    method speak*(d: Duck) =
      echo "Pack pack"
    
    proc newDuck*(): Duck =
      new(result)
    
    type
      Dog* = ref object of Animal
    
    method speak*(d: Dog) =
      echo "Wow wow"
    
    proc newDog*(): Dog =
      new(result)
    
    type
      Tiger* = ref object of Animal
    
    method speak*(t: Tiger) =
      echo "Halum halum"
    
    proc newTiger*(): Tiger =
      new(result)
    
    when isMainModule:
      let animals: seq[Animal] = @[newDuck(), newDog(), newTiger()]
    
      for a in animals:
        a.speak
    

    表面上看起來,似乎可以達到 duck typing 的效果;但是,隨著物件變多,單一繼承往往就會不太夠用,而 Nim 目前沒有官方的介面的方案,這是目前 Nim 較為不足的地方。

    函式重載

    使用 method 即可達成函式重載 (funciton overloading) 或方法重載 (method overloading) 的效果,見上例。

    用 tuple 模擬介面

    單一繼承的程式語言,大部分都會用介面、mixin、trait 等替代機制補足需要多重繼承的需求,但 Nim 到目前為止缺乏這一塊,有一個非正式的方法是透過帶有方法宣告的 tuple 來模擬介面,官方手冊沒有記錄這一點,而出現在某篇國外的部落格文章。我們這裡將先前的例子改寫,加入介面的支援:

    import random
    
    # Some interface in Nim
    type
      IEmployee = tuple[
        salary: proc (): float 
      ]
    
    type
      Employee* = ref object
        s: float
    
    proc salary*(e: Employee): float =
      e.s
    
    proc `salary=`*(e: Employee, salary: float) =
      assert(salary >= 0.0)
      e.s = salary
    
    proc newEmployee*(salary: float): Employee =
      new(result)
      result.s = salary
    
    # Type conversion, from Employee to IEmployee.
    proc toEmployee*(e: Employee): IEmployee =
      return (
        salary: proc (): float =
          e.salary
      )
    
    type
      Programmer* = ref object
        plns: seq[string]
        pes: seq[string]
        pee: Employee
    
    proc salary*(p: Programmer): float =
      p.pee.salary
    
    proc `salary=`*(p: Programmer, salary: float) =
      assert(salary >= 0.0)
      p.pee.salary = salary
    
    proc langs*(p: Programmer): seq[string] =
      p.plns
    
    proc `langs=`*(p: Programmer, langs: seq[string]) =
      p.plns = langs
    
    proc editors*(p: Programmer): seq[string] =
      p.pes
    
    proc `editors=`*(p: Programmer, editors: seq[string]) =
      p.pes = editors
    
    proc solve*(p: Programmer, problem: string) =
      randomize()
    
      let ln = p.langs[random(p.langs.low..p.langs.len)]
      let e = p.editors[random(p.editors.low..p.editors.len)]
    
      echo "The programmer solved " & problem & " in " & ln & " with " & e
    
    proc newProgrammer*(langs: seq[string], editors: seq[string], salary: float): Programmer =
      new(result)
      result.plns = langs
      result.pes = editors
      result.pee = newEmployee(salary = salary)
    
    # Type conversion, from Programmer to IEmployee.
    proc toEmployee*(p: Programmer): IEmployee =
      return (
        salary: proc (): float =
          p.salary
      )
    
    # Main program.
    when isMainModule:
      let ee = newEmployee(
        salary = 500)
      let pr = newProgrammer(
        langs = @["Go", "Rust", "D", "Nim"],
        editors = @["Atom", "Sublime Text", "Visual Studio Code"],
        salary = 100)
    
      let es = @[ee.toEmployee, pr.toEmployee]
    
      for e in es:
        echo e.salary()
    

    從這裡可以看出來,目前在 Nim 語言中,要自己手動轉換成介面的型別;由於 Nim 的語法還沒到 1.0 版,日後有可能變動。

    運算子重載

    運算子重載 (operator overloading) 算是一種使用者自訂的語法糖,讓自訂類別看起來像內建類別,在一些情境中相當實用,像是實作新的數字型別等。Nim 對運算子重載支援良好,其實,Nim 的內建運算子也是用程序來實作,運算子重載則是由使用者實作某個運算子。以下範例實作數學的向量 (vector) 類別,在裡面重載了三個運算子,分別是陣列 getter、陣列 setter、加法運算子:

    type
      Vector = ref object
        arr: seq[float]
    
    proc len*(v: Vector): int =
      v.arr.len
    
    proc `[]`*(v: Vector, i: int): float =
      v.arr[i]
    
    proc `[]=`*(v: Vector, i: int, e: float) =
      v.arr[i] = e
    
    proc newVector*(args: varargs[float]): Vector =
      new(result)
      result.arr = @[]
      for e in args:
        result.arr.add(e)
    
    proc withSize*(s: int): Vector =
      new(result)
      result.arr = @[]
      for i in countup(1, s):
        result.arr.add(0.0)
    
    proc fromArray*(arr: openArray[float]): Vector =
      new(result)
      result.arr = @[]
      for e in arr:
        result.arr.add(e)
    
    proc `+`*(a: Vector, b: Vector): Vector =
      assert(a.len == b.len)
      result = withSize(a.len)
      for i in countup(0, a.len - 1):
        result[i] = a[i] + b[i]
    
    when isMainModule:
      let v1 = newVector(1.0, 2.0, 3.0)
      let v2 = fromArray(@[2.0, 3.0, 4.0])
    
      let v = v1 + v2
      assert(v[0] == 3.0)
      assert(v[1] == 5.0)
      assert(v[2] == 7.0)
    

    在最下方的主程式可看出,透過運算子重載,使得語法看起來更自然。運算子重載比較不是必備的特性,而算是語法上加分項目,許多語言支援運算子重載,但仍然有些現代語言不支援,像是 Java 或 Go 等。

    泛型

    泛型 (generics) 是一種重覆使用演算法的語法機制,可以將同一套程式碼套用在不同型別上,很常用於實作資料結構 (data structures) 或容器 (collections) 中;Nim 對泛型支援良好,且易於使用。以下範例將向量 (vector) 重新以泛型改寫:

    type
      Vector[T] = ref object
        arr: seq[T]
    
    proc len*[T](v: Vector[T]): int =
      v.arr.len
    
    proc `[]`*[T](v: Vector[T], i: int): T =
      v.arr[i]
    
    proc `[]=`*[T](v: Vector[T], i: int, e: T) =
      v.arr[i] = e
    
    proc newVector*[T](args: varargs[T]): Vector[T] =
      new(result)
      result.arr = @[]
      for e in args:
        result.arr.add(e)
    
    proc withSize*[T](s: int): Vector[T] =
      new(result)
      result.arr = @[]
      for i in countup(1, s):
        result.arr.add(T(0))
    
    proc fromArray*[T](arr: openArray[T]): Vector[T] =
      new(result)
      result.arr = @[]
      for e in arr:
        result.arr.add(e)
    
    proc `+`*[T](a: Vector[T], b: Vector[T]): Vector[T] =
      assert(a.len == b.len)
      result = withSize[T](a.len)
      for i in countup(0, a.len - 1):
        result[i] = a[i] + b[i]
    
    when isMainModule:
      let v1 = newVector[float](1.0, 2.0, 3.0)
      let v2 = fromArray[float](@[2.0, 3.0, 4.0])
    
      let v = v1 + v2
      assert(v[0] == 3.0)
      assert(v[1] == 5.0)
      assert(v[2] == 7.0)
    

    由於泛型相當實用,很多現代語言都加入此機制;像是 C# 和 Java 原先不支援泛型,但在後續版本中加入這項特性。