Behaviours: la potencia de OTP

Una de las potencias de Erlang y el framework OTP, son los comportamientos (o behaviours), estos se basan en la Inversión de Control, es decir, que existe un código base que se autocompleta a través de la implementación de unos callbacks definidos en la plantilla, que deben de ser implementados para que todo funcione correctamente. En OTP ya existen de por sí algunos comportamientos para crear servidores (gen_server), máquinas de estados (gen_fsm), generadores de eventos (gen_event), supervisores (supervisor) y aplicaciones (application). No obstante, esto se puede ampliar y, para mostrar un poco el cómo se hace, vamos a ver algo que se sale de la dinámica de Erlang en lo que respecta a la construcción de elementos de servidor. Vamos a realizar una entidad genérica para crear interfaces con wxWidgets.

Una introducción breve a wxWidgets

En el recetario de ElrAr tienen una receta en la que se describe un código de hola mundo con wxWidgets. Este código, tal y como lo presento aquí:

-module(helloworld).
-compile(export_all).
 
-include_lib("wx/include/wx.hrl").
 
main(_Args) ->
    wx:new(),
    Frame = wxFrame:new(wx:null(), ?wxID_ANY, "Hello, World!"),
    setup(Frame),
    wxFrame:show(Frame),
    loop(Frame),
    wx:destroy().
 
setup(Frame) ->
    MenuBar = wxMenuBar:new(),
    File = wxMenu:new(),
 
    wxMenu:append(File, ?wxID_ABOUT, "Hello, World!"),
    wxMenu:appendSeparator(File),
    wxMenu:append(File, ?wxID_EXIT, "Quit"),
 
    wxMenuBar:append(MenuBar, File, "&File"),
    wxFrame:setMenuBar(Frame, MenuBar),
 
    wxFrame:createStatusBar(Frame),
    wxFrame:setStatusText(Frame, "Welcome To wxErlang"),
 
    wxFrame:connect(Frame, command_menu_selected),
    wxFrame:connect(Frame, close_window).
 
loop(Frame) ->
    receive
        #wx{id=?wxID_ABOUT, event=#wxCommand{}} ->
            Str = "Hello, world! From and wxErlang App!",
            MD = wxMessageDialog:new(Frame, Str,
                                     [{style, ?wxOK bor ?wxICON_INFORMATION},
                                      {caption, "Hello, World!"}]),
            wxDialog:showModal(MD),
            wxDialog:destroy(MD),
            loop(Frame);
 
        #wx{id=?wxID_EXIT, event=#wxCommand{type=command_menu_selected}} ->
            wxWindow:close(Frame, [])
 
   end.

Hay una parte de inicialización, un bucle principal en el que se espera por peticiones, y la finalización al presionar uno de los elementos.

Creando el gen_wx

Un behaviour es solo la definición de los callbacks que se deben de implementar para cumplir la especificación dada, y el código de las funciones, como un módulo normal, que se encargarán de la parte común del código.

En este caso, hemos definido los callback init/1, handle_event/3, terminate/1 y code_change/1. Estos callbacks se encargarán de la inicialización del código específico, manejar cada evento que llegue, ejecutando código para finalizar la ejecución y el código que haga falta para el cambio dinámico de código.

El código genérico que se encargará de lo que hemos visto en el ejemplo será el siguiente:

-module(gen_wx).
-export([behaviour_info/1]).
 
-include_lib("wx/include/wx.hrl").
 
-export([start/2, stop/1, code_change/3]).
 
behaviour_info(callbacks) ->
    [{init, 1}, {handle_event, 3}, {terminate, 1}, {code_change, 1}];
behaviour_info(_) ->
    undefined.
 
start(Module, Title) ->
    {ok, spawn(
        fun() -> 
            wx:new(),
            Frame = wxFrame:new(wx:null(), ?wxID_ANY, Title),
            {ok, State} = Module:init(Frame),
            wxFrame:show(Frame),
            loop(Module, Frame, State)
        end
    )}.
 
stop(Pid) ->
    Pid ! stop.
 
loop(Module, Frame, State) ->
    receive
        stop ->
            Module:terminate(State),
            wxWindow:close(Frame, []),
            wx:destroy();
        Msg ->
            case Module:handle_event(Frame, Msg, State) of
                {ok, NewState} -> 
                    ?MODULE:code_change(Module, Frame, NewState);
                {stop, normal} -> 
                    Module:terminate(State),
                    wxWindow:close(Frame, []),
                    wx:destroy()
            end
    end.
 
code_change(Module, Frame, State) ->
    Module:code_change(State),
    loop(Module, Frame, State).

Como se puede observar, la función accesible start/2 genera un proceso y retorna su PID, de modo que es accesible para poder enviarle mensajes. El proceso creado nuevo se mantiene en ejecución con la función loop/3, la cual hace puente con la función code_change/3 para actualizar su código en caso de que exista una nueva versión del código.

La plantilla básica que se queda para el uso de este gen_wx es la siguiente:

-module(helloworld).
-compile(export_all).
 
-behaviour(gen_wx).
 
%% WX callbacks
-export([init/1, handle_event/3, terminate/1, code_change/1]).
 
-include_lib("wx/include/wx.hrl").
 
-record(state, {}).
 
%%====================================================================
%% WX callbacks
%%====================================================================
 
init(Frame) ->
    {ok, #state{}}.
 
handle_event(_Frame, Event, State) ->
    io:format("~p~n", [Event]),
    {ok, State}.
 
terminate(_State) ->
    ok.
 
code_change(_State) ->
    ok.

De esta forma, la programación del código anterior, en base a init/1 se quedaría de la siguiente forma:

init(Frame) ->
    MenuBar = wxMenuBar:new(),
    File = wxMenu:new(),
 
    wxMenu:append(File, ?ABOUT, "Hello, World!"),
    wxMenu:appendSeparator(File),
    wxMenu:append(File, ?EXIT, "Quit"),
 
    wxMenuBar:append(MenuBar, File, "&File"),
    wxFrame:setMenuBar(Frame, MenuBar),
 
    wxFrame:createStatusBar(Frame),
    wxFrame:setStatusText(Frame, "Welcome To wxErlang"),
 
    wxFrame:connect(Frame, command_menu_selected),
    wxFrame:connect(Frame, close_window),
    {ok, #state{}}.

Ya tendríamos la inicialización completa. Y ahora, la función loop/1 del ejemplo original, con su bloque receive, quedaría de la siguiente forma:

handle_event(Frame, #wx{id=?ABOUT, event=#wxCommand{}}, State) ->
    Str = "Hello, world! From and wxErlang App!",
    MD = wxMessageDialog:new(Frame, Str,
                             [{style, ?wxOK bor ?wxICON_INFORMATION},
                             {caption, "Hello, World!"}]),
    wxDialog:showModal(MD),
    wxDialog:destroy(MD),
    {ok, State};
 
handle_event(_Frame, #wx{id=?EXIT}, State) ->
    {stop, normal}.

Con eso ya tendríamos todo el código de ejemplo funcional y dentro de un comportamiento mucho más simple de utilizar, o al menos, con un código más fácil de trazar, ya que sus funciones terminan siendo más cortas que el bloque receive inicial.

Conclusiones

La creación de comportamientos (behaviours) nos da la potencia de abstraer el código, la complejidad de muchas partes existentes, y llevarla a una simpleza en la que poder orientarnos ya solo al problema a resolver y no a todo el entramado de la programación de la solución al completo. Es una técnica que se debería de emplear cada vez más en los desarrollos en Erlang.