web-dev-qa-db-fra.com

Qu'est-ce que la transparence référentielle?

J'ai vu cela dans des paradigmes impératifs

f(x)+f(x)

pourrait ne pas être identique à:

2 * f (x)

Mais dans un paradigme fonctionnel, il devrait en être de même. J'ai essayé d'implémenter les deux cas dans Python et Scheme , mais pour moi, ils sont assez simples.

Quel serait un exemple qui pourrait souligner la différence avec la fonction donnée?

38
asgard

La transparence référentielle, référée à une fonction, indique que vous ne pouvez déterminer le résultat de l'application de cette fonction qu'en regardant les valeurs de ses arguments. Vous pouvez écrire des fonctions référentiellement transparentes dans n'importe quel langage de programmation, par ex. Python, Scheme, Pascal, C.

D'un autre côté, dans la plupart des langues, vous pouvez également écrire des fonctions transparentes non référentielles. Par exemple, cette fonction Python:

counter = 0

def foo(x):
  global counter

  counter += 1
  return x + counter

n'est pas référentiellement transparent, en fait appeler

foo(x) + foo(x)

et

2 * foo(x)

produira des valeurs différentes, pour tout argument x. La raison en est que la fonction utilise et modifie une variable globale, donc le résultat de chaque appel dépend de cet état changeant, et pas seulement de l'argument de la fonction.

Haskell, un langage purement fonctionnel, sépare strictement l'évaluation des expressions dans laquelle des fonctions pures sont appliquées et qui est toujours référentiellement transparente, à partir de exécution d'action (traitement de valeurs spéciales), qui n'est pas référentiellement transparente, c'est-à-dire que l'exécution de la même action peut avoir à chaque fois un résultat différent.

Donc, pour toute fonction Haskell

f :: Int -> Int

et tout entier x, il est toujours vrai que

2 * (f x) == (f x) + (f x)

Un exemple d'action est le résultat de la fonction de bibliothèque getLine:

getLine :: IO String

Suite à l'évaluation de l'expression, cette fonction (en fait une constante) produit tout d'abord une valeur pure de type IO String. Les valeurs de ce type sont des valeurs comme les autres: vous pouvez les transmettre, les placer dans des structures de données, les composer à l'aide de fonctions spéciales, etc. Par exemple, vous pouvez faire une liste d'actions comme ceci:

[getLine, getLine] :: [IO String]

Les actions sont spéciales en ce que vous pouvez dire au runtime Haskell de les exécuter en écrivant:

main = <some action>

Dans ce cas, lorsque votre programme Haskell est démarré, le runtime parcourt l'action liée à main et l'exécute , éventuellement en produisant side -effets. Par conséquent, l'exécution d'une action n'est pas référentiellement transparente car l'exécution de la même action deux fois peut produire des résultats différents selon ce que le runtime obtient en entrée.

Grâce au système de types de Haskell, une action ne peut jamais être utilisée dans un contexte où un autre type est attendu, et vice versa. Donc, si vous voulez trouver la longueur d'une chaîne, vous pouvez utiliser la fonction length:

length "Hello"

renverra 5. Mais si vous voulez trouver la longueur d'une chaîne lue depuis le terminal, vous ne pouvez pas écrire

length (getLine)

car vous obtenez une erreur de type: length attend une entrée de type list (et une chaîne est, en effet, une liste) mais getLine est une valeur de type IO String (un action). De cette façon, le système de types garantit qu'une valeur d'action comme getLine (dont l'exécution est effectuée en dehors du langage principal et qui peut être transparente de manière non référentielle) ne peut pas être masquée à l'intérieur d'une valeur de non-action de type Int.

MODIFIER

Pour répondre à la question exizt, voici un petit programme Haskell qui lit une ligne depuis la console et imprime sa longueur.

main :: IO () -- The main program is an action of type IO ()
main = do
          line <- getLine
          putStrLn (show (length line))

L'action principale se compose de deux sous-actions qui sont exécutées séquentiellement:

  1. getline de type IO String,
  2. le second est construit en évaluant la fonction putStrLn de type String -> IO () sur son argument.

Plus précisément, la deuxième action est construite par

  1. lier line à la valeur lue par la première action,
  2. évaluer les fonctions pures length (calculer la longueur sous forme d'entier) puis show (transformer l'entier en chaîne),
  3. construction de l'action en appliquant la fonction putStrLn au résultat de show.

À ce stade, la deuxième action peut être exécutée. Si vous avez tapé "Bonjour", il imprimera "5".

Notez que si vous obtenez une valeur d'une action en utilisant la notation <-, Vous ne pouvez utiliser cette valeur que dans une autre action, par exemple vous ne pouvez pas écrire:

main = do
          line <- getLine
          show (length line) -- Error:
                             -- Expected type: IO ()
                             --   Actual type: String

parce que show (length line) a le type String alors que la notation do requiert qu'une action (getLine de type IO String) soit suivie d'une autre action (par exemple putStrLn (show (length line)) de type IO ()).

MODIFIER 2

La définition de Jörg W Mittag de la transparence référentielle est plus générale que la mienne (j'ai surévalué sa réponse). J'ai utilisé une définition restreinte parce que l'exemple de la question se concentre sur la valeur de retour des fonctions et je voulais illustrer cet aspect. Cependant, RT en général fait référence à la signification de l'ensemble du programme, y compris les changements d'état global et les interactions avec l'environnement (IO) provoqués par l'évaluation d'une expression. Donc, pour une définition générale correcte , vous devez vous référer à cette réponse.

62
Giorgio
def f(x): return x()

from random import random
f(random) + f(random) == 2*f(random)
# => False

Cependant, c'est pas ce que signifie la transparence référentielle. RT signifie que vous pouvez remplacer l'expression any dans le programme avec le résultat de l'évaluation de cette expression (ou vice versa) sans changer la signification du programme.

Prenons, par exemple, le programme suivant:

def f(): return 2

print(f() + f())
print(2)

Ce programme est référentiellement transparent. Je peux remplacer une ou les deux occurrences de f() par 2 Et cela fonctionnera toujours de la même façon:

def f(): return 2

print(2 + f())
print(2)

ou

def f(): return 2

print(f() + 2)
print(2)

ou

def f(): return 2

print(2 + 2)
print(f())

se comporteront tous de la même manière.

Eh bien, en fait, j'ai triché. Je devrais être en mesure de remplacer l'appel à print par sa valeur de retour (qui n'est pas du tout une valeur) sans changer la signification du programme. Cependant, clairement, si je supprime simplement les deux instructions print, la signification du programme changera: avant, il imprimait quelque chose à l'écran, après il ne le faisait pas. Les E/S ne sont pas référentiellement transparentes.

La règle générale est la suivante: si vous pouvez remplacer n'importe quelle expression, sous-expression ou appel de sous-programme par la valeur de retour de cette expression, sous-expression ou appel de sous-programme n'importe où dans le programme, sans que le programme change sa signification, alors vous avez un référentiel transparence. Et ce que cela signifie, pratiquement, c'est que vous ne pouvez pas avoir d'E/S, ne pouvez pas avoir d'état mutable, ne pouvez pas avoir d'effets secondaires. Dans chaque expression, la valeur de l'expression doit dépendre uniquement des valeurs des parties constituantes de l'expression. Et dans chaque appel de sous-routine, la valeur de retour doit dépendre uniquement des arguments.

25
Jörg W Mittag

Certaines parties de cette réponse sont tirées directement d'un tutoriel inachevé sur la programmation fonctionnelle , hébergé sur mon compte GitHub:

Une fonction est dite référentiellement transparente si, étant donné les mêmes paramètres d'entrée, produit toujours la même sortie (valeur de retour). Si l'on cherche une raison d'être pour une programmation fonctionnelle pure, la transparence référentielle est un bon candidat. Lors du raisonnement avec des formules en algèbre, arithmétique et logique, cette propriété - également appelée substitutivité d'égaux pour égaux - est si fondamentalement importante qu'elle est généralement considérée comme acquise ...

Prenons un exemple simple:

x = 42

Dans un langage purement fonctionnel, le côté gauche et le côté droit du signe égal sont substituables l'un à l'autre dans les deux sens. Autrement dit, contrairement à un langage comme C, la notation ci-dessus affirme vraiment une égalité. Une conséquence de cela est que nous pouvons raisonner sur le code de programme tout comme les équations mathématiques.

De wiki Haskell :

Les calculs purs donnent la même valeur chaque fois qu'ils sont invoqués. Cette propriété est appelée transparence référentielle et permet de conduire un raisonnement équationnel sur le code ...

Pour contraster cela, le type d'opération effectué par les langages de type C est parfois appelé affectation destructrice .

Le terme pur est souvent utilisé pour décrire une propriété d'expressions, pertinente pour cette discussion. Pour qu'une fonction soit considérée comme pure,

  • il ne doit pas présenter d'effets secondaires, et
  • il doit être référentiellement transparent.

Selon la métaphore de la boîte noire, trouvée dans de nombreux manuels mathématiques, les fonctions internes d'une fonction sont complètement isolées du monde extérieur. Un effet secondaire est lorsqu'une fonction ou une expression viole ce principe - c'est-à-dire que la procédure est autorisée à communiquer d'une manière ou d'une autre avec d'autres unités de programme (par exemple pour partager et échanger des informations).

En résumé, la transparence référentielle est indispensable pour que les fonctions se comportent comme true , les fonctions mathématiques également dans la sémantique des langages de programmation.

1
yesthisisuser