Loading...

LA VENGANZA DE LOS NERDS

Original

Mayo de 2002

"Íbamos tras los programadores de C++. Logramos arrastrar a muchos de ellos hasta la mitad del camino hacia Lisp".

  • Guy Steele, coautor de la especificación de Java

En el negocio del software, existe una lucha constante entre los académicos de cabeza puntiaguda y otra fuerza igualmente formidable, los jefes de cabeza puntiaguda. Todos saben quién es el jefe de cabeza puntiaguda, ¿verdad? Creo que la mayoría de las personas en el mundo de la tecnología no solo reconocen este personaje de dibujos animados, sino que también conocen a la persona real en su empresa en la que se basa.

El jefe de cabeza puntiaguda combina milagrosamente dos cualidades que son comunes por sí mismas, pero rara vez se ven juntas: (a) no sabe absolutamente nada sobre tecnología, y (b) tiene opiniones muy fuertes al respecto.

Supongamos, por ejemplo, que necesitas escribir un software. El jefe de cabeza puntiaguda no tiene idea de cómo debe funcionar este software y no puede distinguir un lenguaje de programación de otro, pero sabe en qué lenguaje debes escribirlo. Exactamente. Él piensa que debes escribirlo en Java.

¿Por qué piensa esto? Echemos un vistazo al interior del cerebro del jefe de cabeza puntiaguda. Lo que está pensando es algo así. Java es un estándar. Sé que debe serlo porque lo leo en la prensa todo el tiempo. Como es un estándar, no me meteré en problemas por usarlo. Y eso también significa que siempre habrá muchos programadores de Java, así que si los programadores que trabajan para mí ahora renuncian, como los programadores que trabajan para mí siempre hacen misteriosamente, puedo reemplazarlos fácilmente.

Bueno, esto no suena tan irrazonable. Pero todo se basa en una suposición tácita, y esa suposición resulta ser falsa. El jefe de cabeza puntiaguda cree que todos los lenguajes de programación son más o menos equivalentes. Si eso fuera cierto, estaría en lo cierto. Si los lenguajes son todos equivalentes, claro, usa el lenguaje que todos los demás están usando.

Pero todos los lenguajes no son equivalentes, y creo que puedo demostrarte esto sin siquiera entrar en las diferencias entre ellos. Si le preguntaras al jefe de cabeza puntiaguda en 1992 en qué lenguaje debería escribirse el software, habría respondido con la misma poca vacilación que lo hace hoy. El software debe escribirse en C++. Pero si los lenguajes son todos equivalentes, ¿por qué debería cambiar la opinión del jefe de cabeza puntiaguda? De hecho, ¿por qué los desarrolladores de Java se habrían molestado en crear un nuevo lenguaje?

Presumiblemente, si creas un nuevo lenguaje, es porque crees que es mejor de alguna manera que lo que la gente ya tenía. Y de hecho, Gosling deja claro en el primer libro blanco de Java que Java fue diseñado para solucionar algunos problemas con C++. Así que ahí lo tienes: los lenguajes no son todos equivalentes. Si sigues el rastro a través del cerebro del jefe de cabeza puntiaguda hasta Java y luego retrocedes a través de la historia de Java hasta sus orígenes, terminas sosteniendo una idea que contradice la suposición con la que comenzaste.

Entonces, ¿quién tiene razón? ¿James Gosling o el jefe de cabeza puntiaguda? Como era de esperar, Gosling tiene razón. Algunos lenguajes son mejores, para ciertos problemas, que otros. Y ya sabes, eso plantea algunas preguntas interesantes. Java fue diseñado para ser mejor, para ciertos problemas, que C++. ¿Qué problemas? ¿Cuándo es mejor Java y cuándo es C++? ¿Hay situaciones en las que otros lenguajes son mejores que cualquiera de ellos?

Una vez que comienzas a considerar esta pregunta, has abierto una verdadera caja de Pandora. Si el jefe de cabeza puntiaguda tuviera que pensar en el problema en toda su complejidad, le haría explotar el cerebro. Mientras considere que todos los lenguajes son equivalentes, todo lo que tiene que hacer es elegir el que parece tener más impulso, y dado que eso es más una cuestión de moda que de tecnología, incluso él probablemente puede obtener la respuesta correcta. Pero si los lenguajes varían, de repente tiene que resolver dos ecuaciones simultáneas, tratando de encontrar un equilibrio óptimo entre dos cosas que no conoce: la idoneidad relativa de los veinte o más lenguajes principales para el problema que necesita resolver, y las probabilidades de encontrar programadores, bibliotecas, etc. para cada uno. Si eso es lo que hay al otro lado de la puerta, no es de extrañar que el jefe de cabeza puntiaguda no quiera abrirla.

La desventaja de creer que todos los lenguajes de programación son equivalentes es que no es cierto. Pero la ventaja es que te hace la vida mucho más simple. Y creo que esa es la razón principal por la que la idea está tan extendida. Es una idea cómoda.

Sabemos que Java debe ser bastante bueno, porque es el lenguaje de programación nuevo y genial. ¿O no? Si miras el mundo de los lenguajes de programación desde la distancia, parece que Java es lo último. (Desde lo suficientemente lejos, todo lo que puedes ver es la gran valla publicitaria parpadeante pagada por Sun). Pero si miras este mundo de cerca, encontrarás que hay grados de genialidad. Dentro de la subcultura de los hackers, hay otro lenguaje llamado Perl que se considera mucho más genial que Java. Slashdot, por ejemplo, está generado por Perl. No creo que encuentres a esos tipos usando Java Server Pages. Pero hay otro lenguaje más nuevo, llamado Python, cuyos usuarios tienden a menospreciar a Perl, y más esperando en las alas.

Si miras estos lenguajes en orden, Java, Perl, Python, notas un patrón interesante. Al menos, notas este patrón si eres un hacker de Lisp. Cada uno es progresivamente más parecido a Lisp. Python copia incluso características que muchos hackers de Lisp consideran errores. Podrías traducir programas simples de Lisp a Python línea por línea. Es 2002, y los lenguajes de programación casi han alcanzado los niveles de 1958.

Poniéndose al día con las matemáticas

Lo que quiero decir es que Lisp fue descubierto por primera vez por John McCarthy en 1958, y los lenguajes de programación populares recién ahora están alcanzando las ideas que desarrolló entonces.

Ahora, ¿cómo podría ser eso cierto? ¿No es la tecnología informática algo que cambia muy rápidamente? Quiero decir, en 1958, las computadoras eran monstruos del tamaño de un refrigerador con la potencia de procesamiento de un reloj de pulsera. ¿Cómo podría una tecnología tan antigua ser relevante, y mucho menos superior a los últimos desarrollos?

Te diré cómo. Es porque Lisp no fue realmente diseñado para ser un lenguaje de programación, al menos no en el sentido que entendemos hoy. Lo que entendemos por un lenguaje de programación es algo que usamos para decirle a una computadora qué hacer. McCarthy sí tenía la intención de desarrollar un lenguaje de programación en este sentido, pero el Lisp que realmente terminamos usando se basó en algo separado que hizo como un ejercicio teórico-- un esfuerzo para definir una alternativa más conveniente a la máquina de Turing. Como dijo McCarthy más tarde,

Otra forma de demostrar que Lisp era más limpio que las máquinas de Turing era escribir una función Lisp universal y demostrar que es más breve y comprensible que la descripción de una máquina de Turing universal. Esta fue la función Lisp eval, ... que calcula el valor de una expresión Lisp.... Escribir eval requirió inventar una notación que representara las funciones Lisp como datos Lisp, y dicha notación se ideó para los propósitos del documento sin pensar que se usaría para expresar programas Lisp en la práctica.

Lo que sucedió después fue que, en algún momento de finales de 1958, Steve Russell, uno de los estudiantes de posgrado de McCarthy, miró esta definición de eval y se dio cuenta de que si la traducía a lenguaje de máquina, el resultado sería un intérprete de Lisp.

Esto fue una gran sorpresa en ese momento. Esto es lo que dijo McCarthy al respecto más tarde en una entrevista:

Steve Russell dijo, mira, ¿por qué no programo este eval..., y le dije, ¡oh, oh!, estás confundiendo la teoría con la práctica, este eval está destinado a la lectura, no al cálculo. Pero siguió adelante y lo hizo. Es decir, compiló el eval en mi documento en código de máquina [IBM] 704, corrigiendo errores, y luego lo promocionó como un intérprete de Lisp, que ciertamente lo era. Entonces, en ese momento, Lisp tenía esencialmente la forma que tiene hoy en día....

De repente, en cuestión de semanas creo, McCarthy encontró su ejercicio teórico transformado en un lenguaje de programación real, y uno más poderoso de lo que había pretendido.

Entonces, la breve explicación de por qué este lenguaje de la década de 1950 no está obsoleto es que no era tecnología sino matemáticas, y las matemáticas no se ponen rancias. Lo correcto para comparar Lisp no es el hardware de la década de 1950, sino, digamos, el algoritmo Quicksort, que se descubrió en 1960 y sigue siendo la clasificación de propósito general más rápida.

Hay otro lenguaje que aún sobrevive de la década de 1950, Fortran, y representa el enfoque opuesto al diseño de lenguajes. Lisp fue una pieza de teoría que inesperadamente se convirtió en un lenguaje de programación. Fortran se desarrolló intencionalmente como un lenguaje de programación, pero lo que ahora consideraríamos un lenguaje de muy bajo nivel.

Fortran I, el lenguaje que se desarrolló en 1956, era un animal muy diferente al Fortran actual. Fortran I era prácticamente lenguaje ensamblador con matemáticas. En cierto modo, era menos poderoso que los lenguajes ensambladores más recientes; no había subrutinas, por ejemplo, solo ramas. El Fortran actual ahora es posiblemente más cercano a Lisp que a Fortran I.

Lisp y Fortran fueron los troncos de dos árboles evolutivos separados, uno enraizado en las matemáticas y otro en la arquitectura de la máquina. Estos dos árboles han estado convergiendo desde entonces. Lisp comenzó siendo poderoso, y durante los siguientes veinte años se volvió rápido. Los llamados lenguajes convencionales comenzaron siendo rápidos, y durante los siguientes cuarenta años gradualmente se volvieron más poderosos, hasta que ahora los más avanzados de ellos están bastante cerca de Lisp. Cerca, pero todavía les faltan algunas cosas....

Qué hizo diferente a Lisp

Cuando se desarrolló por primera vez, Lisp incorporó nueve ideas nuevas. Algunas de ellas las damos por sentadas ahora, otras solo se ven en lenguajes más avanzados, y dos siguen siendo exclusivas de Lisp. Las nueve ideas son, en orden de su adopción por la corriente principal,

Condicionales. Un condicional es una construcción if-then-else. Ahora los damos por sentados, pero Fortran I no los tenía. Solo tenía un goto condicional basado estrechamente en la instrucción de máquina subyacente.

Un tipo de función. En Lisp, las funciones son un tipo de datos como los enteros o las cadenas. Tienen una representación literal, se pueden almacenar en variables, se pueden pasar como argumentos, etc.

Recursión. Lisp fue el primer lenguaje de programación en admitirlo.

Tipado dinámico. En Lisp, todas las variables son efectivamente punteros. Los valores son los que tienen tipos, no las variables, y asignar o enlazar variables significa copiar punteros, no a lo que apuntan.

Recopilación de basura.

Programas compuestos por expresiones. Los programas Lisp son árboles de expresiones, cada uno de los cuales devuelve un valor. Esto contrasta con Fortran y la mayoría de los lenguajes posteriores, que distinguen entre expresiones y declaraciones.

Era natural tener esta distinción en Fortran I porque no se podían anidar declaraciones. Y así, mientras que se necesitaban expresiones para que las matemáticas funcionaran, no tenía sentido hacer que nada más devolviera un valor, porque no podía haber nada esperando.

Esta limitación desapareció con la llegada de los lenguajes estructurados en bloques, pero para entonces ya era demasiado tarde. La distinción entre expresiones y declaraciones estaba arraigada. Se extendió de Fortran a Algol y luego a ambos descendientes.

Un tipo de símbolo. Los símbolos son efectivamente punteros a cadenas almacenadas en una tabla hash. Entonces puedes probar la igualdad comparando un puntero, en lugar de comparar cada carácter.

Una notación para el código que utiliza árboles de símbolos y constantes.

Todo el lenguaje está ahí todo el tiempo. No hay una distinción real entre el tiempo de lectura, el tiempo de compilación y el tiempo de ejecución. Puedes compilar o ejecutar código mientras lees, leer o ejecutar código mientras compilas, y leer o compilar código en tiempo de ejecución.

Ejecutar código en tiempo de lectura permite a los usuarios reprogramar la sintaxis de Lisp; ejecutar código en tiempo de compilación es la base de las macros; compilar en tiempo de ejecución es la base del uso de Lisp como lenguaje de extensión en programas como Emacs; y leer en tiempo de ejecución permite a los programas comunicarse usando s-expresiones, una idea recientemente reinventada como XML.

Cuando Lisp apareció por primera vez, estas ideas estaban muy lejos de la práctica de programación ordinaria, que estaba dictada en gran medida por el hardware disponible a fines de la década de 1950. Con el tiempo, el lenguaje predeterminado, encarnado en una sucesión de lenguajes populares, ha evolucionado gradualmente hacia Lisp. Las ideas 1-5 ahora están muy extendidas. El número 6 está comenzando a aparecer en la corriente principal. Python tiene una forma de 7, aunque no parece haber ninguna sintaxis para ello.

En cuanto al número 8, este puede ser el más interesante de todos. Las ideas 8 y 9 solo se convirtieron en parte de Lisp por accidente, porque Steve Russell implementó algo que McCarthy nunca había tenido la intención de implementar. Y sin embargo, estas ideas resultan ser responsables tanto de la apariencia extraña de Lisp como de sus características más distintivas. Lisp se ve extraño no tanto porque tiene una sintaxis extraña, sino porque no tiene sintaxis; expresas programas directamente en los árboles de análisis que se construyen entre bastidores cuando se analizan otros lenguajes, y estos árboles están hechos de listas, que son estructuras de datos Lisp.

Expresar el lenguaje en sus propias estructuras de datos resulta ser una característica muy poderosa. Las ideas 8 y 9 juntas significan que puedes escribir programas que escriben programas. Eso puede sonar como una idea extraña, pero es algo cotidiano en Lisp. La forma más común de hacerlo es con algo llamado macro.

El término "macro" no significa en Lisp lo que significa en otros lenguajes. Una macro Lisp puede ser cualquier cosa, desde una abreviatura hasta un compilador para un nuevo lenguaje. Si realmente quieres entender Lisp, o simplemente expandir tus horizontes de programación, te recomiendo que aprendas más sobre las macros.

Las macros (en el sentido de Lisp) siguen siendo, que yo sepa, exclusivas de Lisp. Esto se debe en parte a que para tener macros probablemente tengas que hacer que tu lenguaje se vea tan extraño como Lisp. También puede ser porque si agregas ese incremento final de poder, ya no puedes afirmar haber inventado un nuevo lenguaje, sino solo un nuevo dialecto de Lisp.

Menciono esto principalmente como una broma, pero es bastante cierto. Si defines un lenguaje que tiene car, cdr, cons, quote, cond, atom, eq, y una notación para funciones expresadas como listas, entonces puedes construir todo el resto de Lisp a partir de él. Esa es, de hecho, la cualidad definitoria de Lisp: fue para hacer esto que McCarthy le dio a Lisp la forma que tiene.

Dónde importan los lenguajes

Entonces, supongamos que Lisp representa una especie de límite al que los lenguajes convencionales se están acercando asintóticamente, ¿significa eso que realmente deberías usarlo para escribir software? ¿Cuánto pierdes al usar un lenguaje menos poderoso? ¿No es más sabio, a veces, no estar en la vanguardia de la innovación? ¿Y no es la popularidad, en cierta medida, su propia justificación? ¿No tiene razón el jefe de cabeza puntiaguda, por ejemplo, al querer usar un lenguaje para el que pueda contratar programadores fácilmente?

Hay, por supuesto, proyectos en los que la elección del lenguaje de programación no importa mucho. Como regla general, cuanto más exigente sea la aplicación, más apalancamiento obtendrás al usar un lenguaje poderoso. Pero muchos proyectos no son exigentes en absoluto. La mayor parte de la programación probablemente consiste en escribir pequeños programas de unión, y para los pequeños programas de unión puedes usar cualquier lenguaje con el que ya estés familiarizado y que tenga buenas bibliotecas para lo que necesites hacer. Si solo necesitas alimentar datos de una aplicación de Windows a otra, claro, usa Visual Basic.

También puedes escribir pequeños programas de unión en Lisp (lo uso como una calculadora de escritorio), pero la mayor ganancia para lenguajes como Lisp está en el otro extremo del espectro, donde necesitas escribir programas sofisticados para resolver problemas difíciles frente a una feroz competencia. Un buen ejemplo es el programa de búsqueda de tarifas aéreas que ITA Software licencia a Orbitz. Estos tipos entraron a un mercado ya dominado por dos grandes competidores arraigados, Travelocity y Expedia, y parecen haberlos humillado tecnológicamente.

El núcleo de la aplicación de ITA es un programa Common Lisp de 200.000 líneas que busca muchos órdenes de magnitud más posibilidades que sus competidores, quienes aparentemente todavía están utilizando técnicas de programación de la era de las mainframes. (Aunque ITA también está, en cierto sentido, utilizando un lenguaje de programación de la era de las mainframes). Nunca he visto ninguno de los códigos de ITA, pero según uno de sus mejores hackers, utilizan muchas macros, y no me sorprende escucharlo.

Fuerzas centrípetas

No estoy diciendo que no haya un costo por usar tecnologías poco comunes. El jefe de cabeza puntiaguda no está completamente equivocado al preocuparse por esto. Pero como no entiende los riesgos, tiende a magnificarlos.

Se me ocurren tres problemas que podrían surgir al usar lenguajes menos comunes. Tus programas podrían no funcionar bien con programas escritos en otros lenguajes. Podrías tener menos bibliotecas a tu disposición. Y podrías tener problemas para contratar programadores.

¿Cuánto problema representa cada uno de estos? La importancia del primero varía según tengas o no control sobre todo el sistema. Si estás escribiendo software que tiene que ejecutarse en la máquina de un usuario remoto encima de un sistema operativo cerrado y con errores (no menciono nombres), puede haber ventajas en escribir tu aplicación en el mismo lenguaje que el sistema operativo. Pero si controlas todo el sistema y tienes el código fuente de todas las partes, como presumiblemente lo hace ITA, puedes usar los lenguajes que quieras. Si surge alguna incompatibilidad, puedes arreglarla tú mismo.

En las aplicaciones basadas en servidores, puedes salirte con la tuya usando las tecnologías más avanzadas, y creo que esta es la causa principal de lo que Jonathan Erickson llama el "renacimiento del lenguaje de programación." Esta es la razón por la que incluso escuchamos sobre nuevos lenguajes como Perl y Python. No estamos escuchando sobre estos lenguajes porque la gente los está usando para escribir aplicaciones de Windows, sino porque la gente los está usando en servidores. Y a medida que el software se desplaza fuera del escritorio y hacia los servidores (un futuro al que incluso Microsoft parece resignado), habrá menos y menos presión para usar tecnologías intermedias.

En cuanto a las bibliotecas, su importancia también depende de la aplicación. Para problemas menos exigentes, la disponibilidad de bibliotecas puede superar el poder intrínseco del lenguaje. ¿Dónde está el punto de equilibrio? Difícil de decir exactamente, pero dondequiera que esté, está por debajo de cualquier cosa que puedas llamar aplicación. Si una empresa se considera en el negocio del software y está escribiendo una aplicación que será uno de sus productos, probablemente involucrará a varios hackers y tardará al menos seis meses en escribirse. En un proyecto de ese tamaño, los lenguajes poderosos probablemente comienzan a superar la conveniencia de las bibliotecas preexistentes.

La tercera preocupación del jefe de cabeza puntiaguda, la dificultad de contratar programadores, creo que es una cortina de humo. Después de todo, ¿cuántos hackers necesitas contratar? Seguramente, a estas alturas todos sabemos que el software se desarrolla mejor en equipos de menos de diez personas. Y no deberías tener problemas para contratar hackers a esa escala para cualquier lenguaje que alguien haya escuchado. Si no puedes encontrar diez hackers de Lisp, entonces tu empresa probablemente está ubicada en la ciudad equivocada para desarrollar software.

De hecho, elegir un lenguaje más poderoso probablemente disminuya el tamaño del equipo que necesitas, porque (a) si usas un lenguaje más poderoso, probablemente no necesitarás tantos hackers, y (b) los hackers que trabajan en lenguajes más avanzados probablemente sean más inteligentes.

No estoy diciendo que no vayas a recibir mucha presión para usar lo que se percibe como tecnologías "estándar". En Viaweb (ahora Yahoo Store), levantamos algunas cejas entre los capitalistas de riesgo y los posibles compradores al usar Lisp. Pero también levantamos cejas al usar cajas genéricas de Intel como servidores en lugar de servidores de "resistencia industrial" como Suns, al usar una variante de Unix de código abierto entonces oscura llamada FreeBSD en lugar de un verdadero sistema operativo comercial como Windows NT, al ignorar un supuesto estándar de comercio electrónico llamado SET que ahora nadie recuerda, y así sucesivamente.

No puedes dejar que los trajes tomen decisiones técnicas por ti. ¿Atemorizó a algunos posibles compradores que usáramos Lisp? Algunos, ligeramente, pero si no hubiéramos usado Lisp, no hubiéramos podido escribir el software que los hizo querer comprarnos. Lo que les pareció una anomalía fue, de hecho, causa y efecto.

Si comienzas una startup, no diseñes tu producto para complacer a los capitalistas de riesgo o los posibles compradores. Diseña tu producto para complacer a los usuarios. Si ganas a los usuarios, todo lo demás seguirá. Y si no lo haces, a nadie le importará cuán cómodamente ortodoxas fueron tus elecciones tecnológicas.

El costo de ser promedio

¿Cuánto pierdes al usar un lenguaje menos poderoso? En realidad, hay algunos datos sobre eso.

La medida más conveniente del poder es probablemente el tamaño del código. El objetivo de los lenguajes de alto nivel es darte abstracciones más grandes, ladrillos más grandes, por así decirlo, para que no necesites tantos para construir un muro de un tamaño determinado. Entonces, cuanto más poderoso sea el lenguaje, más corto será el programa (no simplemente en caracteres, por supuesto, sino en elementos distintos).

¿Cómo te permite un lenguaje más poderoso escribir programas más cortos? Una técnica que puedes usar, si el lenguaje te lo permite, es algo llamado programación ascendente. En lugar de simplemente escribir tu aplicación en el lenguaje base, construyes sobre el lenguaje base un lenguaje para escribir programas como el tuyo, luego escribes tu programa en él. El código combinado puede ser mucho más corto que si hubieras escrito todo tu programa en el lenguaje base; de hecho, así es como funcionan la mayoría de los algoritmos de compresión. Un programa ascendente también debería ser más fácil de modificar, porque en muchos casos la capa de lenguaje no tendrá que cambiar en absoluto.

El tamaño del código es importante, porque el tiempo que se tarda en escribir un programa depende principalmente de su longitud. Si tu programa fuera tres veces más largo en otro lenguaje, tardará tres veces más en escribirse, y no puedes evitar esto contratando más gente, porque más allá de cierto tamaño, las nuevas contrataciones son en realidad una pérdida neta. Fred Brooks describió este fenómeno en su famoso libro El mito del hombre-mes, y todo lo que he visto ha tendido a confirmar lo que dijo.

Entonces, ¿cuánto más cortos son tus programas si los escribes en Lisp? La mayoría de los números que he escuchado para Lisp versus C, por ejemplo, han estado alrededor de 7-10x. Pero un artículo reciente sobre ITA en la revista New Architect dijo que "una línea de Lisp puede reemplazar 20 líneas de C", y dado que este artículo estaba lleno de citas del presidente de ITA, supongo que obtuvieron este número de ITA. Si es así, entonces podemos confiar en él; el software de ITA incluye mucho C y C++ además de Lisp, por lo que hablan por experiencia.

Supongo que estos múltiplos ni siquiera son constantes. Creo que aumentan cuando te enfrentas a problemas más difíciles y también cuando tienes programadores más inteligentes. Un hacker realmente bueno puede exprimir más de las mejores herramientas.

Como punto de datos en la curva, en cualquier caso, si tuvieras que competir con ITA y eligieras escribir tu software en C, ellos podrían desarrollar software veinte veces más rápido que tú. Si dedicaras un año a una nueva función, ellos podrían duplicarla en menos de tres semanas. Mientras que si ellos dedicaran solo tres meses a desarrollar algo nuevo, serían cinco años antes de que tú también lo tuvieras.

¿Y sabes qué? Ese es el mejor escenario. Cuando hablas de razones de tamaño de código, estás asumiendo implícitamente que realmente puedes escribir el programa en el lenguaje más débil. Pero de hecho, hay límites en lo que los programadores pueden hacer. Si estás tratando de resolver un problema difícil con un lenguaje que es demasiado de bajo nivel, llegas a un punto en el que simplemente hay demasiado para mantener en tu cabeza a la vez.

Entonces, cuando digo que el competidor imaginario de ITA tardaría cinco años en duplicar algo que ITA podría escribir en Lisp en tres meses, quiero decir cinco años si nada sale mal. De hecho, la forma en que funcionan las cosas en la mayoría de las empresas, cualquier proyecto de desarrollo que tardaría cinco años es probable que nunca se termine.

Admito que este es un caso extremo. Los hackers de ITA parecen ser inusualmente inteligentes, y C es un lenguaje bastante de bajo nivel. Pero en un mercado competitivo, incluso una diferencia de dos o tres a uno sería suficiente para garantizar que siempre estarías por detrás.

Una receta

Este es el tipo de posibilidad en la que el jefe de cabeza puntiaguda ni siquiera quiere pensar. Y así, la mayoría de ellos no lo hacen. Porque, ya sabes, cuando se trata de eso, al jefe de cabeza puntiaguda no le importa si su empresa recibe una patada en el trasero, siempre y cuando nadie pueda demostrar que es su culpa. El plan más seguro para él personalmente es mantenerse cerca del centro de la manada.

Dentro de las grandes organizaciones, la frase utilizada para describir este enfoque es "mejores prácticas de la industria". Su propósito es proteger al jefe de cabeza puntiaguda de la responsabilidad: si elige algo que es "mejores prácticas de la industria", y la empresa pierde, no se le puede culpar. Él no eligió, la industria lo hizo.

Creo que este término se usó originalmente para describir los métodos de contabilidad, etc. Lo que significa, aproximadamente, es no hagas nada raro. Y en contabilidad, esa probablemente sea una buena idea. Los términos "vanguardia" y "contabilidad" no suenan bien juntos. Pero cuando importas este criterio a las decisiones sobre tecnología, comienzas a obtener las respuestas incorrectas.

La tecnología a menudo debería ser de vanguardia. En los lenguajes de programación, como ha señalado Erann Gat, lo que realmente te dan las "mejores prácticas de la industria" no es lo mejor, sino simplemente el promedio. Cuando una decisión hace que desarrolles software a una fracción de la velocidad de los competidores más agresivos, "mejores prácticas" es un nombre inapropiado.

Así que aquí tenemos dos piezas de información que creo que son muy valiosas. De hecho, lo sé por mi propia experiencia. Número 1, los lenguajes varían en potencia. Número 2, la mayoría de los gerentes ignoran esto deliberadamente. Entre ellos, estos dos hechos son literalmente una receta para ganar dinero. ITA es un ejemplo de esta receta en acción. Si quieres ganar en un negocio de software, simplemente asume el problema más difícil que puedas encontrar, usa el lenguaje más poderoso que puedas obtener y espera a que los jefes de cabeza puntiaguda de tus competidores vuelvan a la media.

Apéndice: Poder

Como ilustración de lo que quiero decir sobre el poder relativo de los lenguajes de programación, considera el siguiente problema. Queremos escribir una función que genere acumuladores, una función que tome un número n, y devuelva una función que tome otro número i y devuelva n incrementado por i.

(Eso es incrementado por, no más. Un acumulador tiene que acumular).

En Common Lisp esto sería


(defun foo (n)
(lambda (i) (incf n i)))

y en Perl 5,


sub foo {
my ($n) = @_;
sub {$n += shift}
}

que tiene más elementos que la versión Lisp porque tienes que extraer parámetros manualmente en Perl.

En Smalltalk, el código es ligeramente más largo que en Lisp


foo: n
|s|
s := n.
^[:i| s := s+i. ]

porque aunque en general las variables léxicas funcionan, no puedes hacer una asignación a un parámetro, por lo que tienes que crear una nueva variable s.

En Javascript, el ejemplo es, de nuevo, ligeramente más largo, porque Javascript conserva la distinción entre declaraciones y expresiones, por lo que necesitas declaraciones de retorno explícitas para devolver valores:


function foo(n) {
return function (i) {
return n += i } }

(Para ser justos, Perl también conserva esta distinción, pero la trata de la manera típica de Perl, permitiéndote omitir los retornos).

Si intentas traducir el código Lisp/Perl/Smalltalk/Javascript a Python, te encuentras con algunas limitaciones. Debido a que Python no admite completamente las variables léxicas, tienes que crear una estructura de datos para almacenar el valor de n. Y aunque Python tiene un tipo de datos de función, no hay una representación literal para uno (a menos que el cuerpo sea solo una sola expresión), por lo que necesitas crear una función con nombre para devolver. Esto es lo que terminas con:


def foo(n):
s = [n]
def bar(i):
s[0] += i
return s[0]
return bar

Los usuarios de Python podrían preguntar legítimamente por qué no pueden simplemente escribir


def foo(n):
return lambda i: return n += i

o incluso


def foo(n):
lambda i: n += i

y supongo que probablemente lo harán, algún día. (Pero si no quieren esperar a que Python evolucione el resto del camino hacia Lisp, siempre podrían simplemente...)

En los lenguajes OO, puedes, en cierta medida, simular un cierre (una función que se refiere a variables definidas en ámbitos de cierre) definiendo una clase con un método y un campo para reemplazar cada variable de un ámbito de cierre. Esto hace que el programador haga el tipo de análisis de código que haría el compilador en un lenguaje con soporte completo para el ámbito léxico, y no funcionará si más de una función se refiere a la misma variable, pero es suficiente en casos simples como este.

Los expertos en Python parecen estar de acuerdo en que esta es la forma preferida de resolver el problema en Python, escribiendo


def foo(n):
class acc:
def __init__(self, s):
self.s = s
def inc(self, i):
self.s += i
return self.s
return acc(n).inc

o


class foo:
def __init__(self, n):
self.n = n
def __call__(self, i):
self.n += i
return self.n

Incluiré estos porque no querría que los defensores de Python dijeran que estaba tergiversando el lenguaje, pero ambos me parecen más complejos que la primera versión. Estás haciendo lo mismo, configurando un lugar separado para almacenar el acumulador; es solo un campo en un objeto en lugar de la cabeza de una lista. Y el uso de estos nombres de campo especiales y reservados, especialmente call, parece un poco un truco.

En la rivalidad entre Perl y Python, la afirmación de los hackers de Python parece ser que Python es una alternativa más elegante a Perl, pero lo que muestra este caso es que el poder es la elegancia definitiva: el programa Perl es más simple (tiene menos elementos), incluso si la sintaxis es un poco más fea.

¿Qué pasa con otros lenguajes? En los otros lenguajes mencionados en esta charla, Fortran, C, C++, Java y Visual Basic, no está claro si realmente puedes resolver este problema. Ken Anderson dice que el siguiente código es lo más cerca que puedes llegar en Java:


public interface Inttoint {
public int call(int i);
}


public static Inttoint foo(final int n) {
return new Inttoint() {
int s = n;
public int call(int i) {
s = s + i;
return s;
}};
}

Esto no cumple con las especificaciones porque solo funciona para enteros. Después de muchos intercambios de correo electrónico con hackers de Java, diría que escribir una versión correctamente polimórfica que se comporte como los ejemplos anteriores está en algún lugar entre muy incómodo e imposible. Si alguien quiere escribir uno, me gustaría mucho verlo, pero personalmente me he agotado.

No es literalmente cierto que no puedas resolver este problema en otros lenguajes, por supuesto. El hecho de que todos estos lenguajes sean equivalentes a Turing significa que, estrictamente hablando, puedes escribir cualquier programa en cualquiera de ellos. Entonces, ¿cómo lo harías? En el caso límite, escribiendo un intérprete de Lisp en el lenguaje menos poderoso.

Eso suena como una broma, pero sucede tan a menudo en diversos grados en grandes proyectos de programación que hay un nombre para el fenómeno, la Décima Regla de Greenspun:

Cualquier programa C o Fortran suficientemente complicado contiene una implementación ad hoc, informalmente especificada, lenta y llena de errores de la mitad de Common Lisp.

Si intentas resolver un problema difícil, la pregunta no es si usarás un lenguaje lo suficientemente poderoso, sino si (a) usarás un lenguaje poderoso, (b) escribirás un intérprete de facto para uno, o (c) tú mismo te convertirás en un compilador humano para uno. Ya vemos que esto comienza a suceder en el ejemplo de Python, donde estamos, en efecto, simulando el código que generaría un compilador para implementar una variable léxica.

Esta práctica no solo es común, sino que está institucionalizada. Por ejemplo, en el mundo OO escuchas mucho sobre "patrones". Me pregunto si estos patrones no son a veces evidencia del caso (c), el compilador humano, en acción. Cuando veo patrones en mis programas, lo considero una señal de problemas. La forma de un programa solo debe reflejar el problema que necesita resolver. Cualquier otra regularidad en el código es una señal, al menos para mí, de que estoy usando abstracciones que no son lo suficientemente poderosas, a menudo que estoy generando a mano las expansiones de alguna macro que necesito escribir.

Notas

La CPU IBM 704 era aproximadamente del tamaño de un refrigerador, pero mucho más pesada. La CPU pesaba 3150 libras, y los 4K de RAM estaban en una caja separada que pesaba otras 4000 libras. El Sub-Zero 690, uno de los refrigeradores domésticos más grandes, pesa 656 libras.

Steve Russell también escribió el primer juego de computadora (digital), Spacewar, en 1962.

Si quieres engañar a un jefe de cabeza puntiaguda para que te permita escribir software en Lisp, podrías intentar decirle que es XML.

Aquí está el generador de acumuladores en otros dialectos de Lisp:


Scheme: (define (foo n)
(lambda (i) (set! n (+ n i)) n))
Goo:    (df foo (n) (op incf n _)))
Arc:    (def foo (n) [++ n _])

La triste historia de Erann Gat sobre las "mejores prácticas de la industria" en JPL me inspiró a abordar esta frase generalmente mal aplicada.

Peter Norvig descubrió que 16 de los 23 patrones en Design Patterns eran "invisibles o más simples" en Lisp.

Gracias a las muchas personas que respondieron a mis preguntas sobre diversos idiomas y/o leyeron borradores de esto, incluyendo Ken Anderson, Trevor Blackwell, Erann Gat, Dan Giffin, Sarah Harlin, Jeremy Hylton, Robert Morris, Peter Norvig, Guy Steele y Anton van Straaten. No son responsables de ninguna opinión expresada.

Relacionado:

Mucha gente ha respondido a esta charla, así que he creado una página adicional para tratar los temas que han planteado: Re: Revenge of the Nerds.

También provocó una discusión extensa y a menudo útil en la lista de correo LL1. Vea en particular el correo de Anton van Straaten sobre compresión semántica.

Algunos de los correos en LL1 me llevaron a intentar profundizar en el tema del poder del lenguaje en La concisión es poder.

Un conjunto más amplio de implementaciones canónicas del punto de referencia del generador de acumuladores se recopilan en su propia página.

Traducción al japonés, Traducción al español, Traducción al chino