На прошлой неделе команда Go опубликовала страницу с подробными проектами спецификаций для обобщений Go 2 и инструментов обработки ошибок. Предложение одновременно хорошо продуманное и смелое, поскольку одна из основных идей - ввести систему контрактов, аналогичную концепциям C ++. Контракты, на языке Go, представляют собой (возможно, синтаксически ограниченные) именованные функции, параметризованные как по значению, так и по его типу, которые используются компилятором для обеспечения того, чтобы некоторый контракт удерживался указанными типами. В контрактах замечательно то, что процедура проверки контрактов очень проста - это всего лишь тело функции, компиляторы проверяют типы, компилируют, а затем отбрасывают свое тело и все.

Но за простоту компилятора часто приходится платить пользователю. Этот пост представляет собой попытку проанализировать предложение и собрать все заметки, которые я сделал во время чтения документа. В разделе Синтаксис дается несколько замечаний по предлагаемому синтаксису, а в разделе Семантика описывается… ну, семантика.

Синтаксис

Одна из самых важных маленьких вещей в Go - это минималистичный и чистый синтаксис. Как правило, предложение остается неизменным, но есть несколько моментов, которые в некоторых местах делают слишком много.

Объявления параметров типа функции

Поскольку методы, похоже, не имеют параметров типа, для автономных функций они могут быть объявлены вместо аргумента приемника метода:

func (type T) Print(slice []T) {
    for _, v in range slice {
        fmt.Printf("%v", v)
    }
}

Это одновременно упрощает форму (type ...)(parameters ...) и более логично с точки зрения того, что параметры типа и обычные параметры должны быть четко разделены. Проблема в том, что такой синтаксис немного неоднозначен визуально, но все же он намного проще с точки зрения записи, чем (type ...)(parameters ...).

Контракт как другой вид типа

Чтобы избежать сложностей совместимости с Go 1 с созданием контракта ключевым словом, контакты можно рассматривать как еще один вид объявления типа, т. Е. Использовать синтаксис.

type Convertible(_ To, f From) contract {}

Однако это может создать семантическую проблему, поскольку Go обычно придерживается прямого сопоставления между типами и представления значений в памяти: структуры сопоставляются с последовательностями именованных полей, интерфейсы сопоставляются со значениями с двойным указателем или парами (val, vtable), в то время как контракты не сопоставляются ни к чему, поскольку они являются исключительно конструкциями времени компиляции.

Семантика

Контракты или нет?

Заявление об этом так далеко в посте может быть странным, но одно из первых замечаний, которые я сделал по поводу предложения контракта, заключается в том, что мне на самом деле не нравится идея иметь функциональные тела, которые не являются функциональными телами, что может вводить в заблуждение. Введение ограничений для контрактных тел требует расширения грамматики Go и отсутствие каких-либо ограничений может вызвать травмы глаз при чтении контрактных тел с _4 _ / _ 5 _ / _ 6_ утверждениями. Разрешение только операторов присваивания и выражений в теле контракта не помешает ему содержать какую-либо логику, помимо простого объявления набора разрешенных операций, что по-прежнему может вводить в заблуждение читателя.

Рассмотрим следующие конструкции:

type Pair struct {
    first, second int
}
type Cons struct {
    head, tail int
}

Просто взглянув на эти объявления, мы можем сказать, что они структурно идентичны и, согласно спецификациям Go, даже имеют одинаковое представление в памяти. Другими словами, эти две структуры изоморфны, и даже если мы никогда не работали с языком Lisp и не видели структуру Cons раньше, мы можем легко вывести, как она себя ведет, поскольку она изоморфна Pair, a простая структура всем известна.

Аналогичным образом рассмотрим эти два интерфейса:

type Index interface {
    Clear(i int)
    Set(i int, value string)
}
type StringMap interface {
    Insert(k string, value interface{})
    Remove(k string)
}

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

Теперь рассмотрим эти два контракта:

contract strseq1(x T) {
    []byte(x)
    T([]byte{})
    len(x)
}
contract strseq2(a T) {
    var c int = len(a)
    var bs []byte = []byte(a)
    T(bs)
}

Они объявляют один и тот же набор операций, но для того, чтобы сказать, что вы должны разместить их рядом и немного подумать. Нелегко неформально проверить, что эти два контракта изоморфны, и чрезвычайно сложно сделать это формально (см. Изоморфизм графов). Другими словами, структуры и интерфейсы декларативны и просты для понимания, в то время как контракты являются обязательными, и чтобы понять контракт, вы должны следовать их утверждениям. Тем не менее, все, что они делают, - это просто объявляют набор операций над некоторыми типами. В Go уже есть интерфейсы для этого, поэтому более простой подход - добавить методы операторов, придумать способ ограничения перегрузки и просто использовать интерфейсы.

Контракты от интерфейсов

Предполагая, что контракты здесь, чтобы остаться, мы можем пойти на компромисс между контрактами и интерфейсами, разрешив использовать имена интерфейсов в качестве имен контрактов в объявлениях, чтобы повторно использовать тривиальные объявления контрактов, т.е. каждое объявление интерфейса должно генерировать эквивалентный контракт, как показано ниже:

type ReadWrite interface {
    Read() string
    Write(string)
}
contract ReadWrite(x T) {
    val s string = x.Read()
    x.Write(s)
}

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

Вывод

Текущее предложение дженериков Go очень хорошо продумано и в значительной степени позволяет избежать всех проблем с системами параметрического полиморфизма в таких языках, как C ++ и Java. Контракты - очень интересная концепция (каламбур), но у них есть некоторые семантические недостатки, которые еще не решены, и, возможно, никогда не будет простого решения. Имея это в виду, если есть какие-либо изменения, чтобы заменить их чистыми и чистыми старыми интерфейсами, мы должны сделать все возможное, чтобы попытаться избежать добавления их в язык в конце, поскольку после добавления таких функций очень трудно избавиться .