viernes, 30 de enero de 2009

Lecciones funcionales para imperativos (2): Inmutabilidad

Se puede objetar que la definición de funciones puras en un lenguaje orientado a objetos sólo aplicaría a métodos static, de los que ponemos en clases de "utilitarios". Y los static no son nada representativos de lo que es el paradigma de objetos. Esta observación parece indicar que el paradigma funcional no se lleva bien con el de objetos, y que cualquier intento de fusión sería forzado y torpe.
Las clases más elementales de una aplicación típicamente definen getters y setters: el clásico "JavaBean". Los getters no son puros porque pueden retornar algo diferente cada vez. Los setters producen efectos colaterales al cambiar el estado del objeto.
Habíamos dicho que las funciones puras arrojan un resultado que depende solamente de los parámetros. Como los métodos de instancia se ejecutan en el contexto de un objeto, siempre tienen disponible su referencia, y podemos entender a esa referencia como un parámetro implícito. ¿Por qué, entonces, si un simple getter sólo depende del estado del parámetro implícito (el objeto al que pertenece), no tiene transparencia referencial?
El motivo es que la mutabilidad del objeto conspira contra este fin. Para que un getter tenga transparencia referencial, es necesario que el atributo retornado no tenga posibilidad de cambiar: que su valor inicial permanezca durante la vida del objeto. Por ende no debería existir el correspondiente setter:


class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() { return this.name; }
public int getAge() { return this.age; }
}


De hecho, en la programación funcional pura no existen las variables. Todo valor es inmutable. En Java es fácil crear un primitivo inmutable, para eso utilizamos la palabra clave final. Pero para hacer inmutable un objeto, eso no alcanza: final garantiza que la referencia no apuntará a otro objeto, pero no impide que mute el estado del objeto referenciado. Para lograr esto tenemos que seguir el patrón del ejemplo de arriba.
Para quien haya programado siempre en lenguajes imperativos, la idea de trabajar sin mutabilidad puede parecer un sinsentido. Pero pensemos en algo de uso corriente: la clase java.lang.String es inmutable. En Java no podemos modificar un objeto String, y sin embargo podemos utilizar esta clase sin problemas.


String s = " test ";
s.trim();


En este ejemplo vemos un ingenuo intento de modificar la variable s aplicando el método trim(). Los que alguna vez cometimos este error, aprendimos que los métodos de String retornan una referencia a un nuevo objeto, el resultado del método. Si no guardamos la referencia, de nada sirvió invocar al método. Java no nos advierte de esta situación porque sus diseñadores siguieron el criterio de C y C++: se considera válido invocar a un método y descartar su resultado, porque se asume que se lo quiso llamar como procedimiento, y que sólo nos interesan los efectos colaterales.
Otro ejemplo interesante de uso intensivo de inmutabilidad es la librería Joda Time.
Entonces, resumiendo, lo que normalmente hacemos con mutabilidad, lo podemos lograr invocando funciones que producen un nuevo resultado que consiste en una transformación del valor original.

Un objeto inmutable es, por definición, thread-safe. La inmutabilidad nos da thread-safety en un sentido absoluto, sin depender de un mecanismo obtuso como el de la "sincronización".

Volviendo al ejemplo de la clase Person, ¿cómo definiríamos un método equivalente a modificar la edad de una persona?


public Person withAge(int newAge) { return new Person(this.name, newAge); }


Nada más simple.
El objetivo no es aplicar el paradigma funcional indiscriminadamente en un lenguaje como Java, porque no está en su naturaleza. Pero sí se puede tomar conceptos útiles y aplicarlos hasta donde sea razonable. Por ejemplo, podemos hacer una función con transparencia referencial cuya implementación usa variables:


public int sumChars(String s) {
int a = 0;
for(int i = 0; i < s.length(); i++)
{
a += s.charAt(i);
}
return a;
}


En un caso así no hay efectos colaterales, toda la mutabilidad está manejada en variables locales.

No hay comentarios:

Publicar un comentario