- 原文リンク: 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 %%
)を定義しています。各関数は 2 回オーバーロードされており、1 つは (Float) -> Float
、もう 1 つは <I: Integer>(I) -> I
です。
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 が採用している貪欲な戦略を単にコピーすればよいのです。
以前の例を再考してみましょう#
以前の 2 つの目標を達成した場合、型制約システムはどのようになるか見てみましょう。まず、以前生成されたノードグラフを復習しましょう:
let a: Double = 1 + -(2)
次に、ノードの制約を復習します:
T1
はExpressibleByIntegerLiteral
型T3
はExpressibleByIntegerLiteral
型T2
はT3
を受け取りT4
を返すメソッドT2
はprefix -
の 6 種類の実装の 1 つであり、Swift ではDouble
、Float
またはFloat80
の優先オーバーロードが考慮されます。T4
はprefix -
の 6 種類の戻り値の 1 つであり、同様に Swift ではDouble
、Float
またはFloat80
の優先オーバーロードが考慮されます。T0
はT1
とT4
を受け取りT5
を返すメソッドT0
はinfix +
の 6 種類の実装の 1 つであり、右側からの引数はprefix -
の戻り値のいずれかであり、Swift ではDouble
、Float
またはFloat80
の優先オーバーロードが考慮されます。T5
はDouble
型
ノード制約を右から左に伝播させる#
私たちは右から左に探索を行います(葉ノードから主幹を探索します)。
ノード制約が T3
から T2
に伝播する際に、新しい制約が追加されます:「T2
ノードの入力値は ExpressibleByIntegerLiteral
から変換された値でなければなりません」。新しい制約ルールと元のルールが同時に作用する後、T2
を持つすべてのノードが新しいルールで成功裏に制約されることが確認されるか、または「特定のオペレータのオーバーロードは一般的なオーバーロードよりも優先される(例えば、prefix -
では Double
、Float
または Float80
が優先されます)」というルールと衝突する場合、新しく作成されたノード制約ルールを放棄できます。ノード制約が T2
から T4
に伝播する際に、新しい制約が追加されます:「T4
は prefix -
の 6 種類の戻り値の 1 つであり、その中で 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 種類の戻り値の 1 つであり、infix +
の第二引数は prefix -
の戻り値のいずれかであり、その中で Double
、Float
または Float80
が優先されます」。上記の制約の共同作用により、最終的に T5
の型を Double
と確認できます。
このプロセスの変化の後、全体のノード制約セットは次のようになります:
T1
はExpressibleByIntegerLiteral
型T3
はExpressibleByIntegerLiteral
型T2
はT3
を受け取りT4
を返すメソッドT2
はprefix -
の 6 種類の実装の 1 つであり、Swift ではDouble
、Float
またはFloat80
の優先オーバーロードが考慮されます。T4
はprefix -
の 6 種類の戻り値の 1 つであり、同様に Swift ではDouble
、Float
またはFloat80
の優先オーバーロードが考慮されます。T0
はT1
とT4
を受け取りT5
を返すメソッドT0
はinfix +
の 6 種類の実装の 1 つであり、右側からの引数は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
の呼び出ししか発生しません。
非公開のアルゴリズムを特定するには多くの推測が必要であり、特定のアルゴリズムの具体的な詳細を知ることは非常に困難です。しかし、私のアルゴリズムは、任意の数量級の状況において、現在のアルゴリズムよりも優れているか、または同等であることは不可能ではないと思います。
Swift はすぐにその型システムを改善するのか?#
私は「私一人ですべての作業を終えました。これらのコードがどれほど完璧に動作するか見てください」と言いたいのですが、それは単なる夢に過ぎません。全体のシステムは数千の論理とユニットで構成されており、特定のノードを単独で議論することはできません。
あなたは Swift 開発チームが型制約システムを線形化しようとしていると思いますか?私は否定的です。
この記事の中で 「[swift-dev] 型チェックのパフォーマンスケーススタディ」 では、公式の開発者が型制約システムが指数時間複雑度のアルゴリズムを採用することは正常であると考えていることが示されています。アルゴリズムの最適化に時間をかけるよりも、標準ライブラリを再構築してより合理的にする方が良いでしょう。
少し愚痴を言わせてください:
- 現在、この記事の最初の 2 章は無駄な努力のように思えます。私は静かにそれを削除すべきです。
- 私は自分の考えが正しいと思います。型制約システムは大幅に改善されるべきであり、そうすれば私たちは上記の問題に悩まされることはなくなるでしょう。
友情提醒:理論的には、型制約システムは言語の最も重要な部分ではないため、改善が行われる場合は小さなバージョンの更新で行われるべきであり、大きなバージョンの更新ではありません。
結論#
私の Swift の使用経験の中で、expression was too complex to be solved in reasonable time
は頻繁に発生するエラーであり、私はこれが単純なエラーだとは思いません。もしあなたが単一の例で大量のメソッドや数学的操作を使用している場合、定期的にこの記事を確認するべきです。
Swift で採用されている指数時間複雑度のアルゴリズムは、コンパイル時間が長くなる問題を引き起こす可能性もあります。私はコンパイル全体の時間配分を正確に統計していませんが、間違いなくシステムは型制約器に関連する計算に大部分の時間を費やしているはずです。
この問題は、私たちが書くコードの中で回避することができますが、正直なところ、そうする必要はありません。もしコンパイラが私が提案した線形時間のアルゴリズムを採用できれば、これらの問題はもはや問題ではなくなるでしょう。
コンパイラが具体的な変更を行うまで、この記事で言及された問題は私たちを悩ませ続け、コンパイラとの闘いは続くでしょう。