Отладка в Erlang

Любое приложение на Erlang состоит из двух базовых структурных единиц: модулей и процессов. В модулях описывается код, а процессы выполняют код из разных модулей в разный момент времени. Было бы интересно в процессе работы приложения понаблюдать за тем, какой процесс вызывает какую функцию, и что она возвращает. Такая возможность, разумеется, в Erlang/OTP есть, и помимо этого, ею реально удобно пользоваться. Она работает очень просто и именно так, как ожидается. Трассировка встроена в виртуальную машину BEAM и использует язык Erlang для описания своего поведения, поэтому её можно с лёгкостью интегрировать в своё приложение в качестве подсистемы мониторинга и диагностики.

Трассировщик

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

Трассировщик запускаеся простой командой:

dbg:tracer().

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

А если недостаточно?

Этот параграф можно не читать.

Если стандартное поведение трассировщика нас не устраивает, то его можно с лёгкостью определить самому, используя функцию dbg:tracer/2. В этой функции первым аргументом описывается тип трассировщика: process или port.

В случае типа process, трассировщик будет работать как обычный erlang-процесс, принимая сообщения и применяя к ним функцию, определённую вторым аргументом.

В случае типа port, вторым аргументом должна идти функция, определённая с помощью dbg:trace_port/2. Использование порта позволяет снизить издержки на добавление трассировочных сообщений в очередь erlang-процесса, посылая их сразу напрямую в драйвер. Определены два трассировочных драйвера: ip и file, из названия которых сразу понятно, что они делают.

Драйвер ip открывает TCP/IP-порт, указанный вторым аргументом, и начинает его слушать. Как только на этот порт подцепляется трассировочный клиент с другой erlang-ноды, используя команду dbg:trace_client/2, он начинает получать сообщения и печатать их на консоль, как обычный dbg:tracer/0. Чтобы переопределить поведение клиента, используем dbg:trace_client/3.

Драйвер file работает похожим образом, но использует запись в файл, для последующего чтения с помощью dbg:trace_client/2.

События

Теперь определяемся, что хотим видеть, и задаём это с помощью команды:

dbg:p(Item, Flags).

Здесь Item задаёт необходимое нам множество процессов, действия которых надо отловить:

  • Переменная типа pid(): будем получать события от этого конкретного процесса
  • Атом all: получаем события от всех процессов в системе
  • Атом new: только от новых процессов, уже запущенные будут игнорироваться
  • Атом existing: только от уже запущенных процессов, новые будут игнорироваться (противоположность new)
  • Какой-то другой атом: будем получать события от процесса, зарегистрированного под этим атомом
  • Целое число: преобразуется в идентификатор процесса <0.Item.0>
  • Кортеж {X,Y,Z}: преобразуется в идентификатор процесса <X.Y.Z>
  • Строка "<X.Y.Z>": преобразуется в идентификатор процесса <X.Y.Z>

Значение Flags может быть атомом или списком следующих атомов:

  • s (send): отправляемые сообщения
  • r (receive): получаемые сообщения
  • m (messages): и то, и другое
  • c (call): вызываемые функции (это самый используемый флаг и подробнее о нём в параграфе про шаблоны функций)
  • p (procs): прочие события процесса
  • sos (set on spawn): флаги трассировки будут наследоваться всеми процессами, порождёнными множеством процессов, заданным Item
  • sol (set on link): флаги трассировки будут наследоваться всеми процессами, с которыми линкуется множество процессов, заданное Item
  • sofs (set on first spawn): то же самое, что и sos, но только для первого порождённого процесса
  • sofl (set on first link): то же самое, что и sol, но только для первого залинкованного процесса
  • all: устанавливает все вышеперечисленные флаги
  • clear: сбрасывает все вышеперечисленные флаги
  • timestamp: добавляет время события

Описание dbg:p/2

Шаблоны функций

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

  • dbg:tp/2,3,4: добавляет трассировку только глобальных функций
  • dbg:tpl/2,3,4: добавляет трассировку локальных функций
  • dbg:ctp/0,1,2,3: удаляет трассировку, заданную dbg:tp/2,3,4 или dbg:tpl/2,3,4
  • dbg:ctpl/0,1,2,3: удаляет трассировку, заданную только dbg:tpl/2,3,4 (локальные функции)
  • dbg:ctpg/0,1,2,3: удаляет трассировку, заданную только dbg:tp/2,3,4 (глобальные функции)

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

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

Разберём пример:

dbg:tracer().
dbg:p(all, [c, timestamp]).
dbg:tp(random, uniform, cx).

Здесь используется функция dbg:tp/3, гре первый аргумент — имя модуля random, второй — имя функции uniform, аргумента арности здесь нет, поэтому будет браться любая из существующих. Последний аргумент — MatchSpec имеет значение cx. Это то, что нам обычно и нужно. Параметр представляет собой суперпозицию двух параметров: c и x. Параметр c показывает, какая функция вызывает трассируемую функцию, а параметр x показывает возвращаемое значение трассируемой функции или исключение, если оно происходит. Вот так будет выглядеть вызов функции random:uniform/1 после включенной трассировки:

random:uniform(14).
(<0.32.0>) call random:uniform(14) ({erl_eval,do_apply,6}) (Timestamp: {1335,366136,885591})
(<0.32.0>) returned from random:uniform/1 -> 7 (Timestamp: {1335,366136,885625})
7

И в случае исключения (передаём неверный аргумент — атом вместо целого числа):

random:uniform(asd).
(<0.32.0>) call random:uniform(asd) ({erl_eval,do_apply,6}) (Timestamp: {1335,366468,181974})
(<0.32.0>) exception_from {random,uniform,1} {error,function_clause} (Timestamp: {1335,366468,181997})
** exception error: no function clause matching random:uniform(asd) (random.erl, line 111)

Это простой пример. При трассировке в больших проектах, наглядность сообщений очень повышается, по ним можно проследить любой произвольный отрезок жизни системы, включая внутренние состояния процессов (помним, что состояния процессов передаются из одной функции в другую, в качестве аргумента, например, handle_cast(Message, State) в процессе gen_server). Если же хочется просто посмотреть состояние нужного процесса gen_server в произвольный момент времени, то для этого существует функция sys:get_status(Pid).

Oстановка трассировки

Остановить трассировку можно функцией dbg:stop_clear(). После этого действия процесс трассировки завершится, а все шаблоны функций очистятся.

Распределённая отладка

Не забываем, что Erlang/OTP имеет хорошие средства для построения кластеров. Отладка тоже не отстаёт. Для этого всего лишь надо добавить удалённые ноды с помощью вызова dbg:n(remotenode1@somehostname). Теперь трассировщик будет принимать сообщения от удалённых нод тоже. Удалить удалённую ноду из списка трассируемых можно вызовом dbg:cn(remotenode1@somehostname).

Заключение

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

Comments

Почему я больше не буду троллить Node.js

В среду ездил в 2gis на мероприятие #DevDay. Там проводился квартирник, посвящённый Node.js и меня пригласили в качестве тролля-эксперта, противника Node.js. Я раньше был замечен в нелестных отзывах о ноде, всяческих сравнениях её с Erlang (не в пользу ноды, разумеется), поэтому должен был троллить и унижать её на квартирнике. Сразу скажу, что после мероприятия у меня абсолютно пропало желание троллить ноду и в ближайшее время я этого делать не буду, делал я это только по незнанию, так как не совсем чётко представлял себе, что такое нода.

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

Затем после перерыва начался сам квартирник, вели который Влад Семёнов (@Semenov) и Степан Столяров (@stevebest). Ребята сразу же рассказали столько страшных вещей про Node.js, что абсолютно перехотелось троллить. Оказалось, что приложение на ноде надо перезапускать каждые два дня, ибо оно течёт, и утечки очень сложно поймать. Говорили, что в Яндекс сделали шаблоны, способные работать на стороне клиента и сервера сначала на JS (сервер на Node.js), так как хотелось использовать один и тот же код, но потом серверную часть переписали на C, получив выигрыш в производительности всего 20%. Видать, стало тяжело поддерживать, и хотя бы на сервере решили упростить задачу. (Яндексоиды, если кто в теме, можете прояснить?)

Говорили про библиотеку socket.io, которая реализует WebSockets и позволяет прозрачно делать даунгрейд в браузерах вплоть до IE5.5, и в качестве примера сервиса, использующую эту библиотеку с Node.js, привели trello.com, где из socket.io удалили всю возможность даунгрейда и поддерживают только новые браузеры. Это расово верно, так как чем меньше сайтов будет поддерживать старое барахло, тем лучше для мировой революции.

Тут я понял, что люди, выбирающие Node.js в здравом уме и твёрдой памяти, не нуждаются в Erlang. Нет смысла сравнивать ноду и ерланг, у них разные непересекающиеся ниши. Сторонников ноды не страшат пилообразные графики CPU и памяти в munin. Ну, упал один инстанс ноды, ну увидели несколько пользователей ошибку на nginx, это ж не катастрофа. Нажмёт он F5 и запрос обработает другой инстанс ноды, а первый пока поднимется. Это не смертельно, это не критично. Зато прикольно, круто и модно. Starbucks одобряє. Когда я раньше троллил ноду, считал, что она наступает ерлангу на пятки. Увы, это не так. Пропасть между нодой и ерлангом просто огроменная.

Общеизвестно, что 95% софта может быть написано абсолютно на любом языке. Не всем нужны кластеры, не всем нужна отказоустойчивость, не всем нужна возможность держать миллион соединений, не у всех есть высокие нагрузки. Это нормально. Оставшиеся 5% в принципе могут быть написаны только на чём-то узкоспециализированном и не имеющем альтернатив: C, ASM, Verilog, Erlang, JS (client-side!). Выбирать технологию для 95% можно руководствуясь лишь личными предпочтениями или модой. Node.js для этого хорошо подходит.

UPD: @illbullet меня поправил. Про шаблонизатор на C и JS — это было в Mail.ru, подробнее тут. Не знаю, почему у меня про Яндекс отложилось.

Comments (9)

Массовый gen_tcp:send из одного процесса

В книжках и мануалах по эрлангу есть несколько тонких мест, в которых нельзя слушать мануалы, а надо читать исходники. Одно из этих мест — gen_tcp:send. Если вы хотите раздать много данных, то надо пользоваться недокументированными возможностями.

gen_tcp:send устроен следующим образом: сначала вызывается port_command, это от 1 до 2 микросекунд; потом ожидается сообщение {inet_reply,Socket,Reply}

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

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

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

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

Однако есть опасность того, что вы забьете выходной буфер драйвера мегабайтами трафика, который клиент не будет вычитывать. Для противодействия этому в port_command есть механизм suspend. Если внутренний буфер переполнен, то драйвер тормозит весь ваш процесс. Это совсем не то, что хочется схлопотать, обслуживая кучу сокетов из одного процесса.

Поэтому надо вызывать port_command(Socket, Data, [nosuspend]). Если он возвращает false, значит писать в этот сокет больше нельзя. Тут надо решить что делать: либо выкидывать этот сокет на какое-то время из обслуживания, либо вообще отключать.

Так же немного неясно, что делать, если сокет закрылся.

Рецепт простой: (catch port_command(Socket, Data, [nosuspend])) либо возвращает true, либо с клиентом что-то плохо. Можно вдаваться в детали, а можно тупо отключить его, потому что он никак не вычитывает данные.

Comments (9)

Отложенная инициализация процесса

Инициализация процесса gen_server происходит в коллбеке init/1. Этот коллбэк вызывается уже запущенным новым процессом с поведением gen_server после того, как внешний процесс захотел его запустить с помощью функции start_link/1. Пусть модуль с описанием этого нового процесса имеет имя zz_worker. Тогда функция, запускающая этот процесс будет выглядеть так:

start_link(Args) ->
    gen_server:start_link(zz_worker, Args, []).

Разберём функцию gen_server:start_link/3. Первый её аргумент — имя модуля, где находятся коллбэки gen_server, описывающие действия процесса в ответ на определённые сообщения. Второй аргумент Args — то, что передаётся в функцию init/1. Третий аргумент — системные параметры, мы их тут не рассматриваем, поэтому просто передаём пустой список.

Процесс, вызывающий zz_worker:start_link/1 получит ответ {ok, Pid :: pid()} только тогда, когда функция init/1 закончит свою работу, а до тех пор вызывающий процесс будет находиться в блокировке. Если инициализация процесса проходит быстро, то и вызывающий процесс быстро получит ответ и продолжит работу. Если для инициализации требуется совершить какое-то длительное действие, то вызывающий процесс получит ответ и продолжит своё выполнение не скоро. Очень часто нас такое поведение устраивает и ничего больше не надо делать. Часто, но не всегда. Иногда возникает необходимость быстро запускать много процессов, не дожидаясь завершения инициализации каждого. Для этого надо уменьшить время выполнения функции init/1, а всю тяжёлую инициализацию оставить на потом.

Рассмотрим функцию init/1:

init(Args) ->
    State = do_long_initialization(Args),
    {ok, State}.

Здесь некая функция do_long_initialization/1 выполняется очень долго, своим выполнением оттягивая момент получения идентификатора этого процесса внешним процессом. Подумаем, как её выполнение оставить на потом. Вспоминаем, что в Erlang у нас все процессы обмениваются сообщениями, и сообщение можно послать любому процессу, в том числе и самому себе. Если мы пошлём сообщение самому себе в функции init/1, то оно гарантированно появится в мейлбоксе самым первым, так как больше никто про этот процесс ещё не узнал и не может ничего ему послать.

init(Args) ->
    self() ! {init, Args},
    {ok, #state{}}.

init/1 возвращает кортеж {ok, State :: term()} если инициализация прошла успешно, причём значение State будет использоваться в дальнейшем в качестве состояния процесса. Сейчас мы просто вернём сконструированный рекорд #state{} со значениями полей по умолчанию. Всё, в init/1 процесс посылает сообщение самому себе, функция на этом завершается и внешний процесс получает идентификатор нового процесса, после чего выходит из состояния блокировки и может работать дальше.

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

handle_info({init, Args}, OldState) ->
    NewState = do_long_initialization(Args),
    {noreply, NewState}.

Здесь в OldState будет то самое старое состояние со значениями по умолчанию. Оно нам сейчас не нужно, просто игнорируем его. И уже в этом обработчике вызываем ту самую долгую функцию do_long_initialization/1, которая возвращает нам новое актуальное значение состояния процесса. Не забываем, что этот обработчик выполнится самым первым после завершения функции init/1, поэтому не паримся насчёт гонок, их не будет.

Вместо посылки сообщения с помощью ! можно так же использовать gen_server:cast(self(), {init, Args}), тогда обрабатывать сообщение надо в обработчике handle_cast/2.

Comments

Делаем свои шаблоны Erlang модулей для Emacs

erlang-mode для Emacs включает довольно приличный набор шаблонов. Попробуйте выполнить Meta-X tempo-temp Tab Tab, и увидите что их там 23 штуки.

Но с ними связаны некоторые неудобства. Во-первых, довольно громоздкий способ вызова (впрочем, это можно обернуть в свою функцию, с более лаконичным именем). Во-вторых, сами шаблоны трудно модифицировать и добавлять (они определены здесь: /usr/lib/erlang/lib/tools-2.6.6.3/emacs/erlang_appwiz.el). А хотелось бы, чтобы каждый шаблон был определен в отдельном файле и был удобен для модификаций.

К счастью, при некотором знании elisp Emacs позволяет сделать все так, как нам хочется :) Для начала определимся, где мы будем их хранить. Например тут: ~/emacs.d/tpl. И положим туда, например, шаблон Erlang-модуля:

-module(module_tpl).
-author('Vasja Pupkin <pupkin@somewhere.com>').

-export([]).

%%% module API

Или вот такой шаблон для gen_server. Или вот такой шаблон для supervisor.

Затем определим где-нибудь (например в ~/.emacs.d/init.el) функцию на elisp, которая будет копировать нужный файл в проект и модифицировать его соотвественно указанному имени. Много кода не понадобится :)

(defun erl-new-file (module-name tpl-file)
  (setq new-file (format "%s.erl" module-name))
  (copy-file tpl-file new-file)
  (switch-to-buffer (find-file new-file))
  (search-forward "(")
  (setq begin (point))
  (search-forward ")")
  (backward-char)
  (kill-region begin (point))
  (insert module-name))

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

(defun erl-new-module (module-name)
  (interactive "MModule name:")
  (erl-new-file module-name "~/.emacs.d/tpl/module_tpl.erl"))

(defun erl-new-supervisor (module-name)
  (interactive "MModule name:")
  (erl-new-file module-name "~/.emacs.d/tpl/supervisor_tpl.erl"))

(defun erl-new-gen-server (module-name)
  (interactive "MModule name:")
  (erl-new-file module-name "~/.emacs.d/tpl/gen_server_tpl.erl"))

Ну вот, теперь, если нам нужно создать в проекте новый модуль gen_server, то мы вызываем Meta-X erl-new-gen Tab, указываем имя модуля, и он создается из шаблона и открывается у нас в редакторе.

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

Comments (1)

Erlang. Прагматичный рассказ про прагматичный язык.

15 октября 2011 выступал на 5-й встрече сообщества scala.by. Было клева, аудитория оказалась весьма заинтересованная, засыпали вопросами. Хотя, казалось бы, Erlang для сообщества Scala программистов немного оффтопик. Но нет. Некоторые даже приехали из других городов, чтобы послушать. Я был весьма польщен этим :)

Встреча была довольно долгая, затянулась часов на 5. Сперва я рассказывал про историю Erlang и давал общий обзор языка (многопоточность, устойчивость к ошибкам, распределенность, горячее обновление). Затем была довольно длинная live coding сессия, где я делал сервер сокращения ссылок :) Сперва сделал его без OTP, потом переделал в нормальный gen_server (чтобы наглядно показать, почему с gen_server лучше, чем без него). Потом была беседа про OTP, и вопросы по другим темам.

Здесь отчет о встрече на сайте сообщества. Фотки доступны здесь и здесь.

Видеозапись встречи:

Первая часть моего выступления также доступна в формате plain txt :) И вот она ниже:

(Read more)
Comments (3)

Несбывшиеся мечты

На заре программирования, в 80-х и 90-х годах люди верили, что если сделать код, имеющий многие уровни абстракций, то наступит счастье. Для добавления новой функциональности надо будет всего лишь унаследовать пару классов, переопределить три метода и всё само заработает. Кроме того, написанный один раз код можно будет использовать повторно. Именно тогда появились такие вещи, как C++, Java, SOAP, CORBA, ASN.1, где, казалось бы, заложено всё, что угодно, расширяй — не хочу.

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

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

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

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

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

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

Слабая связность vs. сильная. Сообщения vs. методы. Объектно ориентированное программирование vs. процессы.

P.S. И да, говорить, что вызов метода в C++/Java/ObjC, это тоже посылка сообщения — глупость. Посылка сообщений подразумевает их потенциальную асинхронность и наличие очереди сообщений. Если сообщения только синхронные и не складываются в очередь, то это не сообщения, а миф.

Comments

Python после Erlang

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

retfun() ->
  io:format("~p~n", [abc]),
  5+5.

вернёт 10. Ну и то, что абсолютно все функции либо возвращают что-то, либо кидают исключение, тоже кажется нормальным поведением.

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

И когда я пишу на питоне что-то вроде:

def retfun(self, arg):
    self.some_other_fun(arg)

а оно мне возвращает None вместо результата функции self.some_other_fun(arg), я очень негодую. Зачем делать функции, которые ничего не возвращают? Ведь если сделать так, что каждая функция должна что-то возвращать, тогда можно будет выкинуть ключевое слово return.

Comments (9)

Релиз CEAN 2.0

После долгих лет забвения вышел релиз CEAN 2.0. Теперь это не только репозиторий с пакетами, но ещё и фреймворк для разработки на Erlang/OTP на основе git и zsh.

  • Добавляет новые команды в Unix shell и Erlang.
  • Фреймворк теперь Open Source (GPL).
  • Возможность генерировать пакеты и инсталляторы.
  • Работает в кластерном окружении. Возможна синхронизация Erlang/CEAN на нескольких хостах, используя всего одну команду.
  • Возможность иметь столько версий инсталляций Erlang/OTP, сколько хочется.
  • Надёжный генератор зависимостей пакетов.

Ссылки:

Comments

HTTP Streaming

«HTTP Streaming» by Макс Лапшин:

Comments

«Зачем рельсовику Ерланг» by Макс Лапшин:

Comments

Вставка кода

Код можно записывать так:

handle_info({Port, {data, Data}}, #state{port = Port} = State) ->
  ?DBG("Data from port: ~p", [Data]),
  port_close(Port),
  {noreply, State#state{port = undefined}};
handle_info(_Info, State) ->
  ?DBG("Unhandled info: ~p", [_Info]),
  {noreply, State}.

или так:

Comments (21)

Erlang Russian теперь работает на платформе Metalkia
Comments