將 Measurements 和 Units 應(yīng)用到物理學(xué)

作者:Ole Begemann矮慕,原文鏈接观游,原文日期:2016-07-29
譯者:鐘穎昏滴;校對(duì):小鐵匠Linus昼窗;定稿:CMB

更新:
2016-08-02
為 Xcode 8 beta 4 更新代碼

本系列其他文章:

(1) Measurements 和 Units 概覽

(2) 乘法和除法(本文)

(3) 內(nèi)容提煉

感謝 Chris EidhofFlorian 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é)議床未,他有 Factor1Factor2 兩個(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è)值得注意的地方:

  1. 目前蔼卡,類型檢查器還不能推斷出返回值的類型,所以我們必須明確的指定其為 Measurement<UnitLength> 類型籍琳,我不能非常確定這是為什么菲宴。我嘗試了 * 運(yùn)算符泛型參數(shù)的各種約束贷祈,但還是不能讓它正常工作。
  2. 計(jì)算結(jié)果的單位是米喝峦,這雖然沒什么大問(wèn)題势誊,但如果改成千米的話會(huì)更好,因?yàn)槲覀儌鬟M(jìn)去的單位分別是千米每小時(shí)和小時(shí)谣蠢,我們會(huì)在下篇文章中解決這個(gè)問(wèn)題粟耻。
  3. 計(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 / Factor1Product / 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ù) Factor1Factor2 定義成完整的泛型參數(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杖爽。


  1. 他們有一個(gè)共同的父類,但在這個(gè)時(shí)候并不重要详瑞。 ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末掂林,一起剝皮案震驚了整個(gè)濱河市臣缀,隨后出現(xiàn)的幾起案子坝橡,更是在濱河造成了極大的恐慌,老刑警劉巖精置,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件计寇,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡脂倦,警方通過(guò)查閱死者的電腦和手機(jī)番宁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)赖阻,“玉大人蝶押,你說(shuō)我怎么就攤上這事』鹋罚” “怎么了棋电?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)苇侵。 經(jīng)常有香客問(wèn)我赶盔,道長(zhǎng),這世上最難降的妖魔是什么榆浓? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任于未,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘烘浦。我一直安慰自己抖坪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布闷叉。 她就那樣靜靜地躺著柳击,像睡著了一般。 火紅的嫁衣襯著肌膚如雪片习。 梳的紋絲不亂的頭發(fā)上捌肴,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音藕咏,去河邊找鬼状知。 笑死,一個(gè)胖子當(dāng)著我的面吹牛孽查,可吹牛的內(nèi)容都是我干的饥悴。 我是一名探鬼主播,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼盲再,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼西设!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起答朋,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤贷揽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后梦碗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體禽绪,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年洪规,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了印屁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡斩例,死狀恐怖雄人,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情念赶,我是刑警寧澤础钠,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站晶乔,受9級(jí)特大地震影響珍坊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜正罢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一阵漏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦履怯、人聲如沸回还。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)柠硕。三九已至,卻和暖如春运提,著一層夾襖步出監(jiān)牢的瞬間蝗柔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工民泵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留癣丧,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓栈妆,卻偏偏與公主長(zhǎng)得像胁编,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鳞尔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容