Так ли плоха статическая типизация?
14 октября 2013
В последнее время все больше людей выбирают языки программирования с динамической типизацией. Сторонники динамической типизации утверждают, что на изучение Erlang’а требуется две недели, после чего можно сразу начать писать боевой код. Что все равно интернеты динамически типизированы, что ошибки типизации быстро находятся и легко устраняются, а настоящую проблему представляют сложные логические ошибки, где статическая типизация все равно не помогла бы. Что статическая типизация — это медленно и скучно, а на Clojure можно легко написать свой Mortal Kombat за два вечера. Давайте же выясним, почему на самом деле вы не должны хотеть динамической типизации.
Начнем с простого. Всем известно, что языки со статической типизацией обычно быстрее языков с динамический типизацией. Что не удивительно, поскольку в языках со статической типизацией типы проверяются на этапе компиляции, а значит зачастую не нужно хранить информацию о типах в скомпилированной программе, и уж точно не нужно постоянно перепроверять типы на этапе исполнения. Кроме того, точная информация о типах позволяет применить к коду дополнительные оптимизации, которые невозможны в языках с динамической типизацией. Как ни крути, скорость становится важна, когда выясняется, что для работы приложения можно использовать вместо 200 серверов всего лишь 10.
Даже в языках с динамической типизацией использовать одну переменную для хранения значений разных типов, например, строки или числа, считается code smell. В результате, вы все равно выводите типы в уме. Только, в отличие от компилятора, иногда вы ошибаетесь. В некоторых динамически типизированных языках есть опциональная проверка типов на этапе компиляции. Но, например, в случае с Erlang (1) она работает в разы медленней проверки типов в Haskell, (2) при ее использовании вам фактически приходится выводить типы вручную или в полуавтоматическом режиме при помощи TypEr, (3) соответствующая программа, Dialyzer, весьма глюкава. Не удивительно, что на практике эрлангисты часто забивают на Dialyzer и выводят типы в уме. Спрашивается, раз вы все равно используете типы, почему бы не поручить компилятору выводить и проверять их?
Дополнение: Независимые источники сообщают, что похожие проблемы имеют место быть в мире Clojure.
Некоторые программисты совершенно незаслуженно упрекают статически типизированные языки в многословности или недостаточной гибкости. В действительности, ничто не мешает, например, писать на Haskell, словно это Erlang с прикрученным Dialyzer. То есть, использовать списки, кортежи, строки и числа, не вводя никаких дополнительных типов. Если же вам вдруг очень захочется объявить список, в котором хранятся как строки, так и числа, то можно просто ввести тип data StrOrInt = S String | I Int
и затем объявить список StrOrInt’ов. Или взять готовый тип Either. Или же можно воспользоваться экзистенциальными типами.
Многие ошибочно полагают, что статическая типизация защищает только от ограниченного класса ошибок, например, что вы не сложите строку с числом. На самом деле, с ее помощью можно сделать намного больше. Например, в веб-фреймворке Yesod при помощи типов, помимо прочего, проверяется, что приложение не сошлется на несуществующий URL. Повсеместно в Haskell при помощи типов осуществляется контроль над побочными эффектами. Помимо прочего, это положительным образом сказывается на производительности приложения. Например, если нужно выполнить расчеты над какими-то данными, вы один раз получите их в «грязном» коде, а затем передадите в чистую функцию, вместо того, чтобы постоянно получать одни и те же данные в разных функциях. В Haskell благодаря типам гарантируется, что при работе со STM, находясь внутри транзакции, вы не попытаетесь передать что-нибудь по сети, а при работе с Template Haskell — что вы не вклеите шаблон туда, где ему не место.
Благодаря типам вы тратите меньше времени на всякую рутину вроде вывода и проверки типов в уме, поиск и устранение тривиальных ошибок, оптимизацию кода, в котором, казалось бы, и тормозить-то негде, а также на написание унылых и скучных тестов из серии «два плюс два равно четыре». Значительно упрощается рефакторинг. В общем, вместо того, чтобы заниматься всей этой ерундой, вы занимаетесь тем, чем и должен заниматься программист — написанием фичей.
Кстати, о тестах. Опыт показывает, что убедить коллег в необходимости написания тестов довольно трудно. Что уж греха таить, даже самого себя в этом убедить непросто. Писать тесты скучно. На их поддержку нужно тратить время. В реальных условиях требования к приложению постоянно меняются. А это означает постоянное переписывание тестов. Значит, либо программисты работают медленнее, либо нужно нанимать дополнительных людей для поддержания тестов. При тестировании большого и сложного приложения тесты также превращаются в большое и сложное приложение, которое не меньше тестируемого нуждается в оптимизации, а также в поиске и устранении ошибок. На практике невозможно покрыть тестами 100% кода, а как автоматизировать тестирование таких приложений, как компиляторы, трехмерные игры или поисковые системы, вообще непонятно. Я уже не говорю о том, что на тесты часто попросту забивают.
Бесспорно, статическая типизация не способна полностью заменить тесты, но зато позволяет существенно сократить их количество. Вместо того, чтобы поддерживать тысячи тестов, поддерживайте десяток основных функциональных тестов в стиле «пользователь может зарегистрироваться и писать комментарии к чужим постам», а также несколько простых нагрузочных тестов. В сочетании с ручным тестированием нового функционала и некой формой бета-тестирования (скажем, выкаткой нового билда на определенный процент пользователей) можно достичь довольно высокого уровня тестирования.
Наконец, в качестве недостатка языков со статической типизацией нередко называют их сложность в изучении. Во-первых, у меня лично на этот счет есть сильные сомнения. Я бы не сказал, что Scala или OCaml существенно сложнее в изучении, чем Perl или Erlang. Да и Haskell, на самом деле, не шибко сложен, просто изучать его нужно медленно и вдумчиво, а не наскоком. Во-вторых, даже если это и так, на практике у вас вряд ли возникнет сильное желание писать проект на Erlang с программистом, который неделю назад прочитал книжку Чезарини и Томпсона. Скорее всего, вам захочется найти человека, который по крайней мере в течение полугода писал на Erlang какие-нибудь проектики для себя. И в-третьих, уж лучше потратить N месяцев на изучение Haskell, после чего начать писать на нем вполне годный код, чем за пару недель освоить любой другой язык и понять, как на самом деле нужно на нем писать, только спустя несколько лет.
Дополнительные материалы:
- Саркастическая статья Десять причин не использовать статически типизированный функциональный язык программирования;
- История разработки одного компилятора, статья Дмитрия Зуйкова во втором выпуске ПФП, прекрасно повествующая о сильных сторонах статической типизации;
- Хабрапост Haskell в продакте, отчет менеджера проекта;
В заключение отмечу, что статическая типизация напоминает мне запасной парашют. С одним парашютом совершать прыжки чуточку удобнее, да и вон толпе народу этот дурацкий запасной парашют еще ни разу не пригодился. Следует ли отсюда вывод, что запасные парашюты лишь мешают парашютистам?
Дополнение: Простая и наглядная демонстрация пользы от типов
Метки: Разработка, Языки программирования.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.