- 原文リンク: Swift の型チェック器における指数時間の複雑性
- 原文著者: 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 回の負の値の取得操作、そして 1 回の 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 を使用すると 4 秒 、 Swift 3 を使用すると 15 秒 かかります。コンパイルプロセス中に、型チェックに多くの時間がかかります。
今、あなたはそんなに時間がかかる問題に直面することはないかもしれませんが、大規模な 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
このコードでは、2 つの関数( f
とカスタム演算子 prefix %%
)を定義しています。各関数は、1 つのパラメータが (Float) -> Float
、もう 1 つが <I: Integer>(I) -> I
という 2 つのオーバーロードを持っています。
f(1)
を呼び出すと、 <I: Integer>(I) -> If(1)
の実装が選択され、 x
は Int
型として処理されます。これはあなたが期待する方法です。
%%1
を呼び出すと、 (Float) -> Float
の実装が使用され、 y
は Float
型として処理されます。これは私たちが期待することとは正反対です。コンパイルプロセス中に、コンパイラは 1
を Float
として処理することを選択しましたが、 Int
として処理しても正常に動作します。このような状況が発生する理由は、コンパイラがメソッドのジェネリックオーバーロードを行う前に変数の型を決定してしまうからです。これは前後の文脈の一貫性に基づくものではなく、コンパイラが expression was too complex to be solved
などのエラーメッセージやパフォーマンスの最適化を回避するための妥協です。
コード内での問題の解決#
一般的に、Swift の表示コードがあまりにも複雑であるという欠陥はそれほど大きな問題ではありません。もちろん、あなたが単一の式の中で以下の特性の 2 つ以上を使用しない限り:
- メソッドのオーバーロード(演算子のオーバーロードを含む)
- 定数
- 不明確な型のクロージャ
- 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
が使用されます
このシステムでは、型制約システムは 最小分離 原則に従います。分割された単位は、各単位が独立した値のセットを持つ個体であるというルールによって制約されています。上記の例では、実際には最小単位は 1 つだけです:上記の制約 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
型です
上記の例と比較して、ここでは 2 つの制約が追加されています。型制約システムがこの例をどのように処理するか見てみましょう。
第一歩:最小分離単位を選択します。今回は制約 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
型として宣言された実装よりも優先されます)。
型制約システムにおけるこの特殊な最適化は、私が 予測不可能な操作 の中で言及した迅速なオーバーロードの特性の一部です。
この特殊な「最適化」により、型制約システムは合理的な解決策を見つけるために 76 回のクエリを必要とします。もし私たちが他の新しいオーバーロードを追加した場合、この数字は想像を超えるものになるでしょう。例えば、例の中にもう一つの infix +
演算子を追加すると、 let a: Double = 0 + 1 + -(2)
のようになり、合理的な解決策を見つけるために 1190 回のクエリが必要になります。
解決策を見つけるこのプロセスは、典型的な指数時間の複雑性を持つ操作です。分離単位内での検索範囲は 「デカルト積」 と呼ばれ、グラフ内の n 個の分離単位に対して、アルゴリズムは n 次元のデカルト積の範囲内で検索を行います(これは空間の複雑性も指数的な操作です)。
私のテストによれば、単一の文内に 6 つの分離単位があるだけで、Swift で expression was too complex
エラーがトリガーされるのに十分です。
線形化された型制約システム#
この記事で繰り返し言及されているこの問題に対する最良の解決策は、コンパイラ内で修正を行うことです。
型制約システムがオーバーロードの問題を解決するために指数時間の複雑性を持つアルゴリズムを採用しているのは、Swift がオーバーロードによって生成された n 次元の 「デカルト積」 空間内の要素を探索し、適切な選択肢を決定する必要があるからです(より良い解決策がない限り、これは最良の解決策であるべきです)。
n 次元のデカルト積空間を生成しないようにするためには、関連するロジックの実装の独立性を実現し、それらが互いに依存しないように設計する必要があります。
始める前に、非常に重要な注意点をお伝えしなければなりません:
友情提醒、これらの内容は私の個人的な意見に過ぎません:次のいくつかの議論は、Swift の型制約システムで関数オーバーロードの問題を解決する方法を理論的な観点から分析したものです。私が提案した解決策を証明するために何かを書くことはありません。これは、私が非常に重要なことを見落とす可能性があることを意味します。
前提#
私たちは次の 2 つの目標を実現したいと考えています:
- ノードが他のノードと相互依存または参照しないように制限する
- 前のメソッドから分析された分離単位が、後のメソッドから分離されたものと交差し、さらに分離単位の 2 つの制約条件を簡素化する
最初の目標は、ノードの制約パスを制限することで実現できます。Swift では、各ノードの制約は双方向であり、各ノードの制約は式の各分岐から始まり、主幹を遍歴し、子ノードを線形に遍歴する方法で伝播します。このプロセスの中で、同じ制約ロジックを選択的に単純に統合してこれらの制約を組み合わせることができます。これにより、他のノードから対応する型制約を参照する必要がなくなります。
第二の目標では、前のメソッドによって減少した型制約の伝播の複雑さを減少させることで、関連する制約条件をさらに簡素化します。各オーバーロードメソッドの分離単位間の最も重要な交差点は、オーバーロード関数の出力であり、別のオーバーロード関数の入力として使用される可能性があります。この操作は、パラメータが相互に交差する 2 つのオーバーロードメソッドによって生成される 2 次元のデカルト積を計算する必要があります。他の可能性のある交差点については、真の意味での数学的な厳密な交差証明を提供することは非常に困難であり、そのような証明は必要ありません。私たちは、複雑な状況での型選択においてSwift が採用する貪欲な戦略をコピーするだけで済みます。
以前の例を再考しましょう#
以前に生成されたノードグラフがどのようになるかを見てみましょう:
let a: Double = 1 + -(2)
次に、ノードの制約を復習しましょう:
T1
はExpressibleByIntegerLiteral
型ですT3
はExpressibleByIntegerLiteral
型ですT2
はT3
を受け取りT4
を返すメソッドですT2
はprefix -
の 6 種類の実装の 1 つですT0
はT1
とT4
を受け取りT5
を返すメソッドですT0
はinfix +
の 6 種類の実装の 1 つです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 種類の実装の 1 つであり、右側から渡されるパラメータは 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 種類の実装の 1 つであり、Swift における特別な操作のオーバーロードが一般的なオーバーロードよりも優先されるという原則を満たすために、Double
、Float
またはFloat80
型のprefix -
オーバーロードが優先されます。T4
はprefix -
の 6 つの戻り値のいずれかであり、同様に Swift における特別な操作のオーバーロードが一般的なオーバーロードよりも優先されるという原則を満たします。T0
はT1
とT4
を受け取りT5
を返すメソッドですT0
はinfix +
の 6 種類の実装の 1 つであり、右側から渡されるパラメータはprefix -
の戻り値のいずれかであり、このプロセスでは Swift における特別な操作のオーバーロードが一般的なオーバーロードよりも優先されるという原則を満たします。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 の可能性を検索しましたが、私の分析によれば、最終的には ConstraintSystem::matchTypes
の呼び出しが 22 回しか発生しませんでした。
非公開のアルゴリズムを特定するには多くの推測が必要であり、特定のアルゴリズムの具体的な詳細を知ることは非常に困難です。しかし、私のアルゴリズムは任意の数量級の状況において、既存のアルゴリズムよりも優れた性能を発揮するか、少なくとも同等であることは不可能ではないと思います。
Swift はすぐにその型システムを改善するのでしょうか?#
私は「私一人で全ての仕事を終えました。これらのコードがどれほど完璧に動作するか見てください」と言いたいのですが、それは単なる夢に過ぎません。全体のシステムは何千もの論理とユニットで構成されており、特定のノードを単独で議論することはできません。
あなたは Swift 開発チームが型制約システムを線形化しようとしていると思いますか?私は否定的な見解を持っています。
この記事の中で 「[swift-dev] 型チェックのパフォーマンスケーススタディ」 は、公式の開発者が型制約システムが指数時間のアルゴリズムを採用することが正常であると考えていることを示しています。時間をアルゴリズムの最適化に費やすよりも、標準ライブラリを再構築してより合理的にする方が良いでしょう。
少し愚痴を言わせてください:
- 現在、この記事の前の 2 章は無駄な努力に過ぎないように思えます。私は静かにそれらを削除すべきです。
- 私は、型制約システムは大幅に改善されるべきだと考えています。そうすれば、私たちは上記の問題に悩まされることはなくなるでしょう。
友情提醒:理論的には、型制約システムは言語の最も重要な部分ではないため、改善が行われる場合は小さなバージョンの更新で行われるべきであり、大きなバージョンの更新ではありません。
結論#
私の Swift の使用経験の中で、 expression was too complex to be solved in reasonable time
は頻繁に発生するエラーであり、私はこれが単純なエラーだとは思いません。もしあなたが単一の例の中で多くのメソッドや数学的操作を使用している場合、定期的にこの記事を確認することをお勧めします。
Swift で採用されている指数時間のアルゴリズムは、コンパイル時間が長くなる問題を引き起こす可能性もあります。私はコンパイル全体の時間配分を正確に統計していませんが、予想通り、システムは型制約器の関連計算に大部分の時間を費やしているはずです。
この問題は、私たちが書くコードの中で回避することができますが、正直なところ、そうする必要はありません。コンパイラが私が提案した線形時間のアルゴリズムを採用できれば、これらの問題はもはや問題ではなくなると確信しています。
コンパイラが具体的な変更を行うまで、この記事で言及した問題は私たちを悩ませ続け、コンパイラとの闘いは続くでしょう。