Tyr 3 — Правильный заказ на доработку

Порядок уточнения — это концепция, введенная Адой в 80-х годах. Вкратце, в нем говорится, что компилятор должен динамически пытаться разработать части программы в правильном порядке. Это альтернатива предварительным объявлениям, которые вы, возможно, знаете из языков C-стиля. Выразительность предварительных объявлений и порядок уточнения одинаковы, если степень детализации одинакова. Причина, по которой эта концепция почти забыта, заключается в том, что гранулярность Ады очень грубая, из-за чего она больше похожа на препятствие, чем на помощь. Фактически, он в основном основан на целых пакетах, что кажется настолько плохим, что, по-видимому, никто больше не подхватил эту концепцию снова. Пока я не сделал это в Тире и не попытался доказать, что это делает жизнь лучше. Теперь я наткнулся на пример, который одновременно иллюстрирует концепцию и показывает, почему так сложно правильно реализовать ее.

Я хотел показать, что что-то вроде статических локальных переменных C может быть реализовано в Tyr как образец того, что уже присутствует в Tyr. Для этого мы, по сути, создаем тип, содержащий локальное состояние функции другого типа:

type T {
  // make x visible in T's body. We could also access it explicitly,
  // but that's not the point here.
  with T_f_GlobalState.x

  // use x in f
  type def f = ++x

  // if we'd define a g, using x the same way would not be allowed
  // type def g = ++x
}

// define a type that holds the state
type T_f_GlobalState {
  // private can be qualified with a scope.
  // We take T.f because that is where x should be visible.
  // x is also visible in the enclosing type T_f_GlobalState.
  // Hence, the extra type holding x.
  private[T.f] type var x = 0
}

Теперь должно получиться так, что x является своего рода счетчиком вызовов f. По-видимому, текущий альфа-компилятор Tyr 0.7 не смог перевести это с сообщением об ошибке, в котором утверждалось, что объект x не виден в f при попытке перевести тело f . Хорошо, это удивительно. Собственно, мне пришлось отладить компилятор, чтобы понять, что там происходит. Чтобы объяснить это, давайте перепишем тот же пример, добавив несколько номеров к интересным местам:

type T {
  with T_f_GlobalState.x // (1)

  // use x in f
  type def f /* (2) */ = ++x
}

type T_f_GlobalState {
  private[T.f /* (3) */] type var x = 0
}

Итак, (1) импортирует x из глобального состояния в T. (2) является определением f. И (3) использование f в объявлении видимости x. Что на самом деле вызывает цикл, так это то, что (2) является определением без типа. Определение члена без типа в Tyr означает, что его тело немедленно обрабатывается, а тип тела используется как тип f. Если указан явный тип, компиляция происходит, как и ожидалось. Я проверил.

Итак, произошло следующее: уточнение видимости x (3) выполнило разрешение перегрузки для T.f. Это прекрасно. Однако текущая реализация должна была принудительно разработать тип T.f в этом случае (2). Теперь интересно то, что способ, которым я реализовал импорт (1), предполагает, что он в принципе никогда не может дать сбой. Это не совсем правильно, потому что причина, по которой (3) была разработана, заключается в том, что разработка видимости в настоящее время является частью разработки типа члена, то есть (3) фактически была вызвана попыткой выяснить, что на самом деле (1). Плохая сторона этого моего заблуждения в том, что не получится даже ошибки в заказе на доработку, объясняющей ситуацию.

Разорвать цикл

На данный момент я должен отметить, что большинство языков просто примут текущее поведение. Большинство компиляторов реализованы для обработки программы сверху вниз в несколько проходов и делают любой результат. И спецификации языка чаще всего находят причины, оправдывающие такое поведение. Однако мне это не нравится, потому что это может привести к трудным для понимания ошибкам, особенно когда затрагивается инициализация объекта. Другой распространенной реакцией на такие проблемы является ограничение выразительности языка. Два очевидных варианта: ограничение грамматики для импорта (1), позволяющее переводу полностью обходить разрешение перегрузки, и ограничение видимости (3) типами, поскольку в таких случаях они в любом случае не требуют какой-либо проработки. Изменение (2) не является вариантом, потому что это значительно уменьшит выразительность языка, по существу удаляя вывод типа.

Или, мы стараемся немного больше и быть лучше, чем наши конкуренты. Один из вариантов — переместить видимость в отдельную категорию разработки. Такое изменение решило бы проблему здесь, но это также потребовало бы много работы. Другой момент, который пришел мне на ум, заключается в том, что мне как человеку не нужно понимать определение f, чтобы решить, что T.f в (3) является функция, определенная в (2). Просто потому, что в любом случае это единственная функция, и я не буду использовать этот тип в этом контексте.

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

type T {
  type def f = f() // f's type would be f's type ⇒ not ok
}

Итак, я сел и попытался протолкнуть разработку участников, насколько мог. Оказывается, это делает работу.

Шаг назад

Хорошо, в этом примере это работает. Скорее всего, он будет работать для большей части кода, когда-либо написанного на Tyr. Но структурно можно было бы столкнуться с той же проблемой, если бы мы выполняли фактическое разрешение перегрузки в (3) с помощью приведения типов. Да, это разрешено, и это соответствует указанию компилятору, какую перегрузку использовать, если есть перегрузки. Но это не настолько срочно, чтобы исправить это сегодня. Однако в долгосрочной перспективе компилятор Tyr должен быть более умным в отношении того, как он обрабатывает импорт. И да, импорт в (1) тоже может использовать приведения. По той же самой причине. Но на данный момент компилятор примет то, что должно работать, и отвергнет то, что не должно.