aprendiendo ( Erlang ).

viernes, 10 de agosto de 2012

REPL (II) en Erlang. Behaviour.

| 0 comentarios |

Siguiendo con las sugerencias y mejoras realizadas por nuestro amigo Manuel Rubio (Bombadil) en su página Bosque Viejo. Como te comenté … Ahora, seré yo quien tome prestado el gen_cmd ;þ.

Creando nuestro propio behaviour (comportamiento).

En su momento vimos como utilizar el gen_server de OTP y como dotar a nuestro módulo del comportamiento de un servidor genérico. Pues bien, la idea, es crear un comportamiento (behaviour) para que, podamos dotar de dicho comportamiento a otros módulos.

El comportamiento o Behaviour, es el mecanismo que nos proporciona Erlang para separar la funcionalidad de un comportamiento más o menos genérico. El módulo que implementa el comportamiento, establece sus requisitos e impone sus condiciones al módulo callback, que se encargará de la funcionalidad. Lo podríamos ver como un interfaz que debe implementar todo módulo que desee tener el comportamiento implementado.

Realmente, crear un behaviour es muy simple. Tremendamente simple. Tan sólo tenemos que implementar la función behaviour_info/1 y exportarla. Esta función recibe el átomo callbacks y retorna un lista de pares {función, aridad}. Como ya hemos comentado estas funciones son las que obligamos a implementar en el módulo callback. Veamos un ejemplo de implementación.

-module(simple_behaviour).

-export([start/2, behaviour_info/1]).

behaviour_info(callbacks) ->
    [{funcion1,1}, {funcion2, 2}];

behaviour_info(_) -> 
    undefined.

Nuestro módulo es un comportamiento, que declara que los módulos callbacks que lo quieran utilizar debe implementar la funciones funcion1/1 y funcion2/2. Ahora, veamos como debemos utilizarlo.

-module(simple_behaviour_callback).

-export([funcion1/1, funcion2/2]).

-behaviour(simple_behaviour).

funcion1 (_) ->
    void.

funcion2 (_,_) ->
    void.

Bien. Ahora tenemos un módulo que implementa el comportamiento simple. Como puedes ver la idea es bien sencilla. Y fácil de llevar a cabo.

De myniCommand a gen_cmd.

Si recordáis, en el artículo sobre REPL nos quedamos con un REPL básico que llamamos, con el simpático nombre de myniCommand. Este es su código.

-module(myniCommand).
-export([linea_comandos/2]).

linea_comandos(Prompt, Interprete) ->
    case io:get_line(standard_io, Prompt) of
        {error, Motivo} ->
            io:format(" error: ~p~n", [Motivo]),
            linea_comandos(Prompt, Interprete);
        {eof} -> 
            linea_comandos(Prompt, Interprete);
        Comando -> % operamos con el comando
            case Interprete(Comando) of
                exit ->
                    ok;
                _ ->
                    linea_comandos(Prompt, Interprete)
            end
    end.

Efectivamente, es una funcionalidad básica. Y como bien apuntó Manuel Rubio se puede encapsular en un Behaviour.

-module(gen_cmd).

-export([start/2, behaviour_info/1]).

behaviour_info(callbacks) ->
    [{handle_cmd,2}, {init, 1}];

behaviour_info(_) -> 
    undefined.

start(Prompt, Module) ->
    {ok, State} = Module:init([]),
    linea_comandos(Module, Prompt, State).

linea_comandos(Module, Prompt, State) ->
    case io:get_line(standard_io, Prompt) of
        {error, Motivo} ->
            io:format("error: ~p~n", [Motivo]),
            linea_comandos(Module, Prompt, State);
        {eof} -> 
            linea_comandos(Module, Prompt, State);
        Comando -> % operamos con el comando
            case Module:handle_cmd(Comando, State) of
                {stop, _Reason} ->
                    ok;
                {ok, NewState} ->
                    linea_comandos(Module, Prompt, NewState)
            end
    end.

No es tan simple como "el comportamiento simple", pero casi. Ahora, para poder implementar un REPL debemos implementar una función init/1, para inicializar un estado, y handle_cmd/2 para manejar el comando e imprimir el resultado.

Usando nuestro gen_cmd. Creando myErl.

Para ilustrar el funcionamiento del gen_cmd hemos implementado un pequeño interprete Erlang.

-module(myErl).
-export([init/1, handle_cmd/2, start/0]).

-behaviour(gen_cmd).

start() ->
    io:format("Bienvenido a myErl.~n"),
    gen_cmd:start("Cmd> ", ?MODULE),
    io:format("Adios.~n").

init([]) ->
    {ok, erl_eval:new_bindings()}.

handle_cmd ( "q\n", _State ) ->
    {stop, normal};
handle_cmd ( Comando, State ) ->
    {_State, Resultado} = eval (Comando, State),
    io:format("~p~n", [Resultado]),
    {ok, _State}.

eval ( Expresion, Bindings ) ->
    {ok,Scanned,_} = erl_scan:string(Expresion),
    {ok,Parsed} = erl_parse:parse_exprs(Scanned),
    {value, Resultado, _Bindings} = erl_eval:exprs(Parsed,Bindings),
    {add_bindings(Bindings, _Bindings), Resultado}.

add_bindings ( Bindings, [] ) ->
    Bindings;
add_bindings ( Bindings, [{Variable,Valor}|Resto] ) ->
    add_bindings(erl_eval:add_binding(Variable, Valor, Bindings), Resto).

Olvidemos por un momento la parte de evaluación y centrémonos en el comportamiento. Para empezar, podemos ver como hemos implementado las funciones callbacks impuesta desde nuestro gen_cmd. La función init/1, devuelve el par {ok, Estado}, y la función handle_cmd/2, como no podía ser de otra forma, se encarga de manejar e interpretar los comandos.

Ahora, que sabemos que, cumplimos el comportamiento gen_cmd pasamos a explicar lo que realiza este módulo. Para empezar, vamos ha definir el estado de nuestro REPL. El estado de nuestro REPL son los bindings que se han realizado en nuestro interprete. Los bindings son el conjunto de variables que hemos definido y sus correspondientes valores. Es decir, una lista de pares nombre-valor. Esta estructura se encarga de definirla la función erl_eval:new_bindings/0 del módulo erl_eval. Este módulo es el meta interprete de Erlang.

Pasamos ahora a evaluar el comando que recibimos. La función eval/2 es la encargada de interpretar el comando y devolver un nuevo estado y el resultado de la evaluación. Los pasos para interpretar un comando Erlang son:

  • Extraer los tokens de la cadena (erl_scan:string/1). Para más información erl_scan.
  • Parsear los tokens (erl_parse:parse_exprs/1). Para más información erl_parse.
  • Evaluar (erl_eval:exprs/2). Para más información erl_eval.

La función para evaluar una expresión nos devuelve el resultado y los nuevos bindings que debemos incorporar a nuestro estado anterior.

Ahora como es de rigor, realicemos una pequeña prueba.

1> c(gen_cmd).
{ok,gen_cmd}
2> c(myErl).
{ok,myErl}
3> myErl:start().
Bienvenido a myErl.
Cmd> A=2.
2
Cmd> A.
2
Cmd> B=[1,2,3].
[1,2,3]
Cmd> lists:max(B).
3
Cmd> F = fun (X) -> X + 1 end.
#Fun<erl_eval.6.80247286>
Cmd> lists:map(F, B).
[2,3,4]
Cmd> B.
[1,2,3]
Cmd> q
Adios.
ok

La verdad, es que para ser un interprete para andar por casa no esta nada mal. Ten en cuenta que no hemos realizado ninguna comprobación de errores a la hora de evaluar una expresión. Pero, con un poco más de esfuerzo estoy seguro que puedes mejorarlo y sacarle más partido. Yo, así lo voy hacer.

Y como puedes comprobar, en 0.3 un interprete Erlang.

Publicar un comentario

0 comentarios:

 
Licencia Creative Commons
Aprendiendo Erlang por Verdi se encuentra bajo una Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0 Unported.