Pull to refresh

Comments 58

Разработчики никогда не позиционировали "Go 2.0" как мажорный релиз, в котором всё сломают и перепишут. По крайней мере, в блогах они пишут, что не стоит на это расчитывать в ближайшем будущем.
Это скорее дополнение к стандартному proposal evaluation process (который не позволяет вносить изменения, значительно меняющие язык), позволяющее, собственно, предлагать изменения вроде дженериков, которые будут значительно менять язык

Само собой.


Но, как я всегда считал, Go 2.0 может сломать Go 1.x Compatibility Promise (который, конечно, всё равно нарушают в некоторой степени)


Добавление дженериков, конечно, этого не должно сделать — это расширение языка, обратную совместимость не должно сломать.

После введения дженериков ряд стандартных пакетов (из очевидного — container) хотелось бы видеть переделанными с пустых интерфейсов на дженерики, что, конечно, сломает обратную совместимость.
Самое то для go2 :)

UFO just landed and posted this here
И в этом примере я не вижу дженериков. Тоже самое можно же интерфейсами написать.

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

UFO just landed and posted this here

Для го дженерики намного менее нужны, чем в других языках. Поэтому их и добавляют только через 10 лет

Нужно понимать, что любой язык вполне живёт без дженериков. К примеру, в JS вообще нету типизации и ничего.


Отсутствие дженериков в Гоу приводит только к одному — динамической типизации в кучи моментов. А динамическая типизация в большинстве случаев — это объективный недостаток.


Но да, вполне можно программировать на динамических языках. Только зачем?

В Go не может быть динамической типизации, потому что это статически типизированный язык. Я понятия не имею, что вы имели под этим в виду, но пустой интерфейс — это скорее аналог void * с RTTI заголовком. Вы же не назовёте C — языком с динамической типизацией из-за того что там есть void *. Непустые интерфейсы же похожи на VMT. В С++ почему-то от наследования не отказываются из-за этого


Отсутствие дженериков в Go приводит только к тому, что люди, которые либо не писали на Go, либо переносят опыт любимых ООП языков, в интернете говорят что Go невозможно пользоваться без дженериков


Класс задач, в которых они нужны, не такой большой. Дженерики могут сделать решение некоторых задач удобнее и упростить написание библиотек, но нет никакой кучи моментов, где без них не обойтись

Ну interface — это тот же object из соседей, ну или any из тайпскрипта. Т.Е. любой тип. Простите, но типизацию, где мы что-то типизируем как "любой тип" мне сложно назвать статической.


Да, если вы будете использовать в си указатель, который позволяет указывать на любой тип, я скажу, что конкретно в этом месте у вас динамическая типизация, по определению)


То, что вы не понимаете, где массово используются дженерики не означает, что они не нужны)


А на Гоу я писал. Отвратительный язык. И не только из-за дженериков. К примеру из-за уродских динамических тегов.

Ну так не пишите, если не нравится. Зачем вы приходите в каждый пост про Go говорить, какой он отвратительный?

Хочу и говорю. Это называется "общаться". Не нравится? Не читайте и не отвечайте.


Тем более я не могу промолчать если кто-то открыто заблуждается.

void* — вполне себе динамическая типизация, именно потому что это "что-то с чем-то" и вместо ошибой типов в компайл тайм мы имеем рантайм ошибка "Ожидалось что придет объект типа Х а пришел типа У". Динамическая типизация — термин очень обширный и каждый понимает свое, но обычно именно что-то в этом духе и имеется в виду

По вашему, Java — тоже динамически типизированный язык? Ведь Object для объектов Java играет точно ту же роль, что и interface{} в Go.

Но нет, никто не называет Java динамическим: все понимают, что Object — полиморфизм, а не динамическая типизация.

Но почему-то не все понимают, что interface{} — такой же полиморфизм, обобщённый на все типы языка, а не только на объекты.

Динамическая типизация — это когда язык сам умеет приводить значения переменных динамического типа к реальному типу. Если бы можно было написать:
a, b := interface{}(3), interface{}(7)
fmt.Println(a + b)
— это была бы динамическая типизация.

Но, нет: всё, что позволяет Go — только копировать значение interface{}. Чтобы использовать значение, необходимо явное ручное приведение interface{} к его реальному типу. Никакой динамической типизации тут и близко нет.

Ещё раз. Я не говорю, что весь язык динамический, но конкретно у этой механики — динамическая природа.


это когда язык сам умеет приводить значения переменных динамического типа к реальному типу

Ну уж нет, это слабая типизация, вот даже цитата из википедии вам:


характерными атрибутами [слабой системы типов] являются понятия приведения типов

Я напомню, что Питон не приводит никакие типы автоматически, но в нём всё ещё динамическая типизация.


И да, если на Джава писать все переменные в виде object, то это будет написание кода в динамическом стиле. Ты можешь в любой метод передать всё, что угодно, а уже локально в рантайме, а не компайл тайме будет проверяться тип.


Хочу заметить, что в том же c# использование object считается дурным тоном и явно указывает на то, что у вас проблемы с типизацией.

class Foo {
    public void Test() {}
}

/**
 * Классический строго-типизированный код
 */
public class Static
{

    public void Main()
    {
        Method(new Foo());
    }

    private void Method (Foo foo) {
        foo.Test();
    }

}

Далее — динамический код, при передаче в метод непривального аргумента, например Method(new Bar()); ошибка будет не в компайл тайме, как в статически типизированных языках, а в рантайме, как в динамически типизированных языках и да, это динамически типизированный код на C#


public class Dynamic
{
    public void Main()
    {
        Method(new Foo());
        Method(new Bar()); // we still can
    }

    private void Method (object foo) {
        ((Foo) foo).Test();
    }
}
Хочу заметить, что в том же c# использование object считается дурным тоном и явно указывает на то, что у вас проблемы с типизацией.

Сомневаюсь, что в Java писать все переменные в виде object не считается дурным тоном, и в C использование void * везде не считается дурным тоном. В Go точно так же использование interface{} никто не рекомендует использовать там, где это не нужно.


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


Ну и что, скажете вы? Какая разница, если его всё равно используют как костыль, потому что нет иных инструментов?
А разница в том, что interface{} нельзя никак использовать, потому что он не реализует никаких методов. Чтобы что-то с ним сделать, его нужно принудительно привести к статическому типу.


Если вы захотите, например, использовать std::set в С++ для каких-то собственных классов, вам нужно будет в этот класс добавить хотя бы operator<, несмотря на то, что std::set является шаблонным. В Go, если вы хотите использовать "sort", вам точно так же нужно, чтобы объекты реализовывали sort.Interface.
Разница с языками, у которых есть чёткие дженерики только в том, что для стандартных типов вроде int, string и т.д. такие операции нужно явно делать вручную.


В нормальных программах поведение описывается интерфейсами, которые реализуют необходимые методы, что в большинстве случаев покрывает любые требования. Исключением являются лишь т.н. "general purpose" библиотеки, в которые действительно необходимо передавать "что угодно" вроде библиотек логирования или работы с БД у которых своя система типов.

Чтобы что-то с ним сделать, его нужно принудительно привести к статическому типу.

Вот именно!


И при ошибке типа будет что?
Правильно, ошибка в рантайме.


А ошибка в рантайме при неправильном типе у нас в каких языках?
Правильно, динамических!


А нафига такую хрень вообще использовать?
Правильно, потому что нету дженериков.


Я рад, что мы с вами сошлись.

Я приведу один пример — модуль sort, который написан авторами гоу и который должен сортировать массивы. Он позволяет отсортировать только массивы из нескольких встроенных массивов — тех, что авторы сами лично определили в библиотеке:


type IntSlice []int
type Float64Slice []float64
type StringSlice []string

То есть да, у вас есть три типа, которые вам можно использовать, а остальные — нет.


Более того, код функции Swap в этих трёх типах был просто продублирован три раза. Да, я совершенно серъёзно, они закопипастили один и тот же код три раза. Более того, чтобы расширить возможности этой библиотеки ещё на другие типы — этот код снова надо копипастить для КАЖДОГО типа, который вы хотите поддержать.


func (x     IntSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x Float64Slice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x  StringSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }

В СиШарпе, к примеру, ты можешь сортировать массив из любых элементов, которые реализуют IComparable, ну или передать отдельный IComparer<T> — сравниватель двех элементов.


И, представьте, ничего не копипаститься для каждого нового типа.

В СиШарпе, к примеру, ты можешь сортировать массив из любых элементов, которые реализуют IComparable

который точно так же скопипащен в каждом типе:


        public int CompareTo(int value) {
            // Need to use compare because subtraction will wrap
            // to positive for very large neg numbers, etc.
            if (m_value < value) return -1;
            if (m_value > value) return 1;
            return 0;
        }

        public int CompareTo(Int64 value) {
            // Need to use compare because subtraction will wrap
            // to positive for very large neg numbers, etc.
            if (m_value < value) return -1;
            if (m_value > value) return 1;
            return 0;
        }

        public int CompareTo(String strB) {
            if (strB==null) {
                return 1;
            }

            return CultureInfo.CurrentCulture.CompareInfo.Compare(this, strB, 0);
        }

Но вы не понимаете, это другое?

Сравнение типа писать — нормально. Ведь разные типы вполне могут сравниваться одинаково. Да даже в рамках одного типа сравнение может идти по разному. Строки можно сортировать по алфавиту, по длине, по сумме кодов цифр. Обратите внимание, к сравнивателю я не имел претензий.


А вот свап двух элементов массива писать — глупо. Ведь как два элемента массива между собой поменять — совершенно не зависит от наполнения массива. Это глупый, ненужный копипаст. Разницу улавливаете?


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

Сравнение типа писать — нормально

Тогда, если вы приводите контрпример в виде свопа двух элементов — то пример свопа и надо приводить.
Но мне почему-то кажется, что, к примеру, для свопа элементов в linked list и array код свопа тоже разный будет (но здесь возможно и нет, думаю если есть возможность определить оператор скобок, то и код свопа одинаков будет). Пакет sort не делает предполжений о том, что ему передают именно массив.


Пакет sort позволяет отсортировать вам слайсы любого типа, которые реализуют интерфейс sort.Interface ровно точно так же, как C# позволяет отсортировать коллекции объектов, типы которых реализуют IComparable.


сам по себе сравниватель должен лежать в клиентском коде, возле самих типов

Он там и будет лежать, потому что для клиентских типов слайсов вы его и будете определять в клиентском коде. Разница в данном случае лишь в том, что в C# вам надо для типа элемента массива определить CompareTo, а в Go — для всего массива метод Less.
А для стандартной библиотеки языка "клиентским кодом" будет являться сама стандартная библиотека, поэтому в ней и лежит сравниватель для типов, определённых непосредственно в самом языке.

Но мне почему-то кажется, что, к примеру, для свопа элементов в linked list и array код свопа тоже разный будет

И это тоже нормально. Вот только на примере СиШарпа для Гоу нужно было бы переопределять своп не LinkedList и ArrayList.


А List<Cat>, List<Dog>, List<Animal> и всех остальных листов. Для каждого возможного Т в листе, который мы хотим сортировать нужно определить три метода.


А если мы хотим сортировать ArrayList<Cat> и LinkedList<Cat>, то для каждого из них — тоже заново написать функцию сравнения котов. Понимаете в чём разница?

Понимаете в чём разница?

В чём разница — я понимаю, но в СиШарпе вам так же нужно написать какие-то примитивы, чтобы пресловутая сортировка в контейнерах работала. Да, их будет меньше. Да, их не нужно писать для каждого контейнера. Но есть нюанс: во время работы вам нужно сортировать не "каждый возможный T", а вполне конкретный контейнер с объектами конкретного типа. Не во всяком проекте нужно сортировать много различных контейнеров, у которых различное содержимое.


Не во всяком проекте вообще нужно сортировать хоть что-нибудь. Зачастую при работе с данными из БД они уже приходят сортированными и от используемого языка это зависит примерно никак.
В таком случае, если вы не будете использовать сортировку, какое преимущество вы получаете от дженериков, если в любом языке вам не нужно будет писать дополнительный код для этого?


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

И, представьте, ничего не копипаститься для каждого нового типа.

В mscorlib код Console.WriteLine выглядит вот так:


        [HostProtection(UI=true)]
        [MethodImplAttribute(MethodImplOptions.NoInlining)]
        public static void WriteLine(float value)
        {
            Out.WriteLine(value);
        }   

        [HostProtection(UI=true)]
        [MethodImplAttribute(MethodImplOptions.NoInlining)]
        public static void WriteLine(int value)
        {
            Out.WriteLine(value);
        }

        [HostProtection(UI=true)]
        [CLSCompliant(false)]
        [MethodImplAttribute(MethodImplOptions.NoInlining)]
        public static void WriteLine(uint value)
        {
            Out.WriteLine(value);
        }

И так 15 раз. Да, я совершенно серьёзно, они закопипастили один и тот же код 15 раз. В последнем случае декораторы разные, но в остальном код почти для всех типов один и тот же.
Кроме того, чтобы WriteLine какую-то полезную информацию выводил для своих классов, придётся переопределить ToString, скорее всего. Но я сварщик не настоящий.

Ещё раз. Я не говорю, что весь язык динамический, но конкретно у этой механики — динамическая природа.
Ещё раз: вы путаете динамику и полиморфизм. Присваивание переменной типа interface{} значения, реализующего interface{} — это именно полиморфизм, а не динамика.
Я напомню, что Питон не приводит никакие типы автоматически, но в нём всё ещё динамическая типизация.
У вас странные представления о Python: 25 + True (абсолютно корректное Pytnon-выражение) — типичное для языков со слабой типизацией автоматическое преобразование типов.

Но, похоже, вы не поняли: я говорил не про автоматическое преобразование значения в значение другого типа (это и есть слабая типизация), а про возможность языка автоматически воспользоваться информацией о реальном типе значения, присвоенного переменной-интерфейсу (interface{} — лишь частный случай такого присваивания). Именно наличие этой возможности отличает динамическую типизацию от статической. И именно потому в Go динамической типизации нет.
Ещё раз: вы путаете динамику и полиморфизм

Ничего я не путаю. Способ указать "тут может быть что угодно" ещё не называется полиморфизмом, как и то, что я могу спрыгнуть с горы ещё не означает, что я умею летать, хотя в воздухе какое-то время я, конечно пробуду. То, что оно в гоу называется красивым словом "interface" ещё не означает, что это сразу стало полиморфизмом, а то, что в TypeScript называется словом any — не стало. Более того, сам по себе полиформизм ещё не означает отсутствие динамики. Полиформизм есть точно так же в полностью динамических языках.


Суть полиформизма в том, что вам не нужно уточнять тип, чтобы с ним работать. Ну, к примеру:


interface IFoo {
    void DoSmth();
}

void Method (IFoo foo) {
    foo.DoSmth();
}

class Bar : IFoo {};
class Qux : IFoo {};

Method(new Bar());
Method(new Qux());

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

к примеру

Но ведь интерфейсы в Go делают тоже самое в компайл-тайме...


type DoSmther interface {
    DoSmth()
}

func Method(DoSmther foo) {
    foo.DoSmth()
}

type Bar struct { }
func (*Bar) DoSmth() { }

type Qux struct { }
func (*Qux) DoSmth() {}

Method(&Bar{})
Method(&Qux{})

И конкретно это — абсолютно нормальный код. И конкретно это — полиморфизм. Вот только вы говорите сейчас про совершенно другую механику.
Мы обсуждаем необходимость использования пустого интерфейса в Гоу. В обычном коде на других языках вы врядли встретите постоянное использование object или any, а в Гоу — это в порядке вещей.

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


В Go использование пустых интерфейсов — не в порядке вещей. Я специально прямо сейчас посмотрел, где в проекте, с которым я работаю, используется пустой интерфейс. Таких пакетов всего три: обёртка над protobuf для сериализации и отправки пакетов, пакет для сериализации объектов и записи их в etcd и пулер для БД, который просто аргументы передаёт в pgx. Во всех этих местах при этом явного ручного приведения типов нет нигде. Да, они внутри encoding/json и библиотеке для protobuf есть, но это входит в класс задач, для которых, как я не раз упоминал, без интерфейсов не обойтись. Но это не "в порядке вещей", а понятные задачи.

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

Этот код я привожу как объяснение того, что такое полиморфизм. А пустой интерфейс — это аналог типа object. Вы читайте, пожалуйста, внимательнее.

Да, они внутри encoding/json и библиотеке для protobuf есть, но это входит в класс задач, для которых, как я не раз упоминал, без интерфейсов не обойтись.

В силу недостаточной выразительности языка.

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

Ещё как нужны. Есть слайсы, мапы и каналы, и они как раз таки обобщённые.

В черновике 2019 года для задания ограничений вводилась новая сущность «contract»: go.googlesource.com/proposal/+/master/design/go2draft-contracts.md

В актуальном варианте от контрактов отказались в пользу интерфейсов:
go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md

Наконец Го становится более-менее юзабельным?

Дженерики нужны далеко не всем. Так что, для многих, он уже давно юзабельный)

Он нужен для наверное почти всех библиотек которые сейчас подключены у меня в проекте. Ничего серхъестественного: логгеры, ормки, диай,…
Библиотеки тоже нужны не всем?

Библиотеки тоже нужны не всем?
Я об этом не говорил. И даже не делал такого вывода, разумеется.

Я имею ввиду, что не все пишут библиотеки общего назначения. Кто-то пользуется библиотеками и не очень переживает, есть внутри дженерики или нет. И вполне себе пишут много разного бизнес-код и прочего.


Я не против дженериков. Я против вывода, что без них язык "недоязык"

Я имею ввиду, что не все пишут библиотеки общего назначения. Кто-то пользуется библиотеками и не очень переживает, есть внутри дженерики или нет.

Но пользуются-то все. И вот как раз для пользователя есть огромная разница в удобстве работы. Можно просто посмотреть на сишарп версии 1.1 и 2.0, тоже без и с генериками.


Я не против дженериков. Я против вывода, что без них язык "недоязык"

Ну, я таких утверждений конечно не делаю, но мне определенно трудно представить продуктивную разработку без них. Пробовал — не получилось. Если в динамике ещё можно как-то накостылить манкипатчингом объектов в рантайме и прочей грязью, то тут и этого не было

А у меня вроде бы получается. Да, есть места, где их не хватает, бесспорно. Но в целом жить можно. Повторюсь, все это мы обсуждаем в контексте о "более-менее юзабельность")

Да, я считаю его неюзабельным. Вы правда хотите пробовать меня переубедить в этом?

Неа, не собираюсь. Ну будем считать, что тоже поделился своим мнением. Без претензий )
Я охотно верю, что он может быть юзабельным для одного и неюзабельным для другого.

Interface types used as type constraints can have a list of predeclared types; only type arguments that match one of those types satisfy the constraint.

То есть всё-таки решили накостылить. Не будет унификации между встроенными и пользовательскими типами.

Перешел с Php на Go. При всех его преимуществах и особенностях, к которым можно привыкнуть, []type.interface != []interface и отсутствие дженериков это реальная боль. С дженериками можно будет уже работать с более привычными структурами.
в PHP тоже нет дженериков )
Так они там и не нужны. ЯП с динамической типизацией.
UFO just landed and posted this here
После введения в PHP 7 нормального контроля типов пошли запросы на добавление в язык обобщённого программирования. А в PHP 8 возможности типизации ещё расширили… Так что и в PHP ждём.

P.S. Существующие в языке типажи (trait) очень плохо сочетаются с типизацией.
Полагаете, что те парни, которые кодили и кодят Syncthing (к примеру, весьма крупный Go- проект) испытывали и испытывают реальную боль от отсутствия generic?
Осталось дождаться возможности циклической загрузки пакетов без проблем :(
Sign up to leave a comment.

Other news