61 votes

Que signifie la composabilité dans le contexte de la programmation fonctionnelle ?

Que veulent dire les programmeurs fonctionnels lorsqu'ils disent qu'une certaine chose est composable ou non composable ?

Voici quelques-unes des déclarations de ce genre que j'ai lues :

  • Les structures de contrôle ne sont pas composables.
  • Les fils ne se composent pas.
  • Les opérations monadiques sont composables.

54voto

j_random_hacker Points 28473

Marcelo Cantos a donné une assez bonne explication mais je pense qu'il est possible de le rendre légèrement plus précis.

Un type de chose est composable lorsque plusieurs instances peuvent être combinées d'une certaine manière pour produire le même type de chose.

Composabilité de la structure de contrôle. Les langages comme le C font une distinction entre expressions qui peuvent être composées à l'aide d'opérateurs pour produire de nouvelles expressions, et déclarations qui peuvent être composées à l'aide de structures de contrôle telles que if , for et la "structure de contrôle de séquence" qui exécute simplement les déclarations dans l'ordre. Le problème de cet arrangement est que ces deux catégories ne sont pas sur un pied d'égalité -- de nombreuses structures de contrôle utilisent des expressions (par exemple, l'expression évaluée par if pour choisir la branche à exécuter), mais les expressions ne peuvent pas utiliser de structures de contrôle (par exemple, vous ne pouvez pas renvoyer un fichier for boucle). Bien qu'il puisse sembler fou ou dénué de sens de vouloir "retourner une for En fait, l'idée générale de traiter les structures de contrôle comme des objets de première classe qui peuvent être stockés et transmis n'est pas seulement possible mais utile. Dans les langages fonctionnels paresseux comme Haskell, les structures de contrôle telles que if y for peuvent être représentées comme des fonctions ordinaires, qui peuvent être manipulées dans des expressions comme n'importe quel autre terme, permettant des choses telles que des fonctions qui "construisent" d'autres fonctions en fonction des paramètres qui leur sont passés, et les retournent à l'appelant. Ainsi, alors que le langage C (par exemple) divise "les choses qu'un programmeur pourrait vouloir faire" en deux catégories et limite la manière dont les objets de ces catégories peuvent être combinés, Haskell (par exemple) n'a qu'une seule catégorie et n'impose pas de telles limites, donc dans ce sens, il offre plus de composabilité.

Composabilité du fil. Je vais supposer, comme Marcelo Cantos, que vous parlez réellement de la composabilité des threads qui utilisent des locks/mutex. Il s'agit d'un cas un peu plus délicat, car à première vue nous peut avoir des threads qui utilisent des verrous multiples ; mais le point important est que nous ne pouvons pas avoir des threads qui utilisent des verrous multiples avec les garanties qu'ils sont censés avoir .

Nous pouvons définir un verrou comme un type de chose sur lequel certaines opérations peuvent être effectuées, avec certaines garanties. Une garantie est la suivante : supposons qu'il y ait un objet de verrouillage x à condition que chaque processus qui appelle lock(x) appelle éventuellement unlock(x) tout appel à lock(x) reviendra finalement avec succès avec x verrouillé par le thread/processus actuel. Cette garantie simplifie grandement le raisonnement sur le comportement du programme.

Malheureusement, s'il y a plus d'une serrure dans le monde, ce n'est plus vrai. Si le thread A appelle lock(x); lock(y); et le fil B appelle lock(y); lock(x); alors il est possible que A saisisse la serrure x et B saisit la serrure y et ils attendront tous deux indéfiniment que l'autre thread libère l'autre verrou : deadlock. Ainsi, les verrous ne sont pas composables, car lorsque vous en utilisez plus d'un, vous ne pouvez pas simplement prétendre que cette importante garantie est toujours valable non sans avoir analysé le code en détail pour voir comment il gère les verrous . En d'autres termes, vous ne pouvez plus vous permettre de traiter les fonctions comme des "boîtes noires".

Les choses qui sont composables sont bonnes car elles permettent abstractions Ils nous permettent de raisonner sur le code sans avoir à nous soucier de tous les détails, ce qui réduit la charge cognitive du programmeur.

32voto

j-g-faustus Points 4315

Un exemple simple de composabilité est la ligne de commande Linux, où le caractère pipe permet de combiner des commandes simples (ls, grep, cat, more, etc.) d'un nombre pratiquement illimité de façons, ce qui permet de "composer" un grand nombre de comportements complexes à partir d'un petit nombre de primitives plus simples.

La composabilité présente plusieurs avantages :

  • Un comportement plus uniforme : A titre d'exemple, en ayant une seule commande qui qui implémente "afficher les résultats une page à la fois" ( more ), vous obtenez un degré d'uniformité de uniformité de la pagination qui ne serait pas possible si chaque commande devait implémentait ses propres mécanismes (et propres mécanismes (et drapeaux de ligne de commande) pour effectuer la pagination.
  • Moins de travail d'implémentation répété (DRY) : Au lieu d'avoir une multitude de implémentations différentes de la pagination, il n'y en a qu'une qui est utilisée partout.
  • Plus de fonctionnalités pour une quantité donnée d'effort de mise en œuvre : Les primitives existantes primitives existantes peuvent être combinées pour résoudre un éventail de tâches beaucoup plus large que ce qui serait le cas si le même effort était consacré à l'implémentation de des commandes monolithiques et non-composables.

Comme le montre l'exemple de la ligne de commande Linux, la composabilité n'est pas nécessairement limitée à la programmation fonctionnelle, mais le concept est le même : disposer de petits morceaux de code qui effectuent des tâches restreintes et construire une fonctionnalité plus complexe en acheminant les sorties et les entrées de manière appropriée.

Le fait est que la programmation fonctionnelle est bien adaptée à cela : Avec des variables immuables et des restrictions sur les effets secondaires, vous pouvez composer plus facilement car vous n'avez pas à vous soucier de ce qui se passe dans la fonction appelée - comme la mise à jour d'une variable partagée qui rendra le résultat invalide pour certaines séquences d'opérations, ou l'accès à un verrou partagé qui entraînera un blocage dans certaines séquences d'appels.

C'est la composabilité de la programmation fonctionnelle : toute fonction ne dépend que de ses paramètres d'entrée, et la sortie peut être transmise à toute fonction capable de gérer le type de la valeur de retour.

Par extension, le fait d'avoir moins de types de données permet une meilleure composabilité. Rich Hickey de Clojure a dit quelque chose du genre

chaque nouveau type d'objet est intrinsèquement incompatible avec tout le code jamais écrit

ce qui est certainement une remarque bien faite.

La composabilité pratique dépend également de la normalisation d'un petit ensemble de types de données, comme le font les commandes Unix avec leur norme de "texte à base de lignes délimitées par des tabulations".

Post-scriptum

Eric Raymond a écrit un livre sur la philosophie Unix, dont deux des principes de conception qu'il a énumérés sont les suivants

  • Règle de la modularité : Écrivez des parties simples reliées par des interfaces propres.
  • Règle de composition : Concevez des programmes destinés à être reliés à d'autres programmes.

De http://catb.org/~esr/écritures/taoup/html/ch01s06.html#id2877537

On peut dire que la composabilité dans la programmation fonctionnelle ramène ces principes au niveau des fonctions individuelles.

22voto

Marcelo Cantos Points 91211

En informatique, la composition est la capacité d'assembler un comportement complexe en agrégeant des comportements plus simples. La décomposition fonctionnelle en est un exemple : une fonction complexe est décomposée en fonctions plus petites et faciles à comprendre, puis assemblée en un système final par une fonction de haut niveau. On peut dire que la fonction de haut niveau a "composé" les pièces pour en faire un tout.

Certains concepts ne se composent pas facilement. Par exemple, une structure de données thread-safe peut permettre l'insertion et le retrait d'éléments en toute sécurité, et ce en verrouillant la structure de données ou un sous-ensemble de celle-ci, de sorte qu'un thread puisse effectuer les manipulations nécessaires sans que ses modifications soient perturbées - et la structure de données corrompue - pendant qu'il travaille. Cependant, une fonction commerciale peut exiger le retrait d'un élément d'une collection, suivi de son insertion dans une autre, et que l'ensemble de l'opération soit effectué de manière atomique. Le problème est que le verrouillage ne se produit que par structure de données. Vous pouvez retirer un élément de l'une d'elles en toute sécurité, mais vous risquez ensuite de ne pas pouvoir l'insérer dans l'autre en raison d'une violation de clé. Ou bien vous pouvez essayer de l'insérer dans la seconde et de le retirer de la première, pour découvrir qu'un autre thread l'a volé sous votre nez. Lorsque vous vous rendez compte que vous ne pouvez pas terminer l'opération, vous pouvez essayer de remettre les choses en place, mais vous vous rendez compte que l'inversion échoue pour des raisons similaires, et vous êtes maintenant dans les limbes ! Vous pouvez, bien sûr, mettre en œuvre un schéma de verrouillage plus riche qui couvre plusieurs structures de données, mais cela ne fonctionne que si tout le monde accepte le nouveau schéma de verrouillage, et supporte le fardeau de l'utiliser en permanence, même lorsque toutes leurs opérations portent sur une seule structure de données.

Le verrouillage de type Mutex est donc un concept qui ne se compose pas. Vous ne pouvez pas implémenter un comportement thread-safe de niveau supérieur simplement en agrégeant des opérations thread-safe de niveau inférieur. La solution dans ce cas est d'utiliser un concept qui compose, comme par exemple STM .

5voto

Aidan Cully Points 3611

Je suis d'accord avec la réponse de Marcelo Cantos, mais je pense qu'elle peut supposer plus de connaissances que certains lecteurs ont, ce qui est également lié à la raison pour laquelle la composition en programmation fonctionnelle est spéciale. La composition de fonctions en programmation fonctionnelle est essentiellement identique à la composition de fonctions en mathématiques. En mathématiques, vous pouvez avoir une fonction f(x) = x^2 et une fonction g(x) = x + 1 . Composer les fonctions signifie créer une nouvelle fonction, dans laquelle les arguments de la fonction sont donnés à la fonction "interne", et la sortie de la fonction "interne" sert d'entrée à la fonction "externe". Composer f externe avec g interne pourrait s'écrire f(g(x)) . Si vous fournissez une valeur de 1 para x alors g(1) == 1 + 1 == 2 donc f(g(1)) == f(2) == 2^2 == 4 . Plus généralement, f(g(x)) == f(x + 1) == (x+1)^2 . J'ai décrit la composition en utilisant le f(g(x)) mais les mathématiciens préfèrent souvent une syntaxe différente, (f . g)(x) . Je pense que c'est parce que cela rend plus clair le fait que f composed with g est une fonction à part entière, qui prend un seul argument.

Les programmes fonctionnels sont entièrement construits à l'aide de primitives de composition. Un programme en Haskell est, pour simplifier à l'extrême, une fonction qui prend en argument un environnement d'exécution et renvoie le résultat d'une manipulation de cet environnement. (Pour comprendre cette déclaration, il faut avoir une certaine connaissance des monades). composition au sens mathématique du terme .

2voto

Brian Points 82719

Autre exemple : considérez la programmation asynchrone dans .NET. Si vous utilisez un langage comme C# et que vous avez besoin d'effectuer une série d'appels d'E/S asynchrones (non bloquants) via les API Begin/End, pour appeler deux opérations, Foo y Bar dans l'ordre, vous devez exposer deux méthodes ( BeginFooAndBar , EndFooAndBar ), où BeginFooAndBar appelle BeginFoo et passe un callback à Intermediate y Intermediate puis appelle BeginBar et vous devez enfiler les valeurs intermédiaires et IAsyncResult l'information de l'état par le biais, et si vous voulez un try - catch Vous devez dupliquer le code de gestion des exceptions à trois endroits différents, et c'est un véritable gâchis.

Mais alors avec F#, il y a la async construit sur des continuations fonctionnelles qui sont composables, ce qui permet d'écrire par exemple

let AsyncFooAndBar() = async {
    let! x = Async.FromBeginEnd(BeginFoo, EndFoo)
    let! y = Async.FromBeginEnd(BeginBar, EndBar, x)
    return y * 2 }

ou ce que vous voulez, et c'est simple, et si vous voulez mettre une try - catch autour, super, le code est dans une seule méthode, au lieu d'être réparti sur trois, vous mettez juste une try - catch autour d'elle et ça marche.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X