iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 29
0
Software Development

30天 Lua重拾筆記系列 第 29

【30天Lua重拾筆記28】進階議題: Meta Programming

同步發表於個人網站

Meta Programming / 元程式設計

元程式設計(英語:Metaprogramming),又譯超程式設計,是指某類電腦程式的編寫,這類電腦程式編寫或者操縱其它程式(或者自身)作為它們的資料,或者在執行時完成部分本應在編譯時完成的工作。多數情況下,與手工編寫全部代碼相比,程式設計師可以獲得更高的工作效率,或者給與程式更大的靈活度去處理新的情形而無需重新編譯。 -- 維基百科

簡單說,元程式設計,就是讓「程式能夠編寫程式」,改變程式運行的部份行為。Lua本身具有部份如此的能力,舉例來說,如果想要建立一組變數A-Z,或許正常會這樣子寫:

A = 1
B = 2
C = 3
-- ....
Z = 26

但Lua可以有更聰明(hacking)的寫法:

for i=65, 65+25 do  -- ASCII Code of A is 65
  print(string.char(i))
  _ENV[string.char(i)] = i - 64 -- 1 to 26
end

還記得_ENV的作用嗎?

不過今個兒沒打算討論這麼廣泛的的元程式設計。會更專注於Lua本身提供的另一個特殊物件 -- metatable / 元表。

metatable / 元表

metatable是Lua對於table一個特殊的屬性。每個table都可以設定另一個table作為其metatablemetatable可以設定/改變該table的部份行為。

setmetatable

MetaPyStr = {}
PyStr = {"Hello, "}

setmetatable(PyStr, MetaPyStr)

getmetatable

要取的table的元表也非常簡單:

meta = getmetatable(PyStr)

改變table行為

metatable是用於描述table相關的物件,其可以設定/改變該table的部份行為。舉例來說,正常的table使用tostring()或是print()得到的結果可能不那麼好理解:

print(PyStr)
--> table: 0x555de2908880

print(tostring(PyStr))
--> table: 0x555de2908880

可以透過設定元表的__tostring()來改變輸出樣子:

MetaPyStr = {
  __tostring = function(PyStr)
    return '<PyStr "' .. PyStr[1] .. '">'
  end
}

setmetatable(PyStr, MetaPyStr)

print(PyStr)
--> <PyStr "Hello, ">

跟Python很像對吧!

如果已經保有metatable的存取,其實可以直接改寫特性:

function MetaPyStr:__tostring()
  return self[1]
end


print(PyStr)
--> "Hello, "

在之後還會看到弱表,其使用了__mode去描述table的模式。

運算子多載

現在,想要實現Python字串的部份行為:

"Hello, " + "World" # => "Hello, World"
"Hello, " * 3 # => 'Hello, Hello, Hello, '

如果需要實現上面Python字串的行為,需要去改寫加法運算(__add)和乘法運算(__mul)

不過在此之前,需要先改寫一下PyStr,讓其是一個產生PyStr物件的函數。而PyStr物件是一個table,其第一個值為初始化時輸入的字串。

function PyStr(str)
  assert(type(str) == 'string',
         'str must is a string')
  local _PyStr = {str}

  setmetatable(_PyStr, MetaPyStr)
  return _PyStr
end

print(PyStr("Hello, "))
--> Hello, 

現在可以來正式實現加法運算與乘法運算:

function MetaPyStr:__add(other --[[PyStr]])
  assert(type(other) == "table")
  assert(type(other[1] == 'string'))

  return self[1] .. other[1]
end

function MetaPyStr:__mul(times --[[integer]])
  times =math.tointeger(times)
  assert(times)  -- check times is integer

  local new_PyStr = {self[1]}
  setmetatable(new_PyStr, MetaPyStr) --  set new_PyStr is Pystr

  for _=2,times do
    new_PyStr[1] = new_PyStr[1] .. self[1]
  end

  return new_PyStr
end


hello = PyStr("Hello, ")
world = PyStr("World")

print(hello + world)
--> Hello, World

print(hello * 3)
--> Hello, Hello, Hello, 

可以讓MetaPyStr本身也可以建立PyStr物件:

function MetaPyStr:new(str --[[string]])
  assert(type(str) == 'string'
         , 'str must is a string')
  local _PyStr = {str}
  setmetatable(_PyStr, self)
  return _PyStr
end


hello = MetaPyStr:new("Hello, ")
print(hello)
--> Hello,

MetaPyStr:new像不像Ruby的用法?
Ruby使用initialize去初始話物件,使用class.new去建立實體。
Lua也可以實現類似設計。有興趣嘗試模擬一下吧!


附帶一題,
function MetaPyStr:new這樣的語法糖,直接為MetaPyStr添加新的方法,是不是也很像Ruby的開放式類別(Open Classes)。

其他運算子

在此列出部份其他運算子的名稱,你可以隨意註冊於metatable,改變物件行為:

  • __add
    加法運算(+)
  • __sub
    剪法運算(-)
  • __mul
    乘法運算(**)
  • __div
    除法運算(/)
  • __idiv
    整數除法運算(//)
  • __mod
    取模運算(%)
  • __eq
    相等運算(==)
  • __concat
    連接運算(..)

上一篇
【30天Lua重拾筆記27】進階議題: debug
下一篇
【30天Lua重拾筆記30】進階議題: 與C交互(+Python)
系列文
30天 Lua重拾筆記36

尚未有邦友留言

立即登入留言