La venganza de los nerds
¿Quieres empezar una startup? Obtén financiación de Y Combinator.
Mayo de 2002
| "Estábamos detrás de 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 a este personaje de dibujos animados, sino que conocen a la persona real en su empresa en la que está modelado.
El jefe de cabeza puntiaguda combina milagrosamente dos cualidades que son comunes por sí solas, pero rara vez se ven juntas: (a) no sabe absolutamente nada de tecnología y (b) tiene opiniones muy firmes al respecto.
Supongamos, por ejemplo, que necesita escribir un programa. El jefe de cabeza puntiaguda no tiene idea de cómo debe funcionar este programa, y no puede distinguir un lenguaje de programación de otro, y sin embargo, sabe en qué lenguaje debería escribirlo. Exactamente. Cree que debería escribirlo en Java.
¿Por qué piensa eso? Echemos un vistazo al cerebro del jefe de cabeza puntiaguda. Lo que está pensando es algo así. Java es un estándar. Debo saberlo, 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í misteriosamente siempre lo hacen, 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 prácticamente equivalentes. Si eso fuera cierto, estaría en lo correcto. 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 demostrárselo sin siquiera entrar en las diferencias entre ellos. Si le preguntara al jefe de cabeza puntiaguda en 1992 en qué lenguaje se debería escribir el software, habría respondido con la misma poca vacilación que hoy. El software debería escribirse en C++. Pero si los lenguajes son todos equivalentes, ¿por qué la opinión del jefe de cabeza puntiaguda debería cambiar alguna vez? 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 en algún aspecto que lo que la gente ya tenía. Y, de hecho, Gosling deja claro en el primer white paper de Java que Java fue diseñado para solucionar algunos problemas con C++. Así que ahí lo tiene: los lenguajes no son todos equivalentes. Si sigue el rastro a través del cerebro del jefe de cabeza puntiaguda hasta Java y luego retrocede a través de la historia de Java hasta sus orígenes, termina sosteniendo una idea que contradice la suposición con la que comenzó.
Entonces, ¿quién tiene razón? ¿James Gosling o el jefe de cabeza puntiaguda? No es sorprendente, Gosling tiene razón. Algunos lenguajes son mejores, para ciertos problemas, que otros. Y sabe, 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 empiece a considerar esta pregunta, habrá abierto una verdadera caja de Pandora. Si el jefe de cabeza puntiaguda tuviera que pensar en el problema en toda su complejidad, le explotaría el cerebro. Mientras considere todos los lenguajes equivalentes, todo lo que tiene que hacer es elegir el que parezca tener más impulso, y dado que es más una cuestión de moda que de tecnología, incluso él probablemente pueda 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 sabe nada: la idoneidad relativa de los veinte 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 simplifica mucho la vida. 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 de moda, el nuevo. ¿O no? Si se mira el mundo de los lenguajes de programación a distancia, parece que Java es lo último. (Desde lo suficientemente lejos, todo lo que se ve es el gran y llamativo cartel pagado por Sun.) Pero si se mira este mundo de cerca, se descubre que hay grados de popularidad. Dentro de la subcultura hacker, hay otro lenguaje llamado Perl que se considera mucho más popular que Java. Slashdot, por ejemplo, es generado por Perl. No creo que esos tipos usen Java Server Pages. Pero hay otro lenguaje más nuevo, llamado Python, cuyos usuarios tienden a menospreciar a Perl, y más esperando entre bastidores.
Si se miran estos lenguajes en orden, Java, Perl, Python, se nota un patrón interesante. Al menos, se nota este patrón si eres un hacker de Lisp. Cada uno se parece progresivamente más a Lisp. Python copia incluso características que muchos hackers de Lisp consideran errores. Se podrían traducir programas simples de Lisp a Python línea por línea. Es 2002, y los lenguajes de programación casi se han puesto al día con 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 solo ahora se están poniendo al día con las ideas que él 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 moles 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 lenguaje de programación es algo que usamos para decirle a una computadora qué hacer. McCarthy finalmente tenía la intención de desarrollar un lenguaje de programación en este sentido, pero el Lisp que terminamos teniendo se basó en algo separado que hizo como 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 demostrar que Lisp era más elegante que las máquinas de Turing era escribir una función universal de Lisp 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 de Lisp eval... , que calcula el valor de una expresión de Lisp.... Escribir eval requirió inventar una notación que representara las funciones de Lisp como datos de Lisp, y tal notación se ideó para los fines del artículo 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. Esto es lo que McCarthy dijo al respecto 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 la teoría con la práctica, este eval está destinado a ser leído, no a ser computado. Pero él siguió adelante y lo hizo. Es decir, compiló el eval de mi artículo en código de máquina [IBM] 704, corrigiendo errores, y luego lo anunció como un intérprete de Lisp, lo que ciertamente era. Así que en ese momento Lisp tenía esencialmente la forma que tiene hoy....
De repente, en cuestión de semanas, creo, McCarthy descubrió que su ejercicio teórico se había transformado en un lenguaje de programación real, y uno más potente de lo que había pretendido.
Así que la explicación corta 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 estropean. Lo correcto con lo que comparar Lisp no es el hardware de los años 50, sino, digamos, el algoritmo Quicksort, que fue descubierto en 1960 y sigue siendo el ordenamiento de propósito general más rápido.
Hay otro lenguaje que aún sobrevive de los años 50, 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 uno 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 algunos aspectos era menos potente que los lenguajes ensambladores más recientes; no había subrutinas, por ejemplo, solo saltos. El Fortran actual es ahora, argumentablemente, más cercano a Lisp que a Fortran I.
Lisp y Fortran fueron los troncos de dos árboles evolutivos separados, uno arraigado en las matemáticas y otro en la arquitectura de la máquina. Estos dos árboles han estado convergiendo desde entonces. Lisp comenzó siendo potente, 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 se volvieron gradualmente más potentes, hasta que ahora los más avanzados están bastante cerca de Lisp. Cerca, pero todavía les faltan algunas cosas....
¿Qué hacía diferente a Lisp?
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 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. Hoy las damos por sentadas, pero Fortran I no las 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 dato 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 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 enlazar 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 posteriores, que distinguen entre expresiones y sentencias. Fue natural tener esta distinción en Fortran I porque no se podían anidar sentencias. Y así, mientras se necesitaban 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 esperando.
Esta limitación desapareció con la llegada de los lenguajes de estructura de bloques, pero para entonces ya era demasiado tarde. La distinción entre expresiones y sentencias se había afianzado. 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. Así, se puede probar la igualdad comparando un puntero, en lugar de comparar cada carácter.
-
Una notación para código que utiliza árboles de símbolos y constantes.
-
Todo el lenguaje allí todo el tiempo. No hay una distinción real entre tiempo de lectura, tiempo de compilación y tiempo de ejecución. Se puede compilar o ejecutar código mientras se lee, leer o ejecutar código mientras se compila, 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 utilizando s-expresiones, una idea reinventada recientemente como XML. Cuando apareció Lisp 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 predeterminado, encarnado en una sucesión de lenguajes populares, ha evolucionado gradualmente hacia Lisp. Las ideas 1-5 son ahora generalizadas. El número 6 está empezando 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 pretendido 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 parece extraño no tanto porque tenga una sintaxis extraña como porque no tiene sintaxis; se expresan los programas directamente en los árboles de análisis que se construyen en segundo plano cuando se analizan otros lenguajes, 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 potente. Las ideas 8 y 9 juntas significan que se pueden 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 quieres entender realmente Lisp, o simplemente ampliar tus horizontes de programación, te recomiendo aprender más sobre macros.
Las macros (en el sentido de Lisp) todavía son, hasta donde yo sé, exclusivas de Lisp. Esto se debe en parte a que para tener macros probablemente tengas que hacer que tu lenguaje parezca tan extraño como Lisp. También puede ser que si añades ese último incremento de potencia, ya no puedas 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 tenga car, cdr, cons, quote, cond, atom, eq, y una notación para funciones expresadas como listas, entonces puedes construir todo lo demás de Lisp a partir de él. Ese es de hecho el 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 acercan asintóticamente, ¿significa eso que deberías usarlo para escribir software? ¿Cuánto pierdes al usar un lenguaje menos potente? ¿No es más sensato, a veces, no estar en el borde mismo de la innovación? ¿Y la popularidad no es 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 ventaja obtendrás al usar un lenguaje potente. Pero muchos proyectos no son exigentes en absoluto. La mayor parte 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. Si solo necesitas pasar datos de una aplicación de Windows a otra, claro, usa Visual Basic.
También puedes escribir pequeños programas de pegamento en Lisp (lo uso como 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 competencia feroz. 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 establecidos, 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 utilizan técnicas de programación de la era mainframe. (Aunque ITA también está, en cierto sentido, utilizando un lenguaje de programación de la era mainframe). Nunca he visto ninguno de los códigos de ITA, pero según uno de sus principales hackers, utilizan muchas macros, y no me sorprende oírlo.
Fuerzas centrípetas
No digo que no haya ningún coste en el uso de tecnologías poco comunes. El jefe de cabeza puntiaguda 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. Sus programas podrían no funcionar bien con programas escritos en otros lenguajes. Podría tener menos bibliotecas a su disposición. Y podría tener problemas para contratar programadores.
¿Cuánto problema representa cada uno de estos? La importancia del primero varía según si usted tiene control sobre todo el sistema. Si está escribiendo software que debe ejecutarse en la máquina de un usuario remoto sobre un sistema operativo defectuoso y cerrado (no menciono nombres), puede haber ventajas en escribir su aplicación en el mismo lenguaje que el SO. Pero si controla todo el sistema y tiene el código fuente de todas las partes, como presumiblemente hace ITA, puede usar los lenguajes que quiera. Si surge alguna incompatibilidad, puede solucionarla usted mismo.
En las aplicaciones basadas en servidores, puede salirse con la suya utilizando 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". Por eso oímos hablar de lenguajes nuevos como Perl y Python. No oímos hablar de estos lenguajes porque la gente los use para escribir aplicaciones de Windows, sino porque la gente los usa 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 de punto medio.
En cuanto a las bibliotecas, su importancia también depende de la aplicación. Para problemas menos exigentes, la disponibilidad de bibliotecas puede superar la potencia intrínseca del lenguaje. ¿Dónde está el punto de equilibrio? Es difícil decirlo con exactitud, pero dondequiera que esté, está por debajo de cualquier cosa que probablemente llamaría una aplicación. Si una empresa se considera a sí misma en el negocio del software, y están escribiendo una aplicación que será uno de sus productos, entonces probablemente involucrará a varios hackers y llevará al menos seis meses escribirla. En un proyecto de ese tamaño, los lenguajes potentes probablemente comiencen 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. ¿Cuántos hackers necesita contratar, después de todo? Seguramente a estas alturas todos sabemos que el software se desarrolla mejor en equipos de menos de diez personas. Y no debería tener problemas para contratar hackers a esa escala para cualquier lenguaje del que alguien haya oído hablar. Si no puede encontrar diez hackers de Lisp, entonces su empresa probablemente esté ubicada en la ciudad equivocada para desarrollar software.
De hecho, elegir un lenguaje más potente probablemente disminuye el tamaño del equipo que necesita, porque (a) si usa un lenguaje más potente probablemente no necesitará tantos hackers, y (b) los hackers que trabajan en lenguajes más avanzados probablemente sean más inteligentes.
No digo que no vaya a recibir mucha presión para usar lo que se percibe como tecnologías "estándar". En Viaweb (ahora Yahoo Store), causamos cierta sorpresa entre los VCs y los posibles compradores al usar Lisp. Pero también causamos sorpresa al usar cajas Intel genéricas como servidores en lugar de servidores "de alta resistencia" como Sun, al usar una variante de Unix de código abierto entonces oscura llamada FreeBSD en lugar de un sistema operativo comercial real como Windows NT, al ignorar un supuesto estándar de comercio electrónico llamado SET que nadie recuerda ahora, y así sucesivamente.
No puede dejar que los trajes tomen decisiones técnicas por usted. ¿Alarmó a algunos posibles compradores que usáramos Lisp? A algunos, ligeramente, pero si no hubiéramos usado Lisp, no habríamos podido escribir el software que los hizo querer comprarnos. Lo que a ellos les pareció una anomalía fue de hecho causa y efecto.
Si inicia una startup, no diseñe su producto para complacer a los VCs o a los posibles compradores. Diseñe su producto para complacer a los usuarios. Si gana a los usuarios, todo lo demás seguirá. Y si no, a nadie le importará cuán cómodamente ortodoxas fueron sus elecciones tecnológicas.
El coste de ser promedio
¿Cuánto pierde al usar un lenguaje menos potente? En realidad, hay algunos datos al respecto.
La medida más conveniente de potencia es probablemente el tamaño del código. El objetivo de los lenguajes de alto nivel es proporcionarle abstracciones más grandes (ladrillos más grandes, por así decirlo, para que no necesite tantos para construir un muro de un tamaño determinado). Por lo tanto, cuanto más potente sea el lenguaje, más corto será el programa (no simplemente en caracteres, por supuesto, sino en elementos distintos).
¿Cómo le permite un lenguaje más potente escribir programas más cortos? Una técnica que puede usar, si el lenguaje se lo permite, es algo llamado programación ascendente. En lugar de simplemente escribir su aplicación en el lenguaje base, construye sobre el lenguaje base un lenguaje para escribir programas como el suyo, y luego escriba su programa en él. El código combinado puede ser mucho más corto que si hubiera escrito todo su 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 su programa fuera tres veces más largo en otro lenguaje, tardaría tres veces más en escribirlo, y no puede evitarlo contratando a más personas, porque más allá de cierto tamaño, los nuevos empleados 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 ha tendido a confirmar lo que dijo.
Entonces, ¿cuánto más cortos son sus programas si los escribe en Lisp? La mayoría de las cifras que he oído para Lisp frente a C, por ejemplo, han sido alrededor de 7-10 veces. 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, 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++, así como Lisp, por lo que hablan por experiencia.
Mi suposición 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 compitieras con ITA y eligieras escribir tu software en C, ellos podrían desarrollar software veinte veces más rápido que tú. Si pasaras 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, ¡tardarías cinco años en tenerlo tú también!
Y sabes qué? Ese es el mejor escenario. Cuando hablas de ratios de tamaño de código, implícitamente asumes que 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 intentas resolver un problema difícil con un lenguaje que es demasiado de bajo nivel, llegas a un punto en el que hay demasiado que mantener en tu cabeza a la vez.
Así que cuando digo que al competidor imaginario de ITA le llevaría cinco años duplicar algo que ITA podría escribir en Lisp en tres meses, me refiero a 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 llevaría cinco años probablemente nunca se completará.
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 detrás.
Una receta
Este es el tipo de posibilidad sobre la que el jefe de cabeza puntiaguda ni siquiera quiere pensar. Y por eso la mayoría de ellos no lo hacen. Porque, ya sabes, al final, al jefe de cabeza puntiaguda no le importa si su empresa es pateada, 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 "la mejor práctica de la industria". Su propósito es proteger al jefe de cabeza puntiaguda de la responsabilidad: si elige algo que es "la mejor práctica 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 métodos contables y demás. Lo que significa, aproximadamente, es no hagas nada raro. Y en contabilidad, eso es probablemente una buena idea. Los términos "de vanguardia" 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 de vanguardia. En los lenguajes de programación, como ha señalado Erann Gat, lo que realmente obtienes con "la mejor práctica de la industria" no es lo mejor, sino simplemente el promedio. Cuando una decisión te lleva a desarrollar software a una fracción de la velocidad de los competidores más agresivos, "la mejor práctica" es un término 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 lo ignoran deliberadamente. Entre ellas, estos dos hechos son literalmente una receta para hacer 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 potente que puedas conseguir y espera a que los jefes de cabeza puntiaguda de tus competidores reviertan a la media.
Apéndice: Potencia
Como ilustración de lo que quiero decir sobre la potencia relativa de los lenguajes de programación, considere 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.
(Es incrementado por, no más. Un acumulador tiene que acumular.)
En Common Lisp 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, por lo que tienes que crear una nueva variable s.
En Javascript el ejemplo es, de nuevo, ligeramente más largo, porque Javascript mantiene la distinción entre sentencias y expresiones, por lo que necesitas sentencias return
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 a la manera típica de Perl al permitirte omitir los return
s).
Si intentas traducir el código Lisp/Perl/Smalltalk/Javascript a Python te encuentras con algunas limitaciones. Como 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 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 obtienes: 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 completamente hacia Lisp, siempre podrían simplemente...)
En los lenguajes OO, puedes, hasta cierto punto, simular un closure (una función que se refiere a variables definidas en ámbitos envolventes) definiendo una clase con un método y un campo para reemplazar cada variable de un ámbito envolvente. 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 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 Incluyo estos porque no querría que los defensores de Python dijeran que estoy 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 demuestra es que la potencia es la elegancia definitiva: el programa Perl es más simple (tiene menos elementos), aunque la sintaxis sea un poco más fea.
¿Qué tal otros lenguajes? En los otros lenguajes mencionados en esta charla (Fortran, C, C++, Java y Visual Basic), no está claro si realmente se puede resolver este problema. Ken Anderson dice que el siguiente código es lo más parecido que se puede conseguir 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 se queda corto de 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 es algo entre muy difícil e imposible. Si alguien quiere escribir una, estaría muy curioso verla, pero personalmente he abandonado.
No es literalmente cierto que no se pueda resolver este problema en otros lenguajes, por supuesto. El hecho de que todos estos lenguajes sean Turing-equivalentes significa que, estrictamente hablando, se puede 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 potente.
Eso suena a broma, pero sucede con tanta frecuencia en diversos grados en proyectos de programación grandes 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, llena de errores y lenta de la mitad de Common Lisp. Si intentas resolver un problema difícil, la pregunta no es si usarás un lenguaje lo suficientemente potente, sino si (a) usarás un lenguaje potente, (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 empieza 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 institucionalizada. Por ejemplo, en el mundo OO se habla mucho de "patrones". Me pregunto si estos patrones no son a veces evidencia de que el caso (c), el compilador humano, está funcionando. 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 potentes, a menudo de que estoy generando manualmente las expansiones de alguna macro que necesito escribir.
Notas
-
La CPU del IBM 704 tenía aproximadamente el tamaño de un refrigerador, pero mucho más pesada. La CPU pesaba 3150 libras, y las 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 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 "la 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 la culpa de ninguna opinión expresada.
Relacionado:
Muchas personas han respondido a esta charla, así que he creado una página adicional para abordar 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 particularmente el correo de Anton van Straaten sobre compresión semántica.
Algunos de los correos de LL1 me llevaron a intentar profundizar en el tema de la potencia del lenguaje en Succinctness is Power.
Un conjunto más grande de implementaciones canónicas del generador de acumuladores benchmark se recopilan en su propia página.
Traducción al japonés, Traducción al español, Traducción al chino