Está en la página 1de 99

Equipo-Fly

..
Conceptos en lenguajes de programación

por John C. Mitchell ISBN: 0521780985

Prensa de la Universidad de Cambridge © 2003 (529 páginas)

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

Conceptos en lenguajes de programación

Prefacio

Parte 1- Función y fundamentos

Capítulo 1 - Introducción

Capítulo 2 - Computabilidad

Capítulo 3 - Lisp: funciones, recursividad y listas

Capítulo 4 - Fundamentos

Parte 2- Procedimientos, tipos, gestión de la memoria y control

Capítulo 5 - La familia Algol y ML

Capítulo 6 - Sistemas de tipos e inferencia de tipos

Capítulo 7 - Alcance, funciones y gestión de almacenamiento

Capítulo 8 - Control en lenguajes secuenciales

Parte 3- Modularidad, abstracción y programación orientada a objetos

Capítulo 9 - Abstracción y modularidad de datos

Capítulo 10 - Conceptos en lenguajes orientados a objetos

Capítulo 11 - Historia de los objetos: Simula y Smalltalk

Capítulo 12 - Objetos y eficiencia en tiempo de ejecución: C ++

Capítulo 13 - Portabilidad y seguridad: Java

Parte 4- Programación lógica y de simultaneidad

Capítulo 14 - Programación concurrente y distribuida

Capítulo 15 - El paradigma y el prólogo de la programación lógica

Apéndice A - Ejemplos de programas adicionales

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

las ventajas y desventajas de los lenguajes de programación que utilizan.

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

Conceptos en lenguajes de programación


John C. Mitchell

Universidad Stanford

PRENSA DE LA UNIVERSIDAD DE CAMBRIDGE

Publicado por el Sindicato de Prensa de la Universidad de Cambridge The Pitt Building,


Trumpington Street, Cambridge, Reino Unido

Prensa de la Universidad de Cambridge

El edificio de Edimburgo, Cambridge CB2 2RU, Reino Unido

40 West 20th Street, Nueva York, NY 10011-4211, EE. UU.

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

Copyright © 2002 Cambridge University Press

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.

Publicado por primera vez en 2002

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

en publicación de la Biblioteca del Congreso disponibles.

0-521-78098-5

Conceptos en lenguajes de programación

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

apreciación de las ventajas y desventajas de los lenguajes de programación que utilizan.

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

de la OTAN sobre técnicas de ingeniería de software, Roma, 1969

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

lenguaje afecta el desarrollo del programa. El texto se divide en cuatro partes:

Parte 1 : Funciones y Fundamentos

Parte 2 : Procedimientos, tipos, gestión de memoria y control

Parte 3 : Modularidad, abstracción y programación orientada a objetos

Parte 4 : Programación lógica y de simultaneidad

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

Simula, Smalltalk, C ++ y Java.

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

Parte 1: Función y fundamentos

Capítulo 1 : Introducción

Capítulo 2 : Computabilidad

Capítulo 3 : Lisp: funciones, recursividad y listas

Capítulo 4 : Fundamentos

Equipo-Fly
Equipo-Fly

Capítulo 1: Introducción

"El medio es el mensaje"


- - Marshall McLuhan

1.1 PROGRAMACIÓN DE IDIOMAS

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

más difíciles del diseño del lenguaje:


Espero que disfrute usando este libro. Al comienzo de cada capítulo, he incluido imágenes de personas involucradas en el desarrollo o análisis de

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

inevitablemente parte de la personalidad de sus diseñadores.

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.

1.2.1 Objetivos generales

En este libro tenemos los siguientes objetivos generales:

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.

1.2.2 Temas específicos

A continuación, se muestran algunos temas específicos que se abordan repetidamente en el texto:

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

los errores no ocurran realmente cuando se ejecuta el programa.

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

1.3 HISTORIAL DEL LENGUAJE DE PROGRAMACIÓN

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

programación en lenguaje ensamblador alrededor de 1960.

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

importantes de la década de 1950 fueron Fortan y Cobol.

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

valor de i al doble del valor de j es i + 2 * j. Antes del desarrollo

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,

hardware más económico y potente, y un creciente interés en la corrección, la seguridad y la interoperabilidad.

Equipo-Fly
Equipo-Fly

1.4 ORGANIZACIÓN: CONCEPTOS E IDIOMAS

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:

Idioma Expresiones Funciones Montón Excepciones Módulos Objetos Hilos

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

manera orientada a conceptos.

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.

El texto se divide en cuatro partes:

Parte 1 : Funciones y fundamentos ( Capítulos 1 - 4 )

Parte 2 : Procedimientos, tipos, gestión de la memoria y control (5-8)


Parte 3 : Modularidad, abstracción y programación orientada a objetos ( 9 - 13 )

Parte 4 : Programación lógica y de simultaneidad ( 14 y 15 )

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

algunas discusiones y comparaciones utilizando la sintaxis C.

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

paradigma de programación más prominente, en la mayoría de

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

otros capítulos del libro.

2.1 FUNCIONES PARCIALES Y COMPUTABILIDAD

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

implementar en computadoras reales.

2.1.1 Expresiones, errores y no terminación

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

computadora idealizada es que es muy simple y muy poderosa.

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

otras llamando la atención sobre su sexualidad y su impacto en su carrera profesional.

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.

No terminación: La evaluación de la expresión continúa indefinidamente.

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

f (x: int) = si x = 0 entonces 0 si no x + f (x-2)

É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.

2.1.2 Funciones parciales


Una función parcial es una función que está definida en algunos argumentos y no definida en otros. Esto es normalmente lo que se entiende por función en

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

los argumentos en el conjunto A, la función F produce valores del conjunto B. El conjunto A

se llama el dominio de F, y el set B se llama el abarcar o la codominio de F.

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:

A función f: A → B es un conjunto de pares ordenados F ⊆ A × B que cumpla las siguientes condiciones:

1. ¿Si? x, y? ? F y ? x, z ?? F, entonces y = z.

2. Por cada X ? A, existe y? B con ? x, y ?? F.

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

tiene al menos un valor para cada argumento en su dominio.

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:

A función parcial f: A → B es un conjunto de pares ordenados F ⊆ A × B satisfaciendo la condición anterior

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.

Los programas definen funciones parciales

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:

f (x: int) = ifx = 0 entonces 0 si no x + f (x-2);

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

matemática F, expresado aquí como un conjunto de pares ordenados:

f = {? x, y? | x es positivo e incluso, y = 0 + 2 + 4 + ... + x}.

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

cualquier X ? A como entrada, se detiene con y = f (x) como salida.

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

expresivos de los diferentes lenguajes de programación.

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

se pueden implementar, ni siquiera en principio.

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

comprenda la idea detrás de ella.

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

entradas, ambas cadenas, y tiene la siguiente salida:


Una parte importante de esta especificación para Q es eso Q (P, x) siempre se detiene por cada PAG y X.

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

programa que funcione de la siguiente manera:

D (P) = si Q (P, P) = se detiene entonces corre para siempre demás detener.

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

archivo. Entonces podemos compilar D y correr D

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;

mientras (i! = f (i)) i = g (i);

printf (... i ...);

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

2.2 RESUMEN DEL CAPÍTULO

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

conceptos principales de esta breve descripción general:

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

problema de la detención es indecidible.

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

y Ullman (Addison Wesley, 2001) o Introducción a la

Teoría de la computación por Sipser (PWS, 1997).

Equipo-Fly
Equipo-Fly

EJERCICIOS

2.1 Funciones parciales y totales

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

enteros mayores que 0 y no se define en los números enteros menores o iguales a 0.

Funciones:

una. f (x) = si x + 2> 3 entonces x * 5 si no x / 0

B. f (x) = si x <0 entonces 1 si no f (x - 2)

C. f (x) = si x = 0 entonces 1 si no f (x - 2)

2.2 Problema de detención si no hay entrada

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

argumento que no es un programa sintácticamente correcto.

¿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.

Simplemente pregunta si es posible hacerlo.

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.

2.3 Problema de detención en todas las entradas

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

problema 2.2, ¿puede resolver el problema de detención utilizando Halt ∀?

Equipo-Fly
Equipo-Fly

Capítulo 3: Funciones Lisp, recursividad y listas

VISIÓN GENERAL

Lisp es el medio elegido por las personas que disfrutan del estilo libre y la flexibilidad.

- - Gerald J. Sussman

Un programador Lisp conoce el valor de todo, pero el costo de nada.


- - Alan Perlis

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

3.1 HISTORIA DE LISP

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

variedad de entornos informáticos.

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

la década de 1950 y principios de la de 1960. Entre otras contribuciones fundamentales al campo,


McCarthy participó en el diseño de Algol 60 y formuló el concepto de tiempo compartido en un memorando de 1959 al director del Centro 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

expresión en los medios electrónicos.

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

3.2 BUEN DISEÑO DE LENGUAJE

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.

Máquina abstracta: Existe un modelo de ejecución de programa simple e inequívoco.

Fundamentos teóricos: La comprensión teórica fue la base para incluir ciertas capacidades y omitir otras.

Estos puntos se desarrollan en las subsecciones siguientes.

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

simbólica de la función como entrada.

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

diseño de otros lenguajes de programación:

Ceceo Computación simbólica, lógica, programación experimental

C Sistema operativo Unix

Simula Simulación

PL / 1 Intenté resolver todos los problemas de programación; no exitoso o influyente

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.

Modelo de ejecución del programa

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

ejecución asociados con el diseño de otros lenguajes de programación:

Fortran Máquina de registro plano Sin pilas, sin recursividad Memoria organizada como matriz lineal

Familia Algol Pila de registros de activación Almacenamiento dinámico

Charla Objetos, comunicando por mensajes

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

funciones computables basada en el cálculo lambda.

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

consecuencias para el diseño de lenguajes de programación.

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

programación. Un resumen del cálculo lambda aparece en Sección 4.2 .

Equipo-Fly
Equipo-Fly

3.3 DESCRIPCIÓN GENERAL DEL LENGUAJE BREVE

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

difieran de los que puede haber usado en la programación Lisp anterior.

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

es extremadamente simple. Para realizar el análisis (ver Sección

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

Lisp, con la forma infija correspondiente para comparar.

Notación de prefijo Lisp Notación infija

(+12345) (1 + 2 + 3 + 4 + 5) ((2 +

(* (+23) (+ 45)) 3) * (4 + 5)) f (x, y)

(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

4.1 si no está familiarizado con las gramáticas):

<atom> :: = <smbl> | <num>

<smbl> :: = <char> | <smbl> <char> | <smbl> <digit> <num> :: = <digit>

| <número> <dígito>

Un átomo que se usa para algunos propósitos especiales es el átomo nulo.

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

particular, como se describe en Subsección 3.4.3 .

Funciones y formas especiales

Las funciones básicas de Historical Lisp son las operaciones

contras coche cdr eq atom

en pares y átomos, junto con las funciones generales de programación

cond lambda definir cotización eval

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

rplaca rplacd conjunto setq


Discutimos estos en Subsección 3.4.9 . Lisp con una o más de estas funciones a veces se llama Lisp impuro.

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.

En general, evaluamos una expresión Lisp

(función arg1... argn)

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

formas especiales. Por ejemplo, evaluamos una expresión condicional

(cond (p1 e1) ... (pn en))

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:

(citar contras) expresión cuyo valor es el átomo "contras"

(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:

(+ 4 5) expresión con valor 9

(+ (+ 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

comillas (+ 1 2)) una lista (+ 1 2)

'(+ 1 2) igual que (cita (+ 1 2))

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

nombre encontrar que se está definiendo y la expresión para la función de búsqueda:

(definir buscar (lambda (xy) (cond

((igual y nil) nil)

((igual x (coche y)) x) (verdadero

(encontrar x (cdr y)))

)))

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

(busque 'manzana' (pera, melocotón, manzana, higo, plátano))

Alcance estático y dinámico

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

Scheme y Lisp. A continuación, se muestra un resumen de algunas de las diferencias de notación:

Ceceo Esquema Ceceo Esquema

defun definir rplacaset ¡auto!

defvar definir rplacdset cdr!

coche, cdr coche, cdr mapcar mapa

contras contras t #t

nulo ¿nulo? nulo #f

átomo ¿átomo? nulo nulo

eq, igual eq ?, igual? nulo '()

Setq ¡colocar! progn comenzar

cond… t cond ... más

Equipo-Fly
Equipo-Fly

3.4 INNOVACIONES EN EL DISEÑO DE LISP

3.4.1 Declaraciones y expresiones

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.

Estos se pueden resumir de la siguiente manera:

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

producir un valor para la expresión.

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

si f (2) = 2 o f (3) = 3 entonces 4 si no 4

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

lenguaje de programación que intentó poner en práctica esta posibilidad teórica.

3.4.2 Expresiones condicionales

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

de una propuesta de McCarthy, modificada por una sugerencia sintáctica de Backus.

La expresión condicional Lisp

(cond (p1 e1) ... (pn en))

podría escribirse como

si p1 entonces e1

de lo contrario, si p2 entonces e2

...

de lo contrario si pn entonces en

demás sin valor

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,

entonces la expresión condicional no tiene valor. Si alguno de los

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

p1, ..., pn son todos nulos

p1, ..., pi falso y pi + 1 indefinido

p1, ..., pi falso, pi + 1 verdadero y ei + 1 indefinido

A continuación, se muestran algunos ejemplos de expresiones condicionales y sus valores:


(cond ((<2 1) 2) ((<1 2) 1)) tiene valor 1 (cond ((<2 1) 2)

((<3 2) 3)) no está definido

(cond (diverge 1) (verdadero 0)) no está definido, si diverge no termina (cond (verdadero 0)

(diverge 1)) tiene valor 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 ||).

3.4.3 La máquina abstracta Lisp

¿Qué es una máquina abstracta?

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

asociada a un lenguaje de programación específico.

La máquina abstracta de Lisp

La máquina abstracta para Pure Lisp tiene cuatro partes:

A Expresión Lisp ser evaluado.

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

expresión actual que se va a evaluar o en las expresiones restantes del programa.

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

programa después de que se evalúe esa expresión.

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:

Contras de las células

proporcionan un modelo simple de memoria en la máquina, se pueden

implementar de manera eficiente y

no están estrechamente vinculados a una arquitectura informática particular.

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

será verdadero o falso).

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:

1. Asignar nueva celda c.

2. Establezca la parte de dirección de c para que apunte al valor de x.

3. Establezca la parte decreciente de c para que apunte al valor de y.

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

a vestido r egister de c. De lo contrario, la aplicación del coche da como resultado un error.

cdr, una función con un argumento: si el argumento es una celda de cons, entonces devuelve el C ontenciones de la

D ecremento r egister de c. De lo contrario, la aplicación de cdr da como resultado un error.

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

contiene la representación de un átomo).

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

como ((A. B). (A. B)).

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 .

Representación de listas por celdas de contras

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.

3.4.4 Programas como datos

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:

(definir sustituto (lambda (exp1 var exp2)

(cond ((atom exp2) (cond ((eq exp2 var) exp1) (true exp2))) (true (cons (sustituto

exp1 var (car exp2))

(sustituto exp1 var (cdr exp2)))))))

(definir sustituto-y-eval (lambda (xyz) (eval (sustituto xyz))))

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.

3.4.5 Expresiones de funciones

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.

Una expresión de función Lisp tiene la forma

(¿lambda (? ¿parámetros?)? cuerpo_función?)


donde? parámetros? es una lista de identificadores y? function_body? es una expresion. Por ejemplo, una función que coloca su argumento en una lista seguida de

los átomos A y B puede escribirse como

(lambda (x) (contras x '(AB)))

Otro ejemplo es esta función que, con una función primitiva utilizada para la suma, suma sus dos argumentos:

(lambda (xy) (+ xy))

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

también podían escribirse en el cálculo lambda y viceversa.

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 cálculo lambda, pero como

(lambda (x) (+ (cuadrado x) 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

parámetro real cuando se aplique la función. Más específicamente, considere la expresión

((lambda (x) (+ (cuadrado x) y)) 4)


que aplica una función al entero 4. Esta expresión sólo se puede evaluar en un contexto en el que y ya tiene un valor. El valor de esta expresión será 16
más el valor de y. Esto se calculará mediante la evaluación de (más (cuadrado x) y) con x establecido en 4. Se dice que el identificador y es un variable
global en esta expresión de función porque su valor debe recibir un valor por alguna definición fuera de la expresión de función. Puede encontrar más
información sobre variables, enlace y cálculo lambda en Sección 4.2 .

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

(fx) = (cond ((eq x 0) 0)

(verdadero (+ x (f (- x 1)))))

Un primer intento podría ser

(lambda (x) (cond ((eq x 0) 0) (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

fue agregar un operador llamado etiqueta para que

(etiqueta f (lambda (x) (cond ((eq x 0) 0) (verdadero (+ x (f (- x 1)))))))

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

definición. En particular, una función recursiva f puede ser declarada por


(definir f (lambda (x) (cond ((eq x 0) 0) (verdadero (+ x (f (- x 1)))))))

Otra notación en algunas versiones de Lisps se define como "definir función", lo que elimina la necesidad de lambda:

(defun f (x) (cond ((eq x 0) 0) (verdadero (+ x (f (- x 1))))))

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

explicación de cómo hacerlo).

3.4.7 Funciones de orden superior

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

una función de primer orden como argumento se llama

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:

(definir componer (lambda (fg) (lambda (x) (f (gx)))))

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:

(definir lista de mapas (lambda (fx))

(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

(cuadro de lista de mapas '(1 2 3 4 5)) => (1 4 9 16 25),

donde el simbolo → significa "evalúa a".

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 .

3.4.8 Recolección de basura

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

puede acceder a la ubicación metro. En otras palabras, reemplazar el contenido de metro

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

basura. Después de la expresión

(coche (contras e1 e2))

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

((lambda (x) (coche (cons xx))) '(AB))

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.

Recolección de basura marcada y barrida

1. Establezca todos los bits de etiqueta en 0.

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

seleccionar, se puede escribir de la siguiente manera:

(definir seleccionar (lambda (x lst) (cond

((igual lst nil) nil) ((igual x (car lst)) (cdr

lst)) (verdadero (seleccionar x (cdr lst)))

)))

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:

typedef struct cell cell; estructura

celular {

celda * coche, * cdr; };

celda * seleccionar (celda * x, celda * lst) {celda *

ptr;

para (ptr = lst; ptr! = 0;) {

if (ptr-> car = = x) return (ptr-> cdr); si no 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:

celda * select1 (celda * x; celda * lst) {celda *

ptr, * anterior;

para (ptr = lst; ptr! = 0;) {

if (ptr-> car = = x) return (ptr-> cdr); si no

anterior = ptr;

ptr = ptr-> cdr;

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

almacenamiento para funciones de orden superior, estudiados en Sección 7.4 .

3.4.9 Pure Lisp y efectos secundarios

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

el campo de disminución de la celda de contras x con y.

En ambos casos, el valor de la expresión es la celda que se ha modificado. Por ejemplo, el valor de

(rplaca (contras 'A' B) 'C)

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

ejemplo, considere el siguiente código:

(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

siguiente lista de cuatro elementos:


Supongamos que llamamos a esta lista x y queremos cambiar el tercer elemento de la lista x a '. En Lisp puro, no podemos cambiar ninguna de las celdas de esta lista,

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

cdddr x significa" cdr de cdr de cdr de x ":

(contras (coche x) (contras (cadr x) (contras y (cdddr x))))

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

celda directamente usando la expresión

(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

3.5 RESUMEN DEL CAPÍTULO: CONTRIBUCIONES DEL LISP

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

3.1 Representaciones de celdas de contras

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.

3.2 Expresiones condicionales en Lisp

La semántica de la expresión condicional Lisp

(cond (p1 e1) ... (pn en))

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

qué o por qué no? ( Pista: Recuerde el problema de la detención.)

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:

I. El valor del condicional no está definido si ninguno de los paquete es verdad.

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

(defun impar (x) (cond ((eq x 0) nil)

((ecuación x 1) t)

((> x 0) (impar (- x 2))) (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

O, y puede definirse de la siguiente manera:

Permite mi 2 para ser indefinido si mi 1 es cierto.

los paralela OR es una construcción relacionada que da una respuesta siempre que sea posible (posiblemente haciendo alguna evaluación de

subexpresión innecesaria). Se define de manera similar:

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?

¿Qué interpretación dificultaría la implementación de operaciones paralelas? ¿Por qué?

3.3 Detección de errores

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,

(+ (E protegido) E '); solo E 'si E se detiene con un error; E + E 'de lo contrario

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

construcción protegida? ¿Qué dificultades podrías encontrar? Tenga en cuenta que,


a diferencia de la de (error? E), la evaluación de (E protegido) no necesita detenerse si la evaluación de E no se detiene.

3.4 Lisp y funciones de orden superior

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

hasta el final de una línea es un comentario.

(definir componer

(lambda (fg) (lambda (x) (f (gx)))))

(definir mapcar

(lambda (f xs)

(cond

((eq? xs ()) ()) ;; Si la lista está vacía, devuelva la lista vacía

(#t ;; De lo contrario, aplique f al primer elemento ...

(contras (f (coche xs))

;; y mapa f en

el resto de

lista

(mapcar f (cdr xs))

)))))

(definir lista de mapas

(lambda (f xs)

(cond

((eq? xs ()) ()) ;; Si la lista está vacía, devuelva la lista vacía

(#t ;; De lo contrario, aplique f a la lista ...

(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

(mapcar inc '(1 2 3 4)) = (2 3 4 5)

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

(mapcar f '(1 2 3 4))

= ((f (coche (1 2 3 4))) (f (coche (2 3 4))) (f (coche (3 4))) (f (coche (4))))

Escriba una versión de compose que nos permita definirmapcar de maplist. Más específicamente, escribe una definición de compose2 para
ese

((compose2 maplist car) f xs) = (mapcar f xs)

para cualquier función f y lista xs.

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

responder primero las partes (b) y (c).

(definir compose2

(lambda (gh)

(lambda (f xs)

(g (lambda (xs) (______)) xs)

)))

B. Cuando se evalúa (compose2 maplist car), el resultado es una función definida por (lambda (f xs) (g…)) arriba, con

I. ¿Qué función reemplaza a g?

ii. y que función reemplaza a h?

C. También podríamos escribir la subexpresión (lambda (xs) (…)) como (compose (…) (…)) para dos funciones. ¿Cuáles son estas dos

funciones? (Escríbalos en el orden correcto).

3.5 Definición de basura

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

este punto puede acceder a la ubicación m.

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?

Explica por qué o por qué no.

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.

3.6 Recuento de referencias

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

cuántos punteros hay para un determinado

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:

(coche (cdr (cons (cons ab) (cons cd))))

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

devolver a la lista de almacenamiento gratuito?

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

de referencias no funciona correctamente en esta estructura.

3.7 Regiones y gestión de la memoria

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.

3.8 Concurrencia en Lisp

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.

ii. Se crea un proceso para evaluar e.

iii. Cuando se completa el proceso de evaluación de e, el valor de e se coloca en la ubicación. l

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

(contras a (futuro e))

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?

(defun fib (n)

(cond ((ecuación n 0) 1)

((eqn1) 1)

(T (más (futuro (fib (menos n 1)))

(futuro (fib (menos n 2)))))))

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.

B. A primera vista, podríamos esperar que dos expresiones

(… 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

preocupe por la eficiencia de los cálculos ni por el grado de paralelismo.

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))

Esta construcción tendría las siguientes características:

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

principal del bloque try.

ii. Si no se producen errores, entonces (intente e (error-1 handler-1)…) es equivalente a e.

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

4.1 COMPILADORES Y SINTAXIS

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

ocurra la secuencia correcta de acciones.

4.1.1 Estructura de un compilador simple

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

separar los problemas principales y discutirlos en orden.

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ó".

La función principal de un compilador se ilustra en este diagrama simple:

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

máquina, de alguna máquina.

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

construcción de compiladores, como Compiladores: principios, técnicas y


Instrumentos por Aho, Sethi y Ullman (Addison-Wesley, 1986), y Implementación del compilador moderno en Java / ML / C por Appel (Cambridge Univ. Press,

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

distinguen en la fase de análisis semántico.

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

el programa donde se declara cada identificador.

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

las propiedades esencialmente sintácticas que surgen en la tercera fase de un compilador.

Generación de código intermedio

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

varias máquinas diferentes.

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.

La siguiente lista describe algunas optimizaciones estándar:

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

mover fuera del bucle.

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

que permiten realizar otras optimizaciones eliminando saltos del código.

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

dentro y fuera de la memoria.

4.1.2 Gramáticas y árboles de análisis

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

forma sistemática de procesar expresiones.

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 |

Dakota del Norte

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.

Algunas expresiones en el lenguaje dadas por esta gramática son

0, 1 + 3 + 5, 2 + 4 - 6 - 8

Secuencias de símbolos que contienen no terminales, como

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

(¡asegúrese de entender cómo!):

mi → norte → Dakota del Norte → dd → 2d → 25 e → e - e → e - e + e → ... → n-n + n → ……

10-15 + 12

Analizar árboles y ambigüedad

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,

entonces los hijos de s en el árbol habrá nodos etiquetados 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

ee, el árbol de análisis tiene la forma


donde hemos contraído los subárboles para cada número de dos dígitos a un solo nodo. Este árbol es diferente de

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

valor de una expresión puede depender de cómo se analice o entre paréntesis.

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

una declaración bien formada y uno de sus árboles de análisis:

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

(s1; s2); s3 o s1; (s2; s3).

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.

4.1.3 Análisis y precedencia

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,

e op1 e op2 e se analizaría como e op1 (e op2 e).

Expresión Precedencia Asociatividad izquierda Asociatividad correcta

5 * 4-3 (5 * 4) -3 (ningún cambio) (ningún cambio)

1 + 1-1 (ningún cambio) (1 + 1) -1 1+ (1-1)

2 + 3-4 * 5 + 2 2 + 3- (4 * 5) +2
((2 + 3) - (4 * 5)) + 2 2+ (3 - ((4 * 5) +2))

Equipo-Fly
Equipo-Fly

4.2 CÁLCULO DE LAMBDA

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.

4.2.1 Funciones y expresiones de funciones

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

y usarlas dentro de expresiones más grandes.

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).

Funciones dadas F y gramo, esta función produce la composición λ X. f (gx) de F y gramo.

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

Lenguaje de programación = aplicado λ- cálculo

= puro λ- cálculo + tipos de datos adicionales.

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 .

4.2.2 Expresiones Lambda

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,

z,…, para representar variables arbitrarias. La gramática de las expresiones lambda es

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

expresiones del cálculo lambda como términos lambda.

A continuación, se muestran algunos términos lambda de ejemplo:

λ xx una abstracción lambda llamada función de identidad

λ x. (f (gx)) otra abstracción lambda

( λ xx) 5 Una aplicación

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

alcance de λ se extiende lo más a la derecha posible. Por ejemplo, λ

X . xy debe leerse como λ x. (xy), no ( λ xx) y.

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,

λ xx define la misma función que λ y. y.

¿Se llaman las expresiones que difieren solo en los nombres de las variables vinculadas? equivalente. ¿Cuándo queremos enfatizar que dos expresiones son?

equivalente, escribimos λ xx =? λ aa, por ejemplo.

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

variables libres de M por inducción sobre la estructura de expresiones, como sigue:

FV ( x) = {x},

FV ( MN) = FV ( M)? FV ( NORTE),

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

del alcance de la λ y entre paréntesis.

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.

Abstracción Lambda en Lisp y Algol

El cálculo lambda tiene una similitud sintáctica obvia con Lisp: la expresión lambda de Lisp

(lambda (x) cuerpo_función)

se parece a la expresión de cálculo lambda

λ 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

agregamos declaraciones al cálculo lambda.

La simple declaración let,

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:

dejar x = M en N es azúcar para ( λ xN) M.

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

la sintaxis dejar x = M en norte como representando ( λ xN) M.

Usando declaraciones let, podemos escribir el programa C desde arriba como

dejar f = ( λ xx) en cuerpo de bloque

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

prueba ecuacional del cálculo lambda tiene el axioma ecuacional

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

de sustituir norte para apariciones gratuitas de X en METRO,

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.

fx aplicado a λ aa se puede calcular si el argumento λ aa se sustituye por el límite


variable f:

( λ f .fx) ( λ y .y) = ( λ y, y) x

Por supuesto, ( λ yy) x puede simplificarse mediante una sustitución adicional, por lo que tenemos

( λ f .fx) ( λ y .y) = ( λ y .y) x = x

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.

Cambio de nombre de variables enlazadas

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

variables vinculadas y luego realizamos β- reducción, obtenemos

( λ 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:

( λ f. λ X . f (fx)) ( λ y. y + x) = λ X.(( λ y. y + x) (( λ y. y + x) x)) = λ x .x + x + x

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

vinculadas en sustitución, definimos el resultado [ N / x] M

de sustituir norte por X en METRO por inducción sobre la estructura de METRO:

[ N / x] x = N,

[ N / x] y = y, dónde y es cualquier variable diferente de X,

[ N / x] (M 1 METRO 2) = ([ N / x] M 1) ([ N / x] M 2),

[ N / x] ( λ xM) = λ xM,

[ N / x] ( λ yM) = λ y. ([N / x] M), dónde y no es gratis en NORTE.

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

sustitución, ahora tenemos una definición precisa de β- equivalencia.

4.2.3 Programación en cálculo Lambda

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

funciones de múltiples argumentos.

Funciones de varios argumentos

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

F curry = λ gramo. λ x.gx

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

gramo. Sin embargo, F se puede usar curry en lugar de F porque

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

Vimos en Subsección 4.2.2 que podemos considerar declaraciones simples,

dejar x = M en norte

como términos lambda adoptando la abreviatura

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

dejar componer = λ F. λ gramo. λ xf (gx) en componer hh

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

construcción let proporciona declaraciones locales.

Recurrencia y puntos fijos

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

algunos instructores deseen omitir esta subsección).

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

programación escribiendo una declaración como

función f (n) {si n = 0 entonces 1 si no n * f (n-1)};

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

f (n) = si n = 0 entonces 1 si no n * f (n-1)


con la declaración recursiva anterior. Esta ecuación establece una propiedad del factorial.

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

recursiva) en ambos lados de la ecuación.

Podemos simplificar la ecuación anterior usando la abstracción lambda para eliminar n del lado izquierdo. Esto nos da

f = λ norte. si n = 0 entonces 1 si no n * f (n-1)

Ahora considere la función G que obtenemos moviendo f al lado derecho de la ecuación:

G = λ F. λ norte. si n = 0 entonces 1 si no n * f (n-1)

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,

los puntos fijos se pueden definir con el

operador de punto fijo:

Y = λ F.( λ xf (xx)) ( λ xf (xx)).

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)).

Usando β- equivalencia de nuevo en el término de la derecha, obtenemos

Y f = ( λ X . f (xx)) ( λ X . f (xx)) = f ( λ X . f (xx)) ( λ X . f (xx)) = f (Y f).


Por lo tanto Y f es un punto fijo de f.

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:

Usando pasos similares, podemos calcular ( YG) 1 = 1! = 1 para completar el cálculo.

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.

4.2.4 Reducción, confluencia y formas normales

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

reducción es un razonamiento ecuacional, pero en cierta dirección. Específicamente, aunque

β- 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

sido un modelo matemático simple de evaluación de expresiones.

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

para evaluar. Por ejemplo, en la segunda expresión,

( λ 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

dos opciones para evaluar la expresión puramente aritmética.

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.

4.2.5 Propiedades importantes del cálculo Lambda

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.

El cálculo lambda tiene las siguientes propiedades importantes:

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

subexpresión. Evaluación en lenguajes de programación puramente funcionales (ver

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.

4.3 SEMÁNTICA DENOTACIONAL

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

instrucciones para ejecutar.

En semántica denotacional, el significado de un programa simple como


x: = 0; y: = 0; mientras x ≤ z hacer y: = y + x; x: = x + 1

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

definiría en estados de máquina en los que el valor de z no es un número entero no negativo.

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

Q debe explicarse con sólo las denotaciones de B, P,

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

reemplazar partes de programas sin cambiar el significado general de todo el programa.

4.3.1 Lenguaje de objetos y metalenguaje

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 expresión 3 + 6 - 4 está escrito con símbolos del lenguaje de objetos.

4.3.2 Semántica denotacional de números binarios

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

función de su árbol de análisis sintáctico.

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

con [[e1]] y [[e2]] como subárboles inmediatos.

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

E [[nb]] = E [[n]] * 2 + E [[b]]

E [[e1 + e2]] = E [[e1]] + E [[e2]]

E [[e1 - e2]] = E [[e1]] - E [[e2]]

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.

4.3.3 Semántica denotacional de programas while

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.

Expresiones con variables

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 |

Dakota del Norte

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 [[x]] (s) = s (x)

E [[0]] (s) = 0

E [[1]] (s) = 1

… =?

E [[9]] (s) = 9

E [[nd]] (s) = E [[n]] (s) * 10 + E [[d]] (s)

E [[e1 + e2]] (s) = E [[e1]] (s) + E [[e2]] (s)

E [[e1 - e2]] (s) = E [[e1]] (s) - E [[e2]] (s)

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

en la semántica de expresiones libres de variables en la subsección anterior.

La sintaxis y la semántica de las expresiones booleanas se pueden definir de manera similar.

Mientras que los programas

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

valores de las variables. A continuación, se muestra un ejemplo sencillo:

x: = 0; y: = 0; mientras x ≤ z hacer (y: = y + x; x: = x + 1)

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

Estado = Variables → Valores

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

Comando = Estado → Expresar

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:

modificar (s, x, a) = λ v? Variables. si v = x entonces a demás s (v)

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

forma sintáctica de programa:

C [[x: = e]] (s) = modificar (s, x, E [[e]] (s)) C [[P1;

P2]] (s) = C [[P2]] (C [ [P1]] (s))


C [[si e entonces P1 si no P2]] (s) = si E [[e]] (s) entonces C [[P1]] (s) si no C [[P2]] (s)

C [[mientras e hago P]] (s) = si no E [[e]] (s) entonces s

más C [[mientras e hago P]] (C [[P]] (s))

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.

En palabras, podemos describir la semántica de los programas de la siguiente manera:

C [[x: = e]] ( s) es el estado similar a s, pero con X teniendo el valor de mi.

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

obtenemos aplicando F al estado resultante de ejecutar P una vez en s.

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,

donde s0 (x) = 1 y s0 (y) = 2.

Como E [[x> y]] (s0) = falso, tenemos

C [[si x> y entonces x: = y si no y: = x]] (s0)

= si E [[x> y]] (s0) entonces C [[x: = y]] (s0) si no C [[y: = x]] (s0)

= C [[y: = x]] (s0)

= modificar (s0, y, E [[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

variable y dado el valor que x tiene en estado s0.

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 = C [[x: = 0]] (s0)

s2 = C [[y: = 0]] (s1)

Usando la semántica de asignación, como arriba, tenemos

s1 = modificar (s0, x, 0)

s2 = modificar (s1, y, 0)

Volviendo al programa anterior, tenemos

C [[x: = 0; y: = 0; mientras x ≤ z hacer (y: = y + x; x: = x + 1)]] (s0)

= C [[y: = 0; mientras x ≤ z do (y: = y + x; x: = x + 1)]] (C [[x: = 0]] (s0)) = C [[y: = 0; mientras x ≤

z do (y: = y + x; x: = x + 1)]] (s1) = C [[mientras x ≤ z do (y: = y + x; x: = x + 1)]] (C [[y: = 0]]

(s1)) = C [[mientras que x ≤ z hacer (y: = y + x; x: = x + 1)]] (s2)

= si no es E [[x ≤ z]] (s2) luego s2

más C [[mientras x ≤ z do (y: = y + x; x: = x + 1)]] (C [[y: = y + x; x: = x + 1]] (s2))

= C [[mientras x ≤ z hacer (y: = y + x; x: = x + 1)]] (s3)

donde s3 tiene y establecido en 0 y x establecido en 1. Continuando de la misma manera, tenemos

C [[mientras x ≤ z hacer (y: = y + x; x: = x + 1)]] (s3)

= si no es E [[x ≤ z]] (s3) luego s3

más C [[mientras x ≤ z do (y: = y + x; x: = x + 1)]] (C [[y: = y + x; x: = x + 1]] (s3))

= C [[mientras x ≤ z hacer (y: = y + x; x: = x + 1)]] (s4)

= si no es E [[x ≤ z]] (s4) luego s4

más C [[mientras x ≤ z do (y: = y + x; x: = x + 1)]] (C [[y: = y + x; x: = x + 1]] (s4))

= C [[mientras x ≤ z hacer (y: = y + x; x: = x + 1)]] (s5) = s5

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

hace x: = x]] (s) no está definido.


De manera similar, C [[while x = y do x: = y]] (s) es s si s (x) ≠ s (y) e indefinido de lo contrario. Si está interesado en obtener más información, hay muchos libros que

cubren la semántica denotacional en detalle.

4.3.4 Perspectiva y semántica no estándar

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

en todos los estados y también para calcular la semántica de los bucles.

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

antes de que se inicialice. Por ejemplo, no podemos decidir si

(programa sin errores complictated); x: = y

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,

Estado = {error}? ( Variables → { init, uninit})


Como es habitual, el significado de un programa será una función de estados a estados. Supongamos que E [[e]] (s) = error si e

contiene cualquier variable y con s (y) = uninit y E [[e]] (s) = OK de lo contrario.

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

alguna variable en e que no se inicializó en s. Por ejemplo, deja

s0 = λ v? Variables.uninit

ser el estado con cada variable sin inicializar. Entonces

C [[x: = 0]] (s0) = modificar (s0, x, en eso)

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

s1 + s2 = λ v? Variables. Si s1 (v) = s2 (v) = en eso entonces en eso demás uninit

Definimos el significado de una declaración condicional por

C [[si e entonces P1 si no P2]] (s)

= si E [[e]] (s) = error o C [[P1]] (s) = error o C [[P2]] (s) = error

entonces error

si no C [[P1]] (s) + C [[P2]] (s)

Por ejemplo, usando s0 como arriba, tenemos

C [[si 0 = 1 entonces x: = 0 si no x: = 1; y: = 2]] (s0) = modificar (s0, x, en eso)

ya que solo x se inicializa en ambas ramas del condicional.

Para simplificar, no consideramos la cláusula para while e do P.

Equipo-Fly
Equipo-Fly

4.4 LENGUAJES FUNCIONALES E IMPERATIVOS

4.4.1 Oraciones imperativas y declarativas

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,

A Claude le gustan los plátanos

es una oración declarativa.

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

también contienen construcciones declarativas como la declaración de función

función f (int x) {return x + 1;}

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

cuyo valor de retorno es 1 mayor que su argumento".

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 * / / *

{int y = x + 1; declara nueva y * /

{int x = y + 1; / * declara nuevo x * /

}}}

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

{int x = 1; / * declara nuevo x * /

x = x + 1; / * asignación a x existente * / / *

{int y = x + 1; declara nueva y * /

{int z = y + 1; / * declara nuevo z * /

}}}

(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.

4.4.2 Programas funcionales versus imperativos

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.

Los lenguajes funcionales puros pasan la siguiente prueba:

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

evaluación de contras normalmente implicaría una nueva celda.

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

poco complicado usarla correctamente para distinguir un lenguaje de programación de otro.

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

Vi a Walter entrar su coche.

Si Walter tiene un Maserati Biturbo, digamos, y ningún otro automóvil, entonces la oración

Vi a Walter entrar su Maserati Biturbo

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

Fue llamado William Rufus por su barba roja.

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

oración que no tiene sentido:

Fue llamado Guillermo IV por su barba roja.

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

independientemente del contexto en el que ocurren.

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

de desarrollo del programador.


eficiencia en tiempo de ejecución. Debido a su creencia en la importancia de la corrección, la legibilidad y la confiabilidad, Backus pensó que los lenguajes funcionales

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;

para (i = 0; i <n; i ++) prod = prod + a [i] * b [i];

Por el contrario, la función del producto interno se definiría en FP combinando las funciones + y × (multiplicación) con operaciones vectoriales.

Específicamente, el producto interno se expresaría como

Producto interno = (Insertar +) ° (Aplicar a todo x) ° Transponer

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

FP es de mayor nivel, o más abstracto, que el código C.

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

funcional es la posibilidad de paralelismo en las implementaciones. Esto se discute posteriormente.

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

limitaciones en el tipo de estructuras de datos y control


estructuras que se pueden definir. Los lenguajes FP que permiten nombres de variables y enlaces (como en Lisp y cálculo lambda) han tenido más éxito, al igual que todos

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

llevó a una reflexión y un debate útiles.

Programación funcional y simultaneidad

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.

Podemos ver cómo surge el paralelismo en lenguajes funcionales puros por

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

expresiones son independientes.

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

ejemplo, evaluación completa en paralelo de un condicional

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

cantidad de paralelismo utilizado en la ejecución.

Programación funcional práctica

La conferencia del Premio Turing de Backus plantea una pregunta fundamental:

¿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

proporciona un método para hacer esto.

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

4.5 RESUMEN DEL CAPÍTULO

En este capítulo, estudiamos los siguientes temas:

el esquema de un compilador simple y problemas de análisis, cálculo

lambda,

semántica denotacional,

la diferencia entre lenguajes funcionales e imperativos.

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

lambda con tipo.

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

exitosa como anticipó en su conferencia del Premio Turing.

Equipo-Fly
Equipo-Fly

EJERCICIOS

4.1 Árbol de análisis

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?

4.2 Análisis y precedencia

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.

C. 1-1 + 1 - 1 + 1, si damos + mayor precedencia que -.

4.3 Reducción del cálculo lambda

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?

4.4 Evaluación simbólica

El fragmento de programa similar a Algol

función f (x)

devolver x + 4

fin;

función g (y)

volver 3 años

fin;

f (g (1));

se puede escribir como la siguiente expresión lambda:

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.

B. Reducir la expresión eligiendo, en cada paso, la reducción que elimina un λ hasta el


derecho como sea posible.

4.5 Reducción lambda con azúcar

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

La expresión lambda "desazucarada", obtenida cuando cada let z = U en V se reemplaza con ( λ z. V) U es

( λ 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

respuesta que esperabas.

4.6 Traducción al cálculo Lambda

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);

printf ("Valor de x =% d \ n", x);

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.

4.7 Orden de evaluación

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 (…))

...

(definir f (lambda (xyz) (contras (auto x) (contras (auto y) (cdr z)))))

...

(f e1 e2 e3)

El orden de evaluación ordinario para la llamada a la función

(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

izquierda a derecha dé un resultado diferente al obtenido al evaluarlas de derecha a izquierda.


Puede completar declaraciones específicas para a o b si lo desea y hacer referencia a a o b en sus expresiones. Explique brevemente, en una o dos oraciones, por qué

un orden de evaluación es diferente del otro.

4.8 Semántica denotacional

El texto describe una semántica denotacional para el lenguaje imperativo simple dado por la gramática

P :: = x: = e | PAG 1; PAG 2 | si mi entonces PAG 1 más PAG 2 | mientras mi hacer pag.

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

texto, donde s 0 = λ v? variables. 0, dando a cada variable el valor 0.

B. La semántica denotacional se utiliza a veces para justificar formas de razonar sobre programas. Escriba algunas oraciones,

refiriéndose a su cálculo en la parte (a), explicando por qué

C [[x: = 1; x: = x + 1;]] ( s) = C [[x: = 2;]] ( s)

para cada estado s.

4.9 Semántica de inicializar antes de usar

En el texto se presenta una semántica denotacional no estándar que describe el análisis inicializar antes de usar.

una. Cuál es el significado de

C [[x: = 0; y: = 0; si x = y entonces z: = 0 más w: = 1]] ( s 0)

en el estado s 0 = λ y? variables.uninit? Muestre cómo calcular su respuesta.

B. Calcula el significado

C[[ si x = y entonces z: = y demás z: = w]] (s)

en estado s con s (x) = inicio, s (y) = inicio, y s (v) = uninit o cualquier otra variable v.

4.10 Semántica de la verificación de tipo

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:

e :: = 0 | 1 | verdadero | falso | x | e + e | si mi entonces mi demás e | dejar X: τ = mi en mi

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.

Un ambiente η es un mapeo de variables a valores. En el análisis de tipos, utilizamos tres valores,

Valores = {integer, boolean, type_error}.

Intuitivamente V [[e]] ( η) = entero significa que el valor de la expresión mi es un número entero (en el entorno η) y V [[e]] ( η) =

error de tecleado significa que la evaluación de podemos implican un error de tipo.

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

que debe mapear la variable X digitar σ. Más precisamente,

Usando esta notación, podemos definir la semántica de las declaraciones por

La cláusula para condicional es

Por ejemplo, con η 0 = λ y? Var. error de tecleado,

V [[ si es cierto, entonces 0 + 1 si no x +1]] η 0 = error de tecleado

como la expresión x + 1 produce un error de tipo si se evalúa X produce un error de tipo. Por otro lado,

V [[ dejar x: int = ( 1 + 1) en (si es verdadero, entonces 0 más X)]] η 0 = entero

ya que la expresión let declara que X es una variable entera.

Preguntas:

una. Muestre cómo calcular el significado de la expresión si es falso, luego 0 más 1 en el entorno η 0

= λ y? Var. error de tecleado.

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

el significado de la expresión let x: int = e 1 en (si mi 2 entonces mi 1 más

X) en el medio ambiente η 0 = λ y? Var. error de tecleado .

C. En declaración deja X: τ = mi en mi, el tipo de X se da explícitamente. También es posible omitir el tipo


de la declaración e inferirla del contexto. Escriba una cláusula semántica para la forma alternativa, deje x = e 1

en mi 2 utilizando la siguiente solución parcial (es decir, complete las partes que faltan en esta definición):

4.11 Evaluación perezosa y paralelismo

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é

pasará con la evaluación perezosa? ¿Evaluación paralela?

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

g, e1 y e2 en paralelo, si el valor de e1 es cero y la evaluación de e2 termina en un error?

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?

4.12 Idiomas de asignación única

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,

dentro del alcance de la variable.

El siguiente fragmento de programa satisface esta condición,

si (y> 3) entonces x = 42 + 29/3 en caso contrario x = 13,39;

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?

devoluciones? cláusula de devoluciones?

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

elt_prod: = x [i] * y [i]

devuelve el valor de la suma elt_prod end para

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

contiene un conjunto de bucles paralelos.

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

secuencialmente en un solo procesador.

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

fuente potencial de efectos secundarios en el idioma que no sea la asignación.

una. Explica cómo podrías ejecutar partes del programa de muestra.

x = 5;

y = f (g (x), h (x));

si y == 5 entonces z = g (x) si no z = 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

proceso 2 - llamar a g (x) proceso

3 - llamar a h (x)

proceso 4 - llame a f (g (x), h (x)) y establezca y a este valor proceso 5 -

prueba y == 5

proceso 6 - llamar g (x) y luego establecer z = g (x) proceso 7 -

llamar h (x) y luego establecer z = h (x)

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

que pasa en el código de ejemplo es por valor.

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

compilador eliminar correctamente estas llamadas de los procesos 6 y 7? Explicar brevemente.

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

condición de asignación única? Explicar brevemente.

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

única y permita reconocer muchos programas útiles de asignación única?

mi. Suponga que un idioma de asignación única no tiene operaciones de efectos secundarios aparte de la asignación. ¿Este idioma pasa

la prueba de lenguaje declarativo? Explica por qué o por qué no.

4.13 Programas funcionales e imperativos

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

traducirlo a un lenguaje funcional.

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

programación? ¿Por qué?

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.

4.14 Lenguajes funcionales y simultaneidad

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

ejecutarse de manera eficiente en paralelo.

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

eficiente en paralelo. Incluya dos razones específicas en su respuesta.


Equipo-Fly
Equipo-Fly

Parte 2: Procedimientos, tipos, gestión de la memoria y control

Capítulo 5 : La familia Algol y ML

Capítulo 6 : Sistemas de tipos e inferencia de tipos

Capítulo 7 : Alcance, funciones y gestión de almacenamiento

Capítulo 8 : Control en lenguajes secuenciales

Equipo-Fly
Equipo-Fly

Capítulo 5: La familia Algol y ML

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

5.1 LA FAMILIA ALGOL DE LENGUAS DE PROGRAMACIÓN

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

de bloques, las funciones y procedimientos, y la escritura estática.

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

algoritmos complejos en publicaciones científicas y de ingeniería.

También podría gustarte