
En Habré, puede encontrar muchas publicaciones que revelan tanto la teoría de las mónadas como la práctica de su aplicación. La mayoría de estos artículos se espera sobre Haskell. No volveré a contar la teoría por enésima vez. Hoy hablaremos sobre algunos problemas de Erlang, formas de resolverlos con mónadas, uso parcial de funciones y azúcar sintáctica de erlando , una biblioteca genial del equipo de RabbitMQ.
Introduccion
Erlang tiene inmutabilidad, pero no mónadas * . Pero gracias a la funcionalidad parse_transform y la implementación de erlando en el lenguaje, todavía existe la posibilidad de usar mónadas en Erlang.
Sobre la inmunidad al comienzo de la historia, no hablé por casualidad. La inmunidad está en casi todas partes y siempre, una de las ideas principales de Erlang. La inmunidad y la pureza de las funciones le permiten concentrarse en el desarrollo de una función específica y no tener miedo a los efectos secundarios. Pero los recién llegados a Erlang, provenientes de Java o Python, por ejemplo, encuentran bastante difícil de entender y aceptar las ideas de Erlang. Especialmente si recuerdas la sintaxis de Erlang. Aquellos que intentaron comenzar a usar Erlang probablemente notaron su inusual e independencia. En cualquier caso, he acumulado muchos comentarios de principiantes y la sintaxis "extraña" lidera la calificación.
Erlando
Erlando es un conjunto de extensiones de Erlang que nos brinda:
- Uso / currículum parcial de funciones con cortes tipo Scheme
- Haskell-como hacer anotaciones
- import-as - azúcar sintáctico para importar funciones desde otros módulos.
Nota: Tomé los siguientes ejemplos de código para ilustrar las características de erlando de la presentación de Matthew Sackman, diluyéndolos parcialmente con mi código y explicaciones.
Corte abstracto
Ve directo al grano. Considere varias funciones de un proyecto real:
info_all(VHostPath, Items) -> map(VHostPath, fun (Q) -> info(Q, Items) end). backing_queue_timeout(State = #q{ backing_queue = BQ }) -> run_backing_queue( BQ, fun (M, BQS) -> M:timeout(BQS) end, State). reset_msg_expiry_fun(TTL) -> fun (MsgProps) -> MsgProps #message_properties{ expiry = calculate_msg_expiry(TTL)} end.
Todas estas funciones están diseñadas para sustituir parámetros en expresiones simples. De hecho, esta es una aplicación parcial, ya que algunos parámetros no se conocerán antes de la llamada. Junto con la flexibilidad, estas características agregan ruido a nuestro código. Al cambiar un poco la sintaxis, al ingresar cut, puede mejorar la situación.
Valor _
- _ se puede usar en plantillas
- Cut te permite usar _ patrones externos
- Si está fuera de la plantilla, se convierte en un parámetro para la expresión en la que se encuentra
- El uso múltiple de _ dentro de la misma expresión conduce a la sustitución de varios parámetros en esta expresión
- El corte no es un reemplazo para los cierres (diversión)
- Los argumentos se evalúan antes de la función de corte.
Cut usa _ en expresiones para indicar dónde se debe aplicar la abstracción. El corte solo envuelve el nivel más cercano en la expresión, pero el corte anidado no está prohibido.
Por ejemplo list_to_binary([1, 2, math:pow(2, _)]).
list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]).
a list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]).
pero no en fun (X) -> list_to_binary([1, 2, math:pow(2, X)]) end.
.
Suena un poco confuso, reescribamos los ejemplos anteriores usando cut:
info_all(VHostPath, Items) -> map(VHostPath, fun (Q) -> info(Q, Items) end). info_all(VHostPath, Items) -> map(VHostPath, info(_, Items)).
backing_queue_timeout(State = #q{ backing_queue = BQ }) -> run_backing_queue( BQ, fun (M, BQS) -> M:timeout(BQS) end, State). backing_queue_timeout(State = #q{backing_queue = BQ}) -> run_backing_queue(BQ, _:timeout(_), State).
reset_msg_expiry_fun(TTL) -> fun (MsgProps) -> MsgProps #message_properties { expiry = calculate_msg_expiry(TTL) } end. reset_msg_expiry_fun(TTL) -> _ #message_properties { expiry = calculate_msg_expiry(TTL) }.
Orden de cálculo de argumentos
Para ilustrar el orden en que se calculan los argumentos, considere el siguiente ejemplo:
f1(_, _) -> io:format("in f1~n"). test() -> F = f1(io:format("test line 1~n"), _), F(io:format("test line 2~n")).
Como los argumentos se evalúan antes de la función de corte, se mostrará lo siguiente:
test line 2 test line 1 in f1
Cortar la abstracción en varios tipos y patrones de código
- Tuplas
F = {_, 3}, {a, 3} = F(a).
- Listas
dbl_cons(List) -> [_, _ | List]. test() -> F = dbl_cons([33]), [7, 8, 33] = F(7, 8).
- Registros
-record(vector, { x, y, z }). test() -> GetZ = _#vector.z, 7 = GetZ(#vector { z = 7 }), SetX = _#vector{x = _}, V = #vector{ x = 5, y = 4 } = SetX(#vector{ y = 4 }, 5).
- Casos
F = case _ of N when is_integer(N) -> N + N; N -> N end, 10 = F(5), ok = F(ok).
- Mapas
test() -> GetZ = maps:get(z, _), 7 = GetZ(#{ z => 7 }), SetX = _#{x => _}, V = #{ x := 5, y := 4 } = SetX(#{ y => 4 }, 5).
- Listas de coincidencia y construcción de datos binarios
test_cut_comprehensions() -> F = << <<(1 + (X*2))>> || _ <- _, X <- _ >>,
Pros
- El código se ha vuelto más pequeño, por lo tanto, es más fácil de mantener.
- El código se ha vuelto más simple y ordenado.
- Se ha ido el ruido de las diversiones.
- Para los principiantes en Erlang, es más conveniente escribir funciones Get / Set.
Contras
- Mayor umbral de entrada para desarrolladores experimentados de Erlang, al tiempo que reduce el umbral de entrada para principiantes. Ahora se requiere que el equipo comprenda el corte y conozca una sintaxis más.
Hacer notación
La coma suave es una construcción de enlace de cálculo. Erlang no tiene un modelo de cálculo vago. Imaginemos qué pasaría si Erlang fuera flojo como Haskell
my_function() -> A = foo(), B = bar(A, dog), ok.
Para garantizar el orden de ejecución, necesitaríamos vincular explícitamente los cálculos definiendo una coma.
my_function() -> A = foo(), comma(), B = bar(A, dog), comma(), ok.
Continuar la conversión:
my_function() -> comma(foo(), fun (A) -> comma(bar(A, dog), fun (B) -> ok end)).
Según la conclusión, la coma / 2 es una función idiomática >>=/2
. La mónada requiere solo tres funciones: >>=/2
, return/1
y fail/1
.
Todo estaría bien, pero la sintaxis es simplemente horrible. Aplicamos transformadores de sintaxis de erlando
.
do([Monad || A <- foo(), B <- bar(A, dog), ok]).
Tipos de mónadas
Como el do-block está parametrizado, podemos usar mónadas de varios tipos. Dentro del bloque do, las llamadas return/1
y fail/1
implementan en Monad:return/1
y Monad:fail/1
respectivamente.
Identidad-mónada.
La mónada idéntica es la mónada más simple que no cambia el tipo de valores y no participa en el control del proceso de cálculo. Se aplica con transformadores. Realiza expresiones de enlace: la coma del software que se discutió anteriormente.
Quizás mónada.
Mónada de cálculos con procesamiento de valores perdidos. Asociar un parámetro con un cálculo parametrizado es la transferencia de un parámetro a un cálculo, vincular un parámetro ausente a un cálculo parametrizado es un resultado ausente.
Considere un ejemplo de maybe_m:
if_safe_div_zero(X, Y, Fun) -> do([maybe_m || Result <- case Y == 0 of true -> fail("Cannot divide by zero"); false -> return(X / Y) end, return(Fun(Result))]).
La evaluación de la expresión se detiene si no se devuelve nada.
{just, 6} = if_safe_div_zero(10, 5, _+4) ## 10/5 = 2 -> 2+4 -> 6 nothing = if_safe_div_zero(10, 0, _+4)
Error-mónada.
Similar a maybe_m, solo con manejo de errores. A veces, el principio de dejar de funcionar no se aplica y los errores deben manejarse en el momento en que ocurren. En este caso, las escaleras de la caja a menudo aparecen en el código, por ejemplo, estas:
write_file(Path, Data, Modes) -> Modes1 = [binary, write | (Modes -- [binary, write])], case make_binary(Data) of Bin when is_binary(Bin) -> case file:open(Path, Modes1) of {ok, Hdl} -> case file:write(Hdl, Bin) of ok -> case file:sync(Hdl) of ok -> file:close(Hdl); {error, _} = E -> file:close(Hdl), E end; {error, _} = E -> file:close(Hdl), E end; {error, _} = E -> E end; {error, _} = E -> E end.
make_binary(Bin) when is_binary(Bin) -> Bin; make_binary(List) -> try iolist_to_binary(List) catch error:Reason -> {error, Reason} end.
Leer esto es desagradable, parece fideos de devolución de llamada en JS. Error_m viene al rescate:
write_file(Path, Data, Modes) -> Modes1 = [binary, write | (Modes -- [binary, write])], do([error_m || Bin <- make_binary(Data), Hdl <- file:open(Path, Modes1), Result <- return(do([error_m || file:write(Hdl, Bin), file:sync(Hdl)])), file:close(Hdl), Result]). make_binary(Bin) when is_binary(Bin) -> error_m:return(Bin); make_binary(List) -> try error_m:return(iolist_to_binary(List)) catch error:Reason -> error_m:fail(Reason) end.
- Lista-mónada.
Los valores son listas que pueden interpretarse como varios resultados posibles de un solo cálculo. Si un cálculo depende de otro, el segundo cálculo se realiza para cada resultado del primero, y los resultados (segundo cálculo) se recopilan en una lista.
Considere el ejemplo con los clásicos triples pitagóricos. Los calculamos sin mónadas:
P = [{X, Y, Z} || Z <- lists:seq(1,20), X <- lists:seq(1,Z), Y <- lists:seq(X,Z), math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)].
Lo mismo con list_m solamente:
P = do([list_m || Z <- lists:seq(1,20), X <- lists:seq(1,Z), Y <- lists:seq(X,Z), monad_plus:guard(list_m, math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)), return({X,Y,Z})]).
- Estado-mónada.
Mónada de la computación con estado.
Al comienzo del artículo, hablamos sobre las dificultades de los principiantes cuando trabajan con un estado variable. A menudo, el código se ve así:
State1 = init(Dimensions), State2 = plant_seeds(SeedCount, State1), {DidFlood, State3} = pour_on_water(WaterVolume, State2), State4 = apply_sunlight(Time, State3), {DidFlood2, State5} = pour_on_water(WaterVolume, State4), {Crop, State6} = harvest(State5), ...
Usando un transformador y notación de corte, este código puede reescribirse en una forma más compacta y legible:
StateT = state_t:new(identity_m), SM = StateT:modify(_), SMR = StateT:modify_and_return(_), StateT:exec( do([StateT || StateT:put(init(Dimensions)), SM(plant_seeds(SeedCount, _)), DidFlood <- SMR(pour_on_water(WaterVolume, _)), SM(apply_sunlight(Time, _)), DidFlood2 <- SMR(pour_on_water(WaterVolume, _)), Crop <- SMR(harvest(_)), ... ]), undefined).
- Omega-mónada.
Similar a list_m mónada. Sin embargo, el pasaje se realiza en diagonal.
Manejo oculto de errores
Probablemente una de mis características favoritas de la mónada error_m
. No importa dónde ocurra el error, la mónada siempre devolverá {ok, Result}
o {error, Reason}
. Un ejemplo que ilustra el comportamiento:
do([error_m || Hdl <- file:open(Path, Modes), Data <- file:read(Hdl, BytesToRead), file:write(Hdl, DataToWrite), file:sync(Hdl), file:close(Hdl), file:rename(Path, Path2), file:delete(Path), return(Data)]).
Importar como
Para una merienda tenemos la sintaxis import_as sugar. La sintaxis estándar para el atributo -import / 2 le permite importar funciones de otros al módulo local. Sin embargo, esta sintaxis no le permite asignar un nombre alternativo a la función importada. Importar como resuelve este problema:
-import_as({my_mod, [{size/1, m_size}]}) -import_as({my_other_mod, [{size/1, o_size}]})
Estas expresiones se expanden en funciones locales reales, respectivamente:
m_size(A) -> my_mod:size(A). o_size(A) -> my_other_mod:size(A).
Conclusión
Por supuesto, las mónadas le permiten controlar el proceso de cálculo mediante métodos más expresivos, ahorrar código y tiempo para respaldarlo. Por otro lado, agregan complejidad adicional a los miembros del equipo no capacitados.
* - de hecho, en Erlang existen mónadas sin erlando. Una expresión que separa comas es una construcción de cálculos de linealización y enlace.
PD: Recientemente, la biblioteca erlando fue marcada por los autores como archivo. Escribí este artículo hace más de un año. Entonces, sin embargo, como ahora, en Habré no había información sobre mónadas en Erlang. Para remediar esta situación, estoy publicando, aunque con retraso, este artículo.
Para usar erlando en erlang> = 22, debe solucionar el problema con erlang en desuso: get_stacktrace / 0. Un ejemplo de una solución se puede encontrar en mi tenedor: https://github.com/Vonmo/erlando/commit/52e23ecedd2b8c13707a11c7f0f14496b5a191c2
Gracias por tu tiempo!