Почему 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)

  1. Сначала компилятор исправляет тип для чего-то, что "наверняка работает", чтобы повторно использовать вывод @slouc (хотя и имеет больше смысла в контексте функции) Left[String,Nothing]
  2. Затем компиляция делает вывод 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.

Статья о вариативности в 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

Другие вопросы по тегам