miércoles, 29 de julio de 2020

Sin comentarios

Pasé gran parte de los últimos meses trabajando en un proyecto nuevo. Lo  estoy desarrollado en C++ moderno y aplicando absolutamente todo lo que aprendí de mis errores en estos años. Y hay algo raro que se dio sin querer con cierta naturalidad en el código: casi no hay comentarios. Eso no suele ser una buena señal, pero en este caso sí que lo es.

Hay dos o tres aspectos importantes que deben quedar claros sobre un algoritmo para luego poder depurarlo, mantenerlo, extenderlo, etc. ¿Qué hace? ¿Cómo lo hace? y ¿Por qué lo hace así? Los comentarios están ahí para decir o aclarar lo que el código no puede... Pero la verdadera pregunta del post es: ¿estás seguro que el código no puede?


Por ejemplo, si en una parte de un programa hay que buscar el mayor de un vector de promedios, un alumno de primer año podría escribir algo como:
    double m = 0;
    for (size_t i=0; i<v.size(); ++i)
        if (v[i]>m)
            m = v[i];
Ya se, no es una buena solución, pero iteremos.... Mirando solo este código, no es tan difícil saber qué hace, y qué representan las variables como i o m. Pero si esto está enredado en medio de un algoritmo más grande, puede no ser tan trivial y tomar un poquito más de esfuerzo individualizarlo. Entonces, ¿agregamos un comentario?

NO! En lugar de agregar antes del código un comentario que diga "// buscar el mayor elemento de v" o al lado de la declaración de m uno "// m guarda el promedio máximo"; es mucho mejor envolver ese código en una función y reemplazarlo algo parecido a:
   double mayor_prom = buscar_mayor(v);
¿Ven como ya no se necesitan comentarios? El código ya dice todo, no hay mucho que agregar (solo faltaría renombra v). Mucho mejor aún sería usar directamente std::max_element, pero solo para dar pie a otros ejemplos, sigamos con nuestra función.

Ahora discutamos un detalle menor que se pueden evitar: el indice i. El for de esta función es para recorrer todos los elementos, y me importan solamente los valores, pero no sus posiciones. Entonces, creo que el for basado en rangos es mejor, porque me evita (o esconde) esa variable auxiliar que no es más que un detalle de implementación (o un mal necesario) de la recorrida:
   double m = 0;
   for(doble x : v)
      if (x>m) m = x;
Así queda claro que la posición no me importa, y no tengo ni que pensar en cómo denominar al contador, o cuáles son sus límites, porque simplemente ya no existe. Y hasta podría reemplazar x por algo más descriptivo como una_nota. Se pueden simplificar muchos algoritmos con este for, o con funciones como std::for_each, std::transform y std::accumulate.

Vamos ahora a casos donde la función falla: cuando los valores son negativos. Para que funcione con negativos, hay que cambiar la inicialización de m. Una solución puede ser:
   double m = std::numeric_limits<double>::lowest();
Y ojo aquí, que existe std::numeric_limits::min() pero no es lo que queremos (min es el más cercano a cero, no el más negativo). En casos así (exagerando un poco), donde hay una función que tal vez por su nombre parezca la que naturalmente va, pero no lo sea, es válido agregar un comentario para aclarar el detalle que no se ve y el por qué de la elección.


Volviendo entonces a las 3 preguntas iniciales: ¿Qué hace? se responde en el nombre de la función y su prototipo, ¿Cómo lo hace? es lo que debe mostrar con claridad el código de su implementación, y ¿Por qué lo hace así? (y no de otra forma tal vez más obvia) es el detalle invisible que probablemente valga la pena comentar.

Ahora, para preparar el último ejemplo, generalicemos la función (template) así podemos reutilizarla para vectores de otras cosas. Si las otras cosas no son números, no vamos a poder resolver la inicialización de m con std::numeric_limits. Una solución alternativa es usar el 1er elemento del vector. ¿Pero qué pasa si el vector está vacío?

Uno podría inferir del prototipo de la función:
    template <typename T>
    T buscar_mayor(const std::vector<T> &v);
que no sirve para vectores vacíos, pues el valor de retorno no admite esa posibilidad (como sí lo haría un iterador o un puntero por ej). Pero supongamos que fuera una función donde no es tan obvio. ¿Qué hacemos? ¿Agregamos un comentario a la función que diga "// el argumento v no puede estar vacío"?

Mejor aún, podemos buscarle la vuelta para documentar este requisito de la función de forma que el compilador lo entienda y lo verifique (recuerdo a Stroustroup diciendo "el compilador no lee los comentarios, y yo tampoco"). Yo uso un par de macros en espíritu similares a las Expects y Ensure de las CppCoreGuidelines para esto:
    Expects(!v.empty());
    T m = v[0];
Entonces, cuando esa condición no se cumpla, en lugar de entrar en los reinos tenebrosos del comportamiento indefinido (donde la más tenebroso es que nadie note nada), el programa reventará y el depurador nos marcará la precondición exacta que no se cumplió. Sería todavía mejor ver esto en el prototipo de la función (antes que en el interior) y hacia allí van los contracts (C++23?), pero de momento me las arreglo con estas macros (buh! macros), y creo que son una de las mejores costumbre que adquirí en los último años.


Vale aclarar para finalizar que de ninguna manera quiere decir que no debe haber documentación. Mi proyecto no tiene casi comentarios en el código, pero sí hay varias páginas describiendo la arquitectura general, los patrones de diseño utilizados y justificando su elección, la división de responsabilidades entre clases, los puntos que se dejan abiertos previendo extensiones, los mecanismos de desacople entre las diferente partes del sistema, etc. Aspectos generales que hay que tener en claro para que luego el detalle en el código pueda resultar más obvio, o para que uno sepa dónde buscar entre tantos cientos de archivos, clases y funciones.

En conclusión, cuando tengan que agregar un montón de comentarios a un código porque no se entiende, piensen primero si no habría una mejor forma de escribirlo para que sí se entienda. Esto empieza con conceptos y lineamientos relativamente simples. Cosas como seleccionar los nombres a conciencia y seguir reglas de estilo; respetar principios como el de única responsabilidad y único nivel de abstracción al diseñar funciones; o usar más la biblioteca estándar. Pero en proyectos grandes es donde se torna desafiante y lleva mucho programar encontrarle la vuelta.

Parece fácil hablar de esto con ejemplos simples y aislados de pocas lineas como los que mostré, pero la generalización no lo es; y obliga a reescribir una y otra vez el mismo código hasta eliminar toda la complejidad accidental y converger a una implementación suficientemente clara. Ya lo dijo el gran Antoine de Saint-Exupéri: "La perfección no se alcanza cuando no hay nada más que añadir, sino cuando no hay nada más que quitar". Creo que estaba reescribiendo un algoritmo cuando dijo eso.

7 comentarios:

  1. "Una boa digiriendo un elefante" ¿que otra cosa podría ser!

    Pablo. Una pista o avance de ese nuevo proyecto.

    ResponderEliminar
    Respuestas
    1. Es una herramienta interna y bastante específica para las cosas que hago de geometría computacional. Para visualizar mallas (de las que se usan para simulaciones numéricas) sobre las que trabajo. No es un proyecto que pueda tener mucho interés general, así que de momento no es algo que esté por publicar o liberar.

      Eliminar
    2. Puede ser un tema de gran interés el que estas desarrollando, podrías por ejemplo exponer tus avances en el foro de GeoGeobra, un entorno de geometría educativa.

      No te olvides de PseInt que nos sirve de gran ayuda a los docentes en la enseñanza y aprendizaje de la lógica algorítmica.

      Una petición, los chic@s se entusiasman con mostrar en la consola animaciones y juegos simulados como texto, quisiéramos un poco de más vida en PseInt, donde por medio de instrucciones se pudiera al menos cambiar el fondo, resaltar texto, algo similar como lo hace SmallBasic

      https://smallbasic-publicwebsite.azurewebsites.net/assets/tutorial-downloads/CodingClub_Practice01.pdf

      Código en SmallBasic

      TextWindow.BackgroundColor="darkgreen" '// Fondo de la consola
      TextWindow.ForegroundColor="green" '// resaltado
      TextWindow.WriteLine("Hello World!")

      Y sería un sueño una ventana gráfica para realizar figuras geométricas, visualizar verdaderas animaciones, juegos, etc

      Código en SmallBasic

      TextWindow.BackgroundColor="darkgreen" '// Fondo de la consola
      TextWindow.ForegroundColor="green" '// resaltado
      TextWindow.WriteLine("Hello World!")
      GraphicsWindow.BackgroundColor="#AFEEEE"
      GraphicsWindow.BrushColor="#8B0000"
      GraphicsWindow.PenColor="#FF8C00"
      GraphicsWindow.FontSize=30
      GraphicsWindow.DrawRectangle(18,25,190,30) '// These four numbers represent: X axis,Y axis, width of the rectangle, and height of the rectangle
      GraphicsWindow.DrawText(20,20,"Hello World!") '// The two numbers are just an example; you are free to choose where you would like your text to start on the X,Y axis


      Gracias

      Eliminar
    3. Te falto pedirle que agregue una funcion a pseint para conectarse con un Rover en el planeta Marte.

      Código en SmallBasic.

      ConnectWindow.Rover = "go_back_to_earth" // vuelve la nave a la tierra.

      Eliminar
  2. Excelente post

    En la mayoría de libros que he leído sobre programación, el uso de los comentarios es casi un dogma. Sin embargo, la claridad para expresar código sin recurrir al comentario es digno de un maestro.

    Gracias por tu gran aporte

    Saludos!

    ResponderEliminar
  3. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  4. Excelente, Pablo. Me parecen muy acertadas las reflexiones, y es algo que habría que aclarar en mucha bibliografía de programación. La primera vez que leí sobre "que hable el código por sí mismo" fue en "Código Limpio" de R. Martin, y me cambió mucho mi mirada sobre darle expresión (semántica) al texto, pero estos consejos son aún más interesantes, sobre todo que el "por qué así y no de otra forma" es tal vez lo que vale comentar. Una joya las frases de Stroustrup y El Principito. Saludos y éxitos en el proyecto. Agustín Roldán.

    ResponderEliminar