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

9

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 quel que soit l'OO ou le paradigme de programmation fonctionnelle, non?

Je crois que ma propre déclaration 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 des outils et des techniques OO - dont je crois que c'est en minimisant les effets secondaires, la fonction pure, etc.

rahulaga_dev
la source
1
@Ruben Je dirais que la plupart des langages fonctionnels autorisent les variables mutables, mais rendent leur utilisation différente, par exemple les variables mutables ont un type différent
jk.
1
Je pense que vous avez peut-être mélangé immuable et mutable dans votre premier paragraphe?
jk.
1
@jk., il l'a certainement fait. Modifié pour corriger cela.
David Arno
6
@Ruben La programmation fonctionnelle est un paradigme. En tant que tel, il ne nécessite pas de langage de programmation fonctionnel. Et certains langages fp tels que F # ont cette fonctionnalité .
Christophe
1
@Ruben no spécifiquement, je pensais à Mvars dans haskell hackage.haskell.org/package/base-4.9.1.0/docs/… différentes langues ont bien sûr des solutions différentes ou IORefs hackage.haskell.org/package/base-4.11.1.0 /docs/Data-IORef.html bien sûr, vous utiliseriez les deux à partir de monades
jk.

Réponses:

7

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 nous ne le pouvons 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.

Ewan
la source
Merci !! très utile. Ainsi, la nouvelle implémentation de l'achat devrait ressembler à Order Purchase() { return new Order(Status = "Purchased") } un champ en lecture seule. ? Encore une fois, pourquoi cette pratique est plus pertinente dans le contexte du paradigme de programmation de fonction? Les avantages que vous avez mentionnés peuvent également être vus dans la programmation OO, non?
rahulaga_dev
dans OO, vous vous attendez à ce que object.Purchase () modifie l'objet. Vous pouvez le rendre immuable, mais alors pourquoi ne pas passer à un paradigme fonctionnel complet
Ewan
Je pense que le problème doit être visualisé parce que je suis un développeur c # pur qui est orienté objet par nature. Donc, ce que vous dites dans un langage qui embrasse la programmation fonctionnelle ne nécessitera pas que la fonction 'Purchase ()' renvoyant la commande achetée soit attachée à une classe ou un objet, non?
rahulaga_dev
3
vous pouvez écrire c # fonctionnel changer votre objet en struct, le rendre immuable et écrire un Func <Order, Order> Purchase
Ewan
12

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, xet y. Les deux commencent par la valeur de 1. ymais double toutes les 13 secondes. Quelle sera la valeur de chacun d'eux dans 20 jours? xsera 1. C'est facile. Cela demanderait des efforts pour trouver yun moyen plus complexe. Quelle heure dans 20 jours? Dois-je prendre en compte l'heure d'été? La complexité de yversus xest bien plus que cela.

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.

Mais la mutabilité n'est pas mauvaise . 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.

David Arno
la source
4
"l'un des moyens les plus simples de réduire la complexité est de rendre les choses immuables par défaut et de les rendre mutables uniquement en cas de besoin": résumé très agréable et concis.
Giorgio
2
@DavidArno La complexité que vous décrivez rend le code difficile à raisonner. Vous avez également abordé ce sujet lorsque vous dites: "Le code est difficile à écrire, difficile à lire, difficile à déboguer; ...". J'aime les objets immuables car ils rendent le code beaucoup plus facile à raisonner, pas seulement par moi-même, mais par des observateurs qui regardent sans connaître tout le projet.
disassemble-number-5
1
@RahulAgarwal, " Mais pourquoi ce problème devient plus important dans le contexte de la programmation fonctionnelle ". Ce n'est pas le cas. Je pense que je suis peut-être confus par ce que vous demandez car le problème est beaucoup moins important dans la PF car FP encourage l'immuabilité évitant ainsi le problème.
David Arno
1
@djechlin, " Comment votre exemple de 13 secondes peut-il être plus facile à analyser avec du code immuable? " Il ne peut pas: ydoit muter; c'est une exigence. Parfois, nous devons avoir un code complexe pour répondre à des exigences complexes. Le point que j'essayais de faire est qu'il fallait éviter toute complexité inutile . Les valeurs de mutation sont intrinsèquement plus complexes que les valeurs fixes, donc - pour éviter une complexité inutile - ne modifiez les valeurs que lorsque vous le devez.
David Arno
3
La mutabilité crée une crise d'identité. Votre variable n'a plus une seule identité. Au lieu de cela, son identité dépend désormais du temps. Donc symboliquement, au lieu d'un seul x, nous avons maintenant une famille x_t. Tout code utilisant cette variable devra désormais également se soucier du temps, ce qui entraînera une complexité supplémentaire mentionnée dans la réponse.
Alex Vong
8

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 des effets secondaires indésirables et inattendus , ce qui est une cause bien connue d'erreurs 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 quel que soit l'OO ou le 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.

Doc Brown
la source
4

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 à 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")
}
Karl Bielefeldt
la source
3

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 pourrait avoir reçu la référence ou ce qu'ils pourraient faire l'objet [depuis n'importe qui rien de là d' autre peut faire l'objet], ou lorsque le récepteur peut examiner l'objet [puisque tous ses les 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 des valeurs et peuvent être librement partagés.

  2. 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 les modifierait réellement . 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 des conteneurs et leurs références comme des 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 partout 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.

supercat
la source
merci pour la réponse impressionnante !! Je pense que la source de ma confusion est probablement parce que je suis de fond c # et que j'apprends à "écrire du code de style fonctionnel en c #" qui continue partout en disant d'éviter les objets mutables - mais je pense que les langages qui embrassent le paradigme de programmation fonctionnelle promeuvent (ou appliquent - pas sûr) si l'application est correcte à utiliser) immuabilité.
rahulaga_dev
@RahulAgarwal: Il est possible que des références à un objet encapsulent une valeur dont la signification n'est pas affectée par l'existence d'autres références au même objet, aient une identité qui les associe à d'autres références au même objet, ou ni l'une ni l'autre. Si l'état du mot réel change, alors la valeur ou l'identité d'un objet associé à cet état peut être constante, mais pas les deux - il faudra changer. Les 50 000 $ sont ce qui devrait faire quoi.
supercat
1

Comme cela a déjà été mentionné, le problème de 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 décrit pas précisément ce que fait réellement la fonction, car dans ce cas, il fait également une mutation d'état. 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'effet similaire au système de type, 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.

Aaron M. Eshbach
la source