Где находиться типу: справа или слева?

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



    Начнем с описания синтаксиса объявления переменных в С-подобных языках. В С было решено отказаться от отдельного синтаксиса описания переменных и позволить объявлять переменные как выражения:

    int x;
    

    Как мы видим, тип переменной стоит слева, затем имя переменной. Благодаря этому мы максимально приближаем объявление переменной к обычному выражению. Допустим, к такому:

    int x = 5;
    

    Или такому:

    int x = y*z;
    

    В принципе, все просто и понятно, и вполне логично, посмотрим на определение функций в C.
    Изначально в C использовался вот такой синтаксис определения функции:

    int main(argc, argv)
        int argc;
        char *argv[];
    { /* ... */ }
    

    Типы переменных описывались не вместе с именами аргументов, но потом синтаксис заменили на другой:

    int main(int argc, char *argv[]) { /* ... */ }
    

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

    int (*fp)(int a, int b);
    

    Здесь fp — ссылка на функцию, принимающую 2 аргумента и возвращающая int. В принципе, не сложно, но вот что будет если одним из аргументов будет ссылка на функцию:

    int (*fp)(int (*ff)(int x, int y), int b)
    

    Уже как-то сложновато или вот такой пример:

    int (*(*fp)(int (*)(int, int), int))(int, int)
    

    В нем, если честно, я заблудился.
    Как видно из описания, при декларировании указателей на функции в языках С есть существенный недостаток в читаемости кода. Теперь посмотрим, какой метод предлагает использовать для чтения определения переменных в С Дэвид Андерсон(David Anderson). Чтение происходит по методу Clockwise/Spiral Rule (часовой стрелке/спирали).
    Данный метод имеет 3 правила:
    1. Чтение начинается с неизвестного элемента движением по спирали;
    2. Обработка выражения по спирали продолжается пока не покроются все символы;
    3. Чтение необходимо начинать с круглых скобок.

    Пример 1:



    Следуя правилу, начинаем с неизвестной str:
    • Двигаемся по спирали и первое, что мы видим, это символ ‘[’. Значит, мы имеем дело с массивом
      — str массив 10-и элементов;
    • Продолжаем движение по спирали и следующий символ это '*'. Значит, это указатель
      — str массив 10-и указателей;
    • Продолжая движения по спирали приходим к символу ';', означающий конец выражения. Двигаемся по спирали и находим тип данных char
      — str массив 10-и указателей на тип char.

    Возьмем пример посложнее
    Пример 2:



    • Первая неизвестная, которая нам встречается, это signal. Начинаем движение по спирали от нее и видим скобку. Это означает, что:
      — signal – это функция которая принимает int и…
    • Здесь мы видим вторую неизвестную и пытаемся проанализировать ее. По тому же правилу двигаемся от нее по спирали и видим, что это указатель.
      — fp указатель на …
    • Продолжаем движение и видим символ ‘(’. Значит:
      — fp указатель на функцию, принимающую int и возвращающую…
    • Идем по спирали и видим 'void'. Из этого следует, что:
      — fp указатель на функцию, принимающую int и ничего не возвращающую;
    • Анализ fp закончен и мы возвращаемся к signal
      — signal – это функция, которая принимает int и указатель на функцию, принимающую int и ничего не возвращающую;
    • Продолжая движение видим символ ‘*’, что означает
      — signal – это функция, которая принимает int и указатель на функцию, принимающую int и ничего не возвращающую, и возвращает указатель на…
    • Идем по спирали и мы видим ‘(’, что означает
      — signal – это функция, которая принимает int и указатель на функцию, принимающую int и ничего не возвращающую, и возвращает указатель на функцию, принимающую int…
    • Делаем последний виток и получаем следующее
      — signal – это функция, которая принимает int и указатель на функцию, принимающую int и ничего не возвращающую, и возвращает указатель на функцию, принимающую int и возвращающую void.


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

    Теперь рассмотрим диаметрально противоположный синтаксис, когда тип переменной находится справа от имени переменной, на примере Go.

    В Go переменные читаются слева направо и выглядят вот так:

    var x int
    var p *int
    var a [3]int
    

    Здесь не нужно применять никаких спиральных методов, читается просто
    — переменная a — это массив, состоящий из 3-х элементов типа int.
    С функциями тоже все достаточно просто:

    func main(argc int, argv []string) int
    

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

    Даже сложные функции, принимающие другие функции, вполне читаются слева направо:
    f func(func(int,int) int, int) int
    

    f — функция, принимающая функцию, которая, в свою очередь, принимает в параметрах 2 целых числа и возвращает целое число, и целое число, и возвращающая целое число.

    Вот такие имеет отличия определение переменных в языках семейства C и Go. Очевидно, Go явно в этом выигрывает. Но если теперь вспомнить, какие языки выросли из старого доброго С – это С++, C#, Java — все они используют определение переменных такого типа. И они построены на парадигмах ООП и не используют (или практически не используют) передачу указателей на функции, все это нам заменили классы. Недостатки, которые выявляются у определения типа переменной слева, улетучиваются при использовании ООП.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 56
    • +10
      Для C++ еще есть известный пазл: константный указатель/указатель на константу
      int *const p1
      int const* p2
      const int* p3
      • +4
        Существует простое правило:
        const модифицирует то, что написано прямо перед ним, за исключением (какой С++ без исключений) случая, когда это первое слово в строке. В этом случае, очевидно, модифицирует то, что прямо после.
        И сразу легко понять что
         const int ** const p4
        

        это константный указатель на указатель на константный int.
        • +2
          Существует простое правило:
          const модифицирует то, что написано прямо перед ним, за исключением (какой С++ без исключений) случая, когда это первое слово в строке.

          Хаха, это простое правило в стиле C++, а в стиле C правило звучит так «надо прочитать объявление в обратную сторону».

          const int *const p; // p - это константный указатель на int костантный
          const int *const *p; // p - это указатель на константный указатель на int костантный
          const int **const **p; // p - это указатель на указатель на константный указатель на указатель на int константный
          
          • 0
            Тот же самый самый костыль в другой форме
            const int p; //int константный
            int const * //указатель на константный int
            

            Единообразия нет все равно.
            «Правило в стиле С++» ничуть не сложнее, что модифицирует первый const знают все.
      • –2
        ЕМНИП синтаксис объявлений в C был сделан так, как удобнее разбирать компилятору. В Паскале же наоборот, как удобно читать человеку, за счет усложнения компилятора. В Go, видимо, нашли некий компромисс. :)
        • +13
          Как раз наоборот: когда компилятор C++ строит AST и встречает int name..., он не знаете что будет дальше — объявление переменной или функции, поэтому эту информацию надо или запоминать где-то или возвращаться назад по исходнику. К тому же для языков с подобными грамматиками сложнее программировать восстановление после сбоев во время синтаксического анализа. Для паскаля как раз проще, для него отлично подходит обычный леворекурсивный парсер без наворотов.
        • +2
          Сорри за оффтоп, но плиз, добавьте мягкий знак в слово «находитЬся» в заголовке.
          • +6
            Синтаксис объявления переменных в Си меня вполне устраивает, и я не вижу в нем ничего непонятного; то, что объявление совмещено с выражениями тоже очень удачно.
            А вот для функций я бы предпочел ключевое слово func вначале, а возвращаемый тип после агрументов
            func foo(int x) int 

            принцип очень простой: сначала пишем существующее, затем новое. То есть сначала пишем имя сущности, которая нам известна (ключевое слово или имя типа), а затем вводим новый идентификатор. Лично мне так гораздо понятнее.
            Ключевые слова var и let, повсеместно используемые в новых языках (и не только в Go) для объявления переменных, очень удобны для компилятора. Они снимают любые неоднозачности, связанные с разбором: после них может быть только объявление переменных.
            Удобны ли они для человека? Думаю, кому как, мне не очень. Но это дело привычки.
            А вот объявление функций с ключевого слова было бы действительно удобно — по общему принципу с объявлением структур, классов, перечислений и т.д. Решалась бы путаница с указателями на функции. Упростилась бы работа компиляторов и IDE. Искать объявления функций в коде стало бы легче. Упростилась бы реализация объявления вложенных функций (напомню, еще в Паскале они были, а в современном С++ есть только частный случай в виде лямбд). Появились бы интересные дополнительные возможности: введение имен возвращаемых значений, введение специальных ключевых слов для специальных функций, удобный синтаксис для возврата сразу нескольких возвращаемых значений и т.д.
            • 0
              собственно в языке Rust приблизительно так и сделано:
              doc.rust-lang.org/book/functions.html
              • 0
                В языке Rust сделано почти так же как в Go: типы после имен переменных, ключевые слова let и let mut (вместо var), fn (вместо func) для фукнций.
              • +1
                Видать комитету тоже так удобнее:
                auto (*cb1)(int) -> int;
                
                auto proc(int x) -> int
                {
                  return 31337;
                }
                

                ;-)

                Для особых ценителей можно:
                #define func auto
                
                func proc(int x) -> int;
                
                • +1
                  О нет. Это сделано нифига не для удобства. Просто в шаблонных функциях так бывает, что тип результата зависит от типа параметров — и тогда его описать до имени функции никак не получится!

                  А так да — можно использовать вполне и без шаблонов.
                  • 0
                    Да как бы да. Я прочитал свой пост, пока думал как лучше переписать — время вышло. Махнул рукой — кому нужно, тот поймёт :)
                    • 0
                      Удобство использования стало как бы бонусом, описывать указатели на функции возвращающие функции стало удобнее
                      auto (*func_ptr)(int) -> 
                          auto (*)(float, int) ->
                              int (*)()
                      

                      • 0
                        Это проблема исключительно парсера С++. В C# прекрасно работает так:
                        IEnumerable<T> Where<T>(Funct<T,bool> predicate)
                        

                        То есть T используется еще до указания, что тип-параметр.
                        • 0
                          Не тот случай, в С++ выражение по типу
                          template<class T>
                          IEnumerable<T> Where(Funct<T,bool> predicate)
                          

                          Тоже будут работать без всякого нового объявления. Новый тип объявления нужен в случае когда шаблонный тип один а возвращается совершенной другой. Например.
                          struct A{};
                          struct B
                          {
                             A func();
                          };
                          
                          template<class T>
                          auto Func(T& _val) -> decltype(_val.func());
                          

                          Можно конечно извернутся и слепить нечто такое
                          template<class T>
                          decltype(((T*)(0))->func()) Func(T& _val)
                          

                          но это не совсем красиво, да и не уверен что будет работать везде и всегда.

                          Ну и да, не стоит забывать что в C# дженерики, а не шаблоны, они работают несколько иначе.
                          • 0
                            Ничего что template указывается до любого объявления? Причем это сделано специально чтобы помочь парсеру. Не забывайте, что С++ использует LL парсер. А LL парсер хорошо работает когда смысл написанного правее зависит от того что написано левее. Поэтому и типы слева, и template писать надо. Можно было бы отказаться, но это бы усложнио парсинг и, скорее всего, увеличило бы время компиляции.

                            Ну и да, не стоит забывать что в C# дженерики, а не шаблоны, они работают несколько иначе.

                            Это к вопросу парсинга не имеет никакого отношения от слова вообще.
                            • +2
                              Это к вопросу парсинга не имеет никакого отношения от слова вообще.
                              имеет причём довольно-таки прямое. Так как у нас тип в дженериках предназначен для всяко-разных проверок и, в общем, не вляет на генерируемый код, то кроме типов в угловых скобках ничего указать нельзя. В C++ — можно, откуда и все беды.
                              • –2
                                Парсинг строит синтаксическое дерево, ему по большому счету без разницы как потом это дерево обрабатывается. Вы вообще не о том говорите.
                                • +2
                                  Насколько я понял, речь о том, что парсить вот такое в качестве возвращаемого типа в C++ — норма: decltype(decltype(_val.func())::n + 10)::result_type, а в C# — нет
                                  • +3
                                    Нужно просто уметь парсить нечто зависящее от типа — а для этого нужно уметь понимать где у нас типы, а где — нетипы.

                                    Я считал что этот пример всем, кто берётся рассуждать о тонкостях C++ известен.

                                    Он просто очень выпукло показывают проблему во всей красе: в зависимости от опций компилятора у вас может быть по-разному построено синтаксическое дерево! Не выбраться другая функция и по другому посчитаться константа, нет — по разному будет именно всё разобрано. Без всяких ifdef'ов или define'ов (они-то вообще до компилятора отрабатывают и как бы «не в счёт»).
                                  • 0
                                    Это вы не о том говорите. Возьмите всем известный пример:
                                    int x = confusing<sizeof(x)>::q < 3 > (2);
                                    Так вот в зависимости от того явзяется у вас q типом или переменной у вас будет построено разное синтаксическое дерево. Хабрапарсер выбирает один вариант (тот, который ему больше нравится), но там есть ещё и второй, где вначале считается confusing<sizeof(x)>::q < 3 и вот уже это сравнивается с двойкой.

                                    В C# подобное невозможно потому что дженерики параметризуются только типами.
                                    • 0
                                      Та же самое может быть в C#. Вместо имени типа может оказаться переменная и выражение
                                      IEnumerable может быть воспрнято как (IEnumerable < T ) > что-то там, где IEnumerable и T — переменные. Но у C# грамматика более стройна и не допускает таких ошибок, после имени типа выражение не напишешь, нужно тип в скобки брать, что делает парсинг однозначным,

                                      Например если для C++ запретить приведения типов в операторной форме, то подобной проблемы не возникнет. Да и многих других проблем можно избежать если поправить синтаксис, но из-за совместимости этого не делают.
                      • 0
                        Кому трудно привыкнуть писать в GO var, может использовать оператор :=
                        • 0
                          еще раз… в си тип размазан по определению, за исключением простых типов: в массиве — тип и размерность, в функции — возвращаемого и аргументов, указатели — привязанно к идентификатору, а не типу… а модификаторы… кто во что горазд.
                        • –6
                          В случае с Go некоторые примеры кода выглядят так, будто их скопировали из описания Компонентного Паскаля.
                          Да и зачем, спрашивается, тащить в новый язык полувековые дефекты и костыли, если можно взять что-то более продуманное, удобное и эффективное?
                          • +4
                            Ну я бы не сказал что это полувековые костыли:) Да и кто сказал что в компонентном паскале костыли?
                            Костыли проявляются после более глубокого изучения языка, а подход типа «раз похоже на компонентный паскаль — значит костыли» совершенно неправильный.
                            Вот например кто нибудь знает, что в С/С++ (и также в C#/Java) есть дефект с приоритетом операций? Сможете назвать и обосновать?
                            • 0
                              Костыли — это про сишный синтасис, причём судя по некоторым статьям — число дефектов в том же С++ год от года только растёт.
                              • 0
                                & и |?
                                <<?
                                • 0
                                  Операторы сравнения < <= > >= == != имеет приоритет выше чем битовые операции & | ^
                                  В результате например вот такая вполне логичная конструкция
                                  if(x & 0x07 > 4)

                                  без скобок вокруг «x & 0x07» некорректна.
                                  • +1
                                    Чего это она некорректна? Очень даже она корректна. Это битовое умножение x и 1, то есть 0, если x=0 и 1 в противном случае.
                                    • +1
                                      Ну в смысле корректна, но бестолкова. Для действий с bool есть логические операции && и ||, которые совершенно правильно имеют приоритет ниже чем сравнения.
                            • +6
                              f func(func(int,int) int, int) int

                              Вот зачем было в go изобретать велосипед, если уже давно в ML-образных языках используется синтаксис вроде
                              f : ((int, int) -> int, int) -> int
                              

                              ИМХО, если хотелось сделать как привычнее, надо было брать синтаксис C. А тут хотели как лучше, явно посмотрели в сторону функциональных языков (да и не только их, любая статья по теории типов пестрит подобной нотацией), но почему-то не захотели вводить двоеточие и стрелочку.
                              • +1
                                А зачем двоеточия и стрелочки, кроме как для красоты?
                                • +1
                                  Принято так, вроде
                                  Да и, имхо, нагляднее, чем func. Хотя, может, и дело привычки.
                                • 0
                                  А, ну в ML-языках двоеточие потому, что запись через пробел (f x) — это применение функции (f(x))
                              • +2
                                Перепишем вашу сложную функцию на Go
                                f func(func(int,int) int, int) int
                                с указанием типа слева
                                int f func(int func(int,int), int)
                                и увидим, что и так нет сложностей с прочтением.

                                Даже если взять более сложную функцию из той же статьи.
                                f func(func(int,int) int, int) func(int, int) int
                                (int func(int, int)) f func(int func(int,int), int) 


                                Так что дело не в бобине.
                                • +1
                                  int f func(int func(int,int), int)
                                  Есть проблема с прочтением. В выделенном мной месте непонятно, что следует за int. Анализатору надо заглянуть вперёд, понять, что там func и только потом понять, что это аргумент-функция, а не аргумент-число. Так-то.
                                  • +1
                                    Да что вы все за анализатор то переживаете. На C++ пережевывает и не потеет. Читать люди будут. И у людей есть вполне очевидные проблемы с чтением сложных конструкций в C++.
                                    А в Go что слева ставь тип, что справа – понять можно без проблем, а дальше уже вопрос вкуса и привычки.
                                • +7
                                  Вот такие имеет отличия определение переменных в языках семейства C и Go. Очевидно, Go явно в этом выигрывает. Но если теперь вспомнить, какие языки выросли из старого доброго С – это С++, C#, Java — все они используют определение переменных такого типа.

                                  Очень «тонкий» намек на преимущества Go, по сути бред.
                                  Сложность типов в C вызвана тремя компонентами:
                                  1) Указателями и повсеместным их использованием
                                  2) Отсутствием нормального описания функционального типа
                                  3) const

                                  В C# и Java всего этого нет, поэтому и проблем с описанием типов нет. В С++ только const иногда мешает, да и то не часто. Так что никакого разительного преимущества Go перед современными языками нету.

                                  Кстати писать тип после придумали в ML, лет за 40 до изобретения Go. Там даже еще дальше пошли — аннотации типов применяются не только к объявлениям, но и в выражениями. Это в сочетании с автоматическим выводом типов добавляет удобства в разы.
                                  • +2
                                    Очевидно, Go явно в этом выигрывает. Но если теперь вспомнить, какие языки выросли из старого доброго С – это С++, C#, Java — все они используют определение переменных такого типа. И они построены на парадигмах ООП и не используют (или практически не используют) передачу указателей на функции, все это нам заменили классы. Недостатки, которые выявляются у определения типа переменной слева, улетучиваются при использовании ООП.

                                    Вы, вырвали слова из контекста и все сказано в следующих 2-х предложения.
                                    • –3
                                      Вы, вырвали слова из контекста и все сказано в следующих 2-х предложения.
                                      Разве это изменило суть высказывания?

                                      Расскажите что вы имели ввиду этой фразой.
                                    • +1
                                      К слову, в GO указатели используются повсеместно. И проблем с типами нет :)
                                      • 0
                                        Я вот нашел в интернетах эквивалентные программы на C и Go, и на Go указателей не было вообще, а на C около 20 мест с указателями.

                                        Причина простая — в C массив это указатель, а в Go используются слайсы. Ну и в Go вывод типов есть, а в C типы указываются явно.
                                    • +8
                                      Написание типа после имени — это необходимость, которую осознали слишком поздно. Такой подход, к примеру, позволяет компилятору выводить тип возвращаемого значения из типа аргументов функции. В C++11 даже (в очередной раз) ввели специальный синтаксис для этого:
                                      template <class U, class V>
                                      auto add(U const& u, V const& v) -> decltype(u + v) {
                                          return u + v;
                                      }
                                      

                                      Написать decltype(u+v) вместо auto нет возможности — там компилятору ещё не видны имена (и соответствующие типы) u и v.

                                      Кроме того, как уже упоминалось, такой подход существенно упрощает как компилятор, так и разработку инструментов. Вспомним ключевое слово typename из C++:
                                      template <class Iterator>
                                      void doSomething(Iterator it) {
                                          // Тут необходимо слово typename, чтобы компилятор мог понять, что вы хотите:
                                          // 1) объявить переменную v с типом указателя на Iterator::value_type;
                                          // 2) вызвать Itarator::value_type.operator*(v), где v нужно взять из окружающего контекста.
                                          typename Iterator::value_type * v;
                                      }
                                      

                                      Если бы было ключевое слово для объявления переменных, такой проблемы бы не возникло:
                                      var v: *Iterator::value_type; // объявление переменной
                                      Iterator::value_type * v;     // умножение
                                      

                                      Языку 30 лет, а мой текстовый редактор, к примеру, зачастую не может распознать объявления переменных. Если бы было слово var, риск ошибки был бы практически нулевой.

                                      Ну и мне лично кажется, что подход с объявлением типа после имени делает опциональный вывод типов более логичным.
                                      // Если тип опустить, то компилятор его выводит, логично.
                                      // К тому же, имена всегда выровнены по левому краю.
                                      var x = 5;
                                      var y: int = 5;
                                      var z: MyType = init();
                                      
                                      // Хм... Ок...
                                      auto x = 5;
                                      int y = 5;
                                      MyType z = init();
                                      
                                      • +3
                                        Точно, тот самый знаменитый пример:)
                                        X * Y; // что это - умножение или объявление указателя?

                                        • +1
                                          Нет, typename тут нужен не потому, что непонятно, умножение это или указатель. typename нужен потому, что непонятно, value_type — это тип или значение. Даже если вы уберёте звёздочку, typename всё равно будет нужен.

                                          Кстати, ещё кроме typename периодически нужен template. Например, в коде типа такого:
                                          	template<typename T>
                                          	struct InstanceFunctor;
                                          
                                          	template<typename T, typename F>
                                          	using FmapResult_t = typename InstanceFunctor<T>::template FmapResult_t<F>;
                                          
                                          	template<typename T, typename F>
                                          	FmapResult_t<T, F> Fmap (const T& t, const F& f)
                                          	{
                                          		return InstanceFunctor<T>::Apply (t, f);
                                          	}
                                          
                                          	// Пример объявления FmapResult_t в конкретной специализации.
                                          	template<typename T>
                                          	struct InstanceFunctor<boost::optional<T>>
                                          	{
                                          		template<typename F>
                                          		using FmapResult_t = boost::optional<ResultOf_t<F (T)>>;
                                          
                                          		template<typename F>
                                          		static FmapResult_t<F> Apply (const boost::optional<T>& t, const F& f)
                                          		{
                                          			if (!t)
                                          				return {};
                                          
                                          			return { f (*t) };
                                          		}
                                          	};
                                          
                                          • 0
                                            Я прекрасно понимаю, зачем и когда нужен typename. Я просто привёл самый популярный пример неоднозначности, которая возникала бы, если бы стандарт не предусматривал typename, и которой бы не было, если бы тип переменной шёл после ключевого слова и имени.
                                        • –1
                                          int (*(*fp)(int (*)(int, int), int))(int, int)


                                          пара typedef'ов обычно решает проблему нечитаемости
                                          • +2
                                            Пара typedef'ов обычно решает проблему нечитаемости
                                            Ага, конечно. Особенно если выражение встречается не в коде, а в документации. Пример с сигналом — он же не из воздуха взялся, а из официальной документации.

                                            Хорошо хоть названия параметров сохранились! По синтаксису они там не нужны, но выкидывание fp превратит выражение в паззл:
                                            void (*signal(int, void (*)(int)))(int);
                                            Вообще же — писать можно на чём угодно, хоть на брайнфаке, но то, что у вас выражение в полстроки невозможно понять и требуется сложный анализ производить — это же ненормально…

                                            Но вообще ворос: справа или слева не очень приниципиален. Можно слева (Java), можно справа (Go), главное — не со всех сторон сразу (как в C/C++). Описания переменных — это одно из мест в C/C++, которые сделаны очевидно плохо.
                                          • +1
                                            Спасибо за статью. Ещё, когда тип пишется справа, для меня это удачно укладывается в математическое представление типа как множества принадлежащих ему объектов.
                                            var x int // x принадлежит множеству целых чисел
                                            var p *int // p принадлежит множеству указателей на объекты, принадлежащие множеству целых чисел
                                            var a [3]int // a принадлежит множеству трёхэлементных массивов объектов, принадлежащих множеству целых чисел
                                            

                                            • –5
                                              Где находиться типу: справа или слева?
                                              Нигде. Типы должны выводиться автоматически. Если тип всё же нужно указать явно, то он должен быть указан для всего выражения.
                                              • +1
                                                Это очень субъективно. Мне например намного привычнее Сишный способ, первое время я вообще не понимал что там за мешанина в Go-коде.

                                                Понятие удобства в данном случае очень сильно зависит от того, какой у человека бэкграунд. Единственный объективный способ сравнения — это посадить 100 программистов, которые никогда не писали на языках со строгой типизацией, и замерить сколько времени их мозг тратит на разбор си-образного и го-образного способов объявления типов. Все остальное — субъективщина и холиворы.
                                                • +3
                                                  это посадить 100 программистов, которые никогда не писали на языках со строгой типизацией

                                                  Возможно, вы имели в виду: явной статической. В питоне типизация построже C будет.
                                                  • 0
                                                    Да, Вы абсолютно правы.

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