Понимание пределов поддержки Scala GADT
Ошибка в Test.test кажется необоснованной:
sealed trait A[-K, +V]
case class B[+V]() extends A[Option[Unit], V]
case class Test[U]() {
def test[V](t: A[Option[U], V]) = t match {
case B() => null // constructor cannot be instantiated to expected type; found : B[V] required: A[Option[U],?V1] where type ?V1 <: V (this is a GADT skolem)
}
def test2[V](t: A[Option[U], V]) = Test2.test2(t)
}
object Test2 {
def test2[U, V](t: A[Option[U], V]) = t match {
case B() => null // This works
}
}
Есть несколько способов сделать изменение ошибки или уйти:
Если мы удалим параметр V по признаку A (и классу case B), часть ошибки "GADT-skolem" исчезнет, но элемент "конструктор не может быть экземпляр" остается.
Если мы переместим параметр U класса Test в метод Test.test, ошибка исчезнет. Зачем? (Аналогично, ошибка отсутствует в Test2.test2)
Следующая ссылка также идентифицирует эту проблему, но я не понимаю предоставленного объяснения. http://lambdalog.seanseefried.com/tags/GADTs.html
Это ошибка в компиляторе? (2.10.2-RC2)
Благодарим вас за помощь в разъяснении этого.
2014/08/05: мне удалось еще больше упростить код и предоставить другой пример, где U связан за пределами непосредственной функции, не вызывая ошибки компиляции. Я все еще наблюдаю эту ошибку в 2.11.2.
sealed trait A[U]
case class B() extends A[Unit]
case class Test[U]() {
def test(t: A[U]) = t match {
case B() => ??? // constructor cannot be instantiated to expected type; found : B required: A[U]
}
}
object Test2 {
def test2[U](t: A[U]) = t match {
case B() => ??? // This works
}
def test3[U] = {
def test(t: A[U]) = t match {
case B() => ??? // This works
}
}
}
Упрощенный, похоже, что это больше похоже на ошибку или ограничение компилятора. Или я что-то упускаю?
Ответы
Ответ 1
Конструкторские шаблоны должны соответствовать ожидаемому типу шаблона, что означает B <: A [U], утверждение, которое истинно, если U является параметром типа метода, который в настоящее время вызывается (поскольку он может быть создан для соответствующий аргумент типа), но неверно, если U - это ранее связанный тип типа класса.
Вы можете, конечно, подвергнуть сомнению значение правила "должно соответствовать". Я знаю, что знаю. Мы обычно избегаем этой ошибки, повышая эффективность проверки до тех пор, пока шаблон конструктора не будет соответствовать. В частности,
// Instead of this
def test1(t: A[U]) = t match { case B() => ??? }
// Write this
def test2(t: A[U]) = (t: A[_]) match { case B() => ??? }
Добавление: в комментарии спрашивающий говорит: "Вопрос прост в том, почему он не работает с параметром типа на уровне класса, но работает на уровне метода". Параметры инициализированного типа класса видны снаружи и имеют неопределенное время жизни, ни одно из которых не соответствует истинным параметрам типа метода. Это имеет значение для жесткости типа. Учитывая Foo [A], как только я получаю Foo [Int], тогда A должен быть Int, когда ссылается на этот экземпляр, всегда и навсегда.
В принципе вы можете обрабатывать параметры типа класса аналогично внутри вызова конструктора, потому что параметр типа все еще несвязан и может быть выведено оппортунистически. Это все. Когда-то там, в мире, нет места для пересмотра.
Еще одно добавление: я вижу, что люди делают это много, принимая в качестве предпосылки, что компилятор является образцом правильности, и все, что осталось для нас, - это размышлять над его выходом, пока наше понимание не развилось достаточно далеко, чтобы соответствовать ему. Такая ментальная гимнастика произошла под этой палаткой, мы могли бы несколько раз встретить Cirque de Soleil.
scala> case class Foo[A](f: A => A)
defined class Foo
scala> def fail(foo: Any, x: Any) = foo match { case Foo(f) => f(x) }
fail: (foo: Any, x: Any)Any
scala> fail(Foo[String](x => x), 5)
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at $anonfun$1.apply(<console>:15)
at .fail(<console>:13)
... 33 elided
То, что текущая версия scala - это все еще то, что она делает. Нет предупреждений. Поэтому, возможно, спросите себя, разумно ли предлагать презумпцию правильности языку и реализации, которые настолько тривиально необоснованны после более чем десяти лет существования.
Ответ 2
Похоже, это компилятор. Из this, martin odersky ставит его как:
In the method case, what you have here is a GADT:
Patterns determine the type parameters of an corresponding methods in the scope of a pattern case.
GADTs are not available for class parameters.
As far as I know, nobody has yet explored this combination, and it looks like it would be quite tricky to get this right.
PS: Спасибо @retronym, который предоставил ссылку из обсуждения здесь
Почему он вызывает ошибку в случае класса:
Это работает:
sealed trait A[-K, +V]
case class B[+V]() extends A[Option[Unit], V]
case class Test[U]() {
def test[V, X <: Unit](t: A[Option[X], V]) = t match {
case B() => null
}
def test2[V](t: A[Option[U], V]) = Test2.test2(t)
}
object Test2 {
def test2[U, V](t: A[Option[U], V]) = t match {
case B() => null // This works
}
}
Чтобы объяснить, почему компилятор сделал ошибку: попробуйте сделать это:
scala> :paste
// Entering paste mode (ctrl-D to finish)
abstract class Exp[A]{
def eval:A = this match {
case Temp(i) => i //line-a
}
}
case class Temp[A](i:A) extends Exp[A]
// Exiting paste mode, now interpreting.
Чтобы понять это, из §8.1.6: выше Temp
является полиморфным типом. Если класс case является полиморфным, то:
Если класс case является полиморфным, то его параметры типа созданный таким образом, чтобы экземпляр c соответствовал ожидаемому тип рисунка. Определенные формальные типы параметров cs первичный конструктор затем берутся как ожидаемые типы шаблоны компонентов p 1,.,, p n. Шаблон соответствует всем объектам созданный из вызовов конструктора c (v1,..., v n), где каждый элемент p я соответствует соответствующему значению v i.
то есть. компилятор с готовностью пытается создать экземпляр Temp
в строке-a, чтобы он соответствовал первому конструктору this
(который, если выше скомпилирован успешно, тогда в случае Temp(1)
будет что-то вроде Exp[Int]
и, следовательно, компилятор создает экземпляр Temp в строке-a с параметром Int
.
Теперь в нашем случае: компилятор пытается создать экземпляр B
. Он видит, что t
имеет тип A[Option[U],V]
, где U
уже зафиксирован и получен из параметра класса, а V
- это общий тип метода. При попытке инициализировать B
он пытается создать таким образом, чтобы в итоге он получил A[Option[U],V]
. Поэтому с B()
он каким-то образом пытается получить A[Option[U],V]
. Но он не может быть B
равен A[Option[Unit],V]
. Следовательно, в конечном итоге он не может инициализировать B
. Исправление этого делает работу
Его не требуется в случае теста-2, потому что: Компилятор, как описано выше, пытается инициализировать параметр типа B. Он знает, что t имеет параметр типа [Option [U], V], где U и V являются общими методами и получены из аргумента. Он пытается инициализировать B на основе agrument. Если аргумент был новым B [String], он пытается получить B [String], и, следовательно, U автоматически получается как Option [Unit]. Если аргумент был новым A [Option [Int], String], то он явно не совпадает.
Ответ 3
Разница связана с тем, какая информация имеет компилятор и среда выполнения в каждом случае, в сочетании с тем, какие существуют ограничения на типы.
Ниже двусмысленности выясняется, что U - это признак и параметр класса, а X - параметр типа метода.
sealed trait A[U]
case class B(u: Unit) extends A[Unit]
class Test[U]() {
def test(t: A[U]) = t match {
case B(u) => u // constructor cannot be instantiated to expected type; found : B required: A[U]
}
}
object Test2 {
def test2[X](t: A[X]) = t match {
case B(x) => x // This works
}
def test3[X] = {
def test(t: A[X]) = t match {
case B(x) => x // This works
}
}
}
Test2.test2(new B(println("yo")))
В Test2.test2 компилятор знает, что будет предоставлен экземпляр A [X] без ограничений на X. Он может генерировать код, который проверяет параметр, предоставленный, чтобы увидеть, является ли он B, и обрабатывать регистр.
В классе Test method test компилятор не знает, что некоторый A [X] предоставляется при вызове, но представлен некоторый определенный тип U. Этот тип U может быть любым. Однако совпадение шаблона относится к типу алгебраических данных. Этот тип данных имеет ровно одно допустимое изменение типа A [Unit]. Но эта подпись для A [U], где U не ограничена, Unit. Это противоречие. В определении класса указано, что U является параметром свободного типа, метод говорит, что это Unit.
Представьте, что вы создаете тест:
val t = new Test[Int]()
Теперь, на сайте использования, метод:
def test(t: A[Int])
Нет такого типа A [Int]. Согласование шаблонов на B - это когда компилятор проверяет это условие. A [U] для любого U несовместим с типом, поэтому "конструктор не может быть создан для ожидаемого типа, найдено: B требуется: A [U]"
Разница с параметром method versus class type заключается в том, что он привязан, а один свободен, во время вызова метода во время выполнения.
Еще один способ подумать о том, что определение метода
def test(t: A[U])
означает, что U - это Unit, что не соответствует объявлению класса, что U - что-то.
Ответ 4
Изменить: это не ответ на вопрос. Я держу его для справки (и комментариев).
Указанная вами ссылка уже дает ответ.
В случае параметризованного метода U
выводится из аргумента фактического вызова метода. Таким образом, из того, что выбран случай B()
, следует, что U >: Unit
(иначе метод не мог быть вызван с помощью B
), и компилятор счастлив.
В случае параметризованного класса U
не зависит от аргумента метода. Итак, тот факт, что был выбран случай B()
, ничего не говорит о компиляторе U
и не может подтвердить, что U >: Unit
. Я думаю, что если вы добавите такой тип, привязанный к U
, он должен работать. Я даже не пробовал.
Ответ 5
Компилятор абсолютно корректен с сообщением об ошибке. Простой пример должен хорошо описать проблему:
sealed trait A[U]
class B extends A[Unit]
class T[U] {
val b: A[Unit] = new B
f(b) // invalid, the compiler can't know which types are valid for `U`
g(b) // valid, the compiler can choose an arbitrary type for `V`
def f(t: A[U]) = g(t)
def g[V](t: A[V]) = ???
}
f
и g
имеют разные типы сигнатур. g
принимает A[V]
, где V
- произвольный параметр типа. С другой стороны, f
принимает A[U]
, где U
не является произвольным, но зависит от внешнего класса T
.
Компилятор не знает, какой тип U
может быть в тот момент, когда он typechecks T
- ему нужно ввести typecheck экземпляр T
, чтобы узнать, какие типы используются. Можно сказать, что U
является конкретным типом внутри T
, а не общим типом. Это означает, что он должен отклонить каждый фрагмент кода, который попадает внутрь T
более конкретным по типу U
.
Вызов g
изнутри f
явно разрешен - ведь в этом случае можно выбрать произвольный тип для V
, который в этом случае равен U
. Поскольку U
больше никогда не упоминается в g
, не имеет значения, какой тип он может иметь.
Ваш другой пример кода лежит в основе тех же ограничений - это просто более сложный пример.
Btw, что этот код
def test3[U] = {
def test(t: A[U]) = t match {
case B() => ??? // This works
}
}
действительно выглядит странно для меня. Не имеет значения, связан ли U
классом или методом - изнутри области привязки мы не можем дать никаких гарантий относительно его типа, поэтому компилятор также должен отклонить этот шаблон.