Сегодня многие системы и языки программирования позиционируются как «мощные». Нельзя сказать, что это плохо. Почти каждый из нас считает это положительным свойством. Но в этом посте я хочу донести такую точку зрения, что во многих случаях нам нужны менее мощные языки программирования и системы. Но прежде чем продолжить, уточню: здесь будет мало оригинальных, моих собственных размышлений. Я буду излагать ход мыслей, возникший по прочтении книги Дугласа Хофштадтера «Гёдель, Эшер, Бах», которая помогла мне собрать воедино разрозненные идеи и мысли, бродившие в голове. Также большое влияние на нижеизложенный материал оказали пост Филипа Вадлера и видеозапись с конференции Scala. Ключевая мысль такова:

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

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

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

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

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

Базы данных и схемы


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

За годы работы меня часто просили сделать поля для ввода текста в произвольной форме. С точки зрения конечного пользователя такое поле воплощает наивысший уровень «мощи», ведь в него можно ввести что угодно. И в рамках этой логики подобное поле является «наиболее полезным».

Но именно потому, что в поле можно вводить любой текст, оно является самым бесполезным, поскольку хуже поддается структурированию. Даже поиск по таким полям работает ненадежно — из-за возможных опечаток и разных способов описания каких-то вещей. Чем дольше я работаю с базами данных, тем сильнее желание жестко регламентировать все, что можно. И когда мне удается это сделать, то из получаемых данных можно извлечь гораздо больше пользы. То есть чем больше я ограничиваю «мощь» (то есть свободу) источников, вводящих данные в систему, тем больше возможностей я получаю при «потреблении» этих данных.

То же самое можно сказать относительно технологий баз данных. Неструктурированные (schemaless) БД предоставляют широкие возможности и гибкость при вводе данных, и бесполезнее при их выводе. Key-value хранилище представляет собой аналог «текста в свободной форме» с теми же недостатками: от него мало толку, если вы хотите извлечь информацию или что-то сделать с данными, поскольку не можете быть уверены в том, что в хранилище присутствуют какие-то конкретные ключи.

HTML


Отчасти, успех веба был обусловлен намеренным ограничением возможностей ключевых технологий — HTML и CSS. Да, это языки разметки, а не программирования, но такими их сделали не случайно. Это была сознательно выбранная концепция, одним из основоположников которой стал Тим Бернерс Ли. Просто процитирую один отрывок: «С 1960-х по 1980-е информатика прилагала немало усилий по созданию все более мощных языков программирования. Но сегодня есть причины, по которым нужно выбирать менее мощные инструменты. Одной из них является то, что чем «слабее» язык, тем больше вы можете сделать с хранящимися в нем данными. Если вы напишете простую декларативную форму, то кто угодно сможет написать программу, анализирующую эту форму самым разными способами. В некоем общем смысле, Семантическая Сеть представляет собой попытку преобразования больших объемов данных в обычный язык, чтобы получить такие возможности по анализу этих данных, которые и не снились их создателям.

К примеру, если веб-страница использует RDF для представления прогноза погоды, то пользователи смогут извлечь эти данные в виде таблицы, как-то обработать, усреднить, построить графики, сопоставить с другой информацией. И сравните это с Java-апплетом: информация может быть подана очень красиво, но ее невозможно анализировать. Поисковый бот не поймет, что представлено на странице. Единственный способ узнать, что же делает Java-апплет, это запустить его перед сидящим перед экраном человеком».


Такую же позицию занял и консорциум W3C: «Хорошая практика: использовать наименее мощный язык, пригодный для выражения информации, связей или приложений во Всемирной Сети».

Это практически полностью противоречит совету Пола Грэма (с оговоркой, что зачастую сравниваются определения «мощи», далекие от формальных): «Если вы можете выбирать из нескольких языков, то при прочих равных было бы ошибкой программировать не на наиболее мощном из них».

Файл формата MANIFEST.in


Перейдем теперь к «настоящим» языкам программирования. В качестве примера я выбрал формат файла MANIFEST.in, используемого инструментами distutils и setuptools. Если вам доводилось создавать пакеты для Python-библиотек, то вы наверняка знакомы с этим форматом.

По сути, он представляет собой очень маленький язык, описывающий, какие файлы должны входить в состав пакета в Python (по отношению к файлу MANIFEST.in, вызываемого из рабочего каталога). Например:

include README.rst
recursive-include foo *.py
recursive-include tests *
global-exclude *~
global-exclude *.pyc
prune .DS_Store

Существует два типа директив:

  • include (include, recursive-include, global-include и graft)
  • exclude (exclude, recursive-exclude, global-exclude и prune)

Возникает вопрос: как интерпретируются эти директивы? То есть какова их семантика?
Можно интерпретировать так: «Файл из рабочего каталога (или подкаталога) должен быть включен в пакет, если соответствует хотя бы одной директиве типа include и не соответствует ни одной директива типа exclude».

Вроде бы, это говорит о том, что данный язык является декларативным. К сожалению, это не так. В документации distutils говорится про MANIFEST.in — директивы нужно понимать следующим образом:

  1. начните с пустого списка файлов, которые должны быть включены в пакет. Точнее, начните со списка по умолчанию;
  2. выполняйте директивы в MANIFEST.in согласно их очередности;
  3. для каждой директивы типа include скопируйте все соответствующие файлы из рабочего каталога в список пакета;
  4. для каждой директивы типа exclude удалите все соответствующие файлы из списка пакета.

Этот пример наглядно иллюстрирует императивную природу этого языка: каждая строка MANIFEST.in является командой, чье действие подразумевает наличие побочных эффектов. Это делает язык более мощным, чем псевдодекларативный вариант, приведенный выше. Рассмотрим пример:

recursive-include foo *
recursive-exclude foo/bar *
recursive-include foo *.png

В результате выполнения списка этих команд png-файл (ниже foo/bar) оказывается включенным в пакет. А все, что выше foo/bar, в пакет не включается. Добиться того же результата средствами декларативного языка было бы сложнее, например:

recursive-include foo *
recursive-exclude foo/bar *.txt *.rst *.gif *.jpeg *.py ...

Поскольку императивный язык мощнее, существует соблазн выбрать именно его. Однако у императивной версии есть ряд важных недостатков:

  1. Трудности с оптимизацией. Когда дело доходит до интерпретирования MANIFEST.in и формирования списка файлов для включения в пакет, есть лишь одно эффективное решение: сначала сделать неизменяемый список всех файлов в каталоге и подкаталогах, а затем применять к нему правила. Cкопировать файлы в выходной список согласно правилам добавления, а затем удалить из него какие-то файлы согласно правилам исключения. Такой подход сейчас реализован в Python.

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

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

    Данная проблема не является теоретической. Я обнаружил, что из-за большого количества файлов в рабочем каталоге, если использовать, например, инструмент tox, выполнение setup.py sdist и ряда других команд может достигать 10 минут. То есть сам tox (использующий setup.py) будет работать очень медленно. Сейчас я пытаюсь решить эту проблему, но, думаю, сделать это будет очень непросто.

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

    Вероятно, единственным приемлемым решением будет вообще отказ от использования MANIFEST.in, и оптимизировать только в тех случаях, когда он совершенно пустой.
  2. Обратная сторона «мощи»: в файлах MANIFEST.in труднее разобраться. В первую очередь, сложнее освоить принципы работы языка. Для декоративной версии документация была бы значительно короче, чем есть на самом деле.

    Кроме того, при анализе специфических MANIFEST.in приходится мысленно исполнять команды, пытаясь представить результат. Куда проще было бы размещать строки в том порядке, в каком было бы удобно вам.

    Все это приводит к возникновению ошибок при создании пакетов. Например, легко поверить, что директива global-exclude *~ в начале MANIFEST.in означает, что все файлы, чьи названия оканчиваются на ~ (временные файлы некоторых редакторов), будут исключены из пакета. На самом деле эта директива вообще ничего не делает. И если одна из последующих директив попытается включить какие-то файлы в пакет, то они будут ошибочно включены. Я нашел следующие примеры этой ошибки (exclude-директивы, не работающие, как задумано):
    • hgview (если разместить в самом начале файла, работать не будет);
    • django-mailer (неработающая глобальная исключающая директива в начале файла).
  3. Вы не можете группировать строки в MANIFEST.in для облегчения восприятия, поскольку изменение их очередности влияет на состав пакета.

Роутинг


Роутинг является одной из составляющих ядра Django. Это компонент, который анализирует URL и передает их обработчику данного URL. При этом, возможно, извлекая из URL какие-то компоненты.

В Django это реализовано с помощью регулярных выражений. Допустим, у нас есть приложение, выводящее информацию о котятах, и в файле kittens/urls.py содержится такой код:

from django.conf.urls import url

from kittens import views

urlpatterns = [
    url(r'^kittens/$', views.list_kittens, name="kittens_list_kittens"),
    url(r'^kittens/(?P<id>\d+)/$', views.show_kitten, name="kittens_show_kitten"),
]

Соответственно, файл views.py выглядит так:

def list_kittens(request):
    # ...

def show_kitten(request, id=None):
    # ...


Регулярные выражения обладают встроенной функцией захвата, используемой для получения параметров, переданных во view-функции. Пусть наше приложение работает по адресу cuteness.com. Тогда адрес www.cuteness.com/kittens/23 будет инициировать вызов кода show_kitten(request, id=«23»).

Поскольку мы теперь можем маршрутизировать URL в конкретные функции, веб-приложения вынуждены почти всегда генерировать эти самые URL. Допустим, нам понадобилось включить на страницу со списком котят ссылки на их личные страницы: show_kitten. И наверняка мы захотим сделать это с помощью повторного использования конфигурации URL-маршрутизации.

Однако использовать мы ее будем в обратном направлении. При выполнении URL-маршрутизации выполним следующее:

URL path -> (handler function, arguments)

При генерировании нам известны функция-обработчик и необходимые аргументы. И мы должны сформировать URL, который приведет пользователя на нужную страницу после выполнения URL-маршрутизации:

(handler function, arguments) -> URL path

Для этого нам необходимо уметь предсказывать поведение механизма маршрутизации. Мы спрашиваем: «Каковы будут входные данные при таких выходных?» В самом начале истории Django в нем еще не было такого функционала. Но оказалось, что в большинстве случаев можно «изменять направление» URL-шаблона. Регулярные выражения можно парсить с целью поиска статичных и захватываемых элементов.

Обратите внимание, что это возможно лишь потому, что используемый для определения URL-маршрутов язык — регулярные выражения — имеет определенные ограничения. Хотя можно было использовать для этого и гораздо более мощный язык. К примеру, определяя URL с помощью функций, которые:

  • используют URL в качестве входных данных;
  • при несовпадении выдают NoMatch;
  • при совпадении возвращают усеченный URL и набор каких-то захваченных параметров.

Тогда наш urls.py выглядел бы так:

from django.conf.urls import url, NoMatch

def match_kitten(path):
    KITTEN = 'kitten/'
    if path.startswith(KITTEN):
        return path[len(KITTEN):], {}
    raise NoMatch()

def capture_id(path):
    part = path.split('/')[0]
    try:
        id = int(part)
    except ValueError:
        raise NoMatch()
    return path[len(part)+1:], {'id': id}

urlpatterns = [
    url([match_kitten], views.list_kittens, name='kittens_list_kittens'),
    url([match_kitten, capture_id], views.show_kitten, name="kittens_show_kitten"),
]

Конечно, можно было бы сделать match_kitten и capture_id более лаконичными:

from django.conf.urls import url, m, c

urlpatterns = [
    url([m('kitten/'), views.list_kittens, name='kittens_list_kittens'),
    url([m('kitten/'), c(int)], views.show_kitten, name="kittens_show_kitten"),
]

Учитывая, что m и c являются возвращающими функциями, этот язык получается более мощным для маршрутизации URL, чем реальный, основанный на регулярных выражениях. Интерфейс для обнаружения совпадений и захвата имеет гораздо больше возможностей — например, можно было бы осуществлять поиск ID в базе данных и др.

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

А с ограниченными по своим возможностям регулярными выражениями получается больше доступных вариантов. В целом, основанная на регулярках конфигурация URL не реверсируется, поскольку простые выражения вроде. (просто точка) нельзя реверсировать уникальным образом. А если мы хотим нормально генерировать классические URL, то нам требуется уникальное решение. Если. все же попадается, то Django произвольно выбирает другой символ, иные подстановочные символы так не обрабатываются. Но поскольку такие символы встречаются только среди захваченных, то вполне можно реверсить регулярные выражения.

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

Помимо прочего, в Python’е не так-то просто определять мини-языки для подобных задач. Их реализация и использование потребуют немалого количества бойлерплейтов и уровня детализации — куда больше, чем при использовании «строковых» языков вроде регулярных выражений. Кстати, в языках типа Haskell подобные вещи делаются намного легче благодаря довольно простым возможностям вроде определения алгебраических типов данных и сопоставления с шаблонами.

Регулярные выражения


Предыдущая глава напомнила мне еще об одной проблеме. В большинстве случаев пользоваться регулярным выражениями довольно просто. Но когда бы вы ни вызвали регулярку, вам сразу становятся доступны все ее возможности, не зависимо от того, нужно вам это или нет. Одним из следствий в некоторых случаях является необходимость backtracking’а, чтобы найти все возможные соответствия. И значит можно умышленно создать такую комбинацию символов, которая будет ОЧЕНЬ долго обрабатываться регулярными выражениями.

Это, кстати, породило целый класс DoS-уязвимостей, одну из которых обнаружили в Django — CVE-2015-5145.

Шаблоны: Django vs Jinja


Создатели шаблонизатора Jinja вдохновлялись языком шаблонов Django, но несколько изменили его философию и синтаксис.

Производительность является одним из главных преимуществ Jinja2. Здесь сразу компилируется Python-код, вместо того чтобы выполнять написанный на Python интерпретатор, как это сделано в Django, что и дает 5-20-кратное увеличение производительности.

Автор Jinja Армин Ронахер (Armin Ronacher) вполне успешно применял такой же подход и для ускорения рендеринга шаблонов Django. Предлагая этот проект, он знал, что API-расширения в Django сильно затрудняют внедрение подхода, реализованного в Jinja. В Django можно использовать собственные шаблонные тэги, что дает практически полный контроль над этапами компиляции и рендеринга. В том числе можно применять такие мощные тэги, как addtoblock в django-sekizai, хоть на первый взгляд это и кажется невозможным. Но даже если в подобных случаях (нечастых) использовался бы более медленный вариант, то все равно была бы польза от быстрой реализации.

Было и еще одно важное отличие, влиявшее на многие шаблоны. В Django передаваемый контекстный объект (содержащий необходимые шаблону данные) может перезаписываться в течение процесса рендеринга шаблона. Шаблонные тэги могут назначать контекст, и некоторые из них (например, url) только этим и занимаются.

Все это позволило реализовать в Django основную часть компиляции в Python по примеру Jinja.

Обратите внимание, что в обоих перечисленных случаях проблема заключается в мощи движка Django — он позволяет авторам кода делать вещи, которые невозможны в Jinja2. В результате мы сталкиваемся с очень большими затруднениями, пытаясь скомпилировать быстрый код.

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

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

Можно сказать, что вполне логично было сделать контекстный объект перезаписываемым, поскольку структуры данных в Python по умолчанию мутабельны. Что приводит нас к самому Python…

Python


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

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

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

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

Допустим, в том же Python, при использовании VCS (например, Git или Mercurial), если вы переместите в другое место функцию из десяти строк, то получите diff на 20 строк, несмотря на то, что с точки зрения самой программы ничего не поменялось. А если что-то все же поменялось (функция была не только перемещена, но и модифицирована), это будет очень сложно определить.

Каждый раз, сталкиваясь с этим, возникает недоумение: почему мы работаем с нашим сложноструктурированным кодом как с кучей строк обычного текста? Это же безумие какое-то!

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

Вот несколько примеров. Возьмем ранее определенную функцию в качестве аргумента по умолчанию:

def foo():
    pass

def bar(a, callback=foo):
    pass

Если поменять порядок строк, то для foo в определении bar вылетит ошибка NameError.
Воспользуемся декоратором:

@decorateit
def foo():
    pass

@decorateit
def bar():
    pass

Из-за возможных эффектов в @decorateit вы не можете поменять очередность этих функций и быть уверенным, что программа будет работать так же. То же можно сказать и о вызове кода в списке аргументов функции:

def foo(x=Something()):
    pass

def bar(x=Something()):
    pass


Атрибуты класса тоже нельзя менять местами:

class Foo():
    a = Bar()
    b = Bar()

Здесь определение b поставить выше a из-за возможных эффектов в конструкторе Bar. Это может показаться теоретизированием, но тот же Django действительно использует это внутри определений Model и Form ради обеспечения стандартного порядка полей, применяя хитрый счетчик на уровне класса внутри базового конструктора Field.

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

Это предоставляет вам широчайшие возможности при написании кода, но зато не позволяет развернуться с точки зрения автоматизации манипуляций с уже готовым кодом. Рефакторинг практически невозможно осуществлять без опаски, поскольку из-за возможностей языка (например, «утиной типизации») нельзя переименовывать методы. А в связи с вероятностью «отражений» и динамического доступа к атрибутам (getattr и прочие) вы вообще не можете безопасно осуществлять переименование в автоматическом режиме.

Так что не спешите обвинять во всех бедах VCS или инструменты для рефакторинга. Все дело в широте возможностей самого Python. Несмотря на огромное количество структур в коде, с ними мало что можно сделать с точки зрения каких-то манипуляций. Так что при ближайшем рассмотрении зависимость diff от порядка строк выглядит уже не так плохо.

Сегодня мы почти не используем декораторы, из-за которых становится важна очередность определений в коде, и потребителям это несколько облегчает жизнь. Но в редких случаях наши инструменты все же оказываются практически бесполезны. Для каких-то потребителей можно провести оптимизацию с учетом некой стандартной ситуации и обнаружить факт сбоя, например, использовать проверки JIT. Но для других средств (скажем, для VCS или инструментов рефакторинга) в случае неудачи будет слишком поздно собирать информацию о выполнении. К моменту обнаружения проблемы вы могли уже зарелизить «испорченный» код, так что лучше проявлять бдительность, чем потом извиняться.

В идеальном языке при переименовании функции diff в VCS должен выглядеть как «Функция foo переименована в bar». Одновременно должна быть предусмотрена возможность экспортирования и импортирования — чтобы можно было обновлять зависимости до версии, в которой foo переименовали в bar. Это можно сделать в «менее мощном» языке. А вот из-за широты возможностей самого Python все остальные инструменты в его окружении могут приносить гораздо меньше пользы.

Насколько это важно, зависит от того, сколько времени вы тратите на манипуляции со своим кодом. Особенно в сравнении с использованием кода для манипулирования данными.

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

В некоторых популярных языках автоматический рефакторинг работает проще, но если вы хотите придать коду строгую продуманную структуру, то вам придется использовать редактор и VCS, иначе быстро запутаетесь. Есть любопытные проекты вроде Lamdu, но ни один из них еще не дорос до уровня серьезного инструмента.

Итоги


Продумывая систему своего проекта и всех ее участников (людей и приложения), включая создание эффективного кода и долгосрочную поддержку, помните: менее мощный язык, как это ни парадоксально, дает больше возможностей. Как говорится, Slavery is freedom. Хотя, конечно, никто не отменял баланс между выразительностью языка и целесообразностью.

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

Нелегко придется и программистам, которые будут пытаться разобраться в коде или модифицировать его.

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

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

Комментарии (30)


  1. khim
    25.11.2015 15:13
    +11

    Какие-то «откровения 60х годов». Я, конечно, извиняюсь, но вокруг идеи «чем сложнее у вас язык тем проще программирование, но и тем сложнее метапрограммирование» LISP построен. И Forth. И много чего ещё. Зачем ломиться в открытую дверь? Про это и так все всё знают. Одна проблема: до метапрограммирования нужно ещё дожить — потому зачастую будущая сложность приносится в жертву текущей.


    1. velvetcat
      25.11.2015 20:03

      Про это и так все всё знают

      Не все здесь активно программировали в 60-е.

      Спасибо автору за интересную тему и хорошую подачу.


  1. a_batyr
    25.11.2015 16:01
    +3

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


    1. a_batyr
      25.11.2015 16:33
      +2

      including the need to produce efficient code, and long term maintainability, less powerful languages are actually more powerful — “slavery is freedom”.
      Возникает ощущение, что автор текста просто троллит.


      1. a_batyr
        26.11.2015 12:08
        +2

        Похоже меня не поняли. По пунктам:
        а. У меня нет претензий к переводчику, меня смущает лишь оригинальный текст.
        б. Автор оригинального текста вывернул мысль, вероятно, просто не подобрал слова.
        в. Как по-моему должна звучать фраза, чтобы не вводить мозг в заблуждение:

        less powerful languages are actually more efficient — «Simplicity is the key to brilliance»
        г. Заодно переведу эту мысль на русский:
        менее мощные языки могут быть более эффективными — «Простота высшая ступень искусства»
        д. Всё что я называю троллингом это метание фразами «Slavery is freedom».


    1. Gorthauer87
      25.11.2015 17:46
      +5

      Мощность языка обязывает разработчика компилятора или транслятора реализовать ее полностью, даже если ей никто пользоваться не будет.


    1. Mingun
      25.11.2015 20:34
      +2

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


  1. solver
    25.11.2015 16:02
    +2

    Странно, как вы пропустили доклад Рича Хикки «Simple Made Easy».
    http://www.infoq.com/presentations/Simple-Made-Easy


  1. deniskreshikhin
    25.11.2015 16:12
    +6

    HTML

    Отчасти, успех веба был обусловлен намеренным ограничением возможностей ключевых технологий — HTML и CSS.



    Такую же позицию занял и консорциум W3C: «Хорошая практика: использовать наименее мощный язык, пригодный для выражения информации, связей или приложений во Всемирной Сети».



    Это практически полностью противоречит совету Пола Грэма (с оговоркой, что зачастую сравниваются определения «мощи», далекие от формальных): «Если вы можете выбирать из нескольких языков, то при прочих равных было бы ошибкой программировать не на наиболее мощном из них».


    Очень спорный момент.

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

    В сравнении с этим всем LISP смотрится очень достойно.

    Т.е. CSS и HTML это технологическая ошибка, а LISP довольно грамотное решения для своей ниши. Там где он нужен, он работает очень хорошо.


  1. Viacheslav01
    25.11.2015 16:30

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


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


    1. torkve
      25.11.2015 16:41

      В этом месте соглашусь с автором статьи — в общем-то он предлагает то, что называется DSL. Делается язык под задачу и в нём будет сложнее накосячить, а разбирать и обрабатывать его, наоборот, проще. Примеров уже и так полно, языки для описания звуковых эффектов, языки для графики типа GLSL и т.п.


      1. Gorthauer87
        25.11.2015 17:47
        +3

        Это если domain постоянно не меняется, а вот в противном случае dsl начинает превращаться в кашу из разных фич.


        1. torkve
          25.11.2015 18:25
          +1

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


      1. AllexIn
        25.11.2015 17:58

        GLSL — это же костыль. Потому что не осилили поддерживать(на уровне железа) весь C целиком. Сейчас GLSL по сути пришел к С. А уж его альтернативы так вообще обычный С.


        1. torkve
          25.11.2015 18:23
          +2

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


  1. vintage
    25.11.2015 16:45
    -2

    Это ж как усложнится язык дифов, если он будет поддеживать такие *мощные* выражения как «Функция foo переименована в bar»? :-)

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


  1. horlon
    25.11.2015 17:29
    +4

    Думаю 90% (если не больше) читателей не поймут сути Вашей статьи. И судя по комментариям — так оно и есть.


  1. torkve
    25.11.2015 17:49
    +6

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

    Например, почтовые адреса: чтобы не рассказывать своими словами, дам ссылку на публикацию об удивительных адресах нашей страны. Можно декомпозировать адрес, предусмотреть сто уровней вложенности, предусмотреть, что для некоторых адресов «город» может превращаться в «городской округ», а внутри него может быть квартал, а в квартале — ещё один город. А потом изобретать способы собрать из этого строковое представление и ловить баги. Проще просто хранить готовое представление. Или хранить его для сложных случаев, а в простых использовать простую схему типа той, что в КЛАДР.

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

    Например, код: автор пишет про классный diff, который надо использовать вместо этих ваших строк. Настолько классный, что я даже на секунду задумался, а может правда, написать такой diff, не для питона, конечно, но для языка, где действительно можно построить AST. Потом, конечно, меня отпустило. Вот простой случай переименования автор описал, классный случай. А если взять чуть более сложный случай, был у нас метод из трёх строк: объявили переменную X, вызвали функцию A, вернули результат. Мы тело этого метода переписали в виде четырёх строк: объявили переменную Y, в цикле вызвали функцию B, вернули результат. Представьте себе этот дифф в виде модификаций AST. Удобно ли это читать? А ведь это простая структурированная информация, готовая к потреблению. Можно собирать дифф в виде AST, в простых случаях писать «function renamed», а в сложных опять генерировать текстовый дифф, обязательно скажет мне кто-нибудь. А зачем? Это тройная работа, невообразимое усложнение, только ради диффа? В рефакторинге это полезно, для диффа — нет. Не нужно усложнять алгоритм там, где хорошо работают простые средства. Простые решения работают надёжнее сложных.

    Или вот ещё про код: долгие десятки лет большая часть веба была написана на Perl. На языке известном своей парадигмой TIMTOWTDI. Невообразимо усложнённый язык, про который даже доказывали, что его невозможно распарсить. А ведь в моём детстве, когда почти все динамические сайты содержали в себе путь /cgi-bin/, я был уверен, что CGI == Perl. Почему так получилось? Потому что код писался простой, никто не выдумывал адских и сложных решений. Когда вы пишете простой и очевидный инструмент, вам важнее написать его быстро и просто. И прочитать его должно быть просто. Когда «менее мощный» язык зарезает ваши возможности, код становится очевиднее и предсказуемее для машины, но не всегда — для человека. В этом был секрет популярности Perl и PHP, на них было просто разрабатываться, и основным потребителем был человек, а отнюдь не машина. Сейчас, когда задачи, которые решают программисты, в том числе и в вебе, усложнились, сдвинулся баланс: нам надо рефакторить сложный запутанный код, анализировать большие модули с кучей зависимостей и связей, мы перекладываем эту задачу на машину. И вот уже машине удобнее упрощённый язык. Фактически, вместо того чтобы писать для людей, мы теперь пишем для машин. И именно для этого приходится переходить на «менее мощные» языки, придумывать декларативные парсеры MANIFEST.in вместо императивных. Человеку удобнее и понятнее последовательность: «включили одно, потом выкинули другое», а более сложное декларативное описание удобнее только машине.

    Поэтому надо формализовать и упрощать всё там, где это нам действительно надо, ad-hoc. А пока потребности нет, оставьте язык для людей, в конце концов им ещё код писать.


    1. velvetcat
      25.11.2015 20:29

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

      Человеку удобнее и понятнее последовательность: «включили одно, потом выкинули другое», а более сложное декларативное описание удобнее только машине.
      Да ладно, все типичные машины выполняют как раз последовательность шагов. А вот людям это менее интересно, ибо «невыразительно».


      1. torkve
        25.11.2015 23:14
        -1

        Ну вот тот же пример из статьи с инклюдами: декларативный пример вам правда кажется более понятным?
        Или вот, например, декларативные языки популярных систем сборки: Makefile, CMake. Каких только извращений не приходится придумывать людям, чтобы заставить их вести себя как надо. Причём в Makefile всё ещё относительно по-божески: декларативно описывается только граф целей, а сами цели пишутся в императивном стиле. Это позволяет в сложных местах плюнуть на кэширование каких-нибудь промежуточных звеньев, например, и написать просто явную последовательность команд. А в CMake декларативно по умолчанию вообще всё, и сборка сложного проекта выглядит… достаточно адово. В каком-нибудь qbs с его структурированным декларативным описанием из-за этого некоторые вещи в принципе нельзя сделать by design, хотя казалось бы, система сборки должна позволять собирать что угодно и как угодно.


        1. stalkerg
          26.11.2015 00:35

          У меня практика использования cmake обратная, ну и кроме того последовательность действий вполне можно описать через зависимости. Но с прошлым вашим постом я соглашусь.


        1. grossws
          26.11.2015 07:30

          был дурак, fxd


      1. torkve
        25.11.2015 23:42

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

        Я же говорил о другом: применительно к аналоговой неструктурированной книжке мы просто можем не знать, какую информацию нам надо извлечь, чтобы не потерять полезный сигнал. То есть дело не в количестве, а в качестве.

        Точно так же, как в вышеупомянутых адресах мы балансируем между тем, чтобы создать сверхсложную структуру, в которую будем пытаться сложить любые данные и тем, чтобы потерять какую-нибудь возможно (не)важную информацию. Это как сверхуниверсальные стандарты типа SOAP или 7-уровневой модели OSI — настолько универсально, что даже неудобно и люди предпочитают использовать более простые и менее универсальные вещи.


    1. vbif
      25.11.2015 23:43
      +2

      По адресам: учитывая, что в КЛАДРе бывает не всё правильно и что нужно, никогда не удастся найти универсальное решение с хранением адреса. Тут надо смотреть необходимый уровень детализации: для областной больницы важна детализация до населённого пункта, в то время как для городской поликлиники придётся помучиться.

      Что касается diff-ов: чтобы определить, что я сейчас перенёс функцию A в другое место, переименовал в B и поменял местами несколько строк, или же просто удалил, а в другом месте написал новую, которая по случайному совпадению оказалась немного похожей — для этого никакого искусственного интеллекта не хватит. Тут и естественному интеллекту без телепатии не обойтись. Однако если делать коммиты частыми и осмысленными, то в этом нет никакой необходимости. Перенёс функцию — закоммитил, переименовал — закоммитил, удалил ненужные строки — закоммитил.


      1. torkve
        26.11.2015 00:09
        +1

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


        1. vbif
          26.11.2015 00:43

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


          1. torkve
            26.11.2015 00:49

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


  1. velvetcat
    25.11.2015 20:22
    +2

    del


  1. nwalker
    26.11.2015 00:08
    -7

    Ну вы все же поосторожнее с громкими заголовками, а то так и до восхваления Go или Oberon можно докатиться.


  1. stalkerg
    26.11.2015 00:37
    -1

    Бред. Автор видит связи там где их нет.