0,0
рейтинг
31 августа 2013 в 14:24

Разработка → Модульного тестирования недостаточно. Нужна статическая типизация! перевод

Когда я работал над своей магистерской диссертацией, я пообещал себе, что опубликую ее в Интернете под свободной лицензией. Я получил степень, но, к сожалению, натолкнулся на одно из неписаных правил ВУЗов — когда вы тратите на интересующий вопрос много времени, он вам надоедает.
Наконец, спустя год, я все-таки ее публикую.
Для тех, кому лень знакомиться с моим полным трудом (в любом случае, 60 страниц текста это не так много для исследовательской работы, но это все же не мало), я предлагаю краткую версию статьи. Прошу заметить, что краткая версия не учитывает некоторые важные сведения, поэтому я прошу писать отзывы только о полной версии.

В своём исследовании я обратил внимание на доводы сторонников динамической типизации, которые считают, что статическая типизация не нужна для выявления ошибок.

Суть их рассуждений в следующем:
  1. Статической типизации недостаточно для выявления багов, поэтому необходимы модульные тесты;
  2. Статическая типизация становится лишней, так как у вас есть тесты;
  3. Из-за статической типизации некоторые корректные программы могут выдавать предупреждения на стадии компиляции.

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


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

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

Критерии для динамического языка были следующими:
  1. этот язык должен быть динамически типизированным;
  2. язык должен поддерживать модульное тестирование;
  3. необходимо достаточное количество opensource-решений на этом языке;
  4. язык должен быть известным и иметь достаточное количество сторонников.

Под эти критерии отлично подходит Python.

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

Haskell удовлетворял всем этим требованиям.

Далее я выбрал четыре случайных проекта, которые предстояло транслировать с Python на Haskell: The Python NMEA Toolkit, MIDIUtil, GrapeFruit и PyFontInfo.

The Python NMEA Toolkit

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

MIDIUtil

Трансляция MIDIUtil привела к выявлению двух ошибок. Одну из них можно было бы отследить с помощью теста. Еще одну устранила бы статическая типизация. Утилита MIDIUtil использует методы struct.pack и struct.unpack, которые не могут быть транслированы в язык Haskell напрямую, поскольку в них используется строка форматирования, содержащая типы аргументов и возвращаемого значения.
Однако во всех случаях использования, форматирующие строки были жестко закодированы, так что код на Haskell мог использовать вместо них жёстко кодированные функции, не теряя выразительности. Если бы в MIDIUtil такие строки хранились в конфигурационном файле, то для её трансляции в язык со статической типизацией пришлось бы серьёзно поработать над архитектурой решения.

GrapeFruit

Трансляция из GrapeFruit не выявила никаких ошибок. А единственную ошибку во время исполнения могла бы ликвидировать статическая типизация. Следует заметить, что статическая типизация помогла бы избавиться от одного теста. Не нашлось ни одной динамической конструкции, непереводимой в статическую.

PyFontInfo

Перевод из PyFontInfo привел к выявлению 6 ошибок типов. Две ошибки при исполнении можно было бы устранить при использовании статической типизации. От одного теста можно было бы избавиться. PyFontInfo тоже использует struct.pack и struct.unpack, поэтому не может быть переведен в Haskell, но это ограничение можно обойти.

Результаты

Опыт показал, что все проекты без особого труда можно было бы транслировать на язык со статической типизацией, причем изменения были бы минимальными. Выяснилось, что модульное тестирование нельзя назвать адекватной заменой статической типизации. В ходе эксперимента обнаружилось 17 ошибок типизации, которые не были обнаружены тестами; но многие ошибки тестами все-таки покрывались.

Заключение

Результаты эксперимента показывают, что модульное тестирование не может считаться заменой статической типизации и не учитывает все проблемы. Существенной проблемой является и то, что построение грамотного теста для типов трудоемко, а в языках со статической типизацией это делается автоматически. Применение статической проверки типов для программ, написанных на динамических языках, позволило бы выявить дефекты, которые не обнаруживаются тестами. И в то же время не требуют крупной переработки архитектуры.
Опыт показал новый взгляд на проблему избавления от ошибок типизации с помощью юнит-тестов. Надеюсь, что это кого-то заинтересует, и они проведут эксперимент на других проектах.
Полную версию статьи вы можете прочитать здесь.
Перевод: Evan Farrer
Ilya Egorov @Mobyman
карма
39,4
рейтинг 0,0
Developer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

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

Комментарии (124)

  • +22
    Странная постановка задачи, странный вывод по результатам, а уж статистика такая статистика…

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

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

    Ну а статистика… нет информации о том, насколько выявленные ошибки типизации критичны да и вообще заметны конечному пользователю этих продуктов (это баги, дыры в безопасности или просто теоретические/потенциальные но не актуальные в текущей реализации проблемы); нет информации о том, сколько в этих проектах ошибок вообще и какую часть из них составляют ошибки типизации; etc.
    • +21
      Вы никогда не сталкивались с адептами ЯП с динамической типизацией? Их основные доводы сводятся буквально к следующему: динамическая типизация — это круто, это свобода, равенство, упячка^H^H^H^H^H, а тесты дадут нам то, что вам, ходящих в кандалах статической типизации даёт эта самая типизация.

      Исследование как раз и показывает, что свобода — это фикция (когда я пытаюсь в проектах с динамическими языками ей воспользоваться народ сразу начинает стонать и говорить, что разобраться в моих программах нельзя), что 100x-1000x замедление платится в большинстве своём за ускорение REPL-цикла и больше ни за что.

      Что значит «необходимо гарантировать отсутствие ошибок типизации», кстати? Это вообще дикое требование какое-то. Статическая типизация приводит к тому, что целый определённый класс ошибок, который не всегда отлавливается тестами, становится невозможен. Взамен мы теряем некую абстрактную «свободу», но если этой свободой на приктике никто не пользуется, то стоит ли грустить о её потере?
      • +1
        А вы писали на динамически типизированных языках, например питоне? Я писал, свобода действительно есть, и ей пользуются. Например любому объекту прикрутил метод __call__, и вуаля, он стал функтором.
        • +1
          Можете развернуть пример? Не могу придумать чего-либо, чего я не смог бы повторить на Haskell.
          На Python я писал, но немного (SublimeHaskell). И в моём случае опыт скорее отрицательный. Нельзя просто взять и поменять тип переменной и поправить места, на которые укажет компилятор.
          • 0
            Я не знаю Хаскель, да и Питон не очень. Но мне вот просто интересно: а можно на Хаскеле вот так сделать?

            var MyClass = function () {
               if (condition) {
                   BaseClass1.call(this);
               } else {
                   BaseClass2.call(this);
               }
            }
            • 0
              С учётом того, что в Haskell нет классов, я не понимаю, что этот код делает.
              • 0
                > Не могу придумать чего-либо, чего я не смог бы повторить на Haskell.

                Эээ. Классы?
                • +8
                  Речь не о шашечках, как я понял, а о практической пользе.
                  Классы ж пишут не чтобы классы были.
                  • 0
                    Я запутался.
                    • +6
                      Просить повторять классы в Haskell это как просить повторить ADT или чистоту в другом языке.
                      Для священных войн подойдёт, а пользы никакой. Всё равно пишут на деле по-разному.
                      Поэтому интерес представляют какие-то более менее реальные задачи.
                      Реализовать классы — это задача, в реальной жизни не стоящая.
                      • –16
                        > Реализовать классы — это задача, в реальной жизни не стоящая.



                        В цитатник, я считаю.
                        • +16
                          Не, ну реально, работа программиста состоит не в создании классов, а в решении реальных задач. Классы — лишь инструмент иногда для решений удобный.
                          • –5
                            Ну как бэ да. Я про это даже пост писал.
                            Существуют предметные области, удобно описываемые в рамках только в рамках ООП. Глупо это отрицать.
                            • –2
                              Хабр, ты окончательно ебанулся.
                            • 0
                              Не знаю за что так заминусовали, не за слово же «только», с которым я не согласен без уточнения «из популярных парадигм».
                              • +1
                                Мне кажется, за картинку не к месту, сводящую всю дискуссию на нет, а остальное по инерции.
            • +2
              Хоть я и не специалист в Haskell, но полагаю, что
              myClass this
              	| condition this = baseClass1_call this
              	| otherwise =      baseClass2_call this
              
              

              А что такого?
          • 0
            Давайте я дам другой пример — мультиметоды.
            Я их реализовывал с помощью нескольких расширений — но, во-первых, не вспомню как (мне не понравилось совершенно), во-вторых по сравнению с простотой «require 'multi'» в руби — небо и земля.
            • 0
              О каких мультиметодах речь?
              Подозреваю, что
              class Multi a b where
                  foo :: a -> b -> IO ()
              

              Это не то, что вы хотите.

              Скорее всего вы хотите что-то типа хранения в списке ссылок на базовые классы и работы с ними. Ну это очень не идиоматично для Haskell, тут даже сам такой список без расширений не завести.
              • 0
                Да понятно, что с расширениями. Без расширений мало что можно обсуждать.

                О чем речь:
                1. Гетерогенный список, который может хранить типы Rocket и Asteroid в коробке
                2. Набор функций collide для столкновения Rocket, Asteroid; Rocket, Rocket; Asteroid, Asteroid как тут: marc-abramowitz.com/archives/2006/03/30/multiple-dispatch-in-ruby/
                3. Функция f, которая принимает 2 таких списка и печатает столкновения

                То есть то, ради чего в Java мы будем городить visitor (двойная диспетчеризация с возможностью дописывания функциональности, а не просто набор свитчей-паттерн матчинга).

                Я реализовывал это, хотя вот в данный момент (прошел год) написать сходу уже не могу нужную комбинацию из forall, запаковывания и распаковывания.
                • 0
                  Даже интересно, как вы это реализовали. Ощущение, что можно проще, но надо видеть реализацию.
            • 0
              Мультиметоды и в статически-типизированных языках реализуемы:
              1. Для C++ Страуструп предложил драфт о вводе в язык мультиметодов. Вот еще.
              2. Я сам разработал шаблонную библиотеку мультиметодов для С++.
              2. С# 4.0 нативно поддерживает. Правда, я не знаю, как там это устроено.

              Так что мультиметоды не прерогатива динамически-типизированных языков.
              • 0
                С# 4.0 нативно поддерживает.

                C# поддерживает динамическую типизацию. Вызываем перегруженный метод Collision((dynamic)arg1, (dynamic)arg2), реальный метод выбирается в рантайме в зависимости от типа аргументов. «Динамичность» здесь только в одном вызове, но это уже не статическая типизация.
          • +4
            писал на python около года, на PHP лет 8. Третий год пишу на Java и только рад смене динамики на статику
            • 0
              И в чем радость? Почему не хватало type hinting?
              • 0
                не знаю как в мире РНР сейчас, но примитивные типы не тайпхинтились до 5.3 точно + возвращаемые значения же

                но справедливости ради я должен сказать что не это было основной причиной перехода с ЯП на ЯП
                • –1
                  А примитивные успешно (иногда слишком) приводились и приводятся один к другому неявно.
          • –1
            class Factorial:
                def __init__(self):
                    self.cache = {}
                def __call__(self, n):
                    if n not in self.cache:
                        if n == 0:
                            self.cache[n] = 1
                        else:
                            self.cache[n] = n * self.__call__(n-1)
                    return self.cache[n]
            
            fact = Factorial()
            


            Далее fact можно юзать как обычную функцию. Повторить можно что угодно, вопрос в том насколько лаконично.
            • +4
              Данный конкретный пример проще написать на замыканиях (даже и в питоне) — а во многих функциональных языках мемоизация коротко реализовывается стандартной high-order function из библиотеки. Или просто выразительностью языка — вот хаскель, например.
            • +3
              Это слово в слово переписывается и на C++.
              Причём тут динамика?

              По поводу Haskell написали выше, можно лишь добавить просто список всех чисел фибоначи
              fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
              fib n = fibs !! n
              
        • +2
          А если в C++ прикрутить operator() то это не то?
      • +5
        Я много лет одновременно пишу и на Perl (слабая динамическая типизация) и на Limbo (строгая типизация). И, честно говоря, мне очень нравятся оба варианта — у каждого из них есть свои плюсы и свои минусы. И мой стиль программирования сильно зависит от используемого языка, его сильных и слабых сторон.

        Так что давайте честно скажем, что дело не в том, какая типизация используется, а в том, что некоторые программисты просто ещё не вышли из детского возраста — отсюда и все эти крики про свободу/равенство с одной стороны и про «целый определённый класс ошибок» с другой. ;-)
        • 0
          Я тоже пишу на языках обоих типов. Все действительно имеет свое преимущество.
        • 0
          Если не секрет, в каких проектах используете Limbo?
          • +1
            Сетевые сервисы, распределённые вычисления. Плюс пишу для себя rogue-like RPG с текстовым и Tk интерфейсами.
  • +3
    Так ведь проверяют не только типы. Проверяют у функций возвращаемые значения и побочные эффекты.
  • +6
    Модульных тестов и статической типизации недостаточно. Нужен опыт и знание языка!

    Кроме того, мы говорим про «какие-то» модульные тесты, которые «что-то» не находят. Так может быть модульных тестов действительно недостаточно, и нужны интеграционные?
    А может быть модульные тесты не находили ошибки потому, что их автору было реально лень писать «все комбинации»?
    В таком случае, можно попробовать инструменты, которые бы автоматически сгенерировали тесты.
    Для C#/VB/F# есть, например, Pex – отличный инструмент, который пытается зайти в каждый участок метода и генерирует тесты в итоге.

    Ну, и пример, почему нужен опыт и знание языка.
    Бывалые C#-перы, конечно же, быстро разгадают пример только с одного пронзительного взгляда. (Но, наверное, еще и потому, что на той строчке акцентируется большое внимание)
    А вот для людей, которые знают язык не настолько хорошо – это может показаться загадкой.
    Зайдите по ссылке и нажмите Ask Pex
    www.pexforfun.com/default.aspx?language=CSharp&sample=EnumSwitch

    using System;
    public enum Color { R, G, B }
    public class Program 
    {
      public static string Puzzle(Color color) 
      {
        switch (color) 
        {
          case Color.R: return "Red";
          case Color.G: return "Green";
          case Color.B: return "Blue";
          default: throw new Exception("impossible"); // Is it really impossible to get here?
                                                      // Ask Pex to find out!
        }
      }
    }
    
    • 0
      А почему так получается? В шарпе энумы можно наследовать?
      • +2
        Нет, энумы не наследуются и имеют «относительно» строгую типизацию, но в некоторых случаях, компилятор можно обмануть.
        Наличие явно задекларированных элементов R, G, B вовсе не означает, что внутри типа Color таких элементов не может быть больше.

        Психоделический код
                static void Main(string[] args)
                {
                    // Неа, компилятор не пустит
                    Puzzle(10);
        
                    // И так компилятор не обмануть
                    int myColor = 10;
                    Puzzle(myColor);
        
                    // Crazy!
                    Color color = Color.R + 10;
                    Puzzle(color);
        
                    // Бинарные операции очень удобны для enum. Но, только если там не завелся бинарный баг :D
                    // Компилятор пропустит и это. 
                    // R = 0 dec; G = 1 dec; B = 2 dec;
                    // 00b | 01b | 10b == 11b (3 dec)
                    Color color2 = Color.R | Color.G | Color.B;
                    Puzzle(color2);
                }
        

    • 0
      Точнее почему не вылетает аналог джавского ClassCastException?
      • +4
        Любой enum — это просто int и считается совместимым с интом. Т. е. можно написать Puzzle((Color)12345) и попасть блок default.
        • 0
          Странная парадигма. Джавская мне нравится больше.
          • +3
            Ну в шарпе enum может представлять собой и флаги (т. е. комбинацию нескольких значений), а ещё его значение могло прийти от неуправляемого кода, который помимо документированных флагов выставляет недокументированные/появившиеся в более новой версии библиотеки. В яве для подобного функционала надо городить костыли и пользоваться EnumSet.
            • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              Но зато в джаве энум почти не связан с числами — это просто именованные инстансы класса. И больше их создать, чем есть — ну никак не получится.
              • +5
                Тем не менее защитную ветку условия с NotSupportedException делать надо даже в яве — у кода нет никаких гарантий, что в перечень значений enum не внесут правки. Соответственно с учётом этого наличие плюшек в виде возможности рассматривать значения enum'а видится более интересным нежели полуработающая защита от выстрела себе в ногу.
                • +1
                  И правда. Но всё-таки в нормальных условиях так не получится ). Хотя видимо именно на этот случай в скале в match case всегда надо определять _.
                • 0
                  Если вы имеете в виду версионность, то у кода есть такая гарантия, если enum определен в том же package (ну т.е. понятно, что его всегда можно расковырять руками и поправить — но защищаться от этого смысла не имеет).
            • 0
              Почему слаботипизированный enum, который никаким образом не ограничивает область значений — это хорошо, а EnumSet — это костыли?

              Для работы с native кодом можно было бы сделать отдельную конструкцию языка (unsafe enum, например), или просто мапить обычные, как это делается со строками и массивами. А так из-за этих граблей страдает высокоуровневый код, которому native interop не нужен абсолютно.

              Вообще, дизайн enum'ов в C# — это, пожалуй, самая слабая сторона языка. По сути, просто взяли его из плюсов, и убрали неявные преобразования (т.е. эквивалент нового enum class из C++11). Для высокоуровневого языка это как-то убого.
              • 0
                дизайн enum'ов в C# — это, пожалуй, самая слабая сторона языка

                По-моему, одна из самых редких проблем, с которыми мне доводилось сталкиваться. Вы бы ещё сказали, что отсутствие инлайновых классов — главная недоработка в C#. :D

                В шарпе с возрастом накопилось некоторое количество архитектурно не очень приятных моментов, но лёгкость переписывания кода с плюсов, нормальный интероп с нативным кодом — это определённо сильные стороны.
      • 0
        Кстати, очень большая часть ошибок происходит именно из-за верных ожиданий для одного языка, но ложных для другого.

        По сути, если предположить, что C# был содран спроектирован по подобию к Java, то и работать он должен похожим образом. И в 95% (цифра с потолка) – это так. Но в оставшихся 5% (остаток от цифры с потолка)…
        • 0
          Согласен.
        • –1
          Именно поэтому шарп и не содран с Java…
    • +3
      Offtop: Огромное спасибо за Pex! Не знал о такой замечательной вещи.
    • 0
      Зашёл на сайт Pex, там как-то странно: Pex есть только для VS2010, а для VS2012 — какая-то поделка для «portable libraries». Всё действительно так печально?
      • +1
        Да, к сожалению, классные идеи очень часто остаются мало-популярными.
        Но, все таки есть возможность запустить Pex из командной строки (для обычного проекта, а не портабл либирарей). Это описано в разделе «Command Line»

  • +1
    Далее я выбрал четыре случайных проекта, которые предстояло транслировать с Python на Haskell

    Классно — портировать на статический язык то, что уже имеет прототип с устоявшейся архитектурой. Вопрос не в том, что динамическую программу нельзя перевести в статическую, а в том, на чем удобнее разрабатывать с нуля.
    А так понятно, что лучше быть здоровым и богатым — со статическим доказательством корректности, кучей тестов и так далее.
  • 0
    Я сам большой сторонник, любитель и всяческий приверженец стиатически-типизированных ЯП. Но одного всё-таки не отнять у динамически-…
    Я тут недавно писал крошесный скриптик работы с XML на груви, так там есть такая штука, как GPath. работают с ним примерно так
    xml.Picture[0].Country[1].getAttribute(«имярек»)
    Вот эти вот Picture и Country — это автоматически созданные во время парсинга xml наследники типа Node с именами, взятыми непосредственно из xml. А в статический вид это не типизируется…
    • НЛО прилетело и опубликовало эту надпись здесь
      • +1
        Да, но тут мы видим по сути ту же динамическую типизацию по сути. Только вместо красивых полей используем некрасивые строки.
        • +2
          В статических C# и F# эта задача решается через dynamic или аналоги, например. И не будет никакого рантаймового оверхеда или рефлекшена — можно сделать такой динамический класс, свойства которого будет разворачиваться вот в эти же самые get(«Picture») из вышестоящего поста.

          (да, ниже уже написали, зря не обновил страницу перед комментированием)
          • +1
            C# — это все-таки не чисто типизированный язык, именно благодаря наличию в нем dynamic. И если вы призываете им пользоваться в данном конкретнос случае, то надо понимать, что код на C# с dynamic в плане типизации ничем не отличается от JS или Python.

            Рантаймовый оверхед там, разумеется, будет. Как ни крути, но runtime string lookup — это медленно, вне зависимости от того, обернут он в сахар или нет.
          • 0
            В статических C# и F# эта задача решается через dynamic или аналоги, например. И не будет никакого рантаймового оверхеда или рефлекшена

            Вообще-то dynamic реализован с помощью reflection, просто с кэшированием. Оверхед там вполне себе — по сравнению со статически типизированным вызовом проигрыш ощутимый (но по сравнению с динамически типизированными языками скорость примерно одинаковая).
        • +1
          А Вы представьте, что в динамически типизированном языке всегда пишете такими некрасивыми строками. Разве только что без кавычек.

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

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

          Не пользоваться статической типизацией в статически типизированном языке — тоже запросто: можно пользоваться событиями в С# без меры, можно создать класс Message с списком object и передавать и ифами вызывать методы (был такой код в проекте), можно сводить все объекты к базовому, передавать через один метод «чтобы через одну дырку», а там ифами определять тип и приводить к наследнику. Способов много, а умельцев еще больше.
          • 0
            А я в общем-то не спорю. Но для многих языков существуют более-менее вменяемые IDE. Для PHP чуть лучше, для питона — чуть хуже, но оно работает. Однако же я предпочитаю динамические для скриптов, а для остального — старую добрую джаву и ее деривативы с выводом типов — скалу с котлином.
          • 0
            Можно представить, что когда программист пытается создать программу, то он определяет множества, интервалы для переменных, в которых может находиться программа в не ошибочном состоянии. Статическая типизация — это мощный инструмент повышения надежности.

            Мощный, но не единственный. Ключевое отличие от динамической — когда производится проверка.
            • 0
              Это так. Но «в которых может находиться программа в не ошибочном состоянии». При статической типизации (если код еще правильно написан), то программа физически не может попасть в ошибочную точку. При динамической как бы попадет, но упадет.
              • 0
                С другой стороны, программа в ошибочную точку может вообще не попасть (может мертвый код, может просто очень редко используемая фича, может какое-то дикое стечение обстоятельств), но статически типизируемая вообще не запустится, а динамически — будет себе спокойно работать.
                • 0
                  Я сторонник падений и не работы в случае ошибки или даже подозрения на нее. Если компилятор нашел ошибку даже в мертвом коде — самое время исправить.

                  Но это, так, мои субъективные предпочтения.

                  Я также пишу на J (динамически типизированный язык), который очень нравится. Но там такая типизация оправдана из-за его подхода к вычислениям. Он работает в основном с многомерными матрицами чисел. Сам приводит к нужным типам.
            • 0
              Поэтому, кстати, странное исследование в посте. Если взять код на динамически типизированном языке и просто преобразовать в код статически типизированного языка, то вряд ли от этого он много выиграет и будет использовать статическую типизацию эффективно.
              • 0
                Ну, какой-то выигрыш можно получить, если динамический код активно работает с типами, то есть проверяет при вызове или выбирает разные ветви исполнения в зависимости от параметров. Скажем, метод на PHP типа
                public function do($param) {
                  assert(is_array($param) || is_string($param));
                  if (is_string($param) {
                    $param = [$param];
                  }
                  // ...
                }
                

                явно будет эффективнее работать если переписать его как (псевдокод типа C++)
                public do(char[] param) {
                  this->param([a]);
                }
                
                public do(char[][] param) {
                  //...
                }
                

                да и читается проще.
                • +1
                  Немного сомневаюсь, что автор поста писал такой серьезный транслятор. Так обычно руками надо переписывать :)

                  Если код преобразован руками, то включается субъективный фактор — умения автора, опыт. Изменение кода может быть настолько серьезным, что будет неизвестно, сам автор нашел ошибки или это следствие статической типизации.

                  Но если даже простое преобразование позволило найти столько ошибок в коде — это показательно. Вообще, не понятно, как происходило исследование и что это было (каюсь, читать по ссылке более полную информацию — лень). Но пост плюсанул — хорошая тема и комментарии.
    • +5
      Ну в том же C# для задач, которые удобнее решаются с динамической типизацией есть режим, включающий эту самую динамическую типизацию (точно так же как для задач, требующих ручного перемалывания указателей есть unsafe). Более того, можно писать классы, наследующиеся от DynamicObject в этом режиме будут на входе получать имя метода/свойства со списком аргументов, а на выходе давать результат вызова. В итоге после написания прослойки на полторы страницы кода код работы с xml будет выглядеть абсолютно так же как и в вашем примере.
      • +1
        Круто.
        • 0
          А вообще я писал штуку, позволяющую генерировать врапперы на основании свойств/атрибутов. Т. е. сначала пишется что-то типа вот этого:
          public abstract class JabberPacket : XMapper
          {
          	[XMapTo ("type")]
          	public abstract string Type { get; set; }
          	
          	[XMapTo("to")]
          	public abstract Jid To { get; set; }
          	[XMapTo ("from")]
          	public abstract Jid From { get; set; }
          	[XMapTo ("id")]
          	public abstract string Id { get; set; }
          }
          
          Потом на XElement натравливается метод AsMapper, который генерирует (или берёт уже готовый) класс, реализующий мясо геттеров/сеттеров. Т. е. что-то вроде XDocument.Load(...).Root.AsMapper().Foo. В итоге получается разделение информации о структуре и бизнес-логики, а так же полное отсутствие оверхеда. В принципе, могу этим поделиться и выложить на гитхабе.
    • 0
      Я так понимаю, что если есть XML, то есть и схема/DTD. Иначе, если может прийти всё, что угодно, то как такой XML обрабатывать? А если есть схема, то по ней можно сгенерировать код. Ну или если генерация кода не нравится по идеологическим причинам — просто написать bean'ы и проставить на них аннотации для маппинга на XML.
      • 0
        Ну там была конкретная задача, конкретный xml и никакого DTD :)
        И то был скрипт, а не программа. Ну и кода раз этак в 5 меньше, чем бины.
    • 0
      xml.Picture[0].Country[1].getAttribute(«имярек»)

      На PHP $xml->Picture[0]->Country[1]['имярек']
      

      И не знаю как в Груви, но тут никаких ошибок, если соответствующих нод нет.
      • +1
        Ну так php вроде тоже динамически типизируется? Я и говорю — один бонус у динамических языков таки есть.
        и да, в груви тоже глотаются исключения.
        • 0
          Мало того, что динмически, так ещё и не строго. Иногда, блин, слишком не строго. Интуитивно сложно догадаться чему и когда будет равно приведение выражение выше к boolean.
  • +4
    > 17 ошибок типизации, которые не были обнаружены тестами
    Это много или мало? Если в проекте миллион строк кода, то наверное мало. Если 50, то многовато. Эта абсолютная величина бессмысленна.
    • +1
      Да, и кроме того, неизвестна критичность самого бага.
      Одно дело, если, например, в упомянутом MIDIUtil файл в итоге будет испорченный – а другое, если на каких-то граничных значениях будет вставлен неверный звук, который никто никогда не заметит.
    • +3
      А главное, что возможно эти ситуации не приводили к ошибкам, «утиная» типизация же.
  • +4
    мне одному кажется, что по 4м случайным проектам нельзя делать такие поспешные выводы? o_O
  • –1
    Статическая типизация слишком многогранна, чтобы однозначно ответить на вопрос где её можно заменить динамическими конструкциями, а где нельзя. Плюс есть ещё статический анализ, который обнаружит явные ошибки типизации до исполнения. Но в отличии от статически типизируемых языков это будет информация для разработчика, а не запрет.
  • 0
    А исследование на положительные аспекты от динамической типизации? а то одногранно получается.
    Мне в данном вопросе больше нравится подход Perl6 — лёгкое совмещение статической типизации и динамической.
  • +1
    Зачем было изобретать велосипед, если есть erlang c его dyalizer'ом, который делает тоже самое, чего автор пытался добится. Причём этой системе уже много лет, все особенности тпизации обсосаны со всех сторон.
  • +1
    Я никогда не понимал смысла динамической типизации. В этом всегда было что-то из категории «шелловых скриптов» (которые я люблю и уважаю, но точно знаю, что как только скрипт перестаёт помещаться на экране — это уже плохая программа, а не скрипт).

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

    В языках с GC и изоляцией программы от указателей на память динамическая типизация мне кажется странной.
    • –1
      Почитайте Metaprogramming Ruby
    • 0
      В языках с GC и изоляцией программы от указателей на память динамическая типизация мне кажется странной.

      А мне ровно наоборот :) Когда работаешь непосредственно с памятью (*alloc и т.п.), то не забыть на какой тип указывает указатель жизненно важно. В этом у Си большой шаг вперед по сравнению с Асмом. А когда все переменные и структуры данных это лишь неявные ссылки на низкоуровневые структуры данных, к которым прямого доступа нет, то проблема забывчивости легко решается добавлением ещё одного поля в эту структуру — типа.
      • 0
        Я ж и говорю — тайпкасты решают единственную проблему «ой, компилятор варнинги выдаёт, когда мы по int'у из памяти читаем».
  • +1
    >> все проекты без особого труда можно было бы транслировать на язык со статической типизацией

    Достаточно попробовать портировать какой-нить ORM чтобы убедиться что это не так.
    • НЛО прилетело и опубликовало эту надпись здесь
      • +1
        Так я не утверждаю что их нет. Я утверждаю, что код существующих ORM на динамически типизированных языках нельзя «без особого труда транслировать на язык со статической типизацией»

        $ git clone github.com/django/django.git
        $ сd django/django/db
        $ du -sh
        1.1M

        $ git clone github.com/ponyorm/pony.git
        $ cd cd pony/pony/
        $ du -sh
        812K

        $ git clone git clone github.com/brosner/sqlalchemy.git
        $ cd sqlalchemy/lib/sqlalchemy/
        $ du -sh
        1,7M

        Теперь попробуйте переписать тот python код на java. Или просто посмотреть на объем исходников Hibernate/JPA
        • –1
          А что, возможности sqlalchemy сравнимы c возможностями хибернейта?
          • 0
            Я б сказал что на 70-90 поцентов функционала все orm пересекаются. Непересекающаяся часть это 100-500к кода на python.
            • 0
              Я бы вот так не сказал. Хибер силён не основными возможностями всех ORM, а, наоборот следующими пунктами
              1) Множество видов построения запросов:
              a) query by example
              b) HQL
              c) criteria API
              d) native
              2) Всякие штуки, связанные с перехватыванием операций — интерцептеры, листенеры
              3) Мощнейшая система аннотаций, которые помогают описать проектируемую БД во всех подробностях — связи, джойны, каскадинг…
              4) Сессии
              5) Транзакции

              А сохранять/загружать в/из БД и правда все могут :).
              • +1
                Теперь я знаю с чего слизана Doctrine :)
                • 0
                  Да, доктрайн практически полностью слизана с JPA, вплоть до названий классов. Согласитесь — она хороша?
                  • 0
                    Угумс. Кстати, тогда она хороший кандидат на сравнение плюсов и минусов различных типов типизации.
                    • 0
                      Мне так не кажется. Ведь ORM — это просто абстракция. По большому счёту в варианте JPA ей вообще пофигу, что там лежит в базе. Что лежит — то и замапили. Просто в случае Java замапили с учётом типов, а в случае пыха — без учёта.
                      • +1
                        Так и получим оценку сколько кода «тратится» на учет типов. Или на динамическую их проверку.
                        • +1
                          А, ну с этой точки зрения да. С другой стороны несложно догадаться, что у PHP узкое место по сравнению с джавой не в типизации, а в интерпретации вместо компиляции.
              • 0
                SQLAlchemy очень серьезная ORM и на нее очень сильно повлиял Hibernate в свое время.

                Если кратко, то да — можно сравнивать, при этом библиотека меньше по размерам. Вот совсем небольшой feature list: www.sqlalchemy.org/features.html

                Если по пунктам:
                1) Запросы пишутся питоновским кодом. Что-то типа:
                query(Model).filter(Model.name == 'foo').limit(20)

                Если хочется, можно комбинировать с сырым SQL, причем на всех уровнях — хоть весь запрос на SQL, хоть куски.
                DSL очень серьезный, умеет почти все. Работает правильно — в момент создания запроса можно «ответвляться». Например, сделали основной запрос, вызвали его с count(), достроили с offset().limit() и получили страницу данных.
                В отличии от других ORM еще ни разу не приходилось писать сырой SQL.

                2) Есть event'ы, которые можно вешать на все уровни — от «вот-вот отправим SQL запрос в БД» до «поменялось значение вот этого свойства модели»

                3) Тоже есть. Включая lazy loading для связей, каскад и т.п.

                4) Есть.

                5) Есть.

                6) Ленивая загрузка есть. Можно на уровне запроса определять как и что грузить, а можно на уровне моделей описать.

                Там вообще много чего есть.

                Если по теме, алхимия серьезно использует динамическую диспетчеризацию и типизацию. Например есть такой модуль sqlalchemy.func. Для простоты примера, предположим у него нет «своих» функций. Любая попытка получения аттрибута по имени из этого модуля, например sqlalchemy.func.max(Model.name) генерирует вызов функции SQL с переданными параметрами. Для примера выше 'MAX(model.name)'. Очень сильно экономит на кодо-генераторах и размере кода. Сделано оно вменяемо и не влияет на производительность.
                • 0
                  Ну что ж — значит sqlalchemy — серьёзная ORM. Я сам-то не писал на питоне, но ощущения у меня почему-то, что это достаточно лёгкая ORM. Ну что ж, приятно осознавать, что в трёх хороших языках есть хотя бы по одной хорошей ORM :).
                  • 0
                    sqlalchemy в мире python близкий аналог Doctrine в мире PHP. Вот в мире Ruby подобного нет (ну или я не смог его найти).
                  • 0
                    Кстати — нет. SQLAlchemy для питона считается большой и тяжеловесной. Слишком много всего умеет, слишком много абстракций, слишком конфигурируемая и «слишком» много кода.

                    Но, как ни странно, достаточно быстрая. Понятно что, где и как происходит + за счет архитектуры есть возможность отказаться от «тормозящих» слоев.
                    Вот reddit, например, ORM не использует, сделали свою обертку поверх SQLAlchemy Core (генератор SQL запросов из питоновских выражений). Ну и вообще, ее хорошо так применяют: www.sqlalchemy.org/organizations.html

                    Обычно питоновские библиотеки маленькие и лаконичные и SQLAlchemy это исключение (как и Django, но ему можно — он комбайн все-в-одном).
                    • 0
                      Ну по вашему описанию стало сразу понятно, что это не лёгкая ORM. А в мире руби народ вообще почему-то предпочитает activerecord, хотя концепция эта, да простят меня рубисты, весьма кастрированная.
                      • 0
                        А у них собственно и выбора нет. Даже ORM с названием DataMapper реализует ActiveRecord.
            • 0
              О, я еще про ленивую загрузку коллекций забыл, это тоже сильная сторона джавских ORM.
  • +8
    Слабо верится, что кто-то дочитает до моего комментария, но вот что пишут авторы мануала по Erlang на тему типизации:

    Английский вариант
    One classic friction point between proponents of static and dynamic typing has to do with the safety of the software being written. A frequently suggested idea is that good static type systems with compilers enforcing them with fervor will catch most errors waiting to happen before you can even execute the code. As such, statically typed languages are to be seen as safer than their dynamic counterparts. While this might be true when comparing with many dynamic languages, Erlang begs to differ and certainly has a track record to prove it. The best example is the often reported nine nines (99.9999999%) of availability offered on the Ericsson AXD 301 ATM switches, consisting of over 1 million lines of Erlang code. Please note that this is not an indication that none of the components in an Erlang-based system failed, but that a general switch system was available 99.9999999% of the time, planned outages included. This is partially because Erlang is built on the notion that a failure in one of the components should not affect the whole system. Errors coming from the programmer, hardware failures or [some] network failures are accounted for: the language includes features which will allow you to distribute a program over to different nodes, handle unexpected errors, and never stop running.

    To make it short, while most languages and type systems aim to make a program error-free, Erlang uses a strategy where it is assumed that errors will happen anyway and makes sure to cover these cases: Erlang's dynamic type system is not a barrier to reliability and safety of programs.


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

    Это может быть правдой для других динамически типизированныз языков, но Erlang имеет определенные отличия и уже определенный послужной список.
    Самым распространенным примером являются знаменитые «девять девяток» (99,9999999%) доступности, предлагаемых на Ericsson AXD 301 ATM, состоящей из более, чем миллиона строк кода.
    Стоит отметить, что все это время компоненты системы выходили из строя много раз, но общая система была доступна все это время.

    Erlang был спроектирован из расчета, что сбой в одном компоненте системы не должен приводить к сбою всей системы. Сбои и ошибки могут появиться везде (программист, железо, сеть): Erlang предоставляет инструменты для распределения вычислений, обработки ошибок и работы без остановок.

    Короче говоря, большинство языков и систем стремятся сделать программу без ошибок, Erlang использует стратегию, где предполагается, что ошибка произойдет в любом случае. Поэтому динамическая типизация не является препятствием для создания отказоустойчивый систем.


    Источник

    От себя добавлю, что не располагаю конкретными фактами, но мне кажется, что самые критические ошибки происходят на уровне логики. А от них не спасает ни статическая типизация, ни даже тесты (Я имею в виду ошибки, когда и программист и тестер поняли задачу по-другому, не так как предполагалось в описании ТЗ)
    • +2
      … Или ТЗ не полностью соответствует применению устройства/программы и т.д.

      С этим ничего не поделать, увы.
    • +1
      Ну насколько я понимаю, пойнт сторонников статической типизации не в том, что «нужна только статическая типизация, а тесты и отказоустойчивость — нет», а в том, что там, где типизация возможна, она лучше, потому, что она даёт гарантии, а не покрывает частные случаи. Что не отменяет остальных методов.
      Если будет возможно статически проверять спецификацию, будет ещё лучше, что, разумеется, не отменит возможности ошибок в самой спецификации.
      • +2
        Статическая типизация не единственный способ гарантировать правильную работу с типами. Главное преимущество статической — ошибки типов выявляются на стадии компиляции(статического анализа), а не на стадии выполнения как в других популярных способах. Ну и плюс не отнимает ресурсов на стадии выполнения.
        • +4
          Ну, собственно, в том и пойнт, что «чем раньше и надёжнее может быть обнаружена ошибка, тем лучше».
  • +3
    Простите, но это очередной пост, где смешались и кони, и люди.

    1) Тестирование — это тестирование корректности работы программы, а не проверка, что везде передаются значения определенного типа. Сколько раз уже обсуждали — один черт, опять в кучу.
    2) Очередное непонимание типизации. Проблемы не в статическая/динамическая, а в строгая/слабая.

    Почему, в каждом каноническом примере про типизацию присутствует Haskell (где программы — это архитектура типов больше, чем алгоритмов), и канонический пример динамического языка (со строгой типизацией, кстати) — Python. А почему никто не делал сравнения С++ (с его выкрутасами над шаблонами) и Erlang (язык-то динамически типизированный, но при этом есть анализ кода и типов). Хватит избитых примеров, и хватит непонятных сравнений.
    • +1
      Меня больше поражает, что в сравнениях часто одной из сторон фигурирует не, скажем так, мэйнстримовый язык. Сравнили бы PHP и C# например.

      А тесты, да, непонятно какой стороной относят к проблемам типизации. Крайне редко они валятся из-за не того типа, а если и валятся, то из-за того, что не проходит строгое сравнение, когда и не строгое бы не прошло, но со строгим спокойней.
      • 0
        Как вариант, но ИМХО, не так показательно. Нужно что-то более контрастное. А так, привычный PHP (с ошибками дизайна, но мы о них прекрасно знаем, и многие к этому привыкли), и C# — отличный язык (если не считать, что многим он не подходит из-за ориентированности на проприетарную платформу).
        • 0
          Я исходил из того, что это, наверное, сейчас самые популярные языки, рынок которых заметно пересекается.
          • 0
            В ключе популярности полностью согласен. Тогда опционально, можно еще и JavaScript взять, вместо PHP, например.
  • –3
    Статью не читал пока, но с тезисом в заголовке категорически согласен. Типизация должна быть только статической. И это вовсе не обязательно означает формальное объявление типов на каждом шагу: умный компилятор (напримар Scala и частично C#) может прослеживать типы самостоятельно, это простая алгебра по-моему.
  • 0
    Зачем было переводить то, что уже переведено? it-talk.org/topic15662.html
    • +1
      Поставим вопрос по другому: зачем переводить то, что перевода не заслуживает вовсе?
    • 0
      (в любом случае, она не является такой уж большой — 60 страниц для исследовательской статьи, равно как в ней особо нет и «птичьего языка»

      Ну, насколько я понимаю — это какой-то автоматический перевод. Таким образом можно весь интернет через google translate прогнать :)

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