typeパラメーターが指定されていないのにScalaがボトムタイプを推測するのはなぜですか?

Nov 23 2020

以下のこの特定のケースでの推論ルールを誰かが説明できるかどうか疑問に思います、そして最も重要なのはそれが合理的/含意ですか?

case class E[A, B](a: A) // class E
E(2) // E[Int,Nothing] = E(2)

私が書いE[Int](2)た可能性があることに注意してください。私にとって重要なのは、たとえば、2番目のパラメーター型がNothing(つまり、ボトム型)であると推測されるのはなぜAnyですか?それはなぜですか、そして合理的/含意は何ですか?

コンテキストを説明するために、これは、Eitherの定義と、LeftおよびRightでどのように機能するかに関連しています。どちらもパターンに従って定義されています

final case class X[+A, +B](value: A) extends Either[A, B]

あなたがそれをインスタンス化するところは、Right[Int](2)と言うと、推論されたタイプはRight[Nothing, Int]、ひいてはEither[Nothing, Int]

EDIT1

ここには一貫性がありますが、それでも合理性を理解することができます。以下は、反変パラメーターを使用した同じ定義です。

case class E[A, -B](a: A)// class E
E(2) // E[Int, Any] = E(2)

したがって、それが反変であり、すべての動作または推論規則を首尾一貫したものにする場合、逆に同じことがあります。しかし、これの合理性はわかりません...。

なぜ反対のルール、つまりAny共変/不変のNothing場合と反変性の場合を推測しないのですか?

EDIT2

理にかなっている@sloucAnswerに照らして、コンパイラが何を、なぜ実行しているのかを理解する必要があります。以下の例は私の混乱を示しています

val myleft = Left("Error") // Left[String,Nothing] = Left(Error)
myleft map { (e:Int) => e * 4} // Either[String,Int] = Left(Error)

  1. 最初に、コンパイラは型を「確実に機能する」ものに修正して、@ sloucの結論を再利用します(関数のコンテキストではより意味がありますが)Left[String,Nothing]
  2. 次に、コンパイルmyleftはタイプEither [String、Int]であると推測します

与えられたマップ定義はdef map[B](f: A => B): Either[E, B]、実際にまたはである(e:Int) => e * 4場合にのみ提供できますmyleftLeft[String,Int]Either[String,Int]

つまり、Nothing後で変更する場合に、型を修正する意味は何ですか。

確かに以下はコンパイルされません

val aleft: Left[String, Nothing] = Left[String, Int]("Error")

type mismatch;
found   : scala.util.Left[String,Int]
required: Left[String,Nothing]
val aleft: Left[String, Nothing] = Left[String, Int]("Error")

それで、なぜ私は型を推論するのでしょうか、それは通常私がその型の変数に対して他のことをするのをブロックし(しかし確かに推論の観点からは機能します)、最終的にその型を変更するので、私はその変数で何かをすることができます推測されるタイプ。

EDIT3

Edit2は少し誤解されており、すべてが@sloucの回答とコメントで明確にされています。

回答

4 slouc Nov 23 2020 at 17:33
  • 共分散:
    タイプF[+A]と関係が与えられるとA <: B、次のことが成り立ちます。F[A] <: F[B]

  • 共変性:
    タイプF[-A]と関係が与えられるとA <: B、次のことが成り立ちます。F[A] >: F[B]

コンパイラーが正確な型を推測できない場合、共分散の場合は可能な限り低い型を解決し、反変性の場合は可能な限り高い型を解決します。

どうして?

これは、サブタイピングの差異に関しては非常に重要なルールです。これは、Scalaの次のデータ型の例で示すことができます。

trait Function1[Input-, Output+]

一般的に言って、型が関数/メソッドのパラメーターに配置されるとき、それはいわゆる「反変の位置」にあることを意味します。関数/メソッドの戻り値で使用される場合、それはいわゆる「共変位置」にあります。それが両方にある場合、それは不変です。

さて、この投稿の最初からのルールを考えると、次のように結論付けます。

trait Food
trait Fruit extends Food
trait Apple extends Fruit

def foo(someFunction: Fruit => Fruit) = ???

供給できます

val f: Food => Apple = ???
foo(f)

関数fは、次のsomeFunction理由の有効な代替手段です。

  • FoodFruit(入力の共変性)のスーパータイプです
  • AppleFruit(出力の共分散)のサブタイプです

これは次のような自然言語で説明できます。

「メソッドにfooは、を取り、Fruit生成できる関数が必要Fruitです。これはfoo、いくつかFruitがあり、それをフィードできる関数が必要であり、いくつかがFruit返されることを期待することを意味します。関数を取得した場合Food => Apple、すべてが正常です-それでもフィードできますFruit(機能はどんな食べ物でも取るので)、そしてそれは受け取ることができますFruit(リンゴは果物なので、契約は尊重されます)。

最初のジレンマに戻って、うまくいけば、これが、追加情報なしで、コンパイラーが共変型の場合は可能な限り低い型に、反変型の場合は可能な限り高い型に頼る理由を説明します。に関数を提供したい場合foo、確実に機能することがわかっている関数がありますAny => Nothing

一般的な分散。

Scalaドキュメントの差異。

Scalaの分散に関する記事(完全な開示:私が書いた)。

編集:

私はあなたを混乱させているものを知っていると思います。

あなたがインスタンス化するときLeft[String, Nothing]、あなたは後に許可されているmap機能でそれInt => Whatever、またはString => Whatever、またはAny => Whatever。これは、前に説明した関数入力の反変性のためです。それがあなたのmap作品です。

「後で変更する場合、タイプをNothingに固定する意味は何ですか?」

Nothing共変性の場合に未知の型を修正するコンパイラに頭を悩ませるのは少し難しいと思います。Any共分散の場合に未知のタイプを修正すると、より自然に感じられます(「何でも」の場合があります)。前に説明した共分散と反変性の二重性のため、同じ理由が反変Nothingと共変に適用されAnyます。

1 DmytroMitin Nov 23 2020 at 20:54

これは、EugeneBurmakoによるScalaでのコンパイル時とランタイムのメタプログラミングの統合からの引用です。

https://infoscience.epfl.ch/record/226166 (p。95-96)

型推論中に、型チェッカーは、型パラメーターの境界、用語引数の型、さらには暗黙的な検索の結果から、欠落している型引数に対する制約を収集します(Scalaは機能依存の類似物をサポートしているため、型推論は暗黙的な検索と連携して機能します)。これらの制約は、未知の型引数が型変数として表され、サブタイプ関係によって順序が課される不等式のシステムと見なすことができます。

制約を収集した後、タイプチェッカーは段階的なプロセスを開始します。このプロセスは、各ステップで、不等式に特定の変換を適用しようとし、同等であるがおそらくより単純な不等式のシステムを作成します。型推論の目的は、元の不等式を、元のシステムの一意のソリューションを表す等式に変換することです。

ほとんどの場合、型推論は成功します。その場合、欠落している型引数は、ソリューションによって表される型に推測されます。

ただし、型推論が失敗する場合があります。たとえば、型パラメータTがファントムである場合、つまりメソッドの用語パラメータで使用されていない場合L <: T <: U、不等式のシステムでの唯一のエントリは、になります。ここでL、およびUはそれぞれ下限と上限です。の場合L != U、この不等式には一意の解決策がなく、型推論の失敗を意味します。

型推論が失敗した場合、つまり、それ以上の変換手順を実行できず、動作状態にまだいくつかの不等式が含まれている場合、型チェッカーは膠着状態を解消します。それは、まだ推論されていないすべての型引数、つまり変数がまだ不等式で表されている引数を取り、それらを強制的に最小化します。つまり、それらを下限に等しくします。これにより、一部の型引数が正確に推測され、一部は一見任意の型に置き換えられる結果が生成されます。たとえば、制約のない型パラメーターは、に推測されますNothing。これは、Scalaの初心者にとって一般的な混乱の原因です。

Scalaで型推論についてさらに学ぶことができます:

HubertPlociniczakによるローカル型推論の復号化 https://infoscience.epfl.ch/record/214757

Guillaume Martres Scala 3、型推論とあなた! https://www.youtube.com/watch?v=lMvOykNQ4zs

Guillaume Martres Dottyとタイプ:これまでの話 https://www.youtube.com/watch?v=YIQjfCKDR5A

スライド http://guillaume.martres.me/talks/

DottyのAleksanderBoruch-Gruszecki GADT https://www.youtube.com/watch?v=VV9lPg3fNl8