点与复数

lua-users home
wiki

Lua 中的点与复数

重新定义用户定义类型的常规算术运算符的含义并不困难。通常情况下,这会是一个运行时错误;如果我有一个表 `t`,那么 `t + 1` 会给出 "attempt to perform arithmetic on global `t' (a table value)"。然而,如果该表有一个元表,那么 Lua 会检查是否定义了 `__add` 函数,并使用它来代替。

关于运算符重载的主要问题是,它是否会使其他程序员更容易使用您的对象。这类似于用户界面的决策;最好遵循您平台上的通用用户界面约定。在界面中,平淡无奇才是好的,而意想不到的会引起误解。例如,您可以让加法运算符连接列表,但正如 Paul Graham 所观察到的 [1],人们倾向于误读这些表达式为算术运算。

我将介绍两个重新定义运算符非常有意义的案例,因为它们都是我们可以使用的常规实数算术的推广。用 `p1 + p2` 来相加两个点不仅方便,而且在数学上也是正确的。

使用这个 `Point` 类,您将能够用类似于常规表示法的方式来表达向量代数。例如,

x = ((p1^p2)..q)*q
表示:取 `p1` 和 `p2` 的叉积,将其与 `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` 对象变得容易。为 x、y 和 z 分量使用索引 1、2 和 3 的决定是相当随意的;很容易更改以匹配您的偏好。有一些限制:虽然 `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 + z` 和 `z + 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

大卫,我不同意你的观点。任何学过一年级大学数学的人都会将 '^' 识别为三维向量的向量积,或者更普遍地将其识别为外积。'..' 用于内积很不理想,我同意。唉,数学记号的历史性怪癖确实不适合计算机。我所遇到过的最适合适应数学记号的编程语言是 Gofer(而不是 Hugs 或 Haskell 本身——标准化的前导符有问题,因为它是由数学背景不足的人设计的)。-- GavinWraith

我认为 '^'(或者更准确地说 \(\wedge\))不太常见。我和美国的一位同事从未见过它这样使用,但我问了一位在英国的人,他说在本科阶段 '^' 和 'x' 的使用频率大约各占一半,尽管在学习外积微积分后,他更喜欢用 'x' 来表示叉积,以区分这两个概念,因为它们在某种程度上是通过 Hodge 星算子相关的。这里还有另一个将 '^' 用作叉积的参考 [2]。在某些情况下,用户定义运算符会很有用,主要是能够用中缀表示法表达它们 [3]。另请参阅 CustomOperators。--DavidManura

一个潜在的混淆点是,Lua 中的 `^` 是右结合的,而叉积不是结合的。所以,`a^b^c` 将意味着 `a^(b^c)`。--DavidManura

我在一次不相关的搜索中偶然发现了这个页面,但看到这句话“用 `p1 + p2` 来相加两个点不仅方便,而且在数学上也是正确的。”几乎让我哭出来。相加两个点在几何上没有任何意义。粗略浏览了文章后,很清楚 'Point' 类也打算用于向量,不知何故?我不会在这里改变任何东西,但我认为至少应该指出这一点。--匿名

在给定的运算(例如,归一化和叉积)下,"向量" 可能更合适。然而,术语的互换很可能是因为点可以表示为[位置向量]。--DavidManura

RecentChanges · preferences
编辑 · 历史
最后编辑于 2014 年 4 月 29 日下午 2:17 GMT (diff)