Monades à Erlang


Sur Habré, vous trouverez de nombreuses publications qui révèlent à la fois la théorie des monades et la pratique de leur application. La plupart de ces articles sont attendus sur Haskell. Je ne reverrai pas la théorie pour la nième fois. Aujourd'hui, nous allons parler de certains problèmes d'Erlang, des moyens de les résoudre avec des monades, de l'utilisation partielle des fonctions et du sucre syntaxique d' erlando - une bibliothèque intéressante de l'équipe RabbitMQ.


Présentation


Erlang a une immuabilité, mais pas de monades * . Mais grâce à la fonctionnalité parse_transform et à l'implémentation d'erlando dans le langage, il est toujours possible d'utiliser des monades dans Erlang.


Au sujet de l'immunité au tout début de l'histoire, je n'ai pas parlé par hasard. L'immunité est presque partout et toujours - l'une des principales idées d'Erlang. L'immunité et la pureté des fonctions vous permettent de vous concentrer sur le développement d'une fonction spécifique et de ne pas avoir peur des effets secondaires. Mais les nouveaux arrivants à Erlang, venant de Java ou de Python, par exemple, ont du mal à comprendre et à accepter les idées d'Erlang. Surtout si vous vous souvenez de la syntaxe Erlang. Ceux qui ont essayé de commencer à utiliser Erlang ont probablement noté son caractère inhabituel et son indépendance. Dans tous les cas, j'ai accumulé beaucoup de commentaires de débutants et la syntaxe «étrange» mène la note.


Erlando


Erlando est un ensemble d'extension Erlang qui nous donne:


  • Utilisation partielle / curry des fonctions avec des coupes de type Scheme
  • Notations de type Haskell
  • import-as - sucre syntaxique pour importer des fonctions à partir d'autres modules.

Remarque: J'ai pris les exemples de code suivants pour illustrer les fonctionnalités d'erlando de la présentation de Matthew Sackman, en les diluant partiellement avec mon code et mes explications.


Coupe abstraite


Allez droit au but. Considérons plusieurs fonctions d'un vrai projet:


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. 

Toutes ces fonctions sont conçues pour remplacer les paramètres en expressions simples. En fait, il s'agit d'une application partielle, car certains paramètres ne seront pas connus avant l'appel. Associées à la flexibilité, ces fonctionnalités ajoutent du bruit à notre code. En modifiant un peu la syntaxe - en entrant cut - vous pouvez améliorer la situation.


Valeur _


  • _ peut être utilisé dans les modèles
  • La coupe vous permet d'utiliser _ des motifs extérieurs
  • S'il est en dehors du modèle, il devient un paramètre pour l'expression dans laquelle il se trouve
  • L'utilisation multiple de _ dans la même expression conduit à la substitution de plusieurs paramètres dans cette expression
  • La coupe ne remplace pas les fermetures (amusements)
  • Les arguments sont évalués avant la fonction cut.

Cut utilise les expressions _ in pour indiquer où l'abstraction doit être appliquée. Couper enveloppe uniquement le niveau le plus proche dans l'expression, mais la coupe imbriquée n'est pas interdite.
Par exemple, list_to_binary([1, 2, math:pow(2, _)]). list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]). en list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]). mais pas dans le fun (X) -> list_to_binary([1, 2, math:pow(2, X)]) end. .


Cela semble un peu déroutant, réécrivons les exemples ci-dessus en utilisant 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) }. 

Ordonnance de calcul des arguments


Pour illustrer l'ordre dans lequel les arguments sont calculés, considérons l'exemple suivant:


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

Étant donné que les arguments sont évalués avant la fonction de coupure, les éléments suivants s'affichent:


 test line 2 test line 1 in f1 

Couper l'abstraction dans différents types et modèles de code


  • Tuples
     F = {_, 3}, {a, 3} = F(a). 
  • Listes
     dbl_cons(List) -> [_, _ | List]. test() -> F = dbl_cons([33]), [7, 8, 33] = F(7, 8). 
  • Records
     -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). 
  • Étuis
     F = case _ of N when is_integer(N) -> N + N; N -> N end, 10 = F(5), ok = F(ok). 
  • Les cartes
     test() -> GetZ = maps:get(z, _), 7 = GetZ(#{ z => 7 }), SetX = _#{x => _}, V = #{ x := 5, y := 4 } = SetX(#{ y => 4 }, 5). 
  • Listes de correspondance et construction de données binaires
     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))). 

Avantages


  • Le code est devenu plus petit, il est donc plus facile à maintenir.
  • Le code est devenu plus facile et plus ordonné.
  • Disparu du plaisir.
  • Pour les débutants à Erlang, il est plus pratique d'écrire des fonctions Get / Set.

Inconvénients


  • Augmentation du seuil d'entrée pour les développeurs Erlang expérimentés, tout en réduisant le seuil d'entrée pour les débutants. Maintenant, l'équipe doit comprendre la coupe et connaître une syntaxe de plus.

Faire de la notation


La virgule souple est une construction de liaison de calcul. Erlang n'a pas de modèle de calcul paresseux. Imaginons ce qui se passerait si Erlang était paresseux comme Haskell


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

Pour garantir l'ordre d'exécution, nous aurions besoin de lier explicitement les calculs en définissant une virgule.


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

Continuez la conversion:


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

Sur la base de la conclusion, la virgule / 2 est une fonction idiomatique >>=/2 . La monade ne nécessite que trois fonctions: >>=/2 , return/1 et fail/1 .
Tout irait bien, mais la syntaxe est tout simplement horrible. Nous appliquons des transformateurs de syntaxe d' erlando .


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

Types de monades


Le do-block étant paramétré, nous pouvons utiliser des monades de différents types. À l'intérieur du do-block, les appels return/1 et fail/1 déployés respectivement sur Monad:return/1 et Monad:fail/1 .


  • Identité-monade.
    La monade identique est la monade la plus simple qui ne change pas le type de valeurs et ne participe pas au contrôle du processus de calcul. Il est appliqué avec des transformateurs. Effectue des expressions de liaison - la virgule logicielle décrite ci-dessus.


  • Peut-être monade.
    Monade de calculs avec traitement des valeurs manquantes. Associer un paramètre à un calcul paramétré est le transfert d'un paramètre à un calcul, lier un paramètre absent à un calcul paramétré est un résultat absent.
    Prenons un exemple de peut-être_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))]). 

    L'évaluation de l'expression s'arrête si rien n'est retourné.


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

  • Erreur-monade.
    Similaire à peut-être_m, uniquement avec la gestion des erreurs Parfois, le principe let it crash ne s'applique pas et les erreurs doivent être gérées au moment où elles se produisent. Dans ce cas, les escaliers du boîtier apparaissent souvent dans le code, par exemple, ceux-ci:


     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. 


La lecture est désagréable, ressemble à des nouilles de rappel dans JS. Error_m vient à la rescousse:


 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. 

  • Liste-monade.
    Les valeurs sont des listes qui peuvent être interprétées comme plusieurs résultats possibles d'un même calcul. Si un calcul dépend d'un autre, le deuxième calcul est effectué pour chaque résultat du premier, et les résultats (deuxième calcul) sont collectés dans une liste.
    Prenons l'exemple avec les triplets pythagoriciens classiques. Nous les calculons sans monades:
     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)]. 

Même chose avec list_m uniquement:


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

  • État-monade.
    Monade de l'informatique dynamique.
    Au tout début de l'article, nous avons évoqué les difficultés des débutants à travailler avec un état variable. Souvent, le code ressemble à ceci:
     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), ... 

À l'aide d'un transformateur et d'une coupe, ce code peut être réécrit sous une forme plus compacte et lisible:


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

  • Oméga-monade.
    Similaire à list_m monad. Cependant, le passage se fait en diagonale.

Gestion des erreurs cachées


Probablement l'une de mes fonctionnalités préférées de la monade error_m . Peu importe où l'erreur se produit, la monade renvoie toujours {ok, Result} ou {error, Reason} . Un exemple illustrant le comportement:


 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


Pour une collation, nous avons la syntaxe import_as sugar. La syntaxe standard de l'attribut -import / 2 vous permet d'importer des fonctions d'autres dans le module local. Cependant, cette syntaxe ne vous permet pas d'attribuer un autre nom à la fonction importée. Import_as résout ce problème:


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

Ces expressions sont développées en fonctions locales réelles, respectivement:


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

Conclusion


Bien sûr, les monades vous permettent de contrôler le processus de calcul par des méthodes plus expressives, d'économiser du code et du temps pour le prendre en charge. D'un autre côté, ils ajoutent une complexité supplémentaire aux membres de l'équipe non formés.


* - en fait, dans Erlang les monades existent sans erlando. Une virgule séparant les expressions est une construction de linéarisation et de liaison de calculs.


PS Récemment, la bibliothèque erlando a été marquée par les auteurs comme archivistique. J'ai écrit cet article il y a plus d'un an. Mais, comme aujourd'hui, sur Habré, il n'y avait aucune information sur les monades à Erlang. Pour remédier à cette situation, je publie, quoique tardivement, cet article.
Pour utiliser erlando dans erlang> = 22, vous devez résoudre le problème avec erlang obsolète: get_stacktrace / 0. Un exemple de correctif peut être trouvé dans ma fourchette: https://github.com/Vonmo/erlando/commit/52e23ecedd2b8c13707a11c7f0f14496b5a191c2


Merci pour votre temps!

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


All Articles