web-dev-qa-db-fra.com

Les fermetures sont-elles considérées comme un style fonctionnel impur?

Les fermetures sont-elles considérées comme impurées dans la programmation fonctionnelle?

Il semble que l'on puisse généralement éviter les fermetures en passant directement des valeurs à une fonction. Par conséquent, les fermetures devraient-elles être évitées dans la mesure du possible?

S'ils sont impurs et que j'ai raison de dire qu'ils peuvent être évités, pourquoi de nombreuses langues de programmation fonctionnelle sont-elles des fermetures?

L'un des critères de A fonction pure est que "la fonction évalue toujours la même valeur de résultat donné la même valeur . "

Supposer

f: x -> x + y

f(3) ne donnera pas toujours le même résultat. f(3) dépend de la valeur de y qui n'est pas un argument de f. Ainsi, f n'est pas une fonction pure.

Étant donné que toutes les fermetures dépendent des valeurs qui ne sont pas des arguments, comment est-il possible pour une fermeture d'être pure? Oui, en théorie, la valeur fermée pourrait être constante, mais il n'ya aucun moyen de savoir que juste en regardant que le code source de la fonction elle-même.

Où cela me conduit à savoir que la même fonction peut être pure dans une situation mais impure dans une autre. On ne peut pas toujours Déterminez si une fonction est pure ou non en étudiant son code source. Au lieu de cela, on peut avoir à le considérer dans le contexte de son environnement au point où il est appelé avant que cette distinction puisse être faite.

Est-ce que je pense à cela correctement?

35
user2179977

La pureté peut être mesurée par deux choses:

  1. La fonction renvoie-t-elle toujours la même sortie, étant donné la même entrée; c'est-à-dire c'est -transparent référentiel?
  2. La fonction modifie-t-elle quelque chose en dehors de lui-même, c'est-à-dire avoir des effets secondaires?

Si la réponse à 1 est oui et que la réponse à 2 est non, la fonction est pure. Les fermetures ne font qu'une fonction impur si vous modifiez la variable fermée.

26
Robert Harvey

Les fermetures apparaissent à Lambda Calculus, qui est la forme la plus pure de la programmation fonctionnelle possible, donc je ne les appellerais pas "impur" ...

Les fermetures ne sont pas "impures" car les fonctions de langues fonctionnelles sont des citoyens de première classe - cela signifie qu'ils peuvent être traités comme des valeurs.

Imaginez ceci (pseudocode):

foo(x) {
    let y = x + 1
    ...
}

y est une valeur. Son valeur dépend de x, mais x est immuable alors y 'S est également immutable. Nous pouvons appeler foo plusieurs fois avec différents arguments qui produiront différentes ys, mais ces ys vivent tous dans des champs différents et dépendent de différentes xs. reste intact.

Maintenant changeons-le:

bar(x) {
    let y(z) = x + z
    ....
}

Ici, nous utilisons une fermeture (nous terminons x), mais c'est la même chose que dans foo - des appels différents à bar avec différents arguments créent des valeurs différentes de y (Rappelez-vous - les fonctions sont des valeurs) qui sont toutes immuables afin que la pureté reste intacte.

Aussi, veuillez noter que les fermetures ont un effet très similaire pour le currying:

adder(a)(b) {
    return a + b
}
baz(x) {
    let y = adder(x)
    ...
}

baz n'est pas vraiment différent de bar - dans les deux nous créons une valeur de fonction nommée y qui renvoie son argument plus x. En matière de fait, dans la Lambda Calculus, vous utilisez des fermetures pour créer des fonctions avec plusieurs arguments - et il n'est toujours pas impur.

10
Idan Arye

Normalement, je vous demanderais de clarifier votre définition de "impure", mais dans ce cas, cela n'a pas vraiment d'importance. En supposant que vous contrastez le terme purement fonctionnel , la réponse est "non", car il n'y a rien sur les fermetures intrinsèquement destructrices. Si votre langue était purement fonctionnelle sans fermeture, il serait toujours purement fonctionnel avec les fermetures. Si au lieu de cela, vous voulez dire "non fonctionnel", la réponse est toujours "non"; Les fermetures facilitent la création de fonctions.

Il semble que l'on puisse généralement éviter les fermetures en passant directement des données à une fonction.

Oui, mais votre fonction aurait un autre paramètre, et cela changerait son type. Les fermetures vous permettent de créer des fonctions basées sur des variables sans Ajout de paramètres. Ceci est utile lorsque vous avez, disons, une fonction qui prend 2 arguments et que vous souhaitez créer une version qui ne prend qu'un argument.

Edit: En ce qui concerne votre propre édition/exemple ...

Supposer

f: x -> x + y

f (3) ne donnera pas toujours le même résultat. f(3) dépend de la valeur de y qui n'est pas un argument de f. Ainsi, f n'est pas une fonction pure.

dépend est le mauvais choix de mot ici. Citant le même article Wikipedia que vous avez fait:

Dans la programmation informatique, une fonction peut être décrite comme une fonction pure si ces deux déclarations concernant la fonction contiennent:

  1. La fonction évalue toujours la même valeur de résultat compte tenu de la même valeur d'argument. La valeur de résultat de la fonction ne peut dépendre d'aucune information cachée ou d'état pouvant changer à titre d'exécution de programme ou entre les exécutions différentes du programme, ni dépendre de toute entrée externe des périphériques d'E/S.
  2. L'évaluation du résultat ne provoque pas d'effet secondaire ou de sortie sémantiquement observable, telle que la mutation d'objets mutables ou de sortie sur des périphériques d'E/S.

En supposant que y est immuable (qui est généralement le cas dans les langages fonctionnels), la condition 1 est satisfaite: pour toutes les valeurs de x, la valeur de f(x) ne change pas . Cela devrait être clair du fait que y n'est pas différent d'une constante et x + 3 est pur. Il est également clair qu'il n'y a pas de mutation ni d'E/S.

5
Doval

Très rapidement: une substitution est "transparente de manière référentielle" si "substituer comme des amortisseurs similaires" et une fonction est "pure" si tous ses effets sont contenus dans sa valeur de retour. Les deux peuvent être rendus précis, mais il est essentiel de noter qu'elles ne sont pas identiques ni même l'une implique l'autre.

Parlons maintenant de fermetures.

Ennuyeux (principalement pur) "fermetures"

Les fermetures se produisent car, comme nous évaluons un terme Lambda, nous interprétons des variables (liées) comme des recherches d'environnement. Ainsi, lorsque nous retournons un terme de Lambda à la suite d'une évaluation des variables à l'intérieur qu'elle aura "fermé sur" les valeurs qu'ils ont prise lorsqu'il a été défini.

Dans le calcul de Lambda uni, c'est une sorte de trivial et toute la notion disparaît. Pour démontrer cela, voici un interpréteur de calcul Lambda relativement léger:

-- untyped lambda calculus values are functions
data Value = FunVal (Value -> Value)

-- we write expressions where variables take string-based names, but we'll
-- also just assume that nobody ever shadows names to avoid having to do
-- capture-avoiding substitutions

type Name = String

data Expr
  = Var Name
  | App Expr Expr
  | Abs Name Expr

-- We model the environment as function from strings to values, 
-- notably ignoring any kind of smooth lookup failures
type Env = Name -> Value

-- The empty environment
env0 :: Env
env0 _ = error "Nope!"

-- Augmenting the environment with a value, "closing over" it!
addEnv :: Name -> Value -> Env -> Env
addEnv nm v e nm' | nm' == nm = v
                  | otherwise = e nm

-- And finally the interpreter itself
interp :: Env -> Expr -> Value
interp e (Var name) = e name          -- variable lookup in the env
interp e (App ef ex) =
  let FunVal f = interp e ef
      x        = interp e ex
  in f x                              -- application to lambda terms
interp e (Abs name expr) =
  -- augmentation of a local (lexical) environment
  FunVal (\value -> interp (addEnv name value e) expr)

La partie importante à noter est dans addEnv lorsque nous augmentons l'environnement avec un nouveau nom. Cette fonction s'appelle uniquement "à l'intérieur" du terme interprété Abs traction (terme Lambda). L'environnement devient "levé" chaque fois que nous évaluons un terme Var et que celles-ci Var s résolve à quel que soit le Name mentionné dans le Env qui a obtenu capturé par la traction Abs contenant le Var.

Maintenant, encore une fois, dans des termes simples LC cela est ennuyeux. Cela signifie que les variables liées ne sont que des constantes aussi loin que quiconque se soucie. Ils sont évalués directement et immédiatement comme les valeurs qu'ils indiquent dans l'environnement comme étiquette lexiquement à ce point.

C'est aussi (presque) pur. La seule signification de tout terme de notre calcul de Lambda est déterminée par sa valeur de retour. La seule exception est l'effet secondaire de la non-résiliation qui est incorporée par le terme oméga:

-- in simple LC syntax:
--
-- (\x -> (x x)) (\x -> (x x))
omega :: Expr
omega = App (Abs "x" (App (Var "x") 
                          (Var "x")))
            (Abs "x" (App (Var "x") 
                          (Var "x")))

Fermetures intéressantes (impures)

Maintenant, à certains arrière-plans, les fermetures décrites dans la plaine LC ci-dessus sont ennuyeuses car il n'ya aucune notion de pouvoir interagir avec les variables que nous avons fermées. En particulier, le mot "fermeture" a tendance à invoquer le code comme le JavaScript suivant

> function mk_counter() {
  var n = 0;
  return function incr() {
    return n += 1;
  }
}
undefined

> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3

Cela démontre que nous avons fermé la variable n dans la fonction interne incr et appeler incr interagit de manière significative avec cette variable. mk_counter Est pur, mais incr est décidément impur (et non transparent de manière transparente).

Qu'est-ce qui diffère entre ces deux cas?

Notions de "variable"

Si nous examinons ce que la substitution et l'abstraction signifient dans la plaine LC sens, nous remarquons qu'ils sont décidément simples. Les variables sont littéralement rien Plus que des recherches d'environnement immédiat. L'abstraction Lambda est littéralement rien Plus que de créer un environnement augmenté pour évaluer l'expression intérieure. Il n'y a pas de place dans ce modèle pour le type de comportement que nous avons vu avec mk_counter/incr car il n'y a pas de variation autorisée.

Pour beaucoup, c'est le cœur de la variable "variable"-variation. Cependant, les séminants aiment distinguer le type de variable utilisée dans LC et le type de "variable" utilisé dans JavaScript. Pour ce faire, ils ont tendance à appeler cette dernière une "cellule mutable" ou "emplacement".

Cette nomenclature suit la longue utilisation historique de la "variable" en mathématiques où elle signifiait quelque chose de plus comme "inconnu": l'expression (mathématique) "x + x" ne permet pas x de varier au fil du temps, mais est censé avoir un sens quelle que soit la valeur (simple, constante) x prend.

Ainsi, nous disons "emplacement" pour souligner la capacité de mettre des valeurs dans une fente et de les sortir.

Pour ajouter plus loin à la confusion, dans JavaScript, ces "emplacements" ressemblent à la même chose que les variables: nous écrivons

var x;

pour en créer un, puis quand nous écrivons

x;

cela nous indique que nous recherchons la valeur actuellement stockée dans cette fente. Pour que cela soit plus clair, les langues pures ont tendance à penser aux fentes comme des noms (mathématiques, de calcul) de Lambda). Dans ce cas, nous devons explicitement étiqueter quand nous obtenons ou mis à partir d'une fente. Cette notation a tendance à ressembler à

-- create a fresh, empty slot and name it `x` in the context of the 
-- expression E
let x = newSlot in E

-- look up the value stored in the named slot named `x`, return that value
get x

-- store a new value, `v`, in the slot named `x`, return the slot
put x v

L'avantage de cette notation est que nous avons maintenant une distinction ferme entre les variables mathématiques et les machines à sous mutables. Les variables peuvent prendre des fentes comme leurs valeurs, mais le logement particulier nommé par une variable est constant tout au long de sa portée.

Utilisation de cette notation, nous pouvons réécrire l'exemple mk_counter (Cette fois dans une syntaxe de type HASKELL, bien que résolument une sémantique de type ONU-HASKELL):

mkCounter = 
  let x = newSlot 
  in (\() -> let old = get x 
             in get (put x (old + 1)))

Dans ce cas, nous utilisons des procédures qui manipulent cette fente mutable. Afin de la mettre en œuvre, nous devrions fermer plus seulement un environnement constant de noms comme x mais également un environnement mutable contenant toutes les machines à sous requises. Ceci est plus proche de la notion commune de "fermeture" que les gens aiment tellement.

Encore une fois, mkCounter est très impur. C'est aussi très référentiellement opaque. Mais remarquez que les effets secondaires ne découlent pas de la capture ou de la fermeture du nom, mais la capture de la cellule mutable et des opérations de passage latéral sur celui-ci comme get et put.

En fin de compte, je pense que c'est la réponse finale à votre question: la pureté n'est pas affectée par la capture variable (mathématique) mais à la place des opérations de réducteur latérales effectuées sur des emplacements mutables nommés par des variables capturées.

Ce n'est qu'en langues qui ne tentent pas d'être proche de LC ou de ne pas tenter de maintenir la pureté que ces deux concepts sont si souvent confondus menant à la confusion.

3
J. Abrahamson

Non, les fermetures ne causent pas une fonction impure, tant que la valeur fermée est constante (ni modifiée par la fermeture ni autre code), qui est le cas habituel dans la programmation fonctionnelle.

Notez que, même si vous pouvez toujours transmettre une valeur comme un argument à la place, vous ne pouvez généralement pas le faire sans difficulté considérable. Par exemple (Coffetscript):

closedValue = 42
return (arg) -> console.log "#{closedValue} #{arg}"

Par votre suggestion, vous pourriez simplement revenir:

return (arg, closedValue) -> console.log "#{closedValue} #{arg}"

Cette fonction n'est pas appelée à ce stade, juste défini, vous devez donc trouver un moyen de réussir votre valeur souhaitée pour closedValue à le point à laquelle la fonction est réellement appelée. Au mieux cela crée beaucoup d'accouplement. Au pire, vous ne contrôlez pas le code au point d'appel, il est donc effectivement impossible.

Les bibliothèques d'événements dans des langues qui ne prennent pas en charge les fermetures fournissent généralement une autre façon de passer des données arbitraires à la rappel, mais ce n'est pas joli et crée beaucoup de complexité tant pour le responsable de la bibliothèque que pour les utilisateurs de la bibliothèque.

1
Karl Bielefeldt