将 Measurements 和 Units 应用到物理学
更新: 本系列其他文章: (1) Measurements 和 Units 概览 (2) 乘法和除法(本文) (3) 内容提炼 感谢 Chris Eidhof 和 Florian Kugler 帮助我想出这个解决方案。 在上篇文章结束时,我就计划实现一个通用的、声明式的方案来描述物理量间依赖关系的解决方案,例如 速度 = 长度 / 时间。现在,让我们来具体实现这个想法吧。 方程的通用形式目前,不同的
首先要注意的是,我们可以把所有的方程都归纳成 a = b × c 的形式,而除法可以被重写成乘法的形式:速度 = 长度 / 时间 ? 长度 = 速度 × 时间。 其次,虽然有些方程超过了两个因子,我们总是可以通过一个中间量将其归纳成只有两个因子的形式(见上述例子中的体积方程式)。 UnitProduct 协议因此,我们需要找到一个方式,可以在类型系统中通过三个 unit 类来描述 a = b × c 的关系,并且我们希望这个实现方式可以被任意类型所接受。所以我们定义一个叫做 /// Describes the relation Self = Factor1 * Factor2. protocol UnitProduct { associatedtype Factor1: Dimension associatedtype Factor2: Dimension } 我们还需要什么呢?为了进行计算,我们需要在计算时指定这些值需要转换的实际 单位。换句话说,我们必须告诉类型系统,米 除以 秒 将会产生一个结果,他的单位是 米每秒,而不是 千米每小时。我们在协议里添加一个叫做 在理想的情况下,我希望这个方法的返回值是 作为一种可行的方案,我们可以引进第三个关联类型来表示乘法的结果。我不喜欢这个方案,因为它强制协议的实现者来指定关联类型。虽然这非常不直观,却是可行的,完整的协议将变成: protocol UnitProduct { associatedtype Factor1: Dimension associatedtype Factor2: Dimension associatedtype Product: Dimension // is always == Self static func defaultUnitMapping() -> (Factor1,Product) } 这足够在类型系统中表示他们之间的数学关系,并且为计算提供了关于单位关系的足够信息。 接下来,我们来写一个具体的实现。请记住,我们想实现的关系是 UnitLength = UnitSpeed × UnitDuration,所以我们让 /// UnitLength = UnitSpeed * UnitDuration /// ? UnitSpeed = UnitLength / UnitDuration extension UnitLength: UnitProduct { typealias Factor1 = UnitSpeed typealias Factor2 = UnitDuration typealias Product = UnitLength static func defaultUnitMapping() -> (UnitSpeed,UnitDuration,UnitLength) { return (.metersPerSecond,.seconds,.meters) } } 我们通过 typealias 明确的指定了关联类型,但其实我们可以不用这么做。编译器可以通过 重载乘法操作符在我们进行计算之前,还有一个步骤要做。我们需要实现协议的乘法操作符。这个方法是这样描述的,“这是一个 /// UnitProduct.Product = Factor1 * Factor2 func * <UnitType: UnitProduct> (lhs: Measurement<UnitType.Factor1>,rhs: Measurement<UnitType.Factor2>) -> Measurement<UnitType> where UnitType: Dimension,UnitType == UnitType.Product { let (leftUnit,rightUnit,resultUnit) = UnitType.defaultUnitMapping() let quantity = lhs.converted(to: leftUnit).value * rhs.converted(to: rightUnit).value return Measurement(value: quantity,unit: resultUnit) } 方法的实现有三个步骤。首先我们从协议中获得单位的映射。然后我们将操作数转换到各自的目标单位并且把他们相乘。最后,我们将结果包装成一个 let speed = Measurement(value: 20,unit: UnitSpeed.kilometersPerHour) // → 20.0 km/h let time = Measurement(value: 2,unit: UnitDuration.hours) // → 2.0 hr let distance: Measurement<UnitLength> = speed * time // → 40000.032 m 成功了,真棒!有三个值得注意的地方:
让乘法可交换我们需要添加三个新的方法来达到这个目的,两个除法重载和一个乘法重载。因为乘法是可交换的,所以 时间 × 速度 与 速度 × 时间 应该完全一致。我们目前的方法不能满足这一点,但这也很好实现。只要再给 * 添加另一个重载方法,交换两个参数即可。直接返回之前重载过乘法的结果: /// UnitProduct.Product = Factor2 * Factor1 func * <UnitType: UnitProduct>(lhs: Measurement<UnitType.Factor2>,rhs: Measurement<UnitType.Factor1>) -> Measurement<UnitType> where UnitType: Dimension,UnitType == UnitType.Product { return rhs * lhs } let distance2: Measurement<UnitLength> = time * speed // → 40000.032 m 除法同样,对于除法而言我们需要重载 Product / Factor1 和 Product / Factor2 两种情况: /// UnitProduct / Factor1 = Factor2 func / <UnitType: UnitProduct>(lhs: Measurement<UnitType>,rhs: Measurement<UnitType.Factor1>) -> Measurement<UnitType.Factor2> where UnitType: Dimension,UnitType == UnitType.Product { let (rightUnit,resultUnit,leftUnit) = UnitType.defaultUnitMapping() let quantity = lhs.converted(to: leftUnit).value / rhs.converted(to: rightUnit).value return Measurement(value: quantity,unit: resultUnit) } /// UnitProduct / Factor2 = Factor1 func / <UnitType: UnitProduct>(lhs: Measurement<UnitType>,rhs: Measurement<UnitType.Factor2>) -> Measurement<UnitType.Factor1> where UnitType: Dimension,UnitType == UnitType.Product { let (resultUnit,unit: resultUnit) } let timeReversed = distance / speed // → 7200.0 s timeReversed.converted(to: .hours) // → 2.0 hr let speedReversed = distance / time // → 5.55556 m/s speedReversed.converted(to: .kilometersPerHour) // → 20.0 km/h 有趣的是,类型检查对除法运算是管用的。我猜测,除法运算时其中的参数能直接追溯到其泛型参数的 通过 5 行代码实现协议现在,描述三个物理量之间的关系,我们需要做的仅仅是实现这个协议,并且实现一个只有一行代码的方法。这里拿电阻( /// UnitElectricPotentialDifference = UnitElectricResistance * UnitElectricCurrent extension UnitElectricPotentialDifference: UnitProduct { static func defaultUnitMapping() -> (UnitElectricResistance,UnitElectricCurrent,UnitElectricPotentialDifference) { return (.ohms,.amperes,.volts) } } 这样使用: let voltage = Measurement(value: 5,unit: UnitElectricPotentialDifference.volts) // → 5.0 V let current = Measurement(value: 500,unit: UnitElectricCurrent.milliamperes) // → 500.0 mA let resistance = voltage / current // → 10.0 ? 结语我觉得这非常酷,而且向我们展示了强大的类型系统是如何帮助我们写出正确代码的。一旦定义好了关系,编译器就不允许我们进行无意义的计算了(比如将分子和分母搞混)。在类型推断的过程中,编译器甚至能告诉我们运算结果的类型(请注意上面最后一个例子中,我们并没有指定 我同样喜欢声明式来描述关系的方式。本质上我们只需要告诉编译器:“嘿,这就是这三个量之间的关系,剩下的事情你搞定”,然后所有的运算和类型检查都很完美地完成了。 展望在第三部分中,我会提出一个可能的解决方案,它能够在计算过程中保留单位(千米除以小时得到千米每小时,而不是米每秒),并讨论当前解决方案的另一个局限性。敬请关注!
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |