/ in Russian, на русском

Увидеть поток выполнения программы на эрланге

Мы в Erlyvideo разрабатываем видеостриминговый сервер Flussonic на Erlang/OTP, и соответственно думаем в терминах этой платформы. Несмотря на то что Эрланг довольно простой язык, нам всё равно при чтении приходится выпрямлять в голове код во что-то более простое.

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

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

В этой статье описывается лишь идея. Никакой автоматической визуализации на данный момент нет.

Какая польза от визуального обозначения?

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

Такой язык существует с незапамятных времён, называется «блок-схемы алгоритмов». Однако в том виде что мы привыкли его использовать он для Эрланга не подходит. Блок-схемы ничего не знают об исключениях и их отлове, нет там подходящих обозначений для получения, отправки сообщений.

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

  • Логи. Вместо того чтобы медитировать в абзац текста, получится увидеть прочерченную дорожку по блокам функций, с точками с метаинформацией в ключевых местах.
  • Анализ алгоритма. Если мы видим три подряд нарисованных цикла, а после прогона примеров с отслеживанием ветвлений в функции мы увидим что все три цикла прошли ровно n итераций, то можно легко догадаться, что сложность алгоритма по меньшей мере 3n, и возможно можно упростить до n объединив эти циклы в один.
  • Покрытие тестами. Сейчас вычисленное покрытие — сколько раз выполнилась та или иная строчка. Получится увидеть как покрытие как политическую карту мира на глобусе, заметить какие-то детали, закономерности.
  • Куча другой информации о программе, использовать которую нам до сих пор не пришло в голову.

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

Seeing Spaces from Bret Victor on Vimeo.

Bret Victor - Inventing on Principle from CUSEC on Vimeo.

Первые идеи и наброски

Очевидно, что сопоставление с образцом (case-of) это одна из фундаментальных конструкций в Эрланге. С её помощью выражаются if, orelse, andalso, в неё транслируется сопоставление с образцом в параметрах функции.

Кроме того, любый цикл — это просто фунция с хвостовой рекурсией с case-of внутри.

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

Представлял стройную систему, где функции закреплены на одной оси, подобно символам деванагари.

Также вдохновлялся этой страницей.

Любая программа на Эрланге при компиляции проходит несколько стадий. На одной из стадий она транслируется в упрощённый язык, который называется Core Erlang, с более бедным и строгим синтаксисом. На этой стадии весь код выровнен и упрощён — то что мне и необходимо для понимания потока выполнения.

Я понял, что для потока выполнения переменные, литералы, и промежуточные значения не имеют значения. Места где могут произойти ошибки, несоответствие образца, блокирование процесса (receive) и также перехват ошибок (catch) — вот что действительно должно быть легко видно.

Базовые элементы

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

Сопоставление с образцом (case-of) — самая часто используемая конструкция. Но сопоставление бывает двух видов — разрешающееся всегда, или нет.

Сопоставление, в котором может возникнуть badmatch рисуется с незавершённой правой веткой с красной стрелкой.

Важно понять как разбивается пространство, что куда отображается, как и что движется:

Или к примеру, сопоставление которое всегда успешно:

Если в сопоставлении есть только одна успешная ветка, то его можно описать такой схемой:

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

Вообще, ветка с красной стрелой обозначает не только badmatch,
а ошибку вообще.

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

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

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

try-catch это такое вычисление вызова, которое может вернуть успешное значение и сопоставить его, либо ошибку, которую тоже сопоставть с образами (catch). Также ошибка будет проброшена выше, если не подошёл ни один образец из catch.

В Эрланге есть конструкция try-catch-after, имеющая блок выполняющийся независимо от успеха.

Для того чтобы правильно отобразить поведение, надо показать как сохраняются ошибки, и снова выбрасываются после выполнения after блока:

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

receive-after это ожидание сообщения подходящего под образец.
Это тоже очень похоже на case-of, но отличие в том, что нет badmatch, есть лишь таймаут. Важно показать, что есть четыре разных варианта receive-after:

  1. after 0, означает, что приём сообщения не блокирующий, если подходящего сообщения нет, двигаемся дальше.
  2. after не указан, либо after infinity. Это означает что процесс будет ждать сообщения до бесконечности.
  3. after X, где X может быть как числом, так и атомом. Если значение равно infinity, то процесс заблокируется навечно. Самое подходящее обозначение для таких receive-after это знак вопроса.
  4. after N, где N гарантированно является положительным целым числом.

Все эти варианты ведут себя по-разному, поэтому их следует отображать по-разному. Вот несколько примеров:

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

Ещё примеры программ

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

Ещё пример:

Замечания

Я намеренно никак не выделил вызовы вроде erlang:spawn/1. Текущий язык описывает только поток выполнения функций. Отображение того как выполняются процессы по этому ландшафту кода наверное потребует дополнительного слоя, возможно со своим собственным языком.

Нерешённые проблемы

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

Вызовы с передачей локальной функции как аргумента пока не отображатся никак (вроде lists:foreach/2). Возможно оборачивать такой вызов в коробку, внутри которой будет полноценная раскрытая функция.

Замеченные проблемы

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