Tyr 1 — Ремейк переключателя

Вот уже около пяти лет у Тюра есть классическое switch высказывание. То, что мы все знаем из других языков. Единственная небольшая разница, которая у него могла быть, заключалась в том, что она не позволяла вам переходить к другим кейсам и фактически возвращала результат взятого кейса. По сути то, что делает match in Scala. И в синтаксисе использовались if и else только потому, что мне никогда не нравилось, что регистр и особенно значение по умолчанию являются ключевыми словами.

Для Tyr 0.7 в списке было удивительное количество изменений, связанных с switch. Добавили привязку переключаемого значения. Затем должно было быть какое-то расширение, позволяющее переключаться между разными типами, а не только целыми числами. Добавление перечислений и объединений перечислений (или объединений с тегами, по сути, объединение с перечислением для случаев в первом поле) и использование переключателя для доступа к случаям также требовало чего-то, что позволяло бы переключаться через Ref, позволяя модифицировать связанное значение в правильном случае. . Кроме того, я всегда хотел иметь эффективный тип переключателя и класс переключателя, которые позволили бы мне принимать статические или динамические решения на основе типа объекта. Что-то, что не будет компилироваться в каскад if-else, что делает сопоставление с образцом в других языках программирования. Даже если выражение соответствия называется переключателем там.

На момент написания этой статьи я реализовал все изменения, кроме изменений типа переключателя. Это потребовало двух изменений в синтаксисе языка. : после метки case пришлось убрать, потому что это никогда не было необходимо и вызывало двусмысленность. Не большая проблема, так как сегодня не так много пользователей. Привязка значений переключателя немного сложнее. Проблема в том, что это требует введения нового названия для интересных случаев. Если целью является статический доступ, т. е. использование имени, которое в любом случае видно в этой области, это имя можно повторно использовать, чтобы скрыть исходное определение в теле:

var x = 7
switch x {
  if 7 x = 4
  else x = 0
}

Здесь, в теле if, мы бы знали, что x на самом деле равно 7, если бы нам это было нужно, например. для создания экземпляра типа или что-то в этом роде. Но его также можно использовать как локальную переменную. Теперь, без каких-либо расширений, если вы хотите переключить результат функции, вам нужно будет написать:

{
  val x = f()
  switch x {
    if 7 x = 4
    else x = 0
  }
}

Блок необходим для того, чтобы после этого x исчез. Большинству программистов, вероятно, все равно, и позже они задаются вопросом, почему они не могут называть каждый результат x или err или почему они сталкиваются с проблемами псевдонимов. Таким образом, мы можем разрешить пользователю явно вводить новое локальное имя:

switch x := f() {
  if 7 x = 4
  else x = 0
}

Все идет нормально. Альтернативой этому могло бы быть копирование других языков и принуждение программистов объявлять x в каждой ветке, что приводило бы к длительным case x : OperatorNode условиям. С моим решением я действительно могу назвать Scala многословным. Кто бы это предвидел?

Перечисленные союзы

Объединения Enum были введены в Tyr как средство взаимодействия с библиотеками C. Например, в SDL это нужно для работы с событиями. Просто потому, что C не имеет средств создания подтипов. Не следует использовать этот шаблон в языках с реальными классами. Тем не менее, любой язык должен предоставлять средства для взаимодействия с C. Итак, мы добавили в Tyr безопасную версию этого шаблона. Чтобы понять, что это значит для switch, мы должны сначала определить enum и enum union:

enum base {
  zero,
  one
}

enum union <: base {
  if zero String
  if one  int
  else    double
}

Теперь мы можем изменить содержимое экземпляров объединения следующим образом:

var c = new union

switch v := c {
  if zero { v = "" }
  if one  { v = 7 }
  else    { v = 3.14 }
}

v в блоках привязаны к соответствующим случаям в определении объединения. Если бы в первом случае это была просто String, мы не смогли бы присвоить ей новое значение. Таким образом, он должен понимать, что связанный v является Ref[union], который преобразуется в Ref[String] в ветви if zero.

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

Однако на самом деле это намного сложнее реализовать, чем можно было бы ожидать на первый взгляд. Проблема в том, как работает разрешение перегрузки. В Tyr любой тип может определять неявные преобразования, о которых разрешение перегрузки знает и будет использовать при необходимости. Если бы мы реализовали какой-то прозрачный прокси, мы бы ожидали, что он позволит нам комбинировать его с переключателем, если базовый тип прокси, например. внутр. Первым шагом к решению проблемы является добавление маркерного интерфейса для потенциальных целей переключения под названием canSwitchOn. Однако у нас все еще есть проблема с Ref[canSwitchOn]. Очевидно, что добавлять canSwitchOn к Ref неправильно.

К счастью, дорожная карта Tyr 0.7 содержала несколько новых функций, которые нам здесь понадобятся. Во-первых, это шаблоны вариантов. Вместе с ним в tyr.lang был добавлен CRef. CRef является ковариантным в своем базовом типе. Это необходимо для фактического сопоставления ссылок, поскольку Ref[int] не является подтипом Ref[canSwitchOn]. Но это подтип CRef[canSwitchOn]. На словах это означает, что переменная int может предоставить canSwitchOn, но не может хранить произвольное canSwitchOn. Очевидно правильно. Другая функция, наконец, реализует Type.|, позволяя создать тип canSwitchOn | CRef[canSwitchOn]. Это как раз ожидаемый тип для цели выражения switch в Tyr 0.7.

Тип переключателя

Теперь можно подумать, что добавить тип переключателя так же просто, как добавить canSwitchOn к Type. К сожалению, это не так. Tyr имеет как статическую, так и динамическую типизацию. Кроме того, у него есть поля классов и функции, открывающие варианты использования, где экземпляры Class ведут себя как обычные объекты. И, наконец, у него есть шаблоны. Давайте поиграем с примером, чтобы проиллюстрировать проблемы:

var r = "" // Ref[String]
switch r {
  if String        false
  if StringLiteral true
  if StringBuffer  false
  else             false
}

Пользователь ожидает, что этот переключатель выберет ветку StringLiteral, верно? r может быть переменной String, но во время выполнения она содержит StringLiteral. Почти все сопоставления шаблонов на основе типов связаны с динамическими типами. Даже если бы мы сделали так, чтобы Type наследовало canSwitchOn, этот пример не сработал бы, потому что не может быть, чтобы String имело неявное преобразование в Type или Class. Это просто сломало бы все. Кроме того, в будущей версии Tyr может быть добавлено переключение строк, и пример сломается. Таким образом, пользователь должен каким-то образом сообщить нам, что его интересует именно тип, а не значение. Итак, вторая попытка:

var r = "" // Ref[String]
switch x := r.class {
  if String        false
  if StringLiteral true
  if StringBuffer  false
  else             false
}

Теперь у нас должно получиться что-то, что может понять, что работает на Class, и создать нужный код. Однако, если бы мы использовали x на ветке, это был бы сам Class. Мы ожидаем увидеть r такого типа. Точнее, что-то, что удовлетворяет x : StringLiteral. Если бы мы сейчас ввели правила, аналогичные правилам для объединений перечислений, у нас возникла бы проблема, заключающаяся в том, что не всегда существует экземпляр, поскольку классы и типы сами по себе являются объектами. Хотя эта проблема не очень приятна, ее все же можно решить, поскольку компилятор может просто распаковать .class, если он существует, и использовать его в качестве маркера. Но тогда почему бы не расширить синтаксис с помощью switch class r или switch type r, верно?

Прежде чем рассматривать этот вариант, позвольте мне объяснить, почему статический переключатель является реальным вариантом использования. Tyr 0.7 имеет некоторую оценку CT. На сегодняшний день неясно, будут ли преобразования CT CFG частью этой или будущей версии. Но, в конце концов, они станут частью Тира. С его помощью мы можем предоставить параметр CT по умолчанию, например:

HashSet[T : Type[_], eq : HashedEqr[T] = switch type T {
  if InheritedEqr[T] T
  else MinEqr[T]
}]

Такое определение позволит компилятору использовать отношение эквивалентности из предоставленного аргумента типа, если оно есть и если пользователь не указал его явно. Именно этого мы и хотим, если напишем HashSet[String] или HashSet[int]. Большинство других языков так или иначе вынуждают нас не реализовывать оба поведения в одном и том же типе, который либо всегда выбирает одно, либо другое решение.

Можно подумать, что мы все еще находимся в ситуации, когда мы могли бы просто установить, что включение типа выполняет статическую проверку, если может, и динамическую проверку в противном случае. Мы могли бы просто игнорировать тот факт, что классы и типы являются объектами, потому что, серьезно, зачем кому-то использовать переключатель, верно? Не совсем. Опять бездна имплицитных преобразований. Если мы вернемся к примеру со строкой, мы могли бы также написать:

var r = "" // Ref[String]
switch type r {
  if String        1
  if StringLiteral 2
  if Ref[String]   3
  else             4
}

Не так уж очевидно, какие ошибки или предупреждения должен создавать этот пример. Или какой из четырех случаев будет фактически выбран. Что бы мы ни выбрали, оценка во время компиляции должна прийти к тому же выводу, что и оценка во время выполнения. И мы также хотим скрыть имплициты в этом коде. Единственный способ сделать это — заставить программиста сказать нам, хочет ли он статическое (switch type) или динамическое решение (switch class).

Итак, для switch type мы взяли бы 3, если бы они присутствовали, и 1, если бы 3 не было. Скрытие неявного преобразования из Ref в String. Для switch class мы всегда возвращали бы 2. Кроме того, для switch class мы выдавали бы ошибку, если бы присутствовало 3, поскольку это не класс.

Скрытие деталей

Суть switch class в том, что это хороший способ избавиться от большого количества приведений типов и объявлений локальных переменных. Ввод лишнего class — справедливая цена за это. Однако, не можем ли мы особенно постараться, чтобы этот довольно распространенный случай работал? На самом деле, мы можем заставить его работать, если замедлим компилятор. Если мы реагируем на неудачный перевод сторожа, проверяя, являются ли все метки ветвей буквальными классами, мы можем это исправить. Если бы охранник был сложным выражением, это имело бы заметный эффект. Однако, если это просто доступ к локальной переменной, никто никогда не заметит эффекта. То, что сегодня может показаться простым, в будущем может стать уродливым. Если мы сейчас добавим это правило, нам, возможно, придется включить в него обработку переключения строк. Не так приятно. Но возможность сказать программистам, что они могут использовать switch практически в любых случаях, с которыми они когда-либо столкнутся, вполне может того стоить.