VENGANZA DE LOS NERDS
OriginalMay 2002
"Estábamos tras los programadores de C++. Logramos arrastrar a muchos de ellos aproximadamente a la mitad hacia Lisp."
- Guy Steele, coautor de la especificación de Java
En el negocio del software hay una lucha continua entre los académicos de cabeza puntiaguda y otra fuerza igualmente formidable, los jefes de cabello puntiagudo. Todos saben quién es el jefe de cabello puntiagudo, ¿verdad? Creo que la mayoría de las personas en el mundo de la tecnología no solo reconocen a este personaje de caricatura, sino que conocen a la persona real en su empresa en la que está modelado.
El jefe de cabello puntiagudo combina milagrosamente dos cualidades que son comunes por sí solas, pero raramente se ven juntas: (a) no sabe nada sobre tecnología, y (b) tiene opiniones muy firmes al respecto.
Supongamos, por ejemplo, que necesitas escribir un software. El jefe de cabello puntiagudo no tiene idea de cómo debe funcionar este software, y no puede distinguir un lenguaje de programación de otro, y sin embargo sabe en qué lenguaje deberías escribirlo. Exactamente. Él piensa que deberías escribirlo en Java.
¿Por qué piensa esto? Echemos un vistazo dentro de la mente del jefe de cabello puntiagudo. Lo que está pensando es algo como esto. Java es un estándar. Sé que debe serlo, porque leo sobre ello en la prensa todo el tiempo. Dado que es un estándar, no tendré 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 misteriosamente siempre lo hacen, puedo reemplazarlos fácilmente.
Bueno, esto no suena tan irrazonable. Pero todo se basa en una suposición no expresada, y esa suposición resulta ser falsa. El jefe de cabello puntiagudo cree que todos los lenguajes de programación son prácticamente equivalentes. Si eso fuera cierto, tendría razón. Si los lenguajes son todos equivalentes, claro, usa el lenguaje que todos los demás están usando.
Pero no todos los lenguajes son equivalentes, y creo que puedo probar esto sin siquiera entrar en las diferencias entre ellos. Si le hubieras preguntado al jefe de cabello puntiagudo en 1992 en qué lenguaje debería escribirse el software, habría respondido con tan poca hesitación como lo hace hoy. El software debería escribirse en C++. Pero si los lenguajes son todos equivalentes, ¿por qué debería cambiar alguna vez la opinión del jefe de cabello puntiagudo? De hecho, ¿por qué deberían los desarrolladores de Java haberse molestado en crear un nuevo lenguaje?
Presumiblemente, si creas un nuevo lenguaje, es porque piensas que es mejor de alguna manera que lo que la gente ya tenía. Y de hecho, Gosling deja claro en el primer documento técnico 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 de la mente del jefe de cabello puntiagudo hacia Java y luego de regreso 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 cabello puntiagudo? No es sorprendente que Gosling tenga razón. Algunos lenguajes son mejores, para ciertos problemas, que otros. Y sabes, eso plantea algunas preguntas interesantes. Java fue diseñado para ser mejor, para ciertos problemas, que C++. ¿Qué problemas? ¿Cuándo es Java mejor y cuándo lo 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 sorpresas. Si el jefe de cabello puntiagudo tuviera que pensar en el problema en toda su complejidad, haría que su cerebro explotara. 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 de las que no sabe nada: 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 cabello puntiagudo 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 hace tu vida mucho más simple. Y creo que esa es la razón principal por la que la idea es tan generalizada. 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 cartelera parpadeante pagada por Sun). Pero si miras este mundo de cerca, descubres que hay grados de genialidad. Dentro de la subcultura hacker, 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 chicos 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 a 1958.
Alcanzando a 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 solo 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 behemoths del tamaño de un refrigerador con el poder de procesamiento de un reloj de pulsera. ¿Cómo podría alguna 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 queremos decir hoy. Lo que queremos decir con un lenguaje de programación es algo que usamos para decirle a una computadora qué hacer. McCarthy eventualmente tuvo la intención de desarrollar un lenguaje de programación en este sentido, pero el Lisp que realmente terminamos teniendo se basó en algo separado que hizo como un ejercicio teórico: un esfuerzo por definir una alternativa más conveniente a la Máquina de Turing. Como dijo McCarthy más tarde,
Otra forma de mostrar que Lisp era más ordenado que las máquinas de Turing era escribir una función universal de Lisp y mostrar que es más breve y comprensible que la descripción de una máquina de Turing universal. Esta fue la función de Lisp eval..., que calcula el valor de una expresión de Lisp.... Escribir eval requirió inventar una notación que representara funciones de Lisp como datos de Lisp, y tal notación fue ideada para los propósitos del documento sin pensar que se usaría para expresar programas de Lisp en la práctica.
Lo que sucedió a continuación fue que, en algún momento a 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. Aquí está lo que McCarthy dijo sobre ello más tarde en una entrevista:
Steve Russell dijo, mira, ¿por qué no programo este eval..., y yo le dije, ho, ho, estás confundiendo teoría con práctica, este eval está destinado a la lectura, no a la computación. Pero él 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 anunció como un intérprete de Lisp, que ciertamente lo era. Así que en ese momento Lisp tenía esencialmente la forma que tiene hoy....
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.
Así que la breve explicación de por qué este lenguaje de los años 50 no está obsoleto es que no era tecnología sino matemáticas, y las matemáticas no se vuelven obsoletas. Lo correcto es comparar Lisp no con el hardware de los años 50, sino, digamos, con el algoritmo Quicksort, que fue descubierto en 1960 y sigue siendo el más rápido para propósitos generales.
Hay otro lenguaje que aún sobrevive de los años 50, 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 fue desarrollado intencionalmente como un lenguaje de programación, pero lo que ahora consideraríamos uno de muy bajo nivel.
Fortran I, el lenguaje que se desarrolló en 1956, era un animal muy diferente del Fortran actual. Fortran I era prácticamente lenguaje ensamblador con matemáticas. En algunos aspectos era menos poderoso que los lenguajes ensambladores más recientes; no había subrutinas, por ejemplo, solo saltos. El Fortran actual es ahora, en cierto modo, más cercano a Lisp que a Fortran I.
Lisp y Fortran fueron los troncos de dos árboles evolutivos separados, uno enraizado en matemáticas y otro en arquitectura de máquinas. 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 aún les faltan algunas cosas....
Lo que Hizo a Lisp Diferente
Cuando se desarrolló por primera vez, Lisp incorporó nueve ideas nuevas. Algunas de estas ahora las damos por sentadas, otras solo se ven en lenguajes más avanzados, y dos siguen siendo únicas de Lisp. Las nueve ideas son, en orden de su adopción por el mainstream,
Condicionales. Un condicional es una construcción if-then-else. Ahora las damos por sentadas, pero Fortran I no las tenía. Solo tenía un goto condicional basado en la instrucción de máquina subyacente.
Un tipo de función. En Lisp, las funciones son un tipo de dato como los enteros o las cadenas. Tienen una representación literal, pueden almacenarse en variables, pueden pasarse como argumentos, y así sucesivamente.
Recursión. Lisp fue el primer lenguaje de programación en soportarla.
Tipado dinámico. En Lisp, todas las variables son efectivamente punteros. Los valores son los que tienen tipos, no las variables, y asignar o vincular variables significa copiar punteros, no lo que apuntan.
Recolección de basura.
Programas compuestos de expresiones. Los programas de Lisp son árboles de expresiones, cada una de las cuales devuelve un valor. Esto contrasta con Fortran y la mayoría de los lenguajes sucesores, que distinguen entre expresiones y declaraciones.
Era natural tener esta distinción en Fortran I porque no podías anidar declaraciones. Y así, aunque necesitabas expresiones para que las matemáticas funcionaran, no había sentido en hacer que nada más devolviera un valor, porque no podría haber nada esperando por ello.
Esta limitación desapareció con la llegada de los lenguajes estructurados por bloques, pero para entonces ya era demasiado tarde. La distinción entre expresiones y declaraciones estaba arraigada. Se extendió desde Fortran a Algol y luego a ambos sus descendientes.
Un tipo de símbolo. Los símbolos son efectivamente punteros a cadenas almacenadas en una tabla hash. Así que puedes probar la igualdad comparando un puntero, en lugar de comparar cada carácter.
Una notación para el código usando árboles de símbolos y constantes.
Todo el lenguaje ahí todo el tiempo. No hay una distinción real entre tiempo de lectura, tiempo de compilación y 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 los macros; compilar en tiempo de ejecución es la base del uso de Lisp como un 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 alejadas de la práctica de programación ordinaria, que estaba dictada en gran medida por el hardware disponible a finales de los años 50. Con el tiempo, el lenguaje por defecto, encarnado en una sucesión de lenguajes populares, ha evolucionado gradualmente hacia Lisp. Las ideas 1-5 ahora son generalizadas. El número 6 está comenzando a aparecer en el mainstream. Python tiene una forma de 7, aunque parece que no hay ninguna sintaxis para ello.
En cuanto al número 8, esta puede ser la más interesante de todas. Las ideas 8 y 9 solo se convirtieron en parte de Lisp por accidente, porque Steve Russell implementó algo que McCarthy nunca había pretendido que se implementara. Y, sin embargo, estas ideas resultan ser responsables tanto de la extraña apariencia de Lisp como de sus características más distintivas. Lisp se ve extraño no tanto porque tenga una sintaxis extraña, sino porque no tiene sintaxis; expresas programas directamente en los árboles de análisis que se construyen tras bambalinas cuando otros lenguajes son analizados, y estos árboles están hechos de listas, que son estructuras de datos de 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 de 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, debería aprender más sobre macros.
Las macros (en el sentido de Lisp) son todavía, hasta donde sé, únicas de Lisp. Esto es en parte porque para tener macros probablemente tienes que hacer que tu lenguaje se vea tan extraño como Lisp. También puede ser porque si agregas ese último incremento de poder, ya no puedes reclamar 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 ello. Esa es de hecho la calidad definitoria de Lisp: fue para hacer esto que McCarthy le dio a Lisp la forma que tiene.
Dónde Importan los Lenguajes
Así que supongamos que Lisp representa un tipo de límite que los lenguajes convencionales están acercándose asintóticamente, ¿significa eso que 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 el borde mismo de la innovación? ¿Y no es la popularidad hasta cierto punto su propia justificación? ¿No tiene razón el jefe de cabello puntiagudo, por ejemplo, al querer usar un lenguaje para el cual puede contratar fácilmente programadores?
Hay, por supuesto, proyectos donde la elección del lenguaje de programación no importa mucho. Como regla general, cuanto más exigente sea la aplicación, más ventaja obtienes al usar un lenguaje poderoso. Pero muchos proyectos no son exigentes en absoluto. La mayoría de la programación probablemente consiste en escribir pequeños programas de pegamento, y para pequeños programas de pegamento 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 pegamento en Lisp (yo lo uso como calculadora de escritorio), pero la mayor ventaja para lenguajes como Lisp está en el otro extremo del espectro, donde necesitas escribir programas sofisticados para resolver problemas difíciles en medio de una feroz competencia. Un buen ejemplo es el programa de búsqueda de tarifas aéreas que ITA Software licencia a Orbitz. Estos chicos entraron en 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 de 200,000 líneas en Common Lisp que busca muchas órdenes de magnitud más posibilidades que sus competidores, que aparentemente todavía están usando técnicas de programación de la era de mainframes. (Aunque ITA también está usando, en cierto sentido, un lenguaje de programación de la era de mainframes). Nunca he visto ningún código de ITA, pero según uno de sus principales hackers, usan muchas macros, y no me sorprende oírlo.
Fuerzas Centrípetas
No estoy diciendo que no haya un costo por usar tecnologías poco comunes. El jefe de cabello puntiagudo no está completamente equivocado al preocuparse por esto. Pero como no entiende los riesgos, tiende a magnificarlos.
Puedo pensar en 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.
¿Qué tan problemático es cada uno de estos? La importancia del primero varía dependiendo de si tienes control sobre todo el sistema. Si estás escribiendo software que tiene que ejecutarse en la máquina de un usuario remoto sobre un sistema operativo cerrado y con errores (no menciono nombres), puede haber ventajas en escribir tu aplicación en el mismo lenguaje que el SO. 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 solucionarlo tú mismo.
En aplicaciones basadas en servidores, puedes salirte con la tuya usando las tecnologías más avanzadas, y creo que esta es la principal causa de lo que Jonathan Erickson llama el "renacimiento del lenguaje de programación." Por eso 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 donde sea que esté, está lejos de cualquier cosa que probablemente llamarías una aplicación. Si una empresa se considera a sí misma en el negocio del software, y está escribiendo una aplicación que será uno de sus productos, entonces probablemente involucrará a varios hackers y tomará 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 cabello puntiagudo, la dificultad de contratar programadores, creo que es una distracción. ¿Cuántos hackers necesitas contratar, después de todo? Seguramente ahora 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 oído. 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 disminuye 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 son más propensos a ser más inteligentes.
No estoy diciendo que no recibirás 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 posibles adquirentes al usar Lisp. Pero también levantamos cejas al usar cajas genéricas de Intel como servidores en lugar de servidores "de fuerza industrial" como Suns, por usar una variante de Unix de código abierto entonces oscura llamada FreeBSD en lugar de un verdadero sistema operativo comercial como Windows NT, por 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. ¿Alarmó a algunos posibles adquirentes que usáramos Lisp? A algunos, ligeramente, pero si no hubiéramos usado Lisp, no habríamos podido escribir el software que les hizo querer comprarnos. Lo que parecía una anomalía para ellos fue de hecho causa y efecto.
Si inicias una startup, no diseñes tu producto para complacer a los capitalistas de riesgo o a posibles adquirentes. 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 ortodoxas y reconfortantes fueron tus elecciones tecnológicas.
El Costo de Ser Promedio
¿Cuánto pierdes al usar un lenguaje menos poderoso? De hecho, hay algunos datos sobre eso.
La medida más conveniente de 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 una pared de un tamaño dado. Así que 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 permite un lenguaje más poderoso que escribas programas más cortos? Una técnica que puedes usar, si el lenguaje te lo permite, es algo llamado programación de abajo hacia arriba. 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, y 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 de abajo hacia arriba debería ser más fácil de modificar también, 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 lleva escribir un programa depende principalmente de su longitud. Si tu programa sería tres veces más largo en otro lenguaje, tomará tres veces más tiempo escribirlo, y no puedes evitar esto contratando a más personas, 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 The Mythical Man-Month, y todo lo que he visto tiende a confirmar lo que dijo.
Entonces, ¿qué tan más cortos son tus programas si los escribes en Lisp? La mayoría de los números que he oído para Lisp frente a 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, asumo que obtuvieron este número de ITA. Si es así, entonces podemos confiar en él; el software de ITA incluye mucho de C y C++ así como de Lisp, así que están hablando desde la experiencia.
Mi suposición es que estos múltiplos ni siquiera son constantes. Creo que aumentan cuando enfrentas problemas más difíciles y también cuando tienes programadores más inteligentes. Un hacker realmente bueno puede exprimir más de mejores herramientas.
Como un punto de datos en la curva, en cualquier caso, si compitieras con ITA y eligieras escribir tu software en C, ellos podrían desarrollar software veinte veces más rápido que tú. Si pasas un año en una nueva característica, ellos podrían duplicarla en menos de tres semanas. Mientras que si ellos pasaran solo tres meses desarrollando algo nuevo, serían cinco años antes de que tú también lo tuvieras.
¿Y sabes qué? Ese es el mejor de los casos. Cuando hablas de proporciones de tamaño de código, estás asumiendo implícitamente que puedes escribir realmente 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 donde simplemente hay demasiado que mantener en tu cabeza a la vez.
Así que cuando digo que le llevaría al competidor imaginario de ITA cinco años 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 la mayoría de las empresas, cualquier proyecto de desarrollo que llevarí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 atrás.
Una Receta
Esta es la clase de posibilidad que el jefe de cabello puntiagudo ni siquiera quiere pensar. Y así, la mayoría de ellos no lo hacen. Porque, ya sabes, cuando se reduce a eso, al jefe de cabello puntiagudo no le importa si su empresa recibe una paliza, siempre y cuando nadie pueda probar que es su culpa. El plan más seguro para él personalmente es mantenerse cerca del centro del rebaño.
Dentro de grandes organizaciones, la frase utilizada para describir este enfoque es "mejor práctica de la industria". Su propósito es proteger al jefe de cabello puntiagudo de la responsabilidad: si elige algo que es "mejor práctica de la industria", y la empresa pierde, no puede ser culpado. No eligió, la industria lo hizo.
Creo que este término se utilizó originalmente para describir métodos contables y así sucesivamente. Lo que significa, aproximadamente, es no hagas nada raro. Y en contabilidad eso probablemente sea una buena idea. Los términos "de vanguardia" y "contabilidad" no suenan bien juntos. Pero cuando importas este criterio a decisiones sobre tecnología, comienzas a obtener las respuestas incorrectas.
La tecnología a menudo debería ser de vanguardia. En lenguajes de programación, como ha señalado Erann Gat, lo que "mejor práctica de la industria" realmente te da no es lo mejor, sino simplemente lo promedio. Cuando una decisión te causa desarrollar software a una fracción de la tasa de competidores más agresivos, "mejor práctica" es un término erróneo.
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 poder. Número 2, la mayoría de los gerentes ignoran deliberadamente esto. 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 enfrenta el problema más difícil que puedas encontrar, usa el lenguaje más poderoso que puedas conseguir, y espera a que los jefes de cabello puntiagudo de tus competidores regresen 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 toma un número n y devuelve una función que toma otro número i y devuelve 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 de 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, así que tienes que crear una nueva variable s.
En Javascript el ejemplo es, nuevamente, ligeramente más largo, porque Javascript mantiene la distinción entre declaraciones y expresiones, así 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 mantiene esta distinción, pero la maneja de manera típica de Perl permitiéndote omitir retornos).
Si intentas traducir el código de Lisp/Perl/Smalltalk/Javascript a Python te encuentras con algunas limitaciones. Debido a que Python no soporta completamente las variables léxicas, tienes que crear una estructura de datos para mantener el valor de n. Y aunque Python tiene un tipo de dato de función, no hay una representación literal para uno (a menos que el cuerpo sea solo una única expresión) así que necesitas crear una función nombrada 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 legítimamente preguntar 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 mi suposición es 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 lenguajes orientados a objetos, puedes, hasta cierto punto, simular un cierre (una función que se refiere a variables definidas en ámbitos que la rodean) definiendo una clase con un método y un campo para reemplazar cada variable de un ámbito que la rodea. Esto hace que el programador realice 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
Incluyo 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 mantener 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 este caso muestra es que el poder es la máxima elegancia: el programa de 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 cercano 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 la especificación porque solo funciona para enteros. Después de muchos intercambios de correos electrónicos con hackers de Java, diría que escribir una versión correctamente polimórfica que se comporte como los ejemplos anteriores está entre lo incómodo y lo imposible. Si alguien quiere escribir una, estaría muy curioso por verla, pero personalmente he agotado mi tiempo.
No es literalmente cierto que no puedes 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 con tanta frecuencia 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 de C o Fortran suficientemente complicado contiene una implementación ad hoc informalmente especificada 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 estamos viendo esto comenzar a suceder en el ejemplo de Python, donde estamos simulando efectivamente el código que un compilador generaría 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 un signo de problemas. La forma de un programa debería reflejar solo 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 algún 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 cabello puntiagudo para que te deje 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 "mejor práctica 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 varios lenguajes y/o leyeron borradores de esto, incluyendo a Ken Anderson, Trevor Blackwell, Erann Gat, Dan Giffin, Sarah Harlin, Jeremy Hylton, Robert Morris, Peter Norvig, Guy Steele y Anton van Straaten. No tienen culpa de ninguna opinión expresada.
Relacionado:
Muchas personas han respondido a esta charla, así que he creado una página adicional para tratar los problemas que han planteado: Re: Venganza de los Nerds.
También provocó una discusión extensa y a menudo útil en la lista de correo LL1. Ver particularmente 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 grande de implementaciones canónicas del benchmark 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