El error del billón de dólares

29 de mayo de 2024

A Tony Hoare o Sir C.A.R. Hoare (1934) le debemos mucho por sus descubrimientos en el campo de la computación. Es posible que su nombre no te suene, pero si has estudiado la carrera de informática casi seguro que te has topado con uno o dos (o más) de sus descubrimientos, por ejemplo, el algoritmo de QuickSort.

Con esa tarjeta de presentación, quizá sobre añadir que también inventó un lenguaje formal para que los procesos concurrentes vivieran en armonía: CSP o Concurrent Sequential Processes, que a día de hoy se usa en el lenguaje de programación Go (o Golang) como base de su implementación multihilo.

Baste decir que es uno de los “Turings”. El premio se le concedió en 1980 por sus contribuciones al diseño de los lenguajes de programación. Y es precisamente aquí, en esta parada, donde nos bajamos y continuamos a pie.

ALGOL, el origen de una herencia maldita

En 1965, Hoare se encontraba trabajando en el sucesor de ALGOL 60, un lenguaje de programación que en su tiempo tuvo un discreto éxito comercial y se usó sobre todo en investigación. Este lenguaje, bautizado en un alarde de originalidad como ALGOL W, fue un diseño conjunto con otro de los grandes, Niklaus Wirth, y estaba basado a su vez en una versión de ALGOL nombrada (sorpresa) ALGOL X.

ALGOL W no llegó a conquistar los corazones de los programadores de ALGOL 60 como su sucesor. Al final, se decantaron por el otro pretendiente al trono: ALGOL 68 (sí, nombrar lenguajes con nombres pegadizos no fue mainstream hasta décadas después).

Pero lo que si consiguió ALGOL W es sentar las bases para crear algo más grande. Mucho más grande: Niklaus Wirth lo adoptó para crear la base de uno de los lenguajes de programación más usados de todos los tiempos y por el que es quizás más conocido Wirth: Pascal.

No obstante, Pascal y su linaje dan para otro artículo. Vamos a seguir centrados en ALGOL W. Este, poseía algo maligno en su interior. Un diabulus in música que inadvertida pero conscientemente se coló en su diseño y del que, hasta el presente, muchos lenguajes de programación arrastran como una herencia maldita…

Un ejemplo de referencia nula

Vamos a poner un ejemplo para que agarremos el compás. Además lo haremos en Java, puesto que seguro que os va a sonar a quienes hayan programado un poco en este lenguaje:

El ejemplo es casi autoexplicativo:

  • En la línea 3 declaramos una variable con el tipo String pero lo inicializamos a ‘null’. Es decir, la variable ‘cadena’ no posee un objeto de la clase String, de hecho, no posee nada. ‘null’ es indicativo de que la variable permanece a la espera de referenciar un objeto String.
  • La pequeña catástrofe está a punto de darse en la línea 6, cuando vamos a usar esa variable. En ese momento, cuando indicamos que vamos a usar el método “length” que poseen los objetos de la clase String, el motor de ejecución Java invoca la referencia que debería poseer ‘cadena’, pero al hacerlo no hay nada, cero, null.

¿Qué provoca esto?

Pues de primeras que el programa detiene su ejecución si no controlamos la excepción. El problema no es que la detenga en ese ejemplo tan trivial pero ilustrativo. ¿Os imagináis si se detiene en un programa que sea responsable del cálculo de aproximación a pista en medio de un vuelo comercial?

Si pensamos en un programa como una gran máquina de estados, nos daremos cuenta de que, en un determinado instante de tiempo, una variable no siempre contiene un objeto, pero, la variable sí está ahí.

Además, esa variable puede ser usada como parámetro para una función o método, ser insertada en una colección: un vector, lista, etc. Si eliminamos el objeto al que apunta esa variable o su memoria es recolectada, la variable contendrá una referencia nula dispuesta para el desastre en el momento que sea invocada.

Ya lo hemos visto en entradas anteriores como en lenguajes como C o C++, donde manejamos la memoria de forma manual, podemos toparnos con variables que ya no contienen el objeto al que apuntaban en memoria.

¿Cómo evitamos las referencias nulas?

Vamos a seguir sobre la base de Java, a modo ilustrativo. Damos por hecho que el resto de los lenguajes de programación donde existe la posibilidad de que un objeto posea una referencia nula tienen o está desarrollando mecanismos defensivos similares.

En primer lugar, lo curioso es que si tenemos un tipo String ¿Por qué el compilador nos permite inicializarlo con ‘null’? ¿Posee ‘null’ un tipo genérico? Pues realmente no, no lo posee. En el caso particular de Java es una palabra clave del lenguaje sin tipo alguno.

De hecho, de una forma estricta, al no tener tipo, el mero hecho de asignar ‘null’ debería ser una excepción ya que no casa con el tipo al que está designada la variable. Es decir, el propio hecho de hacer “String cadena = null;” debería provocar un error de compilación y en tiempo de ejecución, si una variable está posee un valor ‘null’ debería provocar cuanto menos un aviso incluso antes de que la variable sea usada.

Y es que esta es precisamente una estrategia para definir una clase que de “raíz” no admite ser nula: Optional.

Dicha clase no nos libra de los males de null, pero sí nos da un mecanismo muy potente para manejar sus riesgos. Es decir, en vez de esconder el uso de null, lo que se hace es justo lo contrario: se manifiesta abiertamente. Un ejemplo para verlo mejor:

En este caso tenemos una clase Person, si queremos obtener la edad, tenemos un método getter para ello. Hasta ahí todo normal, pero la gracia está en que como no sabemos si esa clase tendrá o no el campo ‘age’ con un valor o es nulo, lo que devuelve no es un entero sino un objeto de la clase Optional.

Ese objeto no es el valor en sí sino una envoltura sobre la que poder trabajar de forma segura. En este caso, pongamos un ejemplo con la clase Person en marcha:

Como vemos, no disponemos del valor directamente (observad la línea 6), sino que estamos de alguna forma obligados a tratarlo como lo que es, algo opcional. En este caso, simplemente preguntamos (línea 16) si posee algún valor bien definido y lo usamos o de lo contrario gestionamos adecuadamente su ausencia (sin excepciones de por medio).

¿Existen lenguajes sin tipos nulos?

Casi todos los lenguajes en los que existe la posibilidad de que una referencia (o puntero, etc.) sea nula se traduce en cierto dolor de cabeza para ir eliminando estas a través de buenas prácticas de programación, programación defensiva o simple corrección de errores.

La posibilidad de que un objeto no sea válido (nulo) está prácticamente presente en todos los lenguajes, pero, lo que sí cambia es la forma que tiene el lenguaje de verlo y sobre todo de las herramientas que provee a los programadores.

Rust, Haskell, Kotlin y Swift entre otros muchos, poseen capacidades nativas o integradas desde el primer día en su entorno de desarrollo y ejecución.

La base es: si un tipo puede ser nulo, debes gestionarlo de forma acorde. Casi todos funcionan de forma similar al Optional (presente en Java desde la versión 8).

Un ejemplo en Rust (la repetición de código es por interés docente):

El tratamiento que se hace en la división por cero es similar a no poseer una referencia adecuada o que el tipo sea nulo. Como sabemos no podemos dividir por cero y por ello, se diseña la función para prepararse en caso de que el divisor lo sea (líneas de la 1 a la 7).

La función no devuelve el resultado de la división sino un Option sobre el que deberemos hacer captura de patrones (líneas 12 a 15, por ejemplo) para determinar si la operación ha sido correcta o no.

Es posible que te preguntes… ¿Pero es que se podría hacer la operación MAL y no devolver un Option, sino directamente un f64? Cierto, se puede:

Pero es precisamente cómo no hay que programar una función. Si la diseñas e implementas a prueba de fallos, no entregas de vuelta un valor f64 que podría llegar a ser nulo o no válido, sino que obligas a quien usa la función a preocuparse de que el valor esté presente y tratar de forma adecuada la casuística de que no lo esté.

Conclusiones

Un fallo en el software puede costarnos mucho dinero. Tanto en repararlo como en paliar los daños que este ha cometido; sin nombrar casos que han llegado a titulares de medios generalistas por el desastre que han causado.

Uno de los fallos más comunes y presentes desde la infancia de los lenguajes de programación son las referencias nulas, inventadas precisamente por uno de los científicos más laureados del campo.

Los lenguajes modernos poseen mecanismos que nos hacen más fácil tratar estos errores y más difícil que tratemos de cometerlos.

Para terminar un símil de la vida real: Imagina una carta que no posee remitente. ¿A dónde vuelve la carta en caso de que la dirección esté mal? Si hay remitente, tiene un mecanismo en caso de que el valor o dirección de destino no sea válido: devolverla al remitente.

Como vemos, muchas veces tratamos con problemas antiguos y no vemos que la solución ya estaba allí desde hace siglos…

The Hacktivist, un documental online sobre el pionero en Ciberseguridad Andrew Huang

Imagen: Freepik.