En estos años en los que los lenguajes funcionales están empezando a convertirse en alternativas reales en casi todos los ámbitos, uno de los conceptos que suele venir asociado a ellos es el de la inmutabilidad.
Si bien es cierto que la inmutabilidad es una idea que no es exclusiva de la programación funcional, sí que cobra una importancia vital en este tipo de lenguajes.
La programación funcional se asienta sobre muchos conceptos matemáticos que requieren de la inmutabilidad para seguir siendo válidos.
Aún así, creo que es un concepto que es interesante de entender independientemente del tipo del paradigma de programación que utilices.
¿Qué es la inmutabilidad?
La idea es muy sencilla de entender: algo es inmutable cuando no se puede modificar.
En el contexto de la programación, una variable es inmutable cuando su valor no se puede modificar. Y un objeto lo es cuando su estado no puede ser actualizado tras la creación del objeto.
Es por tanto una forma de asegurarnos que nuestro objeto no se modifica en lugares inesperados, afectando con ello la ejecución de nuestro programa.
¿Dónde aplica la inmutabilidad?
La inmutabilidad simplifica mucho el tratamiento de la concurrencia
Fuera de la programación puramente funcional, donde ya comentaba que su necesidad es más estricta, la inmutabilidad nos puede ahorrar muchos quebraderos de cabeza.
Imaginemos por ejemplo que estamos trabajando en Java, y tenemos una clase con esta forma:
public class Person {
public String name;
public String surname;
public Person(String name, String surname) {
this.name = name;
this.surname = surname;
}
}
Aunque en Java se usarían getters y setters normalmente, lo podemos dejar así por simplificar.
Si ahora en cualquier punto de la aplicación tenemos una función que utiliza un objeto de tipo Person, éste puede ser modificado desde dentro de ella sin ningún problema:
public void doSomething(Person person) {
person.name = "";
}
En un ejemplo tan simple puede no tener sentido, e incluso hay formas de minimizar los problemas con reglas estrictas sobre cuándo se puede modificar un objeto y cuándo no.
Pero al final la opción está ahí, y cuando los programas se vuelven grandes y aparecen las prisas, se acaban cometiendo violaciones de esas reglas.
En un punto donde genera muchas ventajas es en las aplicaciones multihilo, donde la inmutabilidad simplifica mucho el tratamiento de la concurrencia. Si algo no se puede modificar, da igual que se acceda a ello desde distintos hilos a la vez, así como el orden en que se haga.
La inmutabilidad, en general, hace nuestro código mucho más predecible, porque somos más conscientes de dónde se producen los cambios de estado.
¿Cómo se consigue la inmutabilidad?
En los lenguajes preparados para ello, es bastante sencillo, pues la propia sintaxis nos simplifica mucho la tarea. En lenguajes como Scala podemos hacer una variable inmutable simplemente usando val
en lugar de var
para declararla:
val p = new Person("name", "surname")
En Java tampoco es mucho más complicado. Es simplemente añadir la palabra final
a lo que queramos hacer inmutable. Si por ejemplo, queremos hacer la clase Person
inmutable, sólo tendríamos que hacer lo siguiente:
public final class Person {
public final String name;
public final String surname;
public Person(String name, String surname) {
this.name = name;
this.surname = surname;
}
}
De esta forma, ya podemos estar seguros de que el estado de este objeto no se modificará. Un poco más verboso, pero sencillo ¿verdad?
Los problemas surgen cuando nos encontramos con situaciones más complejas. Como la API de Java no es inmutable, cualquier uso que hagamos de ella es susceptible de violar la inmutabilidad.
Pongamos por ejemplo que la persona guarda un listado de personas que son sus hijos:
public final class Person {
public final String name;
public final String surname;
public final List children;
public Person(String name, String surname, List children) {
this.name = name;
this.surname = surname;
this.children = children;
}
}
Por mucho que hagamos la lista inmutable, su estado es mutable, por lo que podemos hacer:
person.children.add(new Person("n1", "s1"));
Con lo que nuevamente tenemos el mismo problema. Hay muchas formas de solucionar esto, como hacer children
privado y permitir el acceso a sus hijos mediante un método, utilizar las unmodifiable collections del SDK o alguna librería que ofrezca colecciones inmutables como Guava. O simplemente usar un array.
En resumen, si quieres asegurarte de que algo es inmutable, tienes que estar seguro de que los estados de los objetos que utiliza esa clase para su propio estado también lo son.
¿Cómo se modifica el estado en un entorno inmutable?
Puede que te estés haciendo esta pregunta. Si todo es inmutable, no hay cambios de estado, y por tanto el programa no hará nada.
La forma de modificar un objeto inmutable es simplemente copiarlo modificando los valores necesarios. Nuevamente, los lenguajes que prestan atención a la inmutabilidad tienen mecanismos para hacer esto sencillo. Por ejemplo si tenemos una data class en Kotlin:
data class Person(val name: String, val surname: String)
El lenguaje ya nos provee de una función copy
que hará el trabajo duro por nosotros:
val p1 = Person("p1", "s1")
val p2 = p1.copy(surname = "s2")
En Java sería un poco más complejo, pero tampoco tiene mucha historia:
Person p1 = new Person("p1", "s1");
Person p2 = new Person(p1.name, "s2");
Las listas inmutables funcionan igual. Si añadimos un elemento a una lista, en realidad se nos devuelve otra lista que tiene todos los objetos de la anterior más el nuevo.
¿Merece la pena utilizar inmutabilidad siempre?
Si estás desarrollando con el paradigma de orientación a objetos, hay veces que seguramente no merezca la pena. La inmutabilidad tiene el sobrecoste de la generación de objetos nuevos cada vez que cambia el estado, así que esto puede penalizar bastante en el rendimiento.
Así como guía muy general, es recomendable usar objetos mutables en los siguientes casos:
- En objetos con estados muy grandes, cuando la copia de un objeto pueda suponer un tiempo de ejecución considerable.
- Objetos que necesitan ser construidos poco a poco, porque no podamos obtener todo el estado de golpe.
- Mantener el estado de ejecución
Y en general, cualquier situación que requiera de un estado que se modifica a menudo y/o la duplicación de ese estado sea costosa.
La inmutabilidad, por el contrario, presenta ventajas en las siguientes situaciones:
- En objetos que no vayan a requerir modificaciones de estado
- Objetos que sean simples de duplicar, incluso aunque se modifiquen de forma esporádica
- En situaciones de concurrencia
Como comentaba antes, la inmutabilidad hace que el código sea mucho más predecible y más fácil de testear, porque acotamos mucho más los lugares donde se producen modificaciones de estado.
Así que una regla simple puede ser la de hacer inmutable todo el código que sea posible.
Conclusiones
La inmutabilidad es un concepto muy potente que nos puede ayudar a simplificar la complejidad de comprensión de nuestro código y, por tanto, a disminuir las probabilidades de que se produzcan errores inesperados.
Aunque en programación orientada a objetos no sea tan imprescindible como en funcional, es interesante conocer esta idea para aplicarla donde más útil nos pueda parecer.
¿Conocías este concepto? ¿Lo usas habitualmente? Y si no, ¿te parece interesante para aplicarlo en tu día a día?