点和复数 |
|
为用户定义类型重新定义常用算术运算符并不困难。通常情况下,这会导致运行时错误;如果我有一个表 t
,那么 t + 1
会给出“尝试对全局 t
(一个表值)执行算术运算”。但是,如果该表有一个元表,那么 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
对象。使用索引 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 + 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 扩展,并以组的形式操作它们。
A^(-1)Av
。“..”也很奇怪,虽然我承认两个点有点像点积。Lua 的自动字符串转换可能会增加出错的可能性——如果错误地写成“(p1..p2)..3”,那么“..”将被默默地视为普通的字符串连接。“*”当然可以理解为标量积、点积或叉积。不幸的是,我们必须使用的运算符数量有限。这些问题会削弱文章的原始论点,但我同意“+”、“-”等对向量来说很有意义。--DavidManura
我不同意你的观点,David。任何学过大学一年级数学的人都会认识到“^”是三维向量的向量积,或者更一般地说是外积。“..”用于内积是不幸的,我同意。唉,数学符号的历史怪癖确实不适合计算机。我遇到的最适合容纳数学符号的编程语言是 Gofer(不是 Hugs 或 Haskell 本身——标准化的序言搞砸了,因为它是由没有足够数学背景的人设计的)。-- GavinWraith
^
是右结合的,但叉积不是结合的。因此,a^b^c
将意味着 a^(b^c)
。--DavidManura
p1 + p2
添加两个点不仅方便,而且在数学上是正确的”这句话,几乎让我哭出来。在几何意义上,添加两个点绝对没有意义。浏览完这篇文章后,很明显“点”类也打算用于向量,出于某种原因?我无权更改任何内容,但我认为至少应该指出这一点。--匿名