Какие специальные правила есть у компилятора 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).

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


person rmin    schedule 20.12.2016    source источник
comment
в основном, если вы объявляете какой-либо тип как Unit (например, значение или возврат функции), компилятор преобразует любое значение, которое у него есть, в Unit. не уверен, что это тот ответ, который вы ищете, или если вы ищете больше внутренних деталей.   -  person pedrorijo91    schedule 20.12.2016


Ответы (3)


Я собираюсь ответить на вопрос заголовка для большего охвата. 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. Например, при сохранении в базу данных вы можете вернуть сохраненное значение (возможно, с новым идентификатором или чем-то еще).


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

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 = ()
}
person Michael Zajac    schedule 20.12.2016
comment
Спасибо за отличный ответ! Я думаю, что вы имеете в виду 6.26.1, а не 6.22.1, когда ссылаетесь на спецификацию языка. - person rmin; 21.12.2016

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

Отказ от значения

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

person rethab    schedule 20.12.2016

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

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

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

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

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

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

person Eduardo Pareja Tobes    schedule 20.12.2016