协议编程,第1部分:2D空间。
在假期期间,我最终有很多空闲时间,没有电话也没有电脑,只能随身携带iPad。 从这个实验中,我学到了两件事:
- 如果没有在计算机上编程,我将无法花几天的时间
- 使用Apple的Playground应用程序编写和编译Swift很高兴
因为我已经很长时间没有写Swift了,所以我认为这是一个很好的实验,看看自从他们引入条件一致性以来我能走多远。
动机来自尝试编写线性代数库的尝试。 这朝着另一个方向发展,但是结果是一组有用的协议,这些协议封装了复数(ℂ),2d向量(ℝ²)和其他线性空间的行为。
这篇博客文章中有一个Swift游乐场的副本,您可以在这里从iPad或Mac下载。 我强烈建议您尝试一下,因为您将能够运行此处示例所示的代码,并进行一些小的调整和更改。
常用操作
向量空间定义一个字段 ,一个字段具有相当大的一组属性(可交换性等)。 不幸的是,Swift的类型系统无法检查这些属性。 相反,我们将专注于我们需要的几个属性,而Swift允许我们实现并假设它们将是足够的。
我们将使用的属性是:
- 将两件事加在一起,并具有要添加的标识值。
- 将两件事相乘,并具有用于相乘的标识值。
- 二维对象的抽象。
- 将二维对象与标量相乘。
- 计算二维对象的大小。
我们的目标是,一旦我们将所有这些机器编织在一起,将我们的功能添加到类型中就将变得非常容易。
让我们开始吧。
多重复制
乘法协议在两个相同类型的值之间定义一个函数*
,并返回相同类型。 它还具有一个标识值,例如a * id = a
。 正如我们将看到的,对于Int
和Double
,标识值分别为1
和1.0
但对于更复杂的类型则不是这种情况。
鉴于此,我们可以为Int
类型实现乘法。
由于Int
类型已经具有适合协议定义的*
运算符,因此我们只需要定义multId
值。
添加剂
加性协议与加性标识的值和加法运算符+
相似。
并将其实现为Int
:
二维
为了简化我们的未来工作,我们将抽象一些类型具有二维的事实。 这样,他们将需要能够返回每个组件并根据两个组件值创建自己。
该协议将使我们能够为具有二维的类型定义默认实现。
我们没有符合该协议的任何类型,但我们已经可以为其实现Additive
。 的确,任何类型都是可Additive
只要它具有两个维并且其成分也是可Additive
。
至于addId
,由于Additive
是通过将每个组件加在一起来实现的,我们可以通过创建一个具有每个组件标识值的2D对象来对其进行定义。
如我们所见,加法标识不是1
而是一个使用其组件类型的标识值的值。
大小
值的大小是一种表示其“大小”的方式。 通常,对于2D矢量,幅度是矢量的长度。 对于Int
这是它们的绝对值。 该值对于比较两个不同的值很有用,即使所有值之间不一定总顺序相同。
为了描述具有大小的类型,我们需要说它具有返回某种类型的值的属性,并且该值必须是Comparable
。
由于幅度通常需要平方根运算,但不是很常规的运算,因此我们将假定magSquare
返回幅度的平方。
由于我们知道如何从一般意义上计算值的大小(给定带有投影x
和y
的值v
,该大小的平方由vx * vx + vy * vy
),因此任何类型都可以提供以下大小:
- 它有两个维度。
- 组件是乘法的。
- 组分是添加剂。
- 分量的类型与计算幅度的结果相同。
标量
标量协议应允许使用另一个值来“缩放”一个值。 为此,我们将添加一个新的运算符以区分正则乘法和标量乘法。 可以在alt-shift-v
Apple平台上找到◊
运算符。
该协议还可以针对任何提供的类型自动实现:
- 它有两个维度。
- 组件是乘法的。
- 组件的类型与比例因子的类型相同。
现在我们有了所有想要的协议,我们可以开始实现具体类型了。
2D向量
2D向量的定义非常简单。 这是一个结构化的参数,用于说明其每个组件的类型。
然后,我们可以实现TwoDimensions
并省略TwoDimensions
和snd
因为它们已经由struct实现。
最后,我们可以轻松地(无事可做)为Vector
实现Additive
, Magnitude
和Scalar
,并在混合中添加Equatable
。
由于尚不清楚如何将两个2D向量相乘,因此没有Multiplicative
实现。 (是乘积还是乘积?乘以组件?)
现在我们可以使用+
, magSquare
和◊
计算向量的大小有点尴尬,让我们看看如何改进它。
用平方根提高幅度
让我们为具有平方根的类型添加一个新协议:
该协议使用前缀运算符√
,可以在Apple平台上的alt-v
找到它。
现在,只要MagVal
具有平方根,我们就可以扩展“ Magnitude
以提供magnitude
场。
我们现在可以尝试:
但是我们得到一个错误。
“类型’Int’不符合协议
SquareRoot
“
实际上,我们尚未为Int
添加SquareRoot
扩展。 但这是因为没有简单的方法来计算整数的平方根(我们是否假定它是双精度并向下取整?向上取整?截断?)
相反,我们将为Double
所有必要的协议,以使向量在为Doubles
向量时支持magnitude
函数
我们现在可以写
并获得1.414213…
符合预期。 现在我们可以在使用Int
时处理Double
和magSquare
Vector
时使用magnitude
。
复数
为什么在这里停下来? 我们已经有了所有这些协议,因此让我们将它们与新类型一起使用。 让我们实现复数。
如我们所见, Multiplicative
appart没有真正的工作要做来实现那些协议。
这使我们无需任何额外工作即可编写以下内容。
线性功能
我们可以将其带入另一个层次并实现线性函数。 线性函数是形式为f(x) = a * x + b
像以前一样,这使我们可以编写
如我们所见,由于线性函数的大小语义不清楚(至少对我而言),因此我们省略了magnitude
。 我们还省略了Multiplicative
因为将两个线性函数相乘会得到二次函数。 这是一个问题,因为我们对Multiplicative
的定义期望返回与其参数相同类型的值。
花式乘法
我们可以通过引入另一个协议FancyMult
来解决乘法问题,该协议将相同类型的两个值相乘并返回另一个类型的值。
我们可以使用它来实现两个线性函数之间的乘积:
甚至是两个向量的点积:
这使我们可以写出身份(a²x² - b²) = (ax + b) * (ax - b)
权力插曲
出于娱乐目的,这里是基于Multiplicative
运算的幂运算符的实现
这使我们可以写
结合一切
现在,我们可以结合一切并使其按预期工作。 例如,我们可以有复杂的向量:
我们甚至可以拥有复杂向量的线性函数:
这确实证明了协议和条件一致性的强大功能。 它们使我们能够以非常灵活的方式重用我们的代码并组合不同的结构,而不必担心将来的代码将如何实现的细节。
结论
我们面向协议的方法完美地封装了DRY的概念。 我们只定义一次相关的实现,其余的遵循我们程序的结构。 这使得代码非常容易扩展,并使每个组件相对简单。 不幸的是,我们的示例仅允许二维类型。 在下一部分中,我们将看到如何使用协议来模拟依赖类型并具有任意大小的向量。