Pull to refresh
0
Инфопульс Украина
Creating Value, Delivering Excellence

Конец эпохи динамических языков

Reading time8 min
Views45K
Original author: Elben Shira
Несколько последних месяцев я программирую преимущественно на Scala (по работе) и на Haskell (для души). На этой неделе я, правда, ещё немного пописал на Ruby (по работе) и Clojure (для души).

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

Но вот что касается моего новенького, с иголочки, проекта-любимца на Clojure… О, Clojure! Глоток свежего воздуха! Благодатная земля хорошо скомпонованных функций, иммутабельных структур данных и всего такого. Как прекрасен твой синтаксис и как мудра твоя чувствительность! Вся твоя суть в функциях, принимающих мэпы и возвращающих мэпы. И твой SQL-генератор, и слой доступа к БД, и HTML-парсер, и URL-роутер являют собой одну и ту же завораживающую картину мэпов, гоняемых туда-сюда тактами процессора, прекрасную с своём ритме хорошо собранных швейцарских часов.

Вернуться к Clojure после долгого времени это всё равно, что почувствовать себя дома. Это просто окрыляет программиста. Но почему-то в этот раз я ощутил и ещё одно, неожиданное для себя чувство: неопределённость.

Мой диалог с Clojure выглядел как-то так:

— О, свет мой, Clojure! Спасибо тебе за эту восхитительную иммутабельную структуру данных с мэпом для запроса. Могу ли я спросить, что там внутри?
— Разве это не очевидно? Там HTTP-запрос.
— Да-да, конечно. Но в каком же именно виде? Что там за ключи и что за значения?
— Разве это не очевидно? Там HTTP-запрос.
— Да-да, конечно. Я почитаю исходники и документацию в поисках ответа.
— Да, почитай и разберись.
— Я почитал. И что же такое переменные attr и f? А когда я вызываю функцию wrap-params — какие ключи добавляются в мэп?
— Разве это не очевидно?
— Забудь. Я просто добавлю вот сюда и сюда отладочный вывод.

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

Да, Clojure — мощная вещь. Но мощь эта неуправляема, она не имеет вектора и без кого-то, способного дать совет, она может лишь разрушать. И я сейчас не о философских понятиях — только о коде. Кто из нас не страдал от метапрограммирования в Ruby или мэпов в Clojure? Мы сами себе и жертвы и виновники наших страданий.

Вот вам пример: предметно-ориентированные языки (DSL) — это способ решения проблем, или ещё один способ их создания? Давайте поговорим о деструктивной мощи DSL в Clojure. Программисты на Clojure любят DSL-и, структура языка предрасполагает к их использованию. Но я считаю, что что-то здесь не так. Давайте, например, представим такой-себе генератор HTML. Вот вы вместо

<span class="foo">bar</span>


пишете:

[:span {:class "foo"} "bar"]


Знаете, какой DSL лучше всего описывает HTML? Я открою вам секрет: это HTML. Так зачем же вы, скажите пожалуйста, придумываете вместо него свой собственный DSL, который точно ничем не лучше (а скорее всего хуже) и уж наверняка игнорирует все те десятилетия опыта и развития, что были у HTML со всеми его фичами, инструментами и опытом дизайнеров?

Посмотрите как сделаны enlive и yesql и вы поймёте, о чём я говорю.

Но что ещё хуже с DSL, так это то, что я не уверен, верно ли я использую ваш DSL до того самого момента, пока я не получу ошибку на рантайме. Давайте посмотрим на bidi, миленькую библиотеку для URL-роутинга. Её приятно использовать, но есть одно «но». Скажем, мы хотим прописать маршрут для запроса GET /notes. В bidi мы определяем маршруты вот так:

(def my-routes
  ["/notes" :index-handler])


Мы можем протестировать этот обработчик:

(bidi/match-route my-routes "/notes")
;; {:handler :index-handler}
;; Success!

Выглядит просто. Но что, если мне нужно добавить ещё пяток маршрутов:

GET /
GET /notes
GET /notes/:id
POST /notes
POST /notes/:id

Какая ерунда! Всего-то несколько Ctrl+F по документации, внимательное чтение исходников, пару догадок, десяток экспериментов, невероятное количество исключений, выброшенных прямо мне в лицо и вот я чудом дохожу до требуемого результата:

(def my-routes
  ["" {"/" :home-page-handler
       "/notes"
         {:get  {"" :index-handler}
          :post {"" :create-handler}
          ["/" :id] {:get  {"" :show-handler}
                     :post {"" :update-handler}}}}])

Я думаю, во всём этом есть некий неуловимый паттерн, но вот вы видите его с первого взгляда? Вы запомните его раз и навсегда, или в следующий раз снова будете идти по тому же устеленному граблями пути творческого поиска?

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

Давайте посмотрим на несколько текущих попыток решить эту проблему

Частичное введение типов (Gradual typing)
Есть фантастически интересные попытки скрестить бульдога с носорогом и прикрутить к динамическим языкам элементы статической системы типов: Typed Racket, Typed Clojure, TypeScript, Typed Lua. Даже для Python есть "type hints"

Всё это хорошие вещи, их давно не хватало. Они показывают наличие проблемы неопределённости, о которой я здесь говорю, и каких-то попытках её решить.

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

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

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

Этого не достаточно
А что на счёт юнит тестов, тестов стиля кода, контрактов, парного программирования, ревью кода? Какое множество полезных инструментов! Наверняка уж с ними-то можно и на динамических языках писать!

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

Тесты стиля кода для динамических языков бессмысленно ущербны в том плане, что заботятся они о внешнем виде кода, а не о его качестве. Да, проблему с лишним пробелом они найдут, но в пробеле ли действительно проблема?

В то же время использование hlint в Haskell (поскольку это типизированный язык) даёт весьма ощутимый результат. Инструмент знает о вашей программе намного больше, чем просто синтаксис её кода. Он способен находить такие структурные проблемы, как:

  • Есть две эквивалентные анонимные функции, которые можно заменить одной и вызывать из двух мест
  • Вы написали код, который уже содержится в какой-нибудь доступной в проекте библиотеке
  • Вы не перебрали все возможные варианты в case-выражении


Создаётся впечатление, что мы тут говорим об искусственном интеллекте. Ок, а что если мы начнём с инструментов, которые помогут нам предотвращать некоторые рантайм-ошибки и помогут писать качественный код? Хотели бы вы иметь такие штуки, правда?

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

На прошлой неделе я работал с одним проектом на Scala и по ходу дела заметил, что в нём очень легко сделать ошибку, приводящую к записи неверных значений в поисковый кластер Solr. Например, Solr с лёгкостью соглашается записывать значение null в булевое поле. Я потратил неделю на рефакторинг этого хаоса, разбив этот огромный подверженный багам монолитный кусок на маленькие симпатичные модули, крутящиеся вокруг иммутабельных структур данных, как в Clojure. Вначале я был очень доволен собой и кодом. Его было легко читать и легко тестировать. Всё было прекрасно, пока я не обнаружил, что пишу те же самые баги, что и раньше. Один из них даже сломал наш порно-фильтр. Быстрый вопрос: что вы будете делать, когда функция isItPorn() вернёт null?

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

У нас заняло 2 дня чтобы выяснить как это всё можно завязать на систему типов Scala. Но в конце концов мы сделали это. Наша система типов стала проще, мы смогли конвертировать все имеющиеся раньше потенциальные ошибки рантайма в ошибки при компиляции. Адам отрефакторил всю нашу кодовую базу для использования новой системы типов всего за пару часов.

Сегодня я обновил один из моих проектов с использованием данной библиотеки. Получил ошибку от компилятора. Это действительно был баг, я никогда не видел его раньше. Исправлено!

Подобная мощь никогда не будет доступна в языках вроде Ruby и, наверное, Clojure тоже. Но она существует. И вы тоже можете использовать её.

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

Конец эпохи
Вот моя ставка: эпоха динамических языков заканчивается. Больше не будет новых успешных чисто динамических языков. Но они нас многому научили. Теперь мы знаем, что код библиотеки должен поддаваться расширению другими программистами (польза миксинов и мета-программирования), что мы хотим понимать и контролировать структуры данных, что мы презираем многословность. Ах, и ещё мы научились получать удовольствие от использования языков программирования.

Но время двигаться дальше. Мы вскоре увидим сияние восхода нового поколения языков программирования — приятных в использовании, как Clojure, но типизированных. Вместе с ними придут и новые, невиданные доселе инструменты, которые не сможет проигнорировать даже самый отъявленный аскет.

Чтобы всё это произошло, нам необходимо дать нашим инструментам ту информацию, которая им необходима, чтобы нам помочь. Даже хороший психолог не может помочь абсолютно молчаливому пациенту. Мы начнём с предоставления информации о типах, ограничив тем самым область неопределённости. Новые языки программирования, такие как Elm и Crystal находятся на верном пути, равно как и уже существующие Haskell и Scala. Нам нужно больше языков подобного типа.

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

Теолог Герхард Вос однажды описал человеческую жизнь как «уже, но всё ещё нет». С его точки зрения верующим людям судьбой предопределено счастье в загробной жизни, но вот пока ещё им приходится мириться с мирскими бедами и тягостями. Эта метафора хорошо описывает и то, где сейчас находимся мы, программисты. Да, у нас пока нет идеальных языков программирования и действительно умных инструментов, но с моей точки зрения блистательное будущее этой сферы человеческой деятельности уже предопределено. Многие из сегодняшних разработок уже идут по правильному пути, а чему-то ещё только предстоит появиться. Нам ещё предстоит длинная дорога, но мы определённо сможем её пройти.
Tags:
Hubs:
+41
Comments247

Articles

Information

Website
www.infopulse.com
Registered
Founded
1992
Employees
1,001–5,000 employees
Location
Украина