Подтвердить что ты не робот

Какие специальные правила имеет компилятор scala для типа устройства в системе типов

Unit получает специальную обработку компилятором при генерации байтового кода, поскольку он аналогичен void на jvm. Но концептуально, как тип в системе типа scala, кажется, что он также получает особое отношение к самому языку (примеры ниже).

Итак, мой вопрос в том, чтобы прояснить это и понять, какие механизмы используются, и если действительно существует специальное обращение к типу Unit.


Пример 1:

Для "обычных" scala типов, таких как Seq, если метод возвращает Seq, тогда вы должны вернуть Seq (или более конкретный тип, который расширяет Seq)

def foo1: Seq[Int] = List(1, 2, 3)
def foo2: Seq[Int] = Vector(1, 2, 3)
def foo3: Seq[Int] = "foo" // Fails

Первые два примера компилируются, потому что List[Int] и Vector[Int] являются подтипами Seq[Int]. Третий не работает, потому что String не является.

Но если я изменю третий пример на возврат Unit, но он будет компилироваться и запускаться без проблем, хотя String не является подтипом Unit:

def foo3(): Unit = "foo" // Compiles (with a warning)

Я не знаю ни одного другого типа, для которого это исключение будет разрешено в scala. Также компилятор имеет специальные правила для типа Unit на уровне системы типов или существует какой-то более общий механизм при работе, например. неявное преобразование.


Пример 2:

Я также не понимаю, как подразделение взаимодействует в ситуациях, когда правило отклонения обычно применяется.

Например, мы иногда попадаем в эту ошибку с помощью Future[Unit], где мы случайно используем map вместо flatMap и создаем Future[Future]:

def save(customer: Customer): Future[Unit] = ... // Save to database

def foo: Future[Unit] = save(customer1).map(_ => save(customer2))

map создает Future[Future[Unit]], а для компилятора требуется Future[Unit]. Но это компилируется!

Сначала я думал, что это потому, что Future[+T] является ковариантным, но на самом деле Future[Unit] не является подтипом Unit, поэтому, похоже, это не так.

Если тип изменяется на Boolean, например, компилятор обнаруживает ошибку:

def save(customer: Customer): Future[Boolean] = ...

def foo: Future[Boolean] = save(customer1).map(_ => save(customer2)) // Compiler fails this

И для каждого другого типа не Unit он не будет компилироваться (кроме Any, потому что Future[Any] является подтипом Any по совпадению).

Итак, у компилятора есть специальные правила в этом случае? Или происходит более общий процесс?

4b9b3361

Ответ 1

Я собираюсь ответить на титульный вопрос для большего охвата. Unit получает специальное лечение в нескольких местах, больше, чем то, что происходит в этих примерах кода. Частично это связано с тем, что Unit является фигурой компилятора, который сводится к void на JVM.


Значение Отбрасывание

Это самый удивительный случай для людей. Каждый раз, когда ожидаемый тип некоторого значения равен Unit, компилятор заключает в Unit в конце выражения, которое производит значение, в соответствии с SLS - 6.26.1:

Если ee имеет некоторый тип значения, а ожидаемый тип Unit, ee преобразуется в ожидаемый тип, вставляя его в член { ee; () }.

Таким образом,

def foo3(): Unit = "foo"

становится:

def foo3(): Unit = { "foo" ; () }

Аналогично,

def foo: Future[Unit] = save(customer1).map(_ => save(customer2))

становится:

def foo: Future[Unit] = save(customer1).map(_ => { save(customer2); () })

Преимущество этого в том, что вам не нужно, чтобы последний оператор метода имел тип Unit, если вы этого не хотите. Однако это преимущество невелико, потому что если последний оператор вашего метода, который возвращает Unit, не является Unit, это обычно указывает на ошибку, поэтому для него есть предупреждающий флаг (-Ywarn-value-discard).

В общем, мне лучше возвращать более конкретный тип, если это возможно, а не возвращать Unit. Например, при сохранении в базе данных вы можете вернуть сохраненное значение (возможно, с новым ID или что-то еще).


Класс значений

Unit - это класс значений, созданный компилятором Scala, с одним экземпляром (если он вообще должен быть создан как класс). Это означает, что он компилируется до примитива void на JVM, если только вы не рассматриваете его как класс (например, ().toString). Он имеет свой собственный раздел в спецификации, SLS - 12.2.13.


Пустой тип блока

Из SLS - 6.11, тип пустого блока по умолчанию считается Unit. Например:

scala> val x = { }
x: Unit = ()

Равно

При сравнении Unit с другим Unit (который должен быть тем же самым объектом, поскольку есть только один), компилятор выдает специальное предупреждение, информирующее вас о том, что в вашей программе что-то не так.

scala> ().==(())
<console>:12: warning: comparing values of types Unit and Unit using `==' will always yield true
       ().==(())
            ^
res2: Boolean = true

Кастинг

Вы можете нарисовать что-нибудь в Unit, поскольку компилятор будет оптимизировать его (хотя это неясно для меня, если отбрасывание значения берет на себя после вывода типа).

object Test {
  val a = "a".asInstanceOf[Unit]
  val b = a
}

становится:

object Test extends Object {
  def <init>(): Test.type = {
    Test.super.<init>();
    ()
  };
  private[this] val a: scala.runtime.BoxedUnit = scala.runtime.BoxedUnit.UNIT;
  <stable> <accessor> def a(): Unit = ();
  private[this] val b: scala.runtime.BoxedUnit = scala.runtime.BoxedUnit.UNIT;
  <stable> <accessor> def b(): Unit = ()
}

Ответ 2

Как написано в scala спецификация языка в главе 6.26.1:

Значение Отбрасывание

Если e имеет некоторый тип значения, а ожидаемый тип - Unit, e преобразуется к ожидаемому типу, вставив его в член {e;()}.

Ответ 3

ответ rethab уже дал вам ссылку на спецификацию; просто позвольте мне добавить, что

  • вы можете отключить это (сделать предупреждение ошибкой) через флаг -Xfatal-warnings компилятора
  • вы получите лучшие сообщения с флагом -Ywarn-value-discard; для foo3 предупреждение компилятора будет более информативным discarded non-Unit value

Обратите внимание, что это преобразование "any to Unit" является магии компилятора, поэтому ни -Yno-predef, либо -Yno-imports не отключит его; вам нужны флаги выше. Я считаю это частью спецификации языка ошибкой, так как по какой-то причине вам нужно это сомнительное поведение, вы можете просто добавить что-то вроде

implicit def any2Unit(a: Any): Unit = ()

в то время как отказ от него требует наличия неподдерживаемого (по определению, поскольку он нарушает спецификацию) флаг компилятора.

Я также рекомендую wartremover, где у вас this и многое другое.