..
Conceptos en lenguajes de programación
Este libro proporciona una mejor comprensión de los problemas y compensaciones que surgen en el diseño de lenguajes de
programación y una mejor apreciación de las ventajas y desventajas de los lenguajes de programación utilizados.
Tabla de contenido
Prefacio
Capítulo 1 - Introducción
Capítulo 2 - Computabilidad
Capítulo 4 - Fundamentos
Glosario
Índice
Lista de Figuras
Lista de tablas
Equipo-Fly
Equipo-Fly
Contraportada
Este libro de texto para estudiantes de pregrado y postgrado principiantes explica y examina los conceptos centrales que se utilizan en los lenguajes de
programación modernos, como funciones, tipos, gestión de memoria y control. Este libro es único en su completa presentación y comparación de los principales
lenguajes de programación orientados a objetos. Capítulos separados examinan la historia de los objetos, Simula y Smalltalk, y los lenguajes prominentes C ++ y
Java.
El autor presenta temas fundamentales, como el cálculo lambda y la semántica denotacional, en un estilo informal y fácil de leer, centrándose en las principales
ideas proporcionadas por estas teorías. Los temas avanzados incluyen la programación simultánea y orientada a objetos. Un capítulo sobre programación lógica
ilustra la importancia de los métodos de programación especializados para ciertos tipos de problemas.
Este libro le dará al lector una mejor comprensión de los problemas y compensaciones que surgen en el diseño de lenguajes de programación y una mejor apreciación de
Sobre el Autor
John C. Mitchell es profesor de Ciencias de la Computación en la Universidad de Stanford, donde ha sido un profesor popular durante más de una década. Muchos de sus
antiguos alumnos tienen éxito en la investigación y la industria privada. Recibió su Ph.D. del MIT en 1984 y fue miembro del personal técnico de AT&T Bell Laboratories
antes de unirse a la facultad de Stanford. Durante los últimos veinte años, Mitchell ha sido un orador destacado en conferencias internacionales, ha dirigido proyectos de
investigación sobre una variedad de temas, incluido el diseño y análisis de lenguajes de programación, seguridad informática y aplicaciones de la lógica matemática a la
informática, y ha escrito más de 100 artículos de investigación. Su libro de texto de posgrado, Fundación para lenguajes de programación cubre el cálculo lambda, los
sistemas de tipos, la lógica para la verificación de programas y la semántica matemática de los lenguajes de programación. El profesor Mitchell fue miembro del esfuerzo
de estandarización y presidente del programa 2002 de la conferencia ACM Principles of Programming Languages.
Equipo-Fly
Equipo-Fly
Universidad Stanford
477 Williamstown Road, Port Melbourne, VIC 3207, Australia Ruiz de Alarcón
13, 28014 Madrid, España
Dock House, The Waterfront, Ciudad del Cabo 8001, Sudáfrica
http://www.cambridge.org
Este libro está en derecho de autor. Sujeto a las excepciones legales y a las disposiciones de los acuerdos de licencia colectiva pertinentes, no se puede
reproducir ninguna parte sin el permiso por escrito de Cambridge University Press.
Tipografías Times Ten 10 / 12.5 pt., ITC Franklin Gothic y Officina Serif Sistema LATEX2 ε [ TUBERCULOSIS]
Un registro del catálogo de este libro está disponible en la Biblioteca Británica. Datos de catalogación
0-521-78098-5
Este libro de texto para estudiantes de pregrado y postgrado principiantes explica y examina los conceptos centrales que se utilizan en los lenguajes de
programación modernos, como funciones, tipos, gestión de memoria y control. El libro es único en su completa presentación y comparación de los
principales lenguajes de programación orientados a objetos. Capítulos separados examinan la historia de los objetos, Simula y Smalltalk, y los lenguajes
prominentes C ++ y Java.
El autor presenta temas fundamentales, como el cálculo lambda y la semántica denotacional, en un estilo informal y fácil de leer, centrándose en las
principales ideas proporcionadas por estas teorías. Los temas avanzados incluyen la programación simultánea y orientada a objetos. Un capítulo sobre
programación lógica ilustra la importancia de los métodos de programación especializados para ciertos tipos de problemas.
Este libro le dará al lector una mejor comprensión de los problemas y compensaciones que surgen en el diseño de lenguajes de programación y una mejor
John C. Mitchell es profesor de Ciencias de la Computación en la Universidad de Stanford, donde ha sido un profesor popular durante más de una década. Muchos
de sus antiguos alumnos tienen éxito en la investigación y la industria privada. Recibió su Ph.D. del MIT en 1984 y fue miembro del personal técnico de AT&T Bell
Laboratories antes de unirse a la facultad de Stanford. Durante los últimos veinte años, Mitchell ha sido un orador destacado en conferencias internacionales; ha
dirigido proyectos de investigación sobre una variedad de temas, incluido el diseño y análisis de lenguajes de programación, seguridad informática y aplicaciones
de la lógica matemática a la informática; y ha escrito más de 100 artículos de investigación. Su libro de texto anterior, Fundamentos de los lenguajes de
programación ( MIT Press, 1996), cubre el cálculo lambda, sistemas de tipos, lógica para
verificación de programas y semántica matemática de lenguajes de programación. El profesor Mitchell fue miembro del subcomité de lenguajes de
programación del esfuerzo de estandarización ACM / IEEE Curriculum 2001 y el presidente del programa 2002 de la conferencia ACM Principles of
Programming Languages.
Equipo-Fly
Equipo-Fly
Prefacio
Un buen lenguaje de programación es un universo conceptual para pensar en programación. Alan Perlis, Conferencia
Los lenguajes de programación proporcionan las abstracciones, los principios de organización y las estructuras de control que los programadores utilizan para escribir buenos
programas. Este libro trata sobre los conceptos que aparecen en los lenguajes de programación, los problemas que surgen en su implementación y la forma en que el diseño del
Parte 1 contiene un breve estudio de Lisp como un ejemplo trabajado de análisis de lenguaje de programación y cubre la estructura del compilador, el análisis sintáctico, el cálculo
lambda y la semántica denotacional. Un breve capítulo de Computabilidad proporciona información sobre los límites del análisis y la optimización de programas en tiempo de
compilación.
Parte 2 utiliza los lenguajes de la familia de procedimientos Algol y ML para estudiar los tipos, la gestión de la memoria y las estructuras de control.
En Parte 3 observamos la organización del programa utilizando tipos de datos, módulos y objetos abstractos. Dado que la programación orientada a objetos es el
paradigma más destacado en la práctica actual, se comparan varios lenguajes orientados a objetos diferentes. Los capítulos separados exploran y comparan
Parte 4 contiene capítulos sobre mecanismos de lenguaje para concurrencia y sobre programación lógica.
El libro está dirigido a estudiantes universitarios de nivel superior y estudiantes graduados principiantes con algún conocimiento de programación básica. Se
espera que los estudiantes tengan algún conocimiento de C o algún otro lenguaje de procedimiento y algún conocimiento de C ++ o alguna forma de
lenguaje orientado a objetos. Alguna experiencia con Lisp, Scheme o ML es útil para Partes 1 y 2 , aunque muchos estudiantes han completado con éxito el
curso basado en este libro sin estos antecedentes. También es útil si los estudiantes tienen alguna experiencia con el análisis simple de algoritmos y
estructuras de datos. Por ejemplo, al comparar implementaciones de ciertas construcciones, será útil distinguir entre algoritmos de complejidad de tiempo
constante, polinomial y exponencial.
Después de leer este libro, los estudiantes tendrán una mejor comprensión de la gama de lenguajes de programación que se han utilizado durante los últimos
40 años, una mejor comprensión de los problemas y compensaciones que surgen en el diseño de lenguajes de programación, y una mejor apreciación de las
ventajas y trampas de los lenguajes de programación que utilizan. Debido a que los diferentes lenguajes presentan diferentes conceptos de programación, los
estudiantes podrán mejorar su programación importando ideas de otros lenguajes en los programas que escriben.
Expresiones de gratitud
Este libro se desarrolló como un conjunto de notas para Stanford CS 242, un curso de lenguajes de programación que he impartido desde 1993. Cada año,
asistentes de enseñanza enérgicos han ayudado a depurar programas de ejemplo para conferencias, formular problemas de tarea y preparar soluciones
modelo. La organización y el contenido del curso han mejorado enormemente gracias a sus sugerencias. Un agradecimiento especial para Kathleen Fisher,
quien fue profesora asistente en 1993 y 1994 e impartió el curso en mi ausencia en 1995. Kathleen me ayudó a organizar el material en los primeros años y,
en
1995, transcribí mis notas escritas a mano en forma online. Gracias a Amit Patel por su iniciativa en la organización de tareas y soluciones y a Vitaly
Shmatikov por perseverar en el glosario de términos del lenguaje de programación. Anne Bracy, Dan Bentley y Stephen Freund revisaron cuidadosamente
muchos capítulos.
Lauren Cowles, Alan Harvey y David Tranah de Cambridge University Press fueron alentadores y serviciales. Agradezco particularmente la lectura cuidadosa de Lauren y
los comentarios detallados de doce capítulos completos en forma de borrador. También agradecemos a los revisores que reclutaron, quienes hicieron una serie de
sugerencias útiles sobre las primeras versiones del libro. Zena Ariola enseñó a partir de borradores de libros en la Universidad de Oregon durante varios años seguidos y
envió muchas sugerencias útiles; otros instructores de prueba también proporcionaron comentarios útiles.
Finalmente, un agradecimiento especial a Krzystof Apt por contribuir con un capítulo sobre programación lógica. John Mitchell
Equipo-Fly
Equipo-Fly
Capítulo 1 : Introducción
Capítulo 2 : Computabilidad
Capítulo 4 : Fundamentos
Equipo-Fly
Equipo-Fly
Capítulo 1: Introducción
Los lenguajes de programación son el medio de expresión en el arte de la programación informática. Un lenguaje de programación ideal facilitará a los
programadores la escritura de programas de forma concisa y clara. Debido a que los programas están destinados a ser entendidos, modificados y
mantenidos durante su vida, un buen lenguaje de programación ayudará a otros a leer programas y comprender cómo funcionan. El diseño y la
construcción de software son tareas complejas. Muchos sistemas de software constan de partes que interactúan. Estas partes, o componentes de
software, pueden interactuar de formas complicadas. Para gestionar la complejidad, las interfaces y la comunicación entre los componentes deben
diseñarse con cuidado. Un buen lenguaje para la programación a gran escala ayudará a los programadores a gestionar la interacción entre los
componentes de software de forma eficaz. Al evaluar los lenguajes de programación, debemos considerar las tareas de diseño,
Hay muchas compensaciones difíciles en el diseño de lenguajes de programación. Algunas características del lenguaje nos facilitan la escritura de programas
rápidamente, pero pueden dificultarnos el diseño de herramientas o métodos de prueba. Algunas construcciones de lenguaje facilitan al compilador la optimización de
programas, pero pueden hacer que la programación sea engorrosa. Debido a que los diferentes entornos informáticos y aplicaciones requieren diferentes características
del programa, los diferentes diseñadores de lenguajes de programación han optado por diferentes compensaciones. De hecho, prácticamente todos los lenguajes de
programación exitosos se diseñaron originalmente para un uso específico. Esto no quiere decir que cada idioma sea bueno para un solo propósito. Sin embargo, centrarse
en una sola aplicación ayuda a los diseñadores de lenguajes a tomar decisiones coherentes y con un propósito. Una sola aplicación también ayuda con una de las partes
lenguajes de programación. Algunas de estas personas son famosas, con importantes premios y biografías publicadas. Otros son menos reconocidos.
Cuando ha sido posible, he intentado incluir información personal basada en mis encuentros con estas personas. Esto es para enfatizar que los lenguajes
de programación son desarrollados por seres humanos reales. Como la mayoría de los artefactos humanos, un lenguaje de programación refleja
Como descargo de responsabilidad, permítanme señalar que no he intentado ser exhaustivo en mis breves comentarios biográficos. He tratado de
animar el texto con un poco de humor cuando era posible, dejando la biografía seria a biógrafos más serios. Simplemente no hay espacio para
mencionar a todas las personas que han jugado papeles importantes en la historia de los lenguajes de programación.
Los textos históricos y biográficos sobre informática y científicos de la computación se han vuelto cada vez más disponibles en los últimos años. Si le
gusta leer sobre los pioneros de la informática, puede que le guste hojear Fuera de
Sus mentes: las vidas y los descubrimientos de 15 grandes informáticos por Dennis Shasha y Cathy Lazere
u otros libros sobre la historia de la informática. John Mitchell
Incluso si no utiliza muchos de los lenguajes de programación de este libro, es posible que pueda hacer un buen uso del marco conceptual presentado en
estos lenguajes. Cuando era estudiante a mediados de la década de 1970, todos los programadores "serios" (en mi universidad, de todos modos) usaban
Fortran. Fortran no permitía la recursividad y, en general, se consideraba que la recursividad era demasiado ineficaz para ser práctica para la
"programación real". Sin embargo, el instructor de un curso que tomé argumentó que la recursividad todavía era una idea importante y explicó cómo las
técnicas recursivas podrían usarse en Fortran administrando datos en una matriz. Me alegro de haber tomado ese curso y no uno que descartara la
recursividad como una idea poco práctica. En la década de 1980, mucha gente consideraba que la programación orientada a objetos era demasiado
ineficiente y torpe para la programación real. Sin embargo,
Aunque este no es un libro sobre la historia de los lenguajes de programación, se presta cierta atención a la historia a lo largo del libro. Una razón para
discutir los lenguajes históricos es que esto nos da una forma realista de entender las compensaciones de los lenguajes de programación. Por ejemplo,
los programas eran diferentes cuando las máquinas eran lentas y la memoria escasea. Por lo tanto, las preocupaciones de los diseñadores de lenguajes
de programación eran diferentes en la década de 1960 de las preocupaciones actuales. Al imaginar el estado del arte en alguna época pasada, podemos
pensar más seriamente en por qué los diseñadores de lenguajes tomaron ciertas decisiones. Esta forma de pensar sobre los lenguajes y la informática
puede ayudarnos en el futuro, cuando las condiciones informáticas pueden cambiar para parecerse a alguna situación pasada. Por ejemplo,
Cuando hablamos de idiomas específicos en este libro, generalmente nos referimos a la forma original o históricamente importante de un idioma. Por ejemplo, "Fortran"
significa el Fortran de la década de 1960 y principios de la de 1970. Estos primeros idiomas se llamaron Fortran I, Fortran II, Fortran III, etc. En los últimos años, Fortran
ha evolucionado para incluir características más modernas, y la distinción entre Fortran y otros idiomas se ha difuminado hasta cierto punto. De manera similar, Lisp
generalmente se refiere a los Lisps de los años sesenta, Smalltalk al lenguaje de finales de los setenta y ochenta, y así sucesivamente.
Equipo-Fly
Equipo-Fly
1.2 OBJETIVOS
En este libro nos ocupamos de los conceptos básicos que aparecen en los lenguajes de programación modernos, su interacción y la relación entre los lenguajes de
programación y los métodos para el desarrollo de programas. Un tema recurrente es el compromiso entre la expresividad del lenguaje y la simplicidad de
implementación. Para cada característica del lenguaje de programación que consideramos, examinamos las formas en que se puede usar en programación y los
tipos de técnicas de implementación que se pueden usar para compilarlo y ejecutarlo de manera eficiente.
Para entender el espacio de diseño de lenguajes de programación. Esto incluye conceptos y construcciones de lenguajes de programación pasados, así
como aquellos que pueden usarse más ampliamente en el futuro. También intentamos comprender algunos de los principales conflictos y
compensaciones entre las características del lenguaje, incluidos los costos de implementación.
Desarrollar una mejor comprensión de los idiomas que usamos actualmente comparándolos con otros idiomas.
Comprender las técnicas de programación asociadas con diversas características del lenguaje. El estudio de lenguajes de programación es,
en parte, el estudio de marcos conceptuales para la resolución de problemas, la construcción y el desarrollo de software.
Muchas de las ideas de este libro son de conocimiento común entre los programadores profesionales. El material y las formas de pensar que se
presentan en este libro deberían serle útiles en la programación futura y al hablar con programadores experimentados si trabaja para una empresa de
software o tiene una entrevista de trabajo. Al final del curso, podrá evaluar las características del lenguaje, sus costos y cómo encajan.
Computabilidad: Algunos problemas no se pueden resolver con una computadora. La indecidibilidad del problema de la detención implica que los
compiladores e intérpretes de lenguajes de programación no pueden hacer todo lo que desearíamos que pudieran hacer.
Análisis estático: Existe una diferencia entre el tiempo de compilación y el tiempo de ejecución. En el momento de la compilación, se conoce el programa pero no la
entrada. En el tiempo de ejecución, el programa y la entrada están disponibles para el sistema de tiempo de ejecución. Aunque a un diseñador o implementador de
programas le gustaría encontrar errores en el momento de la compilación, muchos no aparecerán hasta el momento de la ejecución. Los métodos que detectan errores
de programa en tiempo de compilación suelen ser conservadores, lo que significa que cuando dicen que un programa no tiene cierto tipo de error, esta afirmación es
correcta. Sin embargo, los métodos de detección de errores en tiempo de compilación suelen decir que algunos programas contienen errores, incluso si es posible que
Expresividad versus eficiencia: Hay muchas situaciones en las que sería conveniente que la implementación de un lenguaje de
programación hiciera algo automáticamente. Un ejemplo discutido en
Capítulo 3 es la gestión de la memoria: el sistema Lisp en tiempo de ejecución utiliza la recolección de basura para detectar ubicaciones
de memoria que el programa ya no necesita. Cuando algo se hace automáticamente, hay un costo. Aunque un método automático puede
evitar que el programador piense en algo, la implementación del lenguaje puede ser más lenta. En algunos casos, el método automático
puede facilitar la escritura de programas y hacer que la programación sea menos propensa a errores. En otros casos, el
La ralentización resultante en la ejecución del programa puede hacer que el método automático no sea viable.
Equipo-Fly
Equipo-Fly
Se han diseñado e implementado cientos de lenguajes de programación durante los últimos 50 años. Hasta 50 de estos lenguajes de programación contenían
nuevos conceptos, refinamientos útiles o innovaciones dignas de mención. Sin embargo, debido a que hay demasiados lenguajes de programación para
estudiar, nos concentramos en seis lenguajes de programación: Lisp, ML, C, C ++, Smalltalk y Java. Juntos, estos lenguajes contienen la mayoría de las
características importantes del lenguaje que se han inventado desde que los lenguajes de programación de alto nivel surgieron del pantano primordial de la
La historia de los lenguajes de programación modernos comienza alrededor de 1958-1960 con el desarrollo de Algol, Cobol, Fortran y Lisp. El cuerpo principal de este libro
cubre Lisp, con una discusión más corta de Algol y los lenguajes relacionados subsiguientes. Aquí se ofrece una breve descripción de algunos lenguajes anteriores para
aquellos que puedan tener curiosidad sobre la prehistoria de los lenguajes de programación.
En la década de 1950, se desarrollaron varios lenguajes para simplificar el proceso de escritura de secuencias de instrucciones de computadora. En esta década, las
computadoras eran muy primitivas según los estándares modernos. La mayor parte de la programación se realizó con el lenguaje de máquina nativo del hardware
subyacente. Esto era aceptable porque los programas eran pequeños y la eficiencia era extremadamente importante. Los dos desarrollos de lenguaje de programación más
Fortran fue desarrollado en IBM alrededor de 1954-1956 por un equipo dirigido por John Backus. La principal innovación de Fortran (una contracción del
traductor de fórmulas) fue que se hizo posible utilizar la notación matemática ordinaria en las expresiones. Por ejemplo, la expresión de Fortran para sumar el
de Fortran, podría haber sido necesario colocar i en un registro, colocar j en un registro, multiplicar j por 2 y luego sumar el resultado a i. Fortran permitió a
los programadores pensar de forma más natural sobre el cálculo numérico mediante el uso de nombres simbólicos para las variables y dejando algunos
detalles del orden de evaluación al compilador. Fortran también tenía subrutinas (una forma de procedimiento o función), matrices, entrada y salida
formateadas y declaraciones que daban a los programadores un control explícito sobre la ubicación de variables y matrices en la memoria. Sin embargo,
eso fue todo. Para darle una idea de las limitaciones de Fortran, muchos de los primeros compiladores de Fortran almacenaban los números 1, 2, 3 ... en
ubicaciones de memoria, ¡y los programadores podían cambiar los valores de los números si no tenían cuidado! Además, no era posible que una subrutina
de Fortran se llamara a sí misma, Capítulo 7 ).
Cobol es un lenguaje de programación diseñado para aplicaciones comerciales. Al igual que los programas de Fortran, muchos programas de Cobol todavía se utilizan
hoy en día, aunque las versiones actuales de Fortran y Cobol difieren sustancialmente de las formas de estos lenguajes de la década de 1950. El diseñador principal de
Cobol fue Grace Murray Hopper, una importante pionera de la informática. La sintaxis de Cobol tenía la intención de parecerse a la del inglés común. Se ha sugerido en
broma que si Cobol orientado a objetos fuera un estándar hoy, usaríamos "agregar 1 a Cobol dando Cobol" en lugar de "C ++".
Los primeros lenguajes que se tratan con todo detalle en este libro son Lisp y Algol, que aparecieron alrededor de 1960. Estos lenguajes tienen funciones o procedimientos
recursivos y administración de memoria de pila. Lisp proporciona funciones de orden superior (aún no disponibles en muchos lenguajes actuales) y recolección de basura,
mientras que la familia de lenguajes Algol proporciona mejores sistemas de tipos y estructuración de datos. Las principales innovaciones de la década de 1970 fueron los
métodos para organizar datos, como registros (o estructuras), tipos de datos abstractos y formas tempranas de objetos. Los objetos se convirtieron en la corriente principal en
la década de 1980, y la de 1990 trajo un interés creciente en la computación, la interoperabilidad y la seguridad centradas en la red y
problemas de corrección asociados con el contenido activo en Internet. El siglo XXI promete una mayor diversidad de dispositivos informáticos,
Equipo-Fly
Equipo-Fly
Hay muchos conceptos de lenguaje importantes y muchos lenguajes de programación. La forma más natural de resumir el campo es utilizar una matriz
bidimensional, con lenguajes a lo largo de un eje y conceptos a lo largo del otro. Aquí hay un bosquejo parcial de dicha matriz:
almacenamiento
Ceceo X X X
C X X X
Algol 60 X X
Algol 68 X X X X
Pascal X X X
Modula-2 X X X X
Modula-3 X X X X X X
ML X X X X X
Simula X X X X X
Charla X X X X X X
C ++ X X X X X X
C objetivo X X X X
Java X X X X X X X
Aunque esta matriz enumera solo una fracción de los lenguajes y conceptos que se pueden cubrir en un texto o curso básico sobre lenguajes de programación,
una característica general debe quedar clara. Hay algunos conceptos básicos del lenguaje, como expresiones, funciones, variables locales y asignación de
almacenamiento de pila que están presentes en muchos lenguajes. Para estos conceptos, tiene más sentido discutir el concepto en general que revisar una
larga lista de lenguajes similares. Por otro lado, para conceptos como objetos e hilos, hay relativamente pocos lenguajes que exhiban estos conceptos de
formas interesantes. Por lo tanto, podemos estudiar la mayoría de los aspectos interesantes de los objetos comparando algunos idiomas. Otro factor que no
está claro en la matriz es que, para algunos conceptos, existe una variación considerable de un idioma a otro. Por ejemplo, es más interesante comparar la
forma en que los objetos se han integrado en los lenguajes que comparar expresiones enteras. Esta es otra razón por la que se comparan los lenguajes
orientados a objetos que compiten, pero los conceptos básicos relacionados con expresiones, declaraciones, funciones, etc., se cubren solo una vez, de una
La mayoría de los cursos y textos sobre lenguajes de programación utilizan alguna combinación de presentación basada en lenguaje y basada en conceptos. En este libro se sigue una
organización orientada a conceptos para la mayoría de los conceptos, con una organización basada en el lenguaje que se utiliza para comparar características orientadas a objetos.
En Parte 1 Se presenta un breve estudio de Lisp, seguido de una discusión sobre la estructura del compilador, el análisis sintáctico, el cálculo lambda y la semántica denotacional. Un
breve capítulo proporciona una breve discusión sobre la computabilidad y los límites del análisis y la optimización de programas en tiempo de compilación. Para los programadores de C,
la discusión de Lisp debería proporcionar una buena oportunidad para pensar de manera diferente sobre programación y lenguajes de programación.
En Parte 2 , avanzamos por los principales conceptos asociados a los lenguajes convencionales que de alguna manera descienden de la familia Algol. Estos
conceptos incluyen sistemas de tipos y verificación de tipos, funciones y asignación de almacenamiento de pila, y mecanismos de control como excepciones y
continuaciones. Después de resumir parte de la historia de la familia de lenguajes Algol, el lenguaje de programación ML se utiliza como ejemplo principal, con
Parte 3 es una investigación de los mecanismos de estructuración de programas. Los importantes avances en el lenguaje de la década de 1970 fueron los tipos de datos abstractos y los módulos
de programas. A finales de la década de 1980, los conceptos orientados a objetos alcanzaron una amplia aceptación. Debido a que la programación orientada a objetos es actualmente el
Parte 3 nos centramos en conceptos y lenguajes orientados a objetos, comparando Smalltalk, C ++ y Java.
Parte 4 contiene capítulos sobre mecanismos de lenguaje para programas concurrentes y distribuidos y sobre programación lógica.
Debido a las limitaciones de espacio, no se tratan varios temas interesantes. Aunque los lenguajes de programación y otros lenguajes de "propósito
especial" no se tratan explícitamente en detalle, se ha intentado integrar algunos conceptos lingüísticos relevantes en los ejercicios.
Equipo-Fly
Equipo-Fly
Capitulo 2: Computabilidad
Algunas funciones matemáticas son computables y otras no. En todos los lenguajes de programación de propósito general, es posible escribir un programa para cada
función que sea computable en principio. Sin embargo, los límites de la computabilidad también limitan el tipo de cosas que pueden hacer las implementaciones del lenguaje
de programación. Este capítulo contiene una breve descripción general de la computabilidad para que podamos discutir las limitaciones que involucran la computabilidad en
Desde un punto de vista matemático, un programa define una función. La salida de un programa se calcula en función de las entradas del programa y el estado de
la máquina antes de que comience el programa. En la práctica, hay mucho más en un programa que la función que calcula. Sin embargo, como punto de partida en
el estudio de los lenguajes de programación, es útil comprender algunos hechos básicos sobre las funciones computables.
El hecho de que no todas las funciones sean computables tiene ramificaciones importantes para las herramientas e implementaciones del lenguaje de programación.
Algunos tipos de construcciones de programación, por muy útiles que puedan ser, no se pueden agregar a lenguajes de programación reales porque no se pueden
En matemáticas, una expresión puede tener un valor definido o no. Por ejemplo, la expresión 3 + 2 tiene un valor definido, pero la expresión 3/0 no. La
razón por la que 3/0 no tiene valor es que la división por cero es
no definido: la división se define como la inversa de la multiplicación, pero la multiplicación por cero no se puede invertir. No hay nada que intentar hacer
cuando vemos la expresión 3/0; un matemático diría simplemente que esta operación no está definida, y ese sería el final de la discusión.
En el cálculo, hay dos razones diferentes por las que una expresión podría no tener un valor:
Alan Turing fue un matemático británico. Es conocido por sus primeros trabajos sobre computabilidad y su trabajo para la inteligencia británica sobre descifrado
de códigos durante la Segunda Guerra Mundial. Entre los informáticos, es más conocido por la invención de la máquina de Turing. No se trata de una pieza de
hardware, sino de un dispositivo informático idealizado. Una máquina de Turing consta de una cinta infinita, un cabezal de lectura y escritura de cinta y un
controlador de estado finito. En cada paso de cálculo, la máquina lee un símbolo de cinta y el controlador de estado finito decide si escribir un símbolo diferente
en el cuadrado de cinta actual y luego si mover el cabezal de lectura-escritura un cuadrado hacia la izquierda o hacia la derecha. La importancia de esta
Turing era un individuo de mente amplia con intereses que iban desde la teoría de la relatividad y la lógica matemática hasta la teoría de números y el diseño
de ingeniería de computadoras mecánicas. Existen numerosas biografías publicadas de Alan Turing, algunas enfatizando su trabajo en tiempos de guerra y
El premio ACM Turing es el más alto honor científico en informática, equivalente a un premio Nobel en otros campos.
Terminación por error: La evaluación de la expresión no puede continuar debido a un conflicto entre el operador y el operando.
Un ejemplo del primer tipo es la división por cero. No hay nada que calcular en este caso, excepto posiblemente detener el cálculo de una manera que
indique que no podría continuar más. Esto puede detener la ejecución de todo el programa, abortar un hilo de un programa concurrente o generar una
excepción si el lenguaje de programación proporciona excepciones.
El segundo caso es diferente: hay un cálculo específico que realizar, pero es posible que el cálculo no finalice y, por lo tanto, no arroje un valor. Por
ejemplo, considere la función recursiva definida por
Ésta es una definición perfectamente significativa de un parcial function, una función que tiene un valor en algunos argumentos pero no en todos los
argumentos. La expresión f (4) que llama a la función f anterior tiene valor 4 + 2 + 0 = 6, pero la expresión f (5) no tiene valor porque el cálculo
especificado por esta expresión no termina.
programación, ya que una función declarada en un programa puede devolver un resultado o no si algún bucle o secuencia de llamadas recursivas no termina. Sin
embargo, esto no es lo que un matemático normalmente quiere decir con la palabra función.
La distinción se puede aclarar con una mirada a las definiciones matemáticas. Una definición razonable de la palabra función es la siguiente: Una función f: A → B del
set A establecer B es una regla que asocia un valor único y = f (x) en B con todo X en
UNA. Esta es casi una definición matemática, excepto que la palabra regla no tiene un significado matemático preciso. La notación f: A → B significa que, dados
La definición matemática habitual de función reemplaza la idea de regla con un conjunto de pares argumento-resultado llamado gráfico de una función. Esta
es la definición matemática:
1. ¿Si? x, y? ? F y ? x, z ?? F, entonces y = z.
Cuando asociamos un conjunto de pares ordenados con una función, ¿el par ordenado? x, y? se utiliza para indicar que y es el valor de la función en el argumento X. En
palabras, las dos condiciones anteriores se pueden establecer como (1) una función tiene como máximo un valor para cada argumento en su dominio, y (2) una función
Una función parcial es similar, excepto que una función parcial puede no tener un valor para cada argumento en su dominio. Esta es la definición matemática:
1. ¿Si? x, y ?? F y ? x, z ?? F, entonces y = z.
En palabras, una función parcial tiene un solo valor, pero no es necesario definirla en todos los elementos de su dominio.
En la mayoría de los lenguajes de programación, es posible definir funciones de forma recursiva. Por ejemplo, aquí hay una función f definida en términos de sí misma:
Si esto se escribiera como un programa en algún lenguaje de programación, la declaración asociaría el nombre de la función f con un algoritmo que
termina en cada par. ≥ 0, pero diverge (no se detiene y devuelve un valor) si x es impar o negativo. El algoritmo para f define la siguiente función
Esta es una función parcial de los enteros. Por cada entero X, hay como máximo uno y con f (x) = y. Sin embargo, si X es un número impar, entonces no hay y con f
(x) = y. Cuando el algoritmo no termina, el valor de la función no está definido. Debido a que una llamada a una función no puede terminar, este programa define
una función parcial.
2.1.3 Computabilidad
La teoría de la computabilidad nos da una caracterización precisa de las funciones que son computables en principio. La clase de funciones en los números naturales
que son computables en principio se llama a menudo la clase de funciones recursivas parciales,
ya que la recursividad es una parte esencial del cálculo y las funciones computables son, en general, parciales en lugar de totales. La razón por la que decimos
"computable en principio" en lugar de "computable en la práctica" es que algunas funciones computables pueden tardar mucho en calcularse. Si una llamada a una
función no regresa durante un período de tiempo igual a la duración de la historia completa del universo, entonces, en la práctica, no podremos esperar a que finalice
el cálculo. No obstante, la computabilidad en principio es un punto de referencia importante para los lenguajes de programación.
Funciones computables
Intuitivamente, una función es calculable si hay algún programa que lo calcule. Más específicamente, una función f: A → B es computable si hay un algoritmo que, dado
Un problema con esta definición intuitiva de computable es que un programa tiene que estar escrito en algún lenguaje de programación y necesitamos
alguna implementación para ejecutar el programa. Es muy posible que, en un lenguaje de programación, haya un programa para calcular alguna función
matemática y en otro lenguaje no lo haya.
En la década de 1930, Alonzo Church de la Universidad de Princeton propuso un principio importante, llamado tesis de Church. La tesis de Church, que es una creencia
generalizada sobre la relación entre las definiciones matemáticas y el mundo real de la informática, establece que cualquier dispositivo informático general puede calcular
la misma clase de funciones en los números enteros. Esta es la clase de funciones recursivas parciales, a veces llamada la clase de funciones computables. Existe una
definición matemática de esta clase de funciones que no se refiere a los lenguajes de programación, una segunda definición que utiliza una especie de dispositivo
informático idealizado llamado Máquina de Turing, y una tercera definición (equivalente) que usa cálculo lambda (ver Sección 4.2 ). Como se menciona en el bosquejo
biográfico de Alan Turing, una máquina de Turing consta de una cinta infinita, un cabezal de lectura y escritura de cinta y un controlador de estado finito. La cinta se
divide en celdas contiguas, cada una de las cuales contiene un solo símbolo. En cada paso de cálculo, la máquina lee un símbolo de cinta y el controlador de estado finito
decide si escribir un símbolo diferente en el cuadrado de cinta actual y luego si mover el cabezal de lectura-escritura un cuadrado hacia la izquierda o hacia la derecha.
Parte de la evidencia que Church citó al formular esta tesis fue la prueba de que las máquinas de Turing y el cálculo lambda son equivalentes. El hecho de que todos los
lenguajes de programación estándar expresen precisamente la clase de funciones recursivas parciales a menudo se resume en la afirmación de que todos los lenguajes
de programación son Turing completos. Aunque es reconfortante saber que todos los lenguajes de programación son universales en un sentido matemático, el hecho
de que todos los lenguajes de programación sean Turing completos también significa que la teoría de la computabilidad no nos ayuda a distinguir entre los poderes
Funciones no computables
Es útil saber que algunas funciones específicas no son computables. Un ejemplo importante se conoce comúnmente como detener el problema . Para simplificar la discusión y
enfocarse en las ideas centrales, el problema de la detención se establece para programas que requieren una entrada de cadena. Si PAG es un programa de este tipo y X es una
entrada de cadena, luego escribimos P (x) para la salida del programa PAG en la entrada X.
Problema de detención: Dado un programa PAG que requiere exactamente una entrada de cadena y una cadena X, determinar si PAG se detiene en la entrada X.
Podemos asociar el problema de la detención con una función fhalt Dejando fhalt (P, x) = " detiene "si PAG se detiene en la entrada y fhalt (P,
x) = " no se detiene "de lo contrario. Esta función fhalt puede considerarse una función en cadenas si escribimos cada programa como una secuencia de símbolos.
los indecidibilidad del problema de la detención es el hecho de que la función fhalt no es computable. La indecidibilidad del problema de la detención es un hecho
importante a tener en cuenta al diseñar implementaciones y optimizaciones de lenguajes de programación. Implica que muchas operaciones útiles sobre programas no
Prueba de la indecidibilidad del problema de la detención. Aunque no necesitará conocer esta prueba para comprender ningún otro tema del libro, algunos de
ustedes pueden estar interesados en una prueba de que la función de detención no es computable. La prueba es sorprendentemente breve, pero puede
resultar difícil de comprender. Si va a ser un científico informático serio, entonces querrá ver esta prueba varias veces, en el transcurso de varios días, hasta que
Paso 1: suponga que existe un programa Q que resuelve el problema de la detención. Específicamente, suponga que el programa Q lee dos
Paso 2: usar el programa Q, podemos construir un programa D que lee una entrada de cadena y, a veces, no se detiene. Específicamente, deje D ser un
Tenga en cuenta que D tiene una sola entrada, que da dos veces a Q. El programa D se puede escribir en cualquier lenguaje razonable, ya
que cualquier lenguaje razonable debería tener alguna forma de programar if-then-else y alguna forma de escribir un bucle o una llamada de
función recursiva que se ejecute para siempre. Si lo piensas un poco, puedes ver que D tiene el siguiente comportamiento:
En esta descripción, la palabra detener significa que D (P) se detiene y corre para siempre significa que D (P)
continúa ejecutando pasos indefinidamente. El programa D (P) se detiene o no se detiene, pero no produce una salida de cadena en ningún caso.
Paso 3: Derive una contradicción considerando el comportamiento D (D) del programa D en la entrada D. ( Si comienza a confundirse acerca
de lo que significa ejecutar un programa con el programa en sí como entrada, suponga que hemos escrito el programa. D y lo guardó en un
con el archivo que contiene una copia de D como entrada.) Sin pensar en cómo D funciona o que D se supone que debe hacer, está claro que D (D) se
detiene o D (D) no se detiene. Si D (D) se detiene, sin embargo, entonces por la propiedad de D dado en el paso 2, esto debe ser porque D (D) corre para
siempre. Esto no tiene ningún sentido, por lo que debe ser que D (D) corre para siempre. Sin embargo, con un razonamiento similar, si D (D) corre para
siempre, entonces esto debe ser porque D (D) se detiene. Esto también es contradictorio. Por tanto, hemos llegado a una contradicción.
Paso 4: debido a que la suposición en el paso 1 de que existe un programa Q resolver el problema de la detención conduce a una contradicción en el
paso 3, debe ser que la suposición es falsa. Por lo tanto, no existe ningún programa que resuelva el problema de la detención.
Aplicaciones
Los compiladores de lenguajes de programación a menudo pueden detectar errores en los programas. Sin embargo, la indecidibilidad del problema de la detención implica que
algunas propiedades de los programas no se pueden determinar de antemano. El ejemplo más simple es detenerse. Supongamos que alguien escribe un programa como este:
i = 0;
Parece muy probable que el programador quiera que se detenga el ciclo while. De lo contrario, ¿por qué habría escrito el programador una declaración para
imprimir el valor de i después de que el ciclo se detenga? Por lo tanto, sería útil para el compilador imprimir un mensaje de advertencia si el bucle no se detiene. Sin
embargo, por muy útil que esto pueda ser, un compilador no puede determinar si el bucle se detendrá, ya que esto implicaría resolver el problema de la detención.
Equipo-Fly
Equipo-Fly
La teoría de la computabilidad establece algunas reglas básicas importantes para el diseño y la implementación de lenguajes de programación. Deben recordarse los siguientes
Parcialidad: Las funciones definidas de forma recursiva pueden ser funciones parciales. No siempre son funciones totales. Una función
puede ser parcial porque una operación básica no está definida en algún argumento o porque un cálculo no termina.
Computabilidad: Algunas funciones son computables y otras no. Los lenguajes de programación se pueden utilizar para definir funciones
computables; no podemos escribir programas para funciones que no son computables en principio.
Completitud de Turing: Todos los lenguajes de programación estándar de propósito general nos dan la misma clase de funciones computables.
Indecidibilidad: Muchas propiedades importantes de los programas no pueden ser determinadas por ninguna función computable. En particular, el
Cuando el valor de una función o el valor de una expresión no está definido porque una operación básica como la división por cero no tiene sentido, un
compilador o intérprete puede hacer que el programa se detenga e informe el error. Sin embargo, la indecidibilidad del problema de la detención implica
que no hay forma de detectar y reportar un error cuando un programa no se detiene.
Hay mucho más en la teoría de la computabilidad y la complejidad de lo que se resume en las pocas páginas aquí. Para obtener más información, consulte uno de los
muchos libros sobre computabilidad y teoría de la complejidad, como Introducción a la teoría, los lenguajes y la computación de los autómatas por Hopcroft, Motwani
Equipo-Fly
Equipo-Fly
EJERCICIOS
Para cada una de las siguientes definiciones de función, proporcione la gráfica de la función. Di si se trata de una función parcial o una función total de los
números enteros. Si la función es parcial, diga dónde está definida e indefinida la función.
Por ejemplo, la gráfica de f (x) = si x> 0 entonces x + 2 si no x / 0 es el conjunto de pares ordenados {? x, x + 2? | x> 0}. Esta es una función parcial. Se define en todos los números
Funciones:
C. f (x) = si x = 0 entonces 1 si no f (x - 2)
Suponga que se le da una función Detener ∅ que se puede utilizar para determinar si un programa que no requiere entrada se detiene. Para que esto sea concreto,
suponga que está escribiendo un programa en C o Pascal que se lee en otro programa como una cadena. ¿Su programa puede llamar a Halt? con una entrada de
cadena. ¿Asumir que la llamada a Detener? devuelve verdadero si el argumento es un programa que se detiene y no lee ninguna entrada y devuelve falso si el
argumento es un programa que se ejecuta indefinidamente y no lee ninguna entrada. ¿No debería hacer suposiciones sobre el comportamiento de Halt? en un
¿Puedes resolver el problema de la detención usando Halt? Más específicamente, ¿puede escribir un programa que lea el texto de un programa? PAG como entrada, lee
un entero norte como entrada, y luego decide si PAG se detiene cuando lee norte como entrada? Puede suponer que cualquier programa PAG se le da comienza con
una declaración de lectura que lee un solo entero de la entrada estándar. Este problema no le pide que escriba el programa para resolver el problema de la detención.
Si cree que el problema de la detención se puede resolver si se le da ¿Detener ?, entonces explique su respuesta describiendo cómo funcionaría un programa que
resuelva el problema de la detención. Si cree que el problema de la detención no se puede resolver utilizando Halt ?, explique brevemente por qué cree que no.
Suponga que se le da una función Detener ∀ que se puede utilizar para determinar si un programa se detiene en todas las entradas. En las mismas condiciones que las del
Equipo-Fly
Equipo-Fly
VISIÓN GENERAL
Lisp es el medio elegido por las personas que disfrutan del estilo libre y la flexibilidad.
- - Gerald J. Sussman
Lisp es un lenguaje de importancia histórica que es bueno para ilustrar una serie de puntos generales sobre los lenguajes de programación. Debido a que Lisp es muy diferente
de los lenguajes orientados a procedimientos y orientados a objetos que puede usar con más frecuencia, este capítulo puede ayudarlo a pensar en la programación de una
manera diferente. Lisp muestra que muchos objetivos del diseño de lenguajes de programación se pueden cumplir de una manera simple y elegante.
Equipo-Fly
Equipo-Fly
El lenguaje de programación Lisp se desarrolló en el MIT a finales de la década de 1950 para la investigación en inteligencia artificial (IA) y computación simbólica. El nombre
Lisp es un acrónimo de LIS t PAG procesador. Las listas comprenden la estructura de datos principal de Lisp.
La fuerza de Lisp es su simplicidad y flexibilidad. Se ha utilizado ampliamente para la programación exploratoria, un estilo de desarrollo de software en el que los sistemas
se construyen de forma incremental y pueden cambiarse radicalmente como resultado de una evaluación experimental. La programación exploratoria se utiliza a menudo
en el desarrollo de programas de IA, ya que un investigador puede no saber cómo el programa debe realizar una tarea hasta que se hayan probado varios programas
fallidos. El popular editor de texto emacs está escrito en Lisp, al igual que el kit de herramientas gráficas de Linux gtk y muchos otros programas de uso actual en una
Se han construido muchas implementaciones diferentes de Lisp a lo largo de los años, lo que ha llevado a muchos dialectos diferentes del idioma. Un dialecto influyente fue
Maclisp, desarrollado en la década de 1960 en el Proyecto MAC del MIT. Otro fue Scheme, desarrollado en el MIT en la década de 1970 por Guy Steele y Gerald Sussman.
Common Lisp es un Lisp moderno con formas complejas de primitivas orientadas a objetos.
Un diseñador de lenguajes de programación y una figura central en el campo de la inteligencia artificial, John McCarthy dirigió el esfuerzo original de Lisp en el MIT a finales de
Computación del MIT. McCarthy se mudó a Stanford en 1962, donde ha estado en la facultad desde entonces.
A lo largo de su carrera, John McCarthy ha abogado por el uso de la lógica formal y las matemáticas para comprender los lenguajes y sistemas de programación,
así como el razonamiento de sentido común y otros temas de la inteligencia artificial. A principios de la década de 1960, escribió una serie de artículos sobre lo
que llamó un Teoría matemática de la computación. Estos identificaron una serie de problemas importantes en la comprensión y el razonamiento acerca de los
programas y sistemas informáticos. Apoyó la libertad política de los científicos en el extranjero durante la Guerra Fría y ha sido un defensor de la libertad de
Ahora, una persona vivaz con cabello y barba canosos, McCarthy es un pensador independiente que sugiere soluciones creativas para
problemas burocráticos y técnicos. Ha ganado varios premios y honores importantes, incluido el Premio ACM Turing en 1971.
El artículo de 1960 de McCarthy sobre Lisp, llamado "Funciones recursivas de expresiones simbólicas y su cálculo por máquina" [ Comunicaciones de la
Asociación de Maquinaria de Computación, 3 ( 4), 184-195 (1960)] es un documento histórico importante con muchas buenas ideas. Además del valor de las
ideas del lenguaje de programación que contiene, el documento nos da una idea del estado del arte en 1960 y proporciona una visión útil del proceso de diseño
del lenguaje. Puede que le guste leer las primeras secciones del documento y hojear las otras partes brevemente para ver lo que contienen. La revista que
contiene el artículo será fácil de encontrar en muchas bibliotecas de ciencias de la computación o puede encontrar una versión reescrita del artículo en formato
electrónico en la Web.
Equipo-Fly
Equipo-Fly
Los esfuerzos de diseño de lenguaje más exitosos comparten tres características importantes con el proyecto Lisp:
Aplicación motivadora: El lenguaje fue diseñado para que un tipo específico de programa pudiera escribirse más fácilmente.
Fundamentos teóricos: La comprensión teórica fue la base para incluir ciertas capacidades y omitir otras.
Aplicación motivadora
Un problema de programación importante para el grupo de McCarthy fue un sistema llamado Tomador de consejos. Este era un sistema de razonamiento de sentido
común basado en la lógica. Como su nombre lo indica, se suponía que el programa debía leer declaraciones escritas en un lenguaje de entrada específico, realizar
razonamientos lógicos y responder preguntas simples. Otro problema importante utilizado en el diseño de Lisp fue el cálculo simbólico. Por ejemplo, el grupo de McCarthy
quería poder escribir un programa que pudiera encontrar una expresión simbólica para la integral indefinida (como en el cálculo) para una función, dada una descripción
La mayoría de los buenos diseños de lenguaje parten de alguna necesidad específica. A modo de comparación, aquí hay algunos problemas motivadores que influyeron en el
Simula Simulación
Un propósito específico proporciona un enfoque para los diseñadores de lenguajes. Nos ayuda a establecer criterios para tomar decisiones de diseño. Una aplicación específica y
motivadora también nos ayuda a resolver uno de los problemas más difíciles en el diseño de lenguajes de programación: decidir qué características dejar de lado.
Un diseño de lenguaje debe ser específico sobre cómo se realizan todas las operaciones básicas. El diseño del lenguaje puede ser muy concreto, prescribiendo exactamente cómo deben implementarse las
partes del lenguaje, o más abstracto, especificando solo ciertas propiedades que deben satisfacerse en cualquier implementación. Es posible equivocarse en cualquier dirección. Un lenguaje que está
demasiado ligado a una máquina dará lugar a programas que no son portátiles. Cuando la nueva tecnología conduce a arquitecturas de máquina más rápidas, los programas escritos en el lenguaje pueden
volverse obsoletos. En el otro extremo, es posible ser demasiado abstracto. Si un diseño de lenguaje especifica solo cuál debe ser el valor final de una expresión, sin ninguna información sobre cómo se
evaluará, puede ser difícil para los programadores escribir código eficiente. La mayoría de los programadores consideran importante tener una buena comprensión de cómo se ejecutarán los programas, con
suficiente detalle para predecir el tiempo de ejecución del programa. Lisp fue diseñado para una máquina específica, la IBM 704. Sin embargo, si los diseñadores hubieran construido el lenguaje alrededor de
muchas características especiales de una computadora en particular, el lenguaje no habría sobrevivido tan bien como lo ha hecho. En cambio, por suerte o por diseño, identificaron un conjunto útil de
conceptos simples que se asignan fácilmente a la arquitectura IBM 704 y también a otras computadoras. El modelo de ejecución Lisp se discute con más detalle en el idioma no habría sobrevivido tan bien
como lo ha hecho. En cambio, por suerte o por diseño, identificaron un conjunto útil de conceptos simples que se asignan fácilmente a la arquitectura IBM 704 y también a otras computadoras. El modelo de
ejecución Lisp se discute con más detalle en el idioma no habría sobrevivido tan bien como lo ha hecho. En cambio, por suerte o por diseño, identificaron un conjunto útil de conceptos simples que se asignan
fácilmente a la arquitectura IBM 704 y también a otras computadoras. El modelo de ejecución Lisp se discute con más detalle en Subsección
3.4.3 .
Un modelo de máquina sistemático y predecible es fundamental para el éxito de un lenguaje de programación. A modo de comparación, aquí hay algunos modelos de
Fortran Máquina de registro plano Sin pilas, sin recursividad Memoria organizada como matriz lineal
Fundamentos teóricos
McCarthy describió Lisp como un "esquema para representar el funciones recursivas parciales de una cierta clase de expresiones simbólicas ". Discutimos la
computabilidad y las funciones recursivas parciales en Capítulo 2 . Estos son los puntos principales sobre la teoría de la computabilidad que son relevantes para el diseño de
Lisp:
Lisp fue diseñado para ser Turing completo, lo que significa que todas las funciones recursivas parciales pueden escribirse en Lisp. La
frase "Turing completo" se refiere a una caracterización de la computabilidad propuesta por el matemático AM Turing; ver Capítulo 2
El uso de expresiones de función y recursividad en Lisp se aprovecha directamente de una caracterización matemática de
Hoy en día es poco probable que un equipo de diseñadores de lenguajes de programación anuncie que su lenguaje es suficiente para definir todas las funciones recursivas
parciales. La mayoría de los científicos de la computación conocen hoy en día la teoría de la computabilidad y asumen que la mayoría de los lenguajes destinados a la
programación general son completos de Turing. Sin embargo, la teoría de la computabilidad y otros marcos teóricos como la teoría de tipos continúan teniendo importantes
La conexión entre Lisp y el cálculo lambda es importante, y el cálculo lambda sigue siendo una herramienta importante en el estudio de los lenguajes de
Equipo-Fly
Equipo-Fly
El tema de este capítulo es un lenguaje que podría llamarse Lisp histórico. Esto es esencialmente Lisp 1.5, de principios de la década de 1960, con uno o dos
cambios menores. Debido a que hay varias versiones diferentes de Lisp de uso común, es probable que algunos nombres de funciones usados en este libro
Un libro atractivo que captura parte del espíritu del Lisp contemporáneo es el libro en rústica basado en Scheme de DP Friedman y M. Felleisen, titulado El
pequeño intrigante ( MIT Press, Cambridge, MA, 1995). Esto es similar a un libro anterior de los mismos autores titulado El pequeño LISPer. La sintaxis de Lisp
4.1 ) fácil, todas las operaciones se escriben en forma de prefijo, con el operador delante de todos los operandos. Aquí hay algunos ejemplos de expresiones
(+12345) (1 + 2 + 3 + 4 + 5) ((2 +
(fxy)
Átomos
Los programas Lisp calculan con átomos y células. Los átomos incluyen números enteros, números de punto flotante y átomos simbólicos. Los átomos simbólicos pueden
tener más de una letra. Por ejemplo, el pato atómico se imprime con cuatro letras, pero es atómico
en el sentido de que no existe una operación Lisp para separar el átomo en cuatro átomos separados.
En nuestra discusión de Lisp histórico, usamos solo números enteros y átomos simbólicos. Los átomos simbólicos se escriben con una secuencia de caracteres y
dígitos, comenzando con un carácter. Los átomos, símbolos y números vienen dados por la siguiente gramática de la forma normal de Backus (BNF) (ver Sección
| <número> <dígito>
Expresiones S y listas
Las estructuras de datos básicas de Lisp son pares de puntos, que son pares escritos con un punto entre las dos partes del par. Poniendo átomos o pares juntos,
podemos escribir expresiones simbólicas en una forma tradicionalmente llamada S-expresiones. La sintaxis de las expresiones S de Lisp viene dada por la siguiente
gramática:
<sexp> :: = <atom> | (<sexp>. <sexp>)
Aunque las expresiones S son los datos básicos de Historical Lisp, la mayoría de los programas Lisp realmente usan listas. Las listas Lisp se construyen a partir de pares de una manera
También usamos funciones numéricas como +, -, y *, escribiéndolos en la notación de prefijo Lisp habitual. La función cons se utiliza para combinar dos
átomos o listas, y car y cdr separan las listas. La función eq es una prueba de igualdad y el átomo prueba si su argumento es un átomo. Estos se discuten
con más detalle en Subsección 3.4.3 en relación con la representación de la máquina de listas y pares.
Las funciones de programación generales incluyen cond para una prueba condicional (si ... entonces ... más ...), lambda para definir funciones, definir para
declaraciones, cotizar para retrasar o evitar la evaluación y eval para forzar la evaluación de una expresión.
Las funciones cond, lambda, define y quote se denominan técnicamente formas especiales ya que una expresión que comienza con una de estas funciones
especiales se evalúa sin evaluar todas las partes de la expresión. Más sobre esto a continuación.
El lenguaje resumido hasta este punto se llama Lisp puro. Una característica de Lisp puro es que las expresiones no tienen
efectos secundarios. Esto significa que evaluar una expresión solo produce el valor de esa expresión; no cambia el estado observable de la máquina.
Algunas funciones básicas que hacer tienen efectos secundarios son
Evaluación de Expresiones
La estructura básica del intérprete o compilador Lisp es la leer-evaluar-imprimir lazo. Esto significa que la acción básica del intérprete es leer una
expresión, evaluarla e imprimir el valor. Si la expresión define el significado de algún símbolo, entonces la asociación entre el símbolo y su valor se guarda
para que el símbolo se pueda usar en expresiones que se escriban más adelante.
evaluando cada uno de los argumentos a su vez, luego pasando la lista de valores de argumentos a la función. Las excepciones a esta regla se denominan
procediendo de izquierda a derecha, encontrando el primer pi con un valor diferente de nil. Esto implica evaluar p1… Pn y un ei si pi no es nil. Volvemos a
esto a continuación.
Lisp usa los átomos T y nil para verdadero y falso, respectivamente. En este libro, verdadero y falso a menudo se escriben en código Lisp, ya que son más
intuitivos y más comprensibles si no ha realizado mucha programación Lisp. Puede leer ejemplos de Lisp que contienen verdadero y falso como si
aparecieran dentro de un programa para el que ya hemos definido verdadero y falso como sinónimos de T y nil, respectivamente.
Un punto un poco complicado es que el evaluador Lisp necesita distinguir entre una cadena que se usa para nombrar un átomo y una cadena que se usa para otra
cosa, como el nombre de una función. La forma de cotización se utiliza para escribir átomos y listas directamente:
(contras ab) expresión cuyo valor es el par que contiene los valores de a y b
(cons (cita A) (cita B)) expresión cuyo valor es el par que contiene los átomos "A" y "B"
En la mayoría de los dialectos de Lisp, es común escribir 'bozo en lugar de (citar bozo). Puede ver en la breve descripción anterior que la cotización debe ser una
forma especial. Aquí hay algunos ejemplos adicionales de expresiones Lisp y sus valores:
(+ (+ 1 2) (+ 4 5)) (entre primero evalúe 1 + 2, luego 4 + 5, luego 3 + 9 para obtener el valor 12 se evalúa como
Ejemplo. Aquí hay un ejemplo de programa Lisp un poco más largo, la definición de una función que busca en una lista. La función de búsqueda toma dos
argumentos, xey, y busca en la lista y una aparición de x. La declaración comienza con define, lo que indica que se trata de una declaración. Luego sigue el
)))
Las expresiones de la función Lisp comienzan con lambda. La función tiene dos argumentos, xey, que aparecen en una lista inmediatamente después de lambda. El
valor de retorno de la función viene dado por la expresión que sigue a los parámetros. El cuerpo de la función es una expresión condicional, que devuelve nil, la lista vacía,
si y es la lista vacía. De lo contrario, si x es el primer elemento (automóvil) de la lista y, la función devuelve el elemento x. De lo contrario, la función realiza una llamada
recursiva para ver si x está en el cdr de la lista y. El cdr de una lista es la lista de todos los elementos que ocurren después del primer elemento. Podemos usar esta
función para encontrar 'manzana en la lista' (pera, melocotón, manzana, higo, plátano) escribiendo la expresión Lisp
Históricamente, Lisp era un lenguaje de ámbito dinámico. Esto significa que una variable dentro de una expresión podría referirse a un valor diferente si se pasa a una
función que declara esta variable de manera diferente. Cuando Scheme se introdujo en 1978, era una variante de Lisp con alcance estático. Como se discutió en Capítulo 7 ,
el alcance estático es común en la mayoría de los lenguajes de programación modernos. Tras la amplia aceptación de Scheme, la mayoría de los Lisps modernos se han
convertido en
con alcance estático. La diferencia entre alcance estático y dinámico no se trata en este capítulo.
Lisp y Scheme
Si quiere intentar escribir programas Lisp usando un compilador de Scheme, querrá saber que los nombres de algunas funciones y formas especiales difieren en
contras contras t #t
Equipo-Fly
Equipo-Fly
Así como prácticamente todos los lenguajes naturales tienen ciertas partes básicas del habla, como sustantivos, verbos, y adjetivos, hay partes del lenguaje de
programación que ocurren en la mayoría de los lenguajes. Las partes gramaticales más básicas del lenguaje de programación son expresiones, declaraciones, y declaraciones.
Expresión: una entidad sintáctica que puede evaluarse para determinar su valor. En algunos casos, la evaluación puede no terminar, en cuyo caso la
expresión no tiene valor. La evaluación de algunas expresiones puede cambiar el estado de la máquina, provocando un efecto secundario además de
Declaración: un comando que altera el estado de la máquina de alguna manera explícita. Por ejemplo, la instrucción de lenguaje de máquina load 4094 r1
altera el estado de la máquina al colocar el contenido de la ubicación 4094 en el registro r1. La declaración del lenguaje de programación x: = y + 3 altera el
estado de la máquina sumando 3 al valor de la variable y y almacenando el resultado en la ubicación asociada con la variable x.
Declaración: una entidad sintáctica que introduce un nuevo identificador, a menudo especificando uno o más atributos. Por ejemplo, una declaración puede introducir
una variable iy especificar que se pretende que tenga solo valores enteros.
Los errores y la terminación pueden depender del orden en que se evalúen las partes de las expresiones. Por ejemplo, considere la expresión
donde f es una función que se detiene en argumentos pares pero se ejecuta para siempre en argumentos impares. En muchos lenguajes de programación, una
expresión booleana A o B se evaluaría de izquierda a derecha, y B se evaluaría solo si A es falso. En este caso, el valor de la expresión anterior sería 4. Sin embargo,
si evaluamos la prueba A o B de derecha a izquierda o evaluamos tanto A como B independientemente del valor de A, entonces el valor de la expresión no está
definido.
Los lenguajes de máquina tradicionales y los lenguajes ensambladores se basan en declaraciones. Lisp es un lenguaje basado en expresiones, lo que significa que las
construcciones básicas del lenguaje son expresiones, no declaraciones. De hecho, Lisp puro no tiene declaraciones ni expresiones con efectos secundarios. Aunque
se sabía por la teoría de la computabilidad que era posible definir todas las funciones computables sin utilizar declaraciones o efectos secundarios, Lisp fue el primer
Fortran y los lenguajes ensambladores utilizados antes de Lisp tenían declaraciones condicionales. Una declaración típica podría tener la forma
si ( condición) ir al 112
Si la condición es verdadera cuando se ejecuta este comando, entonces el programa salta a la declaración con la etiqueta 112. Sin embargo, las expresiones
condicionales que producen un valor en lugar de causar un salto eran nuevas en Lisp. También aparecieron en Algol 60, pero esto parece haber sido el resultado
si p1 entonces e1
de lo contrario, si p2 entonces e2
...
de lo contrario si pn entonces en
en una notación similar a Algol, excepto que la mayoría de los lenguajes de programación no tienen una forma directa de especificar la ausencia de un valor. En resumen,
el valor de (cond (p1 e1)… (pn en)) es el primer ei, procediendo de izquierda a derecha, con pi nonnil y pj nil (representando falso) para todo j <i. Si no existe tal ei,
las expresiones p1… pn tienen efectos secundarios, estos se producirán de izquierda a derecha a medida que se evalúe la expresión condicional.
La expresión condicional Lisp ahora se llamaría un expresión condicional secuencial. La razón por la que se llama secuencial es que las partes de esta
expresión se evalúan en secuencia de izquierda a derecha, y la evaluación termina tan pronto como se puede determinar un valor para la expresión.
Vale la pena señalar que (cond (p1 e1)… (pn en)) no está definido si
(cond (diverge 1) (verdadero 0)) no está definido, si diverge no termina (cond (verdadero 0)
Rigor. Una parte importante de la expresión cond Lisp es que una expresión condicional puede tener un valor incluso si una o más subexpresiones no lo tienen. Por
ejemplo, (cond (true e1) (false e2)) se puede definir incluso si e2 no está definido. Por el contrario, e1 + e2 no está definido si e1 o e2 no está definido. En la
terminología del lenguaje de programación estándar, una forma de operador o expresión es estricto si se evalúan todos los operandos o subexpresiones. Lisp cond no
es estricto, pero la adición sí lo es. (Algunos operadores de C que no son estrictos son && y ||).
La frase maquina abstracta se utiliza generalmente para referirse a un dispositivo informático idealizado que puede ejecutar directamente un lenguaje de
programación específico. Normalmente, una máquina abstracta puede no ser completamente implementable: una máquina abstracta puede proporcionar infinitas
ubicaciones de memoria o aritmética de precisión infinita. Sin embargo, como usamos la frase en este libro, una máquina abstracta debe ser lo suficientemente realista
para proporcionar información útil sobre la ejecución real de programas reales en hardware real. Nuestro objetivo al discutir las máquinas abstractas es identificar el
modelo mental de la computadora que usa un programador para escribir y depurar programas. Por esta razón, existe una tendencia a referirse a la máquina abstracta
A continuación, que es una función que representa el programa restante para evaluar cuando se hace con la expresión actual.
Un lista de asociaciones, comúnmente llamado el Una lista en gran parte de la literatura sobre Lisp y llamado el pila de tiempo de ejecución en
la literatura sobre lenguajes basados en Algol. El propósito de la lista A es almacenar los valores de las variables que pueden ocurrir en la
A montón, que es un conjunto de celdas contras (pares almacenados en la memoria) que pueden ser señalados por punteros en la lista A.
La estructura de esta máquina no se investiga en detalle. La idea principal es que cuando se evalúa una expresión Lisp se pueden crear algunos enlaces
entre identificadores y valores. Estos se almacenan en la lista A. Algunos de estos valores pueden involucrar celdas de cons que se colocan en el montón.
Cuando se completa la evaluación de una expresión, el valor de esa expresión se pasa a la continuación, que representa el trabajo que debe realizar el
Esta máquina abstracta es similar a una máquina de registro estándar con una pila, si pensamos que la expresión actual representa el contador del
programa y la continuación representa el resto del programa.
Hay cuatro funciones principales de igualdad en Lisp: eq, eql, equal y =. La función eq prueba si sus argumentos están representados por la misma secuencia de
ubicaciones de memoria, y = es igualdad numérica. La función eql prueba si sus argumentos son el mismo símbolo o número, e igual es una prueba de igualdad
recursiva en listas o átomos que se implementa mediante el uso de eq y =. Para simplificar, generalmente usamos igual en el código de muestra.
Contras de células
Las celdas de contras (o pares de puntos) son la estructura de datos básica de la máquina abstracta Lisp. Las células de los contras tienen dos partes, históricamente llamadas dirección
parte y la decremento parte. Las palabras dirección y decremento provienen de la computadora IBM 704 y casi nunca se usan en la actualidad. Sólo las letras ayd
permanecen en el carro de acrónimos (para "contenido del registro de direcciones") y cdr (para "contenido del registro de decremento").
Dibujamos contras las celdas de la siguiente manera:
Podemos representar un átomo con una celda de contras poniendo una "etiqueta" que indica qué tipo de átomo es en la parte de dirección y el valor real del
átomo en la parte de disminución. Por ejemplo, la letra "a" podría representarse como un átoma de Lisp como
donde atm indica que la celda representa un átomo y a indica que el átomo es la letra a.
Dado que colocar un puntero en una o ambas partes de una celda de contras representa listas, el patrón de bits utilizado para indicar un átomo debe ser diferente de cada
valor de puntero. Hay cinco funciones básicas en las celdas de contras, que se evalúan de la siguiente manera:
atom, una función con un argumento: si un valor es un átomo, entonces la palabra que almacena el valor tiene un patrón de bits
especial en su parte de dirección que marca el valor como un átomo. La función atom devuelve verdadero si este patrón indica que el
argumento de la función es un átomo. (En Scheme, la función átomo se escribe átomo ?, lo que nos recuerda que el valor de la función
eq, una función con dos argumentos: compara la igualdad de dos argumentos comprobando si están almacenados en la misma
ubicación. Esto es significativo tanto para los átomos como para las celdas contras porque conceptualmente el compilador Lisp se
comporta como si cada átomo (incluido cada número) se almacenara una vez en una ubicación única.
cons, una función con dos argumentos: La expresión (cons xy) se evalúa de la siguiente manera:
4. Regrese un puntero a c.
car, una función con un argumento: si el argumento es una celda de cons, entonces devuelve el C ontenciones de la
cdr, una función con un argumento: si el argumento es una celda de cons, entonces devuelve el C ontenciones de la
Al dibujar celdas de contras, dibujamos el contenido de una celda como un puntero a otra celda o como un átomo. Para nuestros propósitos, no es importante cómo se
representa un átomo dentro de una celda de cons. (Podría representarse como un patrón de bits específico dentro de una celda o como un puntero a una ubicación que
Ejemplo 3.1
Evaluamos la expresión (contras 'A' B) creando una nueva celda de contras y luego configurando el auto de esta celda en el átomo 'A y el cdr de la celda en' B. La
expresión '(AB) tendría el mismo efecto, ya que esta es la sintaxis de un par de puntos punteados
átomo A y átomo B. Aunque esta notación de pares de puntos era una parte común de los primeros Lisp, Scheme y los Lisps posteriores enfatizan las listas sobre los pares.
Ejemplo 3.2
Cuando se evalúa la expresión (cons (cons 'A' B) (cons 'A' B)), se crea una nueva estructura de la siguiente forma:
La razón por la que hay dos celdas de contras con las mismas partes es que cada evaluación de (contras 'A' B) crea una nueva celda. Esta estructura se imprime
Ejemplo 3.3
También es posible escribir una expresión Lisp que cree la siguiente estructura, que también se imprime ((A. B). (A. B)):
Una expresión cuya evaluación produce esta estructura es ((lambda (x) (cons xx)) (cons 'A' B)). Procedemos con la evaluación de esta expresión evaluando
primero el argumento de la función (cons 'A' B) para producir la celda cons dibujada aquí, luego pasando la celda a la función (lambda (x) (cons xx)) que crea
la celda superior celda con dos punteros a la celda (A. B). Las expresiones lambda Lisp se describen en este capítulo en Subsección 3.4.5 .
Debido a que una celda de contras puede contener dos punteros, las celdas de contras pueden usarse para construir árboles. Debido a que los programas Lisp a menudo usan listas, existen
convenciones para representar listas como una cierta forma de árboles. Específicamente, la lista a1, a2, ... an es
representado por una celda contras cuyo coche es a1 y cuyo cdr apunta a las celdas que representan la lista a2,… an. Para la lista vacía, usamos un puntero establecido en NIL.
Por ejemplo, aquí está la representación de la lista (ABC), también escrita como (A. (B. (C.NIL))):
En esta ilustración, los átomos se muestran como celdas de cons, con un patrón de bits que indica un átomo en la primera parte de la celda y el valor real del átomo en la segunda. Para
simplificar, a menudo dibujamos listas colocando un átomo dentro de la mitad de las celdas de una cons. Por ejemplo, podríamos escribir A en la parte de la dirección de la celda superior
izquierda en lugar de dibujar un puntero a la celda que representa el átomo A, y de manera similar para las celdas de lista que apuntan a los átomos B y C. En el resto del libro, usamos
la forma más simple de dibujar; la ilustración aquí es solo para recordarnos que los átomos, así como las listas, deben estar representados dentro de la máquina de alguna manera.
Los datos Lisp y los programas Lisp tienen la misma sintaxis y representación interna. Esto facilita la manipulación de programas Lisp como datos. Una
característica que distingue a Lisp de muchos otros lenguajes es que es posible que un programa construya una estructura de datos que represente una
expresión y luego evalúe la expresión como si estuviera escrita como parte del programa. Esto se hace con la función eval.
Ejemplo. Podemos escribir una función Lisp para sustituir una expresión X para todas las apariciones de y en expresión z y luego evalúe la expresión
resultante. Para aclarar la estructura lógica de la función sustituto-y-eval, definimos sustituto primero y luego lo usamos en sustituto-y-eval. La función
sustituta tiene tres argumentos, exp1, var y exp2. El resultado de (sustituto exp1 var exp2) es la expresión obtenida de exp2 cuando todas las
apariciones de var se reemplazan por exp1:
(cond ((atom exp2) (cond ((eq exp2 var) exp1) (true exp2))) (true (cons (sustituto
La capacidad de usar operaciones de lista para construir programas en tiempo de ejecución y luego ejecutarlos es una característica distintiva de Lisp. Puede
apreciar el valor de esta característica si piensa en cómo escribiría un programa en C para leer algunas expresiones en C y ejecutarlas. Para hacer esto,
tendría que escribir algún tipo de intérprete o compilador de C. En Lisp, puede leer una expresión y aplicarle la función incorporada eval.
El cálculo Lisp se basa en funciones y llamadas recursivas en lugar de asignaciones y bucles iterativos. Esto era radicalmente diferente de otros idiomas
en 1960, y es fundamentalmente diferente de muchos idiomas de uso común ahora.
Otro ejemplo es esta función que, con una función primitiva utilizada para la suma, suma sus dos argumentos:
La idea de escribir expresiones para funciones se remonta al cálculo lambda, que fue desarrollado por Alonzo Church y otros, a partir de la década de
1930. Un hecho sobre el cálculo lambda que se conocía en la década de 1930 era que todas las funciones computables por una máquina de Turing
En el cálculo lambda, las expresiones de función se escriben con la letra minúscula griega lambda ( λ), en lugar de con la palabra lambda, como en Lisp. El
cálculo de lambda también utiliza diferentes convenciones sobre el paréntesis. Por ejemplo, la función que cuadra su argumento y agrega el resultado a y está
escrito como
λ x. (x2 + y)
en Lisp. En esta función, x se llama parámetro formal; esto significa que x es un marcador de posición en la definición de función que se referirá al
3.4.6 Recurrencia
Las funciones recursivas eran nuevas cuando apareció Lisp. McCarthy, además de incluirlos en Lisp, fue el principal defensor de permitir funciones recursivas
en Algol 60. Fortran, en comparación, no permitía que una función se llamara a sí misma.
Los miembros del comité Algol 60 escribieron más tarde que no tenían idea de lo que les esperaba cuando acordaron incluir funciones recursivas en Algol 60. (Es posible que
hayan estado descontentos durante algunos años, pero probablemente estarían de acuerdo ahora en que era un problema. decisión visionaria.)
Lisp lambda permite escribir funciones anónimas, que son funciones que no tienen un nombre declarado. Sin embargo, es difícil escribir funciones
recursivas con esta notación. Por ejemplo, suponga que queremos escribir una expresión para la función f tal que
(verdadero (+ x (f (- x 1)))))
Sin embargo, esto no tiene ningún sentido porque la f dentro de la expresión de la función no está definida en ninguna parte. La solución de McCarthy en 1960
define la f recursiva sugerida anteriormente. En Lisps posteriores, se convirtió en un estilo aceptado solo para declarar una función mediante el uso del formulario de declaración de
Otra notación en algunas versiones de Lisps se define como "definir función", lo que elimina la necesidad de lambda:
El artículo de 1960 de McCarthy comenta que la notación lambda es inadecuada para funciones recursivas de expresión. Esta afirmación es falsa. El cálculo de
Lambda, y por lo tanto Lisp, es capaz de expresar funciones recursivas sin ningún operador adicional como label. Esto era conocido por los expertos en cálculo
lambda en la década de 1930, pero aparentemente McCarthy y su grupo no lo conocían en la década de 1950. (Ver Subsección 4.2.3 para obtener una
La frase función de orden superior significa una función que toma una función como argumento o devuelve una función como resultado (o ambos). Este uso de
orden superior proviene de una convención que llama a una función cuyos argumentos y resultados no son funciones función de primer orden. Una función que toma
función de segundo orden, las funciones en funciones de segundo orden se llaman funciones de tercer orden, etcétera.
Ejemplo 3.4
Si F y gramo son funciones matemáticas, digamos funciones en los números enteros, entonces su composición f? gramo es la función tal que para cada entero X, tenemos
( f? g) (x) = f (g (x)).
Podemos escribir la composición como una función Lisp compose que toma dos funciones como argumentos y devuelve su composición:
La primera lambda se usa para identificar los argumentos a componer. La segunda lambda se usa para definir el valor de retorno de la función, que es
una función. Puede que le guste calcular el valor de la expresión
(componer (lambda (x) (+ xx)) (lambda (x) (* xx)))
Ejemplo 3.5
Una lista de mapas es una función que toma una función y una lista y aplica la función a cada elemento de la lista. El resultado es una lista que contiene todos los resultados de la
aplicación de la función. Usando define para definir una función recursiva, podemos escribir la lista de mapas de la siguiente manera:
(cond ((eq x nil) nil) (verdadero (contras (f (coche x)) (lista de mapas f (cdr x)))))))
No podemos decir si la lista de mapas es una función de segundo o tercer orden, ya que los elementos de la lista pueden ser átomos, funciones o
funciones de orden superior. Como ejemplo del uso de esta función, tenemos
Las funciones de orden superior requieren más soporte en tiempo de ejecución que las funciones de primer orden, como se analiza con cierto detalle en Capítulo 7 .
En informática, basura se refiere a ubicaciones de memoria que no son accesibles para un programa. Más específicamente, definimos basura de la siguiente manera:
En un momento dado de la ejecución de un programa PAG, una ubicación de memoria metro es basura si no se completó la ejecución de PAG desde este punto se
o hacer que esta ubicación sea accesible para PAG no puede afectar la ejecución posterior del programa.
Tenga en cuenta que esta definición no proporciona un algoritmo para encontrar basura. Sin embargo, si pudiéramos encontrar todas las ubicaciones que son basura
(según esta definición), en algún momento de la ejecución suspendida de un programa, sería seguro desasignar estas ubicaciones o usarlas para algún otro propósito.
En Lisp, las ubicaciones de memoria que son accesibles para un programa son celdas de cons. Por lo tanto, la basura asociada con un programa Lisp en ejecución será
un conjunto de celdas de cons que no son necesarias para completar la ejecución del programa. La recolección de basura es el proceso de detectar basura durante la
ejecución de un programa y ponerla a disposición para otros usos. En los lenguajes de recolección de basura, el sistema de tiempo de ejecución recibe solicitudes de
memoria (como cuando se crean las celdas de Lisp contra) y asigna memoria de alguna lista de espacio disponible. La lista de ubicaciones de memoria disponibles es
llamado la lista libre. Cuando el sistema de tiempo de ejecución detecta que el espacio disponible está por debajo de algún umbral, el programa puede suspenderse y se
invoca el recolector de basura. En Lisp y otros lenguajes de recolección de basura, generalmente no es necesario que el programa invoque al recolector de basura
explícitamente. (En algunas implementaciones modernas, el recolector de basura puede ejecutarse en paralelo con el programa. Sin embargo, debido a que la recolección de
basura concurrente genera algunas consideraciones adicionales, asumiremos que el programa está suspendido cuando el recolector de basura se está ejecutando).
La idea y la implementación de la recolección automática de basura parecen haberse originado con Lisp. Aquí tienes un ejemplo de
se evalúa, las celdas de contras creadas mediante la evaluación de e2 normalmente serán basura. Sin embargo, no siempre es correcto desasignar las ubicaciones
utilizadas en una lista después de aplicar car a la lista. Por ejemplo, considere la expresión
Cuando se evalúa esta expresión, la función car se aplicará a una celda de contras cuyas partes "a" y "d" apuntan a la misma lista.
Se han desarrollado muchos algoritmos para la recolección de basura a lo largo de los años. Aquí hay un ejemplo simple llamado marcar y barrer. El nombre
proviene del hecho de que el algoritmo primero marca todas las ubicaciones accesibles desde el programa, luego "barre" todas las ubicaciones no marcadas
como basura. Este algoritmo asume que podemos decir qué secuencias de bits en la memoria son punteros y cuáles son átomos, y también asume que hay
un bit de etiqueta en cada ubicación que se puede cambiar a 0 o 1 sin destruir los datos en esa ubicación.
2. Comience desde cada ubicación utilizada directamente en el programa. Siga todos los enlaces, cambiando el bit de etiqueta de cada celda visitada a 1.
3. Coloque todas las celdas con etiquetas aún iguales a 0 en la lista libre.
La recolección de basura es un muy característica útil, al menos en lo que respecta a la conveniencia del programador. Sin embargo, existe cierto debate
sobre la eficiencia de los lenguajes de recolección de basura. Algunos investigadores tienen evidencia experimental que muestra que la recolección de
basura agrega una sobrecarga del orden del 5% al tiempo de ejecución del programa. Sin embargo, este tipo de medición depende en gran medida del
diseño del programa. Algunos programas simples podrían escribirse en C sin el uso de memoria asignada por el usuario, pero cuando se traducen a Lisp
podrían crear muchas celdas de contras durante la evaluación de la expresión y, por lo tanto, involucrar una gran cantidad de recolección de basura. Por
otro lado, la gestión de memoria explícita en C y C ++ (en lugar de la recolección de basura) puede ser engorrosa y propensa a errores, por lo que para
ciertos programas es muy ventajoso tener una recolección de basura automática.
Ejemplo. En Lisp, podemos escribir una función que tome una lista lst y una entrada x, devolviendo la parte de la lista que sigue a x, si la hay. Esta función, que llamamos
)))
Aquí hay dos programas C análogos que tienen diferentes efectos en la lista que se les pasa. El primero deja la lista sola, devolviendo un puntero al cdr de
la primera celda que tiene su carro igual ax:
celular {
ptr;
ptr-> cdr;
};
};
Un segundo programa en C podría ser más apropiado si solo se usara la parte de la lista que sigue a x en el resto del programa. En este caso, tiene sentido
liberar las celdas que ya no se utilizarán. Aquí hay una función de C que hace exactamente esto:
ptr, * anterior;
anterior = ptr;
gratis (anterior);
}
Una ventaja de la recolección de basura Lisp es que el programador no tiene que decidir cuál de estas dos funciones llamar. En Lisp, es posible
simplemente devolver un puntero a la parte de la lista que desea y dejar que el recolector de basura averigüe si es posible que necesite el resto de la lista
alguna vez. En C, por otro lado, el programador debe decidir, mientras recorre la lista, si es la última vez que el programa hará referencia a estas celdas.
Pregunta para reflexionar. Es interesante observar que los lenguajes de programación como Lisp, en los que la mayoría de los cálculos se expresan mediante funciones
recursivas y estructuras de datos vinculadas, proporcionan recolección automática de basura. En contraste, los lenguajes imperativos simples como C requieren que el
programador libere ubicaciones que ya no son necesarias. ¿Es solo una coincidencia que los lenguajes orientados a funciones tengan recolección de basura y los
lenguajes orientados a asignaciones no? ¿O hay algo intrínseco a la programación orientada a funciones que hace que la recolección de basura sea más apropiada para
estos lenguajes? Parte de la respuesta se encuentra en el ejemplo anterior. Otra parte de la respuesta parece estar en los problemas asociados con la gestión del
Las expresiones Pure Lisp no tienen efectos secundarios, que son cambios visibles en el estado de la máquina como resultado de evaluar una expresión. Sin embargo,
por eficiencia, incluso los primeros Lisp tenían expresiones con efectos secundarios. Dos funciones históricas con efectos secundarios son rplaca y rplacd:
(rplaca xy): reemplace el campo de dirección de la celda de contras x con y, (rplacd xy): reemplace
En ambos casos, el valor de la expresión es la celda que se ha modificado. Por ejemplo, el valor de
es la celda de contras con el automóvil 'C y cdr' B producida cuando se asigna una nueva celda de contras en la evaluación de (contras 'A' B) y luego el automóvil 'A se reemplaza
por' C.
Con estas construcciones, dos apariciones de la misma expresión pueden tener valores diferentes. (Esto es realmente lo que significa el efecto secundario). Por
(lambda (x) (contras (auto x) (contras (rplaca xc) (auto x)))) (contras ab)
La expresión (car x) aparece dos veces dentro de la expresión de la función, pero habrá dos valores diferentes en los dos lugares donde se evalúa
esta expresión. Cuando se utilizan rplaca y rplacd, es posible crear estructuras de listas circulares, algo que no es posible en Lisp puro.
Una situación en la que rplaca y rplacd pueden aumentar la eficiencia es cuando un programa modifica una celda en medio de una lista. Por ejemplo, considere la
pero podemos definir una nueva lista con los elementos A, B, y, D. La nueva lista se puede definir con la siguiente expresión, donde cadr x significa "coche de el cdr de x "y
Tenga en cuenta que la evaluación de esta expresión implicará la creación de nuevas celdas de contras para los primeros tres elementos de la lista y, si no hay más uso para
ellos, eventualmente la recolección de basura de las celdas antiguas utilizadas para los primeros tres elementos de x. En contraste, en Lisp impuro podemos cambiar la tercera
(rplaca (cddr x) y)
Si todo lo que necesitamos es la lista que obtuvimos al reemplazar el tercer elemento de x con y, entonces esta expresión obtiene el resultado que queremos de manera mucho
más eficiente. En particular, no es necesario asignar nueva memoria o memoria libre utilizada para la lista original.
Aunque este ejemplo puede sugerir que los efectos secundarios conducen a la eficiencia, el panorama general es más complicado. En general, es difícil comparar
la eficiencia de diferentes lenguajes si el mismo problema se resuelve mejor de maneras muy diferentes. Por ejemplo, si escribimos un programa usando Lisp puro,
podríamos estar inclinados a usar algoritmos diferentes a los de Lisp impuro. Una vez que comenzamos a comparar la eficiencia de diferentes soluciones para el
mismo problema, también debemos tener en cuenta la cantidad de esfuerzo que un programador debe dedicar a escribir el programa, la facilidad de depuración,
etc. Estas son propiedades complejas de los lenguajes de programación que son difíciles de cuantificar.
Equipo-Fly
Equipo-Fly
Lisp es un elegante lenguaje de programación diseñado en torno a algunas ideas simples. El lenguaje estaba destinado a la computación simbólica, a diferencia del tipo de
computación numérica que era dominante en la mayoría de la programación fuera de la investigación de inteligencia artificial en 1960. Esta orientación innovadora se puede
ver en la estructura básica de datos, listas y en las estructuras de control básicas. , recursividad y condicionales. Las listas se pueden utilizar para almacenar secuencias de
símbolos o representar árboles u otras estructuras. La recursividad es una forma natural de proceder a través de listas que pueden contener datos atómicos u otras listas.
Tres aspectos importantes del diseño del lenguaje de programación contribuyeron al éxito de Lisp: una aplicación de motivación específica, un modelo de
ejecución de programa inequívoco y atención a las consideraciones teóricas. Entre las principales consideraciones teóricas, Lisp se diseñó teniendo en
cuenta la clase matemática de funciones recursivas parciales. La sintaxis Lisp para expresiones de función se basa en el cálculo lambda.
Las siguientes contribuciones son algunas que son importantes para el campo de los lenguajes de programación:
Funciones recurrentes. La programación Lisp se basa en funciones de contras y recursividad en lugar de asignación y bucles while. Lisp
introduce funciones recursivas y admite funciones con argumentos de función y funciones que devuelven funciones como resultados.
Liza. La estructura de datos básica en Lisp temprano era la celda de contras. El uso principal de las celdas contras en las formas modernas de Lisp es
para construir listas, y las listas se usan para todo. La estructura de datos de la lista es extremadamente útil. Además, la presentación Lisp de la
memoria como un suministro ilimitado de celdas de contras proporciona una máquina abstracta más útil para la programación no numérica que las
matrices, que eran estructuras de datos primarias en otros lenguajes de los primeros días de la informática.
Programas como datos. Este sigue siendo un concepto revolucionario 40 años después de su introducción en Lisp. En Lisp, un programa puede
construir la representación de lista de una función u otras formas de expresión y luego usar la función eval para evaluar la expresión.
Recolección de basura. Lisp fue el primer lenguaje en administrar la memoria para el programador automáticamente. La recolección de basura es una
característica útil que elimina el error del programa de usar una ubicación de memoria después de liberarla.
En los años transcurridos desde 1960, Lisp ha seguido teniendo éxito en matemáticas simbólicas y programación exploratoria, como en proyectos de investigación
de IA y otras aplicaciones de computación simbólica o razonamiento lógico. También se ha utilizado mucho para la enseñanza debido a la simplicidad del idioma.
Equipo-Fly
Equipo-Fly
EJERCICIOS
una. Dibuje la estructura de la lista creada al evaluar (cons 'A (cons'B' C)).
B. Escriba una expresión Lisp pura que resulte en esta representación, sin compartir la
celda (B. C). Explica por qué tu expresión produce esta estructura.
C. Escriba una expresión Lisp pura que resulte en esta representación, compartiendo la
celda (BC). Explica por qué tu expresión produce esta estructura.
Mientras escribe sus expresiones, use solo estas construcciones Lisp: abstracción lambda, aplicación de funciones, los átomos ʹ A
ʹ B ʹ Ć, y las funciones básicas de la lista (cons, car, cdr, atom, eq). Suponga una implementación Lisp simple que no intenta hacer ninguna detección inteligente
de subexpresiones comunes u optimizaciones avanzadas de asignación de memoria.
se explica en el texto. Esta expresión no tiene valor si pag 1, …, paquete son falsas y pk + 1 no tiene valor, independientemente de los valores de pk + 2, …,
Pn.
Imagina que eres un estudiante del MIT en 1958 y tú y McCarthy están considerando interpretaciones alternativas para los condicionales en Lisp:
una. Suponga que McCarthy sugiere que el valor de (cond ( pag 1 mi 1)… ( pn en)) debe ser el valor de ek si
paquete es cierto y si, por cada yo <k, el valor de la expresión Pi es falso o indefinido. ¿Es posible implementar esta interpretación? ¿Por
B. Otro diseo para condicional puede permitir cualquiera de varios valores si ms de uno de los guardias
(p1,…, pn) es cierto. Más específicamente (y asegúrese de leer con atención), suponga que alguien sugiere el siguiente significado para
condicional:
ii. Si algun paquete son verdaderas, entonces la implementación deber devolver el valor de ej por
algunos j con pj verdadero. Sin embargo, no es necesario que sea la primera ej.
Tenga en cuenta que en (cond (ab) (cd) (ef)), por ejemplo, si a se ejecuta indefinidamente, c se evalúa como verdadero y e se detiene por error, el
valor de esta expresión debe ser el valor de d, si tiene uno. Describa brevemente una forma de implementar condicional para que las propiedades i y
ii sean verdaderas. Necesitas escribir solo dos o tres oraciones para explicar la idea principal.
C. Según la interpretación original, la función
((ecuación x 1) t)
(impar (+ x 2)))))
nos daría t para números impares y nil para números pares. Modifique esta expresión para que siempre nos dé t para números
impares y nil para números pares según la interpretación alternativa descrita en el inciso b).
D. La implementación normal de Boolean OR está diseñada para no evaluar una subexpresión a menos que sea necesario. Esto se llama cortocircuito
los paralela OR es una construcción relacionada que da una respuesta siempre que sea posible (posiblemente haciendo alguna evaluación de
Permite e2 ser indefinido si e1 es cierto y también permite e1 ser indefinido si e2 es verdad. Puedes asumir que e1 y e2 no tiene
efectos secundarios.
De la interpretación original, la interpretación del inciso a) y la interpretación del inciso b), ¿cuáles nos permitirían implementar
SCOR con mayor facilidad? ¿Qué pasa con Por? ¿Qué interpretación dificultaría o dificultaría la implementación de cortocircuitos?
La evaluación de una expresión Lisp puede terminar normalmente (y devolver un valor), terminar anormalmente con un error o ejecutarse para siempre.
Algunos ejemplos de expresiones que terminan con un error son (/ 3 0), división por 0; (coche 'a), tomando el coche de un átomo; y (+ 3 "a"), agregando una
cadena a un número. El sistema Lisp detecta estos errores, finaliza la evaluación e imprime un mensaje en la pantalla. Su jefe quiere manejar los errores en
los programas Lisp sin terminar el cálculo, pero no sabe cómo, así que su jefe le pide que ...
una. … Implementar una construcción Lisp (¿error? E) que detecta si una expresión E provocará un error. Más específicamente, su
jefe quiere que la evaluación de (¿error? E) se detenga con el valor cierto si la evaluación de E termina por error y se detiene
con el valor falso de lo contrario. Explique por qué no es posible implementar el error. construir como parte del entorno Lisp.
B. … Implementa una construcción Lisp (E protegida) que ejecuta E y devuelve su valor o, si E se detiene con un error, devuelve 0 sin
realizar ningún efecto secundario. Esto podría usarse para tratar de evaluar E y, si ocurre un error, simplemente use 0 en su lugar. Por
ejemplo,
tendrá el valor de E 'si la evaluación de E se detiene por error y el valor de E + E' en caso contrario. ¿Cómo podría implementar la
Las funciones Lisp compose, mapcar y maplist se definen de la siguiente manera, con #t escrito para cierto y () para la lista vacía. Texto que comienza con ;; y continuar
(definir componer
(definir mapcar
(lambda (f xs)
(cond
;; y mapa f en
el resto de
lista
)))))
(lambda (f xs)
(cond
(contras (f xs)
;; y mapa f en
el resto de
lista
(lista de mapas
(cdr
xs))
)))))
La diferencia entre maplist y mapcar es que maplist aplica f a cada sublista, mientras que mapcar aplica f a cada elemento. (Las dos expresiones de función
difieren solo en la sexta línea). Por ejemplo, si inc es una función que suma uno a cualquier número, entonces
mientras que
(lista de mapas (lambda (xs) (mapcar inc xs)) '(1 2 3 4)) = ((2 3 4 5)
(3 4 5) (4 5) (5))
Sin embargo, casi puede obtener mapcar de maplist componiendo con la función de automóvil. En particular, tenga en cuenta que
Escriba una versión de compose que nos permita definirmapcar de maplist. Más específicamente, escribe una definición de compose2 para
ese
una. Complete el código que falta en la siguiente definición. La respuesta correcta es corta y encaja aquí fácilmente. También es posible que desee
(definir compose2
(lambda (gh)
(lambda (f xs)
)))
B. Cuando se evalúa (compose2 maplist car), el resultado es una función definida por (lambda (f xs) (g…)) arriba, con
C. También podríamos escribir la subexpresión (lambda (xs) (…)) como (compose (…) (…)) para dos funciones. ¿Cuáles son estas dos
Esta pregunta le pide que piense en la recolección de basura en Lisp y compare nuestra definición de basura en el texto con la que se da en el artículo de McCarthy de 1960
sobre Lisp. La definición de McCarthy está escrita para Lisp específicamente, mientras que nuestra definición se establece generalmente para cualquier lenguaje de
programación. Responda la pregunta comparando las definiciones ya que se aplican solo a Lisp. Aquí están las dos definiciones.
Basura, nuestra definición: En un punto dado de la ejecución de un programa P, una ubicación de memoria m es basura si ninguna ejecución continua de P desde
Basura, definición de McCarthy: " Cada registro que es accesible para el programa es accesible porque se puede acceder a él desde uno o más de los
registros base mediante una cadena de operaciones car y cdr. Cuando se cambia el contenido de un registro base, puede suceder que el registro al que
apuntaba anteriormente el registro base no pueda ser alcanzado por una cadena car-cdr desde ningún registro base. Dicho registro puede considerarse
abandonado por el programa porque ningún programa posible ya puede encontrar su contenido ".
una. Si una ubicación de memoria es basura según nuestra definición, ¿es necesariamente basura según la definición de McCarthy?
B. Si una ubicación es basura según la definición de McCarthy, ¿es basura según nuestra definición? Explica por qué o por qué no.
C. Hay recolectores de basura que recogen todo lo que es basura según la definición de McCarthy. ¿Sería posible escribir un
recolector de basura para recolectar todo lo que es basura según nuestra definición? Explica por qué o por qué no.
Esta pregunta trata sobre una posible implementación de recolección de basura para Lisp. Tanto Lisp impuro como puro tienen abstracción lambda, aplicación de
funciones y funciones elementales atom, eq, car, cdr y cons. Impure Lisp también tiene rplaca, rplacd y otras funciones que tienen efectos secundarios en las
células de memoria.
Recuento de referencias es un esquema simple de recolección de basura que asocia un recuento de referencias con cada dato en la memoria. Cuando se asigna
memoria, el recuento de referencia asociado se establece en 0. Cuando un puntero se establece para apuntar a una ubicación, se incrementa el recuento para esa
ubicación. Si un puntero a una ubicación se restablece o se destruye, el recuento de la ubicación se reduce. En consecuencia, el recuento de referencias siempre indica
dato. Cuando un recuento llega a 0, el datum se considera basura y se devuelve a la lista de almacenamiento gratuito. Por ejemplo,
después de la evaluación de (cdr (cons (cons 'A' B) (cons 'C' D))), la celda creada para (cons 'A' B) es basura, pero la celda para (cons 'C' D) es no.
una. Describa cómo se podría usar el recuento de referencias para la recolección de basura al evaluar la siguiente expresión:
donde a, b, cyd son nombres previamente definidos para celdas. Suponga que la referencia cuenta para a,
b, cyd se establecen inicialmente en algunos números mayores que 0, para que no se conviertan en basura. Suponga que el resultado
de toda la expresión no es basura. ¿Cuántas de las tres células contras generadas por la evaluación de esta expresión se pueden
B. La función Lisp "impura" rplaca toma como argumentos una celda de cons C y un valor v y modifica C' s
a ddress campo al que apuntar v. Tenga en cuenta que esta operación no no producir una nueva celda de contras; modifica el que recibe
como argumento. La función rplacd realiza la misma función con respecto a la parte decreciente de su argumento cons cell.
Los programas Lisp que usan rplaca o rplacd pueden crear estructuras de memoria que no se pueden recolectar correctamente mediante el recuento de referencias.
Describa una configuración de celdas de cons que se pueden crear mediante el uso de operaciones de Lisp puro y rplaca y rplacd. Explique por qué el algoritmo de recuento
Existe una amplia variedad de algoritmos para elegir al implementar la recolección de basura para un idioma específico. En este problema, examinamos un algoritmo para
encontrar basura en Lisp puro (Lisp sin efectos secundarios) basado en el concepto de regiones. En términos generales, una región es una sección del texto del programa.
Para simplificar las cosas, consideramos cada función como una región separada. La recolección basada en regiones recupera la basura cada vez que la ejecución del
programa sale de una región. Debido a que estamos tratando las funciones como regiones en este problema, nuestra versión de colección basada en regiones intentará
encontrar basura cada vez que un programa regrese de una llamada a función.
una. Aquí hay una idea simple para la recolección de basura basada en regiones:
Cuando una función sale, libera toda la memoria que se asignó durante la ejecución de la función.
Sin embargo, esto no es correcto ya que algunas ubicaciones de memoria que se liberan aún pueden ser accesibles para el programa. Explique la
falla describiendo un programa que posiblemente podría acceder a un fragmento de memoria previamente liberado. No es necesario que escriba
más de cuatro o cinco frases; simplemente explique los aspectos de un programa de ejemplo que sean relevantes para la pregunta.
B. Corrija el método del inciso a) para que funcione correctamente. No es necesario que su método encuentre toda la basura, pero las ubicaciones
que se liberan deberían ser realmente basura. Su respuesta debe tener la siguiente forma:
Cuando una función sale, libera toda la memoria asignada por la función excepto….
Justifica tu respuesta. ( Pista: Su declaración no debe ser más de una oración o dos. Su justificación debe ser un párrafo
corto).
C. Ahora suponga que tiene un recolector de basura basado en regiones que funciona correctamente. ¿Su recolector basado en regiones
tiene alguna ventaja o desventaja en comparación con un recolector simple de marca y barrido?
D. ¿Podría un colector basado en regiones como el que se describe en este problema funcionar para Lisp impuro? Si cree que el problema es
más complicado para Lisp impuro, explique brevemente por qué. Puede considerar el problema para C en lugar de para Lisp impuro si lo
desea, pero no dé una respuesta que dependa de propiedades específicas de C, como la aritmética de punteros. El objetivo de esta
pregunta es explorar la relación entre los efectos secundarios y una forma simple de recopilación basada en regiones.
El concepto de futuro fue popularizado por el trabajo de R. Halstead en el lenguaje Multilisp para Lisp concurrente
programación. Operacionalmente, un futuro consiste en una ubicación en la memoria (parte de una celda de contras) y un proceso que tiene la intención de colocar un valor en
esta ubicación en algún momento "en el futuro". Más específicamente, la evaluación de (futuro e) procede de la siguiente manera:
I. La locación l que contendrá el valor de (e futuro) se identifica (si el valor va a ir a una celda de contras existente) o se crea si
es necesario.
iv. El proceso que invocó (futuro e) continúa en paralelo con el nuevo proceso. Si el proceso de origen intenta leer el contenido
de la ubicación l mientras aún está vacío, el proceso se bloquea hasta que la ubicación se haya llenado con el valor de e.
Aparte de esta construcción, todas las demás operaciones en este problema se definen como en Lisp puro. Por ejemplo, si la expresión e se evalúa como la lista (1 2 3),
entonces la expresión
produce una lista cuyo primer elemento es el átomo 'a y cuya cola se convierte en (1 2 3) cuando termina el proceso que evalúa e. El valor de la construcción
futura es que el programa puede operar en el carro de esta lista mientras que el valor de la cdr se calcula en paralelo. Sin embargo, si el programa intenta
examinar el cdr de la lista antes de que el valor se haya colocado en la ubicación vacía, entonces el cálculo se bloqueará (esperará) hasta que los datos estén
disponibles.
una. Suponiendo un número ilimitado de procesadores, ¿cuánto tiempo esperaría que tome la evaluación de la siguiente función fib
en el argumento n de entero positivo?
(cond ((ecuación n 0) 1)
((eqn1) 1)
Solo nos interesa el tiempo hasta una constante multiplicativa; puede utilizar la notación "gran Oh" si lo desea. Si dos procesadores
realizan dos instrucciones al mismo tiempo, cuéntelo como una unidad de tiempo.
(… E…)
(… (Futuro e)…)
que difieren solo porque una ocurrencia de una subexpresión e se reemplaza con (futuro e), sería equivalente. Sin embargo, hay
algunas circunstancias en las que el resultado de evaluar uno puede diferir del otro. Más específicamente, los efectos secundarios
pueden causar problemas. Para demostrar esto, escriba una expresión de la forma (… e…) de modo que cuando la e cambie a (e
futura), el valor o el comportamiento de la expresión pueda ser diferente debido a los efectos secundarios, y explique por qué. No se
C. Los efectos secundarios no son la única causa de diferentes resultados de evaluación. Escriba una expresión Lisp pura de la forma (… e '…) de
modo que cuando la e' se cambie a (futura e '), el valor o el comportamiento de la expresión pueda ser diferente y explique por qué.
D. Suponga que forma parte de un equipo de diseño de lenguajes que ha adoptado futuros como un enfoque de la concurrencia. El jefe de su
equipo sugiere una función de manejo de errores llamada intente bloquear. La forma sintáctica de un bloque try es
(prueba e
(error-1 handler-1)
(error-2 handler-2)
...
(error-n controlador-n))
I. Los errores son definidos por el programador y ocurren cuando una expresión de la forma (error de aumento-i) se evalúa dentro de e, la expresión
iii. Si el error denominado error-i ocurre durante la evaluación de e, se aborta el resto del cálculo de e, se evalúa la expresión
handler-i, y este se convierte en el valor del bloque try.
Los otros miembros del equipo piensan que esta es una gran idea y, alegando que es una construcción completamente sencilla, le piden que continúe y la implemente.
Crees que la construcción podría plantear algunos problemas. Nombre dos problemas o interacciones importantes entre el manejo de errores y la simultaneidad que
crea que deben tenerse en cuenta. Proporcione ejemplos de códigos cortos o bocetos para ilustrar sus puntos. ( Nota: No se le pide que resuelva ningún problema
asociado con los futuros y pruebe los bloques; simplemente identifique los problemas). Suponga para esta parte que está usando Lisp puro (sin efectos secundarios).
Equipo-Fly
Equipo-Fly
Capítulo 4: Fundamentos
VISIÓN GENERAL
En este capítulo se proporcionan algunos antecedentes sobre la implementación del lenguaje de programación a través de breves discusiones sobre la sintaxis, el
análisis sintáctico y los pasos utilizados en los compiladores convencionales. También observamos dos marcos fundamentales que son útiles en el análisis y diseño de
lenguajes de programación: cálculo lambda y semántica denotacional. El cálculo lambda es un buen marco para definir conceptos sintácticos comunes a muchos
lenguajes de programación y para estudiar la evaluación simbólica. La semántica denotacional muestra que, en principio, los programas pueden reducirse a funciones.
Varios otros marcos teóricos son útiles en el diseño y análisis de lenguajes de programación. Estos van desde la teoría de la computabilidad, que
proporciona una idea del poder y las limitaciones de los programas, hasta la teoría de tipos, que incluye aspectos tanto de la sintaxis como de la semántica
de los lenguajes de programación. A pesar de muchos años de investigación teórica, la teoría actual del lenguaje de programación aún no proporciona
respuestas a algunas preguntas fundamentales importantes. Por ejemplo, no tenemos una buena teoría matemática que incluya funciones de orden
superior, transformaciones de estado y concurrencia. No obstante, los marcos teóricos han tenido un impacto en el diseño de lenguajes de programación y
pueden usarse para identificar problemas como en los lenguajes de programación. Para comparar un aspecto de la teoría y la práctica, Sección 4.4 .
Equipo-Fly
Equipo-Fly
Un programa es una descripción de un proceso dinámico. El texto de un programa en sí se llama sintaxis; las cosas que hace un programa comprenden su semántica. La
función de una implementación de lenguaje de programación es transformar la sintaxis del programa en instrucciones de máquina que se pueden ejecutar para hacer que
Los lenguajes de programación que son convenientes para el uso de las personas se basan en conceptos y abstracciones que pueden no corresponder directamente con
las características de la máquina subyacente. Por esta razón, un programa debe traducirse al conjunto de instrucciones básicas de la máquina antes de que pueda
ejecutarse. Esto lo puede hacer un compilador, que traduce todo el programa en código de máquina antes de que se ejecute el programa, o un Interprete, que combina
traducción y ejecución de programas. Discutimos la implementación del lenguaje de programación mediante el uso de compiladores, ya que esto hace que sea más fácil
John Backus, uno de los primeros pioneros, se convirtió en programador informático en IBM en 1950. En la década de 1950, Backus desarrolló Fortran, el
primer lenguaje informático de alto nivel, que estuvo disponible comercialmente en 1957. El lenguaje todavía se usa ampliamente para programación numérica
y científica. En 1959, John Backus inventó la forma Backus naur (BNF), la notación estándar para definir la sintaxis de un lenguaje de programación. En años
posteriores, se convirtió en un defensor de la programación funcional pura y dedicó su conferencia del Premio ACM Turing de 1977 a este tema.
Conocí a John Backus a través de IFIP WG 2.8, un grupo de trabajo de la Federación Internacional de Procesamiento de Información sobre
programación funcional. Backus continuó trabajando en programación funcional en IBM Almaden durante la década de 1980, aunque su grupo se
disolvió después de su jubilación. Un individuo de modales apacibles y sin pretensiones, aquí hay una cita que da un sentido de su espíritu pionero e
independiente:
"Realmente no sabía qué diablos quería hacer con mi vida. Decidí que lo que quería era un buen equipo de alta fidelidad porque me gustaba la música.
En esos días, realmente no existían, así que fui a un en la escuela de técnicos de radio. Tuve un maestro muy agradable, el primer buen maestro que
tuve, y me pidió que cooperara con él y calcule las características de algunos circuitos para una revista ".
"Recuerdo que hice cálculos relativamente simples para obtener algunos puntos en una curva para un amplificador. Fue laborioso, tedioso y horrible,
pero me interesó en las matemáticas. El hecho de que tuviera una aplicación me interesó".
Dado un programa en algunos lenguaje fuente, el compilador produce un programa en un lengua de llegada, que suele ser el conjunto de instrucciones, o lenguaje de
La mayoría de los compiladores están estructurados como una serie de fases, cada una de las cuales realiza un paso en la traducción del programa de origen al programa de
destino. Un compilador típico puede constar de las fases que se muestran en el siguiente diagrama:
Cada una de estas fases se analiza brevemente. Nuestro objetivo con este libro es solo comprender las partes de un compilador para que podamos discutir cómo se
podrían implementar las diferentes características del lenguaje de programación. No discutimos cómo construir un compilador. Ese es el tema de muchos libros sobre
1998).
Análisis léxico
Los símbolos de entrada se escanean y se agrupan en unidades significativas llamadas tokens. Por ejemplo, el análisis léxico de la expresión temp: = x + 1, que
utiliza la notación de estilo Algol: = para la asignación, dividiría esta secuencia de símbolos en cinco tokens: el identificador temp, la asignación "símbolo": =, la
variable x, el símbolo de suma + y el número 1. El análisis léxico puede distinguir números de identificadores. Sin embargo, debido a que el análisis léxico se
basa en un solo escaneo de izquierda a derecha (y de arriba a abajo), el análisis léxico no distingue entre identificadores que son nombres de variables e
identificadores que son nombres de constantes. Debido a que las variables y constantes se declaran de manera diferente, las variables y constantes se
Análisis de sintaxis
En esta fase, los tokens se agrupan en unidades sintácticas como expresiones, enunciados y declaraciones que deben ajustarse a las reglas
gramaticales del lenguaje de programación. La acción realizada durante esta fase, denominada
análisis se describe en Subsección 4.1.2 . El propósito del análisis sintáctico es producir una estructura de datos llamada árbol de análisis sintáctico, que
representa la estructura sintáctica del programa de una manera conveniente para las fases posteriores del compilador. Si un programa no cumple con los
requisitos sintácticos para ser un programa bien formado, la fase de análisis generará un mensaje de error y finalizará el compilador.
Análisis semántico
En esta fase de un compilador, se aplican reglas y procedimientos que dependen del contexto que rodea a una expresión. Por ejemplo, volviendo a nuestra
expresión de muestra temp: = x + 1, encontramos que es necesario asegurarnos de que los tipos coincidan. Si esta asignación se produce en un idioma en el que
los enteros se convierten automáticamente en flotantes según sea necesario, existen varias formas de asociar los tipos con partes de esta expresión. En el análisis
semántico estándar, los tipos de temp yx se determinarían a partir de las declaraciones de estos identificadores. Si ambos son números enteros, entonces el
número 1 podría marcarse como un número entero y + como suma de números enteros, y la expresión se consideraría correcta. Si uno de los identificadores,
digamos x, es un flotante, entonces el número 1 se marcaría como un flotante y el + se marcaría como una adición de punto flotante. Dependiendo de si temp es un
número flotante o un número entero, también puede ser necesario insertar una conversión alrededor de la subexpresión x + 1. El resultado de esta fase es un árbol
de análisis sintáctico aumentado que representa la estructura sintáctica del programa e incluye información adicional como los tipos de identificadores y el lugar en
Aunque la fase siguiente al análisis se denomina comúnmente análisis semántico, este uso de la palabra semántico es diferente del uso estándar del término para sentido.
Algunos redactores de compiladores usan la palabra semántica porque esta fase se basa en la información del contexto y el tipo de gramática que se usa para el análisis
sintáctico no captura la información del contexto. Sin embargo, en el resto de este libro, la palabra semántica se usa para referirse a cómo se ejecuta un programa, no a
Aunque podría ser posible generar un programa de destino a partir de los resultados del análisis sintáctico y semántico, es difícil generar código eficiente en una
fase. Por lo tanto, muchos compiladores primero producen una forma intermedia de código y luego optimizan este código para producir un programa de destino
más eficiente.
Debido a que la última fase del compilador puede traducir un conjunto de instrucciones a otro, no es necesario escribir el código intermedio con el conjunto de
instrucciones real de la máquina de destino. Es importante utilizar una representación intermedia que sea fácil de producir y fácil de traducir al idioma de
destino. La representación intermedia puede ser alguna forma de código genérico de bajo nivel que tenga propiedades comunes a varias computadoras.
Cuando se usa una única representación intermedia genérica, es posible usar esencialmente el mismo compilador para generar programas de destino para
Optimización de código
Existe una variedad de técnicas que pueden usarse para mejorar la eficiencia de un programa. Estas técnicas son
generalmente se aplica a la representación intermedia. Si se escriben varias técnicas de optimización como transformaciones de la representación
intermedia, estas técnicas se pueden aplicar una y otra vez hasta que se alcance alguna condición de terminación.
Eliminación de subexpresión común: Si un programa calcula el mismo valor más de una vez y el compilador puede detectarlo,
entonces es posible transformar el programa para que el valor se calcule solo una vez y se almacene para su uso posterior.
Propagación de copia: Si un programa contiene una asignación como x = y, entonces puede ser posible cambiar declaraciones
posteriores para que se refieran a y en lugar de ax y eliminar la asignación.
Eliminación de código muerto: Si alguna secuencia de instrucciones nunca se puede alcanzar, entonces se puede eliminar del
programa.
Optimizaciones de bucle: Hay varias técnicas que se pueden aplicar para eliminar instrucciones de los bucles. Por ejemplo, si alguna
expresión aparece dentro de un bucle pero tiene el mismo valor en cada paso a través del bucle, entonces la expresión se puede
Llamadas de función en línea: Si un programa llama a la función f, es posible sustituir el código por f en el lugar donde se llama f. Esto
hace que el programa de destino sea más eficiente, ya que las instrucciones asociadas con la llamada a una función pueden eliminarse,
pero también aumenta el tamaño del programa. La consecuencia más importante de las llamadas a funciones en línea es normalmente
Codigo de GENERACION
La fase final de un compilador estándar es convertir el código intermedio en un código de máquina de destino. Esto implica elegir una ubicación de memoria, un registro
o ambos, para cada variable que aparece en el programa. Existe una variedad de algoritmos de asignación de registros que intentan reutilizar los registros de manera
eficiente. Esto es importante porque muchas máquinas tienen un número fijo de registros y las operaciones en los registros son más eficientes que transferir datos
Usamos gramáticas para describir varios idiomas en este libro. Aunque por lo general no nos preocupa demasiado la pragmática del análisis sintáctico, en esta subsección
examinamos brevemente el problema de producir un árbol de análisis sintáctico a partir de una secuencia de tokens.
Gramáticas
Las gramáticas proporcionan un método conveniente para definir conjuntos infinitos de expresiones. Además, la estructura impuesta por una gramática nos da una
A gramática consta de un símbolo de inicio, un conjunto de no terminales, un conjunto de terminales y un conjunto de producciones. Los no terminales son símbolos que
se utilizan para escribir la gramática y los terminales son símbolos que aparecen en el lenguaje generado por la gramática. En los libros sobre teoría de autómatas y temas
relacionados, las producciones de una gramática se escriben en la forma s → tu con una flecha, lo que significa que en una cadena que contiene el símbolo s, podemos
reemplazar s con los simbolos tu. Sin embargo, aquí usamos una notación más compacta, comúnmente conocida como BNF.
Las ideas principales se ilustran con ejemplos. Un lenguaje simple de expresiones numéricas se define mediante la siguiente gramática:
e :: = n | e + e | ee n :: = d |
d :: = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
donde e es el símbolo de inicio, los símbolos e, nyd son no terminales, y 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + y - son los terminales. El lenguaje definido por esta
gramática consta de todas las secuencias de terminales que podemos producir comenzando con el símbolo de inicio e y reemplazando los no terminales de
acuerdo con las producciones anteriores. Por ejemplo, la primera producción anterior significa que podemos reemplazar una aparición de e con el símbolo n,
los tres símbolos e + e, o los tres símbolos ee. El proceso se puede repetir con cualquiera de las tres líneas anteriores.
0, 1 + 3 + 5, 2 + 4 - 6 - 8
e, e + e, e + 6 - e
no son expresiones en el lenguaje dadas por la gramática. El propósito de los no terminales es realizar un seguimiento de la forma de una expresión a medida que
se forma. Todos los no terminales deben reemplazarse por terminales para producir una expresión bien formada del lenguaje.
Derivaciones
Una secuencia de pasos de reemplazo que da como resultado una cadena de terminales se llama derivación.
Aquí hay dos derivaciones en esta gramática, la primera dada en su totalidad y la segunda con algunos pasos faltantes que el lector puede completar
10-15 + 12
A menudo es conveniente representar una derivación mediante un árbol. Este árbol, llamado árbol de análisis de una derivación, o árbol de derivación, se construye con
el símbolo de inicio como raíz del árbol. Si un paso en la derivación va a reemplazar s con X 1, …, Xn,
El árbol de análisis para la derivación de 10 - 15 + 12 en la subsección anterior tiene una estructura útil. Específicamente, debido a que el primer paso produce
que es otro árbol de análisis para la misma expresión. Un hecho importante sobre los árboles de análisis es que cada uno corresponde a un paréntesis único de
la expresión. Específicamente, el primer árbol corresponde a 10- (15 + 12) mientras que el segundo corresponde a (10-15) +12. Como ilustra este ejemplo, el
Una gramática es ambiguo si alguna expresión tiene más de un árbol de análisis. Si cada expresión tiene como máximo un árbol de análisis, la gramática es inequívoco.
Ejemplo 4.1
Existe una ambigüedad interesante que involucra si-entonces-si no. Esto se puede ilustrar con la siguiente gramática simple:
s :: = v: = e | s; s | si b entonces s | si b entonces s si no s
v :: = x | y | z
e :: = v | 0 | 1 | 2 | 3 | 4 b :: = e
=e
donde s es el símbolo de inicio, s, v, e y b son no terminales y los otros símbolos son terminales. Las letras s, v, e y b representan declaración, variable, expresión y
prueba booleana, respectivamente. Llamamos a las expresiones del lenguaje generadas por esta gramática declaraciones. A continuación, se muestra un ejemplo de
x: = 1; y: = 2; si x = y entonces y: = 3
Esta declaración también tiene otro árbol de análisis, que obtenemos poniendo dos asignaciones a la izquierda de la raíz y la declaración if-then a la derecha.
Sin embargo, la diferencia entre estos dos árboles de análisis no afectará el comportamiento del código generado por un compilador ordinario. La razón es que
s1; s2 normalmente se compila con el código de s1 seguido del código de s2. Como resultado, se generaría el mismo código si consideramos s1; s2; s3 como
Surge una situación más complicada cuando if-then se combina con if-then-else de la siguiente manera:
si b1 entonces si b2 entonces s1 si no s2
¿Qué debería suceder si b1 es verdadero y b2 es falso? ¿Debe ejecutarse s2 o no? Como puede ver, esto depende de cómo se analice la declaración. Una
gramática que permite esta combinación de condicionales es ambigua, con dos posibles
significados de las declaraciones de este formulario.
El análisis es el proceso de construir árboles de análisis para secuencias de símbolos. Supongamos que definimos un idioma L escribiendo una gramática GRAMO. Entonces,
dada una secuencia de símbolos s, nos gustaría determinar si s está en el idioma L. Si es así, nos gustaría compilar o interpretar s, y para este propósito nos gustaría
encontrar un árbol de análisis para s. Un algoritmo que decide si s es en L, y construye un árbol de análisis si lo es, se llama algoritmo de análisis por GRAMO.
Hay muchos métodos para construir algoritmos de análisis a partir de gramáticas. Muchos de estos funcionan solo para formas particulares de gramáticas. Dado que
el análisis sintáctico es una parte importante de la compilación de lenguajes de programación, el análisis sintáctico se suele tratar en cursos y libros de texto sobre
compiladores. Para la mayoría de los lenguajes de programación que podría considerar, es sencillo analizar el lenguaje o hay algunos cambios en la sintaxis que no
cambian mucho la estructura del lenguaje, pero hacen posible analizar el lenguaje de manera eficiente.
Dos cuestiones que consideramos brevemente son las convenciones sintácticas de precedencia y asociatividad de derecha o izquierda. Estos se ilustran brevemente en el
siguiente ejemplo.
Ejemplo 4.2
Un diseñador de lenguaje de programación podría decidir que las expresiones deben incluir suma, resta y multiplicación y escribir la
siguiente gramática:
e :: = 0 | 1 | e + e | ee | e * e
Esta gramática es ambigua, ya que muchas expresiones tienen más de un árbol de análisis. Para expresiones como 1 - 1 + 1, el
El valor de la expresión dependerá de la forma en que se analice. Una solución a este problema es requerir un paréntesis completo. En otras
palabras, podríamos cambiar la gramática a
e :: = 0 | 1 | (e + e) | (ee) | (e * e)
para que ya no sea ambiguo. Sin embargo, como sabe, puede resultar incómodo escribir muchos paréntesis. Además, para muchas expresiones, como 1
+ 2 + 3 + 4, el valor de la expresión no depende de la forma en que se analiza. Por lo tanto, es innecesariamente engorroso requerir paréntesis para
cada operación.
La solución estándar a este problema es adoptar convenciones de análisis que especifiquen un único árbol de análisis para cada expresión. Estos se llaman precedencia
y asociatividad. Para esta gramática específica, una convención de precedencia natural es que la multiplicación (*) tiene mayor precedencia que la suma (+) y la resta
( -). Incorporamos precedencia en el análisis sintáctico tratando una expresión sin paréntesis e op1 e op2 e como si se insertaran paréntesis alrededor del operador de
mayor precedencia. Con esta regla en vigor, la expresión 5 * 4-3 se analizará como si estuviera escrita como (5 * 4) -3. Esto coincide con la forma en que la mayoría de
nosotros pensaría normalmente sobre la expresión 5 * 4-3. Debido a que no existe una forma estándar de que la mayoría de los lectores analicen 1 + 1-1, podríamos
dar igual precedencia a la suma y la resta. En este caso, un compilador podría emitir un mensaje de error que requiera que el programador ponga entre paréntesis 1 +
1-1. Alternativamente, un
Una expresión como esta podría eliminarse la ambigüedad mediante el uso de una convención adicional.
La asociatividad entra en juego cuando dos operadores de igual precedencia aparecen uno al lado del otro. Bajo asociatividad izquierda, una expresión e op1 e op2
e se analizaría como (e op1 e) op2 e, si los dos operadores tienen la misma precedencia. Si adoptamos una convención de asociatividad por la derecha en su lugar,
2 + 3-4 * 5 + 2 2 + 3- (4 * 5) +2
((2 + 3) - (4 * 5)) + 2 2+ (3 - ((4 * 5) +2))
Equipo-Fly
Equipo-Fly
El cálculo lambda es un sistema matemático que ilustra algunos conceptos importantes del lenguaje de programación en una forma simple y pura. El cálculo lambda
tradicional tiene tres partes principales: una notación para definir funciones, un sistema de prueba para probar ecuaciones entre expresiones y un conjunto de reglas
de cálculo llamadas reducción. La primera palabra en el nombre lambda cálculo proviene del uso de la letra griega lambda ( λ) en expresiones de función. (No hay
significado en la letra λ.) La segunda palabra proviene de la forma en que se puede usar la reducción para calcular el resultado de aplicar una función a uno o más
argumentos. Este cálculo es una forma de evaluación simbólica de expresiones. El cálculo Lambda proporciona una notación conveniente para describir lenguajes de
programación y puede considerarse como la base de muchas construcciones de lenguaje. En particular, el cálculo lambda proporciona formas fundamentales de
parametrización (mediante expresiones de función) y vinculación (mediante declaraciones). Estos son conceptos básicos que son comunes a casi todos los lenguajes
de programación modernos. Por lo tanto, es útil familiarizarse lo suficiente con el cálculo lambda para considerar las expresiones en su lenguaje de programación
favorito como esencialmente una forma de expresión lambda. Por simplicidad, se discute el cálculo lambda sin tipo; también hay versiones mecanografiadas del
cálculo lambda. En cálculo lambda mecanografiado, existen restricciones de verificación de tipo adicionales que descartan ciertas formas de expresiones. Sin
embargo, los conceptos básicos y las reglas de cálculo siguen siendo esencialmente los mismos.
Intuitivamente, una función es una regla para determinar un valor a partir de un argumento. Esta vista de funciones se usa informalmente en la mayoría de las matemáticas.
(Ver Subsección 2.1.2 para una discusión de funciones en matemáticas.) Algunos ejemplos de funciones estudiadas en matemáticas son
En la forma más simple y pura del cálculo lambda, no hay operadores específicos de dominio como la suma y la exponenciación, solo la
definición y aplicación de funciones. Esto nos permite escribir funciones como
h (x) = f (g (x))
porque h se define únicamente en términos de aplicación de funciones y otras funciones que asumimos ya están definidas. Es posible agregar operaciones
como la suma y la potenciación al cálculo lambda puro. Aunque los puristas se apegan al cálculo lambda puro sin sumas ni multiplicaciones, usaremos estas
operaciones en ejemplos ya que esto hace que las funciones que definimos sean más familiares.
Las principales construcciones del cálculo lambda son abstracción lambda y solicitud. Usamos la abstracción lambda para escribir funciones: Si METRO es alguna
expresión, entonces λ xM es la función que obtenemos al tratar METRO en función de la variable X. Por ejemplo,
λ. x .x
es una abstracción lambda que define la función de identidad, la función cuyo valor en X es X. Una forma más familiar de definir la función de identidad es escribiendo
Yo (x) = x.
Sin embargo, esta forma de definir una función nos obliga a inventar un nombre para cada función que queramos. El cálculo lambda nos permite escribir funciones anónimas
En notación lambda, es tradicional escribir una aplicación de función simplemente poniendo una expresión de función delante de uno o más argumentos; los
paréntesis son opcionales. Por ejemplo, podemos aplicar la función identidad a la expresión METRO escribiendo
( λ x .x) M.
El valor de esta aplicación es la función de identidad, aplicada a METRO, que acaba siendo METRO. Por lo tanto, tenemos
( λ x .x) M = M.
Parte del cálculo lambda es un conjunto de reglas para deducir ecuaciones como esta. Otro ejemplo de expresión lambda es
λ F. λ gramo. λ X. f (gx).
Podemos extender el cálculo lambda puro agregando una variedad de otras construcciones. Llamamos a una extensión del cálculo lambda puro con
operaciones adicionales un cálculo lambda aplicado. Una idea básica que subyace a la relación entre el cálculo lambda y la informática es el lema
Esto funciona incluso para lenguajes de programación con efectos secundarios, ya que la forma en que un programa depende del estado de la máquina se puede representar
mediante estructuras de datos explícitas para el estado de la máquina. Esta es una de las ideas básicas detrás de la semántica denotacional de Scott-Strachey, como se analiza
en Sección 4.3 .
Sintaxis de expresiones
La sintaxis de las expresiones lambda sin tipo se puede definir con una gramática BNF. Suponemos que tenemos un conjunto infinito V de variables y use x, y,
M :: = x | MM | λ xM
dónde X puede ser cualquier variable (elemento de V). Una expresión de la forma METRO 1 METRO 2 se llama solicitud, y una expresión de la forma λ xM se llama un abstracción
lambda. Intuitivamente, una variable X se refiere a alguna función, la función particular está determinada por el contexto; METRO 1 METRO 2 es la aplicación de la función METRO
1 al argumento METRO 2; y λ xM es la función que, dado el argumento X, devuelve el valor METRO. En la literatura sobre cálculo lambda, es común referirse a las
Hay una serie de convenciones sintácticas que generalmente son convenientes para los expertos en cálculo lambda, pero son confusas de aprender. En
general, podemos evitarlos escribiendo suficientes paréntesis. Sin embargo, una convención que usaremos es que en una expresión que contiene un λ, el
Enlace variable
Una aparición de una variable en una expresión puede ser libre o ligada. Si una variable es libre en alguna expresión, esto significa que la variable no está
declarada en la expresión. Por ejemplo, la variable X es libre en la expresión x + 3. No podemos evaluar la expresión x + 3 tal como está, sin ponerlo
dentro de una expresión más grande que asociará algún valor con X. Si una variable no es libre, entonces debe ser porque es atado.
El símbolo λ se llama un operador vinculante, ya que vincula una variable dentro de un ámbito específico (parte de una expresión). La variable X está atado en λ xM Esto
significa que X es solo un marcador de posición, como X en la integral ∫ f (x) dx o la fórmula lógica
∀ xP (x), y el significado de λ xM no depende de X. Por lo tanto, al igual que ∫ f (x) D X y ∫ f (y) D y describe lo mismo
integral, podemos cambiar el nombre de un λ- atado X a y sin cambiar el significado de la expresión. En particular,
¿Se llaman las expresiones que difieren solo en los nombres de las variables vinculadas? equivalente. ¿Cuándo queremos enfatizar que dos expresiones son?
En λ xM, la expresion METRO se llama el alcance de la encuadernación λ X. Una variable X apareciendo en una expresión METRO está vinculado si aparece en el alcance de alguna λ X y
es gratis de lo contrario. Para ser más precisos sobre esto, podemos definir el conjunto FV ( METRO) de
FV ( x) = {x},
FV ( λ xM) = FV ( M) - x,
donde - significa diferencia de conjunto, es decir, S - x = {y? S | y ≠ X}. Por ejemplo, FV ( λ xx) =? porque no hay variables libres en λ xx, mientras que FV ( λ f.
λ x. (f (g (x))) = {g}.
A veces es necesario hablar de diferentes ocurrencias de una variable en una expresión y distinguir entre ocurrencias libres y ligadas. En la expresion
λ X.( λ y.xy) y,
la primera aparición de X se llama un ocurrencia vinculante, ya que aquí es donde X se vuelve atado. La otra aparición de X es una ocurrencia limitada. Leyendo de
izquierda a derecha, vemos que la primera aparición de y es una ocurrencia vinculante, la segunda es una ocurrencia vinculada y la tercera es gratuita ya que está fuera
Puede resultar confuso trabajar con expresiones que utilizan la misma variable en más de una forma. Una convención útil es cambiar el nombre de las variables
vinculadas para que todas las variables vinculadas sean diferentes entre sí y diferentes de todas las variables libres. Siguiendo esta convención, escribiremos ( λ y. ( λ zz)
y) x en vez de ( λ X.( λ xx) x) x. La convención de variables será particularmente útil cuando lleguemos al razonamiento y la reducción de ecuaciones.
El cálculo lambda tiene una similitud sintáctica obvia con Lisp: la expresión lambda de Lisp
λ X. cuerpo_función,
y ambas expresiones definen funciones. Sin embargo, existen algunas diferencias entre el cálculo Lisp y lambda. Por ejemplo, las listas son el tipo de datos
básico de Lisp, mientras que las funciones son el único tipo de datos en el cálculo lambda puro. Otra diferencia es el orden de evaluación, pero ese tema no
se discutirá en detalle.
Aunque la sintaxis de los lenguajes estructurados en bloques está más lejos del cálculo lambda que de Lisp, los conceptos básicos de declaraciones y
parametrizaciones de funciones en los lenguajes tipo Algol son fundamentalmente los mismos que en el cálculo lambda. Por ejemplo, el fragmento de programa C
int f (int x) {retorno x};
block_body;
con una declaración de función al comienzo de un bloque se traduce fácilmente al cálculo lambda. La traducción es más fácil de entender si primero
dejar x = M en NORTE,
que declara que X tiene valor METRO en el cuerpo NORTE, puede considerarse como azúcar sintáctico para una combinación de abstracción y aplicación lambda:
Para aquellos que no están familiarizados con el concepto de azúcar sintáctica, esto significa que deja x = M en norte es "más dulce" de escribir en algunos casos, pero podemos pensar en
Tenga en cuenta que la expresión en forma de C y la expresión lambda tienen las mismas variables libres y ligadas y una estructura similar. Una diferencia entre C y el
cálculo lambda es que C tiene declaraciones de asignación y efectos secundarios, mientras que el cálculo lambda es puramente funcional. Sin embargo, al introducir historias,
asignaciones de ubicaciones de variables a valores, también podemos traducir programas en C con efectos secundarios en términos lambda. La traducción conserva la
estructura general de los programas, pero hace que los programas parezcan un poco más complicados, ya que las dependencias del estado de la máquina son
explícitas.
Equivalencia y sustitución
Ya hemos discutido α equivalencia de términos. Este es uno de los axiomas básicos del cálculo lambda, lo que significa que es una de las propiedades de los
términos lambda que define el sistema y que se utiliza para probar otras propiedades de los términos lambda. Expresado con más cuidado que antes, el sistema de
dónde [ y / x] M es el resultado de sustituir y para apariciones gratuitas de X en METRO y y ya no puede aparecer en METRO. Hay otros tres axiomas de ecuaciones y
cuatro reglas de inferencia para probar ecuaciones entre términos. Sin embargo, solo nos fijamos en otro axioma de ecuaciones.
El axioma de la ecuación central del cálculo lambda se utiliza para calcular el valor de una aplicación de función ( λ xM) N por sustitución. Porque λ xM es la función que
obtenemos al tratar METRO como una función de X, podemos escribir el valor de esta función en norte sustituyendo norte por X. De nuevo escribiendo [ N / x] M por el resultado
tenemos
que se llama el axioma de β- equivalencia. Algunas advertencias importantes sobre la sustitución se analizan en la siguiente subsección. El valor de λ F.
( λ f .fx) ( λ y .y) = ( λ y, y) x
Por supuesto, ( λ yy) x puede simplificarse mediante una sustitución adicional, por lo que tenemos
Cualquier lector con la edad suficiente para estar familiarizado con la documentación original de Algol 60 reconocerá β- equivalencia como el
copiar regla para evaluar llamadas a funciones. También hay paralelismos entre ( β) y macroexpansión y sustitución en línea de funciones.
Porque λ fijaciones en METRO puede entrar en conflicto con variables libres en NORTE, sustitución [ N / x] M es un poco más complicado de lo que pensamos al principio. Sin
embargo, podemos evitar todas las complicaciones siguiendo la convención de variables: cambiar el nombre de las variables enlazadas en ( λ xM) N de modo que todas las variables
vinculadas sean diferentes entre sí y diferentes de todas las variables libres. Por ejemplo, reduzcamos el término ( λ f. λ X. f (fx)) ( λ yy + x). Si primero cambiamos el nombre de las
( λ f. λ z .f (fz)) ( λ y .y + x) = λ z. (( λ y .y + x) (( λ y .y + x) z)) = λ z .z + x + x
Si nos olvidamos de cambiar el nombre de las variables vinculadas y las sustituimos a ciegas, podríamos simplificar de la siguiente manera:
Sin embargo, debemos sospechar de la segunda reducción porque la variable X es gratis en λ yy + x) pero se vuelve atado en λ xx + x + x. Recuerda: Al hacer ejercicio [ N / x] M, debemos
cambiar el nombre de cualquier variable enlazada en METRO que podría ser lo mismo que las variables libres en NORTE. Para ser precisos sobre el cambio de nombre de las variables
[ N / x] x = N,
[ N / x] (M 1 METRO 2) = ([ N / x] M 1) ([ N / x] M 2),
[ N / x] ( λ xM) = λ xM,
Porque somos libres de cambiar el nombre de la variable vinculada y en λ yM, la cláusula final λ y. ([N / x] M) siempre tiene sentido. Con esta definición precisa de
El cálculo lambda puede verse como un simple lenguaje de programación funcional. Veremos que, aunque el lenguaje es muy simple, podemos programar
con bastante naturalidad si adoptamos algunas abreviaturas. Antes de discutir las declaraciones y la recursividad, vale la pena mencionar el problema de las
La abstracción lambda nos permite tratar cualquier expresión METRO en función de cualquier variable X escribiendo λ xM Sin embargo, ¿y si nosotros
quiero tratar METRO en función de dos variables, X y y? ¿Necesitamos un segundo tipo de abstracción lambda? λ 2 x, yM para tratar
METRO en función del par de argumentos x, y? Aunque podríamos agregar operadores lambda para secuencias de parámetros formales, no es necesario
porque la abstracción lambda ordinaria será suficiente para la mayoría de los propósitos.
Podemos representar una función F de dos argumentos por una función λ X.( λ yM) de un solo argumento que, cuando se aplica, devuelve una segunda
función que acepta un segundo argumento y luego calcula un resultado de la misma manera que f. Por ejemplo, la función
f (g, x) = g (x)
tiene dos argumentos, pero se puede representar en el cálculo lambda mediante una abstracción lambda ordinaria. Definimos F curry por
La diferencia entre F y F el curry es eso F toma un par g, x) como argumento, mientras que F el curry toma un solo argumento
F curry gx = gx = f (g, x)
Así, al final, F el curry hace lo mismo que f. Esta simple idea fue descubierta por Schönfinkel, quien investigó la funcionalidad en la década de 1920. Sin embargo,
esta técnica para representar funciones de múltiples argumentos en el cálculo lambda generalmente se llama Zurra, después del pionero del cálculo lambda
Haskell Curry.
Declaraciones
dejar x = M en norte
dejar x = M en N = ( λ xN) M
La construcción let puede usarse para definir una función de composición, como en la expresión
Usando β- equivalencia, podemos simplificar esta expresión let a λ X. h (hx), La composición de h consigo mismo. En el lenguaje de los lenguajes de programación, la
Un hecho sorprendente sobre el cálculo lambda puro es que es posible escribir funciones recursivas mediante el uso de un "truco" de autoaplicación. Esto no tiene mucho que
ver con las comparaciones entre los lenguajes de programación modernos, pero puede interesar a los lectores con una inclinación técnica. (Es posible que algunos lectores y
Muchos lenguajes de programación permiten definiciones de funciones recursivas. La propiedad característica de una definición recursiva de una función f es que el
cuerpo de la función contiene una o más llamadas a f. Para elegir un ejemplo específico, supongamos que definimos la función factorial en algún lenguaje de
donde el cuerpo de la función es la expresión dentro de las llaves. Esta definición tiene una interpretación computacional sencilla: cuando se llama a f con el
argumento a, el parámetro de función n se establece en a y se evalúa el cuerpo de la función. Si la evaluación del cuerpo llega a la llamada recursiva, este
proceso se repite. Como definiciones de un procedimiento computacional, las definiciones recursivas son claramente significativas y útiles.
Una forma de entender el método de cálculo lambda para la recursividad es asociar una ecuación con una definición recursiva. Por ejemplo,
podemos asociar la ecuación
Específicamente, el valor de la expresión f (n) es igual al valor de la expresión si n = 0 entonces 1 en caso contrario n * f (n - 1) cuando f es la función factorial. El
enfoque del cálculo lambda puede verse como un método para encontrar soluciones a ecuaciones en las que aparece un identificador (el nombre de la función
Podemos simplificar la ecuación anterior usando la abstracción lambda para eliminar n del lado izquierdo. Esto nos da
Aunque puede que no esté claro qué tipo de "álgebra" está involucrada en esta manipulación de ecuaciones, podemos verificar, usando el razonamiento de cálculo
lambda y la comprensión básica de la igualdad de funciones, que la función factorial f satisface la ecuación
f = G (f)
Esto muestra que las declaraciones recursivas implican encontrar puntos fijos. A punto fijo de una función GRAMO es un valor F tal que f = G (f). En el cálculo lambda,
El hecho sorprendente de esta desconcertante expresión lambda es que, para cualquier F, la aplicación Yf es un punto fijo de F. Podemos ver esto por cálculo. Por β-
equivalencia, tenemos
Y f = ( λ X . f (xx)) ( λ X . f (xx)).
Ejemplo 4.3
Podemos definir factorial por hecho = YG, donde términos lambda Y y GRAMO son como se indica arriba, y calcular hecho 2 = 2! utilizando las reglas de cálculo del
cálculo lambda. Estos son los pasos de cálculo que representan la primera "llamada" a factorial:
Vale la pena mencionar que Y no es el único término lambda que encuentra puntos fijos de funciones. Hay otras expresiones que también funcionarían.
Sin embargo, este operador de punto fijo en particular jugó un papel importante en la historia del cálculo lambda.
Las propiedades computacionales del cálculo lambda generalmente se describen con una forma de evaluación simbólica llamada reducción. En términos simples, la
β- La equivalencia se escribió como una ecuación, generalmente la hemos usado en una dirección para "evaluar" las llamadas a funciones. Si escribimos → en lugar
de = para indicar la dirección en la que pretendemos usar la ecuación, obtenemos el paso de cálculo básico llamado β- reducción:
Nosotros decimos eso METRO β- reduce a NORTE, y escribe METRO → NORTE, si norte es el resultado de aplicar uno β- paso de reducción en algún lugar del interior METRO.
La mayoría de los ejemplos de cálculo de esta sección utilizan β- reducción, es decir, β- la equivalencia se usa de izquierda a derecha en lugar de de derecha a izquierda. Por
ejemplo,
( λ F. λ zf (fz)) ( λ y .y + x) → λ z. (( λ y .y + x) ( λ yy + x) z)) → λ zz + x + x.
Formas normales
Intuitivamente, pensamos en METRO → norte en el sentido de que, en un paso de cálculo, la expresión METRO se puede evaluar a la expresión NORTE. Generalmente, este
proceso se puede repetir, como se ilustra en la subsección anterior. Sin embargo, para muchas expresiones, el proceso finalmente llega a un punto de parada. Un punto de
parada, o expresión que no puede evaluarse más a fondo, se llama forma normal . A continuación, se muestra un ejemplo de una secuencia de reducción que conduce a una
forma normal:
Esta última expresión es una forma normal si nuestra única regla de cálculo es β- reducción, pero no una forma normal si tenemos una regla de cálculo para la suma.
Suponiendo la regla de evaluación habitual para expresiones con suma, podemos continuar con
2+1+1
→ 3+1
→ 4.
Este ejemplo debería dar una buena idea de cómo la reducción en el cálculo lambda corresponde al cálculo. Desde la década de 1930, el cálculo lambda ha
Confluencia
En nuestro ejemplo, comenzando con ( λ f. λ X. f (fx)) ( λ aa + 1) 2, hubo algunos pasos en los que tuvimos que elegir entre varias subexpresiones posibles
( λ X.( λ aa + 1) (( λ aa + 1) X)) 2,
podríamos haber evaluado cualquiera de las dos llamadas a función que comienzan con λ y. Esto no es un artefacto del cálculo lambda en sí mismo, ya que también tenemos
2 + 1 + 1.
Una propiedad importante del cálculo lambda se llama confluencia . En el cálculo lambda, como resultado de la confluencia, el orden de evaluación no afecta el valor
final de una expresión. Dicho de otra manera, si una expresión METRO puede reducirse a una forma normal, entonces hay exactamente una forma normal de METRO, independientemente
del orden en el que elijamos evaluar las subexpresiones. Aunque el enunciado matemático completo de la confluencia es un poco más complicado que esto, lo
importante a recordar es que, en el cálculo lambda, las expresiones se pueden evaluar en cualquier orden.
En resumen, el cálculo lambda es un sistema matemático con algunas propiedades sintácticas y computacionales de un lenguaje de programación. Existe una notación
general para funciones que incluye una forma de tratar una expresión como una función de alguna variable que contiene. Existe un sistema de prueba de ecuaciones que
conduce a reglas de cálculo, y estas reglas de cálculo son una forma simple de evaluación simbólica. En la terminología del lenguaje de programación, estas reglas de cálculo
son una forma de expansión macro (¡con cambio de nombre de las variables vinculadas!) O funcionan en el revestimiento. Debido a la relación con in lining, algunas
optimizaciones comunes del compilador pueden definirse y demostrarse que son correctas mediante el uso del cálculo lambda.
Cada función computable se puede representar en cálculo lambda puro. En la terminología de Capitulo 2 , el cálculo lambda es Turing completo.
(Los números se pueden representar mediante funciones y la recursividad se puede expresar mediante Y.)
La evaluación en el cálculo lambda es independiente del orden. Debido a la confluencia, podemos evaluar una expresión eligiendo cualquier
Sección 4.4 ) también es confluente, pero la evaluación en lenguajes cuyas expresiones pueden tener efectos secundarios no es confluente.
La macro expansión es otro escenario en el que confluye una forma de evaluación. Si comenzamos con un programa que contiene macros y expandimos todas las llamadas
de macro con los cuerpos de macro, entonces el programa final completamente expandido que obtengamos no dependerá del orden en que se expandan las macros.
En informática, la frase semántica denotacional se refiere a un estilo específico de semántica matemática para programas imperativos. Este enfoque se desarrolló
a finales de la década de 1960 y principios de la de 1970, siguiendo el trabajo pionero de Christopher Strachey y Dana Scott en la Universidad de Oxford. El
término semántica denotacional sugiere que un significado o denotación está asociado con cada programa o frase de programa (expresión, declaración,
declaración, etc.). La denotación de un programa es un objeto matemático, típicamente una función, en contraposición a un algoritmo o una secuencia de
es una función matemática de estados a estados, en el que un estado es una función matemática que representa los valores en la memoria en algún momento de la
ejecución de un programa. Específicamente, el significado de este programa será una función que mapea cualquier estado en el que el valor de z es un número entero no
negativo norte al estado en el que x = n, y es la suma de todos los números hasta norte, y todas las demás ubicaciones de la memoria no se modifican. La función no se
Asociar funciones matemáticas con programas es bueno para algunos propósitos y no tan bueno para otros. En muchas situaciones, consideramos que un
programa es correcto si obtenemos la salida correcta para cualquier entrada posible. Esta forma de corrección depende únicamente de la semántica denotacional
de un programa, la función matemática desde la entrada hasta la salida asociada con el programa. Por ejemplo, el programa anterior fue diseñado para calcular
la suma de todos los enteros no negativos hasta norte. Si verificamos que la semántica denotacional real de este programa es esta función matemática, entonces
habremos probado que el programa es correcto. Algunas desventajas de la semántica denotacional son que la semántica denotacional estándar no nos dice
nada sobre el tiempo de ejecución o los requisitos de almacenamiento de un programa. A veces, esto es una ventaja disfrazada porque, al ignorar estos
problemas, a veces podemos razonar de manera más eficaz sobre la corrección de los programas.
Las formas de semántica denotacional se utilizan comúnmente para razonar sobre la optimización de programas y los métodos de análisis estático. Si estamos
interesados en analizar el tiempo de ejecución, entonces la semántica operativa podría ser más útil, o podríamos usar semántica denotacional más detallada que
también involucre funciones que representan límites de tiempo. Una alternativa a la semántica denotacional se llama semántica operativa, que implica modelar los
estados de la máquina y (generalmente) las transiciones de estado paso a paso asociadas con un programa. La reducción del cálculo lambda es un ejemplo de
semántica operativa.
Composicionalidad
Un principio importante de la semántica denotacional es que el significado de un programa se determina a partir de su texto.
composicionalmente. Esto significa que el significado de un programa debe definirse a partir de los significados de sus partes, no de otra cosa, como el texto de sus
partes o los significados de programas relacionados obtenidos mediante operaciones sintácticas. Por ejemplo, la denotación de un programa como si B entonces P si no
y Q; no debe definirse con programas construidos a partir de B, P, y Q por operaciones sintácticas como la sustitución.
La importancia de la composicionalidad, que puede parecer bastante sutil al principio, es que si dos piezas de un programa tienen la misma denotación, cualquiera puede
ser sustituida por la otra en cualquier programa. Más específicamente, si B, P, y Q tienen las mismas denotaciones que B ', P', y Q ', respectivamente, entonces si B entonces P
si no Q debe tener la misma denotación que si B 'entonces P' si no Q '. La composicionalidad significa que la denotación de una expresión o declaración de programa debe
ser lo suficientemente detallada como para capturar todo lo que es relevante para su comportamiento en programas más grandes. Esto hace que la semántica denotacional
sea útil para comprender y razonar sobre cuestiones pragmáticas como la transformación y optimización de programas, ya que estas operaciones en programas implican
Una fuente de confusión al hablar (o escribir) sobre la interpretación de expresiones sintácticas es que todo lo que escribimos es en realidad sintáctico.
Cuando estudiamos un lenguaje de programación, necesitamos distinguir el lenguaje de programación que estudiamos del lenguaje que usamos para describir
este lenguaje y su significado. El lenguaje que estudiamos se llama tradicionalmente lenguaje objeto, ya que es el objeto de nuestra atención, mientras que el
segundo idioma se llama metalenguaje, porque trasciende el lenguaje objeto de alguna manera.
Para elegir un ejemplo, consideremos la interpretación matemática de una expresión algebraica simple como 3 + 6 - 4 que puede aparecer en un programa
escrito en C, Java o ML. El significado "matemático" ordinario de esta expresión es el número obtenido al hacer la suma y la resta, es decir, 5. Aquí, los
símbolos en la expresión 3 + 6 - 4 están en nuestro lenguaje de objetos, mientras que el número 5 está destinado a estar en nuestro metalenguaje. Una
forma de aclarar esto es usar un número rayado, como 1, para significar "la entidad matemática llamada número natural 1". Entonces podemos decir que el
significado de la expresión del lenguaje objeto 3 + 6 - 4 es el número natural 5. En esta oración, el símbolo 5 es un símbolo del metalenguaje, mientras que
La siguiente gramática para expresiones binarias es similar a la gramática para expresiones decimales discutida en
Subsección 4.1.2 :
e :: = n | e + e | ee n :: = b |
nótese bien
b :: = 0 | 1
Podemos dar una interpretación matemática de estas expresiones al estilo de la semántica denotacional. En semántica denotacional y otros estudios de lenguajes
de programación, es común olvidar cómo las expresiones se convierten en árboles de análisis sintáctico y simplemente dar el significado de una expresión en
Podemos interpretar las expresiones previamente definidas como números naturales usando la inducción en la estructura de los árboles de análisis sintáctico. Más específicamente, definimos
una función desde árboles de análisis sintáctico hasta números naturales, definiendo la función en una expresión compuesta haciendo referencia a su valor en expresiones más simples. Una
convención histórica es escribir [[e]] para cualquier árbol de análisis de la expresión mi. Cuando escribimos [[e1 + e2]], por ejemplo, nos referimos a un árbol de análisis sintáctico de la forma
Usando esta notación, podemos definir el significado E [[e]] de una expresión e, de acuerdo con su árbol de análisis sintáctico [[e]], de la siguiente manera:
E [[0]] = 0
E [[1]] = 1
En palabras, el valor asociado con un árbol de análisis sintáctico de la forma [[e1 + e2]], por ejemplo, es la suma de los valores dados a los subárboles [[e1]] y [[e2]]. Esta
no es una definición circular porque los árboles de análisis [[e1]] y [[e2]] son más pequeños que los
árbol de análisis sintáctico [[e1 + e2]].
En el lado derecho de los signos iguales, números y operaciones aritméticas *, + y - están destinados a indicar los números naturales reales y las
operaciones de multiplicación, suma y resta con números enteros estándar. Por el contrario, los símbolos + y - en las expresiones rodeadas por corchetes
dobles en el lado izquierdo de los signos de igual hay símbolos del lenguaje objeto, el lenguaje de las expresiones binarias.
Sin entrar en detalles sobre los tipos de funciones matemáticas que se utilizan, echemos un vistazo rápido a la forma de semántica utilizada para un
lenguaje de programación simplificado con asignación y bucles.
Las declaraciones de programa contienen expresiones con variables. Aquí hay una gramática para expresiones aritméticas con variables. Esto es lo mismo que la
gramática en Subsección 4.1.2 , excepto que las expresiones pueden contener variables además de números:
e :: = v | n | e + e | ee n :: = d |
d :: = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
v :: = x | y | z | ...
En el lenguaje de programación simplificado que consideramos, el valor de una variable depende del estado de la máquina. Modelamos el estado de la máquina
mediante una función de variables a números y escribimos E [[e]] (s) para el valor de la expresión e en el estado s. El valor de una expresión en un estado se define de la
siguiente manera.
E [[0]] (s) = 0
E [[1]] (s) = 1
… =?
E [[9]] (s) = 9
Tenga en cuenta que el estado importa en la definición en el caso base, el valor de una variable. De lo contrario, esta es esencialmente la misma definición que
El idioma de mientras los programas pueden definirse sobre cualquier clase de expresiones de valor y expresiones booleanas. Sin especificar ninguna
expresión básica en particular, podemos resumir la estructura de los programas while mediante la gramática
P :: = x: = e | PAG; P | si e entonces P si no P | mientras yo hago P
donde asumimos que X tiene el tipo apropiado para que se le asigne el valor de e en la asignación x: = e y que la prueba
mi tiene tipo bool en declaraciones if-then-else y while. Debido a que este lenguaje no tiene una entrada o salida explícita, el efecto de un programa será cambiar los
Podemos pensar que este programa tiene aportes z y salida y. Este programa usa una variable adicional X establecer y a la suma de todos los números naturales hasta z.
Estados y comandos
El significado de un programa es una función de estados a estados. En un lenguaje de programación más realista, con procedimientos o punteros, es necesario modelar
el hecho de que dos nombres de variables pueden referirse a la misma ubicación. Sin embargo, en el lenguaje simple de los programas while, asumimos que a cada
variable se le asigna una ubicación diferente. Con esta simplificación en mente, dejamos que el conjunto Expresar de representaciones matemáticas de estados de
máquina
En palabras, un estado es una función de variables a valores. Esta es una vista idealizada de los estados de la máquina de dos maneras: no modelamos explícitamente
las ubicaciones asociadas con las variables y usamos estados infinitos que dan un valor a cada variable posible.
El significado de un programa es un elemento del conjunto matemático Mando de comandos, definido por
En palabras, un comando es una función de estados a estados. A diferencia de los estados mismos, que son funciones totales, un comando puede ser un parcial función. La
razón por la que necesitamos funciones parciales es que un programa podría no terminar en un estado inicial.
Una función básica sobre estados que se utiliza en la semántica de asignación es modificar, que se define de la siguiente manera:
En palabras, modificar (s, x, a) es el estado (función de variables a valores) que es como estado s, excepto que el valor de X es
una.
Semántica denotacional
La semántica denotacional de los programas while viene dada por la definición de una función C desde programas analizados hasta comandos. Al igual que con las
expresiones, escribimos [[P]] para un árbol de análisis sintáctico del programa P. La semántica de los programas se define mediante las siguientes cláusulas, una para cada
Como e es una expresión, no un enunciado, aplicamos la función semántica E para obtener el valor de e en un estado dado.
C [[P1; P2]] (s) es el estado que obtenemos aplicando la semántica de P2 al estado que obtenemos aplicando la semántica de
P1 al estado s.
C [[si e entonces P1 sino P2]] (s) es el estado que obtenemos aplicando la semántica de P1 a s si mi es cierto en s y P2 a s en caso contrario.
C [[while e do P]] (s) es una función definida recursivamente F, de estados a estados. En palabras, f (s) es cualquiera s si mi es falso o el estado que
La definición de función recursiva en la cláusula while es relativamente sutil. También plantea algunos problemas matemáticos interesantes, ya que no siempre es
matemáticamente razonable definir funciones mediante condiciones recursivas arbitrarias. Sin embargo, en aras de mantener nuestra discusión simple y directa,
simplemente asumimos que una definición de esta forma se puede hacer matemáticamente rigurosa.
Ejemplo 4.4
Podemos calcular la semántica de varios programas usando esta definición. Para empezar, consideremos un programa simple sin bucles,
si x> y entonces x: = y si no y: = x
que establece tanto x como y al mínimo de sus valores iniciales. Para ser más concretos, calculemos la semántica de este programa en el estado s0,
= si E [[x> y]] (s0) entonces C [[x: = y]] (s0) si no C [[y: = x]] (s0)
En palabras, si el programa si x> y entonces x: = y si no y: = x se ejecuta en el estado s0, entonces el resultado será el estado que es el mismo que s0, pero con la
Ejemplo 4.5
Aunque se necesitan algunos pasos más que en el ejemplo anterior, no es demasiado difícil resolver la semántica del programa.
x: = 0; y: = 0; mientras x ≤ z hacer (y: = y + x; x: = x + 1)
en el estado s0, donde s0 (z) = 2. Algunas definiciones preliminares facilitarán el cálculo. Sean s1 y s2 los estados
s1 = modificar (s0, x, 0)
s2 = modificar (s1, y, 0)
donde s4 tiene y establecido en 1 yx en 2 y s5 tiene y establecido en 3 yx en 3. Los pasos son tediosos de escribir, pero probablemente pueda ver sin hacerlo si s0
(z) = 5, por ejemplo , entonces este programa producirá un estado en el que el valor de x es
0 + 1 + 2 + 3 + 4 + 5.
Como ilustran estos ejemplos, la semántica denotacional de los programas while asocia inequívocamente una función parcial de estados a estados con cada
programa. Un tema importante que no hemos discutido en detalle es qué sucede cuando un bucle no termina. El significado C [[while x = x do x: = x]] de un bucle
que no termina en ningún estado es una función parcial que no está definida en ningún estado. En otras palabras, para cualquier estado s, C [[mientras que x = x
Hay varias formas de ver los métodos estándar de semántica denotacional. Normalmente, la semántica denotacional viene dada por la asociación de una función
con cada programa. Como han observado muchos investigadores en semántica denotacional, un mapeo de programas a funciones debe escribirse en algún
metalenguaje. Debido a que el cálculo lambda es una notación útil para funciones, es común usar alguna forma de cálculo lambda como metalenguaje. Por lo tanto,
la mayoría de la semántica denotacional en realidad tiene dos partes: una traducción de programas a un cálculo lambda (con algunas operaciones adicionales
correspondientes a operaciones básicas en programas) y una interpretación semántica de las expresiones de cálculo lambda como objetos matemáticos. Por esta
razón, la semántica denotacional en realidad proporciona una técnica general para traducir programas imperativos en programas funcionales.
Aunque el objetivo original de la semántica denotacional era definir los significados de los programas de una manera matemática, las técnicas de la semántica
denotacional también se pueden utilizar para definir la semántica "no estándar" útil de los programas.
Un tipo útil de semántica no estándar se llama interpretación abstracta. En la interpretación abstracta, a los programas se les asigna un significado en algún
dominio simplificado. Por ejemplo, en lugar de interpretar expresiones enteras como enteros, las expresiones enteras podrían interpretarse como elementos del
conjunto finito {0, 1, 2, 3, 4, 5, …, 100,> 100}, donde> 100 es un valor utilizado para expresiones cuyo valor podría ser mayor que 100. Esto podría ser útil si
queremos ver si dos expresiones de matriz A [e1] y A [e2] se refieren a la misma matriz localización. Más específicamente, si asignamos valores a e1 del conjunto
anterior, y de manera similar para e2, podríamos determinar que e1 = e2. Si a ambos se les asigna el valor> 100, entonces no sabríamos que son iguales, pero si
se les asigna el mismo entero ordinario entre 0 y 100, entonces sabríamos que estas expresiones tienen el mismo valor. La importancia de utilizar un conjunto finito
de valores es que un algoritmo podría iterar sobre todos los estados posibles. Esto es importante para calcular las propiedades de los programas que se mantienen
Ejemplo. Suponga que queremos construir una herramienta de análisis de programas que verifique los programas para asegurarse de que cada variable se inicialice antes de
usarla. La base de este tipo de análisis de programa puede describirse mediante semántica denotacional, en la que el significado de una expresión es un elemento de un
conjunto finito.
Debido a que el problema de la detención no tiene solución, los algoritmos de análisis de programas generalmente están diseñados para ser conservador.
Conservador significa que no hay falsos positivos: un algoritmo generará correcto sólo si el programa es correcto, pero a veces puede generar error incluso si
no hay ningún error en el programa. No podemos esperar que un análisis computable decida correctamente si un programa accederá alguna vez a una variable
ejecuta la asignación x: = y sin decidir si el programa sin errores complicitado se detiene. Sin embargo, a menudo es posible desarrollar algoritmos eficientes para análisis
conservadores. Si lo piensa, se dará cuenta de que la mayoría de las advertencias del compilador son conservadoras: algunas advertencias podrían ser un problema en
general, pero no lo son en un programa específico debido a propiedades del programa que el compilador no es lo suficientemente "inteligente" para comprender.
Describimos el análisis de inicialización antes de su uso mediante el uso de una representación abstracta de los estados de la máquina que realizan un seguimiento solo de si una
variable se ha inicializado o no. Más precisamente, un estado será un estado de error especial llamado error o una función de los nombres de las variables al conjunto { init, uninit} con
dos valores, uno que representa cualquier valor de la variable inicializada y el otro uno no inicializado. El conjunto Expresar de abstracciones matemáticas de estados de máquina es,
por tanto,
La semántica de los programas viene dada por un conjunto de cláusulas, una para cada forma de programa, como es habitual. Para cualquier programa P,
sea C [[P]] ( error) = error. La cláusula semántica para la asignación en el estado s ≠ error Se puede escribir como
C [[x: = e]] (s) = si E [[e]] (s) = OK luego modifica (s, x, en eso) demás error
En palabras, si ejecutamos una asignación x: = e en un estado s diferente de error, entonces el resultado es un estado que tiene x inicializado o error si hay
s0 = λ v? Variables.uninit
es el estado con la variable x inicializada y todas las demás variables sin inicializar.
Las cláusulas para las secuencias P1; P2 son esencialmente sencillos. Para s ≠ error,
C [[P1; P2]] (s) = si C [[P1]] ( s) = error entonces error más C [[P2]] (C [[P1]] ( s))
La cláusula para condicional es más complicada porque nuestra herramienta de análisis no intentará evaluar una expresión booleana. En cambio, tratamos
condicional como si fuera posible ejecutar cualquiera de las ramas. (Esta es la parte conservadora del análisis). Por lo tanto, cambiamos una variable a
inicializada solo si se inicializa en ambas ramas. Si s1 y s2 son estados diferentes de error, entonces deja que s1 + s2 sea el estado
entonces error
Equipo-Fly
Equipo-Fly
Los idiomas que los humanos hablan y escriben se llaman lenguajes naturales como se desarrollaron de forma natural, sin preocuparse por la legibilidad de la
máquina. En los lenguajes naturales, hay cuatro tipos principales de oraciones: imperativo, declarativo, interrogativo y exclamatorio.
En una oración imperativa, el sujeto de la oración está implícito. Por ejemplo, el sujeto de la oración Recoge ese pez
es (implícitamente) la persona a la que se dirige el comando. Una oración declarativa expresa un hecho y puede constar de un sujeto y un verbo o
sujeto, verbo y objeto. Por ejemplo,
Los interrogativos son preguntas. Una oración exclamativa puede consistir solo en una interjección, como ¡Puaj! o ¡Guau!
En muchos lenguajes de programación, las construcciones básicas son declaraciones imperativas. Por ejemplo, una declaración de asignación como
x: = 5
es un comando a la computadora (el sujeto implícito del enunciado) para almacenar el valor5 en una ubicación determinada. Los lenguajes de programación
que establece un hecho. Una lectura de esto como una oración declarativa es que el sujeto es el nombre f y la oración sobre f es "f es una función
En programación, la distinción entre construcciones imperativas y declarativas se basa en la distinción entre cambiar un valor existente y declarar
un nuevo valor. El primero es imperativo, el segundo declarativo. Por ejemplo, considere el siguiente fragmento de programa:
{int x = 1; / * declara nuevo x * /
x = x + 1; / * asignación a x existente * / / *
}}}
Aquí, solo la segunda línea es una declaración imperativa. Este es un comando imperativo que cambia el estado de la máquina almacenando el valor 2 en
la ubicación asociada con la variable x. Las otras líneas contienen declaraciones de nuevas variables.
Un punto sutil es que la última línea del código anterior declara una nueva variable con el mismo nombre que el de una variable declarada previamente. La forma más sencilla
de comprender la distinción entre declarar una nueva variable y cambiar el valor de una antigua es mediante el cambio de nombre de la variable. Como vimos en el cálculo
lambda, un principio general de vinculación es que se puede cambiar el nombre de las variables vinculadas sin cambiar el significado de una expresión o programa. En
particular, podemos cambiar el nombre de las variables vinculadas en el fragmento de programa anterior para obtener
x = x + 1; / * asignación a x existente * / / *
}}}
(Si hubiera apariciones adicionales de x dentro del bloque interno, también las renombraríamos a z). Después de reescribir el programa a esta forma
equivalente, vemos fácilmente que la declaración de una nueva variable z no cambia el valor de ninguna de las anteriores. variable existente.
La frase Idioma funcional se utiliza para referirse a lenguajes de programación en los que la mayor parte del cálculo se realiza mediante la evaluación de
expresiones que contienen funciones. Dos ejemplos son Lisp y ML. Ambos lenguajes contienen construcciones declarativas e imperativas. Sin embargo, es
posible escribir un programa sustancial en cualquier idioma sin utilizar construcciones imperativas.
Algunas personas usan la frase lenguaje funcional para referirse a lenguajes que no tienen expresiones con efectos secundarios o cualquier otra forma de
construcción imperativa. Sin embargo, usaremos la frase más enfática lenguaje funcional puro
para lenguajes declarativos que están diseñados en torno a construcciones flexibles para definir y usar funciones. Aprendimos en
Subsección 3.4.9 que Lisp puro, basado en atom, eq, car, cdr, cons, lambda, define, es un lenguaje funcional puro. Si se agregan rplaca, que cambia el
coche de una celda, y rplacd, que cambia el cdr de una celda, entonces el Lisp resultante no es un lenguaje funcional puro.
Prueba de lenguaje declarativo: Dentro del alcance de las declaraciones específicas de x1,…, xn, todas las apariciones
de una expresión e que contiene solo variables x1,…, xn tienen el mismo valor.
Como consecuencia, los lenguajes funcionales puros tienen una propiedad de optimización útil: si la expresión e ocurre en varios lugares dentro de un ámbito
específico, esta expresión debe evaluarse solo una vez. Por ejemplo, suponga que un programa escrito en Lisp puro contiene dos apariciones de (cons ab). Un
compilador Lisp optimizado podría calcular (cons ab) una vez y usar el mismo valor en ambos lugares. Esto no solo ahorra tiempo, sino también espacio, ya que la
Transparencia referencial
En parte de la literatura académica sobre lenguajes de programación, incluidos algunos libros de texto sobre semántica de lenguajes de programación, el
concepto que se utiliza para distinguir los lenguajes declarativos de imperativos se denomina transparencia referencial. Aunque es fácil definir esta frase, es un
En lingüística, un nombre o frase nominal se considera referencialmente transparente si puede ser reemplazado por otro sintagma nominal con el
mismo referente (es decir, refiriéndose a lo mismo) sin cambiar el significado de la oración que lo contiene. Por ejemplo, considere la oración
Si Walter tiene un Maserati Biturbo, digamos, y ningún otro automóvil, entonces la oración
tiene el mismo significado porque las frases nominales (en cursiva) tienen el mismo significado. Un contraejemplo tradicional a la transparencia referencial,
atribuido al filósofo del lenguaje Willard van Orman Quine, aparece en la oración
La frase se refiere a Guillermo IV de Inglaterra y rufus significa de color rojizo o anaranjado. Si reemplazamos a William Rufus con William IV, obtenemos una
Evidentemente, el rey se llamaba Guillermo IV porque era el cuarto Guillermo, no por el color de su barba.
Volviendo a los lenguajes de programación, es tradicional decir que un lenguaje es referencialmente transparente si podemos reemplazar una expresión
por otra de igual valor en cualquier lugar de un programa sin cambiar el significado del programa. Esta es una propiedad de los lenguajes funcionales
puros.
La razón por la que la transparencia referencial es sutil es que depende del valor que asociamos con las expresiones. En lenguajes de programación imperativos,
podemos decir que una variable x se refiere a su valor o a su ubicación. Si decimos que una variable se refiere a su ubicación en la memoria, entonces los lenguajes
imperativos son referencialmente transparente, ya que reemplazar una variable con otra que nombra la misma ubicación de memoria no cambiará el significado del
programa. Por otro lado, si decimos que una variable se refiere al valor almacenado en esa ubicación, entonces los lenguajes imperativos no son referencialmente
transparentes, ya que el valor de una variable puede cambiar como resultado de la asignación.
Debate histórico
John Backus recibió el premio ACM Turing de 1977 por el desarrollo de Fortran. En su conferencia asociada con este premio, Backus argumentó que los lenguajes
de programación puramente funcionales son mejores que los imperativos. La conferencia y el documento adjunto, publicados por la Association for Computing
Machinery, ayudaron a inspirar una serie de proyectos de investigación destinados a desarrollar lenguajes de programación funcionales puros prácticos. La
premisa principal del argumento de Backus es que los programas funcionales puros son más fáciles de razonar porque podemos entender expresiones
Backus afirma que, a largo plazo, la exactitud, legibilidad y confiabilidad del programa son más importantes que otros factores como la eficiencia. Esta fue una posición
controvertida en 1977, cuando los programas eran mucho más pequeños de lo que son hoy y las computadoras eran mucho más lentas. En la década de 1990, las
computadoras finalmente alcanzaron la etapa en la que las organizaciones comerciales comenzaron a elegir métodos de desarrollo de software que valoraran más el tiempo
puros serían apreciados como superiores a los lenguajes con efectos secundarios.
Para avanzar en su argumento, Backus propuso un lenguaje de programación funcional puro llamado FP, un acrónimo de programación funcional. FP contiene una serie de
funciones básicas y un rico conjunto de formas de combinación con las que construir nuevas funciones a partir de las antiguas. Un ejemplo del artículo de Backus es un
programa simple para calcular el producto interno de dos vectores. En C, el producto interno de los vectores almacenados en las matrices ayb podría escribirse como
int i, prod;
prod = 0;
Por el contrario, la función del producto interno se definiría en FP combinando las funciones + y × (multiplicación) con operaciones vectoriales.
donde ° es la composición de la función e Insert, ApplyToAll y Transpose son operaciones vectoriales. Específicamente, la Transposición de un par de listas
produce una lista de pares y ApplyToAll aplica la operación dada a cada elemento en una lista, como la función maplist en Subsección 3.4.7 . Dadas una
operación binaria y una lista, Insertar tiene el efecto de insertar la operación entre cada par de elementos de lista adyacentes y calcular el resultado. Por
ejemplo, (Insert +) <1, 2, 3, 4> = 1 + 2 + 3 + 4 = 10, donde <1, 2, 3, 4> es la notación FP para la lista con elementos 1, 2, 3, 4.
Aunque la sintaxis de C puede parecer más clara para la mayoría de los lectores, vale la pena intentar imaginar cómo se compararían si no las hubiera visto antes.
Una faceta de la expresión FP es que todas sus partes son funciones que podemos entender sin pensar en cómo se representan los vectores en la memoria. Por
el contrario, el programa C tiene variables adicionales i y prod que no forman parte de la función que estamos tratando de calcular. En este sentido, el programa
Un punto más general sobre los programas FP es que si una expresión funcional es equivalente a otra, entonces podemos reemplazar una por la otra en cualquier programa.
Esto conduce a un conjunto de leyes algebraicas para los programas de planificación familiar. Además de las leyes algebraicas, una ventaja de los lenguajes de programación
En retrospectiva, el argumento de Backus parece más plausible que su solución. La importancia de la exactitud, legibilidad y confiabilidad del programa ha aumentado en
comparación con la eficiencia en tiempo de ejecución. La razón es que afectan la cantidad de tiempo que las personas deben dedicar al desarrollo, la depuración y el
mantenimiento del código. Cuando las computadoras se vuelven más rápidas, es aceptable ejecutar programas menos eficientes; las mejoras de hardware pueden compensar
el software. Sin embargo, los aumentos en la velocidad de las computadoras no hacen que los humanos sean más eficientes. En este sentido, Backus tenía toda la razón
sobre los aspectos de los lenguajes de programación que serían importantes en el futuro.
El lenguaje FP de Backus, por otro lado, no fue un éxito. En los años transcurridos desde su conferencia, hubo un esfuerzo por desarrollar una implementación
de FP en IBM, pero el lenguaje no se usó ampliamente. Un problema es que el lenguaje tiene una sintaxis difícil. Quizás lo más importante es que existen graves
los lenguajes de programación que admiten la modularidad y la reutilización de los componentes de la biblioteca. Sin embargo, Backus planteó un tema importante que
Un aspecto atractivo de los lenguajes funcionales puros y de los programas escritos en el subconjunto funcional puro de los lenguajes más grandes es que los
programas se pueden ejecutar al mismo tiempo. Esto es una consecuencia de la prueba de lenguaje declarativo mencionada al principio de esta subsección.
usando el ejemplo de una llamada de función f (e1,…, en), donde los argumentos de función e1,…, en son expresiones que pueden necesitar ser evaluadas.
Programación funcional: Podemos evaluar f (e1,…, en) evaluando e1,…, en en paralelo porque los valores de estas
Programación imperativa: Para una expresión como f (g (x), h (x)), la función g podría cambiar el valor de x. De ahí los
argumentos de funciones en imperativo
Los idiomas deben evaluarse en un orden secuencial fijo. Este orden restringe el uso de simultaneidad.
Backus usó el término cuello de botella de von Neumann por el hecho de que al ejecutar un programa imperativo, el cálculo debe avanzar paso a paso.
Debido a que cada paso en un programa puede depender del anterior, tenemos que pasar valores uno a la vez desde la memoria a la CPU y viceversa.
Este canal secuencial entre la CPU y la memoria es lo que llamó el cuello de botella de von Neumann.
Aunque los programas funcionales brindan la oportunidad de paralelismo, y el paralelismo es a menudo una forma efectiva de aumentar la velocidad de cálculo,
es difícil aprovechar de manera efectiva el paralelismo inherente. Un problema que es bastante fácil de entender es que los programas funcionales a veces
proporcionan demasiado paralelismo. Si todos los cálculos posibles se realizan en paralelo, se ejecutarán muchos más pasos de cálculo de los necesarios. Por
si e1 entonces e2 si no e3
implicará evaluar las tres expresiones. Finalmente, cuando se encuentre el valor de e1, uno de los otros cálculos resultará irrelevante. En este caso, se
puede dar por terminado el cálculo irrelevante, pero mientras tanto, se habrán dedicado recursos al cálculo que al final no importa.
En un programa grande, es fácil generar tantas tareas paralelas que el tiempo de configuración y cambio entre procesos paralelos restará eficacia al cálculo. En
general, los lenguajes de programación paralelos necesitan proporcionar alguna forma para que un programador especifique dónde puede ser beneficioso el
paralelismo. Las implementaciones paralelas de lenguajes funcionales a menudo tienen el inconveniente de que el programador tiene poco control sobre la
¿Tienen los lenguajes de programación puramente funcionales ventajas prácticas significativas sobre los lenguajes imperativos?
Aunque hemos considerado muchas de las ventajas potenciales de los lenguajes FP puros en esta sección, no tenemos una
respuesta definitiva. Desde un punto de vista teórico, los lenguajes FP son tan buenos como los lenguajes de programación imperativos. Esto se puede
demostrar cuando se hace una traducción de programas C a programas FP, expresiones de cálculo lambda o Lisp puro. La semántica denotacional
Sin embargo, para responder a la pregunta en la práctica, necesitaríamos realizar grandes proyectos en un lenguaje funcional y ver si es posible producir software
utilizable en un período de tiempo razonable. IBM realizó algunos trabajos para responder a esta pregunta en el proyecto FP (que se canceló poco después de la
jubilación de Backus). En otros laboratorios de investigación y universidades todavía se están realizando esfuerzos adicionales con otros lenguajes como Haskell y
Lazy ML. Aunque la mayor parte de la programación se realiza en lenguajes imperativos, es ciertamente posible que, en algún momento futuro, los lenguajes FP
puros o en su mayoría puros se vuelvan más populares. Ya sea que eso suceda o no, los proyectos de FP han generado muchas ideas de diseño de lenguajes
interesantes y técnicas de implementación que han influido más allá de la programación funcional pura.
Equipo-Fly
Equipo-Fly
lambda,
semántica denotacional,
Un compilador estándar transforma un programa de entrada, escrito en un idioma de origen, en un programa de salida, escrito en un idioma de destino.
Este proceso está organizado en una serie de seis fases, cada una de las cuales involucra propiedades más complejas de los programas. Las primeras
tres fases, análisis léxico, análisis sintáctico y análisis semántico, organizan los símbolos de entrada en tokens significativos, construyen un árbol de
análisis sintáctico y determinan las propiedades del programa dependientes del contexto, como la concordancia de tipos de operadores y operandos. (El
nombre análisis semántico se usa comúnmente en los libros de compiladores, pero es algo engañoso ya que sigue siendo un análisis del árbol de análisis
para condiciones sintácticas sensibles al contexto). Las últimas tres fases, generación de código intermedio, optimización y generación de código de
destino,
El cálculo lambda proporciona un mecanismo de notación y evaluación simbólica que es útil para estudiar algunas propiedades de los lenguajes de programación.
En la sección sobre cálculo lambda, discutimos la vinculación y α conversión. Los operadores de enlace surgen en muchos lenguajes de programación en forma
de declaraciones y en listas de parámetros de funciones, módulos y plantillas. Las expresiones lambda se evalúan simbólicamente mediante el uso de β- reducción,
con el argumento de función sustituido en lugar del parámetro formal. Este proceso se asemeja a la expansión macro y la función en el revestimiento, dos
transformaciones que suelen realizar los compiladores. Aunque el cálculo lambda es un sistema muy simple, teóricamente es posible escribir todas las funciones
computables en el cálculo lambda. El cálculo lambda sin tipo, que discutimos, puede extenderse con sistemas de tipos para producir varias formas de cálculo
La semántica denotacional es una forma de definir los significados de los programas especificando el valor matemático, la función o la función de las funciones que
denota cada construcción. La semántica denotacional es una forma abstracta de analizar programas porque no considera cuestiones como el tiempo de ejecución y
los requisitos de memoria. Sin embargo, la semántica denotacional es útil para razonar sobre la corrección y se ha utilizado para desarrollar y estudiar métodos de
análisis de programas que se utilizan en compiladores y entornos de programación. Algunos de los ejercicios al final del capítulo presentan aplicaciones para la
verificación de tipos, el análisis de inicialización antes de su uso y el análisis de seguridad simplificado. Desde un punto de vista teórico, la semántica denotacional
muestra que todo programa imperativo puede transformarse en un programa funcional equivalente.
En programas funcionales puros, las expresiones sintácticamente idénticas dentro del mismo ámbito tienen valores idénticos. Esta propiedad permite ciertas
optimizaciones y permite ejecutar subprogramas independientes en paralelo. Debido a que los lenguajes funcionales son teóricamente tan poderosos como los
lenguajes imperativos, discutimos algunas de las diferencias pragmáticas entre los lenguajes funcionales e imperativos. Aunque los lenguajes funcionales pueden ser
más sencillos de razonar en ciertos aspectos, los lenguajes imperativos a menudo facilitan la escritura de programas eficientes. Aunque Backus sostiene que los
programas funcionales pueden eliminar el cuello de botella de von Neumann, la ejecución práctica paralela de programas funcionales no ha demostrado ser tan
Equipo-Fly
Equipo-Fly
EJERCICIOS
Dibuje el árbol de análisis sintáctico para la derivación de la expresión 25 dada en Subsección 4.1.2 . ¿Existe otra derivación para 25? ¿Hay otro árbol de
análisis?
Dibuje arboles de análisis para las siguientes expresiones, asumiendo la gramática y la precedencia descritas en Ejemplo 4.2 :
una. 1-1 * 1.
B. 1-1 + 1.
Utilice la reducción del cálculo lambda para encontrar una expresión más corta para ( λ X . λ y.xy) ( λ x.xy). Empiece por cambiar el nombre de las variables vinculadas. Debe hacer todas las
reducciones posibles para obtener la expresión más corta posible. ¿Qué sale mal si no cambia el nombre de las variables vinculadas?
función f (x)
devolver x + 4
fin;
función g (y)
volver 3 años
fin;
f (g (1));
Reduzca la expresión a una forma normal de dos formas diferentes, como se describe a continuación.
una. Reducir la expresión eligiendo, en cada paso, la reducción que elimina un λ hasta el
izquierda como sea posible.
Aquí hay una expresión lambda "azucarada" que usa declaraciones let:
dejar componer = λ F. λ gramo. λ xf (gx) en
dejar h = λ xx + x en
componer hh 3
( λ componer.
( λ h. componer hh 3) λ x .x + x)
λ f. λ g. λ X . f (gx).
Esto está escrito con los mismos nombres de variable que los de la forma let para facilitar la lectura de la expresión.
Simplifique la expresión lambda desazucarada mediante reducción. Escribe una o dos oraciones que expliquen por qué la expresión simplificada es la
Un programador tiene dificultades para depurar el siguiente programa en C. En teoría, en una máquina "ideal" con memoria infinita, este programa se ejecutaría para
siempre. (En la práctica, este programa falla porque se queda sin memoria, ya que se requiere espacio adicional cada vez que se realiza una llamada a una función).
int f (int (* g) (…)) {/ * g apunta a una función que devuelve un int * / return g (g);
int main () {
int x;
x = f (f);
return 0;
Explique el comportamiento del programa traduciendo la definición de f al cálculo lambda y luego reduciendo la aplicación f (f). Este programa
asume que el verificador de tipos no verifica los tipos de argumentos de las funciones.
En el cálculo lambda puro, el orden de evaluación de las subexpresiones no afecta el valor de una expresión. Lo mismo es cierto para Lisp puro: si una
expresión Lisp pura tiene un valor bajo el intérprete Lisp ordinario, entonces cambiar el orden de evaluación de los subterráneos no puede producir un valor
diferente.
Para dar un ejemplo concreto, considere la siguiente sección del código Lisp:
(definir a (…))
(definir b (…))
...
...
(f e1 e2 e3)
(f e1 e2 e3)
es evaluar los argumentos e1 e2 e3 de izquierda a derecha y luego pasar esta lista de valores a la función f.
Da un ejemplo de expresiones Lisp e1 e2 e3, posiblemente usando funciones rplaca o rplacd con efectos secundarios, de modo que evaluar expresiones de
El texto describe una semántica denotacional para el lenguaje imperativo simple dado por la gramática
Cada programa denota una función de estados a estados, en el que un Expresar es una función de variables a valores.
una. Calcula el significado C [[x: = 1; x: = x + 1;]] ( s 0) aproximadamente con el mismo detalle que el de los ejemplos dados en el
B. La semántica denotacional se utiliza a veces para justificar formas de razonar sobre programas. Escriba algunas oraciones,
En el texto se presenta una semántica denotacional no estándar que describe el análisis inicializar antes de usar.
B. Calcula el significado
en estado s con s (x) = inicio, s (y) = inicio, y s (v) = uninit o cualquier otra variable v.
Este problema pregunta por una semántica no estándar que caracteriza una forma de análisis de tipos para expresiones dadas por la siguiente gramática:
En la expresión let, que es una forma de declaración local, el tipo τ podría ser cualquiera En t o bool. El significado V [[e]] ( η) de una expresión mi depende de un
medio ambiente.
Intuitivamente V [[e]] ( η) = entero significa que el valor de la expresión mi es un número entero (en el entorno η) y V [[e]] ( η) =
Aquí están las cláusulas semánticas para las primeras formas de expresión.
V [[ 0]] ( η) = entero,
V [[ 1]] ( η) = entero,
V [[ cierto]]( η) = booleano
V [[ falso]]( η) = booleano
El valor de una variable en algún entorno es simplemente el valor que el entorno le da a esa variable:
V [[x]] ( η) = η ( X)
Por adición, el valor será error de tecleado a menos que ambos operandos sean números enteros:
Debido a que las declaraciones implican establecer los tipos de variables y los tipos de variables se registran en un entorno, la semántica de las
declaraciones implica cambiar el entorno. Antes de dar la cláusula para declaraciones, definimos la notación η [ X → σ] para un entorno similar a η, excepto
como la expresión x + 1 produce un error de tipo si se evalúa X produce un error de tipo. Por otro lado,
Preguntas:
una. Muestre cómo calcular el significado de la expresión si es falso, luego 0 más 1 en el entorno η 0
B. Suponer mi 1 y mi 2 son expresiones con V [[e 1]] η = entero y V [[e 2]] η = booleano en todos los entornos. Muestre cómo calcular
en mi 2 utilizando la siguiente solución parcial (es decir, complete las partes que faltan en esta definición):
En un lenguaje "perezoso", evaluamos una llamada a función f (e) pasando el sin evaluar argumento al cuerpo de la función. Si se necesita el valor del
argumento, entonces se evalúa como parte de la evaluación del cuerpo de f. Por ejemplo, considere
la función g definida por
fun g (x, y) = si = x = 0
entonces 1
de lo contrario si x + y = 0
luego 2
más 3;
En un lenguaje perezoso, evaluamos la llamada g (3,4 + 2) pasando alguna representación de las expresiones 3 y 4 + 2 a g. Evaluamos la prueba x = 0
usando el argumento 3. Si fuera cierto, la función devolvería 1 sin calcular nunca 4 + 2. Debido a que la prueba es falsa, la función debe evaluar x + y, lo
que ahora hace que se evalúe el parámetro real 4 + 2. Algunos ejemplos de lenguajes funcionales perezosos son Miranda, Haskell y Lazy ML; estos
idiomas no tienen asignación u otras características imperativas con efectos secundarios.
Si estamos trabajando en un lenguaje funcional puro sin efectos secundarios, entonces para cualquier llamada de función f (e1, e2), podemos evaluar e1 antes de
e2 o e2 antes de e1. Porque ninguno puede tener efectos secundarios, ninguno puede afectar el valor del otro. Sin embargo, si el lenguaje es flojo, es posible que
no necesitemos evaluar ambas expresiones. Por tanto, algo puede salir mal si evaluamos ambas expresiones y una de ellas no termina.
Como argumenta Backus en su conferencia del Premio Turing, una ventaja de los lenguajes funcionales puros es la posibilidad de evaluación paralela. Por ejemplo, al
evaluar una función llamada f (e1, e2) podemos evaluar tanto e1 como e2 en paralelo. De hecho, también podríamos empezar a evaluar el cuerpo de f en paralelo.
una. Suponga que evaluamos g (e1, e2) comenzando a evaluar g, e1 y e2 en paralelo, donde g es la función definida anteriormente. ¿Es
posible que un proceso tenga que esperar a que se complete otro? ¿Cómo puede suceder esto?
B. Ahora, suponga que el valor de e1 es cero y la evaluación de e2 termina con un error. En el orden de evaluación normal (es
decir, ansioso) que se usa en C y otros lenguajes comunes, la evaluación de la expresión g (e1, e2) terminará con error. ¿Qué
C. Suponga que desea el mismo valor, para cada expresión, como evaluación diferida, pero desea evaluar expresiones en
paralelo para aprovechar su nuevo multiprocesador de bolsillo. ¿Qué acciones deberían suceder si evalúa g (e1, e2) iniciando
D. Supongamos ahora que el lenguaje contiene efectos secundarios. ¿Y si e1 es z y e2 contiene una asignación a z; ¿Todavía puedes
evaluar los argumentos de g (e1, e2) en paralelo? ¿Cómo? ¿O por qué no?
Varios de los llamados idiomas de asignación única se han desarrollado a lo largo de los años, muchos diseñados para computación científica paralela. Las condiciones de
asignación única también se utilizan en la optimización de programas y en los lenguajes de descripción de hardware. Las condiciones de asignación única surgen en el
hardware ya que solo puede ocurrir una asignación a cada variable por ciclo de reloj.
Un ejemplo de un idioma de asignación única es SISAL, que significa flujos e iteración en un lenguaje de asignación única. Otro es SACO, o asignación
única C. Los programas en idiomas de asignación única deben cumplir la siguiente condición:
Condición de asignación única: Durante cualquier ejecución del programa, a cada variable se le puede asignar un valor solo una vez,
porque solo una rama del if-then-else se ejecutará en cualquier ejecución del programa. El programa x = 2; lazo
Siempre; x = 3 también satisface la condición porque ninguna ejecución completará ambas asignaciones.
Los lenguajes de asignación única a menudo tienen construcciones de bucle especializadas, ya que de lo contrario sería imposible ejecutar una asignación dentro de un
cuerpo de bucle que se ejecuta más de una vez. Aquí hay un formulario, de SISAL:
para? rango?
?¿cuerpo?
final para
Un ejemplo que ilustra esta forma es el siguiente ciclo, que calcula el producto de puntos (o interior) de dos vectores:
para yo en 1, tamaño
Este bucle es paralelizable porque diferentes productos x [i] * y [i] se pueden calcular en paralelo. Un programa SISAL típico tiene un bucle externo secuencial que
Suponga que tiene la tarea de crear un compilador de paralelización para un lenguaje de asignación única. Suponga que los programas que compila satisfacen la
condición de asignación única y no contienen ninguna bifurcación de proceso explícita u otras instrucciones de paralelización. Su implementación debe encontrar
partes de programas que se puedan ejecutar de manera segura en paralelo, produciendo los mismos valores de salida que si el programa se ejecutara
Suponga por simplicidad que a cada variable se le asigna un valor antes de que el valor de la variable se use en una expresión. Suponga también que no existe una
x = 5;
y = f (g (x), h (x));
en paralelo. Más específicamente, suponga que su implementación programará los siguientes procesos de alguna
manera:
proceso1 - establecer x en 5
3 - llamar a h (x)
prueba y == 5
Para cada proceso, enumere los procesos que este proceso debe esperar y enumere los procesos que se pueden ejecutar en paralelo
con él. Para simplificar, suponga que no se puede ejecutar una llamada hasta que se hayan evaluado los parámetros y suponga que los
procesos 6 y 7 están no dividido en procesos más pequeños que ejecutan las llamadas pero no se asignan a z. Suponga que el parámetro
B.
Si divide aún más el proceso 6 en dos procesos, uno que llama a g (x) y otro que asigna a z,
y de manera similar dividir el proceso 7 en dos procesos, ¿puede ejecutar las llamadas g (x) y h (x) en paralelo? ¿Podría su
C. ¿La ejecución paralela de los procesos que describe en las partes (a) y (b), si la hubiera, sería correcta si el programa no satisface la
D. ¿Es decidible la condición de asignación única? Específicamente, dado un programa escrito en un subconjunto de
C, para ser más concretos, ¿es posible que un compilador decida si este programa satisface la condición de asignación única?
Explica por qué o por qué no. Si no es así, ¿puede pensar en una condición decidible que implique la condición de asignación
mi. Suponga que un idioma de asignación única no tiene operaciones de efectos secundarios aparte de la asignación. ¿Este idioma pasa
Se escriben muchas más líneas de código en lenguajes imperativos que en lenguajes funcionales. Esta pregunta le pide que especule sobre las razones de
esto. Primero, sin embargo, una explicación de cuáles son las razones no son es dado:
No es porque los lenguajes imperativos puedan expresar programas que no son posibles en lenguajes funcionales. Ambas clases
se pueden completar con Turing y, de hecho, se puede utilizar una semántica denotacional de un lenguaje imperativo para
No es por sintaxis. No hay ninguna razón por la que un lenguaje funcional no pueda tener una sintaxis similar a la de C, por
ejemplo.
No es porque los lenguajes imperativos siempre se compilan mientras que los lenguajes funcionales siempre se interpretan. Lo básico es
imperativo, pero generalmente se interpreta, mientras que Haskell es funcional y generalmente se compila.
Para este problema, considere las propiedades generales de los lenguajes imperativos y funcionales. Suponga que un lenguaje funcional admite funciones de
orden superior y recolección de basura, pero no asignación. Para el propósito de esta pregunta, un lenguaje imperativo es un lenguaje que apoya la
asignación, pero no las funciones de orden superior o la recolección de basura. Use solo estas suposiciones sobre lenguajes imperativos y funcionales en su
respuesta.
una. ¿Existen razones inherentes por las que los lenguajes imperativos son superiores a los funcionales para la mayoría de las tareas de
B. ¿Qué variedad (imperativa o funcional) es más fácil de implementar en máquinas con discos y tamaños de memoria limitados? ¿Por qué?
C. ¿Qué variedad (imperativa o funcional) requeriría ejecutables más grandes cuando se compilaran? ¿Por qué? ¿Qué consecuencia podrían haber
D. tenido estos hechos en los primeros días de la informática? ¿Siguen siendo válidas estas preocupaciones hoy en día?
mi.
Puede ser difícil escribir programas que se ejecuten en varios procesadores al mismo tiempo porque una tarea debe descomponerse en subtareas
independientes y los resultados de las subtareas a menudo deben combinarse en ciertos puntos del cálculo. Durante los últimos 20 años, muchos investigadores
han intentado desarrollar lenguajes de programación que facilitarían la escritura de programas concurrentes. En su Conferencia de Turing, Backus abogó por la
programación funcional porque creía que los programas funcionales serían más fáciles de razonar y porque creía que los programas funcionales podrían
Explique por qué los lenguajes de programación funcional no brindan una solución completa al problema de escribir programas que se puedan ejecutar de manera
Equipo-Fly
Equipo-Fly
VISIÓN GENERAL
Los lenguajes de programación tipo Algol evolucionaron en paralelo con la familia de lenguajes Lisp, comenzando con Algol 58 y Algol 60 a finales de la década de
1950. Los lenguajes de programación similares a Algol más destacados son Pascal y C, aunque C difiere de la mayoría de los lenguajes similares a Algol en algunos
aspectos importantes.
En este capítulo, analizamos algunos de los lenguajes históricamente importantes de la familia Algol, incluidos Algol 60, Pascal y C. Debido a que muchas de las
características centrales de la familia Algol se usan en ML, luego usamos el lenguaje de programación ML para discutir algunos conceptos importantes con más
detalle. La sección ML de este capítulo también es una referencia útil para capítulos posteriores que utilizan ejemplos ML para ilustrar conceptos que no se
encuentran en C.
Hay muchos lenguajes relacionados con Algol que no tenemos tiempo de cubrir, como Algol 58, Algol W, Euclid, EL1, Mesa, Modula-2, Oberon y
Modula-3. Discutiremos Modula y módulos en Capítulo 9 .
Equipo-Fly
Equipo-Fly
Se desarrollaron varias ideas importantes sobre el lenguaje en la familia Algol, que comenzó con el trabajo en Algol 58 y Algol 60 a fines de la década de
1950. La familia Algol se desarrolló en paralelo con los lenguajes Lisp y condujo al desarrollo tardío de ML y Modula.
Las principales características de la familia Algol son la secuencia familiar de enunciados separados por dos puntos que se usa en la mayoría de los lenguajes en la actualidad, la estructura
5.1.1 Algol 60
Algol 60 fue diseñado entre 1958 y 1963 por un comité que incluía a muchos pioneros informáticos importantes, como John Backus (diseñador de Fortran),
John McCarthy (diseñador de Lisp) y Alan Perlis. Algol 60 estaba destinado a ser un lenguaje de uso general, lo que en ese momento significaba que se hacía
hincapié en las aplicaciones científicas y numéricas. En comparación con Fortran, Algol 60 proporcionó mejores formas de representar estructuras de datos y,
como LISP, permitió que las funciones se llamaran de forma recursiva. Hasta el desarrollo de Pascal, Algol 60 era el estándar académico para describir