Поведения — behaviours Erlang-е +11


Добрый день.

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

Зачем нужны поведения? Поведение — суть определение интерфейса. Установка контракта между вызывающей стороной и имплементацией. Ну и все что из этого вытекает в случаях обычного определения интерфейса класса. Только в этом случае мы определяем интерфейс модуля.

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

Если поведения декларируют функцию, совпадающую по имени и количеству параметров, то при компиляции появляется логичное предупреждение conflicting behaviours.

Синтаксическая сторона очень проста. Я положу код для примера и потом продолжу описание.
Код поведения:

-module(sample_behavoiur).
-export([default/2]).

-callback init(Args :: list())->
	{ok, State :: term()}.

-callback action(State :: term())->
	{ok, ActionResult :: term(), State::term()} | {error, ErrorInfo :: term() }.

-callback default(State :: term() )->
	{ok, DefaultResult :: term() }.

-optional_callbacks([default/1]).

-spec default(Mod :: atom(), State :: term())-> {ok, DefaultResult :: term() }.
default(Mod, State)->
	case erlang:function_exported(Mod, default, 1) of
		true ->
			Mod:default(State);
		false ->
			{ok, default}
	end.

И два модуля выполняющие эти поведения:
-module(implement_1).
-behaviour(sample_behavoiur).

-export([init/1,
		 action/1
		]).

-record(state,{list :: [integer()]}).

init(Args) ->
	{ok, #state{list = Args}}.

action(State = #state{list = []})->
	{ok, empty, State};
action(State = #state{list = [Head|Rest]})->
	{ok, Head, State#state{list = Rest}}.

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

simple_test()->
	{ok, Opaq1} = implement_1:init([1,2,3]),
	{ok, 1, Opaq2} = implement_1:action(Opaq1),
	{ok, 2, Opaq3} = implement_1:action(Opaq2),
	{ok, 3, Opaq4} = implement_1:action(Opaq3),
	{ok, empty, Opaq5} = implement_1:action(Opaq4),

	{ok, default} = sample_behavoiur:default(implement_1, Opaq4).
-endif.


-module(implement_2).
-behaviour(sample_behavoiur).

-export([init/1,
		 action/1,
		 default/1
		]).

-record(state,{list :: [integer()]}).

init(Args) ->
	{ok, #state{list = Args}}.


action(State = #state{list = []})->
	{ok, empty, State};
action(State = #state{list = [Head|Rest]})->
	{ok, Head, State#state{list = Rest}}.


default(_State = #state{list = []})->
	{ok, empty};

default(_State = #state{list = [Head|_]})->
	{ok, Head}.
	
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

simple_test()->
	{ok, Opaq1} = implement_2:init([1,2,3]),
	{ok, 1, Opaq2} = implement_2:action(Opaq1),
	{ok, 2} = sample_behavoiur:default(implement_2, Opaq2),

	{ok, 2, Opaq3} = implement_2:action(Opaq2),
	{ok, 3, Opaq4} = implement_2:action(Opaq3),
	{ok, empty, Opaq5} = implement_2:action(Opaq4),

	{ok, empty} = sample_behavoiur:default(implement_2, Opaq4)
-endif.


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

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

Более интересен случай это создание цепочки поведений, когда сам модуль поведения в свою очередь выполняет другое поведение сам. Пример можно посмотреть в часности в стандартной библиотеке Erlang, когда поведение supervisor в свою очередь выполняет поведение gen_server. В этом случае код делится на две части — первая выполняет свои контрактные обязательства, вторая предоставляет служебные функции для других модулей по новому контракту.

В функциональном коде далеко не всегда есть необходимость в определении новых поведений. И критерий необходимости будет прост — два и больше модуля имеющие одинаковую семантику или роль, требуют определения интерфейса и унификации. Затраченое время облегчит и тестирование и будущее расширение кода. Потому-что если есть 2-3 модуля с одной ролью, высок шанс появления еще нескольких.




К сожалению, не доступен сервер mySQL