О типизации

2017-12-24

Да начнётся срач!

Так получилось, что пару месяцев подряд я интенсивно кодил на Котлине. И учил студентов кодить на Яве.

Один разок я даже устроил воркшоп в стиле TDD, где к своему удивлению обнаружил, что добрая половина ява-кода в IntelliJ IDEA пишется по нажатию Alt+Enter. Это автоисправление ошибок: класс новый создать, который тут упоминается, но ещё отсутствует, метод новый добавить, и в нужные интерфейсы, и в нужные классы, когда этот метод впервые упоминается в тестах.

А другое автодополнение, по Alt+Space, но чаще просто выскакивающее само, после точки после имени переменной, позволяет особо не нагружать свою человеческую память тем, что умеют данные классы. Таким образом тоже «пишется» уйма кода.

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

Ну и где моя магия автодополнений? Если что, у меня IDEA Ultimate, и все нужные плугины стоят.

Почему IDEA ничего не знает о существовании функции requests.compat.urljoin, пока я не напишу соответствующий импорт? Почему я, когда смотрю на __init__.py этого самого requests, тоже не вижу никаких compat? Откуда он вообще берётся? Почему в официальной документации requests ничего не сказано про compat? Почему таки использование этого urljoin нахваливают на StackOverflow? Его вообще можно использовать, или это секретное API, которое в любой момент может превратиться в тыкву?

Это лишь один, последний попавшийся, пример. Почему-то в Питоне всегда так. IDE лишь смутно предполагает, что тут сымпортировали, что тут передали, что с этим можно делать. Разработчику нужно либо действовать методом научного тыка, либо писать подробные тесты, либо очень хорошо знать ту систему, которую пишешь, и те библиотеки, которые используешь. Чтобы предполагать несколько больше, чем IDE. А если нужно быстро написать долбаный плугин к совершенно незнакомому фреймворку, вообще чувствуешь себя слепым котёнком. И остаётся только научный тык.

Сладывается у меня подозрение, что это всё из-за динамической типизации. Никто не знает, что тут пришло, лишь бы крякало. Если не крякает, это ваши проблемы, надо было выявить тестами.

Untyped Duck

Тут сразу возражают: ведь весь окружающий мир динамичен. Ведь нам в запросе приходит бог его знает какой JSON, и нам надо его обработать. И в языках с динамической типизацией это делается просто и естественно. А в языках со статической типизацией приходится сначала натягивать этот JSON на какой-то класс. Да и этот класс нужно сначала где-то объявить.

На самом деле, хочется видеть «линзу». Не важно, что там приходит. Но здесь и сейчас я хочу видеть объект с определёнными свойствами. Если нужных мне обязательных свойств нужного типа не пришло — это ошибка. Если пришло, натягиваем на объект, и поехали дальше уже со строгой типизацией.

Собственно, все приличные веб фреймворки так и поступают с входящим JSON. Вопрос только в том, как описать те свойства, которые нам интересны. Можно ява-бобами, можно котлиновыми дата-классами (обожаю их). В нашем секретном фреймворке мы вообще (почти) автоматически натягиваем динамический объект сообщения (а-ля Map) на (почти любой) интерфейс, описывающий ожидаемые свойства этого сообщения.

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

Говорят, интерфейсы в Go как раз и делают то, что нужно. С одной стороны есть произвольные структуры. С другой стороны есть интерфейсы, описывающие ожидаемые методы. Если есть функции над структурой, реализующие эти методы, считается, что эта структура реализует этот интерфейс. Поэтому мы пишем интерфейс, указывая, что мы хотим. И пишем функции, указывающие, как из имеющихся данных получить то, что мы хотим.

Static Typing

Вообще вырисовывается три способа передачи развесистых данных в функцию. Передача динамического объекта, вроде мапы или словаря. Передача статического объекта, с заранее (на этапе компиляции) определённым набором полей. Ну и можно выделить именованные параметры, достаточно динамичный способ, тем не менее, задаваемый при написании кода, которым почему-то брезгуют многие языки. А ведь удобно, не зря же в современных API на JavaScript взялись передавать объекты с именованными полями, как единственный аргумент функции.

В языках с динамической типизацией легче передавать динамические объекты. Словари в Питоне, ассоциативные массивы в PHP, объекты (они там все динамические) в ЯваСкрипте. Создавать новые классы тут муторно и лень.

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

Наверное, это какой-то заговор. И вот эта лёгкость динамичного в динамичных языках и лёгкость статичного в статических языках как раз и приводит к тому, что языки остаются каждый в своей нише, и не идут навстречу друг к другу.

Duck Typing

А если нам нужны не данные, а поведение?

В Питоне есть такое понятие: file-like object. Попробуйте найти в официальной документации, какие именно методы должны быть у таких объектов. Конечно будет read(). Но ведь не только он?

Утиная типизация. Известно (по крайней мере ожидается), что это утка. Утка должна крякать. Но что такое «крякать»? Документация Питона не всегда даёт ответ на этот вопрос. Интерфейсы же, что Явы, что Гоу, дают чёткий ответ на этот вопрос.

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

Впрочем, тут есть и обратная сторона. Видел кучу разработчиков, которые вообще ни разу не открывали документацию того API, который они вовсю используют. Они просто ставят точку, смотрят список методов, который выдаёт IDE, выбирают наболее подходящий (по имени), и используют его. Конечно же они напарываются на неудачные названия методов, всяческие нюансы, вроде ограничений использования данных классов и методов, выбирают не оптимальные решения. Всё равно документацию читать надо. RTFM, как говорится.

Duck Typing

З.Ы. Не уверен, относится ли это к данной дискуссии, но утверждают, что с типами в Питоне вообще всё плохо.