LA VENGANZA DE LOS EMPOLLONES
OriginalMayo 2002
"Estábamos detrás de los programadores de C++. Logramos arrastrar a muchos de ellos a medio camino hacia Lisp".
- Guy Steele, coautor de la especificación de Java
En el mundo del software hay 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 a 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 firmes 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, y sin embargo sabe en qué lenguaje debes escribirlo. Exactamente. Cree que debes escribirlo en Java.
¿Por qué piensa esto? Echemos un vistazo al cerebro del jefe de cabeza puntiaguda. Lo que está pensando es algo así: Java es un estándar. Sé que debe serlo, porque lo he leído 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, por lo que si los programadores que trabajan para mí ahora se van, como misteriosamente siempre lo hacen los programadores que trabajan para mí, puedo reemplazarlos fácilmente.
Bueno, esto no suena tan irrazonable. Pero todo se basa en un supuesto no expresado, y resulta que ese supuesto es falso. 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 correcto.
Pero no todos los lenguajes son equivalentes, y creo que puedo demostrártelo sin siquiera entrar en las diferencias entre ellos. Si le preguntaras al jefe de cabeza puntiaguda en 1992 en qué lenguaje se debería escribir el software, habría respondido con la misma falta de 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 documento técnico de Java que Java fue diseñado para solucionar algunos problemas con C++.
Entonces 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 de vuelta a través de la historia de Java hasta sus orígenes, terminas sosteniendo una idea que contradice el supuesto con el que comenzaste.
Entonces, ¿quién tiene razón? ¿James Gosling o el jefe de cabeza puntiaguda? 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 mejor Java y cuándo es mejor 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 lata de gusanos real. 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 parezca tener más impulso, y dado que eso es más una cuestión de moda que de tecnología, incluso él probablemente pueda dar con 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 sabe nada: la idoneidad relativa de los veinte o más lenguajes líderes 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 sorprendente 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 hace que tu vida sea 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 cool. ¿O no? Si miras el mundo de los lenguajes de programación desde lejos, parece que Java es lo último. (Desde lo suficientemente lejos, todo lo que puedes ver es el gran cartel parpadeante pagado por Sun). Pero si miras este mundo de cerca, encuentras 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, se genera con Perl. No creo que encontrarías 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 como Lisp. Python copia incluso características que muchos hackers de Lisp consideran errores. Podrías traducir programas sencillos de Lisp a Python línea por línea. Es 2002 y los lenguajes de programación casi han alcanzado a 1958.
Alcanzando 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 puede 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 puede cualquier tecnología tan antigua incluso ser relevante, 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 le damos hoy. Lo que entendemos por 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 terminamos teniendo se basaba 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 Lisp universal 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 Lisp eval..., que calcula el valor de una expresión Lisp.... Escribir eval requirió inventar una notación que representa las funciones Lisp como datos Lisp, y tal notación se ideó con fines 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 Lisp.
Esto fue una gran sorpresa en ese momento. Aquí está lo que McCarthy dijo al respecto más tarde en una entrevista:
Steve Russell dijo, mira, ¿por qué no programo este eval..., y le dije, ho, ho, estás confundiendo la teoría con la práctica, este eval está destinado a la lectura, no al cálculo. 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 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ó que su ejercicio teórico se había transformado en un lenguaje de programación real, y más poderoso de lo que había previsto.
Así que 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 vuelven rancias. Lo correcto es comparar Lisp no con el hardware de la década de 1950, sino, por ejemplo, con el algoritmo Quicksort, que se descubrió en 1960 y sigue siendo el método de ordenamiento de propósito general más rápido.
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 era 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 muy de 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 ramas. El Fortran actual ahora está más cerca de Lisp que de Fortran I.
Lisp y Fortran fueron los troncos de dos árboles evolutivos separados, uno enraizado en las matemáticas y otro enraizado en la arquitectura de las máquinas. Estos dos árboles han estado convergiendo desde entonces. Lisp comenzó siendo poderoso y, en los siguientes veinte años, se volvió rápido. Los llamados lenguajes principales comenzaron siendo rápidos y, en 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 falta algunas cosas....
Lo que hizo que Lisp fuera diferente
Cuando se desarrolló por primera vez, Lisp encarnaba nueve ideas nuevas. Algunas de estas las damos por sentadas ahora, otras solo se ven en lenguajes más avanzados, y dos siguen siendo únicas de Lisp. Las nueve ideas, en orden de su adopción por parte de la corriente principal, son:
Condicionales. Un condicional es una construcción de tipo if-then-else. Ahora las damos por sentadas, pero Fortran I no las tenía. Tenía solo un goto condicional basado de cerca en la instrucción de máquina subyacente.
Un tipo de función. En Lisp, las funciones son un tipo de datos al igual que los enteros o las cadenas. Tienen una representación literal, se pueden almacenar en variables, se pueden pasar como argumentos, y así sucesivamente.
Recursión. Lisp fue el primer lenguaje de programación en admitirla.
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 Lisp son árboles de expresiones, cada una de las cuales devuelve un valor. Esto contrasta con Fortran y la mayoría de los lenguajes sucesivos, que distinguen entre expresiones y declaraciones.
Era natural tener esta distinción en Fortran I porque no se podían anidar declaraciones. Y entonces, si bien necesitabas expresiones para que las matemáticas funcionaran, no tenía sentido hacer que cualquier otra cosa devolviera un valor, porque no podía haber nada esperándolo.
Esta limitación desapareció con la llegada de los lenguajes con estructura de bloques, pero para entonces ya era demasiado tarde. La distinción entre expresiones y declaraciones se había arraigado. Se extendió de Fortran a Algol y luego a sus 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 usa árboles de símbolos y constantes.
Todo el lenguaje allí 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 los 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 que los programas se comuniquen usando s-expresiones, una idea recientemente reinventada como XML.
Cuando apareció Lisp por primera vez, estas ideas estaban muy alejadas de la práctica de programación ordinaria, que se dictaba en gran medida por el hardware disponible a finales 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 sintaxis para ello.
En cuanto al número 8, este puede ser el más interesante del lote. Las ideas 8 y 9 solo se convirtieron en parte de Lisp por accidente, porque Steve Russell implementó algo que McCarthy nunca tuvo la intención de implementar. 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 los programas directamente en los árboles de análisis que se construyen detrás de escena cuando se analizan otros lenguajes, y estos árboles se componen 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. Esto puede sonar como una idea bizarra, 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. Un macro de Lisp puede ser desde una abreviatura hasta un compilador para un nuevo lenguaje. Si quieres entender realmente Lisp, o simplemente expandir tus horizontes de programación, te recomiendo aprender más sobre los macros.
Los macros (en el sentido de Lisp) siguen siendo, hasta donde yo sé, únicos 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 último incremento 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. De hecho, esa es la cualidad definitoria de Lisp: fue para lograr 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 se acercan asintóticamente los lenguajes principales, ¿significa eso que deberías usarlo realmente para escribir software? ¿Cuánto se pierde al usar un lenguaje menos poderoso? ¿No es a veces más sabio no estar en el borde mismo de la innovación? ¿Y no es la popularidad en cierta medida una justificación en sí misma? Por ejemplo, ¿no tiene razón el jefe con cabeza de punta 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 apalancamiento obtendrás al usar un lenguaje poderoso. Pero muchos proyectos no son nada exigentes. La mayor parte de la programación probablemente consiste en escribir pequeños programas de pegamento, y para esos 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 transferir datos de una aplicación de Windows a otra, seguro, usa Visual Basic.
También puedes escribir pequeños programas de pegamento en Lisp (lo uso como una calculadora de escritorio), pero el mayor beneficio de los lenguajes como Lisp se encuentra 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 en un mercado ya dominado por dos grandes competidores atrincherados, Travelocity y Expedia, y parecen haberlos humillado tecnológicamente.
El núcleo de la aplicación de ITA es un programa de Common Lisp de 200,000 líneas que busca muchos órdenes de magnitud más posibilidades que sus competidores, que aparentemente aún utilizan técnicas de programación de la era de los mainframes. (Aunque ITA también está usando, en cierto sentido, un lenguaje de programación de la era de los mainframes). Nunca he visto ninguno de los códigos de ITA, pero según uno de sus mejores hackers, utilizan muchos macros, y no me sorprende escucharlo.
Fuerzas centrípetas
No estoy diciendo que no haya costo alguno al usar tecnologías poco comunes. El jefe con cabeza de punta no se equivoca del todo al preocuparse por esto. Pero como no entiende los riesgos, tiende a magnificarlos.
Puedo pensar en tres problemas que podrían surgir del uso de 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 grave es cada uno de estos problemas? La importancia del primero varía según si tienes control sobre todo el sistema. Si estás escribiendo software que debe ejecutarse en la máquina de un usuario remoto sobre un sistema operativo cerrado y con errores (no menciono ninguno), 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 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 principal causa de lo que Jonathan Erickson llama el "renacimiento de los lenguajes de programación". Por eso incluso escuchamos sobre nuevos lenguajes como Perl y Python. No escuchamos 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á cada vez 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 con exactitud, pero dondequiera que esté, está por debajo 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 involucre a varios hackers y tome al menos seis meses escribirla. En un proyecto de ese tamaño, los lenguajes poderosos probablemente comiencen a superar la conveniencia de las bibliotecas preexistentes.
La tercera preocupación del jefe de pelo puntiagudo, la dificultad de contratar programadores, creo que es un señuelo. ¿Cuántos hackers necesitas contratar, después de todo? Seguramente ya 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 potente probablemente disminuya el tamaño del equipo que necesitas, porque (a) si usas un lenguaje más potente probablemente no necesites 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 perciben como tecnologías "estándar". En Viaweb (ahora Yahoo Store), levantamos algunas cejas entre los VC y los posibles adquirentes al usar Lisp. Pero también levantamos cejas al usar cajas genéricas de Intel como servidores en lugar de servidores "de alta resistencia" como Suns, por usar una variante de Unix de código abierto entonces poco conocida llamada FreeBSD en lugar de un sistema operativo comercial real 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? Algunos, ligeramente, pero si no hubiéramos usado Lisp, no habríamos podido escribir el software que les hizo querer comprarnos. Lo que les pareció una anomalía era de hecho causa y efecto.
Si comienzas una startup, no diseñes tu producto para complacer a los VC o a los posibles adquirentes. Diseña tu producto para complacer a los usuarios. Si conquistas a los usuarios, todo lo demás seguirá. Y si no lo haces, a nadie le importará cuán reconfortantemente ortodoxas hayan sido tus elecciones tecnológicas.
El costo de ser promedio
¿Cuánto se pierde al usar un lenguaje menos potente? Hay algunos datos sobre eso.
La medida más conveniente de poder es probablemente el tamaño del código. El punto 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 potente 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, 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 ascendente también debería ser más fácil de modificar, porque en muchos casos la capa de lenguaje no tendría 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, te llevará tres veces más escribirlo, y no puedes evitarlo contratando más personas, porque más allá de cierto tamaño, los nuevos contratados 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, ¿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 sido de alrededor de 7-10x. Pero un artículo reciente sobre ITA en la revista New Architect decía 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 C y C++ además de Lisp, por lo que están hablando por experiencia.
Mi conjetura es 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 sacar más provecho de mejores herramientas.
Como un punto de datos en la curva, en cualquier caso, si compitieran con ITA y eligieran escribir su software en C, podrían desarrollar software veinte veces más rápido que tú. Si pasaras un año en una nueva función, podrían duplicarla en menos de tres semanas. Mientras que si ellos pasaran solo tres meses desarrollando algo nuevo, te tomaría cinco años tenerlo también.
¿Y sabes qué? Ese es el mejor de los casos. Cuando hablas de relaciones de tamaño de código, estás asumiendo implícitamente que puedes escribir el programa en el lenguaje más débil. Pero de hecho hay límites en lo que pueden hacer los programadores. 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 la cabeza al mismo tiempo.
Así que cuando digo que le tomaría cinco años al competidor imaginario de ITA duplicar algo que ITA podría escribir en Lisp en tres meses, me refiero a cinco años si no hay problemas. De hecho, la forma en que funcionan las cosas en la mayoría de las empresas, cualquier proyecto de desarrollo que tomaría cinco años probablemente 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 un diferencial de dos o tres a uno sería suficiente para garantizar que siempre estarías detrás.
Una receta
Este es el tipo de posibilidad que el jefe de pelo puntiagudo 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 pelo puntiagudo no le importa si su empresa es derrotada, 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 las organizaciones grandes, la frase utilizada para describir este enfoque es "mejor práctica de la industria". Su propósito es proteger al jefe de pelo puntiagudo de la responsabilidad: si elige algo que es "mejor práctica de la industria" y la empresa pierde, no se le puede culpar. Él no eligió, lo hizo la industria.
Creo que este término se utilizó originalmente para describir métodos contables y similares. Lo que significa, aproximadamente, es no hacer nada raro. Y en contabilidad, probablemente sea una buena idea. Los términos "vanguardista" y "contabilidad" no suenan bien juntos. Pero cuando importas este criterio a las decisiones sobre tecnología, empiezas a obtener las respuestas equivocadas.
La tecnología a menudo debería ser vanguardista. En los lenguajes de programación, como señaló Erann Gat, lo que realmente te da la "mejor práctica 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 competidores más agresivos, "mejor práctica" es un término equivocado.
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 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 aborda el problema más difícil que puedas encontrar, usa el lenguaje más potente que puedas conseguir y espera a que los jefes de pelo puntiagudo de tus competidores vuelvan a la media.
Apéndice: Potencia
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 en 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 los 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, 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 maneja a la manera típica de Perl permitiéndote omitir los retornos).
Si intentas traducir el código de Lisp/Perl/Smalltalk/JavaScript a Python, te encontrarás 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 sí tiene un tipo de datos de función, no hay una representación literal para uno (a menos que el cuerpo sea solo una expresión única), por lo que necesitas crear una función con nombre para devolver. Esto es lo que terminas obteniendo:
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 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...)
En los lenguajes orientados a objetos, puedes, en cierta medida, simular un cierre (una función que se refiere a variables definidas en ámbitos de encierro) definiendo una clase con un método y un campo para reemplazar cada variable de un ámbito de encierro. 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 ya sea
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
Los incluyo 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; solo que es 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 elegancia definitiva: el programa de Perl es más simple (tiene menos elementos), incluso si la sintaxis es un poco más fea.
¿Qué hay de 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 aproximadamente 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 adecuadamente polimórfica que se comporte como los ejemplos anteriores está entre maldita incómoda e imposible. Si alguien quiere escribir uno, estaría muy curioso por verlo, pero personalmente me he rendido.
No es literalmente cierto que no puedas resolver este problema en otros idiomas, por supuesto. El hecho de que todos estos idiomas 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 le 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 de C o Fortran lo suficientemente complicado contiene una implementación lenta, informal y con errores de la mitad de Common Lisp.
Si intentas resolver un problema difícil, la cuestión 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) te convertirás en un compilador humano para uno. Ya vemos que esto está comenzando 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 de la programación orientada a objetos se habla mucho de "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 debe 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 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 4 KB 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 con cabeza puntiaguda 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 _])
El triste relato de Erann Gat sobre las "mejores prácticas de la industria" en JPL me inspiró a abordar esta frase que se aplica de manera tan equivocada.
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 idiomas y/o leyeron borradores de esto, incluidos 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 las opiniones expresadas.
Relacionado:
Muchas personas han respondido a esta charla, por lo que he configurado una página adicional para tratar los problemas que han planteado: Re: Revenge of the Nerds.
También desencadenó una discusión extensa y a menudo útil en la lista de correo LL1. Vea en particular el correo de Anton van Straaten sobre la compresión semántica.
Parte del correo en LL1 me llevó a intentar profundizar más en el tema del poder del lenguaje en Succinctness is Power.
Un conjunto más amplio de implementaciones canónicas del acumulador generador de referencia se recopilan en su propia página.
Traducción al japonés, Traducción al español, Traducción al chino