Listas y memoria
Cuando uno está empezando a programar (pero después también), siempre es mejor encarar los problemas de a uno y simplificarlos en lo posible. El principio es bastante convincente y dudo que muchos quieran contradecirlo. Sin embargo, la manera de hacer esto no es siempre evidente. O quizás la separación en sí no es posible si no que se hace posible después de cierto trabajo.
Por ejemplo, supongamos que queremos implementar una lista enlazada. Para eso
podemos decir simplemente que una lista puede ser o una lista vacía o un par
que tiene un primer elemento (head
) y el resto (tail
) que es en sí mismo
una lista (a este par head
y tail
lo llamamos Cons
). Usando haskell
se
podía ver algo así:
data Lista a = Vacia | Cons a (Lista a) deriving Show
Lo que se ve bastante simple. Pero para que eso funcione hay muchas cosas que tienen que estar en su lugar. Supongamos que hacemos una lista para ir al chino y no olvidarnos
let compras = Cons "agua" (Cons "yerba" (Cons "miel" (Cons "alfajores" ...)))
(los ...
son para indicar que la lista sigue, no es parte de la sintaxis).
Con eso estamos guardando en la memoria (de la computadora) nuestra lista, en
compras
. Pero una vez que hicimos la compra ya no necesitamos recordar la
lista, entonces podemos olvidarla. Sin embargo, no tenemos que olvidarla antes
de terminar nuestra compra, porque si no podemos tener que volver a ir si es
que nos olvidamos algo. Todo esto de la memoria no es algo que sea inherente al
problema de qué es una lista, y de hecho podemos abstraernos de él. Sin
embargo, esto es porque ya se está manejando. Si existiera algo que se ocupara
de eso, tendíamos que hacerlo nosotros.
La misma idea de lista, si fuésemos a usar c
, sería algo como:
typedef struct Lista Lista;
typedef struct Lista { char* head; Lista* tail; } Lista;
Esto quizá invoque al lector el tema no sólo de la memoria si no también el de
los punteros (que está relacionado) y puede que otras cosas como que esta lista
es menos genérica. Pero vamos de a una. Lo que en haskell
era escribir let vacia = Vacia
acá sería algo como:
Lista vacia = 0;
Pero la función cons
en c
no vine dada, como en haskell
(donde la
definimos al definir el tipo Lista). Podríamos hacer algo así:
Lista* cons(char* head, Lista* tail) {
Lista* lista = malloc(sizeof (Lista));
if (!lista)
return 0;
*lista = (Lista) { .head = head, .tail = tail };
return lista;
}
Donde llamamos a la función malloc
. Entonces tenemos (al menos) dos problemas
mezclados, el manejo de la memoria y la construcción de una lista. Si malloc
devuelve 0
, eso significa que no disponemos de memoria para satisfacer a quien
llamó a cons
y no podemos hacer nada. Todo lo que podemos hacer, en todo
caso, es avisarle que en tal situación se lo vamos a indicar devolviéndole a a
él también 0
, con lo cual él también va a tener que preocuparse por la
memoria (cuando probablemente su problema concreto haya sido otro).
Pero hay otro problema, y es que la memoria pedida con malloc
no se va a
liberar hasta que alguien llame a free
. Es decir, tenemos que hacer otra
función como
void free_lista(Lista* ls) {
if (ls) {
free_lista(ls->tail);
free(ls);
}
}
Esto, suponiendo que las string
s son literales porque si no también
tendríamos que fijarnos si no hay que liberar su memoria también… (podría
pasar que otra lista también apunta a la misma string
así que habría que ver
eso, que no es fácil, y menos si no era el problema que había que resolver en
un principio).
Entonces, es c
un lenguaje obsoleto al lado de las opciones modernas que
tienen muchas más soluciones? Bueno, hay gente que dice eso. Pero yo creo que
es no es así.
Podemos hacer algo como haskell
(o java
, o python
, etc) con lo de que
malloc
no nos de memoria. Quedaría así:
Lista* cons(char* head, Lista* tail) {
Lista* lista = malloc(sizeof (Lista));
if (!lista) {
perror("not enough memory!");
abort();
}
*lista = (Lista) { .head = head, .tail = tail };
return lista;
}
Con esto evitamos que el usuario se tenga que fijar si recibió o no una lista. Simplemente su programa se interrumpe.
En cuanto al otro problema podemos usar Bohem.
Lista* cons(char* head, Lista* tail) {
Lista* lista = GC_malloc(sizeof (Lista));
if (!lista) {
perror("not enough memory!");
abort(); }
*lista = (Lista) { .head = head, .tail = tail };
return lista;
}
Y listo, el /garbage collector/ se ocupa de liberar la memoria cuando no se la necesite más. Lo único es que para compilar el programa hay que conseguir la librería (o biblioteca).
Acá hay más de una forma de hacer esto (y en otro momento podría hacer un post al respecto), pero digamos que la más fácil quizá sea usar el gestor de paquetes de nuestra distribución (si usamos ubuntu apt, en mac brew etc.).
En ubuntu: sudo apt install libgc-dev
. Después en main.c
agregamos
#include <gc/gc.h>
y le pasamos al compilador `pgk-config --libs bdw-gc`
.
Claro que esto no es un argument decisivo, ni mucho menos, para el “debate” sobre la obsolescencia de c, que además tiene otros problemas, como un sistema de tipos tipado tan debilmente por ejemplo. Pero es que por algún motivo que desconozco últimamente me he cruzado con muchas publicaciones de gente con bastantes críticas contra c (y también otros lenguajes con bastantes años, como c++, incluso java) y reclamando dejarlos de lado para usar lo nuevo. Supongo que el paso del tiempo impone la ocurrencia de estos cambios, y las nuevas generaciones adoptan lenguajes que cuando las anteriores empezaron toavía no existían.
De todas formas creo que cuando la discusión adopta la forma tribalista es una pérdida de tiempo. No por ello me opongo rotundamente a observar su devenir, ya que perder el tiempo es una parte importante de la vida, al menos para algunos de nosotros. Sí recomendaría al que preguntase no atarse a un lenguaje de programación ya que de todas formas es una manera de codificar un programa y programar va más allá de dominar esa herramienta en particular.
Por último, quizá muchos vean en este ejemplo de listas otro caso más contra c (y en particular a favor de haskell). Sin embargo, considero que previamente a sacar esa conclusión habría que chequear que no se trate que justamente haskell es justamente práctico para hacer listas y no concluyamos demasiado rápido generalizando ese punto. Antes habría que llevar a cabo una comparación mejor.