jueves, 29 de enero de 2009

Lecciones funcionales para imperativos (1): Transparencia referencial

La transparencia referencial es una propiedad de las funciones. Aplica a toda función matemática, y las de los lenguajes de programación funcional pura. Sin embargo no es una noción ajena a la programación imperativa u orientada a objetos: podemos definir métodos como funciones con transparencia referencial, o "funciones puras". Esta es, quizás, la lección más básica que podemos tomar de la programación funcional, para aplicar en programación imperativa.

Una función tiene transparencia referencial si su resultado depende única y exclusivamente de los parámetros provistos, y no produce efectos colaterales. La implicancia de esto es que, dado un juego de parámetros, no importa cuántas veces llamemos a la función ni desde dónde, el resultado será siempre el mismo.

¿De qué otras cosas podría depender una función que de sus parámetros? De la fecha y hora del sistema, de globales, de valores aleatorios o ingresados por el usuario. En un lenguaje OO, un método también puede depender de atributos mutables, y de métodos sin transparencia referencial, del mismo objeto o de otros. Recordemos además que los singletons y los elementos static son globales.


// Funciones con transparencia referencial

public String capitalize(String x) {
return x.substring(0,1).toUpperCase() + x.substring(1);
}
public int foo(int a, int b) {
int r;
if(a % 2 == 0)
r = b + a * 2;
else
r = b * 5;
return r;
}
public float pi() {
return 3.14;
}


Efectos colaterales

Los efectos colaterales son cualquier cambio en el estado del sistema, como podría ser: modificar globales u otros elementos, y operaciones de salida a dispositivos, incluyendo la pantalla. Cualquier cosa que cambie en el "mundo exterior" a la función es un efecto colateral: una función con transparencia referencial no genera otra novedad en el mundo que el cómputo de su resultado.
Ejemplos de efectos colaterales:


public Dog createDog(List<animal> animals) {
Dog dog = new Dog();
animals.add(dog); // altera una estructura que existe fuera de la función
return dog;
}

Este método produce un efecto colateral al mutar el estado del objeto recibido por parámetro.


public int foo(int a) {
this.someAttr += a; // altera un atributo del mismo objeto
return a > 0? a * a : a;
}

Este otro método es determinístico en su resultado, pero al mutar un atributo, el estado del objeto no será igual cuando se llame al método varias veces con el mismo parámetro.


public int sum(int a, int b) {
System.out.println("sum"); // genera salida por pantalla
return a + b;
}

Este caso parece particularmente inocuo. Es como que para el programa da igual si se imprimió "sum" o no. Sin embargo, consideremos los siguientes programas:


// Programa #1
int x = sum(1, 1);
System.out.println(x);


// Programa #2
int x = sum(1, 1);
x = sum(1, 1);
System.out.println(x);


El resultado de ejecutar uno y el otro es diferente, no es lo mismo imprimir algo una vez que dos veces. La pantalla es parte del "mundo exterior" sobre el que actuó la función.
La propuesta de hacer funciones sin efectos colaterales puede sonar ridícula, y de hecho no puede haber un programa útil sin efectos colaterales. La idea es que estos efectos estén controlados. Cuando hacemos una función, conviene plantear si los efectos colaterales son necesarios, y si no lo son, quizás sea candidata a tener transparencia referencial.
Si determinamos que cierto efecto colateral es necesario, igualmente conviene en lo posible cumplir con el otro aspecto de la transparencia referencial: que el resultado dependa sólo de los parámetros.

La transparencia referencial ayuda a que el código sea más claro y testeable.

Los efectos colaterales tienen mayor sentido en los procedimientos. A los procedimientos se los llama para hacer algo, para cambiar el estado del sistema, para producir un efecto; en cambio una función es un valor.
Los programas en lenguajes de programación funcional consisten en una composición de funciones. En lenguajes imperativos, podemos tener módulos basados en composición de funciones puras, aunque el programa sea impuro. Todo lo que se pueda acotar con una implementación pura será más mantenible y menos proclive a bugs.

No hay comentarios:

Publicar un comentario