В чем привлекательность динамических языков?

Рассказывает Эрик Осхайм

К нам поступил вопрос:

Мне хочется лучше понять, в чем же все-таки состоит привлекательность языков с динамической типизацией. Может ли кто-нибудь, кто знаком и с динамическими языками, и со статически типизированными, мне это объяснить?

Объясню это на примере Scalа сo статической типизацией и Python с динамической. Мне кажется, что я достаточно опытен в обоих языках: хоть сейчас я уже не работаю с Python, у меня есть к нему некая привязанность, и в свое время я написал около сотни тысяч строк на Python.

Отсутствие гарантий

Достаточно очевидно, что основной проблемой с программированием на Python, в сравнении со Scala, является то, что у вас намного меньше надежных гарантий, что программа выдаст желаемый результат. Я признаю, что это большой недостаток.

Большинство других достоинств должны быть рассмотрены с пониманием этого. Если вы цените гарантии, за которые платите временем компиляции, то вы, скорее всего, будете склонны к тому, чтобы не признать никаких достоинств. Мне кажется, что это неправильно. Если вы действительно хотите понять, что же такого привлекательного (или даже приятного) в программировании на Python, вам придется подавить свое недоверие.

Меньше узлов

Когда я работал с Python, правильным было думать о типах данных Python структурно, то есть что типы являются «сильными» (например, строковой тип не может стать числом). Лучше всего думать об этих типах данных как о неком собрании возможностей. Вместо того, чтобы утвердить x Collator и вызывать после этого .collate(), можно просто вызвать .collate() напрямую.

Так как вы не вынуждены использовать типы данных, вы, скорее всего, и не будете этого делать при разработке API. В сравнении со Scala и Java, Python настойчиво поощряет API, которые не требуют использования никаких «экзотических» типов данных, большого количества параметров (за исключением случаев, когда они могут быть разумно заменены аргументами по умолчанию) или большого количества вложенных циклов.

Таким своеобразным способом Python препятствует погружению в объектно-ориентированное программирование. Вы создаете классы только тогда, когда обычный пользователь API может понять, как они работает и для чего их можно использовать. В противном случае вы будете склонны к использованию статических методов (или чего-то похожего), которые могут действовать на более простых типах данных. Подобным образом, в большинстве случаев вы также будете очень сильно склоняться к использованию стандартных собирательных типов данных (массивы, списки).

Это имеет ряд последствий:

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

Можно уточнить последний пункт: вам не нужно волноваться, что кто-то может «встроить» не тот собирательный или числовой тип. Если у вас присутствует тип данных, который ведет себя похоже, вы можете просто использовать его.

(Впоследствии те, кто перешел на Python, скажем, с Java, как правило, создают API, которыми достаточно сложно пользоваться)

Искусственные различия

Как правило, после работы со Scala или Java у вас остается огромное количество классов, которые, по сути, являются контейнерами данных. Case-классы хорошо помогают свести их шаблонность к минимуму. А вот в Python все эти классы — просто кортежи. Вам не придется придумывать для них имена или что-то в этом роде. Меня очень радует то, что можно создавать намного более компактные модули лишь благодаря тому, что не приходится волноваться о выдумывании большого количества имен или о том, какой класс лучше использовать.

Абстракция как распознавание образов

Абстракция по таким вещам, как дисперсия, становится намного проще. Примечательно то, что многие Python-программисты считают неизменяемость очень существенным различием между кортежами и списками (кортежи неизменяемы), и это достаточно логично, если учесть, что через оба можно итерировать, оба индексируются и оба не имеют никаких ограничений в размере. Сравните это с трудностями корректного выражения и абстракции по произведениям типов в Scala. Даже при помощи Shapeless это достаточно непросто.

В целом нахождение абстракций в Python намного больше похоже на распознавание образов. Если вы видите две строфы кода, которые по существу одинаковы, очевидно, что стоит абстрагироваться от их различий и написать один метод. Это относится даже к тем случаям, когда различия сводятся к:

  • именам используемых полей или методов,
  • арности функций или кортежей,
  • инстанцируемым классам,
  • именам классов или пакетов,
  • необходимым импортам.

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

Спекулятивное программирование

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

Одна из первых вещей, которой нам, как программистам, стоит научиться — это эмуляция того, что «машина» (компьютер, интерпретатор, ВМ) будет выполнять. Мы учимся это прослеживать через программы, воображая инкрементирование счетчиков, распределение данных вызываемых функций и т.п. Мы можем использовать print, чтобы посмотреть промежуточные значения, или вызвать исключения, чтобы приостановить процесс и т.п.

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

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

С другой стороны, при работе со Scala приходится иметь представление о том, как работают две разные системы. У вас есть среда выполнения (JVM), которая выделяет память, вызывает методы, делает ввод-вывод и, возможно, вызывает исключения, также как и Python. Но у вас к тому же есть компилятор, который создает и выводит типы, проверяет инварианты и делает целый ряд других вещей. Не существует хорошего способа заглянуть внутрь этого процесса и посмотреть, что там происходит. Большинство людей, вероятно, не сможет интуитивно понять, как работает типизация, как комплексные типы данных кодируются и используются компилятором и т.п. (Хотя в Scala нам повезло иметь таких замечательных людей, как Стивен Компалл, Майлз Сабин и Джейсон Зогг, которые рады об этом рассказать)

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

Очевидным вопросом является «почему мы вообще должны мысленно эмулировать ВМ?» В долгосрочной перспективе я не очень уверен, что это обязательно. Но с нынешним выбором статически типизированных языков большинство людей, вероятно, все же будут продолжать это делать.

К чему спешка?

Люди часто не могут понять, почему многие ученые обожают Python. Как по мне, так это очень логично.

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

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

Итог

Я не планирую прекращать кодить на Scala, Haskell или Rust (или даже C).  Когда я программирую на Python, я ловлю себя на мысли, что скучаю по статическим гарантиям и типоориентированной разработке. Но я все же люблю работать с Python, и когда я пишу код на Scala, я все еще нахожу поводы позавидовать работе с ним.

Перевод статьи «What is the appeal of dynamically typed languages?»