Почему Scala выводит нижний тип, если параметр типа не указан?
Интересно, может ли кто-нибудь объяснить правило вывода в этом конкретном случае ниже, и, самое главное, это рациональное / подразумеваемое?
case class E[A, B](a: A) // class E
E(2) // E[Int,Nothing] = E(2)
Обратите внимание, что я мог бы написать
E[Int](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]
РЕДАКТИРОВАТЬ1
Здесь есть последовательность, но я все еще могу понять рациональное. Ниже приводится то же определение с противоположным параметром:
case class E[A, -B](a: A)// class E
E(2) // E[Int, Any] = E(2)
Следовательно, у нас действительно есть то же самое наоборот, когда это противоположно, и это делает все поведение или правило вывода согласованным. Однако я не уверен в рациональности этого...
Почему не обратное правило, т.е.
Any
когда Ковариант / Инвариант и
Nothing
когда контравариант?
РЕДАКТИРОВАТЬ2
В свете ответа @slouc, который имеет смысл, я все еще не понимаю, что и почему компилятор делает то, что он делает. Пример ниже иллюстрирует мое замешательство.
val myleft = Left("Error") // Left[String,Nothing] = Left(Error)
myleft map { (e:Int) => e * 4} // Either[String,Int] = Left(Error)
- Сначала компилятор исправляет тип для чего-то, что "наверняка работает", чтобы повторно использовать вывод @slouc (хотя и имеет больше смысла в контексте функции)
Left[String,Nothing]
- Затем компиляция делает вывод
myleft
иметь тип Either[String,Int]
данное определение карты
def map[B](f: A => B): Either[E, B]
,
(e:Int) => e * 4
может быть поставлен только если
myleft
на самом деле
Left[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")
Итак, зачем мне делать вывод о типе, который обычно блокирует меня, чтобы делать что-либо еще с переменной этого типа (но наверняка работает с точки зрения вывода), чтобы в конечном итоге изменить этот тип, чтобы я мог что-то сделать с переменной этого типа предполагаемый тип.
РЕДАКТИРОВАТЬ3
Edit2 - это немного недоразумение, и все разъясняется в ответе и комментариях @slouc.
2 ответа
Ковариация: данный
тип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
потому как:
Food
это супертипFruit
(контравариантность ввода)Apple
это подтипFruit
(ковариация вывода)
Мы можем объяснить это на естественном языке так:
"Метод
foo
нужна функция, которая может приниматьFruit
и произвестиFruit
. Это означаетfoo
будет немногоFruit
и ему понадобится функция, которой он может его кормить, и ожидать, что некоторыеFruit
назад. Если он получит функциюFood => Apple
, все нормально - еще может кормитьFruit
(потому что функция принимает любую пищу), и она может получатьFruit
(яблоки фруктовые, поэтому договор соблюдается).
Возвращаясь к вашей первоначальной дилемме, надеюсь, это объясняет, почему без какой-либо дополнительной информации компилятор будет прибегать к наименьшему возможному типу для ковариантных типов и к максимально возможному типу для контравариантных. Если мы хотим предоставить функцию
foo
, есть один, который, как мы знаем, наверняка работает:
Any => Nothing
.
Статья о вариативности в Scala (полное раскрытие: это я написал).
РЕДАКТИРОВАТЬ:
Думаю, я знаю, что вас смущает.
Когда вы создаете экземпляр
Left[String, Nothing]
, тебе разрешено позже
map
это с функцией
Int => Whatever
, или же
String => Whatever
, или же
Any => Whatever
. Это происходит именно из-за контравариантности ввода функции, описанной ранее. Вот почему ваш
map
работает.
"какой смысл фиксировать тип в Nothing, если его нужно изменить позже?"
Я думаю, что немного сложно понять, что компилятор исправляет неизвестный тип для
Nothing
в случае контравариантности. Когда он исправляет неизвестный тип на
Any
в случае ковариантности это кажется более естественным (это может быть "что угодно"). Из-за двойственности ковариации и контравариантности, объясненной ранее, те же рассуждения применимы для контравариантных
Nothing
и ковариантный
Any
.
Это цитата из книги Юджина Бурмако " Объединение метапрограммирования времени компиляции и времени выполнения в Scala ".
https://infoscience.epfl.ch/record/226166 (стр. 95-96)
Во время вывода типа средство проверки типов собирает ограничения на отсутствующие аргументы типа из границ параметров типа, из типов аргументов термина и даже из результатов неявного поиска (вывод типа работает вместе с неявным поиском, поскольку Scala поддерживает аналог функциональных зависимостей). Эти ограничения можно рассматривать как систему неравенств, где аргументы неизвестного типа представлены как переменные типа, а порядок определяется отношением подтипа.
После сбора ограничений средство проверки типов запускает пошаговый процесс, который на каждом шаге пытается применить определенное преобразование к неравенствам, создавая эквивалентную, но предположительно более простую систему неравенств. Цель вывода типа - преобразовать исходные неравенства в равенства, которые представляют собой единственное решение исходной системы.
В большинстве случаев вывод типа проходит успешно. В этом случае отсутствующие аргументы типа выводятся для типов, представленных решением.
Однако иногда вывод типа не удается. Например, когда параметр типа
T
является фантомным, т.е. не используется в термине параметры метода, его единственной записью в системе неравенств будетL <: T <: U
, гдеL
иU
- его нижняя и верхняя границы соответственно. ЕслиL != U
, это неравенство не имеет единственного решения, а это означает сбой вывода типа.Когда вывод типа не удается, т.е. когда он не может предпринять больше шагов преобразования и его рабочее состояние все еще содержит некоторые неравенства, проверка типов выходит из тупика. Он принимает все аргументы неуказанного типа, т.е. те, чьи переменные все еще представлены неравенствами, и принудительно минимизирует их, то есть приравнивает их к их нижним границам. Это приводит к тому, что некоторые аргументы типа выводятся точно, а некоторые заменяются кажущимися произвольными типами. Например, параметры неограниченного типа выводятся из
Nothing
, что часто вызывает путаницу у новичков в Scala.
Вы можете узнать больше о выводе типов в Scala:
Хуберт Плоциничак Расшифровка логического вывода локального типа https://infoscience.epfl.ch/record/214757
Гийом Мартрес Scala 3, вывод типов и вы! https://www.youtube.com/watch?v=lMvOykNQ4zs
Гийом Мартрес Дотти и типы: история до сих пор https://www.youtube.com/watch?v=YIQjfCKDR5A
Слайды http://guillaume.martres.me/talks/
Александр Борух-Грушецкий GADTs в Dotty https://www.youtube.com/watch?v=VV9lPg3fNl8