Point And Complex

lua-users home
wiki

Lua 中的點和複數

重新定義使用者自訂類型的一般算術運算子意義一點都不難。這通常會造成執行時期錯誤;如果有個表格 t,則 t + 1 會傳回「嘗試對全域變數 t(表格值)執行算術運算」。不過,如果表格有 metatable 的話,Lua 會檢查是否有定義 __add 函數,有的話就用這個函數代替。

運算子重載最重要的問題在於它是否會讓其他程式設計師比較好使用你的物件。這跟使用者介面決策很像;對於你的平台來說,遵守一般使用者介面公約真的很重要。在介面中,單調無聊是好事,而驚喜會讓人誤解。例如,你可以讓加法運算子串接清單,但就像 Paul Graham 所見到的 [1],人們很容易誤讀這種算式為算術運算。

我將要展示兩種重新定義運算子非常有道理的狀況,因為它們都是我們所用的實數運算的概括。利用 p1 + p2 來加兩個點,這不只方便,在數學上也是正確的。

使用這個 Point 類別,你就能用類似我們平常用的表示法表逹向量代數了。舉例來說,

x = ((p1^p2)..q)*q
表示:取得 p1p2 的叉積,取得它和 q 的點積,然後用點積結果乘以 q。

-- point.lua
-- A class representing vectors in 3D
-- (for class.lua, see SimpleLuaClasses)
require 'class'

Point = class(function(pt,x,y,z)
   pt:set(x,y,z)
 end)

local function eq(x,y)
  return x == y
end

function Point.__eq(p1,p2)
  return eq(p1[1],p2[1]) and eq(p1[2],p2[2]) and eq(p1[3],p2[3])
end

function Point.get(p)
  return p[1],p[2],p[3]
end

-- vector addition is '+','-'
function Point.__add(p1,p2)
  return Point(p1[1]+p2[1], p1[2]+p2[2], p1[3]+p2[3])
end

function Point.__sub(p1,p2)
  return Point(p1[1]-p2[1], p1[2]-p2[2], p1[3]-p2[3])
end

-- unitary minus  (e.g in the expression f(-p))
function Point.__unm(p)
  return Point(-p[1], -p[2], -p[3])
end

-- scalar multiplication and division is '*' and '/' respectively
function Point.__mul(s,p)
  return Point( s*p[1], s*p[2], s*p[3] )
end

function Point.__div(p,s)
  return Point( p[1]/s, p[2]/s, p[3]/s )
end

-- dot product is '..'
function Point.__concat(p1,p2)
  return p1[1]*p2[1] + p1[2]*p2[2] + p1[3]*p2[3]
end

-- cross product is '^'
function Point.__pow(p1,p2)
   return Point(
     p1[2]*p2[3] - p1[3]*p2[2],
     p1[3]*p2[1] - p1[1]*p2[3],
     p1[1]*p2[2] - p1[2]*p2[1]
   )
end

function Point.normalize(p)
  local l = p:len()
  p[1] = p[1]/l
  p[2] = p[2]/l
  p[3] = p[3]/l
end

function Point.set(pt,x,y,z)
  if type(x) == 'table' and getmetatable(x) == Point then
     local po = x
     x = po[1]
     y = po[2]
     z = po[3]
  end
  pt[1] = x
  pt[2] = y
  pt[3] = z 
end

function Point.translate(pt,x,y,z)
   pt[1] = pt[1] + x
   pt[2] = pt[2] + y
   pt[3] = pt[3] + z 
end

function Point.__tostring(p)
  return string.format('(%f,%f,%f)',p[1],p[2],p[3])
end

local function sqr(x) return x*x end

function Point.len(p)
  return math.sqrt(sqr(p[1]) + sqr(p[2]) + sqr(p[3]))
end

Point 是個簡單的類別,可以使用呼叫符號來建構它(請參考 SimpleLuaClasses)。

> p1 = Point(10,20,30)
> p2 = Point(1,2,3)
> = p1
(10.000000,20.000000,30.000000)
> = p1 + p2
(11.000000,22.000000,33.000000)
> = 2*p1
(20.000000,40.000000,60.000000)
因為 Point 定義了 __tostring,所以 Lua 知道如何印出這種物件。它的格式可能不太完美,但要修改程式碼並不難(可以讓精確度成為一個類別屬性)。簡化的建構函數語法讓向量運算可以很容易地傳回 Point 物件。決定使用索引 1、2 和 3 代表 x、y 和 z 分量完全是任意的;可以輕易地改成符合你的喜好。有些限制:雖然 p*2 應該是有效的運算式,但它卻不是;純量必須先出現。

現在可以定義較高層級的運算。例如,這裡有個對尋找點和直線中間距離有用的函數(如果你正在作一個可編輯的繪圖程式的話,它會非常有用)

-- given a point q, where does the perp cross the line (p1,p2)?
function perp_to_line(p1,p2,q)
  local diff = p2 - p1
  local x = ((q - p1)..diff)/(diff..diff)
  return p1 + x*diff
end

-- minimum distance between q and a line (p1,p2)
function min_dist_to_line(p1,p2,q)
  local perp = perp_to_line(p1,p2,q)
  return Point.len(perp-q)
end

有一個「陷阱」你必須記住;Lua 物件總是按參照傳遞,所以小心不要修改傳遞給函數的點。使用提供的複製建構函數,例如說 local pc = Point(p) 來製作傳遞為參數的一個點的本機副本。

複數

複數是實數的擴充,它們包含所有常見的運算,再加上更多運算。如果 z 是複數,則 1.5 + zz + 1.5 都是複數表達式,因此 __add 必須處理其中一個參數為純數字的情況。

請注意,此複數類別為範例,不應將它用於嚴肅的應用程式。它有一些問題(在原則上可以修正):除法會有精確度損失,模數在某些合理值會溢位,平方根會隨意取捨,且對於實值無法運算,pow 只能運算正整數次方(而且很慢)。

-- complex.lua
require 'class'

Complex = class(function(c,re,im)
                 if type(re) == 'number' then 
                   c.re = re
                   c.im = im
                 else
                   c.re = re.re
                   c.im = re.im
                 end
          end)

Complex.i = Complex(0,1)

local sqrt = math.sqrt
local cos = math.cos
local sin = math.sin
local exp = math.exp

local function check(z1,z2)
  if     type(z1) == 'number' then return Complex(z1,0),z2
  elseif type(z2) == 'number' then return z1,Complex(z2,0) 
  else return z1,z2
  end
end

-- redefine arithmetic operators!
function Complex.__add(z1,z2)
  local c1,c2 = check(z1,z2)
  return Complex(c1.re + c2.re, c1.im + c2.im)
end

function Complex.__sub(z1,z2)
  local c1,c2 = check(z1,z2)
  return Complex(c1.re - c2.re, c1.im - c2.im)
end

function Complex:__unm()
  return Complex(-self.re, -self.im)
end

function Complex.__mul(z1,z2)
  local c1,c2 = check(z1,z2)
  return Complex(c1.re*c2.re - c1.im*c2.im, c1.im*c2.re + c1.re*c2.im)
end

function Complex.__div(z1,z2)
  local c1,c2 = check(z1,z2)
  local a = c1.re
  local b = c1.im
  local c = c2.re
  local d = c2.im
  local lensq = c*c + d*d
  local ci = (a*c + b*d)/lensq
  local cr = (b*c + a*d)/lensq
  return Complex(cr,ci)
end

function Complex.__pow(z,n)
  local res = Complex(z)
  for i = 1,n-1 do res = res*z end
  return res  
end  

-- this is how our complex numbers will present themselves!
function Complex:__tostring()
  return self.re..' + '..self.im..'i'
end

-- operations only valid for complex numbers
function Complex.conj(z)
  return Complex(z.re,-z.im)
end

function Complex.mod(z)
  return sqrt(z.re^2 + z.im^2)
end

-- generalizations of sqrt() and exp()
function Complex.sqrt(z)
  local y = sqrt((Complex.mod(z)-z.re)/2)
  local x = z.im/(2*y)
  return Complex(x,y)
end

function Complex.exp(z)
  return exp(z.re)*Complex(cos(z.im),sin(z.im))
end

然而,這樣算快嗎?

很明顯地,向量和複數算術在 Lua 中會很慢,但這只是相對而言。我可以在大約兩秒鐘內建立 100,000 個點;使用相同時間來加總這個數字。如果您的程式只需要處理數千個點,那這樣的速度已經夠快了。需要處理更多點(例如 GIS 系統)的程式,就算在 C++ 中也不會使用這種表示法。相反地,可以為點的陣列撰寫一個 Lua 擴充程式,並以群組的方式操作這些點。

註解

剛開始,我對於表達式「x = ((p1^p2)..q)*q」不太了解。當看見「^」和「..」時,會認為這是指數運算和串接,但這兩種運算都不適用於這裡。指數運算不適用於長度 >= 2 的向量,因此或許可以降低混淆的可能性,但它適用於方陣,而方陣與向量非常類似,且常在同一個表達式中使用,例如 A^(-1)Av。「..」也很奇怪,雖然我承認這兩個點有點像內積。Lua 的自動字串轉換可能會增加錯誤的機率——如果不小心寫成「(p1..p2)..3」,「..」會被靜默視為一般的字串串接。當然,可以將「*」了解為純量積、內積或外積。不幸的是,我們可以使用運算子的數量有限。這些問題會損及本文的原始論點,但我同意對向量使用「+」、「-」等運算子非常合理。 --DavidManura

David,我無法認同你的說法。任何曾學習到大學一年級數學的人,都應該能辨識出 3D 向量的「^」是向量積,或更廣泛地說,是外積。我同意將「..」用於內積很不幸。唉,數學符號的歷史怪癖真的不適合用於電腦。我用過最能適應數學符號的最佳程式語言是 Gofer(不是 Hugs 或 Haskell 本身——標準序曲搞砸了,因為它是由數學背景不足的人設計的)。 -- GavinWraith

我想「^」(或實際上是 \(\wedge\))比較不常見。我和美國的某個人都沒看過這種用法,但我問了英國的某個人,他說在大學部的「^」和「x」用量約各佔一半,不過學過外微分之後,他喜歡用「x」表示叉積,讓兩種觀念區分開來,因為它們本質上是透過霍吉對偶相關。以下是另一個將「^」用為叉積的參考資料 [2]。有些時候,有能夠自訂運算子的好處,主要是能夠以中綴表示法表示它們 [3]。另外請參閱 CustomOperators。--DavidManura

一個潛在的混淆點是,^ 在 Lua 中為右結合子,但叉積不是結合子。因此,a^b^c 會表示 a^(b^c)。--DavidManura

我在一次不相關的搜尋中意外看到這個頁面,但是看到這句「使用 p1 + p2 來加兩點不僅方便,而且在數學上是正確的」,害我差點哭出來。將兩點相加絕對沒有任何幾何意義。在瀏覽完文章後,很明顯地,「點」類別也有用於向量的目的,不知道是為什麼?我不會修改這裏的內容,但我想至少應該指出這一點。--匿名

根據給定的運算(例如標準化和叉積)來看,「向量」會來得更適合。不過,上述術語的互換可能是因為點可以用 [位置向量] 來表示。--DavidManura

RecentChanges · 偏好設定
編輯 · 歷史
最後編輯:2014 年 4 月 29 日下午 8:17 GMT (diff)