Mônadas em Erlang


Em Habré, você pode encontrar muitas publicações que revelam a teoria das mônadas e a prática de sua aplicação. A maioria desses artigos é esperada sobre o Haskell. Não vou recontar a teoria pela enésima vez. Hoje falaremos sobre alguns problemas de Erlang, maneiras de resolvê-los com mônadas, uso parcial de funções e açúcar sintático de erlando - uma biblioteca interessante da equipe RabbitMQ.


1. Introdução


Erlang tem imutabilidade, mas nenhuma mônada * . Mas, graças à funcionalidade parse_transform e à implementação do erlando na linguagem, ainda há a possibilidade de usar mônadas em Erlang.


Sobre imunidade no começo da história, não falei por acaso. A imunidade está quase em todo lugar e sempre - uma das principais idéias de Erlang. A imunidade e a pureza das funções permitem que você se concentre no desenvolvimento de uma função específica e não tenha medo de efeitos colaterais. Mas os novatos em Erlang, vindos de Java ou Python, por exemplo, acham muito difícil entender e aceitar as idéias de Erlang. Especialmente se você se lembrar da sintaxe Erlang. Aqueles que tentaram começar a usar Erlang provavelmente notaram sua incomum e independência. De qualquer forma, acumulei muitos comentários de iniciantes e a sintaxe "estranha" lidera a classificação.


Erlando


Erlando é um conjunto de extensões Erlang que nos fornece:


  • Uso / curry parcial de funções com cortes semelhantes a esquemas
  • Notações do tipo Haskell
  • açúcar import-as - sintático para importar funções de outros módulos.

Nota: tomei os seguintes exemplos de código para ilustrar os recursos do erlando da apresentação de Matthew Sackman, diluindo-os parcialmente com o meu código e explicações.


Corte abstrato


Vá direto ao ponto. Considere várias funções de um projeto 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 essas funções são projetadas para substituir parâmetros em expressões simples. De fato, esta é uma aplicação parcial, pois alguns parâmetros não serão conhecidos antes da chamada. Juntamente com a flexibilidade, esses recursos adicionam ruído ao nosso código. Alterando um pouco a sintaxe - inserindo cut - você pode melhorar a situação.


Valor _


  • _ pode ser usado em modelos
  • Cortar permite usar _ padrões externos
  • Se estiver fora do modelo, ele se tornará um parâmetro para a expressão em que está localizado
  • O uso múltiplo de _ na mesma expressão leva à substituição de vários parâmetros nessa expressão
  • Cut não é um substituto para fechamentos (diversão)
  • Os argumentos são avaliados antes da função de corte.

Cut usa expressões _ em para indicar onde a abstração deve ser aplicada. O corte envolve apenas o nível mais próximo da expressão, mas o corte aninhado não é proibido.
Por exemplo, list_to_binary([1, 2, math:pow(2, _)]). list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]). para list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]). mas não fun (X) -> list_to_binary([1, 2, math:pow(2, X)]) end. .


Parece um pouco confuso, vamos reescrever os exemplos acima 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) }. 

Ordem de cálculo do argumento


Para ilustrar a ordem em que os argumentos são calculados, considere o seguinte exemplo:


 f1(_, _) -> io:format("in f1~n"). test() -> F = f1(io:format("test line 1~n"), _), F(io:format("test line 2~n")). 

Como os argumentos são avaliados antes da função de corte, o seguinte será exibido:


 test line 2 test line 1 in f1 

Cortar abstração em vários tipos e padrões 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). 
  • Estojos
     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 correspondência e construção de dados binários
     test_cut_comprehensions() -> F = << <<(1 + (X*2))>> || _ <- _, X <- _ >>, %% Note, this'll only be a /2 ! <<"AAA">> = F([a,b,c], [32]), F1 = [ {X, Y, Z} || X <- _, Y <- _, Z <- _, math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2) ], [{3,4,5}, {4,3,5}, {6,8,10}, {8,6,10}] = lists:usort(F1(lists:seq(1,10), lists:seq(1,10), lists:seq(1,10))). 

Prós


  • O código ficou menor, portanto, é mais fácil de manter.
  • O código se tornou mais simples e organizado.
  • Foi o barulho de diversão.
  • Para iniciantes em Erlang, é mais conveniente escrever funções Get / Set.

Contras


  • Aumento do limite de entrada para desenvolvedores experientes do Erlang, enquanto reduz o limite de entrada para iniciantes. Agora, é necessário que a equipe compreenda o corte e conheça mais uma sintaxe.

Faça notação


Vírgula flexível é uma construção de ligação de computação. Erlang não possui um modelo de cálculo lento. Vamos imaginar o que aconteceria se Erlang fosse preguiçoso como Haskell


 my_function() -> A = foo(), B = bar(A, dog), ok. 

Para garantir a ordem de execução, precisaríamos vincular explicitamente os cálculos definindo uma vírgula.


 my_function() -> A = foo(), comma(), B = bar(A, dog), comma(), ok. 

Continue a conversão:


 my_function() -> comma(foo(), fun (A) -> comma(bar(A, dog), fun (B) -> ok end)). 

Com base na conclusão, vírgula / 2 é uma função idiomática >>=/2 . A mônada requer apenas três funções: >>=/2 , return/1 e fail/1 .
Tudo ficaria bem, mas a sintaxe é simplesmente horrível. Aplicamos transformadores de sintaxe de erlando .


 do([Monad || A <- foo(), B <- bar(A, dog), ok]). 

Tipos de mônadas


Como o bloco do é parametrizado, podemos usar mônadas de vários tipos. Dentro do bloco bloqueado, as chamadas return/1 e fail/1 implantadas no Monad:return/1 e Monad:fail/1 respectivamente.


  • Mônada de identidade.
    A mônada idêntica é a mônada mais simples que não altera o tipo de valores e não participa do controle do processo de cálculo. É aplicado com transformadores. Executa expressões de vinculação - a vírgula do software discutida acima.


  • Talvez-mônada.
    Mônada de cálculos com processamento de valores ausentes. Associar um parâmetro a um cálculo parametrizado é a transferência de um parâmetro para um cálculo, vincular um parâmetro ausente a um cálculo parametrizado é um resultado ausente.
    Considere um exemplo 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))]). 

    A avaliação da expressão será interrompida se nada for retornado.


     {just, 6} = if_safe_div_zero(10, 5, _+4) ## 10/5 = 2 -> 2+4 -> 6 nothing = if_safe_div_zero(10, 0, _+4) 

  • Mônada de erro.
    Semelhante ao maybe_m, apenas com tratamento de erros. Às vezes, o princípio deixar travar não se aplica e os erros devem ser tratados no momento em que ocorrem. Nesse caso, as escadas do case geralmente aparecem no código, por exemplo, 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. 


Ler isso é desagradável, parece macarrão de retorno de chamada em JS. Error_m vem ao salvamento:


 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. 

  • Mônada de lista.
    Valores são listas que podem ser interpretadas como vários resultados possíveis de um único cálculo. Se um cálculo depende de outro, o segundo cálculo é realizado para cada resultado do primeiro e os resultados (segundo cálculo) são coletados em uma lista.
    Considere o exemplo com os triplos pitagóricos clássicos. Nós os calculamos sem 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)]. 

A mesma coisa com list_m apenas:


 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})]). 

  • Mônada do estado.
    Mônada da computação com estado.
    No início do artigo, falamos sobre as dificuldades dos iniciantes ao trabalhar com um estado variável. Frequentemente, o código se parece com isso:
     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 um transformador e uma notação de corte, esse código pode ser reescrito de uma forma mais compacta e legível:


 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). 

  • Ômega-mônada.
    Semelhante ao list_m mônada. No entanto, a passagem é feita na diagonal.

Tratamento de erros ocultos


Provavelmente, um dos meus recursos favoritos da mônada error_m . Não importa onde o erro ocorra, a mônada sempre retornará {ok, Result} ou {error, Reason} . Um exemplo que ilustra o comportamento:


 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)]). 

Import_as


Para um lanche, temos a sintaxe import_as sugar. A sintaxe padrão para o atributo -import / 2 permite importar funções de outras pessoas para o módulo local. No entanto, essa sintaxe não permite atribuir um nome alternativo à função importada. Import_as resolve este problema:


 -import_as({my_mod, [{size/1, m_size}]}) -import_as({my_other_mod, [{size/1, o_size}]}) 

Essas expressões são expandidas em funções locais reais, respectivamente:


 m_size(A) -> my_mod:size(A). o_size(A) -> my_other_mod:size(A). 

Conclusão


Obviamente, as mônadas permitem controlar o processo de cálculo por métodos mais expressivos, economizando código e tempo para suportá-lo. Por outro lado, eles adicionam complexidade extra aos membros da equipe não treinados.


* - de fato, em Erlang as mônadas existem sem erlando. Uma expressão de separação por vírgula é uma construção de cálculos de linearização e ligação.


PS: Recentemente, a biblioteca erlando foi marcada pelos autores como arquivística. Eu escrevi este artigo mais de um ano atrás. Porém, como agora, em Habré, não havia informações sobre mônadas em Erlang. Para remediar essa situação, estou publicando, embora tardiamente, este artigo.
Para usar erlando em erlang> = 22, é necessário corrigir o problema com o erlang descontinuado: get_stacktrace / 0. Um exemplo de correção pode ser encontrado no meu fork: https://github.com/Vonmo/erlando/commit/52e23ecedd2b8c13707a11c7f0f14496b5a191c2


Obrigado pelo seu tempo!

Source: https://habr.com/ru/post/pt466697/


All Articles