Как бороться с динамической типизацией?
17.05.2014
|
kaa.python |
Меня, как человека привыкшего к сатитеской типизиции, особенности динамической типизации иногда ставят в тупик. Как вообще с ней жить в сложном проекте?
http://sysdev.me/dynamic-typing-fight/
http://sysdev.me/dynamic-typing-fight/
17.05.2014 20 комментариев |
KP>Меня, как человека привыкшего к сатитеской типизиции, особенности динамической типизации иногда ставят в тупик. Как вообще с ней жить в сложном проекте?
В C++, кстати, подобная проблема проявляется при использовании шаблонов функций, решается схожим образом — в основном комментариями/документацией/именами параметров функции/именами шаблонных параметров. Грядущие концепции несомненно улучшат ситуацию.
По поводу "все помнят параметры у API наизусть" — важны ведь не только сами параметры, но ещё и pre/post-conditions — так что за исключением каких-то тривиальных случаев, описание придётся читать в любом случае.
EP>В C++, кстати, подобная проблема проявляется при использовании шаблонов функций, решается схожим образом — в основном комментариями/документацией/именами параметров функции/именами шаблонных параметров. Грядущие концепции несомненно улучшат ситуацию.
Да, ситуация похожа, это верно. Хотя есть и кардинальное отличие – проблема вылезет на этапе компиляции, а не "когда нибудь потом", что иногда сильно напрягает.
EP>По поводу "все помнят параметры у API наизусть" — важны ведь не только сами параметры, но ещё и pre/post-conditions — так что за исключением каких-то тривиальных случаев, описание придётся читать в любом случае.
Да, за прелести динамической типизации приходится платить
KP>Да, ситуация похожа, это верно. Хотя есть и кардинальное отличие – проблема вылезет на этапе компиляции, а не "когда нибудь потом", что иногда сильно напрягает.
Да, но к сожалению, не всегда. Например, Forward Iterator и Input Iterator имеют одинаковый синтаксис, соответственно ошибки на этапе компиляции не будет (если не делать специальные проверки на forward_iterator_tag).
EP>>По поводу "все помнят параметры у API наизусть" — важны ведь не только сами параметры, но ещё и pre/post-conditions — так что за исключением каких-то тривиальных случаев, описание придётся читать в любом случае.
KP>Да, за прелести динамической типизации приходится платить
Не, я как раз о другом — pre/post-conditions нужно знать и при статической типизации. То есть, независимо от того, знаем ли мы точную сигнатуру функции или нет — нам потребуется дополнительная информация о pre/post-conditions.
Конечно при хорошем/консистентном API, либо совсем тривиальном, о многом можно догадаться взглянув на набор параметров, но всё-таки в общем случае нужно читать документацию (и при статической и при динамической типизации).
KP>Меня, как человека привыкшего к сатитеской типизиции, особенности динамической типизации иногда ставят в тупик. Как вообще с ней жить в сложном проекте?
Присоединяюсь к Евгению насчёт аналогии с шаблонами С++.
На своих задачах я борюсь с поблемой путём широкого использования ассертов. В первую очередь для проверки значений входных параметров — несколько строчек таких ассертов в начале функции работают как документация. Внутри кода ассертами проверяются результаты вызываемых функций. У меня проверки занимаю до 10% времени выполнения программы. То, что проверки происходят в рантайме, а не в компайлтайме компенсируется простотой написания тестов как раз за счёт динамики. Когда у тебя код обвешан ассертами, не нужно писать много тестов — достаточно несколько вызовов на типичных параметрах (иногда даже без проверки результата). Такие тесты потом служат примерами использования функции — самодокументирование кода опять же.
KP>Меня, как человека привыкшего к сатитеской типизиции, особенности динамической типизации иногда ставят в тупик. Как вообще с ней жить в сложном проекте?
KP>http://sysdev.me/dynamic-typing-fight/
Нет принципиальной разницы между статической и динамической типизацией. Например у вас язык со статической типизацией, в программе фигурируют штуки вроде "userName" и "email". И то, и другое вы скорее всего решите хранить в string. Автоматически это означает, что их можно сравнить, можно положить в коллекцию, можно конкатенацию сделать и ещё миллион вариантов. Все эти операции не имеют смысла в контексте вашей программы, но всё-таки они доступны. А как насчёт допустимых значений? Если есть какой-то "userName", очевидно он должен удовлетворять хотя бы "грамматической" валидации — не длиннее, не короче, без каких-то символов и т.д. То же самое и про email. По-хорошему нужно больше внимания уделять этим понятиям — делать классы UserName, Email и в них описывать всю специфику — это в явном виде запретит глупости типа userName+email. Но так обычно не делают: вместо этого просто "подразумевается", что вот этот самый "userName" где-то там раньше провалидировался и можно ему доверять.
В языках с динамической типизацией эти "допущения" просто выражены более явно.
Статическая типизация: "вот этот string — это не просто string, а валидный адрес email"
Динамическая типизация: "вот этот объект — это не просто объект, а строка, и, более, того в ней лежит валидный адрес email"
A>Нет принципиальной разницы между статической и динамической типизацией. ... И то, и другое вы скорее всего решите хранить в string.
То, что можно, наплевав на преимущества статической типизации, прийти к динамике и там — вопросов не вызвает. Можно вообще пойти еще дальше и хранить и то, и другое в System.Object, java.lang.Object и т.п. Объекты же, че Но после реализации собственных типов UserName и Email, принципиальная разница между статической и динамической типизацией все же проявится
KV>Здравствуйте, andyag, Вы писали:
A>>Нет принципиальной разницы между статической и динамической типизацией. ... И то, и другое вы скорее всего решите хранить в string.
KV>То, что можно, наплевав на преимущества статической типизации, прийти к динамике и там — вопросов не вызвает. Можно вообще пойти еще дальше и хранить и то, и другое в System.Object, java.lang.Object и т.п. Объекты же, че А можно реализовать собственные типы UserName и Email, чтобы принципиальная разница между статической и динамической типизацией все же проявилась
Есть "языки со статической типизацией", а есть "языки со статической типизацией в реальном мире". Если вы про первое — не могу не согласиться. Если про второе — передавайте привет вашим единорогам.
A>Если вы про первое — не могу не согласиться.
Смотря, о чем было утверждение "Нет принципиальной разницы между статической и динамической типизацией". Оговорк о реальном мире я там не увидел
A>Если про второе — передавайте привет вашим единорогам.
Передал. Мои единороги, в свою очередь, заверяют ваших в своем нижайшем почтении
KV>Здравствуйте, andyag, Вы писали:
A>>Если вы про первое — не могу не согласиться.
KV>Смотря, о чем было утверждение "Нет принципиальной разницы между статической и динамической типизацией". Оговорк о реальном мире я там не увидел
"Но так обычно не делают" — где-то прямо в середине моего сообщения.
A>>Если про второе — передавайте привет вашим единорогам.
KV>Передал. Мои единороги, в свою очередь, заверяют ваших в своем нижайшем почтении
Вы штаны в радуге испачкали.
A>"Но так обычно не делают" — где-то прямо в середине моего сообщения.
То, как обычно делают или не делают, совершенно не влияет на наличие или отсутствие принципиальных отличий в двух формальных системах. Обычно (и, чем ближе к дедлайну, тем обычнее) вместо наследования от Exception какого-нибудь MyCustomException пишут:
и, как можно догадаться, к следующей версии таких TODO обычно становится еще больше. Но эти примеры (и ваш со строками, и мой с исключениями) в общем случае суть — говнокод, аргументировать которым свою точку зрения по меньшей мере странно.
A>>>Если про второе — передавайте привет вашим единорогам.
KV>>Передал. Мои единороги, в свою очередь, заверяют ваших в своем нижайшем почтении
A>Вы штаны в радуге испачкали.
А вы уже одной ногой наступили в бан
A>Нет принципиальной разницы между статической и динамической типизацией. Например у вас язык со статической типизацией, в программе фигурируют штуки вроде "userName" и "email". [порезал]
Если мы говорим о "userName" и "email" – совершенно верно. А теперь давай перейдем от двух переменных к классу типа Account. У этого объекта появляются поля "userName", "email" и еще пачка других. Этот объект может сам проверить значение "email" на корректность и т.д. И вот, начиная с момента появления класса Account для случая динамической типизации начинают возникать проблемы. Как убедиться что у переданного объекта есть метод getEmail? Как безболезненно удалить getUserName, добавив getFirstName и getLastName вместо него? Как заставить редакто корректно определять тип объекта и организовывать автоподстановку значений?
В итоге выходит что на 1-2 строки кода ты обязан написать 1 строку тестов. Практика хорошая, спускается "сверху" и если не следуешь ей – ССЗБ. В функциях где тип переданного объекта критичен – добавляешь assert-ы с проверкой типов. Для системы автоподстановки в каждую функцию вставляешь комментарии с обозначением типов объектов, т.е. фактически описываешь типы, но проверки на корректность при этом не получаешь. И выходит так, что ты фактически руками делаешь то, что в случае со статическим языком берет на себя компилятор.
KP>Здравствуйте, andyag, Вы писали:
A>>Нет принципиальной разницы между статической и динамической типизацией. Например у вас язык со статической типизацией, в программе фигурируют штуки вроде "userName" и "email". [порезал]
KP>Если мы говорим о "userName" и "email" – совершенно верно. А теперь давай перейдем от двух переменных к классу типа Account. У этого объекта появляются поля "userName", "email" и еще пачка других. Этот объект может сам проверить значение "email" на корректность и т.д. И вот, начиная с момента появления класса Account для случая динамической типизации начинают возникать проблемы. Как убедиться что у переданного объекта есть метод getEmail? Как безболезненно удалить getUserName, добавив getFirstName и getLastName вместо него? Как заставить редакто корректно определять тип объекта и организовывать автоподстановку значений?
KP>В итоге выходит что на 1-2 строки кода ты обязан написать 1 строку тестов. Практика хорошая, спускается "сверху" и если не следуешь ей – ССЗБ. В функциях где тип переданного объекта критичен – добавляешь assert-ы с проверкой типов. Для системы автоподстановки в каждую функцию вставляешь комментарии с обозначением типов объектов, т.е. фактически описываешь типы, но проверки на корректность при этом не получаешь. И выходит так, что ты фактически руками делаешь то, что в случае со статическим языком берет на себя компилятор.
Так я и не утверждаю, что все эти задачи одинаково решаются в СТ и ДТ. В своём сообщении я просто указал, что большинство проблем ДТ можно встретить и в СТ, просто масштаб будет разный. В контексте рефакторинга ДТ полностью сливает, т.к. весь рефакторинг крутится именно вокруг знаний, доступных до запуска программы, а в случае ДТ этих знаний практически 0.
Практическое решение всех этих проблем — понять, что плюсы от динамической типизации очень редко перевешивают минусы и, соответственно, просто не использовать языки с ДТ там, где можно использовать языки с СТ. Примеры когда просто не надо использовать ДТ:
1. Вы решили сделать веб сайт на Ruby/RoR, или Python/Django, или NodeJS/Express. Не надо. Есть альтернативы со статической типизацией.
2. Вы решили сделать одностраничное веб-приложение на JavaScript. Не надо. Вместо JavaScript стоит использовать Dart или TypeScript.
Примеры когда надо использовать ДТ:
1. Вы решили в существующее приложение добавить некий "скриптинг", чтобы можно было изменить поведение без пересборки всей системы. Каким бы ужасным ни был JS, он вполне может быть уместен. Но именно как надстройка, а не как основа.
2. Вы хотите сделать легко расширяемый встроенный DSL. Ruby (и всякие Groovy) здесь легко рвут все СТ языки.
Мне кажется, что все эти проблемы на тему примнения ДТ возникают именно от желания взять ДТ вместо СТ просто потому что это "модно". Оно модно только в статьях на всяких Хабрах. В реальном мире те же RoR-программисты стабильно ноют, что шаг влево от CRUD приложения (одна модель — один контроллер — одна страница) и всё — начинается боль. С другой стороны, если хватает мотивации и культуры самоконтроля, все эти JS/Python/Ruby могут стать отличными инструментами для прототипирования, т.е. во всех тех задачах где намного важнее увидеть результат и понажимать на него, чем сделать архитектуру и сопровождаемость.
ИМХО, если вы используете ДТ язык и честно говорите, что вам приходится 50% времени тратить на контроль типов, вам просто не нужен язык с ДТ для этой задачи.
KP>Меня, как человека привыкшего к сатитеской типизиции, особенности динамической типизации иногда ставят в тупик. Как вообще с ней жить в сложном проекте?
KP>http://sysdev.me/dynamic-typing-fight/
Меня тоже интересует этот вопрос. Все знакомые джаваскриптеры, насколько я их понял, говорят примерно так:
1. обкладывать тестами каждый кусок кода;
2. использовать какие-то lint-ы.
Не знаю, как это в жизни работает.
KP>>http://sysdev.me/dynamic-typing-fight/
T>Меня тоже интересует этот вопрос. Все знакомые джаваскриптеры, насколько я их понял, говорят примерно так:
T>1. обкладывать тестами каждый кусок кода;
Нужно не просто обкладывать тестами, нужно создавать дизайн принципиально тестопригодный. Вместо написания простыней кода, нужно писать короткие функции, которые легко комбинируются и реюзаются.
Их нужно покрыть тестами в обязательном порядке.
T>2. использовать какие-то lint-ы.
T>Не знаю, как это в жизни работает.
Я выключил Lint и прочую хрень, оставил самый минимум — синтаксис и глобальные переменные.
KP>Меня, как человека привыкшего к сатитеской типизиции, особенности динамической типизации иногда ставят в тупик. Как вообще с ней жить в сложном проекте?
Нормально. Внутренний дизайн, если его делать, "как в с++(дотнет, джава)" это смерть для динамического языка.
Нужна внятная схема взаимодействия компонентов. В противном случае лапша растёт с каждой строчкой кода.
I>Нужна внятная схема взаимодействия компонентов. В противном случае лапша растёт с каждой строчкой кода.
Архитектура нужна и важна при разработке на любом языке программирования, так что это заявление мимо кассы. А вот какого-то вменяемого ответа вот на это, я так понимаю, ты тоже дать не можешь?
I>>Нужна внятная схема взаимодействия компонентов. В противном случае лапша растёт с каждой строчкой кода.
KP>Архитектура нужна и важна при разработке на любом языке программирования, так что это заявление мимо кассы.
Статически типизированый язык дает возможность вносить изменения относительно безопасно даже без специальных тулов. В динамических языках этого нет.
Потому в статических языках работает подход "сначала сделали, потом подумали". В джаваскрипте придется все выбросить.
KP>Меня, как человека привыкшего к сатитеской типизиции, особенности динамической типизации иногда ставят в тупик. Как вообще с ней жить в сложном проекте?
Ассерты в функции на то, что приходит значение нужного типа и юнит-тесты кода, вызывающего функцию, чтобы этот вызов прогнало и неверное использование упало на ассерте.
KP>>Меня, как человека привыкшего к сатитеской типизиции, особенности динамической типизации иногда ставят в тупик. Как вообще с ней жить в сложном проекте?
vsb>Ассерты в функции на то, что приходит значение нужного типа и юнит-тесты кода, вызывающего функцию, чтобы этот вызов прогнало и неверное использование упало на ассерте.
Трудно представить ситуацию, когда у вас времени достаточно, чтобы ваять юнит-тесты, но недостаточно, чтобы писать код на статически типизированном языке.
Ассерты, проверяющие типы переменных в динамических языках — это костыли. В некоторых местах это оправдано, но повсеместное их использование ломает все прелести динамической утинной типизации. Зачем тогда вам вообще динамическая типизация?
Ассерты должны проверять наблюдаемое и значимое в данной точке программы поведение объектов, а не типы объектов.