作者:Ole Begemann矮慕,原文鏈接观游,原文日期:2016-07-29
譯者:鐘穎昏滴;校對(duì):小鐵匠Linus昼窗;定稿:CMB
更新:
2016-08-02
為 Xcode 8 beta 4 更新代碼
本系列其他文章:
(2) 乘法和除法(本文)
(3) 內(nèi)容提煉
感謝 Chris Eidhof 和 Florian Kugler 幫助我想出這個(gè)解決方案蚯妇。
在上篇文章結(jié)束時(shí)敷燎,我就計(jì)劃實(shí)現(xiàn)一個(gè)通用的、聲明式的方案來(lái)描述物理量間依賴關(guān)系的解決方案箩言,例如 速度 = 長(zhǎng)度 / 時(shí)間∮补幔現(xiàn)在,讓我們來(lái)具體實(shí)現(xiàn)這個(gè)想法吧陨收。
方程的通用形式
目前澄成,不同的 Unit
類本身對(duì)其他類并沒有什么關(guān)聯(lián)。對(duì)類型系統(tǒng)而言畏吓,他們是獨(dú)立存在的實(shí)體[1]墨状。然而,在現(xiàn)實(shí)生活中菲饼,這些物理量之間是有聯(lián)系的肾砂,并且我們可以通過(guò)一些方程來(lái)描述他們之間的關(guān)系。上面已經(jīng)提到了一個(gè)例子宏悦,這里還有很多:
物理量之間的關(guān)系 |
---|
速度 = 長(zhǎng)度 / 時(shí)間 |
加速度 = 速度 / 時(shí)間 |
面積 = 長(zhǎng)度 × 長(zhǎng)度 |
體積 = 長(zhǎng)度 × 長(zhǎng)度 × 長(zhǎng)度 = 面積 × 長(zhǎng)度 |
電阻 = 電壓 / 電流 |
功 = 功率 × 時(shí)間 |
密度 = 質(zhì)量 / 體積 |
力 = 質(zhì)量 × 加速度 |
壓強(qiáng) = 力 / 面積 |
力矩 = 力 × 長(zhǎng)度 |
首先要注意的是镐确,我們可以把所有的方程都?xì)w納成 a = b × c 的形式包吝,而除法可以被重寫成乘法的形式:速度 = 長(zhǎng)度 / 時(shí)間 ? 長(zhǎng)度 = 速度 × 時(shí)間。
其次源葫,雖然有些方程超過(guò)了兩個(gè)因子诗越,我們總是可以通過(guò)一個(gè)中間量將其歸納成只有兩個(gè)因子的形式(見上述例子中的體積方程式)。
UnitProduct 協(xié)議
因此息堂,我們需要找到一個(gè)方式嚷狞,可以在類型系統(tǒng)中通過(guò)三個(gè) unit 類來(lái)描述 a = b × c 的關(guān)系,并且我們希望這個(gè)實(shí)現(xiàn)方式可以被任意類型所接受荣堰。所以我們定義一個(gè)叫做 UnitProduct
的協(xié)議床未,他有 Factor1
和 Factor2
兩個(gè)關(guān)聯(lián)類型。我們將其類型設(shè)定成 Dimension
振坚,這是所有維度單位的父類:
/// Describes the relation Self = Factor1 * Factor2.
protocol UnitProduct {
associatedtype Factor1: Dimension
associatedtype Factor2: Dimension
}
我們還需要什么呢薇搁?為了進(jìn)行計(jì)算,我們需要在計(jì)算時(shí)指定這些值需要轉(zhuǎn)換的實(shí)際 單位渡八。換句話說(shuō)啃洋,我們必須告訴類型系統(tǒng),米 除以 秒 將會(huì)產(chǎn)生一個(gè)結(jié)果屎鳍,他的單位是 米每秒宏娄,而不是 千米每小時(shí)。我們?cè)趨f(xié)議里添加一個(gè)叫做 defaultUnitMapping()
的靜態(tài)方法來(lái)實(shí)現(xiàn)這個(gè)目的哥艇。他會(huì)返回一個(gè)三元組绝编,分別表示上述的 a,b 和 c貌踏。
在理想的情況下十饥,我希望這個(gè)方法的返回值是 (Factor1, Factor2, Self)
,但是當(dāng)我們通過(guò)一個(gè)具體的類祖乳,例如 UnitLength
來(lái)實(shí)現(xiàn)這個(gè)協(xié)議的時(shí)候逗堵,使用 Self
會(huì)引起一些問(wèn)題。Unit 類不是一個(gè) final 類眷昆,意味著他可以被子類化蜒秤。如果我們通過(guò) UnitLength
來(lái)實(shí)現(xiàn)協(xié)議,并且使用 UnitLength
作為 Self
的返回值亚斋,這個(gè)實(shí)現(xiàn)會(huì)被其子類繼承作媚,但這并不符合我們的協(xié)議,因?yàn)?Self
指向了子類的類型帅刊。出于此纸泡,編譯器不允許我們這樣做。如果 UnitLength
是值類型或者 final 類的話赖瞒,那結(jié)果是不同的女揭。
作為一種可行的方案蚤假,我們可以引進(jìn)第三個(gè)關(guān)聯(lián)類型來(lái)表示乘法的結(jié)果。我不喜歡這個(gè)方案吧兔,因?yàn)樗鼜?qiáng)制協(xié)議的實(shí)現(xiàn)者來(lái)指定關(guān)聯(lián)類型磷仰。雖然這非常不直觀,卻是可行的境蔼,完整的協(xié)議將變成:
protocol UnitProduct {
associatedtype Factor1: Dimension
associatedtype Factor2: Dimension
associatedtype Product: Dimension // is always == Self
static func defaultUnitMapping() -> (Factor1, Factor2, Product)
}
這足夠在類型系統(tǒng)中表示他們之間的數(shù)學(xué)關(guān)系灶平,并且為計(jì)算提供了關(guān)于單位關(guān)系的足夠信息。
接下來(lái)欧穴,我們來(lái)寫一個(gè)具體的實(shí)現(xiàn)民逼。請(qǐng)記住泵殴,我們想實(shí)現(xiàn)的關(guān)系是 UnitLength = UnitSpeed × UnitDuration涮帘,所以我們讓 UnitLength
實(shí)現(xiàn)了協(xié)議:
/// 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)
}
}
我們通過(guò) typealias 明確的指定了關(guān)聯(lián)類型,但其實(shí)我們可以不用這么做笑诅。編譯器可以通過(guò) defaultUnitMapping()
方法推斷其類型调缨,我們返回的三元組 (.metersPerSecond, .seconds, .meters)
指出這些單位是有相關(guān)性的(將兩個(gè)相乘或者相除可以得到第三個(gè))。這些單位也恰好是它們各自類型中的基本單位吆你,但并非一定要這么做弦叶。我們可以選擇其他的值,類似 (.kilometersPerHour, .hours, .kilometers)
妇多,只要他們之間有相關(guān)性即可伤哺。
重載乘法操作符
在我們進(jìn)行計(jì)算之前,還有一個(gè)步驟要做者祖。我們需要實(shí)現(xiàn)協(xié)議的乘法操作符立莉。這個(gè)方法是這樣描述的,“這是一個(gè) UnitProduct
的 * 運(yùn)算符實(shí)現(xiàn)七问,他的左操作符是一個(gè) Measurement<Factor1>
蜓耻,右操作符是一個(gè) Measurement<Factor2>
,返回值是 Measurement<Product>
”:
/// 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)
}
方法的實(shí)現(xiàn)有三個(gè)步驟械巡。首先我們從協(xié)議中獲得單位的映射刹淌。然后我們將操作數(shù)轉(zhuǎn)換到各自的目標(biāo)單位并且把他們相乘。最后讥耗,我們將結(jié)果包裝成一個(gè) Measurement
值并返回有勾。讓我們來(lái)試一下:
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
成功了,真棒古程!有三個(gè)值得注意的地方:
- 目前蔼卡,類型檢查器還不能推斷出返回值的類型,所以我們必須明確的指定其為
Measurement<UnitLength>
類型籍琳,我不能非常確定這是為什么菲宴。我嘗試了 * 運(yùn)算符泛型參數(shù)的各種約束贷祈,但還是不能讓它正常工作。 - 計(jì)算結(jié)果的單位是米喝峦,這雖然沒什么大問(wèn)題势誊,但如果改成千米的話會(huì)更好,因?yàn)槲覀儌鬟M(jìn)去的單位分別是千米每小時(shí)和小時(shí)谣蠢,我們會(huì)在下篇文章中解決這個(gè)問(wèn)題粟耻。
- 計(jì)算結(jié)果會(huì)有一點(diǎn)舍入誤差,主要是由千米每小時(shí)到米每秒的轉(zhuǎn)換引起的眉踱。
讓乘法可交換
我們需要添加三個(gè)新的方法來(lái)達(dá)到這個(gè)目的挤忙,兩個(gè)除法重載和一個(gè)乘法重載。因?yàn)槌朔ㄊ?a target="_blank" rel="nofollow">可交換的谈喳,所以 時(shí)間 × 速度 與 速度 × 時(shí)間 應(yīng)該完全一致册烈。我們目前的方法不能滿足這一點(diǎn),但這也很好實(shí)現(xiàn)婿禽。只要再給 * 添加另一個(gè)重載方法赏僧,交換兩個(gè)參數(shù)即可。直接返回之前重載過(guò)乘法的結(jié)果:
/// 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
除法
同樣扭倾,對(duì)于除法而言我們需要重載 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, rightUnit, leftUnit) = UnitType.defaultUnitMapping()
let quantity = lhs.converted(to: leftUnit).value / rhs.converted(to: rightUnit).value
return Measurement(value: quantity, 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
有趣的是淀零,類型檢查對(duì)除法運(yùn)算是管用的。我猜測(cè)膛壹,除法運(yùn)算時(shí)其中的參數(shù)能直接追溯到其泛型參數(shù)的 UnitType
驾中,而在乘法運(yùn)算時(shí)其中的參數(shù)只是泛型參數(shù)的關(guān)聯(lián)類型。我嘗試在乘法運(yùn)算中將參數(shù) Factor1
和 Factor2
定義成完整的泛型參數(shù)模聋,但這并不管用肩民。如果你知道這是為什么的話請(qǐng)告訴我。
通過(guò) 5 行代碼實(shí)現(xiàn)協(xié)議
現(xiàn)在撬槽,描述三個(gè)物理量之間的關(guān)系此改,我們需要做的僅僅是實(shí)現(xiàn)這個(gè)協(xié)議,并且實(shí)現(xiàn)一個(gè)只有一行代碼的方法侄柔。這里拿電阻(UnitElectricResistance
)共啃,電壓(UnitElectricPotentialDifference
)和電流(UnitElectricCurrent
)來(lái)舉例:
/// 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 ?
結(jié)語(yǔ)
我覺得這非常酷暂题,而且向我們展示了強(qiáng)大的類型系統(tǒng)是如何幫助我們寫出正確代碼的移剪。一旦定義好了關(guān)系,編譯器就不允許我們進(jìn)行無(wú)意義的計(jì)算了(比如將分子和分母搞混)薪者。在類型推斷的過(guò)程中纵苛,編譯器甚至能告訴我們運(yùn)算結(jié)果的類型(請(qǐng)注意上面最后一個(gè)例子中,我們并沒有指定電阻
的類型)。
我同樣喜歡聲明式來(lái)描述關(guān)系的方式攻人。本質(zhì)上我們只需要告訴編譯器:“嘿取试,這就是這三個(gè)量之間的關(guān)系,剩下的事情你搞定”怀吻,然后所有的運(yùn)算和類型檢查都很完美地完成了瞬浓。
展望
在第三部分中,我會(huì)提出一個(gè)可能的解決方案蓬坡,它能夠在計(jì)算過(guò)程中保留單位(千米除以小時(shí)得到千米每小時(shí)猿棉,而不是米每秒),并討論當(dāng)前解決方案的另一個(gè)局限性屑咳。敬請(qǐng)關(guān)注萨赁!
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán)兆龙,最新文章請(qǐng)?jiān)L問(wèn) http://swift.gg杖爽。
-
他們有一個(gè)共同的父類,但在這個(gè)時(shí)候并不重要详瑞。 ?