La inmutabilidad es un término cuya popularidad ha crecido de forma considerable durante los últimos años, especialmente en consonancia con el crecimiento del paradigma de programación funcional. Sin embargo, hablar de inmutabilidad en Go es algo que genera controversia, pues en esta ocasión veremos como el lenguaje nos proporciona pocos recursos para llevarla a cabo. Pero, antes de eso, lo mejor será que hagamos un breve repaso de qué es la inmutabilidad y qué beneficios conlleva.
Inmutabilidad
La inmutabilidad es, básicamente, la propiedad o capacidad de una variable o estructura de datos de permanecer inmutable durante la ejecución del programa, es decir, que esta no pueda ser modificada. En el contexto de Go, si partimos del siguiente ejemplo:
n := 4
s := struct{ text string }{text: "Hello World"}
Diríamos que nuestra variable n
es inmutable si en el hilo de ejecución del programa dicha variable no pudiera cambiar
de valor. De otro modo, podríamos decir que los structs son inmutables si en el hilo de ejecución nunca pudiéramos
modificar el valor de sus atributos, en este caso del atributo text
.
Como podemos ver, la inmutabilidad la podemos tener a dos niveles:
- a nivel de variable, en cuyo caso ni
n
nis
podrían ser modificadas de ningún modo. - a nivel de estructura de datos, en cuyo caso no podríamos modificar los datos referenciados por
s
, pero sí que podríamos modificar la referencia a la que apuntas
.
s := struct{ text string }{text: "Hello World"}
// no sería posible
s.text = "Goodbye World"
// sí sería posible
s = struct{ text string }{text: "Goodbye World"}
Una vez repasado el concepto, para el propósito de este artículo, vamos a centrarnos en la inmutabilidad de las estructuras de datos (que no de las variables), que suele ser lo más común. Entonces, veamos cuáles son los pros y los contras de las estructuras de datos con esta propiedad.
Pros y contras
En cuánto a los beneficios, podríamos enumerar los siguientes:
Seguridad ante accesos concurrentes
El hecho de que una estructura de datos no pueda ser modificada nos va a evitar tener determinados problemas de sincronización entre diferentes gorrutinas, ya que, el hecho de que una u otra gorrutina acceda a dichos datos no será un problema, ya que estos accesos serán siempre de lectura y, en caso de necesidad, serán copiados y modificados en el contexto local de cada una de ellas.
Evitar estados inválidos
Evidentemente, que una estructura de datos sea inmutable no nos va a impedir inicializarla con un estado inconsistente o inválido, pero lo que sí es cierto es que una vez hayamos comprobado dicha inicialización, no tendremos que volver a preocuparnos más. Lo cuál sí sucedería en caso de estar tratando con una estructura de datos mutable.
Una mejor encapsulación
¿Quién no ha intentado seguir el flujo de ejecución de un programa escrito en Ruby o Java (o similar) y ha sido incapaz de determinar si un objeto era modificado en una de las múltiples llamadas a funciones con éste como parámetro?
Por suerte o por desgracia, esta (frustrante) sensación es más habitual de lo que debería, lo que lleva a los desarrolladores a realizar lo que se conoce como “copia defensiva”: es decir, a copiar un objeto antes de pasarlo como argumento de una función por el miedo a que este pueda ser modificado en el interior de la misma.
Esto desaparece con la presencia de estructuras de datos inmutables, ya que en ese caso, ya podremos pasar una referencia a nuestros objetos que éstos nunca serán modificados.
Más fácil de testear
Derivado del punto anterior, también vamos a ver como, al tratar con estructuras de datos inmutables, nuestro código será mucho más fácil de testear. Nuestras funciones serán más puras, tendrán menos efectos colaterales, y por consiguiente, nos será mucho más fácil centrarnos en la salida esperada de las mismas dado una entrada determinada.
Más legible y más fácil de mantener
Finalmente, y como consecuencia de los dos puntos anteriores, nuestro código resultará mucho más legible, y por lo tanto, mucho más fácil de mantener, pues este dejará de lado las posibles confusiones para convertirse en un código más predecible.
Sin embargo, y por desgracia, cuándo empezamos a trabajar con estructuras de datos inmutables, no todo son beneficios. De hecho, resulta evidente pensar que eso así sea, sinó todos los lenguajes dispondrían de estructuras de datos inmutables y todas las estructuras de datos lo serían, lo cuál a día de hoy no es cierto.
Entonces, veamos cuáles son algunos de los problemas (o complejidades) que se nos presentan a la hora de trabajar con estructuras de datos inmutables:
Mayor consumo de memoria y ciclos del garbage collector
Como vimos con los ejemplos del principio del artículo, cuándo hablamos de estructuras de datos inmutables, nos referimos a estructuras de datos que no pueden ser modificadas una vez inicializadas. Esto implica qué, si necesitamos modificar una estructura de datos, lo que vamos a tener que hacer es copiar los datos originales y modificarlos. De modo que, como ya podéis imaginar, la memoria de nuestros programas, en lugar de tener un único objeto que se va modificando con el hilo de ejecución, lo que vamos a tener serán múltiples instancias con pequeños cambios.
Lo que:
-
Conlleva un mayor consumo de memoria (derivado de las múltiples copias modificadas de los datos), especialmente si el compilador no está especialmente optimizado para ello.
-
Implica más ciclos del garbage collector (en caso de que el lenguaje en el que programamos disponga de uno), ya que este ahora tendrá que gestionar las múltiples copias con pequeños cambios y tendrá que ir liberando memoria cada vez que una de esas copias intermedias deje de ser utilizada.
Algoritmos más complejos
Si bien es cierto que antes dijimos que el código resultante de la gestión de estructuras de datos inmutables resultaba más legible y predecible, derivado de que las modificaciones de los datos tenían que ser explícitas en el código, también es cierto que, cuándo tengamos que trabajar con estructuras de datos que van a estar mutando constantemente, el código se volverá inevitablemente más engorroso, fruto de tener que explicitar cada uno de esos cambios. Especialmente cuándo la sintaxis del lenguaje no esté optimizada para ello (i.e. lenguajes no funcionales).
Inmutabilidad en Go
Vale, ya tenemos claro qué es la inmutabilidad, y ya sabemos cuáles son sus pros y sus contras. Ahora bien, ¿tiene Go estructuras de datos inmutables? Y, entonces, ¿para qué todo esto?
Pues no, efectivamente, Go (v1.13), a diferencia de otros lenguajes de programación, no tiene estructuras de datos inmutables. De hecho, hay una propuesta abierta desde el mes de octubre del pasado año (2018), en la que se proponen dos posibles implementaciones diferentes:
- una que respeta el compromiso de (retro)compatibilidad de Go 1.x
- y otra que se propone para la hipotética versión de Go 2.x.
Inmutabilidad de structs a nivel de paquete
Sin embargo, de mientras, sí que hay algo que podemos hacer para beneficiarnos, en parte, de algunas de las condiciones de las estructuras de datos inmutables, pero a nivel de paquete.
Vamos a suponer que tenemos la siguiente estructura de datos:
type Person struct {
Name string
FavoriteColors []string
}
cuyos atributos pueden ser modificados libremente en cualquier parte de nuestro código.
Podríamos hacer algunas modificaciones para limitar y controlar eso:
type Person struct {
name string
favoriteColors []string
}
func (p Person) WithName(name string) Person {
p.name = name
return p
}
func (p Person) Name() string {
return p.name
}
func (p Person) WithFavoriteColors(favoriteColors []string) Person {
p.favoriteColors = favoriteColors
return p
}
func (p Person) FavoriteColors() []string {
return p.favoriteColors
}
de forma que, ahora, nuestra estructura de datos será inmutable desde fuera del paquete.
Cómo podemos observar, lo que hicimos fue:
-
Hacer que los atributos de nuestra estructura de datos sean privados, de modo que las funciones de fuera del paquete dónde está definida la estructura de datos en cuestión no puedan modificar el valor de los mismos.
-
Pasar el parámetro implícito de los métodos definidos sobre la estructura de datos por copia en lugar de por referencia, es decir,
(p Person)
en lugar de(p *Person)
. -
Añadir varios withers a nuestra estructura de datos (entendemos por wither un método similar a un setter pero que, en lugar de modificar el estado de la estructura, devuelve un nuevo estado):
WithName
,WithFavoriteColors
, etc. -
Permitir que nuestras estructuras de datos puedan ser inicializadas de un modo similar al de una fluent interface:
me := Person{}. WithName("Dave"). WithFavoriteColors([]string{"blue", "pink"})
Sin embargo, es importante tener presente que estas podran seguir siendo modificadas desde dentro del paquete dónde esté definido su tipo.
Yendo un pasito más allá
Como hemos podido ver, con las modificaciones que realizamos sobre el código anterior, fuimos capaces de hacer que nuestras estructuras de datos fueran inmutables fuera del paquete donde estaban definidas.
Sin embargo, ¿qué ocurre si nuestro estado internamente utiliza estructuras de datos mutables (slices, maps, etc)?
Eso es, quizás los que estábais más atentos ya os habíais dado cuenta, pero nuestra estructura de datos tiene un bug.
Una potencial fuente de problemas. Y es que internamente está utilizando un slice para representar los colores favoritos
de la persona. De forma que, si llamamos al getter FavoriteColors
y modificamos el slice que nos devuelve esta función,
estaremos modificando también el estado interno de la estructura de datos.
¡ERROR!
Para ello, en este caso, es importante ser consciente de cuáles son las estructuras de datos más habituales en Go y cómo funcionan internamente, además de saber cuál es el método con el que se comparten, por defecto, entre funciones (paso por copia vs paso por referencia).
Una posible solución para este caso podría ser desarrollar nuestra propia estructura de datos inmutable (una lista enlazada, por ejemplo). Aunque, evidentmente, todo sería mucho más fácil si fuera el propio lenguaje el que nos diera las herramientas para ello.
Finalmente, y un poco a modo de deberes, os dejamos un paquete que no hemos tenido la oportunidad de probar, pero que resulta cuánto menos curioso, pues lo que busca es precisamente eso, proporcionarnos estructuras de datos inmutables para nuestros desarrollos. Podéis encontrar más información en myitcv.io/immutable.
Y vosotros, ¿alguna vez habéis usado estructuras de datos inmutables?
Como siempre, estaremos encantados de recibir vuestro feedback en los comentarios del blog o vía Twitter.