web-dev-qa-db-fra.com

"Facile à raisonner" - qu'est-ce que cela signifie?

J'ai entendu beaucoup de fois où d'autres développeurs utilisent cette expression pour "faire la publicité" de certains modèles ou développer des meilleures pratiques. La plupart du temps, cette phrase est utilisée lorsque vous parlez des avantages de la programmation fonctionnelle.

L'expression "Facile à raisonner" a été utilisée telle quelle, sans explication ni exemple de code. Donc, pour moi, cela devient comme le prochain "buzz" -Word, que les développeurs plus "expérimentés" utilisent dans leurs discussions.

Question: Pouvez-vous fournir des exemples de "Pas facile à raisonner", afin de le comparer avec des exemples "Facile à raisonner"?

52
Fabio

À mon avis, l'expression "facile à raisonner" fait référence à du code facile à "exécuter dans votre tête".

Lorsque vous regardez un morceau de code, s'il est court, clairement écrit, avec de bons noms et une mutation minimale des valeurs, travailler mentalement à travers ce que fait le code est une tâche (relativement) facile.

Un long morceau de code avec des noms médiocres, des variables qui changent constamment de valeur et une ramification compliquée nécessiteront normalement, par exemple, un stylo et un morceau de papier pour aider à garder une trace de l'état actuel. Un tel code ne peut donc pas être facilement travaillé dans votre tête, donc ce code n'est pas facile à raisonner.

59
David Arno

Un mécanisme ou un morceau de code est facile à raisonner quand vous devez prendre en compte quelques éléments pour prédire ce qu'il fera, et les éléments dont vous devez tenir compte sont facilement disponibles.

Les vraies fonctions sans effets secondaires et sans état sont faciles à raisonner car la sortie est complètement déterminée par l'entrée, qui est juste là dans les paramètres.

Inversement, un objet avec état est beaucoup plus difficile à raisonner, car vous devez tenir compte de l'état dans lequel se trouve l'objet lorsqu'une méthode est appelée, ce qui signifie que vous devez réfléchir aux autres situations pouvant conduire à ce que l'objet se trouve dans un état particulier.

Les variables globales sont encore pires: pour raisonner sur le code qui lit une variable globale, vous devez comprendre où dans votre code cette variable peut être définie et pourquoi - et il peut même ne pas être facile de trouver tous ces endroits.

La programmation multithread avec état partagé est à peu près la chose la plus difficile à raisonner, car non seulement vous avez un état, vous avez plusieurs threads le modifiant en même temps, alors pour raisonner sur ce que fait un morceau de code lorsqu'il est exécuté par un thread, vous doivent prévoir la possibilité qu'à chaque point d'exécution, un autre thread (ou plusieurs d'entre eux!) exécute à peu près n'importe quelle autre partie du code et modifie les données sur lesquelles vous travaillez sous vos yeux. En théorie, cela peut être géré avec des mutex/moniteurs/sections critiques/peu importe comment vous l'appelez, mais dans la pratique, aucun simple humain n'est réellement capable de le faire de manière fiable à moins qu'il ne limite radicalement l'état partagé et/ou le parallélisme à de très petits sections du code.

47
Michael Borgwardt

Dans le cas de la programmation fonctionnelle, la signification de "Facile à raisonner" est surtout qu'elle est déterministe. J'entendais par là qu'une entrée donnée mènera toujours à la même sortie. Vous pouvez faire ce que vous voulez avec le programme, tant que vous ne touchez pas ce morceau de code, il ne se cassera pas.

D'un autre côté, OO est généralement plus difficile à raisonner car la "sortie" produite dépend de l'état interne de chaque objet impliqué. La manière typique dont il se manifeste est inattendue côté effects: lors du changement d'une partie du code, une partie apparemment sans rapport se casse.

... l'inconvénient de la programmation fonctionnelle est bien sûr qu'en pratique, une grande partie de ce que vous voulez faire est IO et gérer l'état.

Cependant, il y a beaucoup d'autres choses qui sont plus difficiles à raisonner, et je suis d'accord avec @Kilian que la concurrence est un excellent exemple. Systèmes distribués aussi.

9
dagnelies

Éviter une discussion plus large et aborder la question spécifique:

Pouvez-vous fournir des exemples de "Pas facile à raisonner", afin qu'il puisse être comparé avec des exemples "Facile à raisonner"?

Je vous renvoie à "L'histoire de Mel, un vrai programmeur" , un morceau de folklore programmeur qui date de 1983 et qui compte donc comme "légende", pour notre profession.

Il raconte l'histoire d'un programmeur écrivant du code qui préférait les techniques arcanes dans la mesure du possible, y compris le code autoréférentiel et auto-modifiant, et l'exploitation délibérée des bogues de la machine:

une boucle apparente infinie avait en fait été codée de manière à tirer parti d'une erreur de dépassement. L'ajout de 1 à une instruction décodée en tant que "Charger à partir de l'adresse x" produisait normalement "Charger à partir de l'adresse x + 1". Mais lorsque x était déjà l'adresse la plus élevée possible, non seulement l'adresse a été bouclée à zéro, mais un 1 a été transporté dans les bits à partir desquels l'opcode serait lu, changeant l'opcode de "charger de" à "sauter à" afin que l'instruction complète est passée de "charger à partir de la dernière adresse" à "passer à l'adresse zéro".

Ceci est un exemple de code "difficile à raisonner".

Bien sûr, Mel serait en désaccord ...

5
AakashM

Je peux vous donner un exemple, très courant.

Considérez le code C # suivant.

// items is List<Item>
var names = new List<string>();
for (var i = 0; i < items.Count; i++)
{
    var item = items[i];
    var mangled = MyMangleFunction(item.Name);
    if (mangled.StartsWith("foo"))
    {
        names.Add(mangled);
    }
}

Considérez maintenant cette alternative.

// items is List<Item>
var names = items
    .Select(item => MyMangleFunction(item.Name))
    .Where(s => s.StartsWith("foo"))
    .ToList();

Dans le deuxième exemple, je sais exactement ce que fait ce code en un coup d'œil. Quand je vois Select, je sais qu'une liste d'éléments est en cours de conversion en une liste d'autre chose. Lorsque je vois Where, je sais que certains éléments sont filtrés. En un coup d'œil, je peux comprendre ce qu'est names et l'utiliser efficacement.

Quand je vois une boucle for, je n'ai aucune idée de ce qui se passe avec elle jusqu'à ce que je lise réellement le code. Et parfois, je dois le parcourir pour être sûr d'avoir pris en compte tous les effets secondaires. Je dois faire un peu de travail pour même comprendre ce que sont les noms (au-delà de la définition de type) et comment l'utiliser efficacement. Ainsi, le premier exemple est plus difficile à raisonner que le second.

En fin de compte, être facile à raisonner ici dépend également de la compréhension des méthodes LINQ Select et Where. Si vous ne les connaissez pas, le deuxième code est plus difficile à raisonner au départ. Mais vous ne payez le prix pour les comprendre qu'une seule fois. Vous payez le coût pour comprendre une boucle for à chaque fois que vous en utilisez une et à chaque fois qu'elle change. Parfois, le coût vaut la peine d'être payé, mais il est généralement beaucoup plus "facile de raisonner".

5
Kasey Speakman

Une phrase connexe est (je paraphrase),

Il ne suffit pas que le code n'ait " pas de bugs évidents ": à la place, il devrait avoir " évidemment pas de bugs ".

Un exemple de relativement "facile à raisonner" pourrait être RAII .

Un autre exemple pourrait être d'éviter étreinte mortelle : si vous pouvez maintenir un verrou et en acquérir un autre, et qu'il y a beaucoup de verrous, il est difficile d'être sûr qu'il n'y a pas de scénario dans lequel une étreinte mortelle pourrait se produire. L'ajout d'une règle comme "il n'y a qu'un seul verrou (global)" ou "vous n'êtes pas autorisé à acquérir un deuxième verrou pendant que vous en tenez un premier", rend le système relativement facile à raisonner.

2
ChrisW

Le nœud de la programmation est l'analyse de cas. Alan Perlis l'a remarqué dans l'épigramme n ° 32: Les programmeurs ne doivent pas être mesurés par leur ingéniosité et leur logique mais par l'exhaustivité de leur analyse de cas.

Une situation est facile à raisonner si l'analyse de cas est facile. Cela signifie qu'il y a peu de cas à considérer ou, à défaut, peu de cas spéciaux - il peut y avoir de grands espaces de cas, mais qui s'effondrent en raison de certaines régularités, ou succomber à une technique de raisonnement telle que l'induction.

Une version récursive d'un algorithme, par exemple, est généralement plus facile à raisonner qu'une version impérative, car elle ne contribue pas aux cas superflus qui surviennent par la mutation de variables d'état de support qui n'apparaissent pas dans la version récursive. De plus, la structure de la récursivité est telle qu'elle s'inscrit dans un modèle mathématique de preuve par induction. Nous n'avons pas à considérer les complexités comme les variantes de boucle et les préconditions strictes les plus faibles et ainsi de suite.

Un autre aspect de cela est la structure de l'espace du boîtier. Il est plus facile de raisonner sur une situation qui a une division plate ou majoritairement plate en cas par rapport à une situation de cas hiérarchique: cas avec sous-cas et sous-sous-cas et ainsi de suite.

Une propriété des systèmes qui simplifie le raisonnement est l'orthogonalité : c'est la propriété que les cas qui régissent les sous-systèmes restent indépendants lorsque ces sous-systèmes sont combinés. Aucune combinaison ne donne lieu à des "cas particuliers". Si un quelque chose à quatre cas est combiné orthogonalement à quelque chose à trois cas, il y a douze cas, mais idéalement chaque cas est une combinaison de deux cas qui restent indépendants. Dans un sens, il n'y a pas vraiment douze cas; les combinaisons ne sont que des "phénomènes émergents de type cas" dont nous n'avons pas à nous soucier. Cela signifie que nous avons encore quatre cas auxquels nous pouvons penser sans considérer les trois autres dans l'autre sous-système, et vice versa. Si certaines combinaisons doivent être spécialement identifiées et dotées d'une logique supplémentaire, le raisonnement est plus difficile. Dans le pire des cas, chaque combinaison a une manipulation particulière, puis il y a vraiment douze nouveaux cas, qui s'ajoutent aux quatre et trois d'origine.

2
Kaz

Sûr. Prenez la simultanéité:

Sections critiques imposées par les mutex: faciles à comprendre car il n'y a qu'un seul principe (deux threads d'exécution ne peuvent pas entrer simultanément dans la section critique), mais sujettes à la fois à l'inefficacité et à l'impasse.

Modèles alternatifs, par ex. programmation ou acteurs sans verrou: potentiellement beaucoup plus élégant et puissant, mais incroyablement difficile à comprendre, car vous ne pouvez plus compter sur (apparemment) des concepts fondamentaux tels que "maintenant écrivez cette valeur à cet endroit" .

Être facile à raisonner est n aspect d'une méthode. Mais choisir la méthode à utiliser nécessite de prendre en compte tous les aspects en combinaison.

0
Kilian Foth

Limitons la tâche au raisonnement formel. Parce que le raisonnement humoristique ou inventif ou poétique a des lois différentes.

Même ainsi, l'expression est faiblement définie et ne peut pas être définie de manière stricte. Mais cela ne signifie pas que cela devrait rester si sombre pour nous. Imaginons qu'une structure passe un certain test et obtient des notes pour différents points. Les bonnes notes pour CHAQUE point signifient que la structure est pratique dans tous les aspects et donc, "Facile à raisonner".

La structure "Facile à raisonner" devrait obtenir de bonnes notes pour les éléments suivants:

  • Les termes internes ont des noms raisonnables, faciles à distinguer et à définir. Si les éléments ont une certaine hiérarchie, la différence entre les noms des parents et des enfants doit être différente de la différence entre les noms des frères et sœurs.
  • Le nombre de types d'éléments structurels est faible
  • Les types d'éléments structurels utilisés sont des choses faciles auxquelles nous sommes habitués.
  • Les éléments difficilement compréhensibles (récursions, méta étapes, géométrie 4+ dimensions ...) sont isolés - pas directement combinés les uns avec les autres. (par exemple, si vous essayez de penser à des règles récursives changeantes pour les cubes dimensionnels 1,2,3,4..n .., ce sera très compliqué. Mais si vous transférez chacune de ces règles dans une formule en fonction de n, vous aurez séparément une formule pour chaque n-cube et séparément une règle de récursivité pour une telle formule. Et que deux structures séparément peuvent être facilement pensées)
  • Les types d'éléments structurels sont évidemment différents (par exemple, ne pas utiliser de tableaux mixtes à partir de 0 et de 1)

Le test est-il subjectif? Oui, bien sûr. Mais l'expression elle-même est également subjective. Ce qui est facile pour une personne n'est pas facile pour une autre. Ainsi, les tests doivent être différents pour les différents domaines.

0
Gangnus

L'idée des langages fonctionnels pouvant être raisonnés vient de leur histoire, en particulier ML qui a été développé comme un langage de programmation analogue aux constructions que le Logic for Computable Functions utilisé pour le raisonnement. La plupart des langages fonctionnels sont plus proches des calculs de programmation formelle que des impératifs, de sorte que la traduction du code dans l'entrée d'un système de système de raisonnement est moins onéreuse.

Pour un exemple de système de raisonnement, en pi-calcul, chaque emplacement de mémoire mutable dans un langage impératif doit être représenté comme un processus parallèle séparé, tandis qu'une séquence d'opérations fonctionnelles est un processus unique. Quarante ans après le prouveur du théorème LFC, nous travaillons avec des Go de RAM donc avoir des centaines de processus est moins un problème - j'ai utilisé le pi-calcul pour supprimer les blocages potentiels de quelques centaines de lignes de C++, malgré la représentation comportant des centaines de processus, le raisonnement a épuisé l'espace d'état dans environ 3 Go et a résolu un bug intermittent. Cela aurait été impossible dans les années 70 ou aurait nécessité un supercalculateur au début des années 1990, alors que l'espace d'état d'un fonctionnel programme de langue de taille similaire était assez petit pour raisonner à l'époque.

D'après les autres réponses, la phrase devient un buzz, même si une grande partie de la difficulté qui rendait difficile de raisonner sur les langues impératives est érodée par la loi de Moore.

0
Pete Kirkham