8
votes

Fonctions qui ont l'air pur aux appelants mais utilisent mutation en interne

Je viens de recevoir ma copie de expert f # 2.0 et est tombé sur cette déclaration, qui m'a quelque peu surpris:

Par exemple, si nécessaire, vous pouvez Utiliser des effets secondaires sur les données privées structures allouées au début de un algorithme puis jeter ces structures de données avant de retourner un résultat; Le résultat global est alors efficacement un effet secondaire sans effet fonction. Un exemple de séparation de la bibliothèque F # est la bibliothèque implémentation de liste.MAP, qui utilise mutation en interne; les écritures se produisent Sur une donnée interne séparée Structure qu'aucun autre code ne peut accès.

Maintenant, évidemment l'avantage de cette approche est la performance. Je suis juste curieux s'il y a des inconvénients - faire des pièges pouvant venir avec des effets secondaires s'appliquer ici? Est la parallélisibilité affectée?

En d'autres termes, si les performances ont été réservées, il serait préférable d'implémenter list.map d'une manière pure?

(évidemment, cela traite de F # en particulier, mais je suis aussi curieux de la philosophie générale)


1 commentaires

Peut-être changer le titre sur quelque chose de plus direct, comme "sont une fonction pure qui utilise des substances mutables nuisibles internes?"


7 Réponses :


2
votes

Il n'affectera pas si la fonction peut être exécutée en parallèle avec d'autres fonctions. Cela affectera si les internes de la fonction peuvent être faits parallèles, mais il est peu probable qu'il s'agisse d'un problème pour la plupart des petites fonctions (telles que la carte), ciblant un PC.

J'ai remarqué que certains de bons programmeurs F # (sur le Web et dans les livres) semblent être très détendus pour utiliser des techniques impératives pour les boucles. Ils semblent préférer une boucle simple, avec des variables de boucle mutable, à une fonction récursive complexe.


0 commentaires

6
votes

Vous pourriez être intéressé par Simon Peyton Jones's "Fils de l'état fonctionnel paresseux" . Je n'ai jamais fait que par les premières pages, qui sont très claires (je suis sûr que le reste est très clair aussi).

Le point important est que lorsque vous utilisez contrôler.monad.st pour faire ce genre de chose à Haskell, le système de type lui-même applique l'encapsulation. À Scala (et probablement F #), l'approche est plus "Faire confiance à nous que nous ne faisons rien de sournois ici avec ce listbuffer dans votre carte " / code> ".


5 commentaires

En pratique, les effets secondaires incontrôlés sont cependant dans le code de Haskell réel.


@Jonharrop, pouvez-vous donner un exemple?


@sebleblanc: Bien sûr. J'ai étudié plusieurs morceaux de code Haskell open source tel que le gibier frag et trouvé qu'ils avaient eu recours à Unsafeperformio beaucoup. J'ai demandé aux auteurs pourquoi et ils ont tous dit "performance". Depuis, j'ai depuis entendu dire que les problèmes sous-jacents ont été abordés et que cela n'est plus nécessaire, il ne peut plus être le cas.


Pour l'enregistrement, je n'ai pas pu trouver une seule occurrence de UNSAPERFORMIO dans toutes les versions publiées de frag.


Encore une fois, pouvez-vous sauvegarder vos revendications?



4
votes

Si une fonction utilise des structures de données mutables locales, privées (à la fonction), la parallélisation n'est pas affectée. Donc, si la fonction crée en interne crée une matrice de la taille de la liste et iTère sur ses éléments remplissant le tableau, vous pouvez toujours exécuter carte 100 fois simultanément sur le même Liste et ne vous inquiétez pas car chaque instance de carte aura sa propre matrice privée. Étant donné que votre code ne peut pas voir le contenu de la matrice tant qu'elle n'a pas été renseignée, elle est effectivement pure (rappelez-vous, à un niveau, votre ordinateur doit muter l'état de RAM).

D'autre part Si une fonction utilise des structures de données mutables globales, la parallélisation pourrait être affectée. Par exemple, disons que vous avez une fonction Memoize . Évidemment, tout cela a pour but de maintenir un État global (bien que "Global" en ce sens que ce ne soit pas local à l'appel de la fonction, il est toujours "privé" en ce sens qu'il n'est pas accessible en dehors de la fonction) pour que Il ne doit pas nécessairement exécuter une fonction plusieurs fois avec les mêmes arguments, mais elle est toujours pure car les mêmes entrées produiront toujours les mêmes sorties. Si votre structure de données de cache est thread-coffre-fort (comme ConcurrentComment ), vous pouvez toujours exécuter votre fonction en parallèle avec elle-même. Sinon, vous pourriez alors faire valoir que la fonction n'est pas pure car elle a des effets secondaires observables lorsqu'il est exécuté simultanément.

Je devrais ajouter que c'est une technique commune en F # pour commencer par une routine purement fonctionnelle, puis l'optimise en tirant parti de l'état mutable (par exemple, la mise en cache, la boucle explicite) lorsque le profilage montre qu'il est trop lent.


1 commentaires

En fait, c'est une idée fausse commune. Les tripes impures ont tendance à améliorer l'évolutivité car la mutation continue signifie une meilleure réutilisation de l'espace et, par conséquent, moins une allocation et une meilleure empreinte mémoire qui signifie moins de contention à la poubelle et à la mémoire principale, respectivement. Moins de contentions pour les ressources partagées signifie une meilleure évolutivité.



3
votes

La même approche peut être trouvée dans le clojure. Les structures de données immuables dans la liste des clojures, la carte et le vecteur - ont leurs homologues "transitoires" qui sont mutables. Le RÉFÉRENCE DE CLOJURE À PROPOS DE TRANSIENT Saviguez uniquement pour les utiliser uniquement dans le code qui ne peut pas être vu par «aucun autre code».

Il y a des gardes contre les fuites de transitoires dans le code client:

  • La fonction habituelle qui fonctionnent sur les structures de données immuables ne fonctionnent pas sur les transitoires. Les appeleront une exception.

  • Les transitoires sont liés au fil qu'ils sont créés. Les modifier à partir de tout autre thread vont lancer une exception.

    Le code Clojure.core lui-même utilise beaucoup de transitoires derrière les scènes.

    Le principal avantage de l'utilisation de transitoires est la rapidité massive qu'ils fournissent.

    de sorte que l'utilisation étroitement contrôlée de l'état mutable semble être ok dans les langues fonctionnelles.


0 commentaires

2
votes

Un problème peut être qu'un bon compilateur fonctionnel est construit pour optimiser le code "fonctionnel", mais si vous utilisez des éléments mutables, le compilateur peut ne pas optimiser aussi bon que dans l'autre cas. Dans le pire des cas, cela conduit à des algorithmes plus inefficaces alors la variante immuable.

L'autre question que je peux penser est la paresse - une structure de données mutable n'est généralement pas paresseuse, une onction mutable peut donc forcer une évaluation inépcidente des arguments.


0 commentaires

14
votes

Je pense que presque tous les inconvénients des effets secondaires sont liés à "interaction avec d'autres parties du programme". Les effets secondaires eux-mêmes ne sont pas mauvais (comme @gabe dit, même un programme fonctionnel pur se mutartons constamment de RAM), ce sont les conséquences vocales des effets (interactions non locales) qui causent des problèmes (avec le débogage / la performance / la compréhension /etc.). SO Effets sur l'état purement local (par exemple sur une variable locale qui ne s'échappe pas) va bien.

(Le seul détriment que je puisse penser, c'est que, quand un humain voit un tel mutable local, ils doivent raisonner si cela peut s'échapper. Dans F #, les mutailles locales ne peuvent jamais s'échapper (les fermetures ne peuvent pas capturer des mutables), donc le Seul potentiel «taxe mental» provient du raisonnement sur les types de référence mutable.)

résumé: il convient d'utiliser des effets, tant qu'il est simple de vous convaincre que les effets ne se produisent que sur les locaux non échappés. (Il est également correct d'utiliser des effets dans d'autres cas, mais j'ignore ces autres cas, car sur cette question - nous sommes les programmeurs fonctionnels éclairés qui tentent d'éviter des effets chaque fois que c'est raisonnable. :))

(Si vous voulez aller très profondément, des effets locaux, comme ceux de la liste de la liste F #.MAP, ne sont pas seulement un non-obstacle à la parallélysabilité, mais en fait un avantage, du point de vue que plus La mise en œuvre efficace alloue moins, et est donc moins de souche sur la ressource partagée du GC.)


7 commentaires

RE: Les effets locaux peuvent être plus efficaces. Fuzxxl souligne ci-dessous que le code purement fonctionnel peut être plus facilement optimisé par le compilateur. Cet effet semblerait équilibrer, ou l'emporter sur une solution plus efficace non optimisée.


Optimiser les compilateurs sont un mythe (sauf peut-être dans Haskell). OK, cette déclaration est trop forte, mais elle est extrêmement rare pour un compilateur d'une langue impure à faire un meilleur travail de «optimisation» de code pur que pour un humain à l'optimiser à la main avec un code impur. Les compilateurs ne sont tout simplement pas si bon.


@Brian: Étant donné que le matériel sous-jacent est "impur", je pense qu'il est prudent de dire qu'un code impuissant humain optimisant la main suffisamment à la main battra un compilateur de tous les temps. Les compilateurs Haskell sont incroyablement intelligents, mais il y a une raison pour laquelle des choses comme le ST MONAD existent et que toute la magie profonde de GHC est la plupart des performances similaires à un code impuissant de manière compétente (non optimisée). Sans les vastes garanties statiques fournies par HASKELL, il est fou de s'attendre à ce que d'autres langues soient meilleures (bien que j'ai entendu OCAML puisse parfois obtenir des résultats impressionnants).


Vous êtes un grand fan de déclarations entre parenthèses, n'est-ce pas? :)


C'est coz anglais n'a pas #light :)


@CAMCCANN: OCAML obtient une excellente performance grâce à un itinéraire très différent: en disposant d'un modèle de coût simple et prévisible et d'une conception linguistique qui a tendance à faire rapidement du code. C'est fondamentalement le contraire exact de Haskell.


optimiser les compilateurs sont un mythe allez. Vous ne voulez pas par ex. Recherche dans C ++ pour chaque fonction utilisée une fois en code pour écrire en ligne , surtout si c'est grand. Mais un compilateur est suffisamment intelligent pour voir l'optimisation du temps de liaison que appelez 'n' ret inutile ici et en ligne. Il peut également révéler qu'une petite boucle exécutée une seule ou deux fois, et vous dérouler cela - mais vous ne voudrez pas le faire explicitement en code. Plus le compilateur peut effectuer des optimisations dépendantes quelles instructions de processeur sont disponibles et même optimiser la taille du cache CPU (la chose que les peuples gentoo aiment) .



0
votes

Je répondrais à cela avec une question: "Écrivez-vous la fonction ou utilisez la fonction?"

Il y a 2 aspects aux fonctions, aux utilisateurs et aux développeurs.

En tant qu'utilisateur, on ne se soucie pas de la structure interne d'une fonction du tout. Il peut être codé en code d'octet et utiliser des effets secondaires durs en interne à partir de maintenant jusqu'au jour du jugement tant qu'il correspond au contrat de données In-> Données que l'on attend. Une fonction est une boîte noire ou une oracle, sa structure interne est sans importance (en supposant que cela ne fait rien de stupide et externe).

En tant que développeur d'une fonction, la structure interne importe beaucoup. Immutabilité, Const la correction et éviter les effets secondaires Tout aident à développer et à conserver une fonction et à élargir la fonction dans le domaine parallèle.

Beaucoup de gens développent une fonction qu'ils utilisent ensuite, de sorte que ces deux aspects s'appliquent.

Quels sont les avantages d'immutabilité et des structures mutables sont une question différente.


1 commentaires

Mais c'était exactement la question.