Пользователь
0,0
рейтинг
5 ноября 2015 в 07:43

Разработка → Где находиться типу: справа или слева?

Как-то увидев очередную статью на Хабре, посвященную для меня совершенно новому и неизведанному языку 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 — все они используют определение переменных такого типа. И они построены на парадигмах ООП и не используют (или практически не используют) передачу указателей на функции, все это нам заменили классы. Недостатки, которые выявляются у определения типа переменной слева, улетучиваются при использовании ООП.
@BOBS13
карма
26,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (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
          И парсить проще ;-)
      • 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
        Да, Вы абсолютно правы.

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