- 原文作者: Matt Gallagher
- 譯文出自: 掘金翻譯計劃
- 譯者: Zheaoli
- 校對者: geeeeeeeeek, Graning
這篇文章將圍繞曾不斷使我重寫代碼的一些 Swift 編譯器的報錯信息展開:
錯誤:你的表達式太過於複雜,請將其分解為一些更為簡單的表達式。(譯者注:原文是
error: expression was too complex to be solved in reasonable time; consider breaking up the expression into distinct sub-expressions
)
我會看那個觸發錯誤的例子,談談以後由相同底層問題引起以外的編譯錯誤的負面影響。我將會帶領你看看在編譯過程中發生了什麼,然後告訴你,怎樣在短時間內去解決這些報錯。
我將為編譯器設計一種時間複雜度為線性算法來代替原本的指數算法來徹底的解決這個問題,而不需要採用其餘更複雜的方法。
正確代碼的編譯錯誤#
如果你嘗試在 Swift 3 中編譯這段代碼,那麼將會產生報錯信息:
let a: Double = -(1 + 2) + -(3 + 4) + 5
這段代碼無論從哪方面來講都是合法且正確的代碼,從理論上講,在編譯過程中,這段代碼將會被優化成一個固定的值。
但是這段代碼在編譯過程中沒有辦法通過 Swift 的類型檢查。編譯器會告訴你這段代碼太複雜了。但是,等等,這段代碼看起來一點都不複雜不是麼。裡面包含 5 個變量, 4 次加法操作, 2 次取負值操作和一次強制轉換為 Double
類型的操作。
但是,編譯器你怎麼能說這段僅包含 12 個元素的語句相當複雜呢?
這裡有非常多的表達式在編譯的時候會出現同樣的問題。大多數表達式包含一些變量,基礎的數據操作,可能還有一些重載之類的操作。接下來的表達式在編譯時會面對同樣的錯誤信息:
let b = String(1) + String(2) + String(3) + String(4)
let c = 1 * sqrt(2.0) * 3 * 4 * 5 * 6 * 7
let d = ["1" + "2"].reduce("3") { "4" + String($0) + String($1) }
let e: [(Double) -> String] = [
{ v in String(v + v) + "1" },
{ v in String(-v) } + "2",
{ v in String(Int(v)) + "3" }
]
上面的代碼都是符合 Swift 語法及編程規則的,但是在編譯過程中,它們都沒有辦法通過類型檢查。
需要較長的編譯時間#
編譯報錯只是 Swift 類型檢查器缺陷帶來的副作用之一,比如,你可以試試下面這個例子:
let x = { String("\($0)" + "") + String("\($0)" + "") }(0)
這段代碼編譯時不會報錯,但是在我的電腦上,使用 Swift 2.3 將花費 4s 的時間,如果是使用 Swift 3 將會花費 15s 時間。編譯過程中,將會花費大量的時間在類型檢查上。
現在,你可能不会遇到太多需要耗費這麼多時間的問題,但是一個大型的 Swift 項目中,你將會遇到很多 expression was too complex to be solved in reasonable time
這樣的報錯信息。
不可預知的操作#
接下來,我將講一點 Swift 類型檢查器的特性:類型檢查器選擇盡可能的解決非泛型重載的問題。 編譯器中處理這種特定行為的路徑下的代碼註釋對此給出了解釋,這是一種避免性能問題的優化手段,用於優化造成 expression was too complex
報錯的性能問題。
接下來是一些具體的例子:
let x = -(1)
這段代碼將會編譯失敗,我們會得到一個 Ambiguous use of operator ‘-‘
的報錯信息。
這段代碼並不算很模糊,編譯器將會明白我們想要使用一個整數類型的變量,它將會把 1
作為一個 Int
進行處理,同時從標準庫中選擇如下的重載方式:
prefix public func -<T : SignedNumber>(x: T) -> T
然而,Swift 只能進行非泛型重載。在這個例子中,Float
、 Double
、 Float80
類型的實現並不完善,編譯器無法根據上下文選擇使用哪種實現,從而導致了這個報錯信息。
某些特定的優化可以對操作符進行優化,但是可能導致如下的一些問題:
func f(_ x: Float) -> Float { return x }
func f<I: Integer>(_ x: I) -> I { return x }
let x = f(1)
prefix operator %% {}
prefix func %%(_ x: Float) -> Float { return x }
prefix func %%<I: Integer>(_ x: I) -> I { return x }
let y = %%1
在這段代碼裡,我們定義了兩個函數( f
和一個自定的操作 prefix %%
)。每個函數都進行了兩次重載,一個參數為 (Float) -> Float
,另一個是 <I: Integer>(I) -> I
。
當調用 f(1)
的時候,將會選擇使用 <I: Integer>(I) -> If(1)
的實現,然後 x
將會作為 Int
類型進行處理。這應該是你所期待的方式。
當調用 %%1
時,將會使用 (Float) -> Float
的實現,同時會將 y
作為 Float
類型處理,這和我們所期望的恰恰相反。在編譯過程中,編譯器選擇將 1
作為 Float
處理,而不是作為 Int
處理,雖然作為 Int
處理也同樣能正常工作。造成這樣情況的原因是,編譯器在對方法的進行泛型重載之前就已經先行確定變量的類型。這不是基於前後文一致性的做法,這是編譯器對於避免類似於 expression was too complex to be solved
等報錯信息以及性能優化上的一種妥協。
在代碼中解決上述問題#
通常來講,Swift 裡的顯示代碼太過複雜的缺陷並不是一個太大的問題,當然前提是你不會在單個表達式裡使用兩個或兩個以上的下面列出的特性:
- 方法重載(包括操作符重載)
- 常量
- 不明確類型的閉包
- 會引導 Swift 進行錯誤類型轉換的表達式
一般而言,如果你不使用如上面所述的特性,那麼你一般不會遇到類似於 expression was too complex
的報錯信息。然而,如果選擇是用了上面所訴的特性,那麼你可能會面臨一些讓你感到困惑的問題。通常,在編寫一個足夠大小的方法和其餘常規代碼的時候,將會很容易用到上面這些特性,這意味著有些時候我們可能要仔細考慮怎樣避免大量使用上面這些特性。
你肯定是想只通過一點細微的修改來通過編譯,而不是大量修改你的代碼。接下來的一點小建議可能幫得上一些忙。
當上面所訴的編譯報錯信息出現時,編譯器可能建議你將原表達式分割成不同的子表達式:
let x_1: Double = -(1 + 2)
let x_2: Double = -(3 + 4)
let x: Double = x_1 + x_2 + 5
好了,從結果上來看,這樣的修改是有效的,但是卻讓人有點蛋疼,特別是在分解成子表達式的時候會明顯破壞代碼可讀性。
另一個建議是通過顯示類型轉換,減少編譯器在編譯過程中對方法和操作符重載的選取次數。
let x: Double = -(1 + 2) as Double + -(3 + 4) as Double + 5
上面這種做法避免了在使用 (Float) -> Float
或者是 (Float80) -> Float80
編譯器需要去查找相對應的負號重載。這樣的做法很有效的將編譯過程中編譯器的 6 次查找相對應的方法重載過程降至 4 次。
在上面的處理方式中有一個點要注意一下:不同於其餘語言,在 Swift 中 Double(x)
並不等同於 x as Double
。構造函數通常會如同普通方法一樣,當有不同參數的重載需求時,編譯器還是會將構造函數的各種重載加入到搜索空間中(儘管這些重載可能在代碼中的不同的位置)。在前面所舉的例子裡,通過在括號前用 Double
進行顯示類型轉換會解決一部分問題(這種方法有利於編譯器進行類型檢查),同時在一些情況下,採用這種方法會導致出現一些其餘的問題(請參見本文開始所舉的關於 String
的例子)。最終, 使用as
操作符是在不增加複雜度的情況下解決這類問題的最好方式。幸運的是,as
操作符的優先級比大多數二元運算符更高,這樣我們可以在大多數的情況下使用它。
另一種方法是使用一個獨立命名的自定義函數:
let x: Double = myCustomDoubleNegation(1 + 2) + myCustomDoubleNegation(3 + 4) + 5
這種方法可以解決之前方法重載所帶來的一系列問題。然而,在一系列輕量級的代碼裡使用這種方式會讓我們的代碼顯得格外的醜陋。
好了,讓我們來說說最後的方法,在很多情況下,你可以根據情況自行替換方法和操作符:
let x: Double = (1 + 2).negated() + (3 + 4).negated() + 5
因為在使用對應方法時,和使用常見算數運算符相比,會有效的減少重載次數,同時使用 .
操作符時其效率相較於直接調用方法更高,因此,這種方法能有效解決我們前面所提到的問題。
Swift類型約束系統簡析#
編譯時出現的 expression was too complex
錯誤是由 Swift 編譯器的語義分析系統所拋出的。語義分析系統的意義在於解決整個代碼裡的類型問題,從而確保輸入表達式的類型是正確且安全的。
最重要的是,整個報錯信息是由the constraints system solver (CSSolver.cpp)裡所編寫的語義分析系統所定義的。類型約束系統將從 Swift 的表達式裡構建一個由類型和方法組成的圖,並根據節點之間的關係來對代碼進行約束。約束系統將對每個節點進行推算直至每個節點都已獲得明確的類型約束。
講真,上面的東西可能太抽象了,讓我們看點具體的例子吧。
let a = 1 + 2
類型約束系統將表達式解析成下面這樣:
每個節點的名字都以 T
開頭(意味著需要待確定明確的類型),然後它們用來代表需要解決的類型約束或者方法重載。在這個圖裡,這些節點被如下的規則所約束:
T1
是ExpressibleByIntegerLiteral
類型T2
是ExpressibleByIntegerLiteral
類型T0
是一個傳入(T1,T2)
返回T3
的方法T0
是infix +
,其在 Swift 裡有 28 種實現T3
與T4
之間可以進行交換
小貼士:在 Swift 2.X 中,
ExpressibleByIntegerLiteral
的替代者是IntegerLiteralConvertible
在這個系統中,類型約束系統遵循著 最小分離 原則。分割出來的單元被這樣一個規則所約束著,即,每個單元都是一個擁有一套獨立值的個體。在上面的這個例子裡,實際上只有一個最小單元:在上述的約束 4 裡,T0
發生了重載。在重載之時,編譯器選擇了 infix +
實現列表裡第一種實現:即簽名是 (Int, Int) -> Int
的實現。
通過上述這個最小的單元,類型約束系統開始對元素進行類型約束:根據約束 3 T1
、 T2
、 T3
被確定為 Int
類型,根據約束 4 , T4
同樣被確定為 Int
類型。
在 T1
、 T2
被確定為 Int
之後(最開始它們被認為是 ExpressibleByIntegerLiteral
), infix +
的重載方式便已經確定,這個時候編譯器便不需要再考慮其餘可行性,並把其當做最終的解決方案。我們在確定每個節點對應的類型後,我們便可以選擇我們所需要的重載方法了。
讓我們看點複雜的例子吧!#
到目前為止,並沒有什麼超出我們意料之中的異常出現,你可能想象不到當表達式開始變得複雜之時, Swift 的編譯系統將會開始不斷的出現錯誤信息。來讓我們修改下上面的例子:第一・將 2
放在括號裡,第二・添加負號操作符,第三・規定返回值為 Double
類型。
let a: Double = 1 + -(2)
整個節點結構如下圖所述:
節點約束如下:
T1
是ExpressibleByIntegerLiteral
類型T3
是ExpressibleByIntegerLiteral
類型T2
是一個傳入T3
返回T4
的方法T2
是prefix -
,其在 Swift 裡有 6 種實現T0
是一個傳入T1
、T4
,返回T5
的方法T0
是infix +
,其在 Swift 裡有 28 種實現T5
是Double
類型
相較於上面的例子,這裡多了兩個約束,讓我們看看類型約束系統會怎樣處理這個例子。
第一步:選擇最小分離單元。這次是約束 4 :“ T2
是 prefix -
,在 Swift 裡有 6 種實現”。最後系統選擇了簽名為 (Float) -> Float
的實現。
第二步:和第一步一樣,選擇最小分離單元,這次是約束 6 :“T0
是 infix +
,其在 Swift 裡有 28 種實現”。系統選擇了簽名為 (Int, Int) -> Int
的實現。
最後一步是:利用上述的類型約束確定所有節點的類型。
然而,這裡出現了點問題:在第一步裡我們選擇的簽名為 (Float) -> Float
的 prefix -
實現和第二步裡我們選擇的簽名為 (Int, Int) -> Int
的 infix +
實現和我們的約束 5 (T0
是一個傳入 T1
、T4
,返回 T5
的方法)發生了衝突。解決方法是放棄當前的選擇,然後重新回滾至第二步,為 T0
最終,系統將遍歷所有的 infix +
實現,然後發現沒有一種實現同時滿足約束 5 和約束 7 (T5
是 Double
類型)。
所以,類型約束系統將回滾至第一步,為 T2
選取了簽名為 (Double, Double) -> Double
的實現。最後,這種實現也滿足了 T0
的約束。
然而,在發現 Double
類型和 ExpressibleByIntegerLiteral
相互不匹配後,類型約束系統將繼續回滾,尋找合適的重載方法。
T2
總共有 6 種實現,但是最後 3 種實現不能被優化 (因為它們是通用的實現,因此優先級高於顯示聲明參數為 Double
的實現)。
在類型約束系統裡,這種特殊優化是我曾經在Unexpected behaviors一文中提到的快速重載的一些特性。
拜這種特殊的 “優化” 所賜,類型約束系統需要 76 次查詢才能找到一個合理的解決方案。如果我們添加了其餘的一些新的重載,那麼這個數字會變得超出我們的想象。例如,我們在例子裡添加另外一個 infix +
操作符,比如: let a: Double = 0 + 1 + -(2)
,那麼將需要 1190 次查詢才能找到合理的解決方案。
查詢解決方案的這個過程是一個典型的具有指數時間複雜度的操作。在分離單元裡進行搜索的範圍稱為“笛卡爾積”,然後,對於圖中的 n 個分離單元,算法將會在 n 維笛卡爾乘積的範圍內進行查找(這是一個空間複雜度同樣為指數的操作)。
根據我的測試,單語句內擁有 6 個分離單元,便足以觸發 Swift 中的 expression was too complex
的錯誤。
線性化的類型約束系統#
針對本文所反復提到的這個問題,最好的解決方法就是在編譯器中進行修復。
類型約束系統之所以採用時間複雜度為指數算法來解決方法重載的問題,是因為 Swift 需要對方法重載所生成的 n 維“笛卡爾乘積”空間裡的元素進行遍歷並搜索從而確定一個合適的選項(在沒有更好方案之前,這應該是最好的方案)。
為了避免生成 n 維笛卡爾乘積空間,我們需要設計一個方法來實現相關邏輯實現的獨立性,而不讓它們彼此依賴。
在開始之前我必須給你們一個很重要的提醒:
友情提醒,這些東西僅代表我的個人觀點:接下來的一些討論,都是我從理論的角度上來分析怎樣在 Swift 的類型約束系統中怎樣去解決函數重載的問題。我並沒有寫一些東西來證明我提出的解決方案,這可能意味著我會忽略某些非常重要的東西。
前提#
我們想實現如下兩個目標:
- 限制一個節點不應該與其餘節點相互依賴或引用
- 從前一個方法分析出來的分離單元應該與後一個方法分離出來的存在著交集,並進一步簡化分離單元的兩個約束條件。
第一個目標,可以通過限制節點的約束路徑實現。在 Swift 中,每個節點的約束是雙向的,每個節點的約束都從表達式的每一個分支開始,然後依照著遍歷主幹 -> 線性遍歷子節點的方式不斷傳播。在這個過程中,我們可以有選擇性的簡單地合併相同的約束邏輯來組合這些約束,而不是從其餘節點引用相對應的類型約束。
第二個目標裡,支持前面通過減少類型約束的傳播複雜度來進一步簡化相關約束條件。每個重載方法的分離單元之間最重要的交叉點是一個重載函數的輸出,可能會作為另一個重載函數的輸入。這個操作應該根據參數相互交叉的兩個重載方法所產生的 2 維笛卡爾積來進行計算。對於其餘的可能存在的交叉點來說,給出一個真正意義上的數學上的嚴格交叉證明是非常困難的,同時這樣的證明是沒有必要的,我們只需要複製 Swift 裡在複雜情況下的對於類型選擇的時所采用的貪婪策略即可。
讓我們重新看看之前的例子#
讓我們看看如果我們實現了前文所講的兩個目標後,類型約束系統將會變成什麼樣子。首先讓我們複習下之前所生成的節點圖:
let a: Double = 1 + -(2)
然後讓我們也複習下以下節點約束:
T1
是ExpressibleByIntegerLiteral
類型T3
是ExpressibleByIntegerLiteral
類型T2
是一個傳入T3
返回T4
的方法T2
是prefix -
的 6 種實現之一,同時為了滿足在 Swift 中特殊操作重載優先級高於通用運算重載的原則,類型為Double
、Float
或者Float80
的prefix -
重載優先被考慮。T4
是prefix -
的六種返回值之一,同樣為了滿足在 Swift 中特殊操作重載優先級高於通用運算重載的原則,類型為Double
、Float
或者Float80
的prefix -
重載優先被考慮。T0
是一個傳入T1
、T4
,返回T5
的方法T0
是infix +
的 6 種實現之一,同時從右側傳入的參數是來自prefix -
返回參數中的任意一種,在這個過程中為了滿足在 Swift 中特殊操作重載優先級高於通用運算重載的原則,類型是Double
、Float
或者Float80
的參數優先被考慮T5
是Double
類型
將節點約束從右至左傳遞#
我們從右至左進行遍歷(從葉子節點向主幹遍歷)。
在節點約束從 T3
向 T2
傳播時,添加了這樣一個新的約束:“ T2
節點的輸入值必須是一個由 ExpressibleByIntegerLiteral
轉化而來的值”。現在在新的約束規則和原有規則同時發生作用後,一旦我們確認所有擁有 T2
的節點都被新規則約束成功之後,或者是與 “特定操作重載優先於通用操作重載(比如在 prefix -
中 Double
、 Float
或者是 Float80
會被優先重載)” 這條規則衝突之時,便可以丟棄我們新建立的節點約束規則。在節點約束從 T2
向 T4
中傳播的過程中,添加新約束為:“ T4
必須是 prefix -
所返回的 6 中類型的值之一,其中 Double
、Float
或 Float80
優先被考慮)。在節點約束從 T4
朝 T0
傳播的過程中,添加新約束為:“ T0
的第二個參數必須是從 prefix -
返回的 6 種參數裡的任意一種演變而來,其中 Double
、 Float
或 Float80
類型優先)。在結合 T0
已有的節點約束後,T0
的節點約束變為:“ T0
是 infix +
的 6 種實現之一,同時從右側傳入的參數是來自 prefix -
返回參數中的任意一種,在這個過程中類型是 Double
、 Float
或者 Float80
的參數優先被考慮)。在節點約束從 T1
朝 T0
傳遞之時,沒有新的約束條件需要添加(在這裡,T0
已經被我們所增加的約束條件嚴格約束了,同時,原本所使用的 ExpressibleByIntegerLiteral
類型已經被 Double
、 Float
或者 Float80
中的任意一種類型所替代了)。在節點約束從 T0
向 T5
傳播時,需新增加約束為:“ T5
是 infix +
的 6 種返回值中的一種,且 infix +
的第二個參數是來自 prefix -
的返回值,在這個過程中,Double
、 Float
或者 Float80
類型優先被考慮)。在上述約束的共同作用下,我們可以最終確認 T5
的類型為 Double
。
經過上述過程的變動之後,整個節點約束集迭代成下面這個樣子:
T1
是ExpressibleByIntegerLiteral
類型T3
是ExpressibleByIntegerLiteral
類型T2
是一個傳入T3
返回T4
的方法T2
是prefix -
的 6 種實現之一,同時為了滿足在 Swift 中特殊操作重載優先級高於通用運算重載的原則,類型為Double
、Float
或者Float80
的prefix -
重載優先被考慮。T4
是prefix -
的六種返回值之一,同樣為了滿足在 Swift 中特殊操作重載優先級高於通用運算重載的原則,類型為Double
、Float
或者Float80
的prefix -
重載優先被考慮。T0
是一個傳入T1
、T4
,返回T5
的方法T0
是infix +
的 6 種實現之一,同時從右側傳入的參數是來自prefix -
返回參數中的任意一種,在這個過程中為了滿足在 Swift 中特殊操作重載優先級高於通用運算重載的原則,類型是Double
、Float
或者Float80
的參數優先被考慮T5
是Double
類型
將節點約束從左至右傳遞#
現在我們開始從左至右進行遍歷(先遍歷主幹,後遍歷葉子節點)。
首先從 T5
開始遍歷,約束 5 是:“ T5
是 Double
類型的節點”。這時我們為 T0
添加新的約束:“ T0
的返回值類型一定要是 Double
類型的”。在這個約束生效後,我們就可以排除除 (Double, Double) -> Double
之外的 infix +
的重載了。節點約束繼續從 T0
朝 T1
傳遞,根據 infix +
的(Double, Double) -> Double
重載的參數要求,我們為 T1
創建一個新的約束: T1
一定是 Double
類型的。在多種約束的作用下,之前所提到的 “T1
是 ExpressibleByIntegerLiteral
類型” 變為 “T1
是 Double
類型”。在節點約束從 T0
朝 T4
,根據 infix +
的第二個參數的要求,我們確定 T4
的類型為 Double
。節點約束從 T4
朝 T2
傳播的過程中,我們新增加一個約束:“ T2
的返回值一定為 Double
類型”。在以上規則共同作用下,我們可以確定 T2
為 prefix -
的參數類型為 (Double) -> Double
重載。最後根據以上的約束,我們可以得知 'T3' 的類型為 'Double'。
最後整個類型約束系統編程下面這個樣子:
T1
為Double
類型。T3
為Double
類型。`T2
是prefix -
的參數為(Double) -> Double
類型的重載T0
是infix +
的參數為(Double, Double) -> Double
類型的重載T5
為Double
類型。
好了,現在整個類型約束操作便已經告一段落了。
唔,我提出這算法的目的是改善方法重載的相關操作,因此,我將方法重載的次數用 n 表示。然後我將平均每個函數重載次數用 m 表示。
如我前面所述,在 Swift 中,編譯器是通過在一個 n 維的笛卡爾積空間內進行搜索來確定最終的結果。它的時間複雜度是 O(m^n) 。
而我所提出的算法,是在一個 2 維的空間內去搜索 n-1 個分離單元來實現的。其執行時間是 m^2*n. 因為 m 是和 n 相關聯的,我們可以得到其最終的時間複雜度為 O(n) 。
通常來講,在 n 為很大的時候,線性複雜度的算法比指數時間複雜度的算法更能適應當前的狀況,不過我們得搞清楚什麼樣的情況才能被稱之為 n 為很大的數。在這個例子中,3 已經是個非常 “大” 的數了。正如我前面所提到的一樣,Swift 自帶的類型約束系統將進行 1190 次搜索來確認最後的結果。而我設計的算法只需要 336 次搜索。這可以說很明顯的降低了最後的耗時。
我做了一個很有趣的實驗:在之前所提到的 let a: Double = 1 + -(2)
這個例子裡,不管是 Swift 裡的類型約束系統,還是我所設計的算法,它們都是在一個 2 維的笛卡爾積空間內進行搜索,裡面都包含了 168 中可能性。
Swift 里現在所采用的類型約束算法選取了在 prefix -
和 infix +
重載生成的 2 維笛卡爾積空間內的 168 種可能性的 76 種。但是這樣做的話,整個過程裡會產生 567 次對 ConstraintSystem::matchTypes
的調用,其中 546 次是用於搜索相適應的重載函數。
我所設計的算法,搜索了全部 168 種可能性,但是根據我的分析,其最後只產生了 22 次對 ConstraintSystem::matchTypes
的調用。
去確定一個非公開的算法,需要進行很多次的猜測,所以知道某一種算法的的具體細節是一件非常困難的事兒。但是我想,我的算法在任意數量級的情況下,其表現優於或與現在已有的算法持平並不是一件不可能的事兒。
Swfit 很快會改進他的類型系統麼?#
雖然我很想說:“我一個人就把所有工作做完了,看看這些代碼運行的多麼完美啊”,但是這也只能是想想罷了。整個系統由成千上萬的邏輯和單元組成,並不能單獨抽出某一個節點來進行討論。
你覺得 Swift 開發團隊是不是在嘗試把類型約束系統進行線性化處理呢?我對此持否定看法。
在這篇文章裡“[swift-dev] A type-checking performance case study”表明官方開發者認為類型約束系統採用時間複雜度為指數的算法是一件很正常的事兒。與其將時間放在優化算法上,還不如去重構標準庫,使其更為合理。
一點吐槽:
- 現在看來本文的前面兩章簡直就是在做無用功,我應該靜靜的將其刪除。
- 我覺得我想法是正確的,類型約束系統應該進行大幅度改進,這樣我們次啊不會被上面所提到的問題所困擾。
友情提醒:理論上將類型約束系統並不是整個語言最主要的一部分,因此如果其進行了改進,應該是在一個小版本迭代中進行發布,而不是一個大版本更新。
結論#
在我使用 Swift 的經歷裡,expression was too complex to be solved in reasonable time
是一個經常出線的錯誤,而且我並不認為這是一個簡單的錯誤。如果你在單個例子中是用了大量的方法或者是數學操作的時候,你應該定期看看這篇文章。
Swift 里所采用的時間複雜度為指數的算法也可能導致編譯時間較長的問題。 儘管我沒有確切的統計整個編譯裡的時間分配,但是不出意外的話,系統應該將大部分時間放在了類型約束器的相關計算上。
這個問題可以再我們編寫的代碼的時候予以避免,但是講真,沒有必要這麼做。如果編譯器能採用我所提出的線性時間的算法的話,我敢肯定,這些問題都不在是問題。
在編譯器做出具體的改變之前,本文所提到的問題會一直困擾著我們,與編譯器的鬥爭還要持續下去。