La primera mención que puedo encontrar al término "iterator" con el significado con el que lo usa la biblioteca estándar de C++ hoy es en el paper de David Musser y Alexander Stepanov de 1993
Algorithm-oriented libraries
. El concepto sin embargo no era nuevo. En el fundacional paper de Musser y Stepanov de 1988
podemos encontrar una idea similar bajo el nombre de "coordinate". Citando el paper:
In the generic programming approach, we use generic indexing by a generic formal type, Coordinate. Coordinate is instantiated to type Natural for vectors; for linked lists, however, cells themselves can serve as Coordinate values. A generic Find can thus return a Coordinate value that can be used to reference the located element directly.
Bajo otro nombre, y con una interfaz ligeramente distinta a la actual, el concepto era en esencia el mismo del de lo que hoy en día llamamos "iterator".
Y, si lo pensamos un poco, ¿qué es un iterator tal y como lo entiende la biblioteca estándar de C++? Es un identificador de una posición en una secuencia. La biblioteca estándar usa iterators no sólo para iterar por secuencias o acceder a sus elementos, también los usa para representar intervalos (véase funciones como
,
,
o
), puntos de inserción (
,
, el parámetro "hint" de
...), o identificar elementos en su relación con la secuencia (
,
,
...). La razón por la que la biblioteca estándar usa intervalos semicerrados no es por elegir una convención cualquiera, es una importante decisión de diseño que hace que para una secuencia de N elementos existan N + 1 iterators, es decir N + 1 posiciones, ya que en una secuencia de N elementos existen N + 1 puntos de inserción. Pensar en los iterators como mecanismos que sirven para iterar secuencias, aunque es lo que sugiere su nombre, es tener una visión muy limitada sobre lo que representan y todas las posibilidades que ofrecen. Es también importante tener en cuenta que Stepanov, con su trasfondo matemático, pensaba en el diseño de software en términos de valores que son transformados por funciones, no en términos de objetos que llevan a cabo acciones, así que en el contexto de su creación los iterators eran representaciones abstractas de posiciones en una secuencia. Iterar la secuencia era sólo una de las posibles operaciones sobre ellos. Tanto en sus papers como en sus charlas Stepanov ponía un gran énfasis en algoritmos donde la posición relativa de los objetos en la secuencia es relevante: particiones, permutaciones, algoritmos de ordenar, algoritmos sobre secuencias ordenadas como la búsqueda binaria o algoritmos logarítmicos sobre conjuntos, énfasis proveniente en parte de la influencia causada sobre Stepanov por la lectura del maestro Yoda de la informática, Donald Knuth. Para poder definir cualquiera de estos algoritmos de forma genérica es importante tener una potente abstracción de las posiciones sobre una secuencia.
La STL fue un invento revolucionario, que permitía una capacidad de reutilizar código entre contenedores y algoritmos nunca vista hasta el momento. La gente podía escribir contenedores nuevos y usar en ellos los algoritmos existentes, y escribir nuevos algoritmos que funcionaran para todos los contenedores a la vez, incluso los que aún no estaban escritos. Con el tiempo otros lenguajes han ido incorporando estas ideas y podemos encontrar cosas que llevan el nombre "iterator" en lenguajes como Rust, Python o Java. Sin embargo, el significado de "iterator" en estos lenguajes es bastante distinto al que tiene la palabra en C++. Son objetos con una sola operación, que lee el valor actual, avanza al siguiente elemento y notifica cuando ha terminado. En el caso de Java hay dos operaciones, una para leer y avanzar y otra para comprobar si ha terminado, pero ambas interfaces son isomórficas. Un iterator en Rust y Python es más parecido a un generador que va generando los valores de una secuencia que a un marcador de una posición en la secuencia, y este concepto es mucho más fiel al nombre "iterator", ya que su principal (y única) función es poder iterar la secuencia. Al perder la capacidad de representar posiciones en la secuencia, y en su lugar substituir la interfaz por una única operación que avanza y va leyendo los valores, los iterators de estos lenguajes pierden la capacidad de expresar algoritmos relacionados con la posición relativa de los elementos, como permutaciones o algoritmos sobre secuencias ordenadas, y se ven reducidos a mecanismos para iterar secuencias de forma genérica. Tiene todo el sentido del mundo que llamemos a estos mecanismos "iterator", pues es lo que son, iteradores, objetos que iteran secuencias. Quizá los que están mal nombrados son los de C++, para los cuales el nombre original de "coordinate" sería mucho más adecuado.
"Coordinate" es además un nombre mucho más general que "iterator", pues puede referirse a un elemento en cualquier tipo de contenedor, no necesariamente en una secuencia. En el momento en el que hablamos de iterators, y por lo tanto hablamos de iterar, es necesario que el contenedor tenga la topología de una secuencia, que pueda enumerar todos sus elementos en un orden. Sin embargo, esto no es verdad para todos los contenedores. Existen estructuras de datos con topologías distintas a la secuencia, como los árboles o los grafos, donde puede ser interesante hablar de coordenadas y elementos, pero no necesariamente de "siguiente" o "anterior". Podríamos por ejemplo querer escribir una versión genérica del algoritmo de A* que funcione sobre cualquier grafo de lista de adjacencia. Para ello necesitaríamos que la función cogiera como parámetros el grafo y dos nodos, el de inicio y el de destino, que habría que representar de alguna forma. Las operaciones más básicas que toda coordenada tiene que poder hacer son leer el elemento actual y comparar si dos coordenadas representan al mismo elemento o a dos distintos. Partiendo de aquí, podrían existir diferentes familias de coordenadas para diferentes topologías. Los iterators (con toda su jerarquía de input, forward, bidirectional, random acces, contiguous) son las coordenadas que representan secuencias. Además de eso, podríamos definir conceptos para coordenadas de grafos o distintos tipos de árboles, que sustituyan las operaciones "siguiente" y "anterior" por obtener nodos adyacentes en el caso de los grafos u obtener los hijos y el padre en el caso de los árboles.
Evidentemente este es un ejercicio bastante inaplicable porque tras 23 años desde la inclusión del palabro "iterator" al estándar llegamos bastante tarde para cambiarlo por nada. Nos hemos acostumbrado a hablar de iterators y a entenderlos así, y cambiar de palabra es a estas alturas logísticamente imposible. Sin embargo, sí que creo que el ejercicio de reflexionar sobre las palabras que usamos y su significado es enriquecedor para entender mejor las herramientas que usamos para pensar y comunicarnos, y para explorar las implicaciones últimas de esos significados y guiar así nuestros diseños. Se suele decir que uno de los problemas más difíciles en la informática es nombrar cosas. Cuando podemos nombrar algo sabemos lo que es. Si sabemos lo que es también sabemos lo que no es. Y esto nos puede guiar a la hora de decidir qué variables debería contener un tipo de dato o qué operaciones debería poder hacerse sobre él, y cuáles no.
Este ejercicio llama también a una reflexión sobre todas esas clases terminadas en -er y -or, y que pueden limitar nuestra capacidad de razonar y llevarnos a pensar que sólo sirven para una cosa, limitando la capacidad de expresión de nuestros diseños. Los iterators que nacieron antes de la palabra "iterator", cuando se les llamaba "coordinate", no sirven sólo para iterar, mientras que algunos de los diseños posteriores, en los que el nombre vino primero y el diseño después, son considerablemente menos flexibles y potentes. Hay que tener cuidado con el reino de los sustantivos, que en sus ansias imperialistas a veces intenta conquistar nuestros verbos.
Quiero hacer de este texto también una reivindicación de la lectura de los escritos fundacionales de la informática, de los papers y libros que introdujeron las ideas que hoy en día consideramos tan básicas que ni siquiera pensamos sobre ellas, como los peces jóvenes del chiste de David Foster Wallace:
Están dos peces nadando uno junto al otro cuando se topan con un pez más viejo nadando en sentido contrario, quien los saluda y dice "buen día, muchachos, ¿cómo está el agua?". Los dos peces siguen nadando hasta que pasado un tiempo uno se vuelve hacia el otro y pregunta "¿qué demonios es el agua?"
Papers como
The use of sub-routines in programmes
de David Wheeler,
de Musser y Stepanov o
Monads for functional programming
de Philip Wadler, el trabajo de Donald Knuth sobre algoritmos o las ideas de Alan Kay sobre programación orientada a objetos por nombrar unos pocos. Comprender ideas fundacionales en su contexto, entendiendo los problemas que pretendían solucionar y el hilo de pensamiento que llevó a su diseño nos puede enseñar a entender y apreciar mejor estas ideas, y a no perder en su aplicación propiedades esenciales del diseño original cuya comprensión se ha ido perdiendo a lo largo del tiempo a causa de un efecto de teléfono estropeado intergeneracional.