72 votes

Quels sont les avantages et les inconvénients du style "sans point" dans la programmation fonctionnelle ?

Je sais que dans certains langages (Haskell ?), l'objectif est d'obtenir un style sans point, ou de ne jamais se référer explicitement aux arguments des fonctions par leur nom. C'est un concept très difficile à maîtriser pour moi, mais cela pourrait m'aider à comprendre quels sont les avantages (ou peut-être même les inconvénients) de ce style. Quelqu'un peut-il m'expliquer ?

78voto

gasche Points 23208

Le style sans point est considéré par certains auteurs comme la ultime style de programmation fonctionnel. Pour simplifier, une fonction de type t1 -> t2 décrit une transformation d'un élément de type t1 dans un autre élément de type t2 . L'idée est que les fonctions "inutiles" (écrites à l'aide de variables) mettent l'accent sur les aspects suivants éléments (lorsque vous écrivez \x -> ... x ... vous décrivez ce qui arrive à l'élément x ), tandis que les fonctions "sans point" (exprimées sans utiliser de variables) mettent l'accent sur l'élément transformation elle-même, comme une composition de transformations plus simples. Les défenseurs du style sans point soutiennent que les transformations devraient en effet être le concept central, et que la notation ponctuelle, bien que facile à utiliser, nous détourne de ce noble idéal.

La programmation fonctionnelle sans point est disponible depuis très longtemps. Elle était déjà connue des logiciens qui ont étudié logique combinatoire depuis le travail fondateur de Moses Schönfinkel en 1924, et a servi de base à la première étude sur ce qui allait devenir l'inférence de type ML par Robert Feys et Haskell Curry dans les années 1950.

L'idée de construire des fonctions à partir d'un ensemble expressif de combinateurs de base est très séduisante et a été appliquée dans divers domaines, tels que les langages de manipulation de tableaux dérivés de la méthode APL ou les bibliothèques de combinateurs d'analyseurs telles que la bibliothèque de Haskell Parsec . Un défenseur notable de la programmation sans point est John Backus . Dans son discours de 1978 intitulé "Can Programming Be Liberated From the Von Neumann Style ?" (La programmation peut-elle être libérée du style Von Neumann ?), il a écrit :

L'expression lambda (avec ses règles de substitution) est capable de de définir toutes les fonctions calculables possibles de tous les types possibles et de n'importe quel nombre d'arguments. Cette liberté et cette puissance ont leurs inconvénients que ses avantages évidents. Elle est analogue au pouvoir des instructions de contrôle non limitées dans les langages conventionnels conventionnels : la liberté illimitée est synonyme de chaos. Si un Si l'on invente constamment de nouvelles formes de combinaisons pour comme on peut le faire dans le calcul lambda, on ne se familiarisera pas avec le style ou les le style ou les propriétés utiles des quelques formes de combinaison qui qui conviennent à tous les usages. Tout comme la programmation structurée de contrôle afin d'obtenir des programmes à la structure plus simple, une meilleure structure plus simple, de meilleures propriétés et des méthodes comprendre leur comportement, la programmation fonctionnelle renonce à l'expression lambda l'expression lambda, la substitution et les fonctions multiples types de fonctions. Elle permet ainsi d'obtenir des programmes construits avec des formes fonctionnelles formes fonctionnelles familières avec des propriétés utiles connues. Ces programmes sont structurés de telle sorte que leur comportement peut souvent être compris et être compris et prouvé par l'utilisation mécanique de techniques algébriques similaires à celles utilisées pour résoudre des problèmes d'algèbre au lycée.

Les voici donc. Le principal avantage de la programmation sans point est qu'elle impose un style de combinateur structuré qui rend le raisonnement équationnel naturel. Le raisonnement équationnel a été particulièrement mis en avant par les partisans du mouvement "Squiggol" (voir [1] [2]), et utilise en effet une bonne part de combinateurs sans points et de règles de calcul/réécriture/raisonnement.

Enfin, l'une des raisons de la popularité de la programmation sans point parmi les Haskellites est sa relation avec le concept de théorie des catégories . Dans la théorie des catégories, les morphismes (que l'on pourrait considérer comme des "transformations entre objets") sont l'objet fondamental de l'étude et du calcul. Bien que des résultats partiels permettent de raisonner dans des catégories spécifiques dans un style ponctuel, la manière courante de construire, d'examiner et de manipuler les flèches reste le style sans point, et d'autres syntaxes telles que les diagrammes de chaînes de caractères présentent également cette "absence de point". Il existe des liens assez étroits entre les défenseurs des méthodes d'"algèbre de programmation" et les utilisateurs de catégories en programmation (par exemple, les auteurs de l'article sur la banane [2] sont/étaient des catégoristes purs et durs).

Vous pouvez être intéressé par la Page Pointfree du wiki Haskell.

L'inconvénient du style sans point est assez évident : il peut être très difficile à lire. La raison pour laquelle nous aimons toujours utiliser les variables, malgré les nombreuses horreurs de l'ombrage, de l'équivalence alpha, etc., est qu'il s'agit d'une notation qui est tout simplement naturelle à lire et à penser. L'idée générale est qu'une fonction complexe (dans un langage référentiellement transparent) est comme un système de plomberie complexe : les entrées sont les paramètres, ils entrent dans des tuyaux, sont appliqués à des fonctions internes, dupliqués ( \x -> (x,x) ) ou oubliée ( \x -> () (tuyau ne menant nulle part), etc. Et la notation des variables est joliment implicite à propos de toute cette machinerie : vous donnez un nom à l'entrée, et des noms aux sorties (ou aux calculs auxiliaires), mais vous n'avez pas besoin de décrire tout le plan de plomberie, où les petits tuyaux iront pour ne pas gêner les plus gros, etc. La quantité de plomberie à l'intérieur de quelque chose d'aussi court que \(f,x,y) -> ((x,y), f x y) est étonnante. Vous pouvez suivre chaque variable individuellement, ou lire chaque nœud de plomberie intermédiaire, mais vous n'avez jamais besoin de voir l'ensemble de la machinerie. Lorsque vous utilisez un style sans point, toute la plomberie est explicite, vous devez tout écrire et le regarder après coup, et parfois c'est tout simplement laid.

PS : cette vision de la plomberie est étroitement liée aux langages de programmation à pile, qui sont probablement les langages de programmation les moins utiles (à peine) en usage. Je recommanderais d'essayer de programmer dans ces langages juste pour en avoir une idée (comme je recommanderais la programmation logique). Voir Facteur , Chat ou le vénérable Quatrième .

0 votes

" Lorsque vous utilisez un style sans point, tout est explicite," Ne voulez-vous pas dire inutile ici ? Alternativement : implicite ?

0 votes

Je pense que la phrase telle quelle est correcte. Dans le style sans point, vous devez être très explicite sur le flux de valeur des entrées vers les sorties de la fonction, alors que le style avec point s'appuie sur les noms pour éviter cela. Par exemple, il n'y a pas de marque que x y y sont dupliqués dans la partie droite, ils apparaissent simplement deux fois. Si vous essayez d'implémenter cette fonction en style libre, vous verrez à quel point vous devez être plus explicite.

0 votes

Je reste un peu perplexe sur l'ensemble de ce paragraphe, puisque vous avez écrit précédemment The idea is that "pointful" functions (written using explicit variables) ..

59voto

AshleyF Points 1634

Je pense que l'objectif est d'être succinct et d'exprimer les calculs en pipeline comme une composition de fonctions plutôt que de penser à filetage arguments à travers. Exemple simple (en F#) - donné :

let sum = List.sum
let sqr = List.map (fun x -> x * x)

Utilisé comme :

> sum [3;4;5]
12
> sqr [3;4;5]
[9;16;25]

Nous pourrions exprimer une fonction "somme des carrés" comme suit :

let sumsqr x = sum (sqr x)

Et utiliser comme :

> sumsqr [3;4;5]
50

Ou nous pourrions le définir en faisant passer x par la tuyauterie :

let sumsqr x = x |> sqr |> sum

Écrit de cette manière, il est évident que x est passé dans seulement d'être "enfilée" à travers une séquence de fonctions. La composition directe est beaucoup plus agréable :

let sumsqr = sqr >> sum

C'est plus concis et c'est une façon différente de penser à ce que nous faisons ; composer des fonctions plutôt que d'imaginer le processus de circulation des arguments. Nous ne décrivons pas comment sumsqr œuvre. Nous décrivons ce qu'il es .

PS : Une façon intéressante de se familiariser avec la composition est d'essayer de programmer dans un langage concaténatif tel que Forth, Joy, Factor, etc. On peut considérer que ces langages ne sont rien d'autre que de la composition (Forth : sumsqr sqr sum ; ) dans lequel l'espace entre les mots est le opérateur de composition .

PPS : D'autres personnes pourraient peut-être commenter les différences de performance. Il me semble que la composition peut réduire la pression de la CG en la rendant plus évidentes au compilateur qu'il n'est pas nécessaire de produire des valeurs intermédiaires comme dans le cas du pipelining, ce qui contribue à rendre le problème de la "déforestation" plus facile à résoudre.

12 votes

La partie concernant l'amélioration de la compilation n'est pas du tout vraie. Dans la plupart des langages, le style sans point diminue les performances. Haskell s'appuie fortement sur les optimisations précisément parce que c'est le seul moyen de rendre le coût de ces choses supportable. Au mieux, ces combinateurs sont inlinés et vous obtenez une version équivalente sans point.

2 votes

Ce que je voulais dire par "déforestation" réduisant la pression du GC, c'est que le compilateur pourrait éviter d'allouer des valeurs intermédiaires (par exemple, la liste de sqr ) alors qu'il est clair qu'il est simplement transmis à sum pour construire le résultat ; en prenant la composition de la fonction en tant que indice pour le faire. List.sum est vraiment List.fold (+) 0 o List.fold (fun s x -> s + x) . Composer avec la carte est : List.map (fun x -> x * x) >> List.fold (fun s x -> s + x) ou pourraient être fusionnés en un seul : List.fold (fun s x -> s + x * x) 0 évitant ainsi les allocations. Voir : link.springer.com/content/pdf/10.1007/3-540-19027-9_23.pdf

10voto

Robert M Points 11

Bien que je sois attiré par le concept du point-free et que je l'aie utilisé pour certaines choses, et que je sois d'accord avec tous les points positifs mentionnés précédemment, j'ai trouvé ces points négatifs (certains sont détaillés ci-dessus) :

  1. La notation plus courte réduit la redondance ; dans une composition fortement structurée (style ramda.js, ou sans point en Haskell, ou tout autre langage concaténatif), la lecture du code est plus complexe que le balayage linéaire d'un tas de const et en utilisant un surligneur de symboles pour voir quelle liaison va dans quel autre calcul en aval. Outre la structure arborescente ou linéaire, la perte des noms descriptifs des symboles rend la fonction difficile à saisir intuitivement. Bien sûr, la structure arborescente et la perte des liaisons nommées ont aussi beaucoup d'avantages, par exemple, les fonctions semblent plus générales - elles ne sont pas liées à un domaine d'application par les noms de symboles choisis - et la structure arborescente est sémantiquement présente même si les liaisons sont présentées et peuvent être comprises de manière séquentielle (style lisp let/let*).

  2. L'absence de point est la plus simple lorsqu'il s'agit de passer en revue ou de composer une série de fonctions, car il en résulte une structure linéaire que nous, les êtres humains, trouvons facile à suivre. Cependant, il est fastidieux de faire passer un calcul intermédiaire par plusieurs destinataires. Il y a toutes sortes d'enveloppes dans des tuples, des lentilles et d'autres mécanismes minutieux pour rendre accessible un calcul qui, autrement, ne serait que l'utilisation multiple d'une liaison de valeurs. Bien sûr, la partie répétée peut être extraite en tant que fonction séparée et c'est peut-être une bonne idée de toute façon, mais il y a aussi des arguments pour certaines fonctions non courtes et même si elle est extraite, ses arguments devront d'une manière ou d'une autre être passés dans les deux applications, et il peut alors être nécessaire de mémoriser la fonction pour ne pas réellement répéter le calcul. On utilisera beaucoup de converge , lens , memoize , useWidth etc.

  3. Spécifique au JavaScript : plus difficile à déboguer. Avec un flux linéaire de let il est facile d'ajouter un point d'arrêt à n'importe quel endroit. Avec le style sans point, même si un point d'arrêt est ajouté, le flux de valeurs est difficile à lire, par exemple, vous ne pouvez pas simplement interroger ou survoler une variable dans la console de développement. De plus, comme le style point-free n'est pas natif en JS, les fonctions de bibliothèque de ramda.js ou similaires obscurcissent un peu la pile, surtout avec le curry obligatoire.

  4. fragilité du code, en particulier sur les systèmes de taille non triviale et en production. Si une nouvelle exigence apparaît, les inconvénients susmentionnés entrent en jeu (par exemple, il est plus difficile de lire le code pour le prochain mainteneur qui pourrait être vous-même quelques semaines plus tard, et il est également plus difficile de retracer le flux de données à des fins d'inspection). Mais surtout, même une nouvelle exigence apparemment petite et innocente peut nécessiter une structuration entièrement différente du code. On peut arguer que c'est une bonne chose dans la mesure où il s'agira d'une représentation claire de la nouvelle chose, mais la réécriture de grandes parties de code sans points prend beaucoup de temps et nous n'avons pas encore parlé des tests. Il semble donc que le codage plus lâche, moins structuré, basé sur l'affectation lexicale, puisse être plus rapidement réutilisé. Surtout si le codage est exploratoire, et dans le domaine des données humaines avec des conventions bizarres (temps, etc.) qui peuvent rarement être capturées avec 100% de précision et qu'il peut toujours y avoir une demande à venir pour traiter quelque chose de manière plus précise ou plus adaptée aux besoins du client, la méthode qui conduit à un pivotement plus rapide est très importante.

-2voto

fpstefan Points 1

Pour la variante pointfree, le langage de programmation concaténatif, je dois écrire :
J'ai eu une petite expérience avec Joy. La joie est un concept très simple et très beau avec des listes. Lorsque vous convertissez un problème en une fonction Joy, vous devez diviser votre cerveau en une partie pour le travail de plomberie de la pile et une partie pour la solution dans la syntaxe Joy. La pile est toujours gérée par l'arrière. Comme la composition est contenue dans Joy, il n'y a pas de temps de calcul pour un combinateur de composition.

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