Через несколько недель я начинаю выполнять роль Scala, но раньше я не писал ни одной Scala (да, мои будущие работодатели знают об этом), хотя я написал много C # и Haskell. Так или иначе, я пролистал книгу Scala 3 и нашел этот пример < / а>:

enum Option[+T]:
  case Some(x: T)
  case None

Которые, по-видимому, представляют собой дусугары:

enum Option[+T]:
  case Some(x: T) extends Option[T]
  case None       extends Option[Nothing]

Мои два вопроса:

  1. Как именно работает механизм этого обессахаривания? В частности, почему Some по умолчанию расширяет Option[T], тогда как None расширяет Option[Nothing]?
  2. Это кажется странным способом определения Option. Я бы определил это так:
enum Option[+T]:
  case Some(x: T) extends Option[T]
  case None       extends Option[T]

Действительно, с None расширением Option[None] разве это не потерпит неудачу?

Option[string] x = None;

Поскольку Option[T] ковариантен в T, а None не является подтипом string?

Я уверен, что мне здесь не хватает чего-то фундаментального.

5
Clinton 8 Июн 2021 в 15:04

4 ответа

Лучший ответ

Отличным источником информации о том, как работает удаление сахаров в перечислениях, является исходное предложение, приведенное в выпуске № 1970.

Соответствующий раздел для вашего вопроса на этой странице называется «Desugarings». Давайте посмотрим на ваше определение Option.

enum Option[+T]:
  case Some(x: T)
  case None

Some - это параметризованный случай в перечислении с параметрами типа, поэтому применяется Правило №4.

Если E - это класс перечисления с параметрами типа Ts, то случай в его сопутствующем объекте без предложения extends

case C <params> <body>

...

В случае, когда C не имеет параметров типа, предположим, что параметры типа E являются

V1 T1 > L1 <: U1 ,   ... ,    Vn Tn >: Ln <: Un      (n > 0)

где каждая из дисперсий Vi равна "+" или "-". Затем случай расширяется до

case C <params> extends E[B1, ..., Bn] <body>

Итак, ваше дело Some получает условие extends для Option[T], как и следовало ожидать. Затем Правило № 5 дает нам реальный класс case.

С другой стороны, ваш None по тому же признаку использует параметры типа no , поэтому результат основан на дисперсии.

  • Если T инвариантен, мы должны явно указать предложение extends
  • Если T ковариантно, мы получаем Nothing
  • Если T контравариантно, мы получаем Any

Ваш T ковариантен, поэтому мы расширяем Option[Nothing], который, как следует, является подтипом Option[S] для any S. Следовательно, ваше присвоение (и помните, в Scala мы используем val / var для объявления переменных, а не только имя типа)

val x: Option[String] = None;

None - это значение типа None.type, которое расширяет Option[Nothing]. Option[Nothing] является подтипом Option[String], поскольку Nothing является подтипом String. Итак, задание выполнено.

По этой же причине Nil (пустой список) расширяет List[Nothing]. Я могу построить Nil, который имеет конкретный тип (подтип List[Nothing]), а затем я могу прийти и добавить к нему все, что захочу. Результирующий список больше не List[Nothing]; 1 +: Nil - это List[Int], а "a" +: Nil - это List[String], но дело в том, что Nil может поступать откуда угодно в моей программе, а у нас нет чтобы решить, какой тип мы хотим, чтобы он был впереди. Мы не можем (безопасно) использовать ковариацию для изменяемого типа данных, поэтому все это работает только потому, что List и Option неизменяемы. Изменяемая коллекция, такая как ArrayBuffer, неизменна в своем параметре типа. (Примечание: встроенные массивы Java ковариантны, и это несостоятельно и вызывает всевозможные проблемы)

4
Silvio Mayolo 8 Июн 2021 в 12:43

Как именно работает механизм этого обессахаривания? В частности, почему Some по умолчанию расширяет Option [T], тогда как None расширяет Option [Nothing]?

Some по умолчанию расширяет Option[T], поскольку он имеет параметр типа в качестве аргумента (x: T) и обрабатывается аналогично (но не точно так же), как класс case, а None - обрабатывается как «обычное» значение Enum, поскольку оно не имеет определения списка параметров.

Мы можем дополнительно проанализировать фазу компиляции типизатора, добавив -Xprint:typer:

sealed abstract class MyOption[T >: Nothing <: Any]()
     extends
   Object(), scala.reflect.Enum {
    +T
    import MyOption.{MySome, None}
  }

final lazy module val MyOption: MyOption$ = new MyOption$()
final module class MyOption$() extends AnyRef() { this: MyOption.type =>
final case class MySome[T](x: T) extends MyOption[MySome.this.T]() {
  +T
  val x: T
  def copy[T](x: T): MyOption.MySome[T] = new MyOption.MySome[T](x)
  def copy$default$1[T]: T = MySome.this.x
  def ordinal: Int = 0
  def _1: T = this.x
}
final lazy module val MySome: MyOption.MySome$ = new MyOption.MySome$()
final module class MySome$() extends AnyRef() { 
  this: MyOption.MySome.type =>
  def apply[T](x: T): MyOption.MySome[T] = new MyOption.MySome[T](x)
  def unapply[T](x$1: MyOption.MySome[T]): MyOption.MySome[T] = x$1
  override def toString: String = "MySome"
}
case <static> val None: MyOption[Nothing] = 
  {
    final class $anon() extends MyOption[Nothing](), scala.runtime.EnumValue {
      def ordinal: Int = 1
    }
    new $anon(): MyOption[Nothing] & runtime.EnumValue
  } 
 

Мы видим, что MySome расширяется до case class, а None расширяется до значения, определенного на MyOption.

Сильвио предоставил "спецификацию" для определений enum (на сегодняшний день (06.08.2021) я могу не найти формального), где правила 4 и 7 - это то, что мы ищем:

Если E - это класс перечисления с параметрами типа Ts, то случай в его сопутствующем объекте без предложения extends

case C <params> <body>

...

В случае, когда C не имеет параметров типа, предположим, что параметры типа E являются

V1 T1 > L1 <: U1 ,   ... ,    Vn Tn >: Ln <: Un      (n > 0)

где каждая из дисперсий Vi равна "+" или "-". Затем случай расширяется до

case C <params> extends E[B1, ..., Bn] <body>

И для случая None:

case C класса перечисления E, который не принимает параметры типа, расширяется до

val C = $new(n, "C")

Здесь $ new - это частный метод, который создает экземпляр E (см. Ниже).


Это кажется странным способом определения Option.

Не совсем, это имеет смысл, если задуматься. None в этом случае обрабатывается как синглтон, и поскольку T ковариантен по типу аргумента, а Nothing - это нижний тип, что означает, что он наследует все другие типы в Scala, он работает для любого назначения.

2
Yuval Itzchakov 8 Июн 2021 в 12:53

Он сокращается с помощью предложения extends Option[Nothing], поскольку T является параметром ковариантного типа (на это указывает + перед T), который не используется в None дело. Если вы не знаете, параметр ковариантного типа называется out параметр типа в C #.

Причина, по которой десугарирование работает таким образом, заключается в том, что это позволяет вам использовать один и тот же объект None для нескольких разных типов, что экономит память и выделение памяти. Подумайте об этом: параметр типа T сообщает вам, какой тип значения вы можете извлечь из своего Option с помощью e. грамм. звонит .get. Но нет никакого значения, которое можно было бы извлечь из None, так что вы можете использовать одно и то же значение None везде.

1
Matthias Berndt 8 Июн 2021 в 13:26
  1. обессахаривание является частью фазы компиляции, более важно понимать, что enum в scala3 заменяет типы coroduct / sum в scala 2. по сути, это тип объединения с тегами.

  2. Ничто не является нижним типом, поэтому оно простирается от всего. Это двойник Any (корневой объект)

Ps: Я могу расширить их, если хотите, дайте мне знать

1
renghen 8 Июн 2021 в 13:29