No hay ningún problema fundamental: las computadoras pueden programarse totalmente . Simplemente no muy bien. Todavía. Necesita más investigación.
Afortunadamente, eso es exactamente lo que está sucediendo: la programación automática ha visto un renacimiento académico como síntesis de programas en los últimos años. Este trabajo (o lo que he visto de él, ¡hay un sesgo de selección serio en curso!) Lo realizan principalmente investigadores de lenguaje de programación, por lo que lo encontrarán en conferencias de PL como PLDI. (Por ejemplo, veo al menos cuatro documentos sobre síntesis de la sesión de 2014).
¿Entonces, cuál es el problema? Bueno, en realidad hay muchos problemas, pero agruparé arbitrariamente la mayoría de ellos en dos categorías: interfaz y escala. El problema de la interfaz es cómo permitir que los usuarios le digan a la computadora lo que quieren programar. No podemos usar el lenguaje natural porque la PNL todavía no está allí, y hacer que funcione es un problema por separado extremadamente difícil por sí solo. ¿Asi que que hacemos? El problema de la escala es hacer que las computadoras escriban programas más grandes. Los enfoques actuales de propósito general pueden escribir código no trivial, incluso crear nuevos algoritmos, pero solo para problemas muy pequeños. Tal vez, si no tiene prisa, pueden escribir 100 instrucciones, a menudo con restricciones significativas (como “sin bucles”). ¿Cómo extendemos esto a problemas más grandes y más significativos?
Una cosa muy interesante es cuán diferentes son estos dos problemas, todo mientras están fundamentalmente entrelazados. El problema de la interfaz es, al menos en parte, una cuestión de diseño de la interfaz de usuario, que limita con la psicología en lugar de la CS. El problema de la escala es profundamente técnico y requiere algoritmos inteligentes y herramientas sofisticadas. Y sin embargo, como veremos, los dos terminan profundamente relacionados, con interfaces diseñadas para facilitar la búsqueda y sistemas específicos desarrollados para adaptarse a un determinado flujo de trabajo. Esto lo convierte en un campo fascinante.
Interfaz
El primer problema que tenemos es que, para que una computadora te escriba un programa, tiene que saber lo que necesitas. Y necesita saberlo con suficiente detalle para asegurarse de que realmente esté escribiendo algo útil y correcto. Necesitamos un buen sistema para la especificación del programa . Además, para ser útil, esta especificación tiene que ser más fácil de crear que el programa real. ¡Sería bastante inútil si decirle a la computadora lo que querías fuera tan difícil como programarlo tú mismo!
Hay algunos enfoques diferentes para escribir especificaciones que vale la pena pensar. Quizás lo más directo (¡pero no lo más usable!) Es aceptar una especificación lógica , una descripción de lo que el programa debería hacer en algún tipo de lógica formal. Esto es útil porque especificaciones como esta pueden ser más fáciles de escribir, mantener y modificar que los programas reales. Por otro lado, usar la lógica directamente es bastante extraño para la mayoría de las personas, y estas especificaciones pueden ser bastante incómodas.
Un enfoque muy similar es aceptar una especificación ejecutable , que es solo un programa. Espera, dices, ¿cuál es el punto de la programación automática si primero tienes que darle un programa? Bueno, a diferencia de su programa real, la especificación no tiene que preocuparse por el rendimiento . ¡E incluso puede estar en un idioma completamente diferente! Es mucho más fácil esbozar una versión lenta de su algoritmo que implementar una versión real y optimizada. Esto también abre otro uso interesante para estas técnicas de programación automática: en lugar de aceptar la entrada del usuario directamente, se pueden aplicar a programas ya existentes. Por ejemplo, un proyecto transfiere programas entre diferentes conjuntos de instrucciones utilizando el programa existente como una especificación ejecutable para sintetizar un programa en la otra arquitectura.
Otra técnica es el boceto , donde el usuario proporciona parte del programa final con algunas partes que faltan. El sintetizador luego llena estos espacios en blanco. Esto les permite especificar el resto del programa usando aserciones e invariantes : solo pueden afirmar condiciones dentro del boceto, y el sintetizador genera un código que hace que las condiciones siempre se cumplan. Esto también se puede combinar fácilmente con la técnica anterior de tener una especificación ejecutable; Una invariante muy común es que una función se comporta exactamente como otra proporcionada por el usuario. Además de ser una interfaz interesante, los bocetos también amplían en gran medida el tamaño de los programas que pueden generarse automáticamente al permitir que los humanos suministren las partes “obvias”, ¡partes que no siempre son obvias para las computadoras ! El sintetizador Sketch de Berkeley es la herramienta principal que utiliza este enfoque.
La técnica final que cubriré aquí es la programación por demostración . Este es un poco diferente de los demás porque es muy interactivo. La idea es que el usuario suministre algunos pares de entrada / salida y el compilador genere un programa que sea consistente con ellos. Luego, el programa puede preguntarle al usuario qué hacer en entradas ambiguas específicas, hasta que el usuario esté satisfecho con el resultado final. Un gran ejemplo de programación mediante demostración en la práctica es Flash Fill, una función de Excel 2013 originalmente de Microsoft Research.
El punto es que hay muchas maneras diferentes para que el usuario interactúe con un sistema de programación automática, ¡incluidas muchas que no cubrí o que aún no se han inventado! Todos estos tienen características diferentes tanto desde el punto de vista del usuario como desde el punto de vista del rendimiento.
Escala
En el fondo, la síntesis de programas es solo una búsqueda elegante a través del espacio de todos los programas posibles. Un espacio increíblemente increíblemente grande. Uno que aumenta a un ritmo exponencial muy rápido con la duración del programa que desea generar.
Ahora, podrías intentar forzar a este espacio por fuerza bruta. Genere una secuencia de instrucciones, ejecútelo a través de una serie de pruebas y, si no es correcto, pase a la siguiente secuencia. Al final, terminarás buscando linealmente en todas las cadenas de hasta n instrucciones. Claramente, este no es un enfoque ideal, pero eso es lo que realmente hizo un sistema anterior (en un Intel 8086). Y podría generar programas muy optimizados de 4 a 13 instrucciones de longitud que, aunque no son excelentes, ya son útiles. Además, tiene una sólida garantía de que generó el programa más corto posible para una tarea determinada que, con una arquitectura lo suficientemente simple, también significa que es el programa más rápido posible. Muy genial.
Por supuesto, para aplicaciones prácticas, queremos generar programas más largos, idealmente mucho más largos. Hay algunos enfoques modernos interesantes que mejoran significativamente el rendimiento de esta búsqueda, pero todavía están luchando contra el mismo espacio fundamentalmente exponencial, lo que significa que todavía son bastante limitados. A través de un esfuerzo hercúleo, una heurística inteligente y una tonelada de potencia de cómputo en bruto, podemos generar programas de propósito general de 20, 40 o incluso 100 instrucciones de longitud. Mejor, pero aún limitado. Por lo tanto, además de ampliar la escala mejorando la búsqueda subyacente, mucha investigación también trata de escalar aumentando la utilidad de las longitudes que podemos generar; con un poco de inteligencia, ¡puede hacer que 40 instrucciones sirvan de mucho!
Un enfoque reciente que ha demostrado ser sorprendentemente efectivo, y que ha ahorrado a los investigadores de PL una gran cantidad de problemas, está utilizando los sistemas existentes de satisfacción de restricciones para hacer la búsqueda real. El uso de solucionadores SAT y SMT existentes nos permite aprovechar los avances recientes en esas tecnologías, que fueron motivados en gran medida por usos similares para la verificación y síntesis de hardware. Los solucionadores SAT son programas que pueden verificar si se puede satisfacer una fórmula booleana (un conjunto de variables combinadas con AND y OR lógicos). Es decir, verifica si hay alguna asignación a las variables que hace que toda la fórmula sea verdadera. Los solucionadores SMT amplían SAT con “teorías”, tipos distintos de booleanos, lo que nos permite resolver restricciones sobre cosas como los números. Un sintetizador que usa uno de estos solucionadores compila un programa hasta una fórmula lógica, que incluye variables para las partes que deben sintetizarse, y luego lo pasa al solucionador que devuelve valores válidos para esas variables. Finalmente, todo esto se puede extraer nuevamente en un programa completo en el código fuente original.
En la práctica, existen algunos desafíos particulares con el uso de este enfoque. El método exacto utilizado para codificar un programa como una fórmula lógica puede tener efectos gigantescos sobre cuánto tiempo lleva una búsqueda. Los diferentes solucionadores SMT también exponen diferentes capacidades que los hacen más o menos apropiados para diversas tareas. Algunos proyectos pueden salirse con la suya utilizando un solucionador de propósito muy general como Z3, mientras que, en el otro extremo, proyectos como Berkeley’s Sketch (mencionado anteriormente) tienen sus propios solucionadores especialmente optimizados para la síntesis de programas.
Otro enfoque reciente es utilizar una búsqueda aleatoria basada en Markov-Chain Monte Carlo (MCMC) para generar programas. Esto renuncia a algunas de las garantías de un sistema de fuerza bruta o basado en un solucionador, a cambio de manejar mejor ciertos tipos de problemas. En particular, este enfoque puede aprovechar cierta estructura en el conjunto de instrucciones en el que está trabajando, como instrucciones que hacen casi, pero no del todo, las mismas cosas. Esto resulta ser muy poderoso cuando se apunta a x86, que está bastante hinchado en comparación con la mayoría de las otras arquitecturas. STOKE, el sistema que introdujo este enfoque, manejó los cientos de instrucciones disponibles en x86 mejor que los sistemas basados en solucionadores. Desafortunadamente, este enfoque no se aplica tan bien a otras arquitecturas más pequeñas y tiene algunas partes con las que es difícil trabajar: necesita una noción de cuándo un programa es “casi” correcto. La mayoría de las veces, por supuesto, un programa es correcto o completamente incorrecto, y es bastante difícil adivinar qué tan cerca está. STOKE hace esto ejecutando un montón de pruebas y contando cuántas pasan, pero esto no se traduce bien en otros sistemas.
Además de mejorar el núcleo, la búsqueda subyacente, también podemos mejorar nuestra escala cambiando el alcance de los programas que generamos. Sketch, por ejemplo, hace esto mediante partes generadas de un programa proporcionado por el usuario en lugar de todo. Esto permite al usuario especificar la estructura aproximada del resultado final (como “tendrá dos para la indexación de bucles en estas dos matrices”). Ante esto, Sketch evita perder tiempo generando las piezas “obvias” y puede enfocarse exclusivamente en las partes que causan problemas a los humanos. 100 instrucciones van mucho más allá cuando se centran exclusivamente en puntos de acceso y lógica difícil en lugar de abarcar todo su programa.
Otro ejemplo sería cambiar el idioma en el que está trabajando. Un enfoque muy común es limitarnos al código de línea recta : sin ramas ni bucles. Esto hace la vida mucho más fácil para el sistema de búsqueda, a expensas de una menor generalidad. Por supuesto, el código de línea recta sigue siendo útil porque puede optimizar bucles internos estrechos que tienen efectos descomunales en el rendimiento de todo el programa.
Una opción más interesante es crear un lenguaje específico de dominio completo para su tarea, con límites estrictos en el flujo de control. Este es el enfoque que adopta Flash-Fill para generar eficientemente el código de manipulación de cadenas: debajo de las cubiertas, construye el programa en un lenguaje de manipulación de cadenas que (si no recuerdo mal) solo permite un único bucle externo y limita las operaciones que puede realizar . Esto realmente reduce el espacio de búsqueda al enfocar el solucionador solo en las operaciones que importan. En Flash-Fill, el lenguaje subyacente no es ni remotamente usable por los humanos, lo cual está bien porque nunca está expuesto al usuario. En cambio, el usuario solo ve el resultado del programa generado ejecutado en sus datos.
También podríamos compensar tiempos de ejecución largos calculando previamente funciones útiles con anticipación. Está bien tener un proceso que demore días en ejecutarse en un clúster grande si los resultados pueden ser reutilizados por diferentes personas varias veces. Un proyecto particularmente interesante en este sentido genera optimizaciones de mirilla que luego pueden usarse en un compilador de optimización.
Otras herramientas utilizan la misma tecnología para otros fines, como la depuración interactiva o las herramientas. No necesariamente escriben código para usted, pero usan la misma lógica para asesorarlo mientras trabaja.
Conclusión
De todos modos, el punto central es que las computadoras pueden escribir software por sí mismas, y que esta tecnología está bajo investigación activa. Con el tiempo, ambos hemos mejorado los sistemas para sintetizar código y hemos encontrado nuevos usos para los existentes. La mayoría de los avances aún son algo rudimentarios, y es poco probable que su uso sea real en el corto plazo, pero algunas tecnologías específicas como Flash-Fill ya han encontrado su camino en los productos de consumo .
¡Es un campo muy bueno para ver!