Lo que los compiladores pueden enseñarnos sobre aprendizaje

Si hablas un rato con un desarrollador de software, es probable que antes o después salga el tema del aprendizaje. Siempre andamos preocupados porque no nos da tiempo a aprender todo lo que queremos (y seguramente tampoco sea algo tan grave) y somos demasiado conscientes de todo lo que no sabemos.

Una muestra de lo que nos preocupa este tema es la iniciativa lanzada hace unas semanas por Rubén Fernández para ofrecer consejos a los que están empezando sobre cómo enfocar el aprendizaje para ser desarrollador de software.

En este post quiero reflexionar no tanto sobre qué hay que aprender, sino sobre cuándo hay que aprenderlo. Para ello, estableceremos un parelelismo entre estrategias de aprendizaje y algo que conocemos bien: los compiladores y los lenguajes de programación.

Lenguajes compilados a código nativo

En los lenguajes que se compilan a código nativo, como C, C++, Haskell o Go, el compilador realiza todo el trabajo a priori. Absorbe todo el conocimiento sobre el código fuente y lo aplica para obtener código máquina. Y ya está. A partir de ahí, no hay capacidad de reacción. El código generado es el que es.

Si lo comparamos con el aprendizaje, sería similar a encerrarte durante unos cuantos meses a estudiar sobre un tema, aprender sobre él todo lo que creas que vas a necesitar, y luego dedicarte a aplicarlo sin tener que aprender nada más. Ya has aprendido a priori todo lo que necesitabas.

La ventaja de este enfoque es que, cuando llega el momento de ejecutar el programa compilado podrás hacerlo bastante rápido. Un binario compilado en nativo no necesita más proceso antes de ejecutarse y arrancará muy rápido y, probablemente, también se ejecutará bastante rápido.

Es lo mismo que pasaría si, a la hora de hacer un desarrollo, ya tuvieras de antemano todo el conocmiento necesario: podrías avanzar en el proyecto rápidamente y de forma solvente, puesto que ya sabes lo que necesitas para hacerlo.

Este tipo de lenguajes también tienen sus problemas. El tiempo de compilación suele ser grande: como luego no podrás tocar el código compilado, tienes que asegurarte de hacerlo bien y aplicar de antemano todas las optimizaciones que quieras, porque lo que no hagas ahora no podrás hacerlo después. También existe el riesgo de invertir tiempo en compilar u optimizar partes de la aplicación que no vas a necesitar luego porque no llegarán a ejecutarse.

Asimilándolo al proceso de aprendizaje que hemos descrito antes, tendríamos problemas parecidos. Necesitaríamos invertir mucho tiempo al principio para adquirir conocimiento antes de empezar a desarrollar y acabaríamos aprendiendo cosas “por si acaso” que luego probablemente no utilizaríamos.

Hoy en día puede parecer que nadie aprende así, pero antes de que tuviéramos este acceso ubicuo a una cantidad de información inmensa, era lo más habitual. Lo que no aprendieras cuando tenías la información a mano (los profesores, libros, materiales, etc.) luego era muy difícil aprenderlo por otros medios.

Lenguajes interpretados

En el otro extremo tendríamos los lenguajes interpretados como PHP o Powershell. En este tipo de lenguajes no hay un compilador, sino un intérprete, que se encarga de ir ejecutando los comandos que va encontrando en el código fuente.

Llevado al mundo del aprendizaje, sería análogo a ir aprendiendo sobre la marcha.

Las ventajas son claras. Nos ahorramos el tiempo incial de compilación por lo que podemos empezar a ejecutar nuestra aplicación mucho antes. Además, sólo tenemos que invertir tiempo en interpretar aquellas partes de la aplicación que realmente se van a ejecutar, por lo que nos ahorramos trabajo inútil.

Como estrategía de aprendizaje es muy habitual. Es el típico “a programar se aprende programando”. Resulta muy atractiva porque nos permite empezar a ser “productivos” y “aportar valor” muy pronto. Y eso queda muy bien. Sin embargo, creo que en muchas ocasiones se lleva al extremo y se cae en los mismo problemas que los lenguajes interpretados.

Los lenguajes interpretados, aunque consiguen empezar a ejecutar el código antes, suelen ser lenguajes mucho más lentos que los lenguajes compilados. En un lenguaje compilado, cuando llega el momento de la ejecución ya se sabe lo que hay hacer: ya tenemos el código máquina. En un lenguaje interpretado hace falta aprender en ese momento lo que hay hacer interpretando el código fuente.

Hemos conseguido empezar a ejecutar código antes, pero lo estamos haciendo de forma menos eficiente porque tenemos que aprender sobre la marcha como ejecutarlo. Además, el nivel de optimizaciones que se pueden aplicar sobre el código suele ser menor porque llevaría demasiado tiempo hacerlo en tiempo real, por lo que el código que se acaba ejecutando es menos eficiente que el que se habría generado con un compilador estático.

A las estrategias de aprendizaje que llevan el “a programar se aprende programando” al extremo les acaba pasando lo mismo. Si sólo te dedicas a programar, aprenderás, sí, pero te verás abocado a tener que aprender muchas veces lo mismo porque estarás tan ocupado programando que te perderás los principios fundamentales que hay por debajo de lo que estás haciendo.

De hecho la mayoría de lenguajes interpretados modernos cuentan con la posibilidad de generar código nativo mediante técnicas de compilación just in time para mejorar el rendimiento. No sólo ejecutan sobre la marcha, sino que son capaces de invertir tiempo en “aprender” bien cómo ejecutar determinadas áreas del código.

Lenguajes compilados a código intermedio

Existe un tipo de lenguajes, probablemente el más habitual hoy en día, que en lugar de compilar a código nativo compilan a algún tipo de código intermedio que luego es interpretado o compilado just in time a código nativo. La mayoría de los lenguajes que se ejecutan sobre la JVM o sobre el CLR son de este tipo.

Es una estrategia de aprendizaje mixta.

Se realiza un aprendizaje a priori menos profundo que el que se haría con la estrategia del compilador de código nativo, pero ese aprendizaje a priori nos simplifica mucho la fase de interpretación o compilación just in time. Al haber hecho el trabajo de aprendizaje previo, hemos convertido el código fuente en algo más simple (el código intermedio) que resulta más fácil de entender para el intérprete y, por tanto, puede ejecutarlo (o compilarlo just in time) en menos tiempo y aplicar más optimizaciones.

Por ejemplo, el lenguaje original puede tener conceptos relativamente complicados, como async/await o generators o lambdas, y en la fase de compilación a código intermedio convertir todo eso a algo más homogéneo, como clases. Así, el compilador just in time sólo necesita lidiar con el concepto de clase y pueder ser más simple.

Siguiendo con nuestros paralelismos con el aprendizaje, sería equivalente a conocer a priori unos conceptos básicos que nos faciliten luego el aprendizaje “sobre la marcha” del resto de cosas. Si has decidado un tiempo a conocer cómo funciona el patrón MVC, te resultará mucho más sencillo entender cómo funcionan librerías ASP.NET MVC, Struts, o Rails, y no tendrás que reaprenderlo una y otra vez con cada nueva librería.

Los sistemas de este tipo más avanzados son capaces además de variar el esfuerzo dedicado a aprender cómo ejecutar cada parte del código. Pueden empezar por no complicarse mucho la vida y ejecutarlo de la forma más sencilla posible, pero si ven que es un código que se ejecuta muchas veces, pueden recompilarlo aplicando más optimizaciones, e incluso hacerlo teniendo en cuenta no sólo el aspecto del código, sino la forma en que se suele ejecutar.

Ésta es una idea muy interesante cuando estamos aplicando una estrategia de aprendizaje sobre la marcha. Está muy bien avanzar, sacar trabajo adelante, aportar valor y todo eso, pero está aún mejor dedicar tiempo a analizar lo que estamos haciendo y profundizar en ello cuando vemos que es algo que estamos haciendo continuamente. Esa labor de introspección es fundamental para seguir mejorando y no quedarnos con la primera solución que hemos encontrado.

La parte mala de este tipo de aproximación, claro, es que te quedas un poco en tierra de nadie. Hace falta invertir algo de tiempo al compilar, no como en los lenguajes puramente interpretados, y la velocidad de ejecución, sobre todo al principio, no es tan buena como la de un lenguaje compilado a nativo porque hay que invertir algo de tiempo en hacer la interpretación o compilación just in time.

Conclusión

Siempre parece que la virtud es el término medio entre dos extremos, y en este caso el equilibrio que ofrece una estrategia basada en conocer principios fundamentales a priori e irlos completando “sobre la marcha” con cosas más concretas, suena bastante razonable.

Eso no quiere decir que no haya escenarios en los que merezca la pena utilizar alguna de las otras estrategias. La capacidad de adaptación de una estrategía 100% sobre la marcha puede ser muy útil en escenarios muy inciertos, y dedicar mucho tiempo por adelantado a aprender todo lo necesario para resolver un problema puede suponer una ventaja importante a medio/largo plazo si conseguimos rentabilizar todo ese aprendizaje a priori.

Cuestión de contexto.


2 comentarios en “Lo que los compiladores pueden enseñarnos sobre aprendizaje

  1. Como en todo depende del contexto, pero como si dice por acá, ni tan tan ni muy muy, el balance entre lo teorico y lo practico siempre ayudará mucho.

    Aunque valoro la teoria y los cursos, me interesa también la experiencia, por que en teoria la practica y la teoria son lo mismo, pero en la practica no lo son.

    Excelente articulo, nunca hubiera pensado esa analogica en cuanto al tipo de lenguajes y el como aprendemos a programar.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>