web-dev-qa-db-fra.com

qu'est-ce qui peut mal tourner dans le contexte de la programmation fonctionnelle si mon objet est modifiable?

Je peux voir que les avantages des objets mutables par rapport aux objets immuables, comme les objets immuables, enlèvent beaucoup de problèmes difficiles à résoudre dans la programmation multithread en raison de l'état partagé et accessible en écriture. Au contraire, les objets mutables aident à gérer l'identité de l'objet plutôt que de créer une nouvelle copie à chaque fois et améliorent ainsi les performances et l'utilisation de la mémoire, en particulier pour les objets plus gros.

Une chose que j'essaie de comprendre, c'est ce qui peut mal tourner en ayant des objets mutables dans le contexte de la programmation fonctionnelle. Comme l'un des points qui m'a été dit, le résultat de l'appel de fonctions dans un ordre différent n'est pas déterministe.

Je cherche un exemple concret réel où il est très évident ce qui peut mal tourner en utilisant un objet mutable dans la programmation de fonctions. Fondamentalement, si elle est mauvaise, elle est mauvaise indépendamment de OO ou du paradigme de programmation fonctionnelle, non?

Je crois que ma déclaration ci-dessous répond elle-même à cette question. Mais j'ai encore besoin d'un exemple pour pouvoir le ressentir plus naturellement.

OO aide à gérer la dépendance et à écrire des programmes plus faciles et maintenables à l'aide d'outils tels que l'encapsulation, le polymorphisme, etc.

La programmation fonctionnelle a également le même motif de promouvoir le code maintenable, mais en utilisant un style qui élimine le besoin d'utiliser OO outils et techniques - dont je crois que c'est en minimisant les effets secondaires, la fonction pure, etc.

9
rahulaga_dev

Je pense que l'importance est mieux démontrée en comparant à une approche OO

par exemple, disons que nous avons un objet

Order
{
    string Status {get;set;}
    Purchase()
    {
        this.Status = "Purchased";
    }
}

Dans le paradigme OO la méthode est attachée aux données, et il est logique que ces données soient mutées par la méthode.

var order = new Order();
order.Purchase();
Console.WriteLine(order.Status); // "Purchased"

Dans le paradigme fonctionnel, nous définissons un résultat en termes de fonction. une commande achetée [[# # ~] est [~ # ~] le résultat de la fonction d'achat appliquée à une commande. Cela implique quelques éléments dont nous devons être sûrs

var order = new Order(); //this is a 'new order'
var purchasedOrder = purchase(order); // this is a 'purchased order'
Console.WriteLine(order.Status); // "New" order is still a 'new order'

Vous attendriez-vous à order.Status == "Acheté"?

Cela implique également que nos fonctions sont idempotentes. c'est à dire. les exécuter deux fois devrait produire le même résultat à chaque fois.

var order = new Order(); //new order
var purchasedOrder = purchase(order); //purchased order
var purchasedOrder2 = purchase(order); //another purchased order
var purchasedOrder = purchase(purchasedOrder); //error! cant purchase an order twice

Si la commande était modifiée par la fonction d'achat, la commande PurchaseOrder2 échouerait.

En définissant les choses comme des résultats de fonctions, cela nous permet d'utiliser ces résultats sans réellement les calculer. Ce qui en termes de programmation est une exécution différée.

Cela peut être pratique en soi, mais une fois que nous ne savons pas quand une fonction se produira ET que cela nous convient, nous pouvons tirer parti du traitement parallèle beaucoup plus que dans un paradigme OO .

Nous savons que l'exécution d'une fonction n'affectera pas les résultats d'une autre fonction; afin que nous puissions laisser l'ordinateur pour les exécuter dans l'ordre qu'il choisit, en utilisant autant de threads qu'il le souhaite.

Si une fonction mute son entrée, nous devons être beaucoup plus prudents à ce sujet.

7
Ewan

La clé pour comprendre pourquoi les objets immuables sont bénéfiques ne réside pas vraiment dans la recherche d'exemples concrets dans le code fonctionnel. Étant donné que la plupart du code fonctionnel est écrit à l'aide de langages fonctionnels et que la plupart des langages fonctionnels sont immuables par défaut, la nature même du paradigme est conçue pour éviter que ce que vous recherchez ne se produise.

La chose clé à demander est, quel est cet avantage de l'immuabilité? La réponse est qu'elle évite la complexité. Disons que nous avons deux variables, x et y. Les deux commencent par la valeur de 1. y mais double toutes les 13 secondes. Quelle sera la valeur de chacun d'eux dans 20 jours? x sera 1. C'est facile. Cela demanderait des efforts pour trouver y car c'est beaucoup plus complexe. Quelle heure dans 20 jours? Dois-je prendre en compte l'heure d'été? La complexité de y par rapport à x est bien plus encore.

Et cela se produit également en vrai code. Chaque fois que vous ajoutez une valeur de mutation au mélange, cela devient une autre valeur complexe à retenir et à calculer dans votre tête, ou sur papier, lorsque vous essayez d'écrire, de lire ou de déboguer le code. Plus il y a de complexité, plus vous avez de chances de faire une erreur et d'introduire un bug. Le code est difficile à écrire; difficile à lire; difficile à déboguer: le code est difficile à obtenir correctement.

La mutabilité n'est pas mauvaise cependant. Un programme avec une mutabilité nulle ne peut avoir aucun résultat, ce qui est assez inutile. Même si la mutabilité consiste à écrire un résultat sur un écran, un disque ou autre, il doit être présent. Ce qui est mauvais, c'est une complexité inutile. L'un des moyens les plus simples de réduire la complexité consiste à rendre les choses immuables par défaut et à les rendre mutables uniquement en cas de besoin, pour des raisons de performances ou de fonctionnement.

12
David Arno

ce qui peut mal tourner dans le contexte de la programmation fonctionnelle

Les mêmes choses qui peuvent mal tourner dans la programmation non fonctionnelle: vous pouvez obtenir indésirable, inattendu effets secondaires, qui est un cause d'erreurs bien connue depuis l'invention des langages de programmation étendus.

À mon humble avis, la seule vraie différence entre la programmation fonctionnelle et non fonctionnelle est que, dans le code non fonctionnel, vous vous attendez généralement à des effets secondaires, dans la programmation fonctionnelle, vous ne le ferez pas.

Fondamentalement, si elle est mauvaise, elle est mauvaise indépendamment de OO ou du paradigme de programmation fonctionnelle, non?

Bien sûr, les effets secondaires indésirables sont une catégorie de bogues, quel que soit le paradigme. L'inverse est également vrai - les effets secondaires délibérément utilisés peuvent aider à résoudre les problèmes de performances et sont généralement nécessaires pour la plupart des programmes du monde réel en ce qui concerne les E/S et les systèmes externes - également quel que soit le paradigme.

8
Doc Brown

Je viens de répondre à une question StackOverflow qui illustre assez bien votre question. Le principal problème avec les structures de données mutables est que leur identité n'est valide qu'à un instant précis dans le temps, donc les gens ont tendance à s'entasser autant qu'ils le peuvent dans le petit point du code où ils savent que l'identité est constante. Dans cet exemple particulier, il fait beaucoup de journalisation dans une boucle for:

for (elem <- rows map (row => s3 map row)) {
  val elem_str = elem.map(_.toString)

  logger.info("verifying the S3 bucket passed from the ctrl table for each App")
  logger.info(s"Checking on App Code: ${elem head}")

  listS3Buckets(elem_str(1), elem_str(2)) match {

    case Some(allBktsInfo) =>
      logger.info(s"App: ${elem_str head} provided the bucket name as: ${elem_str(3)}")
      if (allBktsInfo.exists(x => x.getName == elem_str(3))) {
        logger.info(s"Provided S3 bucket: ${elem_str(3)} exists")
        println(s"s3 ${elem_str(3)} bucket exists")
      } else {
        logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
        logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
        excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
        println(s"s3 bucket ${elem_str(3)} doesn't exists")
    }

    case None =>
      logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
      logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
      excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
}

Lorsque vous êtes habitué à l'immuabilité, vous ne craignez pas que la structure des données change si vous attendez trop longtemps, vous pouvez donc effectuer des tâches logiquement séparées à votre guise, de manière beaucoup plus découplée:

val (exists, missing) = rows partition bucketExists
missing foreach {row =>
  logger.info(s"WARNING: Provided S3 bucket ${row("s3_primary_bkt_name")} doesn't exist")
  logger.info(s"WARNING: Dropping the App: ${row("app")} from backup schedule")
}
4
Karl Bielefeldt

L'avantage d'utiliser des objets immuables est que si l'on reçoit une référence à un objet avec qui aura une certaine propriété lorsque le récepteur l'examine, et doit donner à un autre code une référence à un objet avec cette même propriété, on peut simplement passer le long de la référence à l'objet sans tenir compte de qui d'autre aurait pu recevoir la référence ou de ce qu'ils pourraient faire à l'objet [puisqu'il n'y a rien d'autre peut faire à l'objet], ou quand le récepteur pourrait examiner l'objet [puisque toutes ses propriétés seront les mêmes quel que soit le moment où elles sont examinées].

En revanche, le code qui doit donner à quelqu'un une référence à un objet mutable qui aura une certaine propriété lorsque le récepteur l'examine (en supposant que le récepteur lui-même ne le change pas) doit également savoir que rien d'autre que le récepteur ne changera jamais cette propriété, ou bien savoir quand le récepteur va accéder à cette propriété, et savoir que rien ne va changer cette propriété jusqu'à la dernière fois que le récepteur l'examinera.

Je pense qu'il est très utile, pour la programmation en général (pas seulement la programmation fonctionnelle), de penser que les objets immuables entrent dans trois catégories:

  1. Les objets qui ne peuvent rien permettre de les changer, même avec une référence. De tels objets et leurs références se comportent comme valeurs et peuvent être librement partagés.

  2. Les objets qui se permettraient d'être modifiés par du code qui y fait référence, mais dont les références ne seront jamais exposées à un code qui en fait les changerait. Ces objets encapsulent des valeurs, mais ils ne peuvent être partagés qu'avec du code auquel on peut faire confiance pour ne pas les modifier ou les exposer au code qui pourrait le faire.

  3. Objets qui seront modifiés. Ces objets sont mieux vus comme conteneurs, et les références à eux comme identificateurs.

Un modèle utile consiste souvent à demander à un objet de créer un conteneur, de le remplir à l'aide de code auquel on peut faire confiance pour ne pas conserver de référence par la suite, puis de disposer des seules références qui existeront n'importe où dans l'univers dans un code qui ne modifiera jamais le objet une fois qu'il est rempli. Bien que le conteneur puisse être de type mutable, il peut être raisonné sur (*) comme s'il était immuable, car rien ne le mutera jamais. Si toutes les références au conteneur sont conservées dans des types de wrapper immuables qui ne modifieront jamais son contenu, ces wrappers peuvent être transmis en toute sécurité comme si les données qu'ils contenaient étaient conservées dans des objets immuables, car les références aux wrappers peuvent être librement partagées et examinées à tout moment.

(*) Dans le code multithread, il peut être nécessaire d'utiliser des "barrières de mémoire" pour garantir qu'avant qu'un thread ne puisse voir une référence à l'encapsuleur, les effets de toutes les actions sur le conteneur soient visibles pour ce thread, mais c'est un cas spécial mentionné ici uniquement pour être complet.

3
supercat

Comme cela a déjà été mentionné, le problème avec l'état mutable est fondamentalement une sous-classe du problème plus large des effets secondaires , où le type de retour d'une fonction ne correspond pas avec précision décrire ce que fait réellement la fonction, car dans ce cas, elle fait également état d'une mutation. Ce problème a été résolu par certains nouveaux langages de recherche, tels que F * ( http://www.fstar-lang.org/tutorial/ ). Ce langage crée un système d'effets similaire au système de types, où une fonction déclare non seulement statiquement son type, mais aussi ses effets. De cette façon, les appelants de la fonction sont conscients qu'une mutation d'état peut se produire lors de l'appel de la fonction et que cet effet se propage à ses appelants.

1
Aaron M. Eshbach