位元詩人 [Nim] 語言程式教學:多型 (Polymorphism)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo 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 原先不支援泛型,但在後續版本中加入這項特性。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。