Lenguajes De Programación, 2da Edición - Doris Appleby-freelibros.pdf [1q7jdkzxyxqv] (2024)

Julius J. Vandekopple Marymount College Traducción

Dr. Marc Karim Boumedine Montaner Profesor asociado ITESM, Campus Ciudad de México

McGRAW-HILL M ÉXICO • B U E N O S A IR ES • C A R A C A S • G U A T E M A L A • LISB O A • MADRID • N U E V A Y O R K SA N JU A N • S A N T A F É D E B O G O T Á • S A N T IA G O • S A O P A U L O

AUCKLAND • LONDRES • MILÁN • MONTREAL • NUEVA DELHI • SAN FRANCISCO SINGAPUR * ST. LOUIS • SIDNEY • TORONTO

Gerente de marca: Carlos Granados Islas Supervisora de edición: Gloria Leticia Medina Vigil Supervisor de producción: Zeferino García García

Prohibida la reproducción total o parcial de esta obra, por cualquier medio, sin autorización escrita del editor. DERECHOS RESERVADOS © 1998, respecto a la primera edición en español por McGRAW-HILL INTERAMERICANA EDITORES, S. A. de C. V. Una División de The McGraw-Hill Companies, Inc. Cedro Núm. 512, Col. Atlampa Delegación Cuauhtémoc 06450 México, D. F. Miembro de la Cámara Nacional de la Industria Editorial Mexicana, Reg. Núm. 736

Translated from the second edition in English of PROGRAMMING LANGUAJES: PARADIGM AND PRACTICE Copyright © MCMXCVII, by The McGraw-Hill Companies, Inc., U.S.A. ISBN 0-07-005315-4 1997, 1991 by McGraw-Hill Companies, Inc. Traducido al español en 1998 por Carlos Granados Islas Todos los derechos reservados. 1234567890

Esta obra se terminó de imprimir en Agosto de 1998 en impresora O F G L O M A S.A. de C.V. Calle Rosa Blanca Núm. 12 Col. Santiago Acahualtepec México, 13 D.F.

En este capítulo Resolución de problemas Herramientas matemáticas /Niveles conceptuales y de implementación 0.2 Paradigmas del lenguaje Paradigmas imperativos / Paradigmas declarativos 0.3 Consideraciones prácticas Desde el bajo, pasando por el alto, hasta el muy alto nivel /Programación a gran escala [ Problemas especiales 0.4 Criterios de lenguaje Descripciones bien definidas /Comprobabilidad / Confiabilidad / Traducción rápida / Código objeto eficiente / Ortogonalidad / Generalidad / Consistencia y notaciones comunes / Uniformidad / Subconjuntos / Extensibilidad / Transportabilidad 0.5 Resumen 0.6 Notas sobre las referencias

En este capítulo Tipos de datos primitivos Entero (integer) / Real / Carácter / Booleano / Apuntador Variables Identificadores / Palabras reservadas y palabras clave / Ligadura / Bloques y alcance / Registros de activación Tipos de datos estructurados Tipos definidos por el usuario / Tipos agregados ¡ Cuestiones de tipo Resumen Notas sobre las referencias Sólo fines educativos - FreeLibros

En este capítulo Abstracción de datos Los datos y el almacenamiento /Tipos de datos abstractos / Tipos genéricos 2.2 Abstracción de control Ramificación / Iteración ¡Recursión / Excepciones 2.3 Abstracción de procedimientos Procedimientos /M ódulos y A D T/C lases de A D T / Objetos /Ejecución concurrente 2.4 Resumen 2.5 Notas sobre las referencias

En este capítulo ALGOL 60 Viñeta histórica: Diseño por comité / Conceptos de ALGOL 60 /Puntos problemáticos en ALGOL 60 / Especificación del lenguaje ALGOL 68 Pascal Viñeta histórica: Pascal y Modula-2: Niklaus Wirth / Filosofía y estructura Ada Viñeta histórica: Ada ¡ Organización del programa / Tipos / La facilidad genérica / Excepciones / El entorno de soporte para programación en Ada (APSE) C Viñeta histórica: El dúo dinámico: Dennis Ritchie y Kenneth Thompson / Tipos de datos en C / Operadores de C / C y UNIX / El C estándar / Ventajas y desventajas Resumen Notas sobre las referencias

Programación con objetos Mensajes, métodos y encapsulamiento /Primeras nociones de objetos en Simula /Objetos en Ada 83 y Ada 95 Clases y polimorfismo Procedimientos y paquetes genéricos en A da/C lases en Object Pascal /Clases en C++ /Implementación de clases heredadas Smalltalk Viñeta histórica: Smalltalk: Alan Kay Herencia y orientación a objetos Tipos y subtipos en jerarquías de herencia /Herencia múltiple / Ejemplares de lenguaje /Ligadura dinámica Java Construcciones del lenguaje Java /Las Interfaces de Programación para Aplicaciones de Java (APIs) / Compilación y ejecución de un programa Java /Hotjava y Applets / Tipos de programa /Diferencias entre Java, C y C++ Resumen Notas sobre las referencias

En este capítulo El paradigma Procesos múltiples Sincronización de procesos cooperativos Semáforos /M onitores / Rendezvous (Punto de reunión) / Paso de mensajes Algunas soluciones de sincronización Semáforos en ALGOL 68, C y Pascal S / Tipos de proceso y monitor en Concurrent Pascal / Rendezvous (Punto de reunión) en Ada y Concurrent C / Paso de mensajes en Occam Tupias y objetos El espacio de tupias de Linda / Objetos como unidades de paralelismo Administración de fallas parciales Resumen Notas sobre las referencias

En este capítulo Lenguajes formales Definición de lenguajes formales /La jerarquía de Chomsky de los lenguajes formales / Viñeta histórica: Clasificaciones de los lenguajes: Noam Chomsky / Viñeta histórica: Alan Turing: Lo que las máquinas no pueden hacer 6.2 Gramáticas regulares Expresiones regulares I Autómatas finitos (FAf NFA y DFA) /Aplicaciones 6.3 Gramáticas Libres de Contexto (CFG) Autómatas descendentes (PDA; Push-Dozon Autómata) / Árboles de análisis sintáctico / Gramáticas ambiguas / Aplicaciones /Formas normales 6.4 Gramáticas para los lenguajes naturales 6.5 Resumen 6.6 Notas sobre las referencias

En este capítulo Sistemas lógicos formales Viñeta histórica: Aristóteles ¡ Demostraciones o pruebas /Búsqueda PROLOG Viñeta histórica: PROLOG: Colmerauer y Roussel / Conversando en PROLOG: hechos, reglas y consultas / Implementaciones de PROLOG /Aplicaciones / Fortalezas y debilidades Resumen Notas sobre las referencias

En este capítulo Características de los lenguajes funcionales Composición de funciones /Funciones como objetos de primera clase /Ausencia de efectos colaterales / Semántica limpia

LISP Viñeta histórica: LISP: John McCarthy /El lenguaje LISP (dialecto SCHEME) / Otras características no funcionales /Dialectos / Common LISP Implementación de lenguajesfuncionales Evaluación débil (lazy evaluation) contra evaluación estricta (strict evaluation) /Alcance y ligaduras J Recolección de bastirá Soporte de paralelismo con funciones Otros lenguajes funcionales A P L / M L / Otros Resumen Notas sobre las referencias

En este capítulo Modelos jerárquicos y de red El modelo relacional Manipulación de bases de datos relaciónales i SQL / Sistemas basados en lógica utilizando PROLOG 9.3 Modelos de datos semánticos 9.4 Modelo de base de datos orientado a objetos 9.5 Resumen 9.6 Notas sobre las referencias Apéndice A Cálculos lógicos (para el capítulo 7) Apéndice B El cálculo lambda (para el capítulo 8) Apéndice C Fuentes de software Referencias índice

Laboratorios 2.1 Tipos de datos abstractos: Ada/Pascal 2.2 Métodos de paso de parámetros: Pascal 3.1 Bloques: Ada/Pascal 3.2 Combinación de características de bajo y alto nivel: C 3.3 Diversión con trucos para C: C 3.4 Herramientas IDE: Pascal/C 3.5 Herramientas APSE: Ada 4.1: Objetos, encapsulamiento y métodos: Object Pascal/Ada/C++ 4.2 Polimorfismo: Object Pascal/Ada/C++ 4.3 Clases y herencia: Object Pascal y C++

Objetos y programación orientada a objetos: Java HTML para utilizarse en el World Wide Web con Java Un applet: Java Simulación de procesamiento en paralelo: Ada Productores-consumidores: Pascal S/Occam 2/ C-Linda Expresiones regulares: grep EBNF: Papel y lápiz Introducción al lenguaje: PROLOG Caníbales y misioneros: PROLOG Practicando con LISP: SCHEME Una función de palíndromos: SCHEME Programación utilizando ciclos iterativos: SCHEME Rastreo y depuración: SCHEME Programación en SCOOPS: SCHEME SQL: dBASE IV

PREFACIO PARADIGMAS DEL LENGUAJE Durante los últimos 10 años, los lenguajes para programación de computadoras han sido organizados en una jerarquía de paradigmas, siendo los principales los mostrados abajo en la figura P.l. Un paradigma se puede considerar como una colección de características abs­ tractas que categorizan ton grupo de lenguajes que son aceptados y utilizados por un grupo de profesionales. Un estudiante que comprende lo que distingue a cada paradigma y tiene alguna experiencia en programación con por lo menos un len­ guaje de cada paradigma puede considerarse básicamente educado en el tema de estudio de los lenguajes de programación. Discutiremos la noción de paradigma en el capítulo 0, y proporcionaremos descripciones ejemplificadas mediante len­ guajes existentes en los capítulos que le siguen.

ORGANIZACIÓN DE ESTE TEXTO Este libro está organizado basándose en cuatro principios: • •

Una buena forma de ordenar a la, en ocasiones, confusa colección de lenguajes de alto nivel es estudiarlos paradigma por paradigma. La mayoría de la gente no comprende un lenguaje a menos que lo utilice en realidad.

FIGURA P.l Una jerarquía de paradigmas de lenguajes de programación Sólo fines educativos - FreeLibros

A fin de hacer uso de un lenguaje, se necesita un manual elemental de ese lenguaje, el cual también hace las veces de un tutorial. Todo lo que un estudiante necesita para obtener los primeros tres principios debería estar disponible de manera fácil y barata. Para hacer esto, proporcionaremos:

La audiencia destinada son estudiantes que pueden programar bien en al me­ nos un lenguaje de alto nivel. No se supone que tengan conocimientos de lenguaje ensamblador, aunque podría ser útil para la comprensión de algunos temas. El texto se encuentra dividido en cuatro partes, como se ilustra en la figura P.2. En el texto comenzamos con algunos conceptos básicos, que pueden hallarse en casi cualquier lenguaje. En vez de considerar los datos como cadenas de bits y un programa como una secuencia de instrucciones para manipular esos bits, vemos los datos, el control de programa y los procedimientos a un nivel mayor o más abstrac­ to. Hecho esto, estudiamos los paradigmas, uno por uno, con un enfoque particular en un lenguaje de ejemplo para cada categoría. Si el lenguaje es un ejemplo particu­ larmente bueno de un paradigma, es llamado un ejemplar. Para proporcionar prácti­ cas inmediatas con cada paradigma, hemos organizado el trabajo del estudiante alrededor de laboratorios de programación semanales que utilicen los ejemplares. El texto da igual peso a los dos paradigmas de nivel superior, imperativo y declarativo. También hace énfasis en los fundamentos teóricos de diferentes tipos de lenguaje. La mayoría de los lenguajes de programación no se han desarrollado simplemente como colecciones de características computacionales útiles. Muchos han intentado instrumentar fielmente teorías matemáticas, que proporcionen el vo­ cabulario y estructura para resolución de problemas y acerca de las cuales se han hecho muchas comprobaciones. Nosotros hemos incorporado estas nociones en el texto mismo o bien las hemos incluido en breves apéndices de teoría matemática subyacente. Cada lenguaje de programación se presenta como un ejemplo de uno de los paradigmas y, si es aplicable, como un modelo de teoría matemática. Cualquier lenguaje, escrito o hablado, tiene sintaxis (forma) y semántica (signi­ ficado). La teoría lingüística también ha influenciado a los lenguajes de programa­ ción, de manera que hemos incluido el capítulo 6 sobre el uso de la lingüística para escribir definiciones de lenguaje formal. Este libro mezcla fundamentos e historia de la teoría con experiencia práctica de programación en 12 lenguajes de ejemplo. Un estudiante no podría esperar ser competente en 12 lenguajes diferentes durante el curso de un solo semestre, pero puede al menos ver de lo que tratan. Se proporcionan de una a seis tareas de programación formales para la mayo­ ría de los capítulos. Estos laboratorios, que pueden implementarse ya sea en confi­ guración de laboratorio cerrado o abierto, presentan aplicaciones sustanciales escritas en el lenguaje de ejemplo bajo discusión. Un paquete de enseñanza com­ pleto incluye el texto, las tareas de laboratorio y el Instructor's M anual* * El material auxiliar sólo está disponible en inglés para profesores o instituciones mediante peti­ ción escrita. Si desea mayor información sobre éste, póngase en contacto con alguno de los representan­ tes de esta casa editora.

Parte I: Conceptos preliminares Cuando el Pascal estándar de 19851incluyó dos niveles del lenguaje, Pascal Nivel 0 y Pascal Nivel 1, los australianos del comité de estándares se quejaron acerca de que el uso del Nivel 0 era un "barbarismo en el lenguaje inglés" [citado en Cooper, 1983]. De acuerdo. N o obstante, hemos incluido un capítulo 0 introductorio, el cual

1 El estándar de 1983 [ANSI/IEEE-770X3.97, 1983] describe solamente Pascal Nivel 0, el cual no incluía arreglos conformantes, los que están en el Nivel 1. El estándar de 1985 incluye tanto el Nivel 0 como el Nivel 1.

describe la noción histórica de los paradigmas científicos, analiza las característi­ cas abstractas de cada uno de los paradigmas anteriores, enumera las caracterís­ ticas concretas de desempeño que todo lenguaje debe poseer y proporciona una introducción a la sintaxis del lenguaje. Este capítulo es el prefacio al trabajo técnico del curso. Los capítulos 1 y 2 describen las construcciones llevadas a cabo en todos los lenguajes, con ejemplos escritos en seudocódigo tipo Pascal/Ada. En el capítulo 1 consideramos los tipos de variables y datos y en el capítulo 2 los conceptos de abstracción para datos, construcciones de control y módulos. Parte II: Lenguajes imperativos La parte II trata sobre el paradigma imperativo, el cual incluye lenguajes con facili­ dades para la asignación de valores a localidades de memoria. El capítulo 3 analiza los lenguajes de procedimiento estructurados en bloques, con los que la mayoría de los estudiantes ha tenido alguna experiencia de programación. Los lenguajes orientados a objetos son el tema del capítulo 4, con Ada, Object Pascal, C++ y Java como ejemplos. El capítulo 5 está dedicado al paradigma que incluye lenguajes para programación distribuida, los que ponen en práctica procesamiento en pa­ ralelo. Aquí veremos, además de Ada, los modelos de memoria compartida (Pascal S), paso de mensajes (Occam) y C-Linda, el cual instrumenta procesamiento concu­ rrente haciendo uso de una organización de memoria denominada espacio de tupias. Parte III: Lenguajes formales y autómatas La parte III es un solo capítulo que examina los lenguajes formales. Su organización sigue la jerarquía de Chomsky de los tipos de lenguaje, enfocándose en usos prácti­ cos para lenguajes de programación. Se enfatiza en el tipo 2, los conocidos como lenguajes libres de contexto, que forman el basamento teórico para las definiciones de muchos lenguajes existentes. El estudio de los lenguajes formales y máquinas teóricas en las cuales éstos pueden ser implementados (teóricamente) pueden cons­ tituir una gran parte del material para un curso en ciencias teóricas de la compu­ tación. Algunos planes de estudio no incluyen un curso de tal naturaleza o algunos estudiantes no los eligen como una alternativa. Por lo tanto, incluimos este breve capítulo para aquellos que no estudiarán lenguajes formales con gran profundidad. Parte IV: Lenguajes declarativos Los lenguajes declarativos son aquellos basados en relaciones o funciones, en los que el programador no considera la asignación de valores a localidades de almace­ namiento (memoria), pero piensa en términos de valores funcionales o las relacio­ nes de entidades entre sí cuando se resuelve un problema. En el capítulo 7 examinamos PROLOG como un ejemplo de un lenguaje para la programación ló­ gica. El dialecto SCHEME de LISP se presenta como un lenguaje funcional en el capítulo 8, y SQL es el ejemplo de un lenguaje para manipulación de bases de datos analizado en el capítulo 9. Algo del material teórico ha sido puesto en apéndices para facilitar la lectura de estos capítulos. Sólo fines educativos - FreeLibros

TAREAS PRÁCTICAS DE LABORATORIO Si bien una copia de por vida para el aprendizaje puede ser el objetivo principal de la educación, los estudiantes de computación también necesitan desarrollar habili­ dades de lenguaje. Hemos incluido tareas de laboratorio para esta formación de habilidades necesaria para el mundo del trabajo y el refuerzo que trata con lengua­ jes concretos proporcionados para el aprendizaje conceptual. Un estudiante que complete con éxito un laboratorio semanal habrá ganado alguna modesta habili­ dad en varios de los lenguajes en este texto. Los estudiantes que se enfoquen a un lenguaje por primera vez, pueden usar uno de los siguientes métodos: • •

Comenzar aprendiendo construcciones básicas, mientras escriben programas cada vez más complejos. Examinar programas modelo y modificarlos o extenderlos lo mejor que pue­ dan.

Hemos empleado generalmente la segunda opción en los 25 laboratorios de este texto, lo cual permite que se aprecie algo del sabor de los lenguajes individuales. A los estudiantes se les pide entonces que modifiquen los programas o les agreguen detalles. Un estudiante podría ser asignado a cerca de la mitad de estos laborato­ rios en un semestre dependiendo del enfoque del curso. Pueden hacerse ya sea como laboratorios cerrados o abiertos dependiendo del entorno de enseñanza de la escuela. Hemos utilizado ambos tipos de laboratorio, y hemos encontrado que una combinación donde los estudiantes hacen algo de trabajo en un laboratorio super­ visado y algo por su cuenta es lo ideal. Cada laboratorio puede ser completado en aproximadamente una hora y media. Las tareas de laboratorio emplean 12 lenguajes diferentes: Ada, C, C++, Java, SCHEME (LISP), Object Pascal, Pascal, PROLOG, SQL y tres lenguajes para proce­ samiento en paralelo: Occam, Pascal S y C-Linda. Los compiladores y/o intérpre­ tes para todos éstos se pueden conseguir para el sistema operativo DOS. Las direcciones donde pueden encontrarse se facilitan en el apéndice C. Las tareas de laboratorio pueden hallarse en el Instructor^ M anual

ORGANIZACIÓN DEL CURSO En nuestra experiencia, el curso de lenguajes de programación beneficia más a los estudiantes si programan de manera activa en varios de los lenguajes estudiados. Pero cuando ellos disponen de un laboratorio además de las tareas de clase, gene­ ralmente son estas últimas las que llegan a despreciarse. Hay 241 ejercicios en el texto, pero ninguno que requiera solución por computadora. De este modo, espe­ raríamos que los estudiantes llegaran a clase preparados con respuestas para sólo algunos de los ejercicios, mientras que completen con éxito todos los laboratorios asignados. Estos materiales están destinados para un curso de un semestre o trimestre en lenguajes de programación, incluyendo un laboratorio semanal cerrado, abierto o mixto. Las 25 tareas de laboratorio están lejos de ser demasiadas para un curso de Sólo fines educativos - FreeLibros

un trimestre o un semestre, y un instructor puede elegir cuáles utilizar dependien­ do del enfoque del curso. Los laboratorios están dirigidos a construcciones particu­ lares de los lenguajes, y algunos pueden ejecutarse seleccionando un lenguaje. El instructor puede elegir en qué lenguajes suministrar experiencias de programa­ ción. La tabla P.l proporciona una lista de los recursos de laboratorio y ejercicios así como una estimación del tiempo requerido para cubrir los diversos materiales pro­ porcionados. Una lectura se considera de 75 minutos, y un laboratorio puede ser de hasta 2 horas. Si los estudiantes no tienen programado un laboratorio, debería emplearse algún tiempo en clase para familiarizarse con una tarea de laboratorio antes de que se les pida completarlo por sí mismos. Un semestre típico se compone de 15 semanas, o 13 si se excluyen los periodos de clase usados para pruebas y repasos. Una clase que reúne tres horas a la semana para lectura, más una vez a la semana para un laboratorio de 1 a 2 horas, debería ser capaz de terminar los 9 capítulos del texto y completar 13 de los 25 laboratorios. La selección de cuáles laboratorios asignar dependerá del enfoque del curso y los lenguajes que estén disponibles en la escuela. Algunos de los laboratorios están disponibles con opción de lenguajes y un instructor puede desear tener estudian­ tes haciendo el mismo problema en dos o más lenguajes. Por ejemplo, el segundo laboratorio de procesamiento en paralelo del capítulo 5 puede hacerse en Pascal S, C-linda y Occam. Los estudiantes se verían beneficiados al repasar los tres lengua­ jes. Ordinariamente, se asignaría a los estudiantes un laboratorio en sólo uno de los lenguajes en que lo hemos implementado, es decir, Ada, C o Pascal para lenguajes estructurados en bloques y Ada, C++ o Java para lenguajes orientados a objetos. La mayoría de los semestres no tienen 32 periodos de lectura de 75 minutos, sino 26, de modo que deben hacerse algunas selecciones. Todos deberían repasar los capítulos 0 a 3, pero después de esto, lo que se omitirá depende de lo que otros cursos ofrezcan en la escuela. Si los estudiantes van a tener un curso completo T A B L A P.l M ateriales en este texto

Introducción Tipos de datos Abstracción Estructura de bloques POO Procesamiento en paralelo Lenguajes formales Programación lógica Lenguajes funcionales Lenguajes para DMBS Cálculos lógicos El cálculo lambda

sobre procesamiento en paralelo, el capítulo 5 puede omitirse. Si hay un curso teó­ rico de ciencias de la computación o de bases de datos, podrán dejarse fuera los capítulos 6 o 9. En un sistema trimestral, pueden omitirse los capítulos 5 ,6 y 9. Podría esperar­ se que estos estudiantes completaran nueve o diez laboratorios.

V IÑ ETAS H ISTÓ RIC A S Los lectores pueden aprender un poco acerca de lo que han hecho las personas en la vida real gracias a las viñetas históricas acerca de prominentes innovadores de lenguajes. Éstas son historias sobre gente real que hizo contribuciones importantes para el desarrollo de los lenguajes de programación. El más antiguo es Aristóteles, quien dio inicio a la lógica formal, mientras que el más reciente es el equipo que diseñó el lenguaje Ada para el Departamento de Defensa de Estados Unidos.

M A TER IA LES D ID Á C TIC O S Los materiales didácticos que complementan el texto incluyen: 1. 2. 3.

241 ejercicios de papel y lápiz al final de las principales secciones del texto. El apéndice C, que contiene fuentes para obtener software. Un Instructor's Manual que contiene: • • •

Soluciones a los ejercicios en el texto. 25 tareas de laboratorio. Los instructores pueden proporcionar a sus alum­ nos copias de los laboratorios que quieran que los estudiantes completen. El código fuente necesario para que los estudiantes completen los labora­ torios puede ser descargado o “bajado" desde http://www.mhcolIege.com. Asegúrese de leer el archivo README para una descripción del contenido del directorio.

Los programas están destinados a su distribución entre los estudiantes. Las soluciones sugeridas son para el uso del instructor. Estos programas son sustancia­ les, y han sido extensamente probados para su ejecución en estaciones de trabajo DOS, Windows o UNIX. Son programas de ejemplo bien escritos para ser modifi­ cados o completados por los estudiantes.

LO N U EV O EN ESTA ED IC IÓ N La primera edición de este libro fue escrita por el primero de los autores, que estu­ vo encantado de haber trabajado con el segundo autor en esta versión. En respues­ ta a las sugerencias de nuestros revisores, los primeros capítulos acerca de conceptos básicos y estructura de bloques fueron rescritos para incluir más conceptos y me­ nos ejemplos de lenguaje, ya que los estudiantes encuentran confuso el pasar de una sintaxis de lenguaje a otra. El capítulo 1 fue dividido en los nuevos capítulos, 1

y 2, y rescrito en seudocódigo tipo Pascal/Ada para aclarar las nociones básicas de tipeado, variables, datos, control y abstracción de procedimiento, así como tam­ bién cuestiones de implementación. El capítulo 4 (lenguajes orientados a objetos) fue ampliado para incluir más material acerca de estos lenguajes. Puesto que algunas de las características de la orientación a objetos ha sido agregada al nuevo Ada 95, hemos incluido un análisis de estas nuevas características. Hemos agregado un análisis del nuevo lenguaje Java de Sun Microsystems y tres nuevos laboratorios de Java al capítulo 4. Java se encuentra disponible por parte de Sun mediante un ftp anónimo en Internet (véase el apéndice C). El capítulo 5 (procesamiento paralelo y distribuido) incluye más ejemplos de lenguaje y tres nuevos laboratorios, e implementa soluciones para el problema del productor-consumidor en Pascal S, C-Linda y Occam, así como también el labora­ torio Ada incluido en la primera edición. Los nuevos laboratorios fueron hechos para poner en práctica este curso basado en PC, puesto que recientemente han aparecido compiladores y equipo de cómputo (hardware) de bajo costo. Tableros individuales llamados transputores pueden ser instalados e interconectados en PC y accesados a través del lenguaje Occam. Linda es un paradigma de programación que puede ser simulado, ya sea en C o en Ada. Pascal S es un subconjunto del Pascal estándar. Los capítulos 6 y 8 fueron rescritos por completo con el objetivo de aclararlos y simplificarlos. El material sobre el cálculo de predicados (capítulo 7) fue traslada­ do a un apéndice, para ser estudiado por los estudiantes que tengan poco conoci­ miento de la lógica formal. También movimos la discusión teórica del cálculo lambda (capítulo 8) a un apéndice. El material teórico para otros capítulos, que fue separa­ do del texto principal en las secciones llamadas "Excursiones teóricas" en la prime­ ra edición, fue simplificado e incluido en el cuerpo del texto. El material sobre lenguajes que fueron tratados sólo de manera breve en la primera edición fue eliminado. De este modo aquí se encontrará sólo un somero análisis de APL, BASIC, COBOL, SNOBOL, SETL, Modula-2, o "pequeños lengua­ jes" especiales. Sin embargo, agregamos alguna discusión del lenguaje funcional tipeado ML al capítulo 8. Se rescribió el código en el texto para conformarlo a las nuevas versiones de los diversos lenguajes utilizados.

RECONOCIMIENTOS Las viñetas históricas fueron escritas por nuestra ex alumna, Laurie Sexton, quien también fungió como asistente de investigación, lector y crítico. Carol Torsone del College of Saint John Fisher proporcionó los laboratorios para el capítulo 5 de procesamiento en paralelo, además del implementado en Ada por George Benjamín. Karen Appleby, de los Laboratorios Watson de IBM, escribió los tres nuevos laboratorios de Java. La primera edición se benefició con las cuidadosas lecturas hechas por varios revisores, quienes en aquel momento quedaron en el anonimato para el primer autor. Apreciamos todos sus minuciosos pensamientos y comentarios. Ellos son: Sólo fines educativos - FreeLibros

Jane Hill, Dickinson College; Jim Beug, California Polytechnic, San Luis Obis­ po; Jon Manney, North Carolina State University; Richard Salter, Oberlin College; Rob Lyndon, Ohio University; Walter Pharr, College of Charleston; Stan Seltzer, Ithaca College; Tom Meyers, Colgate University; Dale Hanchey, Oklahoma Baptist University, y David Jackel, Lemoyne College. Un segundo grupo de revisores hizo muchas sugerencias para modificaciones a la segunda edición. Ellos son: Benjamin Zom, University of Colorado at Boulder; Rajive Bagrodia, UCLA; Shermane Austin, City College of New York; Harold Grossman, Clemson University; Brian Molloy, Clemson University; Salih Yurttas, Texas A&M University; Ephraim Glinert, Rensselaer Polytechnic Institute; T. Ray Nanney, Furman University; Manuel E. Bermúdez, University of Florida; Patty Brayton, Oklahoma City University; K. N. King, Georgia State University, y Donald Bagert, Texas Tech University. Y finalmente, debemos agradecer a nuestro editor, Eric Munson de McGrawHill. Tuvimos necesidad de su atención especial para perseverar hasta el final des­ de la conceptualización hasta la demostración final. Sin Holly Stark, la competente asistente de Eric, el libro nunca habría visto la luz del día. Doris Appleby Julius VandeKopple

En los primeros tres capítulos, examinaremos los conceptos y notación básicos que serán usados a lo largo del libro. Comenzamos en el capítulo 0 con los paradigmas del lenguaje que forman la estructura para nuestro estudio de los lenguajes de pro­ gramación. Los términos básicos del lenguaje incluidos aquí también forman un contexto para discutir las características de lenguajes individuales en capítulos pos­ teriores. Asimismo, presentaremos la notación que se utiliza comúnmente al des­ cribir la sintaxis del lenguaje, la estructura de construcciones de lenguaje válidas. El capítulo 1 incluye temas de tipos de datos, tanto aquellos primitivos para un lenguaje como los tipos estructurados, definidos por el usuario o formados con entradas de otros tipos. Al examinar las variables del programa, consideramos sus atributos y el tiempo que estos atributos están ligados a la variable. Éstas son im­ portantes para una comprensión clara de cómo funciona un lenguaje. Veremos los conceptos de abstracción en el capítulo 2. En la abstracción de datos, los valores de los datos y las operaciones sobre esos valores se consideran en conjunto. La secuencia de las acciones de computadora forma la base para la abs­ tracción del control. La sección acerca de la abstracción de procedimiento incluye un análisis de métodos de paso de parámetros, así como un resumen de la modularización de programas y tipos de datos abstractos.

CAPÍTULO 0 INTRODUCCIÓN 0.0 En este capítulo 0.1 Resolución de problemas Herramientas matemáticas Álgebra Lógica Teoría de conjuntos Teoría de funciones Niveles conceptuales y de implementación 0.2 Paradigmas de lenguaje Paradigmas imperativos El paradigma estructurado en bloques El paradigma basado en objetos El paradigma de la programación distribuida Paradigmas declarativos El paradigma de la programación lógica El paradigma funcional El paradigma de lenguaje de base de datos Ejercicios 0.2 0.3 Consideraciones prácticas Desde el bajo, pasando por el alto, hasta el muy alto nivel

Programación a gran escala Problemas especiales Procesamiento de datos Gráficos Integraciones en tiempo real Ejercicios 0.3 0.4 Criterios de lenguaje Descripciones bien definidas BNF y EBNF Semántica Comprobabilidad Confiabilidad Traducción rápida Código objeto eficiente Ortogonalidad Generalidad Consistencia y notaciones comunes Uniformidad Subconjuntos Extensibilidad Transportabilidad Ejercicios 0.4 0.5 Resumen 0.6 Notas sobre las referencias

Los programas de computadora se utilizan para resolver problemas, y ha habido miles de años de trabajo en matemáticas para este fin. Los lenguajes de programa­ ción están especificados por reglas para formar instrucciones correctas, organizándolas en módulos, someterlas hacia un compilador, el cual traduce el código en un lenguaje comprensible para una máquina en particular, y finalmente ejecutar el programa, es decir, someter la entrada hacia la computadora, la cual la transforma en una salida de acuerdo con las instrucciones en el programa. Este capítulo sirve como una introducción a lo que está por venir.

Métodos matemáticos tradicionales para resolver problemas y cómo han influenciado el desarrollo de los lenguajes de programación. La jerarquía de los paradigmas del lenguaje (tipos de lenguajes): colocar los lenguajes dispares en orden. Algunas consideraciones prácticas en el mundo de los usuarios de compu­ tadora. Criterios para decidir si un lenguaje es "bueno" o "malo". Una breve introducción a las formas Backus-Naur y Backus-Naur Extendida para describir la sintaxis del lenguaje.

RESOLUCIÓN DE PROBLEMAS Cuando usamos una computadora, estamos intentando resolver un problema. Pue­ de ser un problema de negocios que involucra ganancia y pérdida; un problema cien­ tífico que emplea modelos de comportamiento físico; una investigación estadística que evalúa la posibilidad de que ocurra algún evento; un ejercicio de lingüística en

la interpretación del lenguaje natural, o sólo simple procesamiento de texto. La gen­ te resolvía problemas mucho antes de que las computadoras llegaran a ser algo co­ mún, lo cual dio como resultado un tesoro de experiencia para beneficiarnos hoy. Charles Hoare afirma que "el propósito primario de un lenguaje de programa­ ción es ayudar al programador en la práctica de su arte" [Hoare, 1973]. Esta prácti­ ca consiste en el diseño, codificación, documentación y depuración del programa. Las ayudas clásicas de resolución de problemas benefician a los programadores de muchas maneras, incluyendo el diseño de programas. Herramientas matemáticas Cualquier problema que pueda ser expresado simbólica o numéricamente está in­ cluido en el ámbito de las matemáticas. Por lo tanto, la mayoría de los lenguajes de computadora está basada en esta disciplina. Los matemáticos han trabajado de di­ ferentes maneras para representar hechos en formas económicas y no ambiguas. Podemos representar la suma de 1 y 2 mediante 1 + 2 , PLUS(1,2) o ADD 0001 0010. Estas son tres sintaxis diferentes para la misma idea. La sintaxis de un lenguaje de programación está mucho más cercana a un lenguaje formal, en el sentido matemá­ tico, que a los lenguajes naturales que utilizamos en el habla cotidiana. Es impor­ tante mantener en mente la distinción entre los lenguajes naturales, con sus ambigüedades de semántica, y los lenguajes formales precisos. El estudio de los lenguajes de programación mismos es mucho más directo cuando se enfoca desde una perspectiva formal que por costumbre. Consideraremos los lenguajes formales y sus relaciones con los dispositivos de cómputo en el capítulo 6. Cada instrucción matemática o de programas tiene sintaxis (forma) y semántica (significado). La semántica de cada una de las representaciones de "uno más dos" debería adherirse a las nociones estándar de la suma de dos números naturales. En cualquier lenguaje de programación, cada instrucción no debe tener ambigüeda­ des tanto sintáctica como semánticamente. Además, un compilador o intérprete debe ser capaz de decidir si un programa es sintácticamente correcto. Si es así, el sistema de tiempo de ejecución debe entonces correr el programa de acuerdo con su semántica. Nuestra comprensión de estas nociones debe mucho a los matemáti­ cos y lógicos. A lgebra La resolución de problemas matemáticos ha avanzado a través de la aritmética, geometría, álgebra, análisis y sus distintas extensiones y subtópicos. Este gran cuerpo de metodología ha sido automatizado, permitiendo el acceso al usuario a través de lenguajes de programación imperativos de alto nivel, los cuales se definen en la siguiente sección. En la medida de lo posible, la notación de lenguaje, o sintaxis, se conforma al uso matemático aceptado. Además de los métodos de resolución de problemas mencionados anterior­ mente, los sistemas han sido desarrollados con sus propias reglas y lenguajes ma­ temáticos. Tres de los más ampliamente usados son la lógica, la teoría de conjuntos y la teoría de funciones.

Lógica La lógica es la ciencia del razonamiento. Si seguimos su sintaxis y reglas, podemos deducir nuevos hechos a partir de hechos anteriores. También sabemos que los nuevos hechos son tan correctos como lo eran los anteriores. Por ejemplo, si "Todos los pájaros pueden volar" y "Piolín es un pájaro" son declaraciones verdaderas, podemos deducir que "Piolín puede volar" aplicando las reglas del cálculo de pre­ dicados. Para razonar lógicamente, primero debemos decidir lo que constituye una ora­ ción o instrucción y lo que no. Esto es especificado por la sintaxis del lenguaje que se está usando en un sistema lógico en particular. Entre las instrucciones correcta­ mente escritas, algunas son verdaderas y algunas no lo son. A principios de siglo, los matemáticos pensaban que todas las matemáticas podrían expresarse mediante la lógica formal. Aunque esto resultó no ser cierto, así como no es cierto que todos los problemas pueden ser resueltos por computadora, los métodos desarrollados han probado su valor en matemáticas, lingüística y cien­ cias de la computación. Veremos esto con mayor profundidad en el capítulo 7.

Teoría de conjuntos La teoría de conjuntos es otro formalismo. Las esperanzas de que podría capturar todas las matemáticas fueron esbozadas a principios de los treinta cuando se des­ cubrieron inconsistencias. Los profesionales en muchos campos trabajan a gusto con los conjuntos y los encuentran ventajosos para resolver problemas. De este modo, muchos lenguajes de programación incorporan conjuntos directamente en las estructuras disponibles. Un lenguaje, SETL2, que mencionaremos sólo breve­ mente, está basado por completo en conjuntos.

Teoría defunciones Después de las fallas tanto de la lógica formal como de la teoría de conjuntos para incorporar todas las matemáticas, Alonzo Church intentó la tarea a través de las funciones. Una función, como la palabra implica, especifica alguna clase de acción o transformación de la información. La suma es una función que transforma dos números en un tercero de acuerdo con reglas particulares. Un programa de compu­ tadora puede ser visto como una función también, pues transforma su entrada en su salida. Los lenguajes basados en funciones han sido especialmente útiles en el campo de la inteligencia artificial (AI, por sus siglas en inglés). Aunque el progra­ ma original de Church para expresar todas las matemáticas a través de funciones fracasó, el lenguaje de AI más popular, LISP (un acrónimo de procesamiento de lista, LISt Processing en inglés), debe tanto su sintaxis como parte de su semántica al cálculo lambda de Church, el cual discutiremos brevemente en el apéndice B. Examinaremos LISP en el capítulo 8. El más general de todos los lenguajes de programación puede describirse ha­ ciendo uso de funciones y puede utilizarse como entrada para la más general de todas las computadoras, la máquina de Turing. Examinaremos brevemente estos temas teóricos en el capítulo 6.

Niveles conceptuales y de implementación Cualquier lenguaje de programación puede ser considerado equivalente a cualquier otro, en el sentido de que cada uno cambia los valores del almacenamiento.1 Sin embargo, pueden ser bastante diferentes tanto al nivel de concepto como al nivel de implementación. Un lenguaje se organiza en tomo a un modelo conceptual particu­ lar. Un programador de LISP no se preocupa por el almacenamiento, pero piensa en términos de funciones, átomos y listas. Cuando se programa en lógica considera­ mos relaciones y cláusulas. Cuando se trabaja en Pascal, pensamos "de arriba hacia abajo" en términos de procedimientos para realizar determinadas tareas. Una vez que un lenguaje ha sido desarrollado conceptualmente, debe ser implementado de modo que sus estructuras básicas puedan representarse al nivel de bits. Éste es el trabajo del diseñador de compiladores, quien también trabaja a varios niveles conceptuales. En este texto mencionaremos formas de implemen­ tación, pero trabajaremos extensamente en el nivel conceptual.

PARADIGMAS DEL LENGUAJE La noción de paradigmas científicos puede hallarse en el libro La estructura de las revoluciones científicas (The Structure ofScientific Revolutions), de Thomas Kuhn. Él las define como "logros científicos universalmente reconocidos que durante un tiem­ po proporcionan problemas y soluciones modelo para una comunidad de profesio­ nales" [Kuhn, 1962]. Peter Wegner extiende la noción a los paradigmas de lenguajes de programación, los que "pueden definirse comprensiblemente por sus propieda­ des o extensionalmente por uno o más ejemplos" [Wegner, 1988]. Estos términos, comprensión y extensión son tomados prestados de la teoría de conjuntos. Un con­ junto puede ser definido comprensiblemente mediante la descripción de los miem­ bros del conjunto. Por ejemplo, "S es el conjunto de todos los perros negros y blancos" define el conjunto S comprensiblemente. Cualquier perro negro o blanco está des­ tinado a estar en S. D = {Spot, Snoopy, Tyge} está definido extensionalmente. D está construido a partir del conjunto vacío 0 al extenderlo tres veces, D = 0 u {Spot} kj {Snoopy} u {Tyge}. El paradigma estructurado en bloques puede describirse comprensiblemente como el conjunto de todos los lenguajes de programación que soportan estructuras de bloque anidadas, incluyendo procedimientos, o puede ser descrito extensio­ nalmente por medio de la enumeración de lenguajes particulares con su caracterís­ tica, por ejemplo, LenguajesEstructuradosenBloques = {ALGOL, Pascal, Ada, Modula, C}. Al definir un paradigma, no se hace el intento de asegurar que la lista de ejemplares de lenguajes sea exhaustiva. Kuhn utiliza el término ejemplar para un ejemplo que ayuda a definir un paradigma. Un solo lenguaje que incorpore todas

1 El almacenamiento de una computadora es considerado por lo regular como un arreglo o matriz de celdas en las cuales se conservan los valores. Cada celda tiene un nombre único que puede recono­ cerse a través de un identificador legal de un lenguaje de programación. El almacenamiento puede ser implementado de diversas maneras en el hardware de diferentes computadoras físicas.

las características de un paradigma es entonces una realización ejemplar del para­ digma. Podemos investigar un paradigma en particular al explorar las característi­ cas de uno o más lenguajes representativos. Usted no estará muy equivocado si relaciona un paradigma de lenguaje de ejemplo muy bueno para una colección de ideas relacionadas. Un paradigma, y también sus ejemplares, es más útil cuando es simple y dife­ rencia claramente un lenguaje de otro. Los ejemplares pueden ser fabricados para servir de modelo, como ocurre con algunos lenguajes experimentales, o pueden ser lenguajes ya existentes. Podemos decir que Ada es "estructurado en bloques" y también "basado en objetos". De este modo Ada pertenece tanto al paradigma es­ tructurado en bloques como al basado en objetos. Si Ada puede o no servir como un ejemplar depende de su punto de vista de Ada. En este texto, exploraremos los paradigmas seleccionados por Wegner y sus colegas como representación de los lenguajes predominantemente en uso en la ac­ tualidad por grupos significativos de programadores e investigadores. El ensayo de Kuhn ha sido elogiado ampliamente y se considera que ha eleva­ do el nivel de análisis acerca de la naturaleza de la ciencia. El afirma que los logros científicos notables a menudo preceden el reconocimiento de un paradigma abs­ tracto. Tales logros sirven para definir los legítimos problemas y métodos de inves­ tigación en un área de investigación científica para las generaciones siguientes de profesionales. Cuando un nuevo paradigma significativo se hace conocido, atrae a un grupo de simpatizantes fuera de las metodologías competidoras. También debe ser suficientemente abierto para dejar que toda clase de problemas sean resueltos. Ejemplos clásicos de paradigmas científicos competidores son la dinámica aristotélica contra la newtoniana, o la astronomía ptolomeica contra la de Copémico. Un ejemplo más reciente es el de la teoría ondulatoria de la corriente eléctrica con­ tra su contraparte cuántica. Cada una de éstas sirve a un propósito útil en aplica­ ciones particulares. Kuhn sostiene que "a pesar de ambigüedades ocasionales, los paradigmas de una ciencia madura pueden ser determinados con relativa facili­ dad" [Kuhn, 1962]. Wegner cree que la ciencia de la computación está por llegar, si no es que ya lo ha hecho, a su madurez y que los paradigmas para los lenguajes de programación caen dentro de dos clasificaciones, imperativos y declarativos. Los lenguajes impera­ tivos especifican cómo se efectúa un cálculo mediante secuencias de cambios para el almacenamiento en la computadora, mientras que los lenguajes declarativos espe­ cifican qué es lo que se calculará. Kuhn sostiene que los paradigmas ayudan a especificar los acertijos apropia­ dos para ser resueltos, y que un científico es motivado "a tener éxito en la resolu­ ción de un acertijo que nadie haya resuelto antes o que no se haya resuelto tan bien" [Kuhn, 1962]. Wegner se refiere a los problemas en cuanto a la necesidad de resolución, más que como acertijos, y a los paradigmas como descripciones de "pa­ trones de pensamiento para la resolución de problemas" [Wegner, 1988]. Estos patrones son tan elusivos que, en la práctica, los paradigmas se abstraen de mode­ los de computación, lenguajes de ejemplo y características de lenguaje. Las abstrac­ ciones, y no los lenguajes individuales, son de principal importancia cuando se consideran los lenguajes de programación como un grupo. Trataremos con las no­ ciones de la abstracción en el capítulo 2. Sólo fines educativos - FreeLibros

Kuhn traza nuevos paradigmas a partir de la ruptura de uno anterior en una aplicación. Él comenta que "la reinstrumentación es una extravagancia reservada para la ocasión que la demande" [Kuhn, 1962]. El Departamento de la Defensa (DOD, por sus siglas en inglés) reconoció una ruptura, la cual estaba forcejeando en un mar de software escrito en cientos de lenguajes diferentes algunas veces in­ capaces de recibir mantenimiento y con frecuencia frágiles. El desarrollo del len­ guaje Ada, requerido ahora por todos los contratos de la Defensa, involucró el desarrollo simultáneo tanto de un paradigma como de un ejemplo. Quizá la parte más sorprendente del trabajo de Kuhn describe las revoluciones científicas dentro de su contexto social. Cuando existen paradigmas contrastantes, la elección de cuál mantendrá su influencia no siempre está basada en el mérito o la proximidad a la "verdad". Una comunidad u otra decide cuáles problemas son más importantes de resolver, y luego apoya el paradigma más promisorio para atacarlos. Esta decisión se hace en ocasiones mordazmente, con campos hostiles apoyando modelos diferentes. Muchas críticas se elevaron contra la obra La estruc­ tura de las revoluciones científicas cuando apareció por primera vez. Había confusión acerca de la noción misma de paradigma. En una posdata a la segunda edición [Kuhn, 1970], Kuhn intentó separar la noción en dos partes: la constelación de creen­ cias, valores y técnicas compartidas por una comunidad de profesionales, y los modelos concretos o ejemplos mismos. Él identifica cuatro componentes de una disciplina organizada alrededor de un paradigma en particular. Al primero lo lla­ ma generalizaciones simbólicas. Éstas son las reglas o leyes escritas del paradigma. El segundo lo componen las creencias de la comunidad de profesionales, las mane­ ras particulares de proceder que parecen más fructíferas. El tercero se compone de los valores de un grupo acerca de lo que es más importante. La simplicidad, tal como se encuentra en Pascal o LISP puro, podría ser más valorada que la aplicabilidad extendida, uno de los objetivos de PL/I. El cuarto y último compo­ nente son los ejemplares mismos, incluyendo los problemas a ser resueltos con sus soluciones. Los paradigmas de lenguajes de programación y los lenguajes no son inmunes a defensores y detractores. Ciertos lenguajes llegan a convertirse en linguae francae por razones comerciales, científicas o de otro tipo. Reconoceremos los cuatro com­ ponentes de Kuhn a medida que examinemos más de cerca los paradigmas y sus ejemplares de lenguaje particulares. Paradigmas imperativos Los paradigmas imperativos son aquellos que facilitan los cálculos por medio de cambios de estado. Entendemos por estado la condición de una memoria de acceso aleatorio de computadora (RAM),2 o almacenamiento. Es útil pensar en ocasiones en la memoria de la computadora como una serie de "instantáneas", cada una de las cuales captura los valores en todas las celdas de memoria en un momento en particular. Cada instantánea individual registra un estado.

2 El usuario puede leer o escribir en la RAM en contraste con la ROM, la cual es memoria de sólo lectura.

Cuando se introduce un programa, los datos asociados existen en una cierta condición, digamos una lista no ordenada fuera de línea. Es trabajo del programa­ dor especificar una serie de cambios para el almacenamiento que producirán el estado final deseado, quizás una lista ordenada. El almacenamiento involucra mucho más que los datos y un programa almacenado, por supuesto. Incluye tablas de símbolos, pilas en tiempo de ejecución, un sistema operativo y sus colas y pilas asociadas, etcétera. El programa completo, los datos e incluso el CPU mismo pue­ den visualizarse como parte del estado inicial. La primera tarea puede ser introdu­ cir la lista no ordenada y la tarea final la salida de la lista ordenada. Discutiremos las conexiones entre lenguajes y transiciones de estado en el capítulo 6.

El paradigm a estructurado en bloques FORTRAN, el primer lenguaje con bloques de programa, divide el estado en blo­ ques que representan subrutinas y datos comunes. Los bloques de FORTRAN se pueden pensar como un archivo plano, donde cada bloque sigue a sus predeceso­ res. Debido a esta estructura lineal, FORTRAN ya no es considerado como un len­ guaje estructurado en bloques, pero es un ejemplo de un lenguaje orientado a procedimientos, donde los programas se ejecutan a través de llamadas sucesivas para separar procedimientos. Las librerías de FORTRAN de procedimientos útiles y comprobados son una de sus características prácticas. El término estructuras en bloques se refiere ahora a los ámbitos anidados. Es decir, los bloques pueden estar anidados dentro de otros bloques, y pueden conte­ ner sus propias variables. El estado representa una pila con una referencia al blo­ que actualmente activo en la parte superior. En los lenguajes estructurados en bloques, el procedimiento es el principal bloque de construcción de los programas. Ejemplos de lenguajes son Ada, ALGOL 60, Pascal, ALGOL 68 y C.

El paradigm a basado en objetos El paradigma basado en objetos describe los lenguajes que soportan objetos en interacción. Un objeto es un grupo de procedimientos que comparten un estado [Wegner, 1988]. Puesto que los datos son también parte de un estado, los datos y todos los procedimientos o funciones que se le aplicarán pueden ser capturados en un solo objeto. Los ejemplos son Ada, donde los objetos son llamados pa­ quetes; Modula, donde se denominan módulos; y Smalltalk, donde los objetos se llaman (correctamente) objetos. En C++, una colección de objetos se agrupa en una clase. El término orientado a objetos fue utilizado originalmente para distinguir aque­ llos lenguajes basados en objetos que soportaban clases de objetos y la herencia de atributos de un objeto padre por parte de sus hijos. Ada 83 era considerado como basado en objetos, pero no orientado a objetos. Algunas características del paradig­ ma orientado a objetos han sido agregadas a Ada 95, pero algunos profesionales no lo consideran completamente orientado a objetos. Sólo fines educativos - FreeLibros

El paradigm a de la programación distribuida La programación concurrente ha sido dividida en dos amplias categorías, sistemas acoplados en forma débil o fuerte. El término distribuido se refiere por lo general a lenguajes para sistemas acoplados débilmente que soportan un grupo de progra­ madores trabajando en un programa particular de manera simultánea y comuni­ cándose a través de paso de mensajes mediante un canal de comunicación, tal como un enlace de punto a punto o una red de área local (LAN, por sus siglas en inglés). En un sistema distribuido acoplado débilmente, un lenguaje no necesita soportar compartir memoria simultánea, librándose así de algunos problemas. Un sistema acoplado fuertemente permite que más de un proceso en ejecución tenga acceso a la misma ubicación de memoria. Un lenguaje asociado con el siste­ ma debe sincronizar el uso compartido de memoria, de modo que sólo un proceso escriba a una variable compartida a la vez, y de modo que un proceso pueda espe­ rar hasta que ciertas condiciones se satisfa*gan por completo antes de continuar la ejecución. La memoria compartida tiene la ventaja de la velocidad, porque no se necesita pasar mensajes. La programación concurrente está asociada con más de un CPU funcionando simultáneamente en paralelo, compartiendo o no datos. Sin embargo, los CPU múltiples no son esenciales para este paradigma. Lo que es esencial es que el traba­ jo sobre un problema en particular pueda ser compartido. Ada es quizás el lengua­ je mejor conocido que soporte la concurrencia. En Ada, dos o más procedimientos se ejecutan de manera independiente. El compartimiento de resultados ocurre a través de un proceso llamado una reunión (rendezvous). Recientemente, se han hecho trabajos en lenguajes que difuminan la distinción entre los paradigmas acoplados débil y fuertemente. Lenguajes tales como PROLOG concurrente, Linda y Occam tienen algunas características de ambos. En el capítulo 5 consideraremos tanto los paradigmas de variable distribuida como compartida.

Paradigmas declarativos Un lenguaje declarativo es uno en el que un programa especifica una relación o función [Wegner, 1988]. Cuando se programa en el estilo declarativo, no hacemos asignaciones a variables del programa. El intérprete o compilador para el lenguaje en particular administra la memoria por nosotros. Estos lenguajes son de "nivel más elevado" que los lenguajes imperativos, en los que el programador opera más alejado del CPU mismo. Los tres paradigmas declarativos provienen de las matemáticas: la lógica, la teoría de funciones y el cálculo relacional.

El paradigm a de la programación lógica La programación lógica está basada en un subconjunto del cálculo de predicados, incluyendo instrucciones escritas en formas conocidas como cláusulas de Hom. El cálculo de predicados proporciona axiomas y reglas de modo que uno puede de­ ducir nuevos hechos a partir de otros hechos conocidos. Una cláusula de Hom Sólo fines educativos - FreeLibros

permite que sólo un nuevo hecho sea deducido en cualquier instrucción simple. Un sistema de cláusulas de Hom permite un método particularmente mecánico de demostración llamado resolución. Un programa basado en la lógica se compone de una serie de axiomas o he­ chos, reglas de inferencia y un teorema o cuestión por demostrarse. La salida es verdadera si los hechos soportan o apoyan la cuestión, y es falsa en el caso contra­ rio. PROLOG es el ejemplar para lenguajes de programación lógicos.

El paradigm a funcional Los lenguajes puramente funcionales operan solamente a través de funciones. Una función devuelve un solo valor, dada una lista de parámetros. No se permiten asig­ naciones globales, llamadas efectos colaterales. Un programa es una llamada de función con parámetros que posiblemente llaman a otras funciones para producir valores de parámetro real. Una llamada de función de este tipo podría ser: HacerNómina (LibrosBalance (CalculaSalarios (RegistrosEmpleado), LibrosViejos))

que devuelve el valor NuevosLibros. Durante la ejecución de HacerNómi na, no po­ drían hacerse cambios ni a Regi strosEmpI eado ni a Li brosVi ejos. Las funciones mismas son valores de primera clase que pueden ser pasados a otras funciones y devueltos como valores funcionales. Así, la programación funcio­ nal proporciona la capacidad para que un programa (función) se modifique a sí mismo, es decir, aprenda. En la práctica, existen pocos lenguajes puramente funcionales, ya que los efec­ tos colaterales básicos tales como entrada y salida son deseables. LISP es el lengua­ je funcional mejor conocido. El LISP puro existe y tiene devotos seguidores, pero las versiones de producción incluyen muchas características no funcionales.

El paradigm a del lenguaje de base de datos Las propiedades que distinguen a los lenguajes diseñados para tratar con bases de datos son la persistencia y la administración de cambios. Las entidades de base de datos no desaparecen después de que finaliza un programa, sino que permane­ cen activas durante tiempo indefinido como fueron estructuradas originalmente. Puesto que la base de datos, una vez organizada, es permanente, estos lenguajes también deben soportar los cambios. Los datos pueden cambiar y así también pue­ den hacerlo las relaciones entre objetos o entidades de datos. Un sistema de administración de base de datos incluye un lenguaje de definición de datos (DDL, por sus siglas en inglés) para describir una nueva colección de he­ chos, o datos, y un lenguaje de manipulación de datos (DML, por sus siglas en inglés) para la interacción con las bases de datos existentes. Los lenguajes de base de datos pueden estar integrados en otros lenguajes de programación para mayor flexibilidad. También se ha hecho un esfuerzo para ha­ cerlos fáciles de usar, de manera que los que no sean programadores puedan admi­ nistrar los datos y asuntos normales del mundo de los negocios.

E J E R C I C I O S 0. 2 1. Considere un lenguaje que usted conozca bien y analícelo en términos de los cuatro componentes del paradigma mencionados por Kuhn. a. Generalización simbólica: ¿Cuáles son las reglas escritas del lenguaje? b. Creencias de los profesionales: ¿Qué características particulares del lenguaje se cree que sean "mejores" que en otros lenguajes? c. Valores: ¿Qué pensamiento o estilo de programación consideraron mejor los crea­ dores? d. Ejemplares: ¿Qué clase de problemas pueden resolverse más fácilmente en el len­ guaje? 2. Si usted conoce más de un lenguaje, repita el ejercicio 1, comparando este segundo lenguaje con el primero. 3. FORTRAN, acrónimo de FORmula TRÁNslation (traducción de fórmulas), fue el pri­ mer lenguaje que intentó permitir a los programadores expresar sus problemas en notación matemática familiar. Nombre algunos ejemplos que sean fórmulas algebraicas perfectamente válidas, pero que no funcionen bien como expresiones de programación. ¿Cuál es el problema? Se deben considerar tanto las limitaciones de las computadoras como dispositivos de cálculo finitos como las limitaciones del equipo en particular, tal como los teclados y las pantallas de visualización. ¿Qué hay acerca de los símbolos mismos? 4. ALGOL fue el primer lenguaje algorítmico (ALGOrithmic Language). ¿Qué dispositi­ vos deben implementarse para manejar algoritmos con éxito? 5. Si la brevedad del código fuente es valiosa, podemos clasificar los lenguajes en tér­ minos de cuántas líneas son necesarias para escribir el código para un problema en particular. APL, C, BASIC, Pascal y COBOL se enumeran aquí en orden desde el más breve hasta el más extenso en cuanto a la longitud promedio del programa. Analice las características que promueven la brevedad del código en los lenguajes con los que usted esté familiarizado. 6. El procedimiento recursivo más familiar es la función factorial, FACTÍO) * 1 FACT(n) = n * FACTÍn-1), (n > 0)

La recursión a menudo se implementa como una pila en tiempo de ejecución, con un nodo que se inserta sobre la pila cada vez que se llama la función y que se extrae cuando ya no es necesario. Suponga que una referencia al bloque de código para FACI es F. (Aquí "referencia" se refiere a la dirección de memoria donde el código está almacenado.) Trace la pila para FACT (3) . ¿Qué información aparte de F debe estar en cada nodo de la pila para hacer que la recursión funcione?

0.3 CONSIDERACIONES PRÁCTICAS Los programas de computadora son escritos para explotar los límites de las computadoras y sus capacidades de resolución de problemas. Algunas veces su propósito es solucionar de modo eficiente algún problema particularmente tedioso en el mundo real de la ciencia, la industria y los negocios. Así, los lenguajes están diseñados para incorporar características particulares deseadas por los usuarios potenciales. Los lenguajes que fueron diseñados teniendo en mente usuarios parti­ culares incluyen COBOL para la comunidad de negocios y Ada para el DOD.

Desde el bajo, pasando por el alto, hasta el muy alto nivel Un lenguaje se considera de bajo nivel si uno puede manipular directamente la memoria de acceso aleatorio (RAM) de una máquina usando instrucciones en el lenguaje. Aquellos en el más bajo nivel son dependientes de la máquina y asignan valores de 0 o 1 a los bits individuales. Un programa muy simple en lenguaje ensamblador para inicializar un arreglo de cinco elementos al valor 0 es: MOV MOV MOV MOV MOV MOV

Después de la ejecución del fragmento, las palabras 428-436 de la RAM contendrán el valor 0. El código equivalente en Pascal es: var

El código de Pascal es más fácil de comprender, pero hemos perdido control preci­ samente sobre cuáles localidades de memoria contendrán el arreglo. El compilador de Pascal hace esto, supuestamente, de una manera eficiente. En APL podemos hacer la labor con: v <- 5 0

Aquí la instrucción es ciertamente rápida y fácil, pero hemos perdido incluso otro elemento de control sobre la máquina misma. Cuando un programa APL se ejecu­ ta, el espacio debe ser hallado por el vector Vde cinco elementos inmediatamente. V < - V,V

es también una instrucción APL perfectamente válida, la cual produce dos copias de Vrelacionadas juntas, es decir, Ves 0 0 0 0 0 0 0 0 0 0 . Los lenguajes como APL, LISP, PROLOG, SETL2 y SNOBOL son llamados "len­ guajes de muy alto nivel" porque permiten la manipulación directa de estructuras complejas de datos. En APL las estructuras básicas son arreglos y matrices; en PROLOG son relaciones; en SETL, conjuntos y mapas; y en SNOBOL, patrones o conjuntos de cadenas. El objetivo de estos lenguajes es hacer la programación más fácil. La especificación y "codificación" del programa son realizadas en un solo paso directo. Sólo fines educativos - FreeLibros

El precio que se paga por la escritura eficiente de programas puede ser la efi­ ciencia de ejecución. Los programas de muy alto nivel a menudo tienen grandes requerimientos de memoria y se ejecutan lentamente. Sin embargo, son muy útiles para hacer prototipos, los ensayos de versiones preliminares de nuevos sistemas. Los lenguajes son a menudo fáciles de aprender, y por lo tanto son adecuados para estudiantes novatos y para diseñadores de lenguajes de programación y otras apli­ caciones de computadora. Los escritores de compiladores y constructores de má­ quinas están trabajando para traducir programas escritos en estos lenguajes directamente en lenguajes intermedios más eficientes de modo que la parte costosa del desarrollo de la aplicación, los años-hombre de tiempo de programador, pueda ser minimizada. Programación a gran escala Una de las cosas que una computadora hace bien es recordar un gran número de hechos. También puede procesar estos hechos mucho más rápido que cualquier humano. Sólo piense en las posibilidades cuando un grupo de seres humanos tra­ bajan con un grupo de computadoras en un problema particularmente difícil, como la predicción del clima. Es perfectamente razonable abordar esto, puesto que los meteorólogos saben qué clases de datos son necesarias y existe un buen equipo para medir estos datos. El manejo de una economía mundial, donde las fluctuacio­ nes locales cotidianas en tasas monetarias y de bonos afectan los mercados alrede­ dor del mundo, es otro problema de grandes proporciones. Los intentos de coordinar los esfuerzos tanto de humanos como de máquinas se analizarán en los capítulos 4 y 5.

Problemas especiales A medida que las computadoras han ido haciéndose más accesibles, debido tanto al incremento de sus capacidades como a la reducción de precios, están siendo utilizadas para realizar más tareas. Cuando un uso en particular llega a ser impor­ tante en una industria, esto en ocasiones paga el costo de desarrollar máquinas y lenguajes especializados para esa tarea.

Procesam iento de datos Una de las primeras áreas donde las computadoras fueron obviamente útiles fue en el manejo de cantidades masivas de datos. La máquina tabuladora de Hermán Hollerith se utilizó por primera vez en la compilación del censo de 1890 en los Estados Unidos. Durante los últimos 50 años, toda corporación importante debía tener ion departamento de procesamiento de datos (DP, por sus siglas en inglés). El personaje de Dickens, Bob Cratchit, era el clásico procesador de datos con sus li­ bros de contabilidad y su pluma fuente. A medida que estas tareas se fueron meca­ nizando, se desarrollaron los lenguajes de cuarta generación (4GL; 4th Generation Languages) para satisfacer estas necesidades especiales. Sólo fines educativos - FreeLibros

¿Por qué cuarta generación? Aunque diferentes autores dividen los lenguajes de manera distinta, un grupo razonable está de acuerdo en considerar a los lengua­ jes de máquina como de primera generación, a los lenguajes ensambladores como de segunda generación y de tercera generación a los lenguajes de procedimientos.3 Estos últimos incluyen FORTRAN, COBOL, ALGOL, Pascal y C. Gary Hansen [Hansen, 1988] describe los 4GL como los lenguajes que cumplen con las siguientes cinco propiedades: 1. 2. 3. 4. 5.

Programación y estructuras de base de datos, Un diccionario de datos centralizado que contenga información acerca de los componentes del sistema. Programación visual, tal como en el uso de un ratón con iconos. Una interfaz de usuario con habilidades graduadas que permita tanto a los novatos como a los expertos en bases de datos hacer uso de los programas. Un ambiente de programación interactivo, integrado, en funciones múltiples.

Aunque un examen de los lenguajes de cuarta generación completamente funcio­ nales como NOMAD y Application Factory está más allá del alcance de este libro, incluimos el lenguaje de base de datos de IBM llamado SQL en el capítulo 9. G ráficos Los gráficos, por supuesto, están relacionados con gráficas, diagramas y otras re­ presentaciones visuales de datos. Aquí necesitamos lenguajes que puedan mani­ pular puntos individuales (pixeles) sobre una pantalla, monitor o dispositivo de impresión. Más difícil es la incorporación de un lenguaje de gráficos en un lenguaje de programación existente. Éste es a menudo el trabajo de un ambiente de desarro­ llo integrado (IDE, por sus siglas en inglés), el cual también puede incluir editores, depuradores, etc. Un IDE no es parte de un lenguaje per se, pero en ocasiones viene en paquete con un compilador.

Integraciones en tiem po real Las computadoras pueden realizar otras tareas aparte de producir una salida im­ presa a partir de una entrada numérica dada. También pueden enviar señales a aparatos para hacer una cosa u otra, dadas ciertas condiciones. En un caso así, una computadora y sus lenguajes pueden estar insertados o "integrados" en otra má­ quina más grande. Como ejemplos tenemos los monitores médicos, que regulan automáticamente dosis intravenosas dependiendo de los datos tomados del pa­ ciente, los pilotos automáticos en los aviones y la totalidad de la iniciativa de de­ fensa estratégica de Estados Unidos. Uno de los principales propósitos del lenguaje

3 En esta categorización de lenguajes por generación, es interesante el hecho de que LISP no pueda clasificarse en ningún lado. PROLOG es generalmente considerado como un lenguaje de quinta genera­ ción, y Ada puede catalogarse como una extensión de los lenguajes de tercera generación. Los lenguajes funcionales parecen formar por ellos mismos una generación sin nombre fuera de clasificación.

patrocinado por el DOD, Ada, es facilitar estas integraciones en tiempo real. A lo largo del texto veremos ejemplos de características del lenguaje que soportan esta actividad. E J E R C I C I O S 0.3 1. Suponga que queremos imprimir los arreglos definidos en los listados (0.3.1)-(0.3.3). ¿Cuál esperaría que se imprima más rápido? ¿Más lento? ¿Por qué? 2. El lenguaje BASIC permite arreglos no declarados de hasta diez elementos. ¿Por qué piensa que los diseñadores forzaron a los usuarios a declarar arreglos más grandes pero no los pequeños?

0.4 CRITERIOS DE LENGUAJE Existen, o han existido, literalmente cientos de lenguajes de programación. Mu­ chos ya no se usan, mientras que las nociones de otros han sido incorporadas en otros lenguajes. A lo largo de este texto, examinaremos los siguientes criterios para considerar que un lenguaje tiene méritos. Existen muchas otras listas. Éstos fue­ ron sugeridos por primera vez por Barbara Liskov en un curso en el MIT y fueron reportados en Horowitz, 1984. Los criterios están interrelacionados, es decir, un lenguaje con una descripción bien definida puede ser confiable y eficiente, en parte debido a su descripción. Nosotros simplemente definimos los términos aquí.

Descripciones bien definidas Los programadores de FORTRAN o PL/I trabajaban a menudo como un grupo. Si uno no sabía o había olvidado cómo escribir el código para efectuar una tarea par­ ticular, la cosa más fácil por hacer era bajar al vestíbulo y preguntarle a un amigo. Los manuales eran volúmenes inmensos pobremente organizados que enseñaban mediante ejemplos con más frecuencia que por cualquier otro método.

BN FyEBN F rLos diseñadores de ALGOL 60 rectificaron esto al proporcionar una sencilla des­ cripción del lenguaje en 18 páginas. La sintaxis del lenguaje está descrita en la for­ ma Backus-Naur Form (BNF), seguida de ejemplos de programación. BNF es un ejemplo de un metalenguaje, un lenguaje utilizado para describir otro lenguaje, en este caso uno de programación. BNF tiene símbolos, llamados metasímbolos, y reglas propias, las cuales son empleadas para definir la sintaxis del lenguaje parti­ cular de programación en cuestión?) ÍPor sintaxis entendemos una colección de instrucciones formadas al seguir un conjunto de reglas que diferencian los programas válidos de los no válidos.)La sintaxis por sí misma no da significado a un lenguaje; meramente define la colec­ ción de frases y sentencias que son combinaciones válidas de los caracteres del lenguaje. Examinaremos las definiciones del lenguaje con más cuidado en el capí-

tulo 6. No obstante, a fin de comprender las descripciones de lenguaje que siguen, usted necesitará comprender un poco de BNF desde ahora. (^BNF emplea los metasímbolos : |, <, >, . , y negritas, como se ve a continuación: Metasímbolo Significado : : se define como | alternativamente, o

se remplaza por su definición

algo

Una palabra escrita en negritas se conoce como terminal o token que indica un elemento de lenguaje indivisible que no permi­ te otros remplazos^)

En la breve discusión de BNF que sigue, utilizaremos el seudocódigo tipo Pascal, usado cuando describamos las características del lenguaje en los capítulos 1-3, como un ejemplo. Comenzaremos con la definición BNF para un programa en seudocó­ digo mostrado en el listado (0.4.2). <programa>

<encabezado-programa>, ;'

(0.4.2)

<encabezado~programa> ::*prograa progra* ;

<parte-def i ni ci ón-constantes> <parte-defi ni ción-tipos> <parte-declaración-variables> <parte-declaración-procedimientos-funciones> <parte-declaraciones>

<parte-dedaraciones> <declaración-compuesta>

<declaración-compuesta>

begln <secuencia-declaraciones> end

Los identificadores de seudocódigo están descritos en BNF como: (

::= |<1dent1f1cadorxletra>|<1dent1f1cador>

(0.4.3)

La definición BNF se puede leer como, "Un identificador se define como una letra, o un identificador seguido por una letra, o un identificador seguido por un dígito". / Observe que la definición es recursiva, puesto que

::-a|b|c|d|e|f|g|h|1|J|k|l|B|n|o|p|q|r|s|t|u|v|«|x|y|z

>

(0.4.4)5

0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8|9

4 En algunas versiones de BNF, ::= se remplaza por = o por -> , y

por algo. Los tokens pueden escribirse en comillas simples, para indicar que son indivisibles, es decir, 'a' en vez de a. 5 Nuestro seudocódigo no es sensible al tamaño de letra (minúsculas o mayúsculas), de modo que el listado de las letras minúsculas implica la inclusión de letras mayúsculas también.

Sólo fines educativos - FreeLibros

18

PARTE I: Conceptos preliminares

Ejemplos de identificadores son: q, G, Sopa, V17a, a34kTMNs,y MARI LYN. Para demos­ trar que V17a conforma con la definición, hagamos las sustituciones siguientes:

(0,4.5)

a a 7a 7a <1dent1ficador>l7a <1etra>17a V17a

^Puesto que las definiciones BNF no siempre son obvias, los diagramas de sin­ taxis o gráficas de ferrocarril han llegado a ser populares, en especial en manuales de lenguaje elementales.]La gráfica para un identificador se ilustra en la figura 0.4.1. Si uno sigue las flechas, se encuentran las mismas restricciones que en la defi­ nición BNF. Nuestro seudocódigo no es sensible a la caja tipográfica de las letras, de modo que pueden utilizarse tanto mayúsculas como minúsculas en la defini­ ción. \Los caracteres individuales son tokens, y nuestro seudocódigo tiene también otros tokens. Algunos de éstos son símbolos especiales, tales como +, - , =, ; y : Otros son conocidos como símbolos-palabra, los cuales incluyen las palabras reservadas, que no pueden ser redefinidas dentro de un programa. Observe que estas palabras reservadas siempre aparecen en negritas. La BNF para éstas es: <símbolo-palabra>

prograt | const | type | procedure | functlon | var | begln | end | dfv | aod | and | not | or | 1n | array | file | record | set | case | of | for | to | downto | do | 1f | then ) else | repeat | untll | whlle | wlth | nll

(0.4.6)

Los identificadores también son tokens, como lo son números y cadenas de carac­ teres. ) BNF fue extendido de diversas maneras, generalmente conocidas como EBNF (Forma Extendida Backus-Naur, por sus siglas en inglés). Los símbolos adicionales definidos por la Organización Internacional de Estándares (ISO; International Standards Organization) en su Estándar Revisado de Pascal de 1980 [ISO-DP7185, 1980] son como se ve a continuación: Símbolo [algo] Caigo) (esto | eso)

Significado ocurrencia de 0 o 1 de algo, es decir, opcional 0 o más ocurrencias de algo agrupación; ya sea esto o eso v

(0.4.7)

En EBNF la definición de un identificador puede abreviarse a: <1dentificador>

C|}

Sólo fines educativos - FreeLibros

(0.4.8)

CAPÍTULO 0: Introducción Identificador

Dígito —

19

FIGURA 0.4.1

Gráfica de identificador Letra

Letra

Advierta la economía y claridad del estilo, coryla recursión eliminada de la defini­ ción BNF. Ahora continuaremos la definición de un programa, iniciada anteriormente en el listado (0.4.2). La definición EBNF para

es: <secuencia-declaraciones>

declaracióntdeclaración}

(0.4.9)

Ejemplos de [algo] y de (esto I eso) se presentan en la definición para una instruc­ ción como sigue: <declaración>

(<declaración-simple> | <declaración-estructurada))

(0.4.10)

<dedaración-simple> ::= <declaración-vacía> | <declaración-asignación> ] <decla raci ón-procedi mi ento> <declaración-estructurada>

<declaración-compuesta> | <dedaración-condicional> | <declaración-repeti tiva> | <decl aración-with>

<declaración-condicional>

<declaración-if> | <declaración-case>

1f <expresión-booleana> then declaración [<parte~e1se>] end íf:

<declaración-if>

else declaración

<parte~else>

A manera de ejemplo de cómo todo esto funciona, primero necesitamos las defini­ ciones EBNF aplicables para expresiones simples. Luego demostraremos que la expresión simple A + B * 2 es sintácticamente correcta. 1.

<expres i ón - s i mpl e>

2.

3.

[signo]

«operador-sumaxtérmino»

(0.4.11)

C3 | | | <expresiones> | not

4.

:

5.

| |

6.

<secuencia-dígitos>

7.

8.

<secuenda-dígitos> |

dfgitoCdigito) + | - | o * | / | dlv | aod | and

Un árbol de sintaxis o sintácticof mostrando la derivación de A + B * 2, se ilustra en la figura 0 .4 .2 . Otras definiciones EBNF para construcciones de lenguajes en seudocódigo se presentarán en el capítulo 1. La manera de usarlo deberá resultar clara a medida que vayamos encontrando los diversos ejemplos. Sólo fines educativos - FreeLibros

20

PARTE I:

Conceptos preliminares

Sem ántica /

Un lenguaje también debe estar definido semánticamente al describir de manera precisa lo que significa una construcción particular.) Por ejemplo, la expresión (X < 3) significa en seudocódigo que X debe tener un valor; ese valor es comparable al entero 3, y la expresión es verdadera si el valor < 3, y es falsa en otros casos. El lenguaje natural es notoriamente ambiguo, de manera que se hacen esfuerzos para describir formalmente la semántica del lenguaje así como también la sintaxis. (Dos métodos matemáticos formales están siendo utilizados para describir la semántica de los lenguajes. El primero es axiomático y el segundo denotacional. La semántica axiomática está basada en el cálculo de predicado, ¡el cual examinare­ mos en el capítulo 7 cuando estudiemos el lenguaje PROLOG declarativo basado en la lógica.(La semántica axiomática define declaraciones sobre programas que son o bien verdaderas o bien falsas durante varias etapas en la ejecución de un programa. Estas declaraciones son por lo regular de la forma de condiciones pre­ vias y condiciones posteriores, las cuales son hechas antes y después de una decla­ ración tal como un ciclo iterativo o procedimiento ),Si puede probarse que cada condición es verdadera, sin importar la entrada de datos, el programa tendrá la garantía de estar correcto. La semántica denotacional está basada en la teoría de funciones; Estudiaremos lenguajes de programación basados en funciones en el capítulo 8. (Cada programa y cada procedimiento está asociado con una función (recursiva). Si el programa P está asociado con la función fpy si Xy X2, . . Xn son entradas para P, entonces íp(Xy Xn) debería producir un valor correspondiente a la salida deseada de P, dadas X„V? X^,.... Xn como entradas. 2 ' ' 1 Un tercer método semántico consiste en describir una máquina teórica para un lenguaje y cómo es su funcionamiento. Un trabajo del escritor de compiladores sería entonces implementar esta máquina para una pieza de hardware en particular. El

<expresión-simple>

l

(1)

l

(2) /

(7) /

|

(3)

(2)

\

8)) ((8

( 2)) j (2

| (0.4.3)

(3)

(0.4.4) FIGURA 0.4.2

Un árbol de sintaxis: los números entre paréntesis a la derecha de cada derivación o rama indican la regla utilizada

Sólo fines educativos - FreeLibros

CAPÍTULO 0: Introducción

21

diseñador tendrá ya garantizado que el lenguaje funciona correctamente en la má­ quina teóricalyVeremos un ejemplo de una máquina de este tipo en el capítulo 7. (Los métodos de semántica formal son importantes por varias razones. En pri­ mer lugar, proporcionan una definición de lenguaje no ambigua; segundo, sumi­ nistran estándares de modo que un lenguaje no variará de implementación a otra; y tercero, ofrecen una base para pruebas de corrección tanto de compiladores como de programas. ¡ Comprobabilidad (Probar con certeza matemática que un programa es correcto es un proceso lento. Sin embargo, C. A. R. Hoare cree que "las ventajas prácticas de la comprobación de programas eventualmente se sobrepondrán a las dificultades, en vista de los costos crecientes de los errores de programación"![Hoare, 1969]. La prueba de que un programa es correcto involucra tres pasos: primero, la comprobación de que el pro­ grama cumple con la intención del programador; segundo, probar que el compilador traduce de manera correcta a código de máquina la sintaxis y la semántica del len­ guaje empleado; y tercero, comprobar que la máquina misma funciona correcta­ mente. ( Una meta para cualquier lenguaje de programación es probar que ion compilador para el lenguaje lo interpreta de manera precisa. Esto es a menudo difícil de hacer si la definición del lenguaje incluye descripciones en lenguaje natural de lo que se desea mediante un trozo particular de sintaxis. Si la sintaxis puede describirse en un lenguaje formal, y la semántica puede escribirse axiomáticamente, un compilador puede ser probado formalmente para satisfacer por completo tanto la definición sintáctica como la semántica de un lenguaje, j La sintaxis de Pascal fue definida en BNF, y su semántica definida axiomá­ ticamente por su diseñador, Nicholas Wirth, en colaboración con C. A. R. Hoare. El PL/I fue diseñado usando el lenguaje de definición Viena (VDL; Vienna Definition Language) y ALGOL 68 fue definido en una gramática vW de dos niveles (llamada así por el nombre de su inventor, A. van Wijngaarden) que era demasiado enigmá­ tica para la mayoría de los usuarios. Estos últimos dos metalenguajes forman bases para comprobación de compiladores. Si un lenguaje está definido en VDL, incluye una descripción de lo que pasa cuando cada declaración del lenguaje se ejecuta teóricamente en una computadora teórica. Si un compilador implementa fielmente la computadora teórica, puede probarse que la ejecución del programa es correcta. La gramática vW no describe una computadora teórica, pero permite que parte de la semántica que trata con declaraciones sea definida en la gramática. Por lo tanto, no pueden generarse programas correctos gramaticalmente que vuelvan a declarar variables o que las definan de una manera inconsistente. Confiabilidad El software se considera confiable si se comporta como es anunciado y produce los resultados que el usuario espera. Cuando se presenta un error, debería ser fácil­ mente detectado y corregido. Un lenguaje de programación fomenta la escritura de

Sólo fines educativos - FreeLibros

22

PARTE I: Conceptos preliminares

programas confiables de maneras a menudo sutiles.^La declaración goto es quizá la más notoria característica de lenguaje pensada para dar como resultado programas no confiables [Dijkstra, 1968b]. El problema que subyace aquí es que los progra­ mas con muchos gotos hacia atrás y hacia adelante son difíciles de leer para cual­ quiera que no sea su creador, y por lo tanto, difíciles de modificar o depurar. Las características de sintaxis poco usuales también pueden fomentar errores. El lenguaje C utiliza = como un operador de asignación. X = 5 asigna el valor 5 a la localidad de almacenamiento designada para X. Para hacer comparaciones, se utili­ za = X == 5 compara el valor de X con cinco y si es verdadero o falso, dependien­ do de si X es o no igual a 5. Puesto que C permite asignaciones casi en cualquier sitio de una declaración, la sustitución inadvertida de = por el símbolo poco fami­ liar = puede no producir un error, únicamente resultados ininteligibles. Los identificadores tanto en Modula-2 como en C son sensibles a la caja tipográfica de las letras. Así, Cuenta y cuenta representan distintas variables, que son confundidas fácilmente tanto por un programador como por un revisor subsecuente. ( Un lenguaje confiable debería ser capaz de manejar errores durante el tiempo de ejecución. Una sobrecarga (overflow) aritmética ocurre cuando se calcula un en­ tero que es mayor de lo que puede ser soportado por el hardware particular involucrado. Puede presentarse gran variedad de errores durante la entrada de datos, desde la lectura al pasar el final de un archivo hasta un valor no permitido introducido de manera interactiva. Estas clases de errores son llamadas excepciones, y las provisiones del lenguaje para tratar con ellas son conocidas como manejadoras de excepción. La interrupción de un programa no siempre es aceptable, en particular para aplicaciones en tiempo real.) 1 Para lenguajes de programación, la confiabilidad por lo general se refiere a los mecanismos que promueven la escritura, mantenimiento y depuración de progra­ mas correctos, y el subsecuente manejo de excepciones cuando un programa se ejecuta. Traducción rápida Los lenguajes de programación que consideraremos en este texto son generalmen­ te independientes de la máquina. Es decir,un programa escrito en el lenguaje pue­ de ser traducido y luego ejecutado en una variedad de máquinas diferentes. Un programa que escribimos se encuentra en código fuente. Éste debe ser traducido a un lenguaje que una máquina particular pueda reconocer, y por último en código de máquina que puede ejecutarse en realidad. La máquina en la que un programa se ejecuta se denomina el anfitrión y su(s) lenguaje(s), lenguaje(s) anfitrión(es). Coloca­ mos la (s) opcional después de lenguaje porque una máquina puede tener más de un lenguaje anfitrión. Cualquier máquina debe tener un lenguaje asociado de má­ quina de bajo nivel escrito en código binario. También puede tener un lenguaje ensamblador de nivel superior específico de la máquina. Con frecuencia resulta práctico traducir primero el código fuente a código intermedio, el cual es intermedio entre el código de máquina y el código fuente. El código intermedio puede ser o puede no ser uno de los lenguajes anfitrión. La traducción del código fuente involucra tres pasos: análisis lexicográfico, análisis sintáctico y análisis semántico. El análisis lexicográfico, o rastreo, identifica Sólo fines educativos - FreeLibros

CAPÍTULO 0: Introducción

23

cuáles tokens representan valores, identificadores, operadores, etcétera. El análisis sintáctico, llamado simplemente sintáctico, reconoce las declaraciones válidas mien­ tras que rechaza las declaraciones no válidas del lenguaje fuente. El análisis semántico determina el "significado" de una declaración. Algunos traductores pue­ den realizar dos o más de estos tres procesos en un solo paso sobre el código fuente. ) Los traductores son intérpretes o generativos, los cuales generan un código inter­ medio. Un intérprete es en sí mismo un programa que traduce una expresión o declaración de lenguaje, calcula y luego imprime o utiliza de otro modo su resulta­ do. Los intérpretes son por lo regular más fáciles de escribir que los traductores generativos, pero se ejecutan más lentamente. Una ventaja de un intérprete es que los errores de ejecución así como los de sintaxis son detectados a medida que se encuentra cada declaración, eliminando así cualquier duda acerca de dónde reside el problema. Los lenguajes LISP y PROLOG tienen tanto intérpretes como compiladores, siendo los primeros utilizados para el aprendizaje y la experimenta­ ción, donde los resultados línea por línea son deseables. Un compilador es general­ mente más ventajoso para programas extensos. ; Las partes más comunes de un traductor generativo son el compilador, el ligador y el cargador. El compilador traduce código fuente a código intermedio orientado a la máquina, denominado código objeto. El ligador enlaza de manera conjunta códi­ go intermedio compilado independientemente en un solo módulo de carga, resol­ viendo las diferencias entre tokens. Su salida puede estar en el mismo código intermedio como su entrada pero está libre de referencias de un módulo a otro. El código resultante es así relocalizable, puesto que contiene cualquier información que necesita y es independiente de otros segmentos del programa. El cargador hace la traducción final en código de máquina y carga el programa en diversas localidades de memoria. La salida del cargador es un módulo ejecutable en código de máquina. Durante cada fase, se hacen entradas en varias tablas que mantienen el registro de los tipos de variables, direcciones de memoria, etcétera. Es importante en algunos casos, por ejemplo, una aplicación interactiva, que el código fuente se traduzca rápidamente. Por otro lado, si un programa se va a com­ pilar solamente una vez y va a ejecutarse a menudo, la velocidad de compilación puede no ser una preocupación principal., Se han hecho intentos exitosos para compiladores de un paso, los que rastrean el código fuente sólo una vez, mientras que algunos traductores efectúan muchos pasos (por ejemplo, algunos de los pri­ meros compiladores PL/I de IBM, que ejecutan más de 30 pasos para compilar un programa completo). (Algunos factores que afectan el número de pasos necesarios para un compilador en particular son [Tremblay, 1985]: 1.

2. 3. 4. 5.

¿Cuánta memoria está disponible? ¿Pueden caber simultáneamente en la me­ moria tanto el código fuente como el código objeto que están siendo gene­ rados? ¿Qué tan rápido es el compilador mismo y cuánta memoria requiere? ¿Qué tan grande es el programa objeto y que tan rápido debe ejecutarse? ¿Debe optimizarse el código objeto? ¿Qué clase de características de depuración se requieren para el código fuente? ¿Qué clases de detección y recuperación de errores se requieren para el código ejecutable? Sólo fines educativos - FreeLibros

24

PARTE I: Conceptos preliminares

6.

¿Cuántas personas estarán involucradas en la escritura del compilador? ¿Po­ dría ser ventajoso permitir que cada una escriba un paso independiente reali­ zando una fase simple del proceso de compilación?

Código objeto eficiente Después de que el código fuente se compila en código objeto, no se hace referencia adicional al lenguaje fuente. Así es en tiempo de compilación que los asuntos de la eficiencia en el uso de memoria y tiempo de ejecución deben ser considerados. Existe generalmente un balance comparativo entre el trabajo que el programador debe hacer y el trabajo que el compilador puede hacer. Por ejemplo, un lenguaje que tiene todas las declaraciones de tipo y de variables precediendo a otro código puede asignar todas las localidades de memoria en un momento, acelerando la compilación. Por supuesto, el programador tendrá que hacer estas declaraciones antes de que un programa pueda ser compilado. Algunos compiladores, llamados compiladores de optimización, ejecutan uno o dos pasos más después del análisis semántico para incrementar la eficiencia del código compilado. Las primeras optimizaciones, tales como la eliminación de subexpresiones comunes, son independientes de la máquina, mientras que las mejoras finales dependen de la máquina particular en la que el programa se ejecu­ tará. Los lenguajes de muy alto nivel, donde los programas manipulan estructuras complejas tales como registros, listas, relaciones o conjuntos, dependen de compiladores de optimización por eficiencia. Los lenguajes de programación eje­ cutan la gama de los parecidos a C, donde el programador puede trabajar muy cerca del CPU mismo, hasta lenguajes de manipulación de bases de datos (DML, por sus siglas en inglés), donde las estructuras físicas subyacentes están profunda­ mente ocultas. En los lenguajes de menor nivel, un código objeto eficiente refleja con frecuencia la habilidad del programador, mientras que en los lenguajes de muy alto nivel, un código objeto eficiente depende de la habilidad o capacidad de los escritores de compiladores.; Ortogonalidad La palabra ortogonal viene del griego y se refiere a líneas rectas cruzándose en án­ gulos rectos. Las variables aleatorias se consideran ortogonales si son independientes entre sí. Es en este sentido de independencia que las características del lenguaje pueden considerarse ortogonales. Con esto queremos decir que los componentes son independientes entre sí y que se comportan en la misma manera en cualquier circunstancia. Un ejemplo se encuentra en los conceptos de tipos y funciones. Un tipo descri­ be la estructura de los elementos de datos. Una función es un procedimiento por el que pasa un número finito de valores de parámetro y devuelve un único valor hacia el procedimiento que la invoca. En un lenguaje ortogonal, los tipos son inde­ pendientes de las funciones, y no se aplican restricciones a los tipos de parámetros que pueden ser pasados o al tipo de valor que puede ser devuelto. Así, podríamos ser capaces de pasar una función a una función, y recibir una función de regreso. Sólo fines educativos - FreeLibros

CAPÍTULO 0: Introducción

25

LISP incorpora esta característica particular, pero deben comprenderse ciertas difi­ cultades inherentes y tratar con ellas. ALGOL 68 fue pensado y diseñado como un lenguaje completamente ortogonal. Tiene muy pocas construcciones integradas, y el programador es capaz de cons­ truir lo que quiera mediante la combinación de las diversas características. Nunca llegó a ser popular en los Estados Unidos, en parte debido a que era demasiado ortogonal. Los programadores querían estructuras especiales que se comportaran de maneras predecibles. í La no ortogonalidad puede ser molesta y conducir a errores. Para el programa­ dor novato en Pascal, parece no haber una buena razón por la que una función no pueda devolver un registro o por la que un archivo deba ser pasado como un pará­ metro var. Generalidad La generalidad está relacionada con la ortogonalidad. Se refiere a la existencia de sólo las características necesarias del lenguaje, con las otras compuestas en una manera libre y uniforme sin limitación y con efectos previsibles) Como ejemplo de una carencia de generalidad, considere la del tipo de unión libre en Pascal. Una unión libre es un registro que puede tener un campo que varía en el tipo dependiendo de su uso. Consideraremos las uniones libres en el capítulo 1. En un registro de esta clase, la variable de campo variante puede funcionar como un apuntador y no ser directamente accesible para impresión u otros usos. En otro momento durante la misma ejecución, puede ser tipificado (declaración de tipo) como un entero, con su valor disponible para impresión, operaciones aritméticas, etcéteraJEsta característi­ ca no es general, porque la localidad de memoria relacionada con las variables de campo variante no se trata de manera uniforme y los efectos no son previsibles. Consistencia y notaciones comunes Como hemos mencionado antes, los problemas para solución por computadora con frecuencia son concebidos en el lenguaje de las matemáticas. De este modo, la notación de los lenguajes de programación debería ser consistente con las notacio­ nes comúnmente usadas en este campo. Usamos para indicar resta y números negativos. Así, 5 - 3 y -5 deberían permitirse en lenguajes que soporten aritmética de enteros. 1 e {1,2,3} es la notación común para la pertenencia a un conjunto, y por ello es preferible a la versión en Pascal 1 1n Cl, 2, 3]. Sin embargo, no todos los conjuntos de caracteres soportan g , {, y }, de modo que en ocasiones se hacen sustituciones. Uniformidad La consistencia está relacionada con la uniformidad.6 Con esto queremos decir que nociones similares deberían verse y comportarse de la misma manera. Una 6 Las mismas nociones que aquí denominamos uniformidad, siguiendo la definición de Liskov, se conocen como regularidad en la versión de otros autores.

Sólo fines educativos - FreeLibros

26

PARTE I: Conceptos preliminares

cuestión de uniformidad tiene que ver con la necesidad de tener inicios y fi­ nales. ¿Debería todo "fin" estar precedido por un "inicio" correspondiente? De manera similar, ¿debería toda declaración finalizar con un signo de punto y coma(;)? En un lenguaje completamente uniforme, la respuesta debería ser sí a ambos asuntos.

Subconjuntos Un subconjunto de un lenguaje es una implementación de sólo una parte del mis­ mo, sin características especiales. Las especificaciones originales para el lenguaje Ada del DOD no permiten subconjuntos. La motivación para esto fue el deseo del DOD para hacer que sus contratistas produjeran software que explotara un Ada con todas sus características. Después de todo, las características innecesarias no fueron in­ cluidas. Una de las desventajas de este enfoque era que los estudiantes no podían empezar a aprender el lenguaje hasta que tuvieran disponibles compiladores com­ pletamente validados; por esta razón no existió un cuerpo de programadores hasta varios años después de que el lenguaje había sido completado. Algunos lenguajes son extensos, con muchos componentes especiales. Estos pueden ejecutarse solamente en máquinas grandes y no están disponibles para compañías y escuelas más pequeñas a menos que se trate de subconjuntos de los mismos. Otra ventaja de los subconjuntos es el desarrollo incremental de un len­ guaje. Con esto nos referimos a la versión inicial de un lenguaje de núcleo peque­ ño, con otras características que van siendo liberadas a medida que se van desarrollando.

Extensibilidad í El inverso de los subconjuntos es la extensibilidad. Un lenguaje puede tener un

núcleo estándar, el cual es invariable en cada implementación, pero con varias ex­ tensiones. Las ventajas de los subconjuntos son mejoradas cuando un lenguaje puede ser extendido en formas útiles. A principios de 1968, los desarrolladores de COBOL (COmmon Business Oriented Language; lenguaje común orientado a los negocios) adoptaron este enfoque mediante la definición de un "núcleo" que todos los compiladores debían satisfacer. Once módulos estandarizados fueron agregados, los cuales pueden o pueden no ser incluidos en cualquier compilador de COBOL dado. Ada 95 ha adoptado un enfoque modular semejante. Los diseñadores de Pascal incluso usaron otro enfoque, definiendo un peque­ ño lenguaje estándar portátil, que carecía de algunas características deseables, tales como capacidades de gráficos y manejo de cadenas de caracteres. Los implem entadores de Pascal agregaron varias m ejoras, las cuales hicieron a sus compiladores atractivos para los programadores, pero los programas resultantes eran menos portátiles. Por ejemplo, el Pascal Estándar no tiene tipo de cadena (string), pero casi todos los compiladores de Pascal proporcionan uno integrado en el lenguaje mismo o en un módulo especial para ser incluido con la mayoría de los archivos fuente. Sólo fines educativos - FreeLibros

CAPÍTULO 0: Introducción

27

Transportabilidad Un lenguaje es transportable si sus programas pueden compilarse y ejecutarse en diferentes máquinas sin tener que rescribir el código fuente. Para conseguir la transportabilidad se han establecido las organizaciones de estándares nacionales e internacionales para producir descripciones de lenguaje a las cuales deben adherir­ se las implementaciones. Las más activas de éstas son el Instituto Nacional Ameri­ cano de Estándares (ANSI; American National Standards Institute), la Institución Británica de Estándares (BSI; British Standards Institution), la Organización Inter­ nacional de Estándares (ISO; International Standards Organization) y el Instituto de Ingenieros Eléctricos y Electrónicos (IEEE; Institute of Electrical and Electronics Engineers). Éstos grupos tienen varios comités oficiales, que preparan y revisan estándares para diferentes lenguajes. ‘ Los estándares pueden desarrollarse después de ganar alguna experiencia con un lenguaje en particular, como es el caso de Pascal, o antes de que un lenguaje sea diseñado, como ocurre con Ada. i La estandarización temprana puede per­ petuar características de diseño deficientes no reconocidas, al tiempo que demora el fomento de dialectos incompatibles. LISP es quizás el lenguaje con la mayor longevidad no estandarizada. LISP fue diseñado e implementado a principios de los años sesenta, pero es solamente hasta ahora que se está estandarizando a Common LISP. Sin embargo, la parte estandarizada será solamente un pequeño núcleo, con diferentes implementadores libres de hacer cualquier extensión que ellos deseen. E J E R C I C I O S 0. 4 1. Complete el lado derecho del árbol de sintaxis de la figura 0.4.2. 2. Dibuje un árbol de sintaxis para demostrar que lo que sigue son expresiones de seudocódigo sintácticamente correctas. Escriba el número de la regla utilizada a la derecha de cada sustitución como se hizo en la figura 0.4.2. a.

(3 + X) * Y

b. not (A or B)

c.

2 or A

3. El ejercicio 2c representa una expresión sintácticamente correcta que es semánti­ camente incorrecta. Si un compilador fuera escrito para implementar nuestro seudo­ código, ¿cuándo podría detectarse este error: durante el análisis lexicográfico, sintáctico o semántico, o bien en tiempo de ejecución? 4. Debe escribirse descripciones bien definidas tanto para la sintaxis como para la se­ mántica de un lenguaje. Encuentre la definición de una declaración "for" en dos diferentes formalismos. Dos posibilidades son diagramas de sintaxis en Pascal y EBNF para ALGOL 60 o Ada. ¿Cuál de ellas encuentra más fácil de leer? 5. Haciendo uso de las descripciones que haya encontrado para el ejercicio 4, examine las definiciones semánticas. ¿Son definiciones de lenguaje natural o formal? Para encontrar estas definiciones semánticas, usted tendrá que localizar el estándar o in­ forme oficial. Los diagramas de sintaxis aparecen con frecuencia en los libros de texto, pero las definiciones semánticas pueden olvidarse, con su significado explica­ do en el cuerpo del texto o mediante ejemplos. 6. Haga uso de declaraciones EBNF en el listado (0.4.10) para mostrar que la declara­ ción que presentamos a continuación es sintácticamente correcta, mientras que la declaración b no lo es. ¿Por qué b es ambigua?

Sólo fines educativos - FreeLibros

28

PARTE I: Conceptos preliminares

a. ! f (N - 1) then prlnt ( ‘N GANA!'): else 1f (N = 2) then prlnt ( ‘N PONE!'): end I f ; end if; b. i f (M < 4) then i f (M < 2) then prlnt C'M GANA!') else print (M MUESTRA 0PONE!): end If;

7. Cuando se produce un código objeto, la optimización involucra el reacomodo y el cambio de las operaciones para hacer que el programa se ejecute más rápido. Una de estas técnicas se denomina plegamiento (folding), el proceso de calcular en tiempo de compilación operaciones aritméticas que son conocidas [Gries, 1971]. Suponga­ mos que nuestro código fuente incluye la siguiente secuencia de declaraciones: H

1 + 1; I ;= 3: B

6.2 + 1

Éstos pueden optimizarse a H := 2; I

3: B := 9.2

Optimice las siguientes secuencias de declaraciones: a. X10: Y X / 2; Z b. X 10: Y :« X + Z; Z c. case I of 1: Prlnt (I * 2):

SQR(X) - (X + Y); SQRÍX) - (X + Y):

2: Prlnt (I * 3): 3: Prlnt (I * 4): else Print (I) end case;

8. Si usted está familiarizado con algún lenguaje ensamblador, convierta las secuencias de código del ejercicio 3 en código ensamblador tanto optimizado como no optimizado. 9. Encuentre tantas características no ortogonales o no generales como pueda de un lenguaje con el que usted esté familiarizado. Para cada uno de ellos, ¿por qué piensa que haya sido hecha esa restricción?

0.5 RESUMEN Primero examinamos los métodos tradicionales para la resolución de problemas, lo que incluía álgebra, lógica y teoría de funciones. Después analizamos la organi­ zación de Peter Wegner de los lenguajes de programación en paradigmas imperati­ vos y declarativos. Los lenguajes imperativos funcionan mediante el cambio de los valores de la memoria de la computadora, llamada almacenamiento, mientras que el estilo declarativo involucra la escritura de comandos para realizar alguna ac­ ción, por ejemplo, clasificar una lista. Mecanismos ocultos dentro del lenguaje mis­ mo conducen entonces las instrucciones. El álgebra es la base para la mayoría de los lenguajes imperativos, mientras que las otras dos herramientas matemáticas forman la base para los lenguajes declarativos. El paradigma imperativo se divide adicionalmente en lenguajes estructurados en bloques, orientados a objetos y distribuidos. Los primeros dos grupos progra­ man ideas en unidades de programa llamadas bloques u objetos. Cada uno puede Sólo fines educativos - FreeLibros

CAPÍTULO 0: Introducción

29

tener datos locales para la unidad. El objeto agrupa operaciones sobre los datos con los datos mismos. El paradigma declarativo incluye, además de lenguajes lógicos y basados en funciones, un paradigma para operaciones de bases de datos. Éstos se basan con frecuencia en la teoría de las relaciones. No todos los lenguajes se clasifican dentro de un paradigma u otro, ya que muchos tienen características de más de uno de ellos. Existen también lenguajes diseñados para abordar problemas de cómputo especiales, tales como visualización de gráficos y aquellos que se ejecutan en tiempo real y controlan otras clases de máquinas. Los lenguajes deben ser confiables, comprensibles, eficientes en términos de tiempo de ejecución y consumo de espacio, y deben satisfacer las necesidades de una comunidad, ya sean científicos, hombres de negocios o usuarios no técni­ cos. Cada uno de estos grupos está acostumbrado a un vocabulario particular y una manera de ver las cosas; de este modo, existe una gran variedad de lenguajes y muy probablemente esto continuará siendo así.

0.6

NOTAS SOBRE LAS REFERENCIAS Un texto bien escrito y bastante fácil de leer acerca de la semántica axiomática es [Gries, 1981]. El libro tiene muchos ejemplos fáciles, lo que permite la compren­ sión, pero esto constituye también su desventaja. En ningún sitio se encuentra un programa de por lo menos longitud o complejidad promedio analizado usando la metodología de condición previa y condición posterior. [Tennent, 1976] y [Gordon, 1979] proporcionan buenas instrucciones a la semántica denotacional. Tanto la se­ mántica axiomática como la denotacional son consideradas en [Mandrioli, 1986]. Al estudiante interesado en traductores se le recomienda acudir a [Calingaert, 1988]. La cobertura es la de un nivel de "primer libro", con material restringido a la traducción de lenguajes de procedimientos. Otro texto interesante es [Kamin, 1990], el cual considera a LISP, APL, SCHEME, SASL, CLU, Smalltalk y PROLOG a través de intérpretes escritos en Pascal. Un volumen del IEEE Tutorial [Wasserman, 1980] contiene resúmenes breves acerca de lenguajes de programación, diseño de lenguajes, estructuras de control, tipos de datos, Pascal y Ada, administración de bases de datos y manejo de excep­ ciones, experiencias en el diseño de nuevos lenguajes y definiciones de lenguaje axiomático. La colección también incluye artículos originales escritos por implementadores de lenguajes líderes.

Sólo fines educativos - FreeLibros

CAPÍTULO 1 VARIABLES Y TIPOS DE DATOS 1.0 En este capítulo 1.1 Tipos de datos primitivos Entero (integer) Real Carácter Booleano Apuntador Ejercicios 1.1 1.2 Variables Identifícadores Palabras reservadas y palabras clave Ligadura Ligadura de nombre Ligadura de dirección y tiempo de vida Ligadura de valor Ligadura del tipo Bloques y alcance Alcance estático Bloques

31 32 32 33 34 35 36 39 39 39 40 41 41 41 42 43 43 44 45

Alcance dinámico Registros de activación Ejercicios 1.2 1.3 Tipos de datos estructurados Tipos definidos por el usuario Tipos subrango Tipos enumerados Tipos agregados Arreglos Cadenas Registros Tipos unión Conjuntos Listas Cuestiones de tipo Verificación de tipos Tipificación fuerte y débil Ejercicios 1.3 1.4 Resumen 1.5 Notas sobre las referencias

Sólo fines educativos - FreeLibros

46 47 50 51 51 51 52 53 53 56 57 59 61 62 63 63 65 66 67 68

CAPÍTULO

1

Variables y tipos de datos

Los lenguajes imperativos proporcionan una abstracción para el código máquina. Las variables actúan como abstracciones para las celdas de memoria, con nombres que remplazan las referencias a las direcciones de la máquina. La entrada en una celda está asociada con algún tipo. Los lenguajes de computadora generalmente suministran algunos tipos de datos primitivos, tales como de carácter y entero. En muchos casos los datos pueden tener alguna estructura, tal como un arreglo o re­ gistro, de modo que las capacidades de esta clase por lo general también son sopor­ tadas. Una variable debe estar ligada a las propiedades asociadas con ella. Aparte de su nombre y dirección asociada, debería estar ligada a algún tipo y a un valor. El momento de esta ligadura, ya sea durante la compilación o la ejecución, llega a ser importante en la comprensión de un lenguaje. Cuando agregamos funciones y pro­ cedimientos, debemos considerar también el alcance y el tiempo de vida de estas variables.

1.0

EN ESTE CAPÍTULO Cuando se consideran cuestiones de variables y de tipos, es útil examinar tanto los conceptos básicos como algunos principios para su implementación. En este capí­ tulo consideraremos: • • • • •

Tipos de datos primitivos y sus representaciones. Ligadura de atributos a las variables. Bloques, alcance e implementación mediante registros de activación. Tipos estructurados y su distribución. Verificación de tipos y cuestiones de compatibilidad de tipos. Sólo fines educativos - FreeLibros

32

PARTE I:

Conceptos preliminares

1.1

TIPOS DE DATOS PRIMITIVOS Los lenguajes suministran al programador ciertos tipos de datos básicos, especifi­ cando tanto el conjunto de elementos de datos como un conjunto de operaciones sobre los mismos. El número de tipos varía, desde LISP puro con un tipo esencial, la expresión simbólica o S-expresión, hasta un lenguaje rico como Ada, con seis tipos básicos: enumerado (enumeration), entero (integer), real, arreglo (array), re­ gistro (record) y acceso (access), así como tipos derivados de éstos. Los tipos enu­ merados que presentaremos incluyen tipos carácter y booleano. Muchos lenguajes incluyen tipos primitivos tales como entero, real, carácter, booleano y apuntador. Mientras que las especificaciones de estos tipos pueden va­ riar entre lenguajes y máquinas, existe un número de aspectos en común. Sin em­ bargo, advierta que todos éstos difieren de los tipos agregados, tales como arreglos y registros, los cuales se componen de otros tipos y se analizarán en la sección 1.3. Entero (integer) Uno de los tipos de datos primitivos más comunes es el entero (integer). Para mu­ chos lenguajes, el tamaño del entero puede determinarse mediante el tamaño de palabra de la máquina objeto.1 Si bien son posibles varias representaciones, si una máquina soporta aritmética de complemento a 2 con una palabra de 16 bits, y utili­ za un bit para el signo, el valor más grande de 15 bits sería +32,767. Por lo tanto, esto podría probablemente llegar a ser el valor de maxlnt en esta máquina para un lenguaje como Pascal. Claramente esto puede ser un problema si deseamos que los programas sean portátiles entre máquinas con diferentes tamaños de palabra que soportan un lenguaje común. Algunos lenguajes, como C y Ada, también proporcionan tipos de enteros cor­ tos y enteros largos. Estos generalmente dependen de la implementación acerca de qué soporte de hardware se encuentra disponible y podría usar un byte o palabra para enteros cortos, mientras que los enteros largos pueden estar compuestos de palabras dobles o cuatro palabras. De nueva cuenta, si la transportabilidad es im­ portante, se debe estar consciente de las diferencias entre máquinas objeto. También ha llegado a ser común para un lenguaje el soporte de enteros sin signo, en los cuales sólo se utilizan valores positivos. En este caso, no es necesario hacer espacio para un bit de signo de manera que se puede alcanzar un valor máxi­ mo de 65,535 en una máquina de 16 bits. El lenguaje C incluye aun enteros cortos y largos sin signo. Algunas máquinas (como la IBM 370) son capaces de almacenar enteros en formato decimal en lugar de binario. En esta representación decimal codificada en binario (BCD, Binary Coded Decimal), los dígitos del 0 al 9 son almacenados en cuatro bits cada uno, de modo que 0011 0101 representarían 35. Las operaciones aritméticas necesitan estar soportadas, y puede haber un límite sobre el número de dígitos permitidos. Si bien un lenguaje puede soportar un tipo como el de los ente­ 1 Esto hace referencia a la máquina en la que el código objeto resultante se ejecutará.

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

33

ros BCD, puede no estar soportado por el hardware de la máquina objeto. En este caso, un compilador podría proporcionar el soporte en software o no soportar el tipo. Por supuesto, éstos no son los únicos modelos. El lenguaje de conjuntos SETL22 permite que un entero sea prácticamente "infinito" en tamaño, limitado sólo por la memoria disponible. Un lenguaje de esta clase puede ser particularmente útil para problemas matemáticos que involucren grandes números. Real Es importante advertir que la representación en computadora de los números rea­ les difiere en forma significativa del concepto en un curso de matemáticas, en el cual la mayor parte de los números reales no tiene una representación decimal exacta. En los lenguajes de computadora, debemos recordar que el valor real puede representarse solamente mediante una aproximación. Por ejemplo, pi y sqrt(2) tie­ nen representaciones decimales infinitas y no repetitivas en matemáticas, pero de­ ben ser aproximadas mediante algún valor digital para uso de la computadora. La representación numérica de punto fijo especifica tanto un número fijo de dígitos como la posición del punto decimal (o binario). Son entonces como muchos enteros, excepto por el punto (decimal o binario) de base. Están disponibles en lenguajes como COBOL y PL/I. Una declaración muestra en PL/I es: DECLARE TAX FIXED DECIMAL (8,2):

Aquí la variable TAX puede representar un número decimal en el intervalo des­ de -999999.99 hasta 999999.99. Puesto que son útiles en el trabajo con valores mo­ netarios, una máquina puede realmente soportar tales tipos de punto fijo como decimales codificados en binario (BCD), o pueden ser simulados por números de punto flotante. Mientras que BCD no es soportado en muchas máquinas, un tipo binario fijo sí lo sería. Observe, sin embargo, que la especificación para el número exacto de dígitos binarios a utilizar puede no coincidir con la estructura de byte o palabra de la máquina. Un número de punto flotante está basado en la idea de la notación científica, en la cual representamos tanto la mantisa (parte fraccionaria) como el exponente de un número. La notación 3.2843E-4 se emplea comúnmente en salidas impresas para representar 3.2843*1(K Sin embargo, a fin de utilizar los comandos integrados de punto flotante y el hardware, aquéllos se almacenan generalmente en binario, con algunos bits para el exponente y algunos otros para la fracción, como se ilustra en la figura 1.1.1. Es interesante observar que los números sucesivos no están igualmente espa­ ciados como lo están en la notación de punto fijo. Por ejemplo, considere la siguien­ te secuencia decreciente de números con partes fraccionarias de 2 dígitos: 1.2E-3, 2 SETL (SET Language; lenguaje de conjuntos) y su sucesor SETL2 son lenguajes de programación de muy alto nivel, desarrollados en la Universidad de Nueva York, los cuales están basados en la noción matemática de la teoría de conjuntos. Sus características de diseño los han hecho útiles para los prototi­ pos de software.

Sólo fines educativos - FreeLibros

34

parte

I: Conceptos preliminares

Exponente

Fracción

Bit de signo para fracción F I G U R A 1.1.1

Representación de punto flotante

1.1E-3,1.0E-3, 9.9E-4, 9.8E-4, etc. El tamaño de paso entre los primeros tres es de .0001, pero es de .00001 entre los últimos tres. Los reales de precisión doble proporcionan más bits tanto para el exponente como para la mantisa. Los estándares para la aritmética binaria de punto flotante han sido establecidos por la IEEE [IEEE-754, 1985]. Si bien la mayoría de los len­ guajes no suministran control sobre la precisión de estos reales (más que los de precisión simple o doble), algunos lenguajes como PL/I y Ada prevén los elemen­ tos para hacerlo así.

Carácter Los caracteres se representan en la computadora mediante códigos numéricos. El ASCII (American Standard Code for Information Interchange) es el más común y con frecuencia es soportado por el hardware.3 Para el ASCII de 7 bits, los có­ digos de 0 a 127 representan tanto caracteres imprimibles (caracteres alfanuméricos) como también cierto número de caracteres de control, útiles para el control de la impresora y de la pantalla. Los códigos de 8 bits proporcionan conjuntos de carac­ teres extendidos en el intervalo de 128 a 255. El lenguaje Java soporta un código de 16 bits conocido como Unicode4 a fin de soportar más caracteres que no se encuen­ tran en la lengua inglesa. La ordenación numérica de los códigos proporciona un ordenamiento natural de los caracteres mismos, por lo tanto pueden utilizarse ope­ radores relaciónales para compararlos. Mientras que el programa fuente y los da­ tos de entrada son generalmente caracteres, las cadenas representan datos numéricos que pueden ser convertidos a una representación entera o real a medida que son leídas. En algunos lenguajes, el tipo carácter (char) puede emplearse para representar objetos diferentes a caracteres simples. En C, cha r puede ahorrar espacio en lugar de los enteros cortos. Las cadenas de caracteres generalmente son un tipo de datos más útil y se discuten posteriormente en este capítulo.

3 El EBCDIC (Extended Binary Coded Decimal Interchange Code; código de intercambio decimal codificado en binario extendido) se emplea en las macrocomputadoras (mainframes) de IBM. 4 En Unicode, la palabra niño por ejemplo, puede ser un identificador Java válido.

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

35

Booleano El tipo booleano es quizá el tipo más simple y es común en los lenguajes de propósi­ to más general. Los dos valores, verdadero (true) y falso (false), pueden estar orde­ nados, de modo que false < true (aunque no necesariamente para todos los lenguajes), pero tales comparaciones (si están definidas para el tipo) serían poco usuales. Los conectores lógicos and, or y not pueden ser empleados para formar expresiones, aunque xor5 y otros también podrían proporcionarse. Las variables booleanas se emplean más comúnmente como marcadores o "banderas" (flags) tales como endOfData o notFound. Parecería algo natural implementar valores booleanos como bits simples, ha­ ciendo uso del 0 para falso y 1 para verdadero. Puesto que muchas máquinas no pueden direccionar bits simples, un byte o palabra pueden ser asignadas. En C se utilizan valores enteros, con falso como el valor 0 y verdadero como cualquier va­ lor distinto de cero. Una causa común de errores en programación es la suposición de que un len­ guaje hará cortocircuito en algunas expresiones booleanas. Por ejemplo, considere las expresiones: ( i - 0) or ( a/ i > 0) then . . .

1)

if

2)

whlle ( i <« 100) and (aC13 > 0) do ...

Una vez que se evalúa el operando izquierdo, puede parecer que el operando dere­ cho no tiene necesidad de ser. En el primer ejemplo, or es verdadero si cualquier operando es verdadero. Suponiendo que i es 0, puesto que el operando izquierdo es verdadero, pareciera que no es necesario evaluar la expresión de la derecha. De cualquier modo, un compilador evaluaría la expresión de la derecha, lo cual daría como resultado un error de división entre cero. En el segundo ejemplo, and es falso si cualquier operando es falso. Si i alcanza el valor 101, el operando izquierdo es falso, lo que conduciría a que el operando derecho no pudiera ser evaluado. Sin embargo, el arreglo a no puede ser definido si el índice es mayor que 100, de modo que la evaluación del operando derecho produce un error. Para evitar este problema, Ada proporciona los operadores booleanos especiales and then y o r el se, los cuales dan el resultado de cortocircui­ to deseado. El código puede cambiarse a: a h íle ( i <- 100) end then ( a CU > 0) do ...

Si i tiene el valor 101, la evaluación fallida del lado izquierdo evita la evaluación del derecho. Java usa los operadores I (or) y & (and) como los operadores lógicos que evalúan ambos operandos, mientras que I I y && realizan la evaluación abre­ viada de los operandos.

5 x or (exclusive or; or exclusivo) es verdadero si cualquiera de los operandos es verdadero, pero no ambos.

Sólo fines educativos - FreeLibros

36

PARTE I: Conceptos preliminares

Apuntador El tipo apuntador ( pointer) es diferente de los tipos primitivos precedentes. En lugar de contener directamente un objeto de datos, contiene la ubicación de un objeto. De aquí que los valores del apuntador sean las direcciones de memoria de otros obje­ tos, de manera similar a la idea del direccionamiento indirecto utilizado en lengua­ je ensamblador. Aquéllos pueden llamarse tipos de referencia o acceso en algunos lenguajes. Por ejemplo, la ubicación de memoria asociada con una variable entera i pue­ de contener el valor 12. Si p es un apuntador a un entero en la dirección 3080, en­ tonces p contiene la dirección 3080, mientras que la ubicación 3080 puede contener un valor entero de 15, como se ilustra en la figura 1.1.2. Con el fin de probar si una variable apuntador p contiene una dirección o no, su contenido puede compararse con un valor de apuntador especial n1l o nuil, el cual no puede representar una dirección válida. Las variables apuntador están asociadas normalmente con un tipo simple.6 En Pascal, por ejemplo, considere el listado (1.1.1). type gradeRec - record letter: c h a r ; number: integer; end; var

(1-1*1)

p, q: Ai n t e g e r ; r: AgradeRec;

Esto asigna suficiente almacenamiento para que cada variable p, q y r contenga una dirección, como se muestra en la figura 1.1.3. Una dirección (o ni 1) puede almacenarse en cada una durante la ejecución. La dirección real contenida en una variable apuntador normalmente no es conocida por el usuario, pero uno puede emplearla en asignaciones tales como q: = p, la cual copia la dirección que se encuentra en p hacia q. Observe que r también contiene una dirección. Esta puede apuntar hacia un registro, como se muestra en la figura 1.1.4. A fin de manipular el contenido de una celda para una dirección, debemos desreferenciar el apuntador. Haciendo uso de notación Pascal para el ejemplo de la i

p

3080

FIGURA 1.1.2 Una variable entera contra un apuntador a un entero

6 Éste no es el caso para PL/I, el cual simplemente permite la declaración de una variable de tipo POINTER (APUNTADOR).

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

37

‘B’ FIGURA 1.1.3 Asignación inicial de variables apuntador

86 FIGURA 1.1.4 r contiene la dirección de un registro

figura 1.1.2, p hace referencia a la dirección 3080. D espués de hacer la desreferenciación, pAhace referencia al valor 15, el contenido de la dirección 3080. Puesto que r Aes del tipo gradeRec, r A . 1e t t e r y r A. number podrían emplearse para especificar las entradas de campo. Los valores en la figura 1.1.4 podrían ser asigna­ dos mediante los enunciados r A , letra := 'B ' ; r A , número :•* 86;

Los apuntadores son particularmente interesantes en el sentido de que propor­ cionan un medio para administrar la memoria dinámica en un área especial de almacenamiento llamada la pila. El término pila indica que tenemos un depósito de memoria en cuyo espacio puede ser asignado y desasignado de manera dinámi­ ca durante la ejecución. El espacio puede ser creado en el momento en que sea necesario. Cuando ya no es necesario, puede ser devuelto a la pila para su uso posterior. Es importante hacer notar que los objetos asignados aquí comúnmente no están asociados con variables en forma directa sino que se tiene acceso a ellos sólo mediante apuntadores. Si trabajamos con objetos (tales como una pila o cola) en un arreglo de tamaño fijo o estático, gran parte del arreglo puede estar vacío o, aún peor, el tamaño asignado puede ser demasiado pequeño. Con el almacena­ miento dinámico, el uso del almacenamiento de la pila puede incrementarse (y disminuirse) como sea necesario. Sin embargo, puede ser posible para un progra­ ma ejecutarse fuera del almacenamiento de pila, si hace un uso excesivo del alma­ cenamiento dinámico. En este caso, puede ser necesario ejecutar de nuevo el programa después de asegurarse de que se tiene disponible una pila más grande. Cuando un nuevo objeto es creado en la pila, se asigna almacenamiento para un objeto del tipo apropiado, y el apuntador a (la dirección de) ese objeto se de­ vuelve. En Pascal esto se realiza mediante el procedimiento llamado new( p);. Des­ pués de la llamada, p contiene la dirección de un objeto del tipo apropiado, como se ilustra en la figura 1.1.5. Asumiendo la declaración en el listado (1.1.1), el objeto a la derecha en la figura 1.1.5 es de tipo entero.

FIGURA 1.1.5 n ew ( p) asigna memoria de almacenamiento en la pila

Sólo fines educativos - FreeLibros

38

PARTE I: Conceptos preliminares

P

FIGURA 1.1.6

di s p0 s e ( p) crea una referencia colgante

Pascal proporciona el procedimiento di spose (p) para desasignar el almacena­ miento en la dirección p. Puesto que diversos apuntadores pueden contener la mis­ ma dirección, se debe tener cuidado de no desasignar uno de ellos, de otro modo se crearán referencias colgantes. Por ejemplo, suponga que comenzamos con la configu­ ración de la figura 1.1.6. Si ahora utilizamos di spose (p), la ubicación donde 7 ha sido almacenado pue­ de volver a utilizarse para algún otro propósito. Puesto que q todavía contiene esta dirección, es ahora una referencia colgante dentro de la pila. El programador debe asegurarse de que no hay otras referencias a una dirección antes de desasignarla. Cuando se cambia el contenido de un apuntador mediante una asignación, es posible perder el acceso a la dirección anterior almacenada allí, sin importar el hecho de que pueda contener datos útiles. Este almacenamiento perdido se deno­ mina basura porque ya no se tiene acceso al mismo y no ha sido desasignado. Por ejemplo, considere la configuración inicial mostrada en la figura 1.1.7. Si aplicamos la asignación p : * q;, entonces la dirección donde 4 fue almacenado ya no será accesible. Como otro enfoque sobre la administración de la pila, algunos lenguajes (como LISP) proporcionan un recolector de basura, el cual sigue la pista al almacenamiento inaccesible y permite que sea reasignado. Si bien una implementación del compilador de Ada puede proporcionar recolección de basura, esto no es común. De aquí que el lenguaje Ada incluya un procedim iento genérico llam ado unchecked_deal 1 oca t i on para permitir la eliminación de basura.

P

P

FIGURA 1.1.7

p : “ q; crea basura inaccesible Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

39

E J E R C I C I O S 1. 1 1. Los dígitos decimales pueden almacenarse en cuatro bits en una máquina binaria. Puesto que los patrones de bits 0000 a 1111 representan 0 a 15, nosotros solamente utilizamos de 0 a 9 para un dígito decimal. a. ¿Cuáles son las ventajas y desventajas de emplear esta notación BCD para repre­ sentar enteros? b. ¿Existe algún problema adicional si utilizamos una representación de este tipo para números decimales de punto fijo? 2. Es posible para un lenguaje soportar enteros de precisión "infinita". a. ¿Cómo puede un tipo de esta clase ser almacenado? b. ¿Qué problemas y dificultades presenta esto? 3. Los códigos de caracteres ASCII y EBCDIC tienen diferente ordenación de los carac­ teres. ¿Qué problemas crea esto para un lenguaje? 4. ¿Cuáles son las ventajas y desventajas de almacenar booleanos en bits en lugar de palabras? 5. Si un lenguaje soporta tanto and como el and then para cortocircuito, ¿bajo qué cir­ cunstancias podrían producir resultados diferentes? 6. En C, una variable booleana b se considera falsa si b = 0, y verdadera en cualquier otro caso. Analice los méritos de esto contra true - l y f a l s e - 0, o true - algún valor especial reservado y fal se - algún otro valor especial reservado. 7. Los apuntadores apuntan al almacenamiento dinámico asignado en la pila. a. ¿Cuáles son las ventajas y desventajas de que un lenguaje no soporte la desasignación del almacenamiento de pila? b. ¿Cuáles son las ventajas y desventajas del soporte de recolección de basura?

1.2

VARIABLES Cuando se escribe en código de máquina, se utilizan las direcciones de la máquina para especificar dónde serán almacenados los elementos. El programador tiene que seguir la pista de qué tipo de objeto contiene una celda de almacenamiento. Exten­ diendo esto de alguna manera, una variable proporciona una abstracción para esta noción. Como describiremos en breve, una variable está vinculada a una tupia7 de atributos: (nombre, dirección, tipo, valor). Otros conceptos importantes incluyen el alcance y el tiempo de vida de la variable, así como también cuestiones acerca del tiempo de ligadura o fijación, reglas de alcance y verificación de tipo.

Identificadores

Los identificadores o nombres no sólo se utilizan para variables. En un programa, los nombres pueden asignarse a cosas como procedimientos, etiquetas, tipos y más.

7 Una n-tupla es un conjunto ordenado de n entradas. Aquí, los atributos conforman una 4-tupla.

Sólo fines educativos - FreeLibros

40

PARTE I: Conceptos preliminares

Mientras que los primeros lenguajes permitían solamente caracteres simples como nombres, la mayoría de los lenguajes tipo ALGOL permiten algunas cadenas de letras y dígitos. La cadena comienza con una letra para evitar la confusión sintáctica, como, por ejemplo, entre un nombre como lOx y el entero 10. Los nom­ bres en COBOL, versiones iniciales de FORTRAN (hasta FORTRAN-77) y PL/I estaban restringidos a letras mayúsculas, pero la mezcla de mayúsculas y mi­ núsculas es normal para muchos lenguajes. Sin embargo, se debe tener cuidado de verificar las reglas del lenguaje. Por ejemplo, un compilador FORTRAN-90 puede reconocer letras minúsculas, pero no se requiere hacerlo, de manera que el uso continuo de mayúsculas es común. Los lenguajes pueden poner límites sobre la longitud de los nombres o sobre el número de caracteres significativos. En los primeros compiladores de C, solamente los primeros ocho eran significativos, de modo que Col aDatos y Col aDatos2 no se podían distinguir. El C ANSI ahora especifica que los primeros 31 sean significati­ vos. Aunque algunas especificaciones del lenguaje permiten cualquier longitud de nombre, una implementación puede forzar limitaciones. Los lenguajes como C y Ada también permiten el uso del carácter subrayar, y LISP permite el guión. Puesto que un programa puede ser más legible con nombres significativos, se fomenta el uso de identificadores con múltiples palabras. En Pascal, se pueden mezclar letras mayúsculas y minúsculas para usar nombres como col aDatos, mientras quelos programadores de Ada pueden utilizar col a_datos. Cuan­ do los nombres no son sensibles a la caja tipográfica de las letras, entonces Col aDatos, col adatos, col aDatos yCOLADATOS se referirán todos a la misma variable. Si bien las convenciones de estilo para un lenguaje de programación pueden ser establecidas por el uso común, los programadores son guiados con frecuencia por los manuales de referencia estándar. En el estándar Ada 83, por ejemplo, los identificadores estaban enumerados en letras mayúsculas (como C0LA_DAT0S), mien­ tras que el estándar Ada 95 utilizaría Col a_Datos. Como resultado de ello, los libros están comenzando a cambiar a este nuevo estilo. Sin embargo, los nombres en C son sensibles a la caja tipográfica de las letras, de modo que debe tenerse cuidado al nombrar y quizá adoptar una convención para emplear identificadores en minúsculas para variables y nombres, comenzan­ do con una letra mayúscula para procedimientos y funciones. Cualquier variación de la convención puede ocasionar errores en los programas. En Java, la convención es iniciar los nombres de clases (que se presentan en el capítulo 2) con letras ma­ yúsculas, mientras que otros identificadores comienzan con minúscula, por ejem­ plo, colaDatos. La práctica normal para otros lenguajes varía, de manera que es importante verificar las convenciones para nombres cuando se aprende un nuevo lenguaje.

Palabras reservadas y palabras clave Muchos lenguajes hacen uso de ciertos nombres como parte de sus sintaxis (tales como for, whlle, of, else, end, etc.) o como operadores o funciones especiales (mod, nll, not, sin, y rutinas de entrada/salida como read o prlnt). Cualquier palabra cuyo significado esté predefinido y no pueda ser vuelto a definir por el Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

41

programador se conoce como una palabra reservada. Cuando se comienza con un nuevo lenguaje, no es raro que un programador novato utilice sin quererlo una de las palabras reservadas como nombre de variable. Afortunadamente el compilador reconocerá este error como un error simple y producirá un mensaje claro de error. Si el mensaje de error es confuso, podría resultar bastante complicado para un no­ vato diagnosticar el problema. Existe con frecuencia un número de palabras que no están reservadas pero tienen un significado predefinido. Estas palabras clave pueden, de hecho, ser defini­ das por el usuario para otro propósito. Por ejemplo, en Pascal, la mayoría de los tipos predefinidos ( i n t e g e r , r e a l , boolean, etc.) y funciones predefinidas (trunc, s q r t , s i n , ln, etc.) no son reservados. En Ada, se proporcionan varios de dichos elementos en el paquete Standard. Sin embargo, si se utiliza un nombre como integer para una variable, entonces el significado predefinido puede quedar no disponi­ ble, y el programa puede ser más difícil de leer. El mismo problema puede surgir en FORTRAN, en el cual no hay palabras reservadas. Ligadura La ligadura de una variable es la asignación de sus atributos: nombre, dirección, tipo y valor. Con el fin de comprender adecuadamente la semántica de un lenguaje, debería conocerse el tiempo de ligadura de estos atributos, si se encuentra asociado al tiempo de compilación, carga o ejecución. El código fuente del programa se con­ vierte en código de máquina en tiempo de compilación. Durante el tiempo de carga las direcciones reubicables del código máquina se asignan a direcciones reales. Las asociaciones que se presentan durante la ejecución se dice que ocurren en tiempo de ejecución. Una ligadura estática es la que ocurre antes del tiempo de ejecución y permane­ ce fija durante la misma. Una ligadura dinámica es aquella que normalmente se pre­ senta o puede cambiar durante el tiempo de ejecución.

Ligadura de nombre La ligadura de nombre generalmente ocurre durante el tiempo de compilación. Si el lenguaje requiere que se declaren las variables, la ligadura puede ocurrir cuando el compilador ve la declaración de la variable.

Ligadura de dirección y tiempo de vida Como veremos más adelante en esta sección cuando se analicen los registros de activación, la ligadura de dirección de variables globales ocurre en tiempo de carga y es transparente para el usuario. Las variables locales para un procedimiento son comúnmente asignadas a espacio en la pila de tiempo de ejecución, por lo tanto las direcciones están ligadas al tiempo de activación durante el tiempo de ejecución. Puesto que las variables proporcionan una noción abstracta de ubicaciones de me­ moria, no hay necesidad de conocer la dirección absoluta. Si bien esto es un poco Sólo fines educativos - FreeLibros

42

PARTE I: Conceptos preliminares

más complicado en una máquina de memoria virtual,8es todavía consistente con el punto de vista del usuario. Se encuentra por lo regular que un lenguaje puede permitir que dos identificadores estén vinculados a la misma dirección. Considere en Pascal, por ejemplo, un procedimiento con un parámetro formal que es un parámetro var. Cuando se llama al procedimiento, el parámetro formal es entonces asociado con la misma dirección que el correspondiente parámetro real. Para complicar aún más las cosas, también es posible que el mismo nombre sea ligado a direcciones diferentes. Suponga que un programa tiene una variable glo­ bal llamada i. Un procedimiento también puede declarar i como una variable local. A pesar del nombre duplicado, éstas son claramente declaradas como dos varia­ bles diferentes. En el caso de un procedimiento recursivo, no obstante, una variable local tiene la probabilidad de ser asociada con una dirección diferente cada vez que se llama al procedimiento. Aclararemos cómo funciona esto en el análisis acerca de registros de activación para llamadas de procedimiento posteriormente en esta sec­ ción y en el análisis sobre recursión en la sección 2.2. Los objetos de datos pueden ser creados y destruidos durante la ejecución. Cuando llamamos un procedimiento de Pascal, los parámetros formales y las va­ riables locales son asignadas cuando el procedimiento es llamado y son desasignadas cuando termina. El periodo en que el objeto se encuentra vinculado a una dirección se conoce como su tiempo de vida.

Ligadura de valor La ligadura de valor de las variables se presenta generalmente en tiempo de ejecu­ ción, puesto que los valores pueden cambiar mientras se ejecuta una asignación o un enunciado de lectura, por ejemplo. Obsérvese que, como vimos en la sección 1.1, el almacenamiento real requerido por un valor puede ser diferente para tipos primitivos diferentes. En este sentido, el valor es algo así como una abstracción de una celda de memoria: almacenamiento para un elemento, independientemente de qué almacenamiento real sea necesario. Si el lenguaje soporta variables inicializadas, tales como la siguiente notación tipo Ada: var sum: integer :-0;

entonces la ligadura es todavía dinámica porque el valor puede cambiar. Las cons­ tantes pueden ser manejadas de manera diferente si tenemos una sintaxis tan sim­ ple como la de Pascal, así que puede ser posible ligar éstas en tiempo de compilación. Sin embargo, esto no siempre es así con las constantes Ada, puesto que algunas cuestiones de tipo pueden retardar la ligadura hasta el tiempo de ejecución.

8 En una máquina con memoria virtual, solamente parte del programa y los datos pueden cargarse en la memoria. El usuario ve el programa como un todo, mientras que el sistema puede cargar las secciones a medida que sean necesarias.

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

43

Ligadura de tipo La ligadura de tipo es estática en los lenguajes que requieren la declaración de varia­ bles. Los lenguajes como Pascal, C y Ada requieren declaraciones explícitas. Sin embargo, BASIC y FORTRAN tienen alguna tipificación implícita. Los nombres de variables BASIC como Ason reales, k% es entero, A$ es cadena. Las variables de FORTRAN comienzan con I y hasta N de manera predeterminada para el tipo entero, mientras que las otras están predeterminadas para reales. APL, SNOBOL4 y SETL2 están entre los lenguajes que soportan ligadura de tipo dinámica. En SETL2, por ejemplo, un programa puede contener enunciados como: val val

ti, 3, “helio", 63; 7;

Mientras que inicialmente contiene un conjunto, val posteriormente se vincula al entero 7. El tipo debe entonces fijarse cuando el valor se vincule, en tiempo de ejecución. Bloques y alcance La ligadura de un nombre de variable ocurre cuando se declara. El conjunto de enunciados y expresiones para el cual una variable es ligada se denomina el alcance de la variable. Las reglas de alcance de un lenguaje especifican cuáles variables son visibles en expresiones o enunciados. La colección de variables, funciones y procedimientos que son visibles en cual­ quier punto durante la ejecución (junto con las direcciones asociadas) se conocen como un ambiente. Esto incluye los identificadores locales, mientras que las reglas de alcance determinan la ligadura para los no locales. En el procedimiento en seudocódigo mostrado en el listado (1.2.1), las expre­ siones entre el beflln y el end están en el alcance de 1, i, sumy si ze. Constituyen el alcance total de 1 e i , pero no de sum o si ze. Aquí 1 e i son variables ligadas, en la medida en que están ligadas con los valores particulares asignados a ellas en el procedimiento, sum y si ze están libres en addLi st, de aquí que sus valores deban obtenerse de algún alcance más extenso. procedure addLi s t (1: arrayType); var i : i nt eger ; begin sum

(1.2.1)

0;

fo r i 1 to size do sum sum + ICil; end for; print ('The sum is: end procedure;

sum);

Por supuesto, los procedimientos pueden tener variables locales, tal como la i an­ terior, así como los parámetros, es decir, 1. También pueden tener subprocedimientos,

Sólo fines educativos - FreeLibros

44

PARTE I: Conceptos preliminares

los cuales se encuentran ligados al procedimiento padre, con variables libres. Por variable libre queremos decir una que no está ligada localmente al procedimiento en que se le utiliza. La variable s i ze anteriores libre en addLi st. En muchos lenguajes, las variables ligadas incluyen parámetros y variables declarados para ser locales para un procedimiento. Las variables globales son libres en todos los procedimien­ tos, excepto en el principal. Lo que pase con estas variables libres depende del tipo de ligadura que ocurra. En el seudocódigo del listado (1.2.2), v se encuentra ligada a cada bloque, pro­ grama a y procedimientos b y c; de modo que nombra una variable diferente en cada uno. Es útil en ocasiones pensar en ellas como a.v, b . v y c . v . x es libre en b pero está ligada en c . wse encuentra ligada en el programa a, pero es libre tanto en b como en c, y es por lo tanto una variable global, y se encuentra ligada en el procedimiento b, pero es libre en c, mientras que z está ligada solamente en c. prograi a; var v, w, x, y: integer;

(1.2.2)

procedure b; var v, y: integer; procedure c (v: integer); var x, z: integer; begln Ce)

b; end procedure; begln (b) end procedure; begln Cal

b; end prograi;

Alcance estático Los lenguajes basados en ALGOL 60 emplean un método de alcance estático o alcan­ ce lexicográfico. En este caso, una variable que es libre en un procedimiento obtiene su valor del ambiente en el cual el procedimiento está definido, en lugar de hacerlo donde se llama al procedimiento. Esto significa que la ligadura de una variable se determina por la estructura de un programa, no por lo que pasa en tiempo de eje­ cución. Con frecuencia es útil en este caso construir un diagrama de contorno para el programa. Si una variable se encuentra libre en un procedimiento, examinamos en el exterior el bloque contenedor más próximo en el cual se encuentre ligada. Para el listado (1.2.2) creamos el diagrama de contorno mostrado en la figura 1.2.1. Si y ocurre en el procedimiento c, estaría ligada a su valor en el procedimiento b, puesto que éste es el ambiente contenedor más cercano en el cual y se encuentra

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

45

ligada. Si ahora, como en el listado (1.2.2), el procedimiento c llama al procedi­ miento b, cualquier referencia a x durante la ejecución d e b e s a . x , n o c . x , puesto que a se encuentra en un bloque contenedor, mientras que c . x no es visible. De manera similar, durante la ejecución de c, una referencia a y sería b. y, no a . y, pues­ to que b es el bloque contenedor más próximo. El nombre "alcance lexicográfico" proviene del hecho de que podemos determinar la ligadura de una variable exami­ nando el código fuente para hallar el ambiente o bloque más interno en el cual el nombre de la variable esté ligado.

Bloques Un bloque es una sección contigua de código en la cual las variables locales pueden ser declaradas. Mientras que esto incluye nuestro programa y procedimientos, al­ gunos de los lenguajes estructurados en bloques incluyen un constructor de blo­ ques que puede ser colocado en el código, como el seudocódigo en el listado (1.2.3). block b; var i , j : i nt eg er ; begln

(1.2.3)

end block;

Sería común para tales bloques ser empleados como el cuerpo de un ciclo iterativo whlle» por ejemplo, de modo que el ciclo pueda tener sus propias variables loca­ les. De modo similar, pueden ser utilizados como el cuerpo de las cláusulas then o el se en ion enunciado 1f. Una construcción de esta clase se introdujo en ALGOL 60 y se encuentra disponible en lenguajes como C y Ada. Mientras que Pascal incluye declaraciones dentro de los procedimientos, sus estructuras de instrucciones anidadas no permiten tales declaraciones en línea. Sin embargo, como un descendiente de ALGOL 60, todavía se le considera un lenguaje estructurado en bloques. En algunas formas la distinción entre bloques e instruc­ ciones anidadas puede ser borrosa. Considere el seudocódigo en el listado (1.2.4), basado en un ejemplo que se encuentra en el informe de ALGOL 60 [Naur, 1963].

a vwx y

FIGURA 1.2.1 Diagrama de contorno para alcance estático

Sólo fines educativos - FreeLibros

46

PARTE I: Conceptos preliminares block q; var i , k: i nt eger ; w: r e a l ; begln fo r i :« 1 to m do fo r k 1 to m do w

(1.2.4)

aCl.k];

a[1, k]

: —a t k . i l ;

aHk.il

w;

end for; end for; end block;

Aquí, i , kyw son locales al bloque, mientras que a y mson libres. En ALGOL 60, las variables locales son visibles a todo lo largo del bloque. En Pascal se realizó un cambio sutil. Puesto que las variables de control de un ciclo iterativo for (aquí i , ky m) están destinadas a controlar el número de iteraciones y no hacer nada más, se hicieron dos reglas: primero, el cuerpo del ciclo no puede contener ningún enunciado que cambie estas variables, y segundo, son completa­ mente indefinidas a la salida del ciclo.9 En un sentido, el ciclo for i crea entonces un bloque en el cual i tiene una nueva definición. Los diseñadores de Ada llevaron esta noción un paso más allá. En la versión de Ada para este código, las variables de control del ciclo no necesitan estar explícita­ mente declaradas. Cuando empleamos un ciclo for i , i se declara de manera implícita para ser un subtipo entero en el intervalo 1 .. ma la entrada del ciclo no puede ser alterada en el cuerpo del ciclo y deja de existir a la ejecución de end 1oop.

Alcance dinámico Por alcance dinámico nos referimos a que una variable libre obtiene su valor del ambiente desde el cual es llamada, en lugar del ambiente en el que es definida. Esto no debería confundirse con las variables dinámicas, las cuales son o variables apun­ tador que pueden ser asignadas o destruidas en la pila (véase la sección 1.1), o bien variables locales a un procedimiento que son creadas cuando el alcance del proce­ dimiento se introduce y dejan de existir cuando se sale. Considere, por ejemplo, el seudocódigo del listado (1.2.5). prograi b; var a: Int eger; procedure pl ; begtn

(1.2.5)

p r i n t (a);

end procedure; procedure p2;

9 Puesto que está sin definir, el valor de la variable de control del ciclo externa al ciclo puede variar de un compilador a otro.

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

47

var a: i n t eg er begín a

0;

p l:

end procedure: begln a

7;

p2;

end p ro gr» ;

¿Cuál valor de a será impreso? Con alcance estático, cuando pl es llamado, se ob­ tiene el valor de a del bloque que contiene pl, el cual es b, por consiguiente el valor impreso sería 7. Con alcance dinámico, la llamada a pl se presenta en p2, de manera que el valor de a se toma del ambiente de p2, y se imprimiría 0. Es interesante observar que el alcance estático está prevaleciendo en los len­ guajes de programación. Las excepciones incluyen APL y algunos dialectos de LISP. John McCarthy [McCarthy, 1960 y 1965] diseñó LISP como un lenguaje con alcan­ ces dinámicos a fin de hacer posible el compartimiento de código con variables libres. Versiones más recientes, tales como SCHEME [Steele, 1978] y Common LISP [Steele, 1984], utilizan alcances estáticos. Registros de activación La implementación de la asignación de memoria para un procedimiento o función se proporciona comúnmente a través de un registro de activación o marco. La infor­ mación que necesita el procedimiento incluye parámetros y variables locales, así como la manera de regresar al ambiente que lo llama. La figura 1.2.2 demuestra la clase de información mantenida para cada proce­ dimiento. El vínculo dinámico apunta al registro de activación del procedimiento que se llama. El vínculo estático proporciona acceso al alcance lexicográficamente encerrado. La dirección de regreso y el estado anterior de la máquina son necesa-

Vínculo dinámico Vínculo estático Dirección de retorno Estado de retorno Valor de retorno Parámetros Variables focales

F I G U R A 1.2.2

Información en un registro de activación

Sólo fines educativos - FreeLibros

48

PARTE i: Conceptos preliminares

rios para restablecer el ambiente de llamada a la salida. Una función necesita un lugar para almacenar el valor de retomo. La memoria se asigna tanto para paráme­ tros formales como para variables locales. También es común asignar lugar para variables temporales usadas para los pasos intermedios en los cálculos, el número de parámetros, etc. Para el presente análisis de asignación de memoria y alcance, es suficiente considerar un registro de activación simplificado. La pila es un lugar natural para mantener estos registros, de modo que con frecuencia se les llaman marcos de pila. Cuando se llama un procedimiento, su regis­ tro de activación se coloca en la parte superior de la pila y se establecen los vínculos apropiados. Para aclarar esto, considere el seudocódigo en el listado (1.2.6). prograi a; var v» w: Integer; procedure b ( x : Integer); var y: Integer; procedure c; var z: integer begln Ce)

(1.2.6)

end procedure; begln Ib) c; end procedure; procedure d; var s, t: integer begln Cd) end procedure; begln la} b; d; end prograi;

La figura 1.2.3 muestra la pila del registro de activación con los vínculos dinámicos y variables locales a medida que ocurren los cambios cuando entramos y salimos de los procedimientos. La evaluación de los vínculos estáticos se dejará como un ejercicio. El registro de activación establece el ambiente local de un bloque. Con alcance estático, los vínculos estáticos proporcionan acceso al ambiente de los bloques que limitan. Para el ambiente dinámico, los vínculos dinámicos podrían seguirse hasta que se encuentre un ambiente que incluya la declaración necesaria. Este ejemplo también ayuda a aclarar la diferencia entre alcance y tiempo de vida. Las variables están ligadas a direcciones y están vivas mientras el registro de activación apropiado se encuentre sobre la pila. Imagine en el listado (1.2.6) que tuviéramos una llamada del procedimiento d al procedimiento b. Mientras que el alcance estático de locales en b y d es separado porque están separados lexicográ­ ficamente, los locales en d aún estarían vivos cuando se llame a b. Los detalles de esto se dejarán como un ejercicio.

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

49

dyn

dyn

dyn nil

a

dyn

X

X

y

y

dyn nil

dyn nil

V

w Principio a

dyn

dyn nil

V

w

w

w

Entrada b

Entrada c

Salida c

dyn

s t a

dyn nil

dyn nil

v

V

1

dyn nil

v

w Salida b

Entrada d

Salida d

F I G U R A 1.2.3

Registros de activación para el listado (1.2.6)

Los lenguajes variarán en la clase de información que debe mantenerse en los registros de activación. Sin anidación de bloques en FORTRAN, la estructura pue­ de ser más simple. Y un lenguaje que soporta recursión puede necesitar más infor­ mación, como lo veremos en la sección 2.2. Si bien esto estaba destinado a ser una simple introducción al concepto gene­ ral de registros de activación, pueden surgir otros puntos interesantes. Si un local es de un tipo arreglo, por ejemplo, necesitaríamos asignar lugar para el arreglo entero en el registro de activación, lo cual daría como resultado una pérdida de eficiencia. En lugar de utilizar vínculos estáticos para formar una cadena estática, todos los vínculos estáticos pueden mantenerse en un solo arreglo, llamado la pan­ talla, para mejorar la eficiencia. Los detalles adicionales se dejarán para un curso en diseño de compiladores. Sólo fines educativos - FreeLibros

PARTE i: Conceptos preliminares

E J E R C I C I O S 1. 2

1. Las variables no inicializadas son aquellas que no han sido asignadas a ningún va­ lor. Al dejarlas sin reconocer, esto puede provocar errores del programa difíciles de encontrar. Analice los méritos de las soluciones siguientes: a. Forzar al programador a asignar valores iniciales cuando una variable es creada (APL). b. Inicializar variables en tiempo de compilación si se encuentra el enunciado ade­ cuado (FORTRAN). c. Inicializar automáticamente las variables numéricas a 0 (BASIC). d. Inicializar variables para algún indicador especial (SETL2). e. Hacer la inicialización más fácil, pero no obligatoria, en el tiempo de declaración (Ada y C). 2. Ahora que usted ha visto los detalles de los registros de activación, a. Vuelva a leer la sección de ligadura de dirección, en lo que se refiere a cuáles variables se ligan en tiempo de carga y cuáles en tiempo de ejecución. b. Revise la definición de ambiente, considerando cada caso de reglas de alcance estático y alcance dinámico para la visibilidad de las no locales. 3. En Pascal, un procedimiento debe declararse antes de que pueda ser llamado, a me­ nos que se haga una declaración "forward". ¿Por qué es esto necesario? 4. Considere las reglas de alcance estático y dinámico para el código del listado (1.2.5). a. Dibuje un diagrama de contomo para alcance estático y confirme la salida de 7. b. Dibuje los registros de activación, utilice el vínculo dinámico para alcance diná­ mico y confirme la salida como 0. 5. Dibuje un diagrama de contomo para el código del listado (1.2.6). 6. Como en la figura 1.2.3, dibuje los registros de activación para el listado (1.2.6), pero incluya tanto los vínculos estáticos como los dinámicos. 7. Suponga que el listado (1.2.6) incluye una llamada del procedimiento d al procedi­ miento b. Dibuje la secuencia de registros de activación, incluyendo tanto vínculos dinámicos como estáticos. 8. Considere el seudocódigo en el listado (1.2.7). prograi a; const x * 1; var z: 1n t e g e r ; procedure p(x: 1n t e g e r); var y: 1n t e g e r ; begln fp) y z * x; prlnt (y); end procedure; procedure q(x: i n t e g e r ); var z: integer; procedure r; var y: integer begln Cr3 y z+1; p(y>: end procedure; begln Cq)

Sólo fines educativos - FreeLibros

(1.2.7)

CAPÍTULO 1: Variables y tipos de datos

51

z :» 2; r; end procedure; begin (a) z 3; qíx); end prograi; a. Dibuje un diagrama de contorno para determinar el alcance estático. b. Dibuje los registros de activación para la ejecución de este seudocódigo, inclu­ yendo tanto vínculos dinámicos como estáticos. c. Suponiendo un alcance estático, ¿qué valor sería impreso? d. Suponiendo un alcance dinámico, ¿qué valor se imprimiría?

1.3 TIPOS DE DATOS ESTRUCTURADOS Si bien hemos analizado los tipos primitivos en la sección 1.1, en la práctica encon­ tramos que los datos generalmente están estructurados de alguna manera. La ma­ yoría de los lenguajes imperativos proporciona algún soporte para tipos estructurados. Los usuarios pueden ser capaces de definir sus propios tipos, y esto puede crear programas más significativos. Se pueden combinar varios tipos para crear tipos agregados, compuestos de elementos de otros tipos, tales como arreglos y regis­ tros. La mayoría de los lenguajes de programación tiene al menos un tipo integrado, aunque existen lenguajes sin tipos, tales como APL y MUMPS, donde los objetos de datos pueden ser coaccionados automáticamente de un tipo a otro. Incluso aquí, el programador está pensando y el programa funcionando en alguna clase de tipo estructurado.

Tipos definidos p o r el usuario

Cuando un tipo se compone de valores discretos que tienen un único predecesor y sucesor, se hace referencia al mismo como un tipo ordinal o (en Ada) tipo discreto. Esto incluye tipos carácter, booleano y entero. El tipo real generalmente es excluido (aunque hay un orden, no está compuesto de valores discretos). Muchos lenguajes permiten que el programador defina nuevos tipos ordinales, ya sea al definir subrangos de aquellos previamente definidos, o bien mediante enumeración.

Tipos subrango Un tipo subrango se utiliza para restringir los valores de algún tipo padre para que estén dentro de un intervalo (range) especificado. El tipo padre está limitado a tipo ordinal en Pascal, mientras que Ada permite subrangos de tipos de punto fijo y

Sólo fines educativos - FreeLibros

52

PARTE I: Conceptos preliminares

punto flotante. Ya que las operaciones son aquellas definidas en el tipo base, esto no crea realmente un tipo nuevo. La mayoría de los lenguajes incluso permite ope­ raciones, incluyendo asignaciones, que sean realizadas entre los tipos base y subrango. type monthRange - integer 1 .. 12; dayRange - integer 1 .. 31; var month: monthRange; today, day: dayRange;

(1.3.1)

Los tipos subrango son empleados comúnmente para hacer el código más legible. En el seudocódigo del listado (1.3.1), el nombre de tipo mencionado implica el uso de variables de ese tipo. Mientras que mes y di a podrían ser simplemente de tipo entero, el subrango aclara el uso destinado. Si hora es otro tipo, ¿qué pasa al espe­ cificar el intervalo 1 . . 12, deberían permitirse asignaciones entre los dos tipos? Esto es una cuestión de equivalencia de tipo, lo que se examina más adelante en esta sección. Un beneficio adicional de los tipos subrango es la asistencia disponible en la verificación de errores. Si una variable se asigna a un valor fuera del rango o inter­ valo especificado durante el tiempo de ejecución, un error de restricción puede ayudar al programador a encontrar el problema. Puesto que esta verificación cons­ tante puede significar tiempos de ejecución más extensos, algunos compiladores pueden ofrecer un interruptor que active o desactive la verificación de intervalo (e incluso pueda desactivar la verificación de intervalo de manera predetermina­ da). Éste puede desactivarse después de que se complete alguna depuración preli­ minar, suponiendo que uno esté dispuesto a arriesgarse a errores a fin de mejorar los tiempos de ejecución.

Tipos enumerados En los tipos enumerados se enumeran todos los valores que pueden tomarse me­ diante ese tipo. Considere el seudocódigo de ejemplo en el listado (1.3.2). type meses - (Ene, Feb, Mar, Abr, May, Jun, Jul, Ago, Sep, Oct, Nov, D i c ); var mes: meses;

(1.3.2)

Los valores se conocen como literales de enumeración, mostrados aquí como identificadores. No pueden ser también utilizados para nombres de variable. En muchos lenguajes, el tipo booleano es esencialmente un tipo enumerado predefinido: bool eano - ( fa 1s e , true);. Ada también permite que los caracteres sean utiliza­ dos como literales de carácter, de aquí que el tipo carácter en Ada también sea considerado un tipo enumerado predefinido. El listado de las literales de enumeración proporciona un ordenamiento de los valores discretos, por tanto son también tipos ordinales. El código puede incluir

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

53

comparaciones, tales como 1f mes <- Junio then, o construcciones iterativas como for Mes Enero to Diciembre do. A fin de ir paso a paso a través de los valores, las funciones pred y succ devuelven el predecesor o el sucesor en la lista, aunque un intento de encontrar succ( Di ci embre) provocaría una condición de error. La cuestión del diseño de lenguaje que surge es la del uso repetido de las mis­ mas literales de enumeración. Mientras que no se permite en Pascal o C, esto es importante en Ada, puesto que los tipos de carácter caen dentro de esta categoría. Una declaración en seudocódigo tal como: type vocales - ( * a \ 4e \

4i \

'o', V ) ;

incluye las mismas literales de carácter que aquellas en el tipo carácter predefinido. De aquí que Ada haga previsiones para esta sobrecarga10 de literales. Los tipos enumerados definidos por el usuario pueden bien no estar soportados por las rutinas de entrada/salida. Un intento de pri nt (mes) podría ocasionar un error a menos que el lenguaje tenga una previsión especial para salida de este tipo. Cuando se programa en lenguajes sin tipos enumerados, es práctica común emplear simplemente enteros. Si definimos los identificadores Enero - 1, Febrero - 2, etc., y mes es de tipo entero, entonces mes Enero tiene sentido, como lo tiene for mes Enero to Diciembre do. Tipos agregados FORTRAN II tenía cinco tipos de datos simples: entero, real, real de doble preci­ sión, complejo y lógico. El único tipo agregado era el arreglo. Las cadenas de carac­ teres eran facilitadas a través de un tipo Hollerith11mutilado, el cual estaba realmente relegado a los enteros. No había otros tipos, de manera que los usuarios mantenían el "significado real" de los datos en sus cabezas o escritos a través de numerosas líneas de comentarios. La mayoría de los lenguajes más recientes (incluyendo FORTRAN 90) permi­ ten cierto número de tipos agregados, formados por componentes de otros tipos. Éstos incluyen por lo regular cadenas, arreglos, registros y posiblemente otros. Todo ello le da la capacidad al usuario para combinar diversos componentes de maneras que hacen más significativas las estructuras.

Arreglos Un arreglo es una colección de elementos de tipo hom*ogéneo. Este tipo general­ mente está ligado de manera estática con información proporcionada en la declara­ ción de tipo. Las entradas son seleccionadas mediante un índice o su subíndice que especifica su ubicación dentro del arreglo. En la declaración de seudocódigo, type gradeList - array El .. 100] of integer;

cada entrada es de tipo entero, mientras que los índices son enteros en el intervalo de uno a 100. 10 La sobrecarga se refiere a la situación en la que un elemento simple tiene múltiples significados. 11 Nombrado en honor a Hermán Hollerith, quien desarrolló la tarjeta perforada en el siglo XIX.

Sólo fines educativos - FreeLibros

54

PARTE i: Conceptos preliminares

Si bien algunos lenguajes con declaraciones como el seudocódigo var a: Integer [100];

pueden limitar los índices a enteros comenzando por 0 o 1, ahora es común permi­ tir tipos enumerados y tipos carácter, como en el ejemplo del listado (1.3.3). type days - (Sun, Mon, Tue, Wed, Thu, Fri, Sat); weekSales - array [ days ] of real; grades - ‘A ’ .. ‘F '; gradeCounts - array [ grades ] of integer; shoeSaleCounts - array [ 5 .. 15 3 of integer;

(1.3.3)

En el último ejemplo, si el almacén solamente maneja tamaños de calzado del 5 al 15, este subintervalo (subrange) de enteros puede tener sentido. En otros casos, pueden ser apropiados los enteros negativos. La especificación de un índice de arreglo permite la selección de una entrada del arreglo. Los lenguajes generalmente usan ya sea a ( i ) o a [ i ] para notación. En el antiguo FORTRAN, el paréntesis cuadrado o corchete no estaba disponible en el teclado, así que el compilador tenía que diferenciar entre una llamada de función con parámetros y un arreglo. Cuando el tamaño del conjunto de caracteres se incrementó, el corchete llegó a estar disponible, de modo que muchos lenguajes lo adoptaron para arreglos. Ada volvió a los paréntesis puesto que éstos convienen más para uso matemático. Sin embargo, la legibilidad del código puede confundir si el uso no es obvio para el lector. Mientras que el tipo del elemento generalmente se fija de manera estática, el enfoque para el número de entradas varía. Puesto que el principal objetivo del diseño en Pascal era la simplicidad, los límites inferior y superior son constantes, de modo que el tamaño del arreglo puede ser determinado estáticamente. En algunos casos, podría resultar útil ser capaz de designar el tamaño del arre­ glo durante el tiempo de ejecución. Suponga que tenemos una rutina que clasifica­ rá un arreglo con índices enteros desde 1 hasta 100. Si pudiera escribirse para clasificar un arreglo con cualquier subrango entero, llenando los límites inferior y superior dinámicamente, haría el código más reutilizable. Ada soporta esto con un tipo de arreglo no restringido. En este caso, el tipo arreglo incluye el tipo del índice, pero los límites no se asignan sino hasta el tiempo de ejecución. Observe que, una vez que se fija el tamaño (incluso en tiempo de ejecución), el tamaño no cambia durante su tiempo de vida. Esto es todavía menos que un esquema dinámico ver­ dadero, tal como el soportado en APL, que permite que el tamaño del arreglo crez­ ca y disminuya según sea necesario. Si se permiten los límites de variable, tal como en el seudocódigo, type 11 s t : array Cm .. n: integer] of integer;

entonces my n pueden completarse en una llamada de procedimiento si tenemos una variable declarada tal como: var a: listíl .. 100];

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

55

Los arreglos dinámicos están disponibles en Java haciendo uso de otro enfo­ que. Un arreglo puede declararse mediante: int a [] - new int [5];

Como en C, los índices comienzan con 0. Un arreglo multidimensional puede de­ clararse como: int a [] - new int [5]t ];

en el que cualquier otra dimensión excepto la primera puede asignarse posterior­ mente. Como resultado, int oneDimDynamic * new int [13C];

le proporcionará el efecto de un arreglo dinámico unidimensional. La implementación de arreglos requiere tanto de información acerca del tipo, mantenida en un descriptor, como asignación de memoria para las entradas del arreglo. Si suponemos que el índice de un arreglo unidimensional es un subintervalo de los enteros, entonces el descriptor debe contener el intervalo de valores índices (límites inferior y superior) y el almacenamiento de memoria requerido para cada entrada, como se ilustra en la figura 1.3.1. Este esquema permite el acceso aleatorio de las entradas del arreglo, puesto que la dirección de la entrada en la i-ésima ubica­ ción del arreglo puede calcularse mediante la fórmula IthAddrs - baseAddrs + (i - Ib) * storagePerEntry Descriptor índice LB índice UB Tipo de entrada Almacenamiento necesario por entrada ------------------------Dirección base del almacenamiento de arreglo

Almacenamiento de entrada — >

F I G U R A 1.3.1

Descriptor de arreglo

Sólo fines educativos - FreeLibros

56

PARTE I: Conceptos preliminares

Una configuración similar se utiliza para un arreglo de dos dimensiones, don­ de existen dos conjuntos de índices. Sin embargo, como la memoria de la compu­ tadora es lineal, las entradas deben almacenarse en una sola lista. Si los valores se almacenan un renglón o línea a la vez, están en orden de línea mayor. En orden de columna mayor se almacenan por columna. Estos esquemas proporcionan una forma eficiente de seleccionar una entrada de arreglo o cambiar un valor. Una estructura de datos, tal como una pila, puede crearse fácilmente dentro de una estructura de dimensiones fijas de esta clase. Puesto que la inserción o eliminación de entradas al frente o a la mitad es deficiente, exis­ ten problemas al utilizarlas para colas u otros tipos de datos abstractos más diná­ micos. El lenguaje de conjuntos SETL2 proporciona una interesante alternativa en una tupia, la que permite tipos de entrada heterogéneos y es dinámica en cuanto al tamaño. No es necesaria una declaración previa de tamaños de tupia. Es mucho más fácil para el usuario insertar o eliminar secciones y crear una cola, pero el costo de esta capacidad de programación de muy alto nivel es de mayor lentitud en tiem­ pos de ejecución, debido a los detalles adicionales que deben ser manejados por el compilador.

Cadenas Una cadena de caracteres se compone de una secuencia de caracteres. Cierto núme­ ro de lenguajes, incluyendo Java, incorporan las cadenas como un tipo primitivo, y esto es probablemente más conveniente para el usuario. Sin embargo, en Pascal, Ada y C, el carácter es el tipo primitivo, de manera que las cadenas deben almace­ narse como arreglos de caracteres. En Pascal, deben almacenarse como arreglos empaquetados con el fin de permitir comparaciones lexicográficas. Pueden tomarse diversos enfoques para mantener la longitud de una cadena. Aquí se considerarán tres de ellos. Pascal y Ada requieren una declaración del ta­ maño de la cadena como en el ejemplo de seudocódigo lastNameType - strlng [1 .. 151;

por lo tanto usan una longitud de cadena estática. Ésta puede ser implementada como un bloque contiguo de almacenamiento para el número de caracteres especificado. Las cadenas deben coincidir exactamente con el tamaño declarado, de modo que puede ser necesario el truncamiento o el relleno. Si se desean cadenas más cortas, el arreglo puede ser completado parcialmente, pero el programador debe seguir la pista del número de caracteres utilizados. PL/I permite un esquema de longitud variable con un máximo fijo. En este caso, las cadenas más extensas son truncadas, y el compilador lleva la cuenta del número de caracteres llenados. SNOBOL4 permite una longitud de cadena dinámica (dentro de ciertos límites de memoria). Esto es ciertamente más conveniente de usar, pero se requiere de un sistema superior. O es necesaria una lista vinculada de caracteres, o las cadenas tendrían que almacenarse en memoria dinámica en la pila, lo que se describió en la sección 1.1 bajo los tipos de apuntador.

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

57

A menudo es útil una variedad de operaciones para manejo de cadenas. Cuan­ do se soporta la ordenación léxicográfica por los operadores de relación, entonces 'balón' < 'barón' puesto que T < 'm'. Las subcadenas pueden seleccionarse mediante funciones predefinidas, tales como: substrtnombre, 1, 10)

la cual extrae los primeros 10 caracteres de nombre. Otro enfoque es utilizar cortes (slices) en los que nombre [1 .. 101

realiza la misma función. Los cortes están soportados en Ada. La concatenación de cadenas forma una cadena más larga mediante la unión de dos cadenas. Por ejemplo, 'programa' + 'ción' forman la cadena 'programación'. Cuando un lenguaje soporta solamente longitudes estáticas de cadena, deben to­ marse algunas precauciones si el resultado será almacenado en una variable de cadena. Las funciones de emparejamiento de patrones son también muy útiles para el pro­ cesamiento de cadenas. Por ejemplo, p o s ( ‘1 ’, ‘bello*)

devuelve 3, la primera posición de la letra * 1 * en la cadena ‘ helio*. SNOBOL4 es un lenguaje de procesamiento de cadenas que soporta una variedad de operacio­ nes muy poderosas de emparejamiento de patrones. Algunos lenguajes, como Java por ejemplo, no incluyen funciones de manejo de cadenas en forma directa, pero proporcionan un paquete (como el java.lang de Java) que incluye una clase de cadenas y métodos para manipularlas.

Registros Mientras que las entradas en un arreglo son hom*ogéneas, un registro es una estruc­ tura agregada en que las entradas pueden ser heterogéneas. La estructura del re­ gistro fue introducida por vez primera en COBOL y ha sido común en los lenguajes de programación desde entonces. Permite que la agrupación de la información se mantenga en un elemento particular. Considere el ejemplo de seudocódigo del lis­ tado (1.3.4). type fecha - record mes: 1 .. 12; día: 1 .. 31; año: integer; end record; RegEmpleado - record nombre: string [25];

Sólo fines educativos - FreeLibros

(1.3.4)

58

PARTE I: Conceptos preliminares T a s a P a g o : real; cumpleaños: date: end record; ver empleado: employeeRec;

En este caso, los datos sobre un empleado se mantienen juntos, en vez de en varia­ bles separadas. Los componentes o campos se especifican utilizando identificadores como nombres de campos. Dentro del RegEmpl eado, el campo para cumpl eaños es en sí mismo un registro, lo cual demuestra que son posibles los niveles múltiples. La selección de un campo en COBOL y ALGOL 68 se realiza con una notación como nombre o f empl eado, seleccionando el campo del nombre de la variable empl eado. En la mayoría de los lenguajes tipo Ada la selección se hace con una nota­ ción con punto, tal como e m p l e a d o . n o m b r e . De manera semejante, empl eado. cumpl eaños .año especifica una referencia de nivel múltiple. Puesto que esta notación completamente especificada puede llegar a ser engorrosa cuando se codifica, Pascal proporciona una notación wlt h, en la que se establece el registro'de modo que solamente los campos necesiten ser especificados, como se demuestra en el seudocódigo del listado (1.3.5). wlth empleado.cumpleaños do mes :« 5; día :- 12; año := 1971; end wlth;

(1.3.5)

El uso de w l t h parece funcionar mejor con secciones más pequeñas de código, puesto que las referencias a nombres de campo escondidos en el código pueden llegar a ser menos comprensibles. También debe tener cuidado si utiliza anidación de wlths. Si tenemos empl eadol y empl eado2, una referencia al campo Tasa Pago puede ser ambigua a menos que se especifique empl e adol . Tasa Pago o empl eado2. Tasa Pago. Las operaciones sobre registros están generalmente limitadas. Es común per­ mitir la asignación de registros completos del mismo tipo, tales como: empleadol :«* empleado2;

en vez de requerir que se copie cada campo. De manera similar, puede ser posible comparar la igualdad de dos registros en una declaración 1f. La asignación de memoria se hace generalmente como un bloque contiguo pa­ ra cada campo, como se ilustra en la figura 1.3.2. Puesto que el almacenamiento para cada campo es conocido, el desplazamiento para cada componente puede calcularse fácilmente. Un registro simple es a menudo menos útil en programación que una colección de registros, tales como un arreglo de registros. En el último caso, empl eadoE i ] .nombre podría referirse al i-ásimo de una lista de empleados. También es común que uno de los campos sea de tipo apuntador, de manera que puedan crearse listas liga­ das de registros. Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

59

nombre TasaPago cumpleaños.mes cumpleaños.día cumpleaños.año

F I G U R A 1.3.2

Asignación de almacenamiento para un registro

Tipos unión Si es deseable o necesario almacenar más de un tipo de valor en la misma ubica­ ción, puede ser posible utilizar un tipo unión. ALGOL 68 y C permiten tales tipos unión de manera similar al seudocódigo del listado (1.3.6). (1.3.6)

type intReal - unión i: integer; r: real; end unión; var x: i n t R e a l ; y; real;

A diferencia de la estructura de registro en la cual los valores de ambos tipos serían almacenados, aquí x puede contener únicamente un solo valor de tipo entero o real. Los identificadores i y r se utilizan como etiquetas o discriminantes, que indi­ can cuál variante se está utilizando. Asignaciones tales como: y X .i

x.r; 7;

pueden utilizarse, pero debe tenerse cuidado para asegurar que el valor de tipo apropiado se está almacenando. Después de almacenar un valor entero, sería im­ propia una referencia a x . r . Debido al uso de etiquetas, tales construcciones se co­ nocen como uniones discriminadas. En los lenguajes que permiten la omisión de etiquetas, se conocen como uniones libres. Comenzando con Pascal, se ha vuelto práctica común formar tipos unión con registros variantes. La parte variante puede presentarse al final de la declaración de registro. Considere el ejemplo de Pascal del listado (1.3.7). (1.3.7)

type RegEmpleado * record nombre: string [251; case asalariado: boolean of

Sólo fines educativos - FreeLibros

60

PARTE I: Conceptos preliminares true: ( salarlo: r e a l ; Miembrosindicato: boolean); false: ( TasaHoras: r e a l ; Horastrabajadas: r e a l ) end; {registro! var empleado: employeeRec;

El campo de etiqueta asalariado permite la discriminación del tipo de datos man­ tenidos acerca de empleados asalariados de aquellos referentes a los empleados por horas, y el código puede tomar la forma mostrada en el listado (1.3.8). 1f empl eado. asalari ado then begln PagoHensual Salario / 12: 1f empleado.Mi embros indicato then PagoMensual :- (1 - 0.02) * PagoMensual end else (por hora! PagoMensual :« Horastrabajadas * TasaHoras

(1.3.8)

El almacenamiento asignado para un registro variante debe ser suficiente para el mayor de los registros por almacenarse, y deben mantenerse descriptores de registro para cada una de las variantes. La figura 1.3.3 muestra la asignación para el listado (1.3.7). El almacenamiento requerido para el campo booleano Miembrosindicato es menor que el necesario para el campo real Horastrabajadas en este caso, mientras que sal ario y TasaHoras son ambos reales. En otros ejemplos las formas de las variantes pueden diferir en gran medida. Observe que, puesto que el campo de etiqueta puede cambiarse sin cambiar los datos, el problema de asegurar que el valor del campo de etiqueta empareja con los valores almacenados todavía existe. Ada se protege contra esto al requerir que un campo etiqueta sólo pueda ser cam­ biado si todos los campos en el registro son reasignados apropiadamente. Otro problema que se presenta en Pascal es que pueden formarse uniones li­ bres mediante la omisión del campo de etiqueta, tal como sucede en el ejemplo del listado (1.3.9).

nombre

nombre

asalariado (t)

asalariado (f)

salario

TasaHoras

Miembrosindicato

Horastrabajadas

F I G U R A 1.3.3

Asignación traslapada para registro variante

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos type RegEmpleado - record nombre: string [253; case boolean of true: ( salario: real; Mi e m b r o s i n d i c a t o : boolean): false: ( TasaHoras: r e a l ; Horastrabajadas: real) end; Cregistrol

61

(1.3.9)

Puesto que no hay campo para la etiqueta, es imposible distinguir el tipo variante. Esta clase de estructura puede utilizarse para engañar al compilador en la realiza­ ción de algunas conversiones de tipo que el lenguaje no permitiría de otra manera. A fin de evitar los problemas que se presentan en Pascal, el diseño de la construc­ ción del registro variante en Ada evita escribir un código de tal naturaleza.

Conjuntos En el sentido matemático, un conjunto es cualquier colección no ordenada de ele­ mentos distintos, a diferencia de los arreglos, los cuales están ordenados. En el modelo de Pascal, los elementos deben ser de tipo hom*ogéneo. Este tipo base está limitado a los tipos enumerados y de subrango, puesto que son de tamaño finito. Considere el ejemplo del listado (1.3.10). type intSet - set of 1 .. 10: var s: intSet begln s [1, 3, 5. 9];

(1.3.10)

end

Observe que se hace uso de paréntesis cuadrados o corchetes, puesto que los pa­ réntesis de llave se utilizan para comentarios. La implementación de Pascal utiliza un modelo de conjunto potente. El conjun­ to potente (powerset) de un conjunto es la colección de todos sus subconjuntos, de aquí la idea de que seríamos capaces de construir cualquier subconjunto. Puesto que el conjunto base tiene 10 elementos, cualquier subconjunto puede ser represen­ tado por una cadena de 10 bits, con el bit 1/0 indicando si el elemento base corres­ pondiente se encuentra o no se encuentra en el subconjunto. El conjunto [1,3,5,9] puede entonces representarse como 1010100010, con sólo el I o, 3o, 5o y 9o bits en el intervalo 1.. 10. Los límites en el tamaño del conjunto base son dependientes de la implementación y con frecuencia se mantienen bastante pequeños de modo que la cadena de bits quepa en una palabra de máquina. Ésta es una severa limitación sobre el uso de los conjuntos. Sólo fines educativos - FreeLibros

62

PARTE I: Conceptos preliminares

Las operaciones de conjuntos incluyen x In s, para probar si x es un miembro del conjunto s. De manera similar, si <- s2 es verdadero si s 1 es un subconjunto de s2. Las operaciones para unión, intersección y diferencia de conjuntos se en­ cuentran disponibles. La notación s + Cx] conforma un conjunto cuyos elementos son x y los pertenecientes a s. El lenguaje de conjuntos SETL2 proporciona un modelo más cercano al modelo matemático. Los elementos pueden ser heterogéneos, sin que haya un conjunto base que limite el tamaño, y los conjuntos son de tamaño dinámico, creciendo y disminuyendo como sea necesario. El costo para esta flexibilidad es generalmente una velocidad de ejecución lenta.

Listas Los lenguajes declarativos LISP y PROLOG incluyen un tipo lista. Las entradas en listas pueden ser ya sea elementos (llamados átomos) u otras listas. Considere la representación usual de una lista ligada, mostrada en la figura 1.3.4. En seudocódigo, se puede pensar en una declaración tal como la que se exhibe en el listado (1.3.11). (1.3.11)

type listPtr - A list; list - record data:

; link: U s t P t r end;

A diferencia de LISP, esta declaración simple restringe las entradas para que sean del mismo tipo. Si empleamos la notación de punto (dot notation) de LISP (a . b) para denotar las entradas en la lista, entonces la última entrada es (en . nil), donde ni 1 repre­ senta el apuntador nulo o lista vacía. La lista completa puede expresarse como: (el . (e2 . ( ... (en . n i l ) ... )))

Es más conveniente escribir esto como (el e2 . . . en). En esta notación la lista (a b c ) tiene tres entradas, como ( a ( b e ) d), siendo aquí (b c ) la entrada media de la lista. Es importante observar las equivalencias en el listado (1.3.12). (a) - (a . n i l )

(1.3.12)

(a b) - (a . ib . n i l ) )

F I G U R A 1.3.4

Representación de lista ligada

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

63

Las operaciones en listas incluyen la capacidad de construir y desensamblar listas. Las funciones c a r y c d r 12 seleccionan los dos componentes de un par con punto (una entrada y un apuntador de lista). Considerando el listado (1.3.12), ( ca r (a b)) - a, el átomo, mientras que ( cd r (a b )) - (b), la lista. De manera semejante, cons permite la unión de un par, de modo que (cons a (b c d) ) - (a b c d). Detalles adicionales se proporcionarán en el capítulo 8 acerca de LISP. Cuestiones de tipo Diversas cuestiones importantes de tipo surgen en el diseño del lenguaje. Si un lenguaje requiere declaraciones de tipo, el uso de una variable debe ser consistente con su tipo declarado. Además, cuando las expresiones que involucran algún ope­ rador (tal como +) son evaluadas, los tipos de operando deben ser consistentes con aquellos permitidos para ese operador.

Verificación de tipos La verificación de tipos es el proceso de evaluar las expresiones en cuanto a la compa­ tibilidad de tipo. Por ejemplo, en los enunciados a + 3 * b; p(t+l, 2.5, x);

C

b debe ser de un tipo que permita la multiplicación por un entero. De manera se­ mejante, los operandos para suma y asignación pueden ser evaluados. Los tipos de los parámetros reales para la llamada al procedimiento p pueden ser verificados en cuanto a la compatibilidad con los tipos de los parámetros formales. A fin de evaluar la compatibilidad de tipos, primero debemos ver cómo tratan los lenguajes la equivalencia de tipo: bajo qué circunstancias dos nombres de tipo se consideran el mismo tipo. Considere las declaraciones en seudocódigo del listado (1.3,13). type mes hora arreglol arregío2 arregío3 arregío4 var m: mes

(1.3.13) 1 1 -

.. 12; .. 12; array [1 .. 121 of integer; array [mes] of Integer; array [1 .. 12] of integer; array3;

12 Las fundones car y cdr se reladonan con la organización de las antiguas máquinas IBM 704 en las cuales se ejecutaba LISP, en donde car significa "contenido del registro de acceso" y cdr "contenido del registro de decremento" (ambos por sus siglas en inglés). Se pronuncian respectivamente "k ar" y "kudder" (también en inglés).

Sólo fines educativos - FreeLibros

64

PARTE I: Conceptos preliminares h: hora; a,b:arreglol; c: a r r e g l o 3 ; d, e:array CI .. 121 of integer;

Puesto que arreglol hasta arreglo4 tienen todos la misma estructura, formada por los tipos primitivos, tienen equivalencia estructural En la equivalencia de nombre, un lenguaje requeriría que las variables y operandos tuvieran el mismo nombre de tipo; por lo tanto el ejemplo representa cuatro tipos diferentes. La equivalencia estructural es soportada en FORTRAN y ALGOL. Sin embar­ go, en el listado (1.3.13), mes y hora son estructuralmente equivalentes, aunque las asignaciones u operaciones entre los tipos ciertamente serían confusas. Las reglas de compatibilidad de Pascal no se clasifican totalmente en alguna categoría. La equivalencia de nombre se requiere para el paso de parámetros, pero no en la ma­ yoría de los otros casos. Además, Pascal soporta la equivalencia de declaración, en la cual a r r e g l o3 y a r r e g l o4 se consideran compatibles puesto que a r r e g l o4 es un duplicado de la declaración a r r e g l o 3 . Ada utiliza una forma de equivalencia de nombre. En el listado (1.3.13), las variables a y b son compatibles entre sí, pero no con c, d o e. De hecho, d y e no son siquiera compatibles entre sí en Ada, puesto que la notación es considerada simplemente como una abreviación para dos declara­ ciones separadas. Puesto que Ada soporta arreglos no restringidos en los cuales los límites infe­ rior y superior son variables, los límites no pueden ser parte de un tipo de esta clase, aunque el tipo del índice podría serlo. Considerando una declaración en seudocódigo de type list: array [m .. n: integer! of real;

el tipo l i s t solamente puede especificar el tipo del índice (entero) y de las entradas (real), no los límites. En la sintaxis de Pascal correspondiente a [ANSI/IEEE-770x3.97, 1983], los límites deben estar incluidos. Como se analizó en la sección 1.2, si se declaran tipos de variable, entonces la ligadura de tipo ocurre generalmente durante el tiempo de compilación. En este caso, la mayor parte de la verificación de tipo puede hacerse de manera estática. Si la información de tipo se mantiene en tiempo de ejecución, entonces puede ocu­ rrir la verificación de tipo dinámica. Si los tipos de objeto sólo pueden ser determi­ nados durante el tiempo de ejecución y está por realizarse la verificación de tipo, ésta debe hacerse dinámicamente. La siguiente sección acerca de tipificación fuerte y débil proporcionará mayor información acerca de esto. Con el fin de que los operandos en modo mixto sean compatibles, puede ser necesario realizar una coerción de tipo (type coerción), en la cual el compilador pro­ porciona una conversión implícita de un tipo incompatible a uno que sea compati­ ble. En la expresión 3 * b, si b es real, entonces 3 puede ser convertido implícitamente a 3.0 para permitir la operación. Otros lenguajes no permiten tales operaciones en modo mixto pero proporcionan funciones para efectuar la conversión necesaria, tal como f 1oatC3) * b. La verificación de tipo de los operandos es complicada por la práctica de sobre­ carga de operador, el uso de un operador para varios tipos de operando. Por ejemplo, Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

65

en Pascal, el operador + se utiliza tanto para aritmética de enteros como para arit­ mética real, así como para la unión de conjuntos. Una complicación adicional es el uso del operador - tanto en forma unitaria (tal como - a) como en forma binaria (a - b). El operador = (prueba de igualdad) con frecuencia se sobrecarga fuerte­ mente y puede ser definido para tipos agregados. Ada complica aún más esto al permitir sobrecarga adicional de parte del programador. Los operadores dados pueden definirse en tipos definidos por el usuario o en tipos diferentes de operandos. No obstante, la sobrecarga de operadores puede hacer mucho más legible un pro­ grama. El empleo de operadores diferentes para cada tipo (tales como +i nt, +r ea 1, +set) podría ser bastante más engorrosa.

Tipificación fuerte y débil Se dice que un lenguaje está fuertemente tipificado si las reglas de tipo son estricta­ mente impuestas tanto durante el tiempo de compilación como durante el tiempo de ejecución. Si las reglas de tipo no son impuestas, a pesar de las declaraciones de tipo implícitas o explícitas, el lenguaje se considera débilmente tipificado. Una definición útil de la tipificación fuerte se debe a Gehani [Feuer, 1982]: 1. 2.

Todo objeto en el lenguaje pertenece a exactamente un tipo. Ocurre conversión de tipo al convertir un valor de un tipo a otro. La conversión no ocurre al visualizar la representación de un valor como un tipo diferente.

Mientras que se considera por lo general que Pascal es fuertemente tipificado, existen ciertas excepciones. Una de éstas es el registro variante, analizado anterior­ mente en esta sección con el tipo unión. Considere el código del listado (1.3.14). type

(1.3.14)

horrible : record case b: boolean of true: Cint: integer); false: (c2: array [1 .. 2] of char) end: var h: horrible; begln h.int :« 1; 1f (h .c 2 [ 1 3 - c h r ( O ) ) then

En este ejemplo, h contendrá ya sea un entero o arreglo de dos caracteres, pero no hay manera de decir en tiempo de compilación cuál variante está activa. El estándar de Pascal 83 [ANSI/IEEE-770x3.97, 1983] establece que el fragmento del listado (1.3.14) debería causar un error. La variante h. i nt sería activada en el enunciado h. i nt : - l ; y la variante h. c2 estaría completamente indefinida. Debería ocurrir un error al encontrar la variante inactiva h. c2 en el enunciado 1f. Sin embargo, preci­ samente lo que significa "completamente indefinida" y "causar un error" se le deja al escritor del compilador. Sólo fines educativos - FreeLibros

66

PARTE I: Conceptos preliminares

Dado el estándar Pascal 74 o la falta de detección de alta calidad de errores en el compilador, el resultado todavía sería ambiguo. Suponiendo una máquina de 16 bits, los enteros se almacenan algunas veces en los 8 primeros bits más significati­ vos y en ocasiones en los últimos. De este modo, h. i nt podría ser representado (en dígitos hexadecimales secuenciales) como 00:01 o 01:00. Entonces, si los campos de variante están traslapados, y si un carácter ocupa ocho bits, el valor de ( H. c2[1] ch r (0)) será verdadero en el primer caso de almacenamiento de enteros y falso en el segundo. Si cualquiera de estos si (if) no es verdadero, nuestro resultado podría ser o bien verdadero o falso. El lenguaje Ada, que está basado en Pascal, resolvió el problema de variante al requerir únicamente imiones estáticas y discriminadas, de modo que la consisten­ cia puede ser verificada en tiempo de compilación. Nuestro registro variante del listado (1.3.14) estaría declarado en Ada como se muestra en el listado (1.3.15). type b: boolean; type noTanHorrlble (t a g : b) ís record case etiqueta 1$ when true «> 1n t : integer; when fa l se -> c2: array [1 .. 2] of char end case; end record; var hl: noTanHorrible (true): t: b: h2: noTanHorrlble (T):

(1.3.15)

hl tendría siempre un campo 1nt, y nunca un campo c2. h2 podría tener cualquie­ ra de ellos, pero el registro completo debe ser especificado, como en: h2 h2

(false, ('0'. '«')); o (true, 35):

El lenguaje C fue desarrollado con diferentes objetivos de diseño que Pascal o Ada y es débilmente tipificado. Si se solicita imprimir el entero 67 en formato de carácter, el resultado será el carácter 'B' debido a que tiene el valor 67 en ASCII. Una comparación, tal como 8 < '8' está permitida, resultando verdadera porque '8' tiene el código ASCII 56. Las direcciones apuntador pueden ser tratadas como nú­ meros decimales sin una conversión explícita. Mientras que las variables tienen un tipo declarado, pueden ser convertidas a otro tipo casi sin que el programador se ocupe de ello. C proporciona acceso de máquina manifiesto, pero puede llevar a errores de programación difíciles de encontrar. E J E R C I C I O S 1.3 1. Si usted no puede leer o imprimir entradas de tipos enumerados, ¿cuáles son algu­ nas ventajas de utilizarlos? 2. Algunos lenguajes soportan el tipo carácter y otros el tipo cadena, como tipo primi­ tivo. ¿Cuál puede ser el razonamiento y ventajas detrás de cada decisión?

Sólo fines educativos - FreeLibros

CAPÍTULO 1: Variables y tipos de datos

67

3. ¿Cuáles son las ventajas y desventajas de tener verificación de intervalo (rango) desactivada durante la ejecución de tipos subrango? ¿Por qué un compilador podría tener esto como configuración predeterminada? 4. ¿Cuáles son las ventajas y desventajas de tener el tipo booleano como un tipo enu­ merado predeterminado? ¿Es útil del todo la ordenación implicada? 5. El lenguaje BASIC permite arreglos no declarados de hasta 10 elementos. ¿Por qué piensa usted que los diseñadores forzaron a los usuarios a declarar arreglos mayores pero no los pequeños? 6. En un arreglo no restringido, los límites inferiores y superiores del índice no necesi­ tan ser especificados. ¿Cuáles son las ventajas y desventajas de esta construcción? 7. ¿Cuál es la excepción en Pascal a la regla de que todos los tipos deben ser declarados antes de que puedan utilizarse? 8. Considere las siguientes cuatro suposiciones: 1) Los campos de variante están traslapadas; 2) Un solo carácter ocupa 8 bits; 3) Los enteros de 16 bits se almacenan con los dígitos más significativos primero; 4) Los enteros de 16 bits se almacenan con los dígitos menos significativos primero. Escriba el fragmento del listado (1.3.14) bajo las suposiciones: a. 1 ,2 y 3. b. 1 ,2 y 4. c. 1 y 3 con caracteres que ocupan 6 bits. d. 1 y 4 con caracteres que ocupan 6 bits. e. Del inciso a hasta el d sin el 1. 9. Explique cómo las reglas de Ada que gobiernan los registros variantes resolverían los incisos anteriores 8a a 8b. 10. Dibuje una representación de lista ligada para las listas (en notación LISP): a. (a b c) b. (a (b c) d) 11. ¿Cuáles son las ventajas y desventajas de un lenguaje que soporte la coerción de tipo (entre enteros y reales) para cálculos numéricos tales como 4 + 3.2? 12. ¿Cuáles son algunas ventajas y desventajas de un lenguaje que soporte la equivalen­ cia de tipos como a. equivalencia de nombre b. equivalencia estructural?

1.4 RESUMEN Los tipos primitivos en lenguajes imperativos generalmente incluyen los tipos en­ tero, real, carácter y booleano. Los tipos apuntador proporcionan acceso al almace­ namiento dinámico. Las variables están limitadas a los atributos: nombre, dirección, tipo y valor. La ligadura puede ser estática o dinámica, dependiendo del atributo y del lenguaje. Las variables pueden ser declaradas como locales en un bloque o ser libres. Las reglas de alcance, que pueden ser estáticas o dinámicas, determinan la visibilidad de las variables libres. Los registros de activación son un medio para implementar llamadas de procedimientos, y proporcionar almacenamiento para variables loca­ les así como información de ámbito. Los tipos estructurados soportan maneras de organizar datos. Los tipos defini­ dos por el usuario pueden hacer más legibles los programas, además de proporcio­ nar mejor confiabilidad. Sólo fines educativos - FreeLibros

68

PARTE I: Conceptos preliminares

Los arreglos y conjuntos son colecciones de datos hom*ogéneos, mientras que los registros permiten colecciones de tipos no hom*ogéneos relacionados. Es posi­ ble una variedad de representaciones de cadena, y toda una variedad de operacio­ nes de manejo de cadena puede ser muy útil. Los tipos unión pueden resultar útiles para almacenar diferentes tipos de elementos, pero pueden ocasionar algunos pro­ blemas de diseño de lenguaje. Las listas son un tipo agregado básico para lenguajes que soportan procesamiento de listas. Se examinaron la asignación de memoria o espacio de almacenamiento y las cuestiones de implementación, puesto que pue­ den ser de interés en el diseño de lenguajes. Las declaraciones de tipo pueden permitir que un lenguaje realice verificación de tipo en tiempo de compilación, mientras que alguna verificación de tipo puede ocurrir de manera dinámica. Esto puede verse complicado por la sobrecarga del operador: el uso de un operador con más de un tipo de operando. La compatibili­ dad de tipo de los operandos es una consideración importante cuando se evalúan expresiones. Las restricciones de un lenguaje fuertemente tipificado proporcionan detección de errores y confiabilidad, mientras que un lenguaje débilmente tipifica­ do permite fáciles conversiones de tipo cuando se desee.

1.5 NOTAS SOBRE LAS REFERENCIAS Las cuestiones de implementación han sido examinadas aquí sólo de manera bre­ ve. Aquellos que deseen más detalles podrían desear consultar libros de organiza­ ción de computadoras o diseño de compiladores. Puede hallarse información adicional acerca de la representación de datos nu­ méricos y alfanuméricos en [Knuth, 1981]. Él incluye algoritmos y análisis para aritmética de precisión simple y doble. Una introducción legible a diversas repre­ sentaciones de datos se encuentra en [Mano, 1982]. Los detalles acerca de bloques, alcances y la visibilidad de las variables pueden encontrarse en libros de diseño de compiladores tales como [Aho, 1986]. Además de información de tabla de símbolos, los registros de activación se explican adicionalmente. [Aho, 1986] también suministra más detalles para la implementación de arre­ glos y registros, pero es más bien técnico.

Sólo fines educativos - FreeLibros

CAPÍTULO 2 ABSTRACCIÓN

2.0 En este capítulo 2.1 Abstracción de datos

72 72

Excepciones Ejercicios 2.2

89 92

Los datos y el almacenamiento Tipos de datos abstractos Independencia de datos y ocultamiento de información Consideraciones teóricas Ejemplo de implementación Tipos genéricos Ejercicios 2.1

73 73

2.3 Abstracción de procedimientos

93

74 75 79 81 82

2.2 Abstracción de control

83

Ramificación Iteración Recursión

83

Procedimientos Funciones y operadores Parámetros Módulos y ADT Clases de ADT Objetos Ejecución concurrente Ejercicios 2.3

94 95 97 101 103 103 104 104

86 88

2.4 Resumen 2.5 Notas sobre las referencias

105 106

Sólo fines educativos - FreeLibros

CAPÍTULO

2

Abstracción

"Euclides sólo ha visto la belleza desnuda." Para Edna St. Vincent Millay, la abs­ tracción de Euclides del plano geométrico comprendía la "belleza desnuda", mien­ tras que las visiones más confusas de otros no lo hacían. Euclides percibió los componentes fundamentales del plano y los expresó en nueve axiomas generales y siete postulados. Demostró que éstos son suficientes para describir el plano y sus figuras, y también que cada axioma o postulado es necesario. Las propiedades esen­ ciales se pierden si alguno es omitido. Abstraer es condensar un objeto grande a sus partes esenciales, ignorando los detalles: revelar la estructura subyacente. Cuan­ do usted escribe un artículo, puede incluir un breve resumen o sumario para per­ mitir que los lectores potenciales sepan si están interesados en seguir leyendo. La abstracción también significa encontrar esas partes esenciales de un ejemplo que deben ser compartidas por cualquier otro ejemplo que se considere semejante. En una pintura abstracta pueden haberse eliminado todas las representaciones de la realidad visual excepto ciertas líneas o colores para enfatizar algo en particular. Muchos científicos computacionales, incluyendo a Edsger Dijkstra, han nota­ do que la cantidad de complejidad con que la mente humana puede arreglárselas en cualquier momento es considerablemente menor que la necesaria para escribir incluso un software bastante simple. Peter Denning [Denning, 1988] describe la abstracción en las ciencias de la computación como "modeladora de implementaciones potenciales. Estos modelos suprimen los detalles al tiempo que retienen características esenciales; son receptivos al análisis y proporcionan medios para calcular predicciones del com portam iento del m odelo". Por ejemplo, dos implementaciones para una lista lineal son un arreglo y una lista ligada. La abs­ tracción es la misma para ambas, una lista que incluya las operaciones usuales para manipularla. Mucho de las matemáticas tiene que ver con los sistemas abstractos que nos ayudan a organizar nuestro mundo y nuestro pensamiento. Los siete postulados de la geometría euclidiana pueden haber sido el primero de tales sistemas que usted haya encontrado. Definen las características esenciales de un mundo plano sin perspectiva, en términos de las dos nociones indefinidas, punto y línea. Este

Sólo fines educativos - FreeLibros

72

PARTE I: Conceptos preliminares

sistema no funciona muy bien cuando se describe la geometría del ojo, donde las vías paralelas del tren parecen encontrarse a la distancia. Para esto utilizamos un conjunto diferente de axiomas para definir la geometría proyectiva. Se necesita un sistema distinto más, la geometría esférica, para modelar el globo. Entre los lenguajes de programación, algunos sistemas funcionan mejor para ciertos tipos de problemas que otros. Para que los programadores sean producti­ vos, las abstracciones que han probado ser útiles para aplicaciones necesitan estar disponibles en los lenguajes que ellos utilizan. Las abstracciones en los lenguajes para programar computadoras son diferentes de aquellas en sistemas matemáti­ cos. Debemos considerar la abstracción tanto en su relación para resolver proble­ mas como en su relación para una máquina física. Existe un "cómo hacerlo" acerca de la computación que puede estar ausente en matemáticas. Necesitamos pensar en términos de máquinas abstractas como también en paradigmas de lenguaje. Para nuestra lista implementada, una máquina abstracta puede incluir localidades de almacenamiento consecutivas con operaciones de acceso aleatorio, o celdas binarias que contienen datos en la primera y la dirección de la celda subsecuente en la se­ gunda. De manera ideal, en un lenguaje de programación de propósito general, todas las abstracciones para todas las aplicaciones potenciales estarían integradas para uso del programador.

2.0

EN ESTE CAPÍTULO Barbara Liskov del MIT y sus colegas [Liskov, 1977; Zilles, 1986] han identificado tres clases de abstracción soportada por los lenguajes de programación: • • •

Abstracción de datos Abstracción de control Abstracción de procedimiento

Una abstracción de datos consiste en un conjunto de objetos y un conjunto de ope­ raciones caracterizando su comportamiento. La abstracción de control define un método para secuenciar acciones arbitrarias. La abstracción de procedimiento es­ pecifica la acción de un cálculo sobre un conjunto de objetos de entrada y el (los) objeto(s) de salida producido(s).

2.1

ABSTRACCIÓN DE DATOS La determinación de "datos" en un crucigrama es algo así como "materia bruta para una computadora". Los diccionarios antiguos definen el concepto como "co­ lección de hechos utilizada como una base para hacer inferencias", mientras que los nuevos incluyen la noción de computación que se realiza sobre estos hechos. El Random House Dictionary define "data" como "el plural de datum". En todas estas definiciones, el énfasis está en los elementos individuales, los cuales pueden ser recolectados de alguna manera. Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción

73

Los lenguajes de programación de alto nivel ven los datos de acuerdo con lo que puede hacerse hacia y con ellos. Para cada clase de datos, se aplican ciertas operacio­ nes ya sea para extraer o para unir partes de ellos. Por ejemplo, si nuestros datos se componen de nombres, es decir, cadenas de caracteres, un selector puede imprimir el último nombre de una cadena. Un constructor podría, en combinación con un selector, agregar una dirección apropiada para un nombre, o podría producir una lista de todos los nombres cuyo apellido comience con A. Lo que es importante re­ cordar es que solamente ciertos selectores y constructores se aplican para ciertos ti­ pos de datos. No tiene sentido multiplicar dos nombres entre sí para construir un simple objeto a partir de otros dos, o extraer el primer nombre de un entero. Los datos y el alm acenam iento

El almacenamiento, el cual consiste en la recopilación de valores de datos en un momento particular durante la ejecución de un programa, se compone de bits, y puede representarse como una serie de ceros y unos. Puede no tener otras caracte­ rísticas de definición.1Los lenguajes de programación de alto nivel fueron desarro­ llados para ayudar a los programadores a resolver de manera correcta los problemas. Los métodos de programación estructurados están destinados a mejorar tanto la confiabilidad como el entendimiento de los programas. Muy pocos programadores pueden asegurarse de la exactitud de sus programas si sólo tienen acceso a ellos a través de páginas y páginas de cadenas de bits. Grace Hopper, del equipo que desarrolló COBOL, informa que uno de sus supervisores no permitía que los pro­ gramadores utilizaran siquiera lenguaje ensamblador, pues se creía que el contacto directo con la máquina producía mejores programas. El pensamiento actual es que los usuarios serán capaces de emplear las computadoras de manera más efectiva si hay a la disposición lenguajes con abstracciones integradas que sean útiles en sus áreas de aplicación en particular. Estas abstracciones incluyen operaciones, estruc­ turas de datos y estructuras de control. Tipos de datos abstractos

Los enteros con frecuencia están integrados a un lenguaje. Si la instrucción n - 5 + 3 ocurre en un programa donde = es el operador de asignación, el contenido de la localidad de almacenamiento asignada a n será considerada como el entero 8. Por otro lado, si n - '0* + n contendrá la cadena ‘0K\ Cada tipo de datos es reconocido no solamente por sus elementos de datos, sino por las operaciones aso­ ciadas con él. A un conjunto de elementos de datos se le conoce como dominio de datos (en forma abreviada, D).2 A uno o más dominios de datos con operaciones asociadas se les denomina tipo de datos abstractos (ADT, por sus siglas en inglés). 1 El almacenamiento o memoria, por supuesto, tiene estructura puesto que está organizado enbytes, palabras, bloques, páginas, etc. También se dirige, y diferencia entre registros, de RAM, ROM , direccionable por el usuario y sectores no direccionables. Tal organización no tiene que preocuparle a nadie que programe en un lenguaje de alto nivel. 2 Lo que hemos llamado un dominio de datos a menudo se denomina un objeto de datos. En este capítulo, reservaremos el término objeto para referirnos a un "contenedor para datos", según [Liskov, 1986]. Entre los lenguajes orientados a objetos, el término se utiliza para referirse a los módulos jerárqui­ cos que contienen tipos de datos abstractos.

Sólo fines educativos - FreeLibros

74

PARTE I: Conceptos preliminares

Como un ejemplo, el tipo entero en Pascal se describe en el listado (2.1.1). D - (0, ±1, +2, ..., imaxint} Identificador constante: maxint (dependiente de la máquina) Operaciones: Operadores unitarios - [+, -) Operadores binarios = C+, *, dlv, iod3

(2.1.1)

En LISP, la lista es el tipo de datos integrado básico, y los enteros se describen (en el dialecto SCHEME) en el listado (2.1.2). DI - C0, +1, ±2, ...], D2 » [#T, #F) Constants: #T, #F (representing true and false) Procedures: (* numl, num2) -> num (+ numl, num2) -> num (- numl, num2) -> num (abs num) -> num (integer? obj) returns #T if obj is an integer, #F otherwise. (zero? num) returns #T if num = 0, #F otherwise.

(2.1.2)

Subyacente a estas descripciones se encuentra una abstracción matemática común que define los enteros y sus propiedades, y su fundamento es la abstracción para un anillo,3 la cual describe todas las estructuras con las mismas operaciones y com­ portamiento que los enteros. Se espera que los enteros con sus operaciones asocia­ das se comporten de manera apropiada en cualquier máquina en la que se ejecute un programa. De este modo, se necesita una abstracción adicional que represente las propiedades enteras de un CPU para completar nuestro tipo de datos abstrac­ tos para los enteros. Los compiladores reales para computadoras en particular re­ presentan implementaciones de estas abstracciones, como lo hace la sintaxis particular utilizada. Un lenguaje estándar, que especifica las características necesa­ rias de cualquier implementación del lenguaje puesto en consideración, especifica algunos detalles de implementación así como también de sintaxis para los tipos de datos.

Independencia de datos y ocultamiento de información El enfoque para resolución de problemas llamado refinamiento por pasos involucra dos actividades: la definición de los módulos de programa necesarios para llevar a cabo las diversas actividades involucradas en la solución, y la definición de tipos de datos, incluyendo sus interacciones con las actividades de solución. Considere el problema de trazar rutas de aviones. Algunas de éstas serán vuelos sin escalas

3 Un anillo es una estructura R * <S, +, * . 0, 1>, donde S es un conjunto. + y * son operadores binarios en S que tienen las mismas propiedades que la suma y la multiplicación de enteros, como por ejemplo, a + b = b + a, a + -a = 0, a * 1 = 1 * a = a, y a * (c + d) = a * c + a * d, entre otras. Para una definición completa, consulte cualquier texto de álgebra moderna, por ejemplo [MacLane, 1968].

Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción

75

entre ciudades, mientras que otras involucrarán uno o más vuelos de conexión. Cuando iniciamos el programa, la forma de los datos es bastante vaga, quizá una lista de ciudades y el número de vuelos diarios deseados entre ellas. Muy al princi­ pio en el proceso de resolución del problema, será obvio que estaremos trabajando con una gráfica, puesto que las conexiones entre dos ciudades son de doble senti­ do, y cualquier ciudad determinada puede estar conectada con más de una ciudad. Sin embargo, no necesitamos preocupamos acerca de cómo representar la gráfica con los tipos disponibles en el lenguaje que hemos elegido en este nivel. Todo lo que necesitamos hacer es pensar en la gráfica en relación con las operaciones que deseamos. Considere, por ejemplo, las del listado (2.1.3). conectatciudadl, ciudad2, día, hora) desconectaícíudadl, cíudad2, día, hora) distanciatciudadl, ciudad2) listaTodasCiudades dondePuedoIrDesde(ciudad)

(2.1.3)

Cada módulo del programa tendrá conocimiento acerca de las ciudades y las rutas sólo a través de estas operaciones, conocidas pero todavía no especificadas, asocia­ das con las ciudades y las rutas. El encapsulamiento de datos se refiere al agrupamiento de información acerca de los tipos y operaciones de un tipo de datos abs­ tractos en una unidad de programa simple. Si después de que los datos y sus operaciones asociadas han sido definidos, llega a ser necesario cambiar la representación de los datos, incluyendo la gráfica de las rutas, no necesitará cambiarse en el programa más que estas operaciones. Esta propiedad se conoce como independencia de datos; es decir, que los datos reales son independientes de su representación. Los programas escritos de manera independiente de las representaciones de datos finales ofrecen muchas ventajas. Entre éstas se encuentra el ocultamiento de información, el cual hace que un programa sea más fácil de comprender para el usuario, hace que los programas sean transportables entre lenguajes y máquinas diferentes, y consigue que ciertas medidas de seguridad sean prácticas. El prin­ cipio del ocultamiento de información consiste en hacer visible todo aquello que sea esencial para el conocimiento del usuario, y ocultar todo lo demás. Discutire­ mos esto con más detalle en la sección 2.3 que trata sobre la abstracción de procedi­ mientos.

Consideraciones teóricas Usted puede haberse sorprendido de que esta sección acerca de abstracción de da­ tos haya comenzado con un análisis de bits y almacenamiento de caracteres en la máquina. Esto nos lleva de regreso a la diferencia entre abstracciones matemáticas y abstracciones relacionadas con la computadora, donde la máquina real está siem­ pre escondida en el fondo. Necesitamos la seguridad de que las abstracciones desa­ rrolladas para una aplicación puedan ser implementadas tanto en el lenguaje de alto nivel que estemos empleando como en su implementación de máquina a tra­ vés de un compilador, en conformidad con las notaciones comunes que teníamos

Sólo fines educativos - FreeLibros

76

PARTE I: Conceptos preliminares

en mente. Sólo que, ¿cuál clase de máquina abstracta representa nuestros tipos de datos abstractos, incluyendo sus dominios de datos y procedimientos asociados? Antes de que podamos contestar esta pregunta, debemos estar absolutamente se­ guros de lo que queremos decir con tipos de datos abstractos, lo que habilita a un programador para posponer la selección de estructuras de datos reales hasta que todos los usos de los datos se hayan comprendido por completo. También facilitan la modificación y mantenimiento del programa para mejorar el desempeño o dar cabida a nuevos requerimientos. La ciencia de la computación teórica emplea los métodos de las matemáticas para especificar y probar nociones semánticas, el "significado" de las construccio­ nes del lenguaje. La abstracción de datos puede definirse brevemente como el par [objetos, operaciones]. Algunos análisis de los tipos de datos abstractos (ADT), no se molestan en absoluto en manejar objetos. Cualquier objeto que esté sujeto a las diversas operaciones es aceptable. De acuerdo con esta manera de pensar, un ADT se describe enteramente mediante sus operaciones. Un ADT, cuando es implementado en una computadora (teórica), especifica qué clase de valores puede mantener un objeto o contenedor para datos en particular. El contenedor de datos, por supues­ to, debe ser especificado eventualmente en términos de bits, bytes y palabras de computadora. La carga de este análisis teórico es investigar cómo podemos hacer precisas estas nociones, y probar que una implementación. de un tipo de datos representa fielmente el tipo abstracto. Dos enfoques de esta clase han sido explotados: el mé­ todo de modelos abstractos iniciado por C. A. R. Hoare [Hoare, 1972] y la especifi­ cación algebraica presentada por John Guttag [Guttag, 1977],

M odelos abstractos . El método de los modelos abstractos incorpora procedimien­ tos más condiciones sobre los datos en los que ellos funcionan. Estas condiciones pueden ser de tres clases: condiciones previas (precondiciones), condiciones poste­ riores (poscondiciones) e invariantes. Una precondición debe ser verdadera antes de que se ejecute un procedimiento, una poscondición debe serlo cuando un proce­ dimiento termina, y una invariante debe ser verdadera tanto a la entrada como a la salida de un procedimiento. Es el trabajo ya sea del programador o del escritor del compilador especificar y comprobar estas condiciones cuando se implementa un procedimiento. Como se le solicitará investigar en el ejercicio 2.1.2, no es posible o incluso deseable para un lenguaje de alto nivel incluir todos los tipos de datos abstractos que un usuario pueda querer. De este modo, la verificación de tipos de datos debe ser abordada tanto por el implementador como por el usuario de un lenguaje. Este método fue presentado por C. A. R. Hoare [Hoare, 1972], haciendo uso de la sintaxis de SIMULA [Dahl, 1966], el primer lenguaje basado en clases. Una clase contiene un tipo, o tipos, de datos, más una descripción de las operaciones asocia­ das. Considere el seudocódigo de ejemplo en el listado (2.1.4). speclfication SmallIntSets;

(2.1.4)

export initialize» size, insert, remove, isln;

Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción

77

constant maxSize: integer; type integer, boolean, smallíntSet; function initialize(): smallíntSet; function size(s: smallíntSet): integer; function insert(s: smallíntSet; i: integer): smallíntSet; function removefs: smallíntSet; i: integer): smallíntSet; function i sin (i: integer; s: smallíntSet): boolean; end specificatíon;

Aquellos identificadores (tipos, procedimientos, etc.) que van a ser visibles fuera de la especificación están incluidos en la lista de exportación. Una invariante para los cinco procedimientos (funciones aquí) es: i: 0 sizeís: smallíntSet) maxSize

De esta forma, para cualquier parámetro s, que representa un sma 11 I n t S e t , s i ze ( s ) debe estar entre 0 y cualquier valor que haya sido establecido para maxSi ze. Para i n i t i a 1 i z e, no hay precondiciones, puesto que i n i t i a 1 i z e no tiene parámetros. La poscondición que debe probarse además de la invariante i es la de salida s : s O.

Para s i ze, la invariante debe mantenerse y también la poscondición s i ze ( s ) = |s |, donde |s ( es la cardinalidad del conjunto s. Para insert, la invariante i debe mantenerse tanto para la entrada si como para la salida s2, así como las dos poscondiciones mostradas en el listado (2.1.5). 1)

if (i e

si)

(2.1.5)

then |s2| = |sl| else |s2] = |si| + 1;.

2)

s2 = si u

{1}

Observe cómo estas condiciones están expresadas usando el lenguaje de la teoría de conjuntos. Obsérvese también que aquellos dos tipos previamente definidos, i n t e g e r y bool ean, están incluidos en Smal 1 I nt Se t s. Las propiedades de los valo­ res enteros y booleanos son heredadas por smal 1 I n tS e t , el cual nos capacita para comparar s i z e ( s ) con maxSize sin definir específicamente <. El método de los modelos abstractos es realmente más detallado que el que hemos presentado aquí. Existen tres niveles de abstracción involucrados. El nivel más alto, o más abstracto, es el conjunto T de todas las clases definidas como tipos de datos. El segundo es la clase particular o tipo abstracto t, tal como Smal 1 I n t Se t s. Incluidos en la clase t = Smal 1 I n t S e t s están una constante (maxSi ze), tipos de datos ( i n t e g e r con parámetro i , s m a l l í n t S e t con parámetro s y boolean con valores verdadero/true y falso/false); y cinco procedimientos. En el nivel más bajo se en­ cuentran las implementaciones de los procedimientos y la estructura de datos

Sólo fines educativos - FreeLibros

78

PARTE I: Conceptos preliminares

sma 111ntSet, así como la especificación de los dominios de datos para entero (integer) y booleano (bool ean). El método de Hoare de los modelos abstractos proporciona mapeos entre cada uno de estos niveles, los cuales están formalmente probados para interpretar el tipo de datos abstractos (ADT) de acuerdo con las invariantes, precondiciones y poscondiciones. E specificación alg ebraica . El segundo método para probar formalmente que los tipos de datos abstractos realmente hacen lo que pensamos que deberían hacer se debe a John Guttag [Guttag, 1977]. Una especificación algebraica tiene dos partes: una especificación sintáctica y un conjunto de relaciones. Un ejemplo de una especifica­ ción para una cola se proporciona en el listado (2.1.6). (2.1.6)

Syntax:

newQueue () add (queue, item) front (queue) remove (queue) isEmpty (queue)

- » queue -> queue —>item —» queue -> boolean

Relations 1)

isEmpty(newQueue()) = true

2)

isEmpty(add(q,item)) = false

3)

front(newQueue()) = error

4)

front(add(q,item)) = if isEmpty(q) then item

5)

remove(newQueue()) = error

6)

remove(add(q,item)) = if isEmpty(q) then newQueue()

el se front(q)

else add(remove(q),item)

Esta especificación sería escrita en la fase de diseño, antes incluso de considerar un lenguaje de computadora. La ventaja de este sistema es que no necesitamos em­ plear ningún metalenguaje,4 tal como el lenguaje de teoría de conjuntos anterior, para hablar acerca de los procedimientos que estamos definiendo. La desventaja es que debemos convencemos nosotros mismos o probar que las relaciones son con­ sistentes y esencialmente completas. Cuando decimos que las relaciones 1 a 6 anteriores son consistentes, queremos decir que no se contradicen entre sí. Es decir, no podemos demostrar que alguna relación (i) sea falsa, dado que las otras cinco relaciones son verdaderas. Para con­ siderar completa la especificación, debemos estar seguros de que no hemos olvida­ do alguna característica necesaria para una cola. Las condiciones de frontera, tales como aquellas que ocasionaron errores anteriormente, son en particular fáciles de descuidar. En cualquier implementación de un ADT para una cola, necesitaríamos de­ mostrar que se mantienen las relaciones anteriores. Además, cada uno de los cinco procedimientos puede ser proporcionado con invariantes, precondiciones y 4 Un sistema teórico S es escrito en un lenguaje particular Ls. Cuando analizamos S, usamos Ls y el lenguaje de la lógica, el cálculo de predicado. Esto incluye relaciones tales como =, or y &. Si empleamos cualquier otro lenguaje L para examinar S, L se denomina un metalenguaje; es decir, L analiza S.

Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción

79

poscondiciones. Existen dos tipos: aquellos inherentes al tipo de datos abstractos mismo y aquellos que dependen de la implementación en particular. Por ejemplo, si nosotros implementamos una cola como un arreglo, una precondición depen­ diente sobre add (q , i tem) sería que q ya no estuviera llena. Una precondición inhe­ rente sobre r emov e (q) sería que q no estuviera vacía.

Ejemplo de implementación Zilles y sus colegas [Zilles, 1986] identifican dos requerimientos que un lenguaje que soporte abstracciones de datos debe satisfacer: 1.

2.

Se necesita una construcción lingüística que permita la implementación de una abstracción de datos como una unidad. La implementación involucra la selec­ ción de una representación para los objetos de datos y la definición de un algo­ ritmo para cada operación en términos de esta representación. El lenguaje debe limitar el acceso a la representación únicamente a las opera­ ciones. Esta limitación es necesaria para asegurar que las operaciones caracte­ ricen completamente el comportamiento de los objetos.

El primer requerimiento significa que el lenguaje mismo debe dar cabida a algún método para empaquetar tipos de datos y sus operaciones asociadas en una clase. El segundo facilita la verificación de los programas y la independencia de datos. Supongamos que nuestro seudocódigo incluye la sintaxis para declaraciones de especificación siguiendo el patrón del listado (2.1.4). También supondremos que las funciones pueden devolver tipos agregados. Entonces una implementación par­ cial del listado (2.1.6) toma la forma mostrada en el listado (2.1.7). spedflcatlon ItemGueue; lip o rt item; export queue, newQueue, destroy, add, front, remove, isEmpty; type queue, ítem; functlon newQueueí): queue; Cefectos: devuelve una nueva cola sin elementos en ella.3 functlon destroyívar q: queue): queue; [efectos: desasigna el almacenamiento para todos los nodos en la q.3 functlon addívar q: queue; i: item): queue; [modifica: q efectos: agrega i al final de q.3 functlon frontíq: queue): item; (efectos: devuelve el elemento al frente de la q.3

Sólo fines educativos - FreeLibros

(2.1.7)

80

PARTE I: Conceptos preliminares functlon removefvar q: queue): queue; (modifica: q efectos: elimina el primer elemento de q, a menos que q esté vacía, en cuyo caso ocurrirá un error.) functlon isEmptyfq: queue): boolean; (efectos: devuelve el valor verdadero o true si q está vacía, y false de otro modo.) end speclfIcatlon; lapleientatlon ItemQueue; type queue - "queueNode queueNode - record element: item; next: queue end record; (queueNode) functlon newQueueO: queue; begln newQueue := n il; end functlon; (NewQueue)

end lapleaentatlon;

Si podemos agrupar objetos de datos y sus operaciones en conjunto, y si la implementación se oculta al usuario, la estructura soporta tipos de datos abstrac­ tos. De manera ideal, las únicas operaciones permitidas sobre elementos del tipo cola (queue) son las definidas en la especificación, es decir, newQueue, d e s tr o y, add, f r o n t , remove e i sEmpty. En un programa que usa el ADT ItemQueue en el cual var q : queue ; ha sido declarado, el enunciado q : = nil debería ser ilegal. Las únicas asignaciones para q deben hacerse a través de newQueue, des tr oy , add o remove.

L A B O R A T O R I O 2.1: T I P O S D E D A T O S A B S T R A C T O S : A D A / P A S C A L

Objetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Construir y utilizar un tipo de datos abstractos en un lenguaje con facilidades para construcción de módulos. 2. Compilar el paquete o módulo por separado, si es posible, e incorporarlo en otro programa. 3. Investigar las medidas de seguridad en el lenguaje que se esté utilizando mediante intentos de operaciones ilegales en elementos tales como tipos privados.

Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción

81

Tipos genéricos

Una de las molestias de un lenguaje como Pascal es la necesidad de escribir nuevos procedimientos y funciones para cada tipo de datos. Por ejemplo, si queremos un procedimiento de intercambio para cada uno de los tipos i n t e g e r , r e a l y char, necesitaríamos tres procedimientos, con declaraciones de procedimiento como las mostradas en el listado (2.1.8). procedure swaplnt(var n, m: integer);

(2.1.8)

procedure swapReal(var x, y: real); procedure swapChar(var el, c2: char);

Podría ser útil tener un único nombre de procedimiento de intercambio que tratara con estos tres (y quizá más) tipos de parámetros. Un tipo genérico puede actuar como una plantilla para elementos de distintos tipos mediante el uso de un parámetro en la declaración de tipo. Una facilidad genérica es soportada por Ada, Smalltalk, C++ y Object Pascal, entre otros. En Pascal, cualquier tipo de arreglo, tal como: type íntlist = array [1 .. 1003 of integer;

viene con operaciones estándar para el arreglo (tales como la de hacer índices) sin importar el intervalo o tipo de entrada del arreglo. Ada permite que el intervalo se deje en blanco cuando se declara el tipo base, y el tipo se instaura cuando el inter­ valo se proporciona posteriormente. Considere las declaraciones de seudocódigo en el listado (2.1.9). type

(2.1.9)

intlist = array [m .. n: integer] of integer;

var list: intlist [1 .. 100];

Aquí hemos especificado que los índices serán del tipo subrango entero, lo cual es genérico puesto que la m .. n actúa como una lista de parámetros para ser instaurada posteriormente. La inclusión de 1 .. 100 en la declaración para la variable l i s t proporciona los límites de intervalo necesarios. En nuestro ejemplo del listado (2.1.7), obtuvimos un comienzo al hacer un ADT ItemQueue general para cualquier tipo i tem que se hubiera querido. Podemos de­ sear hacer ItemQueue un tipo base para una variedad de tipos i tem declarándolo como un ADT genérico. Suponga que cambiamos la definición de ItemQueues a la forma mostrada en el listado (2.1.10). specificatión ItemQueue; export queue, newQueue, destroy, add, front, remove, isEmpty;

Sólo fines educativos - FreeLibros

(2.1.10)

82

PARTE

i: Conceptos preliminares

type queue (generic type item);

end speclficatlon;

Todo lo que hemos hecho es mover la declaración de i tem de modo que aparezca como un parámetro del tipo q ue ue en la especificación y nombrarlo gener 1c. Ahora podemos crear y usar un I temQueue que contenga elementos reales, como se mues­ tra en el listado (2.1.11). type

(2.1.11)

specífi catión

use ItemQueue; type realQueue = new queue(real); var Q: realQueue; begln Q := newQueue(); end;

También podríamos declarar otras colas, como en el listado (2.1.12). type speclficatlon

(2.1.12)

use ItemQueue; type charQueue = new queue(char); var Q: charQueue; begln Q := newQueue(); end;

Los ejemplos de new anteriores son especificaciones genéricas. La facilidad genéri­ ca no necesita estar sujeta a estas especificaciones, pero es útil para declarar ejem­ plos new de funciones o procedimientos individuales. Con una especificación genérica obtenemos, por supuesto, versiones de cada procedimiento y función es­ pecializadas para el (los) tipo(s) de datos particular(es) que queremos utilizar. [Piense cuán favorable sería programar un procedimiento de intercambio solamente una vez, y entonces declarar nuevos ejemplos de éste para pares de valores que quisié­ ramos intercambiar!

E J E R C I C I O S 2.1 1. Cuando se hace un modelo del tráfico que cruza un puente, es necesaria una abstrac­ ción para una cola. Enumere tantas abstracciones como pueda para las aplicaciones que siguen. a. Un contador de entradas para una tienda b. Un sistema de conteo LIFO (último que entra, primero que sale, por sus siglas en inglés); un sistema FIFO (primero que entra, primero que sale, por sus siglas en inglés). c. La construcción de un diccionario. d. Un paquete de procesamiento de palabras.

Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción

2. 3.

4. 5.

6.

7. 8. 9.

83

e. Un demostrador automático de teoremas. f. Un sistema de reservaciones para una aerolínea. g. Un sistema computarizado de inyección de combustible en un automóvil. Dé dos razones por las que un lenguaje de propósito general con todas las abstrac­ ciones útiles integradas no es práctico. Usando manuales para dos o más lenguajes disponibles para usted: a. ¿Qué tipos de datos están integrados? b. Escriba una descripción de uno de estos tipos de datos, incluyendo el (los) dominio(s) de datos, las constantes asociadas y las operaciones, como en los lista­ dos (2.1.1) y (2.1.2). Defina un tipo de datos abstracto para apuntadores. ¿Usted permitiría operaciones aritméticas ilimitadas, como en el lenguaje C? Si no fuera así, ¿cuáles incluiría? Considere la especificación de Smal 1 IntSets en el listado (2.1.4). a. H aciendo uso de un lenguaje familiar para usted, sugiera las diferentes implementaciones para Smal 1 IntSets. b. ¿Cuáles son las precondiciones y poscondiciones para remove e i s I n? c. De las implementaciones que realizó en el inciso a, elija una, y escriba procedi­ mientos para si ze, insert, remove e is l n. d. Especifique Smal 1 IntSets algebraicamente, como en el listado (2.1.6). Demuéstrese a usted mismo que las relaciones 1 a 6 del listado (2.1.6) describen com­ pletamente una cola (queue). Puede encontrar útil el uso de una cola de ejemplo. ¿Existen algunos otros procedimientos que pueda usted querer? Si es así, ¿qué rela­ ciones adicionales son necesarias? Verifique que la descripción del tipo de datos abstractos para queue del listado (2.1.7) satisface la especificación algebraica del listado (2.1.6). El listado (2.1.7) incluye una implementación de seudocódigo de la operación newQueue. Implemente las otras cuatro operaciones del tipo queue en seudocódigo. Escriba un procedimiento swap (intercambio) genérico en seudocódigo y declare nuevas versiones del mismo para reales, enteros y caracteres (véanse los listados (2.1.10) y (2.1.11)).

2.2

ABSTRACCIÓN DE CONTROL La mayoría de los programas se construyen para transformar o responder a los datos. Hemos examinado brevemente las abstracciones de datos anteriores, y aho­ ra consideraremos mecanismos que nos permitirán movemos a través de una es­ tructura de datos, cambiando o manteniendo los valores como deseemos.

R am ificación

Por lo general un programa se ejecuta en forma secuencial, comenzando con el primer enunciado y terminando con el último. La ramificación involucra la reubicación de la ejecución del programa en una porción de nuestro código fuente posiblemente diferente del enunciado subsecuente. Aquellos que estén familiari­ zados con un lenguaje ensamblador reconocerán que la ramificación puede llevar­ se a cabo usando un enunciado de ramificación (condicional) o un enunciado de salto. En la mayoría de las máquinas, una reubicación de un enunciado ramificable

Sólo fines educativos - FreeLibros

84

PARTE i: Conceptos preliminares

está restringida a un pequeño intervalo de direcciones y/o etiquetas, mientras que un salto permite la reubicación a cualquier palabra. Los saltos son necesarios para implementar procedimientos, pero también han sido implementados directamente en código fuente a través del enunciado goto. Todavía persiste la controversia acerca de la conveniencia de permitir gotos, co­ menzando con el famoso artículo de Dijkstra, "Go to statement considered harmful" ("El enunciado goto se considera nocivo") [Dijkstra, 1968b]. Puede ser útil recordar que los primeros lenguajes de programación de alto nivel (por ejemplo FORTRAN) eran escritos para máquinas particulares, y comen­ zaban con un lenguaje ensamblador, el cual era luego rescrito para convertirlo en algo más parecido a un lenguaje científico convencional. Así, las construcciones en ensamblador se matizaron para que se parecieran al inglés. Tales finuras estilísticas son llamadas a menudo "azúcar sintáctica": pueden no ser necesarias, pero hacen el lenguaje más atractivo para un programador. Los diseñadores modernos de len­ guajes con frecuencia comienzan con un lenguaje familiar para la comunidad de usuarios finales, y se preocupan después por los compiladores y ensambladores. Por ejemplo, la sintaxis de ALGOL y sus sucesores, Pascal y Ada, es similar a un lenguaje algebraico que describe algoritmos.5 Los enunciados de ramificación de alto nivel más comunes son 1f •. .then... (else) y case. El primero proporciona una ramificación de dos vías y la segunda una ramificación de múltiples vías. Cuando un lenguaje como Pascal o C no requiere completar un enunciado 1f con end 1f, pueden presentarse algunos problemas. Considere el fragmento de seudocódigo en el listado (2.2.1). y 1 1f y = 0 then x := 3 else x := 1; print (x); CI será impreso} z := y < 0; 1f z then tf y > -5 then x := 3 else x *.= 5; printfz, x); {falso, se imprimirá 1}

(2.2.1)

Para ver por qué el valor de x permanece en 1 después de que el segundo 1f es ejecutado, deberíamos estar conscientes de que estas reglas de lenguaje establecen que un else pertenece al 1f más cercano que pueda aceptarlo. La sangría mostra­ da en el listado (2.2.2) ilustra su evaluación adecuada. z := y < 0; 1f z then 1f y > -5 then x := 3 else x 5;

(2.2.2) Ceste 1f no tiene cláusula else)

5 Un algoritmo es una descripción ordenada de los pasos necesarios para resolver un problema.

Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción

85

Aquí se asigna a z el valor booleano falso ( f a l s e ) , puesto que la expresión y < 0 es falsa ( f a l se). Así, el enunciado es: If f a l se then.. . El problema de más 1fs que elses se conoce como el problema del "else colgante". Los lenguajes como Ada requieren el uso de end 1f, el cual puede ayudar a evitar confusiones. Utilizando esta construcción, el enunciado anterior se escribiría en seudocódigo como se muestra en el listado (2.2.3). 1f y <

(2.2.3)

then

if y > -5 then x :- 3; else x := 5; end if; end if;

Esto aclara cuál enunciado If tiene una construcción else y cuál no. El enunciado case depende de un discriminante para seleccionar el caso apro­ piado. El ejemplo de seudocódigo del listado (2.2.4) incluye el discriminante today. case today of

(2.2.4)

Mon..Thu: work; Fri:

work; party;

otherwise: relax; end case;

La selección de múltiples vías también puede ser soportada por una extensión del enunciado 1f, tal como: 1f

then <enunciado> (e ls e if then <statement>} [else <statement>] end If;

El 1f y cada elself tienen una condición por probarse. La evaluación sigue su curso a través de cada una hasta que se encuentra una condición verdadera, de donde se devuelve el resultado correspondiente. Si todas son falsas, se aplica la ramificación else. Considere la función en seudocódigo del listado (2.2.5). function salesTax (state: strin g [2]; cost: real): real; var taxRate: real; begin if state = ’A Z 1 then taxRate := 0.05; elseif state

= 'C A ' then

taxRate := 0.06;

elseif state

= 1C T ' then

taxRate := 0.075;

elseif state

= 1N J 1 then

taxRate := 0.06;

else taxRate

:= 0;

end if; salesTax := taxRate * cost; end function;

Sólo fines educativos - FreeLibros

(2.2.5)

86

PARTE I: Conceptos preliminares

Por lo tanto, sal esTax(‘CT\ 100) =7.5, salesTax( ‘AZ’ , 100) = 6.0 y salesTax(' VT’ , 100) = 0. El enunciado else será ejecutado si todas las expresiones condicionales precedentes son falsas. Es común que el discriminante de caso (case) deba ser de tipo ordinal (limitado a tipos entero, carácter, booleano, enumerado o subrango). Si una condición involucra una prueba de valores reales, todavía puede realizarse una selección de múltiples vías mediante la construcción elseif, como se ilustra en el siguiente ejemplo: if numGrade >=

90then grade := 'A';

elseif numGrade >= 80 then grade

(2.2.6)

:= 1B *;

elseif numGrade >= 70 then grade

:= 'C1;

elseif numGrade >= 60 then grade

:= 'D1;

else grade ;= 1F 1; end if;

Si numGrade = 84.3, entonces la primera prueba es falsa; la segunda prueba es verdadera, de modo que grade llega a ser 4B* y salimos de la construcción. Iteración Por iteración nos referimos a la repetición (quizás cero, uno o más veces) de un enunciado o bloque de enunciados. Esto permite movemos a través de todos los elementos de un agregado de una manera ordenada, visitando cada uno solamente una vez. Por ejemplo, si la vajilla de plata es un conjunto de cuchillería, podríamos querer ir hasta el último elemento, contando el número de tenedores, cuchillos, cucharas, etc. Puede no importamos exactamente cómo se realiza esto, sólo el re­ sultado que se obtiene. El iterador o repetidor más simple es un enunciado for. Considere el listado (2,2.7). sum := 0; for i := 1 to 20 do

(2.2.7)

sum := sum + i ; end for;

El ciclo se repite sobre los enteros entre 1 a 20, calculando sus sumas a medida que pasamos por ellos. Durante la ejecución del enunciado for ocurren los siguientes pasos: 1. 2. 3.

La variable de control de ciclo (vcc) i se inicializa al límite de arranque. Si la vcc es igual o menor que el límite final, el cuerpo del ciclo se ejecuta, de otro modo salimos del ciclo. La vcc se incrementa y el control regresa al paso número 2.

Observe que en el caso for i := 5 to 1 do, la prueba en el paso 2 es falsa, de modo que el cuerpo del ciclo nunca se ejecuta. Muchos lenguajes proporcionan una ca­ racterística como for i := 5 downto 1 do para permitir un orden inverso. Los tamaños de paso distintos de 1 también pueden soportarse.

Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción

87

Puesto que el incremento de la vcc se hace automáticamente, no debería modificarse dentro del cuerpo de un ciclo f or, puesto que hacerlo así podría com­ prometer la prueba en el paso 2. En Pascal, la vcc está indefinida hasta la salida, de aquí que el programador no pueda confiar en la vcc teniendo algún valor particu­ lar sobre la terminación del ciclo.6 En el uso de for i := 1 to n do, ¿n puede ser cambiada dentro del cuerpo del ciclo? Esto podría ocasionar un problema si la prueba en el paso 2 compara a i con n cada vez antes de que se ejecute el ciclo. Es común que los lenguajes establezcan el límite de terminación una vez antes de la primera ejecución y que se hagan com­ paraciones para este valor fijo, más que con la variable n. Otro enfoque sería calcu­ lar y fijar el número de iteraciones antes de proceder a la ejecución del ciclo. El ciclo del listado (2.2.7) también puede realizarse mediante un enunciado que se repita hasta que se encuentre una condición de terminación, como se mues­ tra en el listado (2.2.8). sum := 0; i := 1; delta := 1; max := 20;

(2.2.8)

repeat sum := sum + i ; i :=

i

+ delta;

until i > max;

Sin embargo, puesto que la prueba ocurre al final, una construcción tal requiere que el cuerpo del ciclo se ejecute al menos una vez. Un ciclo whlle se prueba al principio del ciclo en vez de al final, como en el listado (2.2.9). sum := 0; i := 0; delta := 1; max := 20;

(2.2.9)

while i < max do begin i :=

i

+ delta;

sum := sum + i ;

end while;

Puesto que la prueba inicialmente puede ser falsa, permite cero iteraciones del ci­ clo, cuando esto es apropiado. Si cero iteraciones deben ser capaces de ocurrir en una sección de código, la construcción while deberá utilizarse en lugar de la cons­ trucción rep eat .. .until. Tal proceso sistemático funciona bien para los datos que están en alguna clase de orden lineal. Los ciclos for están limitados por lo común a tipos ordinales, de modo que podríamos tener: for ch := ' a ' to ’z 1 do for day := Mon to Fri do

(subrango carácter) {tipo enumerado)

La programación declarativa trata con el "qué son" de los datos más que con el "cómo hacerlo". Una interrogante declarativa típica sería: cuál(x: x vive en Michigan) 6 En Turbo Pascal, el último valor de la variable de control del ciclo se mantiene después de dejar el ciclo.

Sólo fines educativos - FreeLibros

88

PARTE I: Conceptos preliminares

El sistema haría la iteración a través de la base de datos en cuestión y respondería con todos los individuos que viven en Michigan. Cómo se realiza esta iteración se explorará en la parte IV. Recursión La iteración también puede describir el comportamiento de un procedimiento. En un procedimiento iterativo, los enunciados se ejecutan secuencialmente, aun cuan­ do el control puede ser transferido temporalmente a otro procedimiento o función. Para tales procedimientos uno entra a su entorno en la "parte superior" y sale en exactamente un sitio. En la recursión se pueden crear muchos entornos diferentes para un procedi­ miento o función. Esto se hace cuando un procedimiento/función contiene una llamada a sí mismo (o a otro procedimiento que eventualmente llama al original), creando de esta forma una invocación adicional de su entorno. Por ejemplo, suponga que a es un arreglo de entradas de enteros, luego considere la función de seudocódigo del listado (2.2.10), el cual agrega las primeras n entradas del arreglo: functlon sumArría: intArray; n: integer): integer;

(2.2.10)

(a es el nombre del arreglo, suma desde la entrada 1 hasta la n]

begin 1f n - 1 then sumArr := a t l l ;

else sumArr := sumArría,n-1) + a[n];

end 1f; end functlon;

Un entorno para sumArr incluirá tres nombres de variables: sumArr(para el valor de retorno), a y n. El uso de sumArr al lado derecho en la cláusula el se invoca la llama­ da recursiva a la función. La figura 2.2.1 traza la llamada para sumArr([3,2,6] ,3), donde [3,2, 6] es la notación para un arreglo de las tres entradas mostradas. Existen cuatro entornos en la ejecución, etiquetados de 0 a 3. El entorno 0 es el entorno de llamada, pero cada uno desde el 1 hasta el 3 tiene los mismos tres nom­ bres (sumArr, A y n), aunque sus ubicaciones son diferentes. Finalizamos con tres llamadas a sumArr, como se muestra por la secuencia de registros de activación en la figura 2.2.2 (página 90). Como segundo ejemplo, considere la función s umN del listado (2.2.11), que agrega los valores (1 + ... + n) + t, la suma de los primeros n enteros más algún valor t. functlon sumNín, t: integer): integer; (agrega los enteros 1 .. n al valor t)

begin 1f n « 1 then sumN := 1 + t;

Sólo fines educativos - FreeLibros

(2.2.11)

CAPÍTULO 2; Abstracción 0)

sumArr([3,2,6],3)

—>

?

—>

?

89

cali sumArr([3,2,6],3) = sumArr([3,2,6],2) + a [3]

1)

cali sumArr([3,2,6],2) ?

= sumArr([3,2,6] ,1) + a [2]

2)

cali sumArr([3,2,6] ,1)

3)

= a [1] {since

n

=

1}

= 3

—»

?

—>

3

—»

?

return

2)

= sumArr([3,2,6],1) + a [2]

?

= 3 + 2 =

5

5

return

1)

= sumArr([3,2,6],2) + a [3]

—>

?

= 5 + 6 = 11

—»

11

—>

?

—>

11

?

return

0)

= sumArr([3,2,6],3) =

11

F I G U R A 2.2.1

Evaluación de la función recursiva sumArr

else sumN := sumNCn-1, n+t); end 1f; end functlon;

La llamada recursiva toma ventaja del hecho de que (1 + ... + n) + 1 = (1 +... (n - 1 ) ) + (n + 1). Si deseamos agregar los enteros del 1 al 3, la llamada simplemente sería la expresión sumN (3,0). Al lector se le pedirá que evalúe esta llamada en el ejercicio 2.2.4 trazando los registros de activación, como se hizo en la figura 2.2.2. Otra vez tenemos tres llamadas a sumN. Sin embargo, en este caso, cuando lle­ gamos al entorno de nivel 3, la función toma ya el valor 6. Nosotros simplemente necesitamos pasar este valor de regreso a través de los entornos 2 y 1, hacia el entorno de llamada. El lector alerta podría preguntarse por qué ese valor de 6 tiene que pasarse todo el camino de regreso a la pila recursiva. Por supuesto, la respues­ ta es que no lo hace, de modo que podríamos simplemente salir allí. Una función cuyo valor llega a ser definido en la parte superior de la pila recursiva se conoce como cola recursiva. Como veremos en la parte IV, los compiladores o intérpretes para las versiones más novedosas de LISP, incluyendo SCHEME y COMMON LISP, han sido optimizados para terminar funciones de cola recursiva en la parte supe­ rior en lugar de en la parte inferior de la pila. Excepciones Una excepción ocurre cuando la ejecución del programa se interrumpe debido a que se presenta algún evento inusual. Si un programa se encuentra ejecutándose en Sólo fines educativos - FreeLibros

90

PARTE I: Conceptos preliminares

sumArr

dyn sumArr 3

3

a [3,2,6] n

sumArr

dyn

sumArr

sumArr ? 2

dyn

sumArr

n

I

sumArr

sumArr ?

sumArr ?

sumArr ?

1 a [3,2,6]

3

n

3

llamada sumArr ([3,2,6],2)

dyn sumArr 5 a [3,2,6] n

2

dyn

sumArr

dyn

nil sumArr ? 1

dyn nil

llamada sumArr ([3,2,6],3)

sumArr

2

nil

n

2

a [3,2,6]

nil

a [3,2,6]

sumArr

2

2

dyn

dyn sumArr ?

a [3,2,6] n

1

a [3,2,6] n

3

Salir y calcular sumArr + A[2] =>3 + 2 =>5

nil sumArr 11 1 a [3,2,6] n

3

Salir y calcular sumArr + A[3] -> 5 + 6 =>11

F I G U R A 2.2.2

Registros de activación para una llamada recursiva

Sólo fines educativos - FreeLibros

1

a [3,2,6] n

3

llamada sumArr ([3,2,6], 1) => 3 {puesto que n=1}

CAPÍTULO 2: Abstracción

91

tiempo real, es particularmente importante que tales eventos se manejen de mane­ ra apropiada. Nuestros astronautas no estarían muy felices de ver "ERROR 12, SUBÍNDICE DEL ARREGLO FUERA DE INTERVALO, PROGRAMA ABORTA­ DO" parpadeando en sus monitores a mitad de camino a Marte. Un programa bancario podría incluir una rutina especial si ion cliente intentara depositar una cantidad inusualmente grande, fuera del intervalo declarado de la variable de en­ trada. Se alcanza una excepción cuando ocurre un evento fuera de lo común, y se trans­ fiere el control a un manejador de excepción. Como ejemplos, la excepción podría surgir por hechos como la división entre cero, una sobrecarga aritmética, una va­ riable fuera de intervalo, espacio insuficiente para la pila o errores en los datos de entrada (tal como 2t, cuando lo que se espera es un valor entero). Un lenguaje pue­ de soportar también excepciones definidas por el usuario. La ubicación del manejador de excepción es otra cuestión importante del len­ guaje. El código para el manejador puede ser parte del bloque en el que ocurre la excepción, o podría estar situado en una estructura como un procedimiento. En cualquier caso, deben especificarse las reglas de ámbito para el manejador. Después de completar la ejecución del manejador, ¿a qué punto en el programa regresa la ejecución? Esto se conoce como la continuación de la excepción. En el modelo de reanudación, el control regresa al punto de ocurrencia. En este caso, debe­ mos conocer si una expresión, enunciado o bloque está por ser reevaluado o si la ejecución continúa después de la ubicación de la excepción. En el modelo de termina­ ción, la ejecución del bloque en el que se presenta la excepción es terminada. Las excepciones no manejadas en un bloque pueden ser propagadas dinámicamente al bloque de llamada mediante el paso de la información a su registro de activación. Si el manejador es local a un bloque, entonces se requieren manejadores para cada bloque. Puesto que una excepción puede necesitar ser tratada en forma diferente, dependiendo de dónde ocurra, esto puede ser preferible a tener un manejador que intente tratar con todas las ocurrencias. Los diseñadores de PL/I fueron pioneros en la administración ordenada de interrupciones inesperadas de programas con condiciones ON. El programador puede invalidar cualquier acción normal que fuera tomada por un sistema operativo, es­ cribiendo: On

ON-unit

Por ejemplo, considere el listado (2.2.12). ON ZERODIVIDE X := -999;

(2.2.12)

ON ENDFILE(SYSIN) BEGIN PUT PAGE LIST( 1END OF LISTING1); MOREDATA = 'NO1; END;

El primer elemento asignaría a X el valor -999 en cualquier momento que se haga un intento por dividir entre cero. En el segundo, imprime un mensaje y la bandera (flag) MOREDATA se establece cuando la entrada está al final del archivo. Uno mismo también puede alcanzar una excepción; por ejemplo: Sólo fines educativos - FreeLibros

92

PARTE i: Conceptos preliminares IF DELTA < 0.001 THEN SIGNAL ZERODIVIDE;

Aquí la rutina ZERODIVI DE sería invocada siempre que la variable DELTA llegue a ser menor que 0.001, y entonces se le asignaría a X el valor - 999. El PL/I sigue el modelo de reanudación, aunque lo que pasa después de que ocurre una excepción es tratado de manera algo inconsistente. En particular, ¿cuál X llega a ser -999 después de un intento de dividir entre cero? Los programadores de PL/I también pueden inhabilitar las excepciones, de modo que la ejecución del programa continúe. Dependiendo de la excepción, a continuación se generarían únicamente disparates. En Ada, el manejador de excepción es parte de una especificación de bloque, y los usuarios pueden definir sus propias excepciones, como en el fragmento de pro­ grama del listado (2.2.13). Invalid: exceptlon;

(2.2.13)

begln 1f Data < 0 then ralse Invalid; end 1f; exceptlon when Constraint_Error Put (“Error - datos trien Invalid -> Put (“Error - valor when others => Put (“Ocurrió algún end;

“> fuera de rango” ); negativo usado"); otro error");

Aquí Inval i d es una excepción definida por el usuario que se alcanza en el enun­ ciado 1f mostrado. Ada sigue el modelo de terminación, de modo que sale del bloque una vez habiendo terminado el manejador. Si no se proporciona manejador de excepción, ésta es propagada dinámicamente hacia el bloque de llamada. Para una excepción definida por el usuario, debería declararse en un bloque más grande para asegurar que pueda ser propagada.

E J E R C I C I O S 2.2

1. Un enunciado case puede estar restringido a discriminantes de tipo ordinal. Si un lenguaje soporta la construcción 1f . . . e 1se 1f . . . e 1se . . . end 1f ; para selección de múltiples vías, ¿por qué soportar ambas construcciones? 2. Algunos autores defienden la eliminación de enunciados como repeat del listado (2.2.8), que efectúa la iteración al menos una vez, en favor del w hlle que hace la prueba antes de entrar al ciclo de iteración. ¿Cuál es su opinión? En particular, ¿qué pasa si existe una condición de prueba, tal como alcanzar el final de un archivo de entrada, o que los elementos de datos se encuentren en un cierto intervalo? 3. Rastree los registros de activación como en la figura 2.2.2 para la evaluación de factor i al (4) para la función en seudocódigo del listado (2.2.14). Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción functlon factorial(n: integer): integer;

93

(2.2.14)

begln if n ~ 1 then factorial

:= 1;

el se factorial

:= n * factorial (n

- 1);

end 1f; end functlon;

4. Rastree los registros de activación como en la figura 2.2.2 para la llamada de función recursiva de cola sumN(3,0) del listado (2.2.11). Observe la diferencia entre su ejecu­ ción y la de sumArr correspondiente al listado (2.2.10). 5. Si puede ocurrir una excepción (tal como la división entre cero) en más de un lugar en un programa, ¿pueden necesitar manejarse de forma diferente o puede un manejador global tratar con todas las ocurrencias?

2.3 ABSTRACCIÓN DE PROCEDIMIENTOS En la sección 2.1 acerca de tipos de datos abstractos, encontramos que un ADT contiene tanto un tipo de datos como sus operaciones asociadas. En esta sección, comenzaremos examinando una operación o proceso por realizarse. En términos de Pamas [Pamas, 1972], observamos desde el pimío de vista de un módulo de subprograma como una "asignación de responsabilidad". Un subprograma de esta clase tendría su propio nombre y podría contener declaraciones, procedimientos y funciones. Un lenguaje incluso puede soportar compilación separada de algún tipo de subprogramas. Un programa generalmente tendrá las siguientes secciones: 1. 2. 3.

Datos de entrada Datos de procesamiento Resultados de salida

El programa podría descomponerse en tres partes, cada una responsable de una de las tres actividades en particular. Ésta es una abstracción de procedimientos puesto que no nos importa la forma en que cada parte vaya a realizarse, sólo la manera como se comunican entre sí. Estas tres partes podrían ser procedimientos, pero también podrían ser algo más. Un módulo de subprograma podría incluir tipos de datos abstractos así como otras funciones y procedimientos. Podemos pensar en un módulo de subprograma de esta clase como una "caja negra". Entradas conoci­ das se introducen a la caja, y se extraen resultados verificables. No obstante, los detalles de lo que ocurre en el interior de la caja permanecen ocultos. Pamas establece los beneficios de la programación modular como: 1.

Administrativo: El tiempo de desarrollo debería acortarse debido a que grupos separados trabajarían en cada módulo con poca necesidad de comunicación. Sólo fines educativos - FreeLibros

94 2. 3.

PARTE i: Conceptos preliminares

Flexibilidad de producto: Sería posible hacer cambios drásticos a un módulo sin necesidad de cambiar otros. Legibilidad: Sería posible estudiar el sistema un módulo a la vez. Por consi­ guiente, el sistema entero puede estar mejor diseñado debido a que se com­ prende mejor.

Una abstracción de procedimientos para simplificar un programa se consigue mediante la especificación de un proceso o función por realizarse. Por ejemplo, un editor puede hacer uso de un programa extenso para transformar en libro un texto suministrado por un autor. Un módulo de subprograma podría recibir el texto en cierta etapa del proceso y producir un índice. Aquí la función podría ser indexModule(textFÍles) => index

Nosotros debemos, por supuesto, especificar cuidadosamente los requerimientos sobre los t e x t F i 1 es, y también describir cuál será la salida. Aunque los usuarios no necesitan preocuparse por lo que ocurre dentro de i ndexModul e, la forma de t e x t F i 1es debe estar bien y completamente especificada, de modo que un usuario posible­ mente novato pueda preparar t e x t F i 1es para que i ndexModul e trabaje de manera apropiada. La variable Index puede no ser el producto final. Puede haber otros módulos, tales como: moduleAssemble(textFiles, index) -> galleys

El t e x t F i l e s aquí puede estar sujeto a diferentes requerimientos que cuando se utilizó como entrada al i ndexModul e. De manera que, ¿por qué no emplear un nom­ bre diferente, tal como i ndexedTextFi l e s , para hacer más clara la distinción? Esto puede ser una buena idea, pero ciertamente no es necesaria, El punto clave es que la descripción de t e x t F i 1es se encuentra en la interfaz entre cualquiera de los mó­ dulos de que proviene y hacia los que va. En una interfaz diferente, la descripción puede ser completamente distinta. Nos sentimos bastante cómodos con esta no­ ción cuando consideramos los procedimientos. Por ejemplo, f i ndThi r d L e t t e r ( x ) ciertamente esperaría una entrada x diferente que squareRoot(x). Si modificamos e l t e x t F i l e s mientras se construye el índice, nuestra función i ndexModul e produciría un par de salidas, en lugar de una sola; es decir, indexModu1e2(textFiles) => (newTextFiles,index)

En un sistema ideal, la modularización podría ser completamente ortogonal (es decir, independiente; véase la sección 0.4), sin restricciones sobre cualquier entrada o salida.

Procedimientos Antes de examinar más los agregados o colecciones de declaraciones y/o procedi­ mientos y funciones, como necesitaríamos hacer para los tipos de datos abstractos, Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción

95

consideraremos los procedimientos mismos. Ciertamente la definición de Pamas acerca de un módulo de subprograma como una asignación de responsabilidad incluirá los procedimientos. Abelson y los Sussmans definen un procedimiento como "un patrón para la evolución local de un proceso computacional" [Abelson, 1985]. Por local, quieren decir que un procedimiento lleva a cabo su asignación de responsabilidad en un entorno separado del resto del programa; y que un procedimiento es un patrón, el cual permite que su trabajo sea realizado sobre diversos objetos reales de maneras similares, dependiendo de los objetos presentes. Un procedim iento es una abstracción en dos sentidos. Prim ero, por parametrización, donde hacemos abstracción de la identidad de varios ejemplos de datos. Aquí los valores reales de los datos no son importantes; nuestro interés se centra en el número y tipos de los elementos de datos. El segundo sentido es la abstracción por especificación. Nosotros especificamos el comportamiento de un procedimiento solamente por cuáles resultados puede esperar el usuario. Es irrele­ vante la forma en que estos resultados son conseguidos. Ésta es la caja negra des­ crita anteriormente, donde los detalles del "cómo" están ocultos para el usuario. Estas dos abstracciones trabajando juntas permiten que los procedimientos estén separados del resto de un programa (mejorando la comprensión y su corrección) y sean modificados individualmente, sin cambiar las partes de un sistema que los llame. Funciones y operadores Las funciones son procedimientos de un tipo especial que devuelve un valor (o, en algunos lenguajes, múltiples valores). La especificación debe indicar el tipo del valor que se devolverá. Un lenguaje puede poner algunos límites a este tipo de valor de retomo. En Pascal, por ejemplo, el resultado debe ser de tipo ordinal, real o apuntador. Los tipos agregados como los arreglos y los registros no están permi­ tidos. Esto puede restringir severamente la flexibilidad del uso de las funciones. Mientras que una función parece ser similar en su notación a un procedimien­ to, una función es un bloque que representa una abstracción de una expresión. Como tales, las funciones pueden utilizarse en código como expresiones, como en el listado (2.3.1). z

f(x) + f(y);

(2.3.1)

if empty(stack) then ... print (f(x), z);

Aquí son empleadas como operandos de operadores aritméticos, en una expresión condicional que devuelve un resultado booleano, y como parámetros de otras fun­ ciones y procedimientos, entre otros. El listado (2.2.10) demostró una forma de especificar el valor que se devuelve en un estilo tipo Pascal: el uso del nombre de la función en el lado izquierdo de un enunciado de asignación, tal como: sumArr := a [1];

Sólo fines educativos - FreeLibros

96

PARTE i: Conceptos preliminares

En los registros de activación asociados de la figura 2.2.2, vemos que se proporcio­ na almacenamiento para el valor de regreso sumArr. Otro enfoque común para la sintaxis de un valor devuelto es por medio de un enunciado de retorno, como en Ada. El ejemplo anterior se escribiría: retufn (a[l]);

Es importante observar que, a fin de ser coherente con la noción matemática de las funciones, el iónico efecto sería la producción del resultado. No habría ningunos otros efectos colaterales; es decir, cambios ya sea en sus parámetros de llamada o en otras variables de un ámbito cerrado. Podríamos evitar funciones enteramente mediante el uso de procedimientos que devuelvan un valor a través de un parámetro. Sin embargo, esto hace la noción matemática usual de composición de funciones difícil de expresar. Los lenguajes funcionales, como Puré LISP, evitan por completo los procedimientos, trabajando solamente con funciones. De manera semejante, C y C++ emplean sólo funciones, mientras que un procedimiento es esencialmente una función que devuelve el tipo especial vold. Para nuestros propósitos actuales, utilizaremos la palabra "procedi­ miento" para incluir tanto los procedimientos como las funciones. Algunos lenguajes también soportan operadores definidos por el usuario. Con­ sidere, por ejemplo, la definición de seudocódigo del listado (2.3.2). operator max(a, b: integer): integer; begin if a >= b then max := a; else max := b; end if; end operator;

(2.3.2)

La definición es muy cercana a la de una función, pero el uso notacional en un programa puede ser diferente. Puesto que max tiene dos operandos, es un operador binario y puede ser usado con notación infija, en la cual el operador aparece entre los dos operandos. De aquí que se use como m x max y; en lugar de m : = m ax ( x, y) ;. Si x = 3 y y = 5, entonces m contendrá el resultado de 3 max 5 = 5. Un operador unitario tendría un operando. Suponiendo que a es de un tipo arreglo, podríamos tener m := max a;, donde la mayor de las entradas del arreglo se coloca en m. Aquí max se emplea en notación prefija. C y C++ también tienen algunos operadores de tipo posfija, usados como i++ e i - por ejemplo. Cuando se define un operador, su precedencia de operador debe ser clara. Por ejemplo, en m := x max y + 2;, ¿se aplica primero max o +? El lenguaje puede proporcionar la sintaxis que permita establecer la precedencia. Algunos lenguajes, tales como Ada, limitan las definiciones de operador para permitir al programador volver a definir operadores existentes para diferentes ti­ pos de operando. En este caso, el operador se define simplemente como una fun­ ción. Si consideramos un número complejo c como un par [a,b] de números reales (que representen la expresión a + bi), podríamos definir en Ada, function M+ 11(C 1» C2: Complex) return Complex is

Sólo fines educativos - FreeLibros

capítulo

2: Abstracción

97

Esta sobrecarga de operador (véase la sección 1.3) puede ser particularmente útil cuando se definen ADT, puesto que la notación común de los operadores existen­ tes puede ser definida para nuevos tipos de datos. En este caso, la precedencia de operador es la misma que la del operador predefinido.

P arám etros Los parámetros están asociados con los procedimientos, y especifican la forma o patrón de objetos de datos con los cuales trabajarán. Por ejemplo, squareRoot(x:

in real; y: out real);

tiene dos parámetros form ales en números reales, x e y . Los modificadores de seudocódigo in y out siguen la sintaxis de Ada. A un parámetro 1n debe suministrársele un valor en el momento en que ocurra una llamada de procedi­ miento, considerando que el procedimiento mismo proporcionará un valor para un parámetro out. Un valor puede ser tanto recibido como devuelto a través de un parámetro 1n out (de nuevo utilizando la sintaxis de Ada). Cuando el módulo de llamada llama al procedimiento squar eRoot (2, r e s u l t ) , 2 y r e s u l t tomarán el lugar de x e y, y son denominados parámetros reales, El proce­ dimiento squareRoot obtiene el valor 2 de x y pone su resultado en el contenedor de datos nombrado r e s u 11. A fin de devolver un valor a través de un parámetro o u t o 1n out, debe ser posible almacenar el resultado en el parámetro real. Esto significa generalmente que el parámetro real correspondiente debe ser una variable, una entrada de arreglo, etc., de tipo compatible, no un valor literal. Cuando se llama un procedimiento, el control se transfiere al entorno del pro­ cedimiento, el cual puede o no tener partes en común con el entorno de llamada. Si se desea comunicación entre el que llama y el que es llamado, deben hacerse arre­ glos para pasar los valores de ida y vuelta a través de los parámetros del procedi­ miento. Como se analizó en la sección 1.2, las variables que no están ligadas localmente deben ser declaradas en algún otro entorno y ser visibles mediante las reglas de ámbito aplicables. Los cambios en estas variables no locales, o efectos colaterales, por lo general no se recomiendan, porque ellos pueden ocultar la comunicación entre el que llama y el que es llamado, lo cual puede hacerse adecuadamente a través de parámetros. P arám etros p o r v alor. Un parámetro por valor es aquel en el cual el valor del pará­ metro real se copia en la ubicación identificada con el nombre del parámetro for­ mal correspondiente. En muchos lenguajes, éste es el modo de paso de parámetros predeterminado, el modo que se usa si ninguno se da de manera explícita. Los parámetros por valor proporcionan un modelo para parámetros 1n, puesto que vienen dentro de un procedimiento, pero no proporcionan nueva información de salida. Estos parámetros por valor con frecuencia están estrechamente asociados con las funciones, en las cuales solamente un valor se calcula y devuelve, permane­ ciendo todos los otros parámetros sin cambio en el ambiente o entorno de llamada.

Sólo fines educativos - FreeLibros

98

PARTE 1: Conceptos preliminares

Una desventaja es que, si el parámetro es de un tipo agregado grande, debe hacerse suficiente espacio para la copia pasada al parámetro formal. El tiempo ne­ cesario para la transferencia también puede ser costoso. P arám etros p o r referencia. Un parámetro por referencia se comporta de modo algo parecido a una variable global, en que cualquier cambio a un parámetro formal resulta también en cambios para el parámetro real correspondiente. Esto propor­ ciona un modelo para un parámetro 1n out. Lo anterior se realiza al pasar al pro­ cedimiento la dirección del parámetro real, en lugar de su valor. Una dirección de esta clase se conoce como referencia a una variable, de aquí el término parámetro por referencia. Para los parámetros del tipo agregado, los parámetros por referencia pueden ser más eficientes que los parámetros por valor. Puesto que no se copia el agregado completo, sólo su dirección, existen ahorros tanto en almacenamiento como en tiem­ po. Si se utiliza un parámetro por referencia en lugar de un parámetro por valor a fin de conseguir estos ahorros, y solamente el paso 1n está destinado, debe tenerse cuidado de que no ocurran cambios inadvertidos al parámetro real. Puesto que la dirección del parámetro real se pasa al parámetro formal, puede ocurrir sobrenombramiento o "alias" (aliasing): podemos tener más de un nombre para la misma ubicación. Esto puede hacer el programa más difícil de entender. Considere el procedimiento, procedure p(x: in out integer);

en el cual x está implementada como un parámetro por referencia. Si existe una llamada p( a ), y si a es visible dentro de p, entonces a y x son sobrenombres. Si bien esto puede no parecer un problema, suponga que extendemos la decla­ ración a: procedure p(x, y: in out integer);

Una llamada a p ( a , a ) asocia tanto a x como a y a la misma dirección, de aquí que x e y lleguen a ser sobrenombres, y el efecto del procedimiento puede oscurecerse. En la década de los cincuenta, FORTRAN era el único lenguaje de alto nivel que estaba ampliamente disponible. Su único modo de paso de parámetros era por referencia. Así, cualquier parámetro podía ser pasado 1n out. Un procedimiento AODONETO( X) podría dar como resultado el valor de X incrementado en 1. Sin em­ bargo, contrario a las intenciones de la mayoría de los programadores, ADD0NET0C2) resultaría en la constante 2 incrementada a 3, dependiendo de la implementación. Una referencia a la ubicación de una constante no necesitaba ser diferente a una referencia a la ubicación asignada a una variable. Esto no podía pasar si el 2 era pasado por valor, puesto que 2 sería copiado en el parámetro formal deADDONETO. P arám etros p o r resu ltado. Un parámetro por resultado es aquel que no recibe un valor hasta entrar a su procedimiento, pero se le asigna un valor durante la ejecu­ ción del proceso, que es disponible subsecuentemente para el módulo de llamada. Esto proporciona un modelo para los parámetros out, lo cual requiere generalmen­

Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción

99

te almacenamiento local para el parámetro, y los parámetros de resultado se copian de regreso al parámetro real a la salida. De este modo tenemos las mismas desven­ tajas de transferencia y almacenamiento que para los parámetros por valor. La dirección para el valor de retorno puede establecerse ya sea en el momento de la llamada o justo antes de regresar del procedimiento. Desgraciadamente, pue­ den surgir diferentes respuestas. Considere la llamada p ( a [ i ] ) . Supóngase que i cambia de 1 a 2 dentro del cuerpo del procedimiento. El momento de la fijación de dirección determina si el resultado en el parámetro formal se obtiene de regreso hacia a [13 o hacia a [23. P arám etros p o r resultado-valor,: Un parámetro por resultado-valor se comporta como un parámetro por valor hasta que el control regresa al entorno de llamada. Como parte de esta transferencia de control, el nuevo valor o resultado, calculado para ese parámetro en el entorno del procedimiento, se copia de regreso al parámetro real. Esto proporciona otro modelo para los parámetros 1n out. Como ocurre con los parámetros por resultado, el tiempo de fijación de direc­ ción para el resultado de retomo es importante. Aho y cois. [Aho, 1986] asume la convención de fijar la dirección de regreso al momento de la llamada, de modo que el valor recibido y regresado se refiere a la misma ubicación. Sin embargo, incluso bajo esta suposición pueden presentarse diferentes resul­ tados entre las implementaciones de resultados por valor y referencia de paráme­ tros 1n out. Considere el ejemplo en seudocódigo del listado (2.3.3). program inoutparms; var a: integer; procedure p(x: in out integer); begln

(2.3.3)

x := 5; a

2;

end procedure; begin a := 1; p(a); print(a);

end program;

Como un parámetro por referencia, x y a se refieren a la misma dirección, de aquí que el valor 2 se imprima. Para resultado-valor, x se cambia a 5 dentro del procedi­ miento, y este valor se devuelve al parámetro real una vez completado, de aquí que se imprima 5. El estándar Ada 83 [ANSI-1815A, 1983] especifica que los parámetros escalares 1n out están por implementarse como resultado-valor, pero estos tipos compues­ tos pueden implementarse mediante el constructor del compilador ya sea como referencia o como resultado-valor. No obstante, un programa debe producir el mis­ mo resultado para ser considerado válido. P arám etros p o r nombre. Cuando se utiliza un parámetro por nombre, se pasa el nombre del parámetro real, más que una dirección o copia. Por ello, paso por nom­ bre significa que el nombre de un parámetro real es sustituido textualmente por el

Sólo fines educativos - FreeLibros

100

PARTE i: Conceptos preliminares

parámetro formal en el cuerpo (entre el begln y el end) del procedimiento al cual se pasa. Considere el ejemplo de seudocódigo del listado (2.3.4). procedure increment(name x: real; in d: real); begln

(2.3.4)

x := x + d;

end procedure;

Una llamada de incre mentía, .01); daría como resultado: procedure increment(name x: real; in d: real); begln a ;= a + d;

end procedure;

y se ejecutaría a a + .01. El paso por nombre es poderoso, porque las funciones y procedimientos pue­ den pasarse así como también variables estructuradas y simples. El ejemplo usual que demuestra este poder es el del listado (2.3.5) siguiente. functlon SIGMA(name i: integer; in 1, u: integer; ñame x: real): real; var s: real; begin s:= 0; for i 1 to u do

(2.3.5)

s := s + x;

end for; SIGMA := s; end function;

Una llamada a SIGMA( i , 1, m, SIGMA( j , 1, n, a [ i , j ] ) ) calcula: m

n a[i,j]

i=l j=l Ésta es una facilidad afrontada por pocos lenguajes, pero implementada en ALGOL 60.

Sin embargo, el paso por nombre puede rendir algunos resultados inespera­ dos. Se le solicitará a usted explorar algunos de estos peligros en el ejercicio 2.3.7. P rocedim ientos com o parám etros. Algunos lenguajes permiten el paso de proce­ dimientos o funciones como parámetros. En este caso el parámetro real es el nom­ bre de un procedimiento, mientras que el parámetro formal indica que es un procedimiento y especifica sus tipos de parámetro. program procparam; var a, b: integer; procedure p ( x : integer; procedure r(z: integer)); var b: integer; begin

Sólo fines educativos - FreeLibros

(2.3.6)

CAPÍTULO 2: Abstracción

101

r(x);

end procedure; procedure s(y: integer); begin end procedure; begin •

a := 0 ; b := 1; p(a.s); • • •»

end program;

En el ejemplo del listado (2.3.6), la declaración de p indica que el parámetro de procedimiento r tendría un solo parámetro entero. Esto permite alguna verifica­ ción de tipo estático dentro de p. En la llamada p ( a , s ), pasamos el parámetro s del procedimiento, pero no sus parámetros reales, puesto que todavía no son conoci­ dos. Sin embargo, el compilador puede comparar estáticamente la lista de paráme­ tros de s para la del parámetro r del procedimiento real. Un punto adicional de consideración es el tratamiento de las variables no loca­ les. Suponga que el cuerpo de s incluía una referencia a una variable b, que es no local a s . Tiene sentido tratar la llamada r ( x ) como si s ( x ) apareciera en su lugar. En el ámbito estático, entonces, b (dentro del cuerpo de s) haría referencia a la declaración en el programa principal. Con el fin de llevarlo a cabo, la llamada p( a #s ) enviaría un par (CP, EP), el apuntador de código para el procedimiento, y un apun­ tador de entorno para su registro de activación, el cual determina la referencia ade­ cuada. Los lenguajes orientados a objetos también permiten el paso de procedimien­ tos que son miembros de objetos. Pospondremos este análisis para el capítulo 4. L A B O R A T O R I O 2 . 2 : M É T O D O S DE P A S O DE P A R Á M E T R O S : P A S C A L

Objetivos (Los laboratorios pueden encontrarse en el Instnictor's Manual.) 1. Investigar los mecanismos de paso de parámetros, particularmente como se implementan en los compiladores. 2. Investigar los problemas que surgen de las variables globales y diversas técnicas de paso de parámetros.

Módulos y ADT El término modularización se emplea para describir varias nociones diferentes. Como mencionamos anteriormente, un módulo de subprograma puede considerarse como una "asignación de responsabilidad" que realiza una función particular. El térmi­ no módulo ha llegado a significar más que esto. Recordando la sección 2.1, a fin de proporcionar tipos de datos abstractos, son necesarias unidades de programa que soporten los tipos de datos y las operaciones en ellas. Bajo este enfoque, considera­ remos un módulo como una unidad nombrada de programa la cual soporta: Sólo fines educativos - FreeLibros

102 1. 2. 3.

PARTE I: Conceptos preliminares

Encapsulamiento Independencia de datos Ocultamiento de información

El encapsulamiento de datos es el agrupamiento de operaciones y tipos de da­ tos dentro de la misma unidad de programa. Puesto que la especificación de mó­ dulo no especifica la representación, proporciona independencia de datos. Y, puesto que a los usuarios se les puede dar acceso solamente a lo que necesitan conocer, se soporta el ocultamiento de la información. Los lenguajes que soportan la modularización proporcionan dos clases de módulos: módulos de definición, los que describen formalmente las interfaces para el módulo, y los módulos de implementación, los cuales pueden estar ocultos al usuario e implementan la definición fielmente. Vimos un ejemplo de esto en nuestro ejem­ plo de ADT del listado (2.1.7), el cual incluye tanto una especificación como una implementación para una ItemQueue. Una noción modular importante es el alcance del ocultamiento de informa­ ción realizado. ¿Precisamente cuáles variables, constantes, tipos, procedimientos y funciones son accesibles dentro y fuera de un módulo en particular? Aquellos que se enumeran para ser visibles fuera del módulo en el cual están definidos se dice que son exportados desde un módulo, y aquellos por usarse, pero defini­ dos e implementados en otros módulos, son importados dentro de un módulo. El uso de tales listas de importación y exportación proporciona un medio para ha­ cer accesibles al usuario solamente aquellos tipos y procedimientos que definen el ADT. Los diferentes lenguajes han dado nombres distintos a sus módulos, y la no­ ción de módulo difiere entre ellos. El diseñador de Pascal, Niklaus Wirth, promo­ vió el concepto de módulo, del cual se derivó el nombre del lenguaje Modula (y Modula-2). El Turbo Pascal de Borland proporciona una mejora a Pascal denomi­ nada una Unidad (Unit), la cual se ofrece para módulos separados. Ada se basa en Modula en la definición de sus módulos, llamados paquetes, los cuales se describi­ rán en el capítulo 3. Otra noción útil de los módulos es el uso de secciones independientes de pro­ gramas, siendo cada módulo independiente de todos los demás. Tal independen­ cia ayuda en la comprobación de que los programas son correctos. Si cada módulo hace lo que se supone que debe hacer, y las interfaces de los módulos son correctas, un programa debería producir el resultado deseado, dada la entrada apropiada. Como los programas y sistemas han llegado a ser más complejos, la modularización se ha convertido en una necesidad para la comprensión de un diseño de sistema, consiguiendo completar un programa extenso en una cantidad de tiempo razona­ ble, y demostrando que funciona de manera adecuada. Una ventaja de la modularización es que partes de programa autocontenidas pueden ser probadas de manera independiente. Equipos de programación separa­ dos pueden escribir módulos, compilarlos y depurarlos, sin comunicarse con el resto del equipo del proyecto. Esto, por supuesto, requiere de un criterio de diseño muy específico de manera que todos los elementos encajarán perfectamente cuan­ do llegue el momento de armar el programa completo.

Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción

103

Clases de ADT Al analizar los módulos anteriores, vimos que la noción de ADT era factible debido a la capacidad de formar colecciones de tipos de datos y procedimientos relaciona­ dos. Sin embargo, nuestro ejemplo de ItemQueue del listado (2.1.7) dependía de la especificación del tipo de i tem. Las clases pueden representar colecciones de ADT, puesto que proporcionan plantillas para los ADT, como se mencionó en la sección 2.1 bajo "Tipos genéricos". Por ejemplo, real Queue = new Queue ( R e a l ) e i ntQueue - new Queue ( I n t e g e r ) pueden ser dos muestras de un ADT para una clase de ItemQueues. Estos ejem­ plos pueden ser dinámicos, es decir, construidos y destruidos durante el tiempo de ejecución. Ejemplos y detalles adicionales se presentarán en el análisis de lenguajes basados en objetos del capítulo 4. Objetos Wegner describe un objeto como un grupo de procedimientos que comparten un estado [Wegner, 1988]. Considere otra vez nuestro ADT para un ItemQueue del listado (2.1.7). Si q es del tipo queue, entonces add(q,5) da como resultado un cambio de estado para el objeto q. Si consideramos que q está definido y puede cambiar sólo a través de las operaciones definidas, entonces podemos visualizar el objeto realmente como el par [objeto, operaciones]. Un lenguaje puede considerarse orientado a objetos si soporta: • • • •

Abstracción de datos Ocultamiento de información Polimorfismo Herencia

La abstracción de datos se refiere a la habilidad para encapsular tanto el tipo de datos como las operaciones por realizarse, proporcionando así ocultamiento de la información. Por consiguiente, el encapsulamiento mediante un objeto incluye la privacidad de datos para un objeto, compartir datos con otros objetos, datos globales compartidos por todos los objetos y un conjunto de mensajes, o protocolo, al cual un objeto responde. Polimorfismo, que quiere decir "muchas formas", se refiere a la capacidad de que diferentes objetos respondan al mismo mensaje de manera distinta. Por ejem­ plo, mientras que 'A' y 3 son objetos diferentes, podemos aplicar el mensaje suce­ sor a cada uno. Entonces sucesor ( 'A') y sucesor (3) darán respuestas diferentes, ‘ B* y 4, cada una apropiada al objeto. Las operaciones definidas para ion objeto se conocen como métodos. Cuando un objeto recibe un mensaje, el método asociado se selecciona y aplica. El estado de un objeto se mantendrá entre invocaciones de métodos. Podemos considerar un mensaje como el nombre de un método. Si bien los métodos suenan mucho a procedimientos, existen diferencias. A fin de soportar el polimorfismo, necesitamos ser capaces de enviar el mismo mensaje a diferentes objetos. Los procedimientos están definidos generalmente por el núme­ Sólo fines educativos - FreeLibros

104

PARTE I: Conceptos preliminares

ro y el tipo de sus parámetros. En nuestro ejemplo anterior, el mensaje .sucesor debe estar definido tanto para objetos carácter como para objetos enteros. Para pro­ veer esto, un lenguaje tendría que soportar alguna sobrecarga de nombres de mé­ todo, proporcionando definiciones de un nombre de procedimiento para diferentes tipos de parámetro. Los objetos pueden organizarse dentro de una jerarquía de clases. Un lenguaje soporta la herencia si los subobjetos heredan los atributos de un objeto padre. Los detalles adicionales se dejarán para el capítulo 4.

Ejecución concurrente Si los módulos son independientes entre sí, pueden ejecutarse de manera concu­ rrente si se tienen disponibles múltiples procesadores. La concurrencia demanda sincronización en el tiempo así como la especificación de una interfaz de datos. Un módulo puede tener que esperar a que otro se complete antes de proceder. Una complicación adicional surge cuando los módulos no son completamente independientes, pero comparten datos. Si usted trabaja en una red, habrá experi­ mentado retardos cuando utiliza el mismo software que otros usuarios. Las redes pueden proporcionar una copia de un compilador o editor particular dentro del espacio de trabajo individual de un usuario, en cuyo caso no se presenta comparti­ miento. Otros sistemas mantienen solamente una copia de dicho software en el ser­ vidor de archivos y los usuarios tienen acceso al mismo mediante alguna clase de método de compartimiento de tiempo. Aquí el usuario probablemente no esté cam­ biando los datos compartidos, los cuales pueden ser un compilador, editores, u otra utilidad, sino solamente esté utilizándolos, de modo que no se aplican muchos pro­ blemas de sincronización. Analizaremos la ejecución concurrente en el capítulo 5. E J E R C I C I O S 2.3 1. Suponga que un lenguaje proporciona sólo procedimientos y no funciones. ¿Cómo podría usted poner en práctica un procedimiento para calcular la longitud de la hipotenusa de un triángulo si los procedimientos s q u a r e ( x . y ) y s q u a r e r o o t ( x , y ) fueran suministrados? ¿Qué métodos de paso de parámetros deberían utilizarse para x y para y? 2. ¿Por qué el lenguaje Pascal proporciona procedimientos además de funciones? 3. Cree un operador unitario max, haciendo uso de notación de seudocódigo como en el listado (2.3.2), cuyo operando sea un arreglo de 10 entradas de enteros. El resultado debería ser el mayor valor de esas 10 entradas. 4. a. ¿Por qué un arreglo pasado por resultado-valor requeriría más memoria que el mismo arreglo pasado por referencia? b. En programación en tiempo real, ¿qué es más deseable, parámetros por resulta­ do-valor o por referencia? ¿Puede usted pensar en situaciones donde su respues­ ta podría diferir? 5. Si un parámetro por referencia se comporta de alguna manera como una variable global, ¿qué ventajas tendría pasar por referencia en lugar de utilizar variables globales? 6. Considere el procedimiento del listado (2.3.7).

Sólo fines educativos - FreeLibros

CAPÍTULO 2: Abstracción procedure p(in out x, y: integer); begin

105 (2.3.7)

x := 5; y := 2; end procedure;

Suponga que los parámetros se pasan por resultado-valor. Una llamada de p{a ,a) puede ocasionar resultados ambiguos, de allí que se conozca como una colisión. ¿Cuál es el problema aquí? 7. Considere el procedimiento del listado (2.3.8), destinado a intercambiar los valores de dos variables enteras, x e y. procedure swap(x, y: integer); var temp: integer; begin

(2.3.8)

temp = x; x := y; y := temp; end procedure;

Suponga que i = 1, a [ 11 = 2 y a [ 2 ] * 3 cuando llamamos a swa p( a [ i1, i) . ¿Cuáles son los valores de i , a C1 ] y a [ 2 ] al completar el procedimiento swap si: a. x e y se pasan por valor? b. x se pasa por valor e y por referencia? c. x e y se pasan por referencia? d. x e y se pasan por nombre? e. Repita los incisos a a d si la llamadafuera swap( i , a[ i ]). 8. ¿Cómo podría usted calcular la triple suma detodos los elementos de una matriz tridimensional a Ci , j , k] haciendo uso del procedimiento SIGMA de ALGOL 60 del listado (2.3.5) y llamándolo por nombre?

2.4

RESUMEN Hemos examinado en este capítulo las abstracciones, que elevan un lenguaje de programación por encima del nivel de la máquina. Éstas se agrupan en tres catego­ rías: abstracción de datos, de control y de procedimientos. Los métodos de inicio de abstracción de datos desde los bits y bytes subyacen­ tes son a través de tipos de datos simples como enteros, reales y caracteres; a través de tipos de datos estructurados como registros, arreglos, listas y conjuntos, como se presentaron en el capítulo 1; y a través de tipos de datos abstractos, donde los datos son empacados y definidos por sus operaciones asociadas. Las diferencias entre los lenguajes reflejan el nivel de abstracción y si el tipo es reforzado o no. También examinamos dos métodos para probar teóricamente que una implementación de un tipo de datos representa fielmente un tipo abstracto: los modelos abs­ tractos y la especificación algebraica. La abstracción de control involucra movimiento en tiempo de ejecución a tra­ vés de un programa. Los métodos para ramificación de dos o múltiples vías, interacción y recursión fueron examinados en varios lenguajes. La abstracción de procedimiento involucra la asignación de tareas individua­ les para procedimientos y sus interfaces. Aquí consideramos los módulos, inclu­ Sólo fines educativos - FreeLibros

106

PARTE I: Conceptos preliminares

yendo procedimientos asociados y datos. Una de las ventajas importantes de la modularización es el ocultamiento de información, de manera que los usuarios conozcan todo lo que necesitan, pero nada más. Tal ocultamiento promueve la com­ prensión mediante la eliminación de detalles innecesarios, y facilita la revisión y seguridad del programa. La modularización también fomenta el desarrollo des­ cendente de programas, el cual puede hacerse por miembros independientes de un equipo, y la concurrencia, donde más de un módulo puede ejecutarse al mismo tiempo. Esto finaliza nuestra consideración de los conceptos preliminares. En los capí­ tulos subsecuentes, veremos cómo estas abstracciones han sido puestas en práctica en diversos lenguajes. En la parte II examinaremos los lenguajes imperativos, con­ siderando la estructura de bloques, objetos y concurrencia. La parte IV trata de los lenguajes imperativos diseñados sobre la base de funciones, lógica matemática o los fundamentos para diseño y mantenimiento de bases de datos.

2.5 NOTAS SOBRE LAS REFERENCIAS El artículo introductorio de Hoare acerca de modelos abstractos [Hoare, 1972] es bastante pesado para aquellos no familiarizados con la notación de la lógica mate­ mática y teoría de demostraciones formales. Un tratamiento más accesible se en­ cuentra contenido en [Zilles, 1986], capítulo 4. Un artículo anterior por Liskov y Zílles [Liskov, 1975] analiza los propósitos de las técnicas de especificación forma­ les, criterios para la evaluación de tales técnicas y los métodos tanto de los modelos abstractos como de la especificación algebraica. El artículo está bien escrito y es accesible para los estudiantes universitarios. Podría suministrar los fundamentos para un buen informe de seminario. John Guttag ha desarrollado un sistema para auxiliar en la generación automática de especificaciones algebraicas. Las referen­ cias a este trabajo pueden encontrarse en [Guttag, 1977]. Algunas de las extravagancias del paso por nombre están documentadas en [Knuth, 1967]. Las inseguridades y ambigüedades en la construcción fueron tan extensas que el paso por nombre no ha sido implementado en la mayoría de los lenguajes modernos.

Sólo fines educativos - FreeLibros

P A R T E II

Lenguajes imperativos

En los siguientes tres capítulos consideraremos los lenguajes imperativos, donde un imperativo es un comando (en este caso, para que una computadora haga algo). Las variables representan localidades de memoria en la unidad central de procesa­ miento (CPU) de una computadora, y un lenguaje imperativo proporciona los co­ mandos para almacenar o cambiar de manera secuencial los valores en estas localidades. Por ejecución secuencial queremos decir que los comandos se propor­ cionan y se efectúan uno después de otro en el tiempo. Por ejemplo, var Ñame: str1ng; Ñame := "Jack"; Ñame := Ñame + " el Destripador";

proporciona cuatro comandos. El primero, para encontrar una localidad de almace­ namiento e identificarla con la variable Nombre; el segundo, para almacenar el valor "Jack" en esa localidad; el tercero, para concatenar " el D e s t r i pador" al valor de Nombre; y finalmente, para remplazar "Jack" con la cadena concatenada en la ubica­ ción identificada con Nombre. En el capítulo 3 examinaremos los lenguajes de procedimientos que facilitan la organización de un programa en bloques o procedimientos separados, cada uno de los cuales lleva a cabo una tarea específica. El capítulo 4 considera los lenguajes que soportan la programación orientada a objetos (POO), donde los procedimien­ tos y los datos son agrupados en módulos significativos llamados objetos. En el capítulo 5 veremos algunos lenguajes que soportan la ejecución en paralelo, donde múltiples CPU corren de manera simultánea, trabajando en diferentes partes de un problema al mismo tiempo.

Sólo fines educativos - FreeLibros

CAPÍTULO 3 ESTRUCTURA EN BLOQUES 3.0 En este capítulo 3.1 ALGOL 60 Viñeta histórica: Diseño por comité Conceptos de ALGOL 60 Estructura en bloques Declaraciones de tipo explícitas para variables y procedimientos Reglas de alcance para variables locales Expresiones y enunciados anidados if... then...else Llamada por valor y llamada por nombre Subrutinas recursivas Arreglos con límites dinámicos Puntos problemáticos en ALGOL 60 Especificación del lenguaje Ejercicios 3.1 3.2 ALGOL 68 3.3 Pascal Viñeta histórica: Pascal y Modula-2: Niklaus Wirth Filosofía y estructura Tipificación de datos fuerte Ejercicios 3.3

110 111 111 113 114

114 115 116 117 118 118 119 120 121 123 124 124 126 127 129

3.4 Ada Viñeta histórica: Ada Organización del programa Tipos La facilidad genérica Excepciones El entorno de soporte para programación en Ada (APSE) Ejercicios 3.4 3.5 C Viñeta histórica: El dúo dinámico: Dennis Ritchie y Kenneth Thompson Tipos de datos en C Conversiones de tipo y representaciones Operadores de C Un ejemplo de operaciones de bits de bajo nivel Arreglos, apuntadores y el operador coma C y UNIX El C estándar Ventajas y desventajas Ejercicios 3.5 3.6 Resumen 3.7 Notas sobre las referencias

Sólo fines educativos - FreeLibros

129 130 132 135 140 141 142 143 145

146 148 150 151 153 157 158 159 159 159 160 161

CAPÍTULO

3

Estructura en bloques

El paradigma estructurado en bloques está caracterizado por • • •

Bloques anidados Procedimientos Recursión

Un bloque es una sección de código contigua en la que pueden localizarse las va­ riables. Así cualquier información que se vaya a utilizar exclusivamente dentro de un bloque, y que no necesite ser conocida por los bloques circundantes, puede ocultarse. Esta característica es ventajosa por varias razones. Primero, ubica cambios que podrían hacerse en el futuro. Las variables locales pueden afectar el desempeño solamente en el (los) bloque(s) en que sean visibles. Segundo, cuando se comprue­ be que es correcto, pueden hacerse suposiciones al inicio y al final de un bloque. Si la estructura del bloque puede utilizarse para demostrar que las suposiciones del final necesariamente siguen de aquellas al inicio y las operaciones realizadas den­ tro del bloque, las pruebas complejas se facilitan. Tercero, un programador o grupo de programadores no necesitan preocuparse por nombres conflictivos para cual­ quier variable local dentro de un bloque. Finalmente, la estructura en bloques faci­ lita la organización del programa si un bloque incorpora un concepto simple. La estructura de ALGOL 60 fue un comienzo en esta dirección. Una vez que los bloques han sido implementados, los procedimientos se si­ guen naturalmente como bloques nombrados que pueden ser llamados desde otras partes de un programa, y que facilitan el intercambio de información explícita en­ tre el bloque que llama y el que es llamado a través de parámetros. Como vimos en el capítulo 1, el modelo de implementación para los bloques es la pila. Solamente un bloque puede estar activo a la vez, y su memoria o almacenamiento asignado ocupa el tope o parte superior de la pila en tiempo de ejecución. Cuando termina un bloque, su asignación de memoria será extraída, y la memoria para el bloque Sólo fines educativos - FreeLibros

110

PARTE n: Lenguajes imperativos ALGOL 60

í

f

Pascal

A LG O L 68

"T" CPL

Simula 67

f Modula-2

Ada

FIGURA 3.0.1 Linaje de los lenguajes tipo ALGOL1

que llama será reactivada. Vimos en el capítulo 2 que la implementación de la pila soporta la recursión, como invocaciones sucesivas de un procedimiento recursivo que puede ser empujado sobre la pila en tiempo de ejecución y extraído en orden inverso, pasando los valores de regreso a la pila. Los bloques de ALGOL fueron un buen comienzo, pero no lo suficiente para asegurar la modificación y corrección locales para grandes sistemas complejos. El primer artículo que planteaba las necesidades para ocultamiento y conexiones de información más explícitas entre módulos fue [Pamas, 1971]. Él propuso que los diseñadores de sistemas deberían controlar la distribución de la información de diseño, puesto que "un buen programador hace uso de la información útil que se le proporciona", y alguien tendría que estar a cargo. En los descendientes de los blo­ ques, los módulos y los objetos, el control explícito de la información ha sido implementado. Los datos, procedimientos o módulos enteros pueden ser visibles o invisibles para un usuario o programador que utilice, pero que sea incapaz de modificar, las características ocultas.

3.0 EN ESTE CAPÍTULO El linaje de los lenguajes tipo ALGOL se muestra en la figura 3.0.1. En este capítulo, examinaremos las ramificaciones para ALGOL 68, Pascal-Ada y CPL-BCPL-C. La ramificación Simula-Smalltalk-C++/Java se considerará en el capítulo 4. 1 La figura 3.0.1 indica las principales influencias de ALGOL en los lenguajes posteriores. Existen muchas variantes de este diagrama; por ejemplo, véase [Sammet, 1969], [Barón, 1989], [Sethi, 1989] o [Sebesta, 1993].

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

111

3.1 ALGOL 60 VIÑETA HISTÓRICA

Diseño por comité Es comúnmente aceptado que nada bueno puede provenir de un comité. Puesto que hay mucho en juego, el compromiso es inevitable, por lo cual es más proba­ ble que se obtengan mejores resultados de los esfuerzos de un individuo. Si uno fuera a observar de manera superficial la historia de ALGOL (ALGOrithmic Language; lenguaje algorítmico), se podría concluir que esta opinión es válida. ALGOL no pudo siquiera acercarse a su meta de llegar a ser un lenguaje de progra­ mación universal. Visto de manera diferente, es una historia de éxito en la que el actor principal, ALGOL, llegó a ser uno de los más importantes hitos conceptuales en la historia de las ciencias de la computación. La historia comenzó en 1957. FORTRAN acababa de entrar a la escena computacional, y una revolución en la programación estaba en marcha. Nuevos lenguajes estaban surgiendo por todas partes. Muchos grupos de usuarios en los Estados Unidos comenzaron a ver que la situación se estaba saliendo de control. Si un programador se mudaba, era casi inevitable que él o ella tuvieran que apren­ der un nuevo lenguaje de programación. El tiempo y los recursos se estaban des­ perdiciando. Los grupos solicitaron a la Asociación para Maquinaria de Computa­ ción (ACM; Association for Computing Machinery) que propusiera una solución. Una organización alemana, la Sociedad para las Matemáticas Aplicadas y Mecáni­ ca (GAMM), pugnaba por resolver el mismo problema, de modo que en mayo de 1958, la ACM y la GAMM unieron fuerzas. Un comité conjunto se reunió en Zurich para desarrollar un lenguaje de programación universal. Los vínculos cercanos de FORTRAN con IBM y sus productos habrían hecho que su elección pareciera como "el Departamento de Transporte de los Estados Unidos dando su aprobación a United Airlines o Ford Escorts™" [Barón, 1986]. Así, este comité inicial de ocho se embarcó en el diseño de un lenguaje de progra­ mación enteramente nuevo. Después de ocho días de trabajo, el grupo completó un borrador del lenguaje ALGOL, conocido originalmente como IAL (Lenguaje Algebraico Internacional, por sus siglas en inglés). Aunque el borrador se realizó rápidamente, no todo fue miel sobre hojuelas en las reuniones del comité. En un punto, una reunión llegó a un completo estancamiento acerca de los puntos decimales. Los americanos em­ plean un punto, mientras que los europeos utilizan una coma. Un miembro del comité golpeaba la mesa, jurando "nunca (voy a) usar un punto para separar las cifras decimales". Este conflicto fue resuelto mediante la decisión de que ALGOL se representara a tres niveles: como lenguaje de referencia, lenguaje de hardware y lenguaje de publicación. Esto dio a todos la libertad para representar los puntos decimales como quisieran en el lenguaje de publicación. El producto del trabajo del comité, el informe ALGOL 58, dio a conocer los objetivos del nuevo lenguaje: Sólo fines educativos - FreeLibros

112

• • •

PARTE II: Lenguajes im p erativ o s

El nuevo lenguaje estaría tan cerca como fuera posible de la notación matemá­ tica estándar y sería legible con poca explicación adicional. Sería posible utilizarlo para la descripción de procesos de cómputo y publica­ ciones. Sería mecánicamente traducible a programas de máquina.

Este informe generó un gran interés, e IBM consideró abandonar FORTRAN a fa­ vor de ALGOL. Es interesante hacer notar que, como apunta Barón, "muchos de los inventores europeos del lenguaje... se dieron cuenta de que 'Algol'2 es el nombre de la segun­ da estrella más brillante en la constelación de Perseo. la cantidad de luz que emana de Algol es cambiante: aproximadamente cada 69 horas, la estrella es eclipsada por un gran cuerpo opaco, su estrella gemela, que se encuentra a cerca de 10 millones de kilómetros de distancia. Sin embargo, Algol siempre se las arregla para recobrar su brillantez. El doble sentido no pasó inadvertido para los europeos: el lenguaje ALGOL no sería eclipsado por FORTRAN" [Barón, 1986], Pero ALGOL sifué eclip­ sado cuando IBM tomó la decisión de quedarse con FORTRAN. ALGOL todavía era un borrador de manera que los programadores pudieran hacer sugerencias acer­ ca de su forma final, mientras que FORTRAN estaba completo y depurado. En enero de 1959, trece miembros de la ACM y la GAMM se reunieron en París por seis días para transformar ALGOL 58 en un lenguaje completo, ALGOL 60. El informe resultante fue único en el sentido de que la sintaxis del lenguaje estaba descrita en la nueva forma de Backus-Naur (BNF), desarrollada por los miembros del comité John Backus y Peter Naur. La semántica estaba descrita en un inglés claro, sin ambigüedades, lo que dio como resultado un informe muy legible [Naur, 1963]. "La brevedad y elegancia de este informe contribuyeron de manera signifi­ cativa a la reputación de ALGOL como un lenguaje elegante y simple" [MacLennan, 1987]. ALGOL 60 probó ser un importante adelanto en las ciencias de la computación. La pasión europea por el orden influyó en su metamorfosis para convertirlo en el primer lenguaje estructurado de segunda generación. Se introdujeron importantes construcciones de lenguaje [Wegner, 1976], tales como: • • • • •

Estructura en bloques Declaraciones de tipo explícitas para variables Reglas de alcance para variables locales Tiempos de vida dinámicos, opuestos a los estáticos, para variables Expresiones y enunciados anidados if-then-else

2 En muchos círculos, la regla de las letras mayúsculas para los nombres de los lenguajes de progra­ mación consiste en que todas las letras sean mayúsculas si el nombre es un acrónimo, por ejemplo ALGOL, que viene de "ALGOrithmic Language", y solamente la primera letra en mayúscula para los nombres propios, como por ejemplo, Pascal. Hemos seguido esta costumbre excepto para citas que no concuerden, incluyendo esta referencia a la estrella Algol. No hemos utilizado guiones en ALGOL 60 ni en ALGOL 68, pues no fueron usados en los informes originales. No obstante, ion utilizados con fre­ cuencia en la literatura. A Modula-2 se le agregó el guión en los escritos de Wirth, y el guión solamente es omitido en ocasiones.

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

• • •

113

Llamadas por valor y llamadas por nombre para parámetros de procedi­ mientos Subrutinas recursivas Arreglos con límites dinámicos

Estas nuevas construcciones condujeron en forma directa al desarrollo de Pascal, Modula-2 y Ada. La notación BNF, utilizada por primera vez en el informe de ALGOL 60, hizo posible el desarrollo de una teoría formal de lenguajes de progra­ mación, la cual facilita el diseño exitoso de compiladores. De este modo ALGOL, un fracaso comercial, se considera un triunfo científico. IBM no fue el único responsable de la caída de ALGOL en el mercado. Por alguna razón, ALGOL 60 no tenía enunciados de entrada/salida. Este aparente­ mente gran defecto estaba pensado por sus diseñadores para hacer a ALGOL inde­ pendiente de la máquina, pues se ajusta para un lenguaje verdaderamente universal. En su lugar, se proporcionaba una biblioteca de rutinas de E/S, específica para cada implementación. Esta noción de separar la E/S de la especificación del len­ guaje se continuó en Ada, pero Ada incluye una biblioteca estándar. Finalmente, esta situación de E/S fue corregida en ALGOL 68, pero era demasiado tarde. El hecho de que el informe de ALGOL 68 fuera considerado generalmente ilegible no ayudaba mucho. Los diseñadores de ALGOL 68 se afanaron para proporcionar construcciones de lenguaje de máxima generalidad y flexibilidad. Sin embargo, estas construcciones probaron ser demasiado complejas para ser aprendidas fácil­ mente por un programador de aplicaciones. El futuro de ALGOL 68 está en blanco. Sus usuarios están casi extintos en Esta­ dos Unidos, y son una especie en peligro también en Europa. Pero los sucesores de ALGOL 60, Pascal, Modula-2 y Ada, son un éxito tanto comercial como científico. Y el lenguaje de programación C también está medrando.

Conceptos de ALGOL 60 í

ALGOL ha tenido tan gran influencia sobre los lenguajes de programación que el término "tipo ALGOL" se utiliza ampliamente para describir lenguajes con las si­ guientes seis características [Horowitz, 1984]: 1. 2. 3. 4. 5. 6.

Es un lenguaje algorítmico; es decir, facilita la solución paso por paso de pro­ blemas, incluyendo ciclos repetitivos. El algoritmo es transmitido a la computadora como una serie de cambios al almacenamiento (memoria). Las unidades básicas de cálculo son el bloque y el procedimiento. Las variables son tipificadas, y los tipos son verificados en tiempo de compila­ ción y/o tiempo de ejecución. Utiliza la regla de alcance lexicográfico (estático); es decir, el entorno de un procedimiento es aquel en el que está definido. Está diseñado para ser compilado, más que interpretado.

Sólo fines educativos - FreeLibros

114

PARTE n: Lenguajes imperativos

Aunque muchas de estas ideas fueron mencionadas en los capítulos 1 y 2, las exa­ minaremos adicionalmente en las secciones que siguen.

Estructura en bloques Puesto que los bloques fueron presentados en el capítulo 1 utilizando pseudocódigo en el listado (1.2.4), consideremos la versión de ALGOL 60 mostrada en el listado (3.1.1). Q: begin integer 1, k ; real w ; for 1 1 step 1 until m do for k :« 1+1 step 1 until m do begin w A[i,k] ; AC1,k] ACk.i]; A[k,i] w end for 1 and k end block Q

(3.1.1)

De acuerdo con las reglas de alcance, las variables locales 1, k y wson visibles a lo largo del bloque, mientras examinamos los bloques encerrados para declaraciones de variables no locales como Ay m. ALGOL 60 define un bloque ya sea como etiquetado o no etiquetado. Como etiquetado, se puede tener acceso a Qdesde el exterior mediante un enunciado tal como go to Q. Un bloque no etiquetado podría ser el mismo si las dos referencias a la etiqueta Qfueran eliminadas. En PL/I y Ada, han sido implementados tanto los bloques etiquetados como los no etiquetados, mientras que en Pascal, las variables locales pueden ser decla­ radas solamente en procedimientos o funciones.

D eclaraciones de tipo explícitas para variables y procedim ientos FORTRAN facilita la declaración de variables, pero permite la declaración implíci­ ta de enteros y reales. A menos que se declare de otra forma, cualquier variable en FORTRAN que comience con I, J, K, L, M o N es un entero, y cualquier otra es real. ALGOL 60 tiene tres tipos de variable simple: entero (integer), real y booleano (boolean), y todas las variables deben estar declaradas de manera explícita. Una variable booleana puede tener el valor true (verdadero) o f a 1se (falso). Los carac­ teres y cadenas no están tipificados, pero pueden ser pasados por nombre como un parámetro real. El único tipo estructurado en ALGOL 60 es el array (arreglo), el cual es un conjunto ordenado de elementos del mismo tipo. Por ejemplo, Integer array A[ 1:20] describe un arreglo unidimensional de 20 enteros. El enunciado Integer array B[1f c<0 then 2 else 1:20)

declara un arreglo B semejante a A, a menos que la variable c tenga un valor menor que 0, en cuyo caso B tiene solamente 19 localidades de almacenamiento, indizadas desde 2 hasta 20. Analizaremos los arreglos con límites dinámicos más adelante.

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

115

ALGOL 68 agregó los tipos record y character, entre otros, donde un record (re­ gistro) es una plantilla para un agregado que contenga elementos posiblemente de diferentes tipos. Cualquier declaración de tipo de ALGOL 60 puede estar precedida por la de­ signación own; por ejemplo, own Integer array AC5:100]. En este caso, a la salida del bloque en el cual A está declarado, su valor será retenido y puede ser accesado en la reentrada del bloque. Las variables locales y sus valores en Pascal, Modula y Ada son destruidos a la salida del bloque en el que son declaradas. Sin embargo, en C la noción de variables "propias" (own) ha sido implementada. Una variable C declarada para ser statlc retendrá sus valores durante la vida del programa, mien­ tras que variables automáticas (la clase de almacenamiento predeterminada) son destruidas a la salida de sus bloques de definición. R eglas de alcan ce p a ra variables locales El almacenamiento para las variables ALGOL declaradas locales en un bloque no está asignado hasta la entrada al bloque, y es desasignado a la salida del bloque. Sin embargo, existen ciertas excepciones a esta regla. La primera es para variables own, como se anotó con anterioridad. La segunda es una ejecución del enunciado sw1 tch, el cual es el enunciado de opción múltiple (case) de ALGOL. Es en realidad un enunciado "go to" disfrazado. Un ejemplo de un enunciado switch es: switch S := SI, S2, Q[m], if v > -5 then S3 else S4;

(3.1.2)

Cada una de las cuatro expresiones en el lado derecho del enunciado se evalúa en relación con una etiqueta. Si S - 3, entonces el control se dirigirá al enunciado etiquetado por el valor de la tercera expresión, QEml ALGOL permite que esta etiqueta haga referencia a una línea de código externo al bloque en el que ocurre el enunciado switch. El informe ALGOL 60 establece que en un caso así, "los conflic­ tos entre los identificadores para las cantidades en esta expresión y los identificadores cuyas declaraciones son válidas en el sitio del señalador de conmutación o switch se evitarán a través de cambios sistemáticos convenientes de los identificadores posteriores" [Naur, 1963]. Esto significa que si m = 5 en el bloque B2, donde el enunciado switch sea encontrado, y el valor de QC5] sea una etiqueta en el bloque B, fuera de B2, el nombre de la variable mpuede cambiarse en B si su valor o tipo difieren del de men B2. (Véase el ejercicio 3.1.3 para una exploración adicional de esta situación.) El enunciado un tanto barroco switch de ALGOL 60 es semejante al G0 TO calculado de FORTRAN, en donde G0 T0( L1 Ln) S conmuta la ejecución para el enunciado etiquetado Li, si el valor de S = i. Puesto que FORTRAN no tiene bloques anidados, un G0 T0 es bastante directo, y la ejecución continúa en el enun­ ciado apropiadamente etiquetado. Sin embargo, en los lenguajes estructurados en bloques, las variables deben ser desasignadas a la salida de un bloque, de modo que las reglas llegan a ser bastante estrictas. En Pascal, un goto sólo puede hacer referencia a un enunciado en el bloque en el cual está declarada la etiqueta. Uno no puede transferir a un enunciado compuesto, tal como un for, 1f o case, puesto que la(s) variable(s) de control no estaría(n) activa(s). En Ada, las reglas para acomodar Sólo fines educativos - FreeLibros

116

PARTE ü: Lenguajes imperativos

paquetes y tareas son algo más complejas. Como regla general, los goto de Ada pueden transferir en el mismo nivel lexicográfico. Debido a la desorganización resultante del program a y a los errores subsecuentes, los goto generalmente no son recomendados sino sólo permitidos para usos especiales, tales como la terminación de un bloque o un programa debi­ do a un error. No se permite la transferencia en un bloque contenido, y si la ejecu­ ción se transfiere a un bloque circundante, el bloque donde ocurre el goto y todos los bloques intermedios deben desactivarse durante la transferencia. En la figura 3.1.1, si se transfiere el control desde el bloque S hasta el enunciado etiquetado 1 en el bloque P, los bloques S, Ry Qdeben desactivarse durante la transferencia.

Expresiones y enunciados anidados if...then...else ALGOL fue el primer lenguaje que permitió enunciados anidados así como tam­ bién bloques. Un enunciado if A then SI el se S2

no tiene restricciones sobre los enunciados SI y S2;puedeserun 1f...then...else a cualquier nivel de anidación. El Informe ALGOL 60 proporciona lo siguiente como un ejemplo de un enunciado válido de ALGOL: 1f if if a then b else c then d else f then g else h


¿Puede usted ordenar esto último? ¿Cuáles variables representan necesariamente expresiones booleanas?

P: Etiqueta 1;

FIGURA 3.1.1 Efecto de go to en activaciones de bloques

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

117

L lam ada p or valor y llam ada por nombre Los parámetros en ALGOL 60 son parámetros por nombre de manera predetermi­ nada, aunque las especificaciones permiten el uso de parámetros por valor. Consi­ dere la función del listado (3.1.3). procedure Increment(u, inc); valué u, inc; real u, inc;

(3.1.3)

begln u := u + inc; end;

La llamada puede ser Increment( x + y, z). Los parámetros reales x + y y z se pasan por valor a los parámetros formales u e i nc. A la entrada del bloque de procedimiento para Increment, se asigna almacenamiento para dos números rea­ les, y los valores de x + y y z se almacenan en las localidades para u e i nc, respec­ tivamente. Ninguna de las variables x, y o z son modificadas por el procedimiento Increment. No hay vínculo entre los parámetros reales y formales después de la copia inicial de los parámetros reales a los formales. En contraste, considere el listado (3.1.4). procedure Increment2(u, inc); real u, inc;

(3.1.4)

begln u := u + inc; end;

Aquí los parámetros se pasan por nombre, el procedimiento predeterminado en ALGOL 60. El efecto es que la llamada Increment2(x, z) es remplazada en el en tor­ no del que llama mediante el cuerpo de Increment2, conelnombrex sustituido para el citado parámetro formal u, y z para i nc; es decir, begln x := x + z end;

Aquí el valor de x se cambia. Si la llamada fuera Increment2(x, y + z), la sustitución sería, begln x

x + thunk; end;

El thunk proporciona una dirección de código para la expresión y + z. Dondequie­ ra que sea encontrado el thunk, el control se dirige a esa dirección, se calcula y + z y su valor es devuelto en lugar del thunk. La llamada por nombre es muy poderosa, como hemos visto en la sección 2.3. Como otro ejemplo, considere el procedimiento Integral de ALGOL en el listado (3.1.5). real procedure Integral (func, low, high, interval); (3.1.5) real procedure func; real low, high, interval; begln integer i, n; real Lastlnterval; n := entier (high - low); Integral

comment: entier s trúncate;

:= 0;

for i:= 1 step 1 until n do Integral

:= func (low + i*interval/2) * interval;

Lastlnterval Integral

:= high - (low + n*interval);

:= func (LastInterval/2) * Lastlnterval;

end;

Supongamos que la llamada fue Integral (sqrt, 0, 10, 0.001). Cada vez que se encuentra func, el control se transferirá a código para la función sqrt (mediante un thunk), donde el valor apropiado se calculará y devolverá a Integral. Sólo fines educativos - FreeLibros

118

PARTE II: Lenguajes imperativos

Sin embargo, no todo está bien con la llamada por valor. En el ejercicio 2.3.6 consideramos un simple procedimiento de intercambio, donde se encontró (eso esperamos) que, al utilizar parámetros por nombre, una llamada a swa p ( I , AHI]) no necesariamente conmutaba los dos parámetros. Debido a irregularidades tales como ésta, la llamada por nombre ha desaparecido esencialmente de los lenguajes imperativos modernos. Sin embargo, el mecanismo es empleado en los lengua­ jes funcionales SCHEME y ML para f o r z a r (forcé) la evaluación de una expresión que haya sido previamente r et a rd a da (delay). Examinaremos este uso más ade­ lante, en el capítulo 8.

Subrutinas recursivas Aunque el informe no hace una mención explícita de la recursión, ésta es permitida gracias a lo que el informe no dice. El listado (3.1.6) muestra cómo se define un procedimiento en la BNF del informe. d e c la r a c ió n de p

r

o

c

e

d

i

m

i

e

n

t

o

s

)

;

( 3. 1. 6)

procedure <encabezado de procedimientoxcuerpo de procedimiento) |

procedure <encabezado de procedimientoxcuerpo de procedimiento). <cuerpo de procedimiento) ::= <enunciado> ]

El término códi go se refiere a procedimientos que no son de ALGOL. Los diseñadores previeron que los procedimientos de lenguaje ensamblador o FORTRAN serían importados a un programa ALGOL. La manera precisa en la cual esto iba a hacerse se dejó para el hardware y/o lenguajes de publicación y no estaba especificado en el informe, que consideraba únicamente el lenguaje de referencia. La definición del cuerpo del procedimiento especifica que sea un enunciado o código, pero no pone ninguna restricción sobre el enunciado. Un tipo de enuncia­ do es una llamada de procedimiento, de manera que una llamada a P, dentro del procedimiento P, es bastante aceptable. PL/I, el cual se estaba desarrollando aproxi­ madamente al mismo tiempo, también permite procedimientos recursivos, pero sólo si son declarados para serlo así; por ejemplo, una versión recursiva de la fun­ ción factorial se declara en PL/I como: Factorial: procedure (n) recursive;

pero en ALGOL como: Integer procedure Factorial

(n);

Se deja al escritor del compilador de ALGOL la tarea de reconocer que F a c t o r i al sea realmente recursiva y se implemente de manera apropiada.

Arreglos con lím ites dinámicos En un lenguaje tal como Pascal, el tamaño o dimensión de un arreglo debe ser declarado antes de que un programa sea compilado.3 De esta manera su almacena­ 3 El Estándar ISO Pascal Nivel 0 excluye los tipos de arreglos dinámicos, pero la discutida extensión Nivel 1 incluye parámetros de arreglos concordantes, los cuales permiten parámetros de arreglo con límites superior e inferior de sólo lectura [Cooper, 1983].

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

119

miento puede ser asignado antes de que el programa sea ejecutado. Una segunda ventaja es que el tipo índice necesita ser verificado solamente una vez. Si su valor máximo está dentro de los límites del arreglo, no necesitan hacerse verificaciones adicionales. La verificación del máximo para un tipo de índice simple puede ser más rápida que otras pruebas más complejas. En una situación donde el tamaño de un arreglo depende de algún valor calculado por el programa, el arreglo se declara en ocasiones para tener algún tamaño máximo estimado, y entonces se llena sólo parcialmente. ALGOL 60, PL/I y Ada prescriben arreglos con límites dinámicos. Estos límites pueden calcularse en tiempo de ejecución, pero deben ser conocidos antes de que se utilice el arreglo. El almacenamiento se encuentra entonces para el arreglo com­ pleto, precisamente como ocurre para las variables dinámicas. ALGOL 68 requiere de arreglos con límites flexibles, los cuales pueden cambiar después de que el arre­ glo ha sido creado y se ha asignado almacenamiento para él. APL es incluso menos demandante, y cualquier variable puede tener un arreglo de cualquier tamaño como su valor, simplemente mediante la asignación de un arreglo a ésta. Puntos problemáticos en ALGOL 60 En 1967, la Communications o f the ACM publicó un artículo de Donald Knuth [Knuth, 1967], en el cual se reunían todas las ambigüedades y errores detectados en el infor­ me de ALGOL 60. Por “ambigüedades" Knuth quiso decir que cierto número de personas con conocimientos encontraban distinto significado en una parte del in­ forme. Un "error" constituye una ambigüedad en la cual casi todos estaban de acuerdo en la corrección necesaria. Mencionamos algunas de ellas, así como varios remedios que se verían en los sucesores de ALGOL 60. En primer lugar, consideraremos algunas de las nueve ambigüedades. 1. Si se permiten efectos colaterales, entonces el orden de los cálculos debe estar especificado. (Una función tiene un efecto colateral si además de calcular un valor, se hace cambios a otras variables no locales.) Knuth proporciona el ejemplo del listado (3.1.7), que dejamos como el ejercicio 3.1.8 para que el lector encuentre las 11 posibles respuestas. begin

(3.1.7)

integer procedure f(x,y); valué y,x; integer y,x; a ;= f ;= x + 1; integer procedure g(x); integer x; x := g := a +

2;

a := 0; outreal4 (1, a + f(a, g(a))/g(a)) end;

4 outreal ( 1 , . . . ) indica que un procedimiento de salida debería ser suministrado por el escritor del compilador para la salida en el dispositivo número 1. Es una expresión del lenguaje de referencia, y puede ser diferente en cualquier lenguaje de publicación particular para ALGOL 60.

Sólo fines educativos - FreeLibros

PARTE II: Lenguajes imperativos

12 0

Obsérvese que cada uno de los procedimientos f y g tiene un efecto colateral. El procedimiento f incrementa el valor de la variable global a en 1, y g se incrementa en 2. Obsérvese también que tanto x como y son parámetros por valor en el proce­ dimiento f, pero parámetros por nombre en g. Una de las salidas es 4V2/ lo que ocurre si el orden del cálculo es como sigue: 1. 2. 3. 4.

g( a ) se calcula primero como el denominador de una fracción. f í a , g( a)), el numerador, se calcula en segundo lugar. Los parámetros va lúe en f se calculan primero con a, y después con g( a). a + f í a , g( a)) /g( a ) se calcula y sale al último.

2. Permisibilidad de un enunciado go to dentro de un procedimiento. Los goto violan el principio de una entrada /una salida en un procedimiento, lo que hace difícil la depuración. La idea de un procedimiento incorpora la transferencia de control desde una rutina de llamada a la que se llama. La que se llama se intro­ duce en la parte superior al inicio, y cuando se sale, regresa al enunciado inmedia­ tamente posterior al punto en que fue llamada. Los goto permiten regresar a (casi) cualquier lugar.5 3. ¿Hasta qué punto tienen que especificarse los tipos de variable, y qué cam­ bios de tipo automático pueden ocurrir? Por ejemplo, si x e y son enteros, ¿se per­ mite siempre x := x/y? Si es así, ¿x se redondea? ¿Se trunca? 4. Las variables own son un desastre. 5. No se especifica precisión para los números reales. En particular, ¿cuándo pueden considerarse iguales dos reales?

Entre las correcciones, solamente tres se mencionarán aquí. 1.

La división entre cero debería dar como resultado un error. El informe sugiere que "ciertos identificadores deberían estar reservados para las funciones estándar de análisis". Se sugiere, pero no se especifica, que éstos podrían incluir abs, sign, sqrt, sin, eos y arctan. Knuth sugiere que esto causaría confusión, a menos que la lista se adhiriera estrictamente a todas las implementaciones, y no sólo se agregara a ellas. 3. La llamada por nombre debería restringirse (recuerde el ejercicio 2.3.6). 2.

Especificación del lenguaje ALGOL 60 fue el primer lenguaje que tuvo una completa descripción de defini­ ción, como se detalla en el "Informe sobre el lenguaje algorítmico ALGOL 60" ("Report on the Algorithmic Language ALGOL 60") [Naur, 1963]. Cualquier compilador escrito para ALGOL tenía que implementar fielmente cada elemento del lenguaje como estaba definido. El informe consiste de cinco capítulos que ha­ cen un total de 17 páginas: 5 R. L. Clark [Clark, 1973] sugirió que el problema del "go-to" era en realidad un problema de "dónde-viene". Si un programa contiene diversos enunciados de la forma go to L, y si ocurre un error en o subsecuente al enunciado etiquetado L, no podemos saber dónde buscar el error, puesto que no sa­ bríamos de "dónde-vino".

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

1. 2. 3. 4. 5.

121

Estructura del lenguaje Símbolos básicos, identificadores, números y cadenas Expresiones Enunciados Declaraciones

El informe fue escrito en el lenguaje de referencia. Los lenguajes de publicación también se permitirían, lo cual podría diferir de un país a otro, pero "la correspondencia con la representación de referencia debe estar asegurada". La intención de los dife­ rentes lenguajes de publicación es facilitar la comunicación entre profesionales de la computación de modo que se permita un estilo de lenguaje más natural. Estrechamente relacionadas con los lenguajes de publicación están las represen­ taciones de hardware, las cuales se relacionan con máquinas individuales. Por ejem­ plo, el lenguaje de referencia define: coperador relacional> ::= < I < > I = I < I > I Muchos teclados no están equipados para manejar >, <., Pueden enumerarse las sustituciones particulares para una representación de hardware, pero sus significa­ dos deben concordar con las nociones matemáticas usuales representadas en la referencia. Una de las más grandes contribuciones del informe es el uso de la forma BackusNaur (Backus-Normal), o BNF, por sus siglas en inglés, para definir el lenguaje de referencia. En los cincuenta, el lingüista Noam Chomsky [Chomsky, 1965] estaba intentando desarrollar una teoría matemática de los lenguajes naturales, es decir, los de uso cotidiano para la comunicación entre las personas. Aunque sus cuatro tipos no incluyen todos los lenguajes hablados o escritos, la jerarquía ha sido muy útil para los lenguajes formales y de programación. Aunque el trabajo de Backus se realizó en forma independiente del de Chomsky, se reconoció rápidamente que la notación BNF era equivalente a las gramáticas de Chomsky del tipo 2, o libres de contexto. Ambas utilizan definiciones recursivas para identificar las unidades váli­ das de un lenguaje. La BNF se presentó en el capítulo 0. Examinaremos los lenguajes formales y sus relaciones con las máquinas teóricas de manera adicional en el capítulo 6.

E J E R C I C I O S 3. 1 1. Analice las ventajas y desventajas de la designación own en ALGOL 60. ¿Qué tendría que considerar un programador acerca de una variable own en la primera entrada dentro del bloque donde esté declarada? ¿Y en las entradas subsecuentes? 2. ALGOL permite arreglos con límites dinámicos. Si se declara own array A[1:1001 en un procedimiento P, ¿qué ocurre a los valores retenidos si P inicializa todos los 100 elementos de A, y luego cambia los límites de Aa, digamos, 1:50? ¿Qué está disponi­ ble en la segunda invocación? (¡No tema! Éste es un problema del escritor del compilador y son aceptables varias soluciones.) 3. Rastree el valor de las variables B.m y B2.m en el código de ALGOL 60 del listado (3.1.8), siguiendo la semántica del informe. (Aquí, B.m se refiere a men el bloque B, y B2 .m a la men el bloque B2.)

Sólo fines educativos - FreeLibros

122

PARTE n: Lenguajes imperativos B: begln integer array Q[1..20]; real m, r;

(3.1.8)

Q[2] := 1; 1:

m := 3.1416; r := 2.0; begin print (m*2*r); end;

B2: begin integer m; m := 2; S := 3; switch S := SI, S2, Q[m], if v>-5 then S3 else S4 end; end;

4. ¿Puede ver usted por qué una llamada de Increntent2(x + y, z), usando la declara­ ción del listado (3.1.4), no está permitida en ALGOL 60? 5. En cálculos numéricos, es bastante común sumar los elementos de un arreglo, IA [ i ] (i=j hasta n). El paso por nombre realiza esto de manera bastante cuidadosa em­ pleando una técnica conocida como el dispositivo de Jensen, como se muestra en el listado (3.1.9). real procedure SigmafA, i, low, high); valué low, high; real A; integer i, low, high; begin real sum; for i := low step 1 until high do

(3.1.9)

sum := sum + A;

Sigma := sum end;

6. 7. 8.

9.

a. ¿Por qué 1owy hi gh son parámetros por valor? b. Rastree la llamada total :« SigmaCAEk], k, 1, 20). Tenga cuidado de sustituir correctamente los parámetros por nombre Ae i . c. ¿Por qué necesitamos pasar de manera explícita a la variable índice i ? ¿Por qué los arreglos no pueden expandirse y contraerse? Por ejemplo, ¿cuál es el error de conectar dos partes de un arreglo de tamaño n con un apuntador desde los primeros i elementos hasta el último (n - i)? APL es por lo regular interpretado, más que compilado. ¿Por qué esto haría más fácil asignar arreglos a cualquier variable? Puesto que el informe de ALGOL 60 no especifica en qué orden deben proceder los cálculos, o en qué orden se evalúan los parámetros etiquetados valué, existen 11 posibles valores impresos en el dispositivo de salida 1, en la ejecución del enunciado outrealtl, a + f (a ,g(a) )/g(a)), analizado anteriormente. a. Encuentre tantos como pueda. b. Es difícil imaginar un ejemplo de la vida real de una función tal que f (a, g(a)) / g(a). ¿Por qué piensa usted que Donald Knuth haya prestado alguna atención a ella? Diferentes lenguajes de programación utilizan diferentes estrategias en identificadores con significados especiales. Por ejemplo, en FORTRAN es perfectamente válido de­ cir if = 2. Supuestamente, un compilador debería ser capaz de analizar si "if" es parte de un enunciado 1f...then o de un nombre de variable. ALGOL no especificaba palabras reservadas, pero sugería que ciertas funciones familiares deberían ser pro­ porcionadas. Analice los pros y los contras de: a. Ninguna palabra reservada. b. Tan pocas palabras reservadas como sea posible. c. Una extensa lista de funciones especiales, nombrada por palabras reservadas (ALGOL 68 tenía arriba de 100). Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

123

d. Una extensa lista de funciones definidas, las cuales podrían ser redefinidas por el usuario (la solución de PL/I). e. Una pequeña lista de palabras reservadas más una lista de procedimientos y fun­ ciones definidas que pudieran ser redefinidas por el usuario (la solución de Pascal).

3.2 ALGOL 68 ALGOL 68 fue el primer lenguaje en ser completamente descrito en una gramática formal, una gramática W, llamada en ocasiones gramática vW.6 En BNF, que fue utilizada para el informe de ALGOL 60, los autores fueron capaces de describir la sintaxis, pero no la semántica del lenguaje. Aun cuando un lenguaje puede ser expresado completamente en la gramática W, los lectores lo hallaban extremada­ mente difícil de comprender. Esta oscuridad7se cita con frecuencia como una de las razones de la muerte de ALGOL 68. La característica que define a ALGOL 68 es su ortogonalidad. "Un lenguaje ortogonal tiene un pequeño número de construcciones básicas y reglas para combi­ narlas de maneras regulares y sistemáticas. Se hace un intento muy deliberado por eliminar restricciones arbitrarias" [Tanenbaum, 1976]. Por ejemplo, una función mapea parámetros en un solo resultado. En ALGOL ortogonal, cada parámetro y el resultado funcional pueden ser de cualquier tipo, mientras que solamente pueden ser devueltos valores escalares o de apuntador mediante una función de Pascal o PL/I. Las reglas y restricciones arbitrarias son eliminadas en ALGOL 68, reducien­ do los errores de programa y la frustración del programador. Los procedimientos en ALGOL 68 son de modo8 proc. Puesto que los paráme­ tros de cualquier modo pueden ser pasados a un procedimiento o devueltos como un valor funcional, los procedimientos también pueden. Parecería poco práctico transferir un procedimiento como un segmento de código dentro o fuera de otro procedimiento, por lo que la posibilidad se implementa por lo regular al pasar un apuntador. Un apuntador, o referencia, al segmento de código se convierte en el parámetro real o valor funcional. Tanenbaum [Tanenbaum, 1976] proporciona el ejemplo elemental mostrado en el listado (3.2.1) para producir una suma de valo­ res funcionales, f(l) + f(2) + ... + f(n), para una función arbitraria f. proc sum = (int n, proc (real) real f) real:

(3.2.1)

begin real sum := 0; for i to n do sum := sum+f(i) od; sum end

6 La gramática-vW, llamada así en honor de su inventor A. van Wijngaarden, es sensible al contexto, mientras que BNF es libre de contexto. Por ejemplo, el enunciado FORTRAN IF (IF - 1) X - 2, es sensible al contexto en el cual el IF se utiliza, siendo el primer IF un condicional, y el segundo un nombre de variable. Analizaremos estas diferencias en la parte III. 7 Los programadores no esperaban aprender ALGOL 68 haciendo uso de la definición, y se escribie­ ron diversos tutoriales para ellos, por ejemplo [Tanenbaum, 1976]. 8 Los tipos en ALGOL 68 son llamados modos. Muchas nociones comunes fueron renombradas para advertir al usuario que las ideas eran algo diferentes que en otros lenguajes.

Sólo fines educativos - FreeLibros

124

PARTE II: Lenguajes imperativos

Una llamada a suma debe ser suma(100, sen), lo que nos daría s e n ( l ) + sen(2) + . . . + sen(lOO). Obsérvese que el contador i del ciclo for está predeterminado para el modo entero, comenzando en 1. Puesto que sen requiere de un parámetro real, i se transforma automáticamente en un real, para su uso con f ( i ). Esta noción de procedimientos como objetos de primera clase estaba presente en LISP y se experimentaba en SIMULA, el primero de los lenguajes orientados a objetos. El paso de procedimiento sobrevivió en Pascal sólo en forma limitada. Otro de los logros genuinos de ALGOL 68 fue su uso de los operadores. Un operador es un símbolo que representa un procedimiento o función, tal como los operadores aritméticos binarios, + y *, o el unitario, -. 2 + 3,5 * 6 y -2 son familiares para todos nosotros. Un operador puede tener precedencia sobre otro, de manera que 2 + 3 * 5 se evalúa como 17 en vez de 25. Uno no solamente puede definir nuevos operadores en ALGOL 68, sino definir y volver a definir la precedencia también. De esta forma, si uno quiere que 2 + 3 * 5 = 25, como en algunas calculado­ ras portátiles simples donde * no tiene precedencia sobre +, uno puede lograrlo en ALGOL 68. El principio ortogonal dicta que podemos volver a definir la preceden­ cia predeterminada integrada en ALGOL, puesto que podemos definir la prece­ dencia para los operadores definidos por el usuario. Los diseñadores de Ada incluyeron operadores definidos por el usuario, como los tienen aquellos lenguajes declarativos tales como PROLOG y LISP. Un usuario de C++ pueden volver a defi­ nir un operador existente, pero no puede redefinir su precedencia. Aunque ALGOL 68 ganó poca popularidad en Estados Unidos, muchas de sus características pioneras han sido empleadas en otros lenguajes.

3.3 PASCAL En contraste con el mucho más complicado ALGOL 68, ALGOL 60 influenció un lenguaje mucho más simple, diseñado para enseñar estilo y buenos principios de programación. Éste es el lenguaje Pascal.

VIÑETA HISTÓRICA Pascal y M odula-2: N iklaus W irth

La complejidad tiene y mantendrá una fuerte fascinación para mucha gente. Es verdad que vivimos en un mundo complejo y tratamos de resolver problemas inherentemente complejos, lo que con frecuencia requiere de mecanismos complejos. Sin embargo, esto no debería disminuir nuestro deseo por hallar soluciones elegantes,9que convencen por su claridad y eficiencia. Las soluciones simples y elegantes son más efectivas, pero son

9En matemáticas, la palabra elegante se usa a menudo para describir una teoría o construcción que es muy parca. Es decir, contiene todo lo que es necesario, pero excluye cualquier adorno innecesario. Fred Astaire sería elegante, mientras que Liberace no.

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

125

más difíciles de encontrar que las complejas, y requieren de más tiempo, lo que a menu­ do creemos que no se puede afrontar. (Niklaus Wirth, Conferencia por el Premio Turing, 1984.) [Wirth, 1985.] Durante el intervalo desde mediados hasta finales de los sesenta, ALGOL fue el foco de mucha atención en el mundo de la programación de computadoras. Niklaus Wirth estaba en el centro de todo, trabajando en versiones mejoradas de ALGOL 60 en el laboratorio ETH en Zurich. La necesidad de un sucesor para ALGOL se había hecho patente después de la publicación del informe revisado. Aunque contenía muchas ideas conceptuales brillantes, el lenguaje carecía de capacidades prácticas tales como variables de carácter y E/S. Wirth y Tony Hoare, de la Universidad de Oxford, pero ambos en ese entonces en la Universidad de Stanford, sugirieron al comité ALGOL varias modestas pero importantes mejoras a ALGOL 60. Las ideas fueron rechazadas y el sucesor llegó a ser el demasiado complejo ALGOL 68. Wirth, rehusándose a ser intimidado por un comité de mentes estrechas, desa­ rrolló su propio sucesor para ALGOL 60, llamado ALGOL-W. Durante los siguien­ tes cuatro años, con la ayuda de tres asistentes, desarrolló un sucesor para ese lenguaje, que llegó a ser conocido como Pascal, en honor de Blaise Pascal, el mate­ mático, científico y escritor religioso de nacionalidad francesa. Pascal es en muchos sentidos una versión elegante de ALGOL 60. "A l igual que ALGOL 60, el lenguaje Pascal estándar contiene todo el código necesario para implementación en computadoras" [Barón, 1986]. Es al mismo tiempo hermoso y práctico. Wirth había diseñado Pascal teniendo los siguientes dos objetivos en mente [Cooper, 1983]: 1. 2.

Proporcionar un lenguaje de enseñanza que pudiera llevar conceptos comunes a todos los lenguajes mientras evitara inconsistencias y detalles innecesarios. Definir un lenguaje estándar verdadero que fuera barato y fácil de implementar en cualquier computadora.

Estos objetivos han sido cumplidos. Muchas universidades y colegios enseñan Pascal como un primer lenguaje de programación, y ha sido el lenguaje empleado por el AP Computer Science Exam para estudiantes de preparatoria (aunque estén cam­ biando a C++). Que Pascal sea un lenguaje estructurado tiene mucho que ver con su popularidad en el mundo de la educación. De acuerdo con Wirth, los programas son diseñados "de acuerdo con los mismos principios de los circuitos electrónicos; es decir, claramente subdivididos en partes con solamente unos cuantos alambres cruzando a través de sus fronteras" [Wirth, 1985]. Él cree que los estudiantes debe­ rían programar de este modo, especialmente al principio de su educación, porque "el lenguaje en el que se enseña al estudiante a expresar sus ideas tiene una in­ fluencia profunda en sus hábitos de pensamiento e invención" [Jensen, 1974]. Un importante hito en la historia de Pascal ocurrió cuando Kenneth Bowles desarrolló un sistema operativo y compilador de Pascal para su uso en mini y microcomputadoras, incluyendo un editor de texto, ensamblador y ligador. Este sistema es el Pascal UCSD (Universidad de California en San Diego) y se distribu­ yó a instituciones educativas así como también a las industrias. Desde 1984, versio­ Sólo fines educativos - FreeLibros

126

PARTE II: Lenguajes imperativos

nes interpretadas y el veloz Turbo Pascal han aumentado su popularidad. Sin em­ bargo, Wirth se ha dirigido hacia intereses más actuales, en particular hacia la pro­ gramación concurrente. El tenaz apego de Niklaus Wirth a una elegante y estricta disciplina de progra­ mación lo han convertido en uno de los principales arquitectos de la ciencia de la computación. En su Conferencia por el Premio Turing de 1984 señaló que "El tema [lenguajes de computadora] parecía estar compuesto de un uno por ciento de cien­ cia y 99 por ciento de hechicería, y esta mezcla tenía que cambiarse". El compromi­ so de Wirth con este cambio ha moldeado la estructura conceptual de las ciencias de la computación y continuará su influencia en los años por venir.

Filosofía y estructura

Los propósitos de Wirth al diseñar Pascal [Wirth,1971] fueron: 1. Permitir la expresión sistemática y precisa de conceptos y estructuras de pro­ gramación. 2. Permitir el desarrollo sistemático del programa. 3. Demostrar que un lenguaje con un rico conjunto de datos flexibles y estructura de programa facilita poder implementarlo con eficiencia. 4. Demostrar que el uso de un lenguaje independiente de la máquina con datos flexibles y estructuras de programa para escribir compiladores conduce a un incremento en la legibilidad, verificabilidad y consecuentemente su confiabilidad, sin pérdida de eficiencia. 5. Ayudar a ganar más conocimiento de los métodos de organización de grandes programas y administración de proyectos de software. 6. Tener facilidades extensivas de verificación de errores y, por tanto, que sea un buen vehículo para la enseñanza de la programación. De este modo, Pascal no fue previsto como un lenguaje de producción, sino como un lenguaje experimental y de enseñanza. La selección del DOD de Pascal como el fundamento para Ada nos da una evidencia del éxito de Wirth para lograr sus objetivos. Un programa de Pascal está estructurado en bloques, con la anidación permiti­ da a cualquier nivel, pero en una manera especial. Su forma es: prograi ñame (lista de identificadores de archivo); labe] declarations constant declarations type declarations variable definitions procedure y function definitions cuerpo del programa encerrado por beg1n...end.

(3.3.1)

La lista de definiciones de funciones y procedimientos puede ser realmente larga y tener separada la lista de variables del programa principal de su cuerpo. Uno pue­ Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

127

de necesitar mirar atrás varias páginas de código fuente para hallar precisamente cuál es el intervalo de i ndexType, o si x es de valor real o entero. Los bloques locales que encapsulan una sección de código relacionado no son parte de Pascal. Cada bloque debe ser un procedimiento, una función, el bloque de programa principal o un bloque de enunciado, tal como una construcción for o wh1 le. Esta estructura es simple, pero fomenta las variables globales o variables con alcance innecesaria­ mente extenso.

Tipificación de datos fuerte Pascal insiste (hasta cierto punto) en datos fuertemente tipificados, en los cuales las reglas de tipo están estrictamente impuestas (véase la sección 1.3). Cada variable, cada constante y cada procedimiento o función debe ser declarado antes de ser utilizado. La tipificación fuerte ayuda a evitar errores de programación y también facilita el trabajo del escritor del compilador. Los tipos de Pascal se adhieren a la definición de tipificación fuerte, con dos excepciones. Los registros variantes pueden incluir uniones libres en la parte va­ riante, y los procedimientos pasados como parámetros no son objetos tipificados. Ya hemos examinado el problema de los registros variantes de Pascal en el listado (1.3.14) de la sección 1.3. Un ejemplo de las facilidades de paso de procedimientos en Pascal se muestra en el listado (3.3.2). function realFunctionSum (a, b: integer;

(3.3.2)

function f (i: integer): real): real; var j: integer; sum: real; begin sum := 0; for j := a to b do sum :a sum + f(j); realFunctionSum := sum end;

Los parámetros de la función f anteriores son tipificados, pero las funciones mis­ mas no son tipos. Si deseamos una función de valor entero, tendríamos que definir una función diferente, i ntegerFuncti onSum, con el parámetro, function g(k: integer): integer;

Ada ha ampliado la noción de efectuar las mismas operaciones en objetos de tipos diferentes al proporcionar procedimientos y funciones genéricos. La regularidad en un lenguaje significa que no hay excepciones a las reglas. Considere de nuevo la forma del registro variante en Pascal: Sólo fines educativos - FreeLibros

128

PARTE n: Lenguajes imperativos

=

record

(3.3.3)

case

of : ; :

end;

Una característica irregular de Pascal es la terminación tanto de las construcciones record como case mediante el end único. Uno esperaría (y, de hecho, uno puede usar) dos end, uno para cada una. Un lenguaje regular es más fácil de recordar para los programadores y así fomenta una programación eficiente. Existen situaciones prácticas donde no todo lo que se necesita puede ser enu­ merado previamente. Una de ellas se encuentra en una lista ligada, donde las "li­ gas" lo mismo apuntan que forman parte de los registros, como se muestra en el listado (3.3.4). (3.3.4)

type link = ^listNode; listNode =

record

item: itemíype; next: link

end;

Esta característica irregular con el 1i stNode, del cual se hizo referencia antes de ser definido, parece inevitable. Ada aclara esto un poco al escribir la declaración mos­ trada en el listado (3.3.5). type Listjiode; — Declaración incompleta type Link Is access List_Node; type List_Node 1s record Item: Item_Type: Next: Link; end record;

(3.3.5)

El requerir la declaración incompleta de Li st_Node permite la regla de Ada de que cualquier tipo de datos mencionado debe haber sido previamente definido sin ex­ cepción. Mientras examinamos un fragmento de Ada, existen algunas otras cosas que notar también. Primero, la palabra clave 1s es sólo una finura (azúcar sintáctico) para =, el cual puede utilizarse de manera intercambiable con 1s o are. El end re­ cord; es también opcional; un simple end ; bastará. Sin embargo, los signos de punto y coma marcan un cambio con respecto a Pascal, donde son empleados para separar enunciados. Un enunciado de Ada siempre termina co n ;. Una de las motivaciones para este cambio con respecto a la regla en Pascal de que los signos de punto y coma sean utilizados para separar enunciados fue el error común del programador de Pascal de colocar u n ; antes de un else en un enunciado 1f.. .then...else. DebeSólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

129

riamos ser muy claros aquí acerca de la diferencia entre separar y terminar enuncia­ dos. Por ejemplo, un enunciado 1f...then...else se define como: i f <expression> then <$tatementl> else <statement2>

No es necesaria la separación de enunciados si enunciadol (Statementl) y enunciaao2 (Statement2) son ambos enunciados simples debido a que el else las separa. Sin etnbargo, si empleamos puntos y comas para terminar los enunciados, enun­ ciadol y enunciado2 terminarán cada uno c o n ;. Los diseñadores de Ada también pensaron en acercarnos al lenguaje natural, donde los enunciados representan ora­ ciones y deben tener alguna clase de puntuación. Como se analizó en el capítulo 0, ortogonalidad significa la capacidad de com­ binar libremente características de lenguaje independientes. Obviamente, las fun­ ciones de Pascal no son ortogonales, puesto que solamente pueden ser devueltos valores escalares o apuntador. También existen limitaciones sobre los parámetros, con archivos que siempre son pasados por referencia. Lo que es más, el método predeterminado de paso de parámetros en Pascal es por valor, de manera que procedure p(f: TipodeArchivo); causará un error, mientras que procedure p(var f: TipodeArchivo); nolohará. E J E R C I C I O S 3.3 1. Como un lenguaje de enseñanza, Pascal omitió algunas características comunes en lenguajes de producción. Por ejemplo, no hubo tipo de cadena integrado (aunque a menudo era soportado en las implementaciones). a. ¿Por qué puede estar ausente un tipo de cadena? b. Nombre algunas otras características comunes de un lenguaje de producción que estaban ausentes. 2. El uso de un; antes de un else en un enunciado 1f.. .then...else era un problema, pero podía ser colocado antes de un end. ¿Por qué esto no causaba también un pro­ blema? 3.4 ADA Ada fue diseñado a petición del Departamento de Defensa de Estados Unidos (DOD; Department of Defense) como un "lenguaje común para la programación de siste­ mas a gran escala y en tiempo real" [ANSI-1815A, 1983]. Es un lenguaje algorítmi­ co fuertemente tipificado con las estructuras usuales de control para iteración, recursión, ram ificación, procedimientos y funciones. También proporciona modularidad, donde los tipos de datos y procedimientos pueden ser empacados y compilados en forma separada. Para facilitar la programación en tiempo real, Ada proporciona tareas en paralelo de modelado y manejo de excepciones sin detener la ejecución del programa. El DOD estaba preocupado por la transportabilidad de programas y patrocinó el desarrollo de una definición de lenguaje estándar de Ada 83 [ANSI-1815A, 1983], la cual fue seguida por Ada 95 [ANSI/ISO-8652, 1995], Ada fue escrito con "tres intereses fundamentales: confiabilidad y mantenimiento del programa, programa­ ción como una actividad humana y eficiencia" [ANSI/ISO-8652,1995]. Sólo fines educativos - FreeLibros

130

PARTE H: Lenguajes imperativos

VIÑETA HISTÓRICA Ada A mediados de los setenta el DOD, el cual no se caracteriza por su restricción pre­ supuestaria, estaba gastando cerca de tres mil millones de dólares al año en soft­ ware. Estamos acostumbrados a ver tales cifras en relación con las fuerzas arma­ das, pero en este caso el costo era demasiado exagerado. Algo tenía que hacerse para disminuir el gasto en software. Una gran parte del problema era el hecho de que más de 450 diferentes lenguajes de programación o dialectos incompatibles del mismo lenguaje estaban siendo usados por los militares. Esto creaba problemas de transportabilidad limitada de máquina a máquina, reúso limitado de los procedi­ mientos en programas subsecuentes y confusión general. Había llegado el momen­ to de encontrar un lenguaje estándar en el que todos los programas para el departa­ mento fueran escritos. Puesto que alrededor de 56 por ciento del software adquirido era empleado para aplicaciones de computadora integradas o de misión crítica, se decidió que este lenguaje estándar debía estar encaminado hacia esas aplicaciones. "Gran parte de la programación de computadora hecha por los militares de Estados Unidos es usada para controlar hardware militar: tanques, aviones, bombas nucleares. Para controlar este hardware, un programa de computadora debe funcionar en Tiempo reaT; es decir, mientras el tanque está rodando o el avión se encuentra volando. Un piloto de un avión caza de la armada no puede esperar a que los resultados regre­ sen desde el centro de cómputo hasta el día siguiente" [Barón, 1986]. Los sistemas en tiempo real integrados están integrados dentro de un sistema mecánico más grande, tal como un robot o un avión sin piloto. En 1975, el DOD estableció el Grupo de Trabajo de Lenguaje de más Alto Orden (HOLWG; Higher-Order Language Working Group) para hallar un lenguaje estándar para aplicaciones de computadora integradas. El primer paso del HOLWG fue de­ sarrollar un conjunto de requerimientos para este lenguaje con sugerencias de la Armada, la Marina, la Fuerza Aérea, las universidades y la industria. De 1975 a 1979, a medida que el conjunto de requerimientos evolucionaba y crecía, el nombre dado al conjunto cambió, desde Strawman ("Hombre de paja"; 1975), pasando por Woodenman ("Hombre de madera"; 1975), Tinman ("Hombre de hojalata "; 1976), Ironman ("Hombre de hierro"; 1978) hasta Steelman ("Hombre de acero"; 1979). Este conjunto final Steelman contiene cerca de 100 requerimientos. Éstos restringieron el lenguaje "para tener construcciones de lenguaje con características especificadas en áreas tales como tipos de datos, estructuras de control, módulos, tareas y excepcio­ nes. Ciertos requerimientos globales acerca de 'legibilidad', 'generalidad no excesi­ va ', 'simplicidad' y 'verificabilidad' también fueron incluidos" [Wegner, 1980]. El siguiente paso dado por HOLWG fue el estudio de lenguajes existentes para ver si alguno de ellos podía satisfacer el conjunto de requerimientos. Después de un estudio intensivo de los 26 lenguajes candidatos existentes, se decidió que nin­ guno satisfacía todos los requerimientos, y que un nuevo lenguaje de vanguardia tendría que ser desarrollado. HOLWG recomendó que uno de estos lenguajes, ALGOL 68, Pascal o PL/I, debía utilizarse como fundamento para el diseño. Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

131

Se convocó a una competencia internacional de diseño del lenguaje. Diecisiete grupos enviaron propuestas, pero sólo cuatro fueron elegidos para un desarrollo adicional. Estos recibieron financiamiento por seis meses para producir un diseño de lenguaje preliminar. A cada grupo se le dio el nombre de un color para mante­ ner el anonimato y asegurar evaluaciones justas. Estos grupos fueron CII Honeywell Bull (Verde), Intermetrics (Rojo), Softech (Azul) y SRI International (Amarillo). Es interesante hacer notar que cada uno de estos grupos eligió Pascal como una base para sus diseños de lenguaje. Al término de los seis meses, los grupos Rojo y Verde fueron seleccionados como finalistas y se les dio un año más para el desarrollo. En 1979, el equipo Verde fue nombrado ganador. Este equipo, dirigido por Jean Ichbiah, dio un nuevo nombre al lenguaje Verde: "Ada". El nombre hacía honor a Augusta Ada Byron, condesa de Lovelace e hija del poeta inglés Lord Byron. "Ella fue la asistente, socia y patrocinadora de Charles Babbage, el matemático e inven­ tor de una máquina calculadora llamada la máquina analítica (Analytical Engine). Con la ayuda de Babbage, ella escribió un programa casi completo para calcular los números de Bemoulli hacia 1830. Debido a este esfuerzo, se puede decir que la condesa fue la primera programadora de computadoras del mundo" [Gehani, 1994]. El equipo de Jean Ichbiah completó el diseño de Ada en septiembre de 1980, sólo después de considerar más de siete mil comentarios y sugerencias de expertos en diseño de lenguajes de más de 15 países. En enero de 1983, Ada llegó a ser un estándar nacional estadounidense y militar. A partir de 1984, todo el software mili­ tar integrado tenía que estar programado en Ada. Aun cuando se había desarrollado un lenguaje estándar, el problema de ocu­ par demasiados lenguajes no fue resuelto. El DOD se dio cuenta de que si se desa­ rrollaban subconjuntos y superconjuntos de Ada y se permitía que retuvieran el nombre de Ada, volvería a aparecer el problema de la transportabilidad. Para ase­ gurar que esto no pasaría, el DOD tomó "la decisión sin precedente de registrar el nombre de 'Ada' como una marca registrada. Esto proporcionó la capacidad de controlar el uso de este nombre y garantizar que cualquier cosa llamada 'Ada' fue­ ra el lenguaje estándar. Es decir, los subconjuntos y superconjuntos de Ada no po­ drían ser llamados legalmente 'Ada'" [MacLennan, 1987], Además de esta marca registrada, el DOD estableció el proyecto de Validación de Compiladores de Ada (Ada Compiler Validation) para desarrollar un conjunto de pruebas estándar usadás para determinar si un compilador implementaba de hecho el lenguaje estándar. Este proceso incluye más de 2500 pruebas. El DOD ha renunciado a su marca regis­ trada, aunque tanto los contratos de la Defensa como los de la OTAN especifican el uso de compiladores Ada validados. Aunque fue diseñado para procesos integrados, Ada no está restringido a estas aplicaciones. Ichbiah ve un uso potencial para Ada tanto en los negocios como en la educación. Debido a sus ricas características de propósito general, Ada ha llega­ do a ser más popular y está siendo empleado como el lenguaje de programación para principiantes en gran número de colegios y universidades. Ada tiene sus problemas y sus críticos. Aunque está basado en el pequeño len­ guaje Pascal, Ada es enorme. Es más de tres veces el tamaño de Pascal. Este tamaño ha sido considerado el defecto más grande de Ada. Un lenguaje de tiempo real debería tener cerca de 100 por ciento de confiabilidad. ¿Puede un lenguaje comple­ jo como Ada satisfacer este criterio? Tony Hoare, uno de los críticos de Ada, excla­ Sólo fines educativos - FreeLibros

132

PARTE II: Lenguajes imperativos

ma con vehemencia, "No permitan que este lenguaje, en su estilo actual, se utilice en aplicaciones donde la confiabilidad es crítica, es decir, estaciones de energía nuclear, misiles crucero, sistemas de alerta temprana, sistemas de defensa de misiles antibalísticos. El próximo cohete que extravíe el rumbo como resultado de un error del lenguaje de programación puede no ser un cohete de exploración espacial en un inofensivo viaje hacia Venus: puede ser una cabeza nuclear que estalle sobre una de nuestras propias ciudades. Un lenguaje de programación no confiable cons­ tituye un riesgo mucho más grande para nuestro entorno y para nuestra sociedad que emplear autos poco seguros, pesticidas tóxicos o accidentes en estaciones de energía nuclear" [Barón, 1986]. La versión revisada del estándar Ada 83 se denomina Ada 95 [ANSI/ISO-8652, 1995]. Inicialmente se le conocía como Ada 9X debido a que en los noventa el últi­ mo dígito del año aún era desconocido en el momento de desarrollo. Aparte de la corrección de errores menores, están incluidas diversas mejoras, particularmente en las áreas de la programación orientada a objetos y en el procesamiento en para­ lelo y distribuido. Se consideraba importante mantener la compatibilidad hacia arriba, de modo que las herramientas y software existentes no llegaran a hacerse obsoletos. Sin embargo, las necesidades de software para sistemas de información son muy diferentes de aquellas para sistemas en tiempo real. Se espera que diver­ sas adiciones al lenguaje puedan encargarse de las necesidades específicas de dife­ rentes usuarios.

Organización del programa Un programa en Ada se compone de una o más unidades de programa, las cuales pueden ser compiladas de forma separada. Una unidad puede ser un subprograma, un paquete, una tarea o una unidad genérica. Cada unidad tendrá ordinariamente una especificación y un cuerpo. La especificación es información pública necesaria para ejecutar la unidad, mientras que el cuerpo puede estar oculto al usuario y contiene enunciados ejecutables. Un subprograma puede ser un procedure (procedimiento) o una functlon (fun­ ción). Un programa necesita un procedimiento principal para ejecutarse, el cual llamará otras unidades del programa. Por ejemplo, supóngase que deseamos im­ primir la fecha, haciendo uso de un procedimiento principal llamado P r i nt_Date, como el que se muestra en el listado (3.4.1). with

Calendar, Integer_IO, Text_I0;

procedure Print_Date is use Calendar, Integer_IO, Text_I0; Today: Time; begin Today := Clock; Text_IO.Put("The date is: "); Integer_IO.Put(Month(Today)); Text_I0.Put('7"); Integer_IO.Put(Day(Today)); T e x t _ I 0 . P u t ( 7 " ) ; Integer_IO.Put(Year(Today)); end;

Sólo fines educativos - FreeLibros

(3.4.1)

CAPÍTULO 3: Estructura en bloques

133

Se utilizan tres paquetes predefinidos con esta unidad de procedimiento: Calendar, Integer_IO y Text_I0. El tipo Ti me está declarado en el paquete Calendar. Parte de la especificación para Cal enda r se muestra en el listado (3.4.2). (3.4.2)

package Calendar 1s type Time 1s prívate; subtype subtype subtype subtype

1s 1s 1s 1s

Year_Number Month_Number Oayjumber Day_Duration

Integer Integer Integer Duration

range 1901 .. 2099: range 1 .. 12; range 1 .. 31; range 0.0 .. 86_400.0;

functlon Clock return Time; functlon functlon functlon functlon

Year Month Day Seconds

(Date: (Date: (Date: (Date:

Time) return Time) return Time) return Time) return

Yearjumber; Month_Number¡ Day_Number; Day_Duration;

functlon Time_0f (Year : Year_Number; Month : Month_Number; Day : Day_Number; Seconds : Day_Duration) return Time; Time_Error: exceptlon; — puede ser levantada por Time__0f; prívate — implementación dependiente de la especificación del tipo para Time end;

Esta especificación sería seguida por un cuerpo de paquete dependiente de la implementación definiendo cada función en la especificación, como se plantea en el listado (3.4.3). Obsérvese que un grupo de funciones y tipos relacionados está empacado junto en Calendar. Puesto que Time es un tipo privado, sólo se puede tener acceso a él a través de las funciones Clock, Year, Month, Day, Seconds y Time_0f. Es a través de tipos privados que Ada soporta tipos de datos abstractos. El tipo privado limitado es incluso más restrictivo que el tipo privado. Los valo­ res pueden ser asignados a tipos privados, y las variables pueden ser probadas por igualdad o desigualdad. Si una variable es declarada para ser privada limitada, incluso estas operaciones deben ser definidas explícitamente. package body is Calendar functlon Clock return Time 1s begin ... end; function Year (Date: Time) return Year_Number is begin ... end; end Calendar;

Sólo fines educativos - FreeLibros

(3.4.3)

134

PARTE n: Lenguajes imperativos

Dejamos el análisis de las tareas hasta el capítulo 5, donde combinaremos una con­ sideración de paradigmas de programación distribuidos y concurrentes. Ada está estructurado en bloques, cuyos bloques están formados por enuncia­ dos como en el listado (3.4.4). (3.4.4)

declare — declaraciones de tipo y variable aquí begln — las declaraciones van aquí end;

Como en ALGOL, los bloques pueden estar anidados en cualquier nivel. Así las variables declaradas en un bloque exterior pueden hacerse invisibles en un bloque interior si se declaran de nuevo, como se muestra en la figura 3.4.1. Hay una diferencia entre alcance y visibilidad. Una variable existe a lo largo de su alcance, pero puede no ser accesible; es decir, visible. Aunque la Nexterior es invisible en B1 ock2, no deja de existir. De esta manera, B1 ock2 está dentro del alcan­ ce de la Nexterior. De hecho, Ada permite la referencia a la Nexterior invisible en el bloque interno mediante el uso de B1 ockl. N. Un uso indiscriminado de Nen B1 ock2 tiene el mismo resultado de utilizar B1 ock2. N. Los bloques sirven a otros propósitos aparte de organizar las unidades de pro­ grama. Además de controlar la visibilidad, se encargan de los niveles de control. Uno puede dejar un bloque o un ciclo empleando un enunciado goto, o dejar un ciclo hasta el bloque inmediatamente circundante utilizando un ex1t. Ninguno puede ser usado para salir de un subprograma, pero pueden incluirse tantos return como se quiera tanto en una función como en un procedimiento. El goto está algo restringido, pero se incluyó para facilitar la traducción de programas desde otros lenguajes hasta Ada o la generación automática de programas Ada. Los goto son muy notables en programas Ada, pues las etiquetas están marcadas por corchetes, por ejemplo, <

N: Integer := 0; begin

Bloque2 declare

N: Integer; begin

N := 2; end Bloque2; end Bloquel;

Alcance de Bloquel

Visibilidad de la N exterior

Alcance de Bloque2

FIGURA 3.4.1

Alcance y visibilidad para bloques de Ada Sólo fines educativos - FreeLibros

Visibilidad de la N interior

CAPÍTULO 3: Estructura en bloques

135

Los paquetes y las tareas interaccionan de manera diferente. Un paquete es una unidad pasiva que se tiene en cuenta o es realizada (llamada elaborated —ela­ borada— en Ada) en el ámbito donde es declarada. Las tareas dependen del blo­ que o subprograma en el cual se ejecutan y todas deben completarse antes de que la unidad de la cual dependen se ejecute.

Tipos

Ada tiene tanto tipos escalares como estructurados, como se muestra en la figura 3.4.2. Entre los reales, existen dos tipos: F1 oat, que puede especificar precisión rela­ tiva; y Fi xed, para situaciones que requieren precisión absoluta. La exactitud relati­ va se define en términos de dígitos significativos; esto es, 3.46 tiene la misma exactitud relativa que 0.000346 o 3,460,000,000,000, tres dígitos significativos. Las declaraciones para reales de tipo flotante se demuestran en el listado (3.4.5), type Area_Measure is digits 7;

(3.4.5)

type Person_Height is digits 4 range 0.5 .. 9.0;

en el cual las variables tienen siete y cuatro dígitos significativos, respectivamente. Los reales fijos son declarados utilizando la palabra reservada delta, que indica el intervalo de error permitido. type Money is delta 0.005 range -1000.0 .. 10_000.0;

FIGURA 3.4.2

Tipos escalares y estructurados de Ada Sólo fines educativos - FreeLibros

136

PARTE II: Lenguajes imperativos

El cálculo con reales fijos es más lento que con tipos flotantes, pero es necesario en algunas situaciones. No todos los tipos de Ada están construidos directamente en el lenguaje. Los tipos Boolean, I nt ege r, F l o a t , C h a ra c te r , N a t u r a l , P o s i t i v e y S t r i n g están defi­ nidos junto con operaciones sobre ellos en una especificación de paquete llamada Standard, la cual es una implementación dependiente pero requerida como parte de cualquier compilador de Ada. Standa rd está siempre disponible a todo lo largo del alcance de cualquier programa. Ada también tiene tres tipos anónimos: universal-integer, universal-float y universal-fixed. Las literales y las constantes son de tipo universal, tales como: PI: constant := 3.141_592_65;

Si Nes de tipo flotante dlgíts 7, entonces la asignación de N 2.0 + PI; conver­ tirá tanto la literal 2.0 como la constante PI de tipos universales a d f g lt s 7 yN = 5.141593. (Obsérvese que el resultado está redondeado, más que truncado.) Las conversiones automáticas no están permitidas en Ada, así que la expresión 3.6 + 5 tendría que ser escrita ya sea 3.6 + F1 oatC 5) o Integer(3.6) + 5. En el primero de los casos, el resultado sería 8.6, y en el segundo, 9. Sin embargo, los enteros univer­ sales y los reales universales pueden combinarse para las operaciones * y /, con un resultado de real universal. El resultado de * o / operando sobre dos tipos fijos devuelve un valor universal fijo, con exactitud dependiente de la implementación, delta. Ada incluye cierto número de operadores útiles, llamados atributos, para escalares. Si P es del tipo Person Height, como se declaró en el listado (3.4.5), el atributo P ' F i r s t es 0.5, P ’ Last es 9.0 y P ’ D i g i t s es 4. No se proporciona un tipo conjunto en Ada (véase la figura 3.4.2), pero los operadores integrados sobre arre­ glos facilitan la implementación, como se muestra en el listado (3.4.6). type Set

is array (Positive range <>) of Boolean;

(3.4.6)

subtype Color is Set (1..3); Red

: constant Col or

Yellow

: constant Col or

= (T F,F); ■ (F T,F);

Blue

: constant Color

Orange

: constant Col or

■ (F F,T); = (T T.F);

Purple

: constant Color

= (T F,T);

Green

: constant Col or

= (F T.T);

White

: constant Color

- (F F.F);

B1 ack

: constant Color

= (T T.T);

C

: Color;

El signo O , llamado box, indica que el intervalo será llenado posteriormente. Si asignamos C Red and Y e l l ow:, el color resultante será C = Wh i te. Si asignamos C := Red or Ye 11 ow;, obtenemos C = Orange. De manera similar, not Green = Red y Orange xor Yel low = Red, mientras que Orange xor Blue = Black, or representa la unión de conjuntos; and es la intersección de conjuntos, y xor es la diferencia simétrica de conjuntos: los elementos que están en uno, pero no en ambos, con­ juntos. Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

137

String (cadena) es un tipo de arreglo predefinido: array (Positive range <>) of Character;

S t r i ng puede ser utilizado solamente para constantes y determina la longitud de una cadena constante en asignación. Tanto S t r i ng como Posi t i ve son tipos defini­ dos en el paquete S t a n d a r d . Además del paquete requerido S t a n d a r d , una

implementación válida para Ada debe proporcionar también las unidades de Li­ brería Cal endar, I O_Exceptions, D i r e c t _ I 0 , Low_Level_IO, S e q u e n t i a l _ I 0 , System, Text_I0, Unchecked_Conversion y Unche ckec LD eal l oc at ion. Los registros de Ada son muy parecidos a los de Pascal, con unos cuantos ador­ nos adicionales. Como en Pascal, un registro puede tener solamente una parte va­ riante, la cual debe ser el último componente, como se muestra en el listado (3.4.7). type Device 1s (p rin te r, disk, drum); type State 1s (Open, closed);

(3.4.7)

type Peripheral (Unit: Device := Disk) 1s — Disk es el predeterminado record Status: State:

case Unit 1s when p rin te r ->

--componente variante

Line_Count: Integer range 1 .. PageJSize;

when others => C y lin d e r : Cylin derjn dex; Track

: Track_Number;

end case; end record; P e r i p h e r a l es un registro discriminado (discriminated record) con tres posibles subtipos dependiendo del discriminante Unit: P e r i p h e r a l ( P r i n t e r ), P e r i p h e r a l ( D i s k ) o P e r i p h e r a l (Drum). Todos los subtipos tienen en común el componente Stat us. D is k y Drum también tienen Cyl i nd e r y Track en común, mientras que Pr 1n t e r tiene un componente Li ne_Count. Si se declara que una variable es del tipo P e r i p h e r a l , sin discriminante, Di sk es el valor predeterminado de Unit.

Los arreglos y registros de Ada pueden también ser asignados como agregados. Para nuestro tipo Set del listado (3.4.6) podríamos dar valores iniciales usando: S: Set := (F,F,F);

(3.4.8)

S: Set := (1 .. 3 => F);

Para P e r i pheral en el listado (3.4.7), podríamos haber agregado asignaciones tales como las mostradas en el listado (3.4.9). P: Peripheral

:= (Printer,Open,1);

P: Peripheral

:= (Disk, Open, 1, 0);

P: Peripheral

:= (Drum, Closed, 0, 0);

Sólo fines educativos - FreeLibros

(3.4.9)

138

PARTE II: Lenguajes imperativos

Un arreglo de dimensión 3 X 3 podría ser declarado en cualquiera de las for­ mas mostradas en el listado (3.4.10). A: array

(0..2.0..2) of Real

:= ((0.0,0.0,0.0),

(3.4.10)

(0.0,0.0,0.0), (0.0,0.0,0.0)); A: array

(0--2.0..2) of Real

:= (0..2 =>(0.0,0.0,0.0));

A: array

(0..2.0..2) of Real

:= (0..2 => (0..2 => 0.0));

Rebanadas de arreglos de una dim ensión tam bién pueden ser asignadas, como en: B: array

(0..2) of Integer := (3, 4, 5);

(3.4.11)

C := B( 1 ..2);

Aquí B'First = 0, mientras C'First = 1. PeroB'Last - C' La st = 2, donde Fi rst y Last son atributos de arreglo. El resultado se muestra en la figura 3.4.3. Para evitar errores, los programas Ada por lo regular hacen la iteración sobre los arreglos desde Fi rst hasta Last, en lugar de ir desde 1 hasta N. Como se mencionó en la sección 1.3, Ada soporta un tipo de arreglo no restrin­ gido, que permite que los límites de arreglo se designen en tiempo de ejecución. En la declaración type List is array (Integer range <>) of Integer;

los límites de la caja < > deben llenarse cuando declaramos elementos de tipo List, tales como: L: List(1 .. 10);

También es posible crear subtipos del tipo Li st, que pueden luego ser utiliza­ dos en declaraciones: (3.4.12)

subtype Li st_10 is Li s t (1 .. 10); L: List 10;

B:

C:

FIGURA 3.4.3 Rebanada de Ada

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

139

Además de los tipos escalares y estructurados, Ada proporciona un tipo access para direcciones de localidad de almacenamiento. Como ocurre con el tipo apunta­ dor de Pascal, un tipo access debe tener acceso al almacenamiento de un tipo dis­ tinto, como se muestra en el listado (3.4.13). (3.4.13)

type Node; type List is access Node; type Node is record Item: String(l..20); Next: List; end record; Grocery_List, Jobs_List, Name_List, Temp: List;

Grocery L i s t , Jobs L i s t y Ñame L i s t tendrán todos el valor inicial n u i l . Ésta es la

única situación donde Ada asigna valores iniciales a variables sin una asignación programada explícita. Los tipos access se encargan de la asignación dinámica usando new L i s t ' ( " e g g s " ) ; asignará una la función new. D e este modo, Grocery L i s t nueva localidad de memoria accesada por Grocery L i s t , con Grocery L i s t . Item = "eggs". Podríamos haber escrito Grocery L i s t new L i s t ' ( " e g g s " , n u l l ) ; con el mismo efecto. ¿Ve usted por qué? Los ejemplos de esta notación están incluidos en los enunciados Ada mostrados en el listado (3.4.14). declare

P rio r, Temp, Bought: L is t ;

(3 .4.14)

Grocery

: S t r i n g ( l . . Length);

L

: Length;

begin Grocery_List := new L is t * ('"'); P rior

Grocery_List;

Get_Line (Grocery, L);

whlle (Grocery / - "That's a11") Temp new List'(G rocery);

- - L cuenta la longitud de Grocery

loop

— Hace la l i s t a de Grocery

Prior.Next := Temp; P rio r :« Temp; Get_Line (Grocery, L);

end loop; P rior Temp

Grocery_List; Grocery_Li s t . Next;

whlle Temp / - nuil loop If Store_Has(Temp.Item) then

— l i s t a vacía o f i n de l i s t a

Buy(Temp.Item); Bought :- Temp; Temp :- Temp.Next; Prior.Next := Bought.Next; Bought := n u i l ;

else P rior := Prior.Next; Temp := Temp.Next;

end If; end loop;

Sólo fines educativos - FreeLibros

140

PARTE n: Lenguajes imperativos

En el listado (3.4.14), los nodos para la lista de comestibles (Grocery) original com­ pleta están todavía asignados, aun cuando después de que "compramos" ( Buy ) un elemento, el acceso al mismo se inicializa en nuil. Aunque no se requiere, algunos compiladores de Ada incluyen un recolector de basura, que de manera periódica devuelve memoria de almacenamiento a la cual ya no se tiene acceso hacia la bandeja de almacenamiento disponible. Para el programador valiente, Ada proporciona un procedimiento genérico llamado uncheckecLdeal 1oca t i on, que puede devolver memoria a la pila, de manera similar al procedimiento di s pose de Pascal. Sin embargo, el programador que haga esto no tiene garantías de parte de Ada contra los apuntadores colgantes (dangling pointers), como describimos en la sección 1.1, y por lo tanto corre bajo su responsabilidad. Ada 95 tiene características incluidas para soportar programación orientada a objetos. Puesto que éste es el paradigma descrito en el capítulo 4, dichas caracterís­ ticas de lenguaje se describirán allí. La facilidad genérica El diccionario de sinónimos incluido en un popular procesador de texto enumera "común", "general" y "universal" como sinónimos para genérico. Ya hemos visto el sabor de las facilidades genéricas de Ada en el uso de o , o caja, para límites de arreglo. Definimos Set en el listado (3.4.6) como un tipo de arreglo general, con índices que serán determinados a medida que surjan las necesidades. St r i n g es también un tipo de arreglo genérico predefinido: subtype Positive is Integer range 1 .. Integer'Last;

(3.4.15)

type String is array(Positive range <>) of Character;

En Ada, cuando utilizamos la palabra reservada genéri co (generlc), quere­ mos decir que el tipo (type), procedimiento (procedure) o paquete (package), más que el i nterval o ( range), está aún por determinarse. Podríamos haber especi­ ficado Sets genéricamente, como se muestra en el listado (3.4.16).10 generlc type Base Is (<>); package Sets Is type Set Is array (Base) of Boolean; type Elements Is array (Natural range o ) of Base; functlon Create_Set (A: Elements) return Set; functlon (A, B: Set) return Set; — intersección functlon "+" (A, B: Set) return Set; — unión end Sets;

(3.4.16)

package body Sets Is -define todas las funciones aquí end Sets; 10 Una versión más completa de una definición de conjunto genérica puede ser hallada en [Barnes,

1996].

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

141

Ada es un lenguaje fuertemente tipificado, de modo que una unidad genérica no se compila cuando se encuentra primero, sino cuando se instancia. Para nuestro ejemplo anterior, podemos instanciar Color como se muestra en el listado (3.4.17). type Primary is (R, Y, B);

(3.4.17)

package Color is new Sets (Base => Primary); E: Elements; C, Red, Yellow, Blue, Orange, Purple, Green, White, Black: Set;

Es esta palabra reservada new la que dispara la compilación de la unidad genérica, con el tipo P ri ma ry llenado para O . Puesto que Elements son arreglos de tipo P r i ma ry, toman la forma ( R), ( R , Y ), etcétera. Podemos asignar colores como en el listado (3.4.18). E := (R); Red := M a k e j e t (E);

(3.4.18)

Yellow := Make_Set ((Y)); Orange := Make_Set ((R, Y)); White := Make_Set (()); Black := Make_Set ((R, Y, B));

Los otros colores podrían asignarse de manera similar. Aplicando los operadores, Red + Ye ll ow = Orange y Orange * Red = Red. La facilidad genérica toma en cuenta la reutilización del software y la restric­ ción de la visibilidad de partes del programa cuando se combina con declaraciones [Mmlted] prívate.

Excepciones Como se mencionó en la sección 2.2, una excepción es un evento inesperado en la ejecución del programa que causaría ordinariamente un error. Un manejador de excepciones es una unidad de programa que se invoca solamente si ocurre la ex­ cepción. Ada tiene cinco excepciones predefinidas: Constrai nt_Error Numeric__Error Program__Error Storage^Error Taski ng__Error

La primera ocurre si se violan las restricciones, tal como exceder los límites del arreglo o utilizar la componente variante equivocada de un registro variante. Los errores numéricos son cosas tales como intentar dividir entre cero o la incapacidad del sistema para proporcionar un valor suficientemente preciso para un tipo fijo. Los errores de programa ocurren cuando se intenta llamar subprogramas que aún no han sido elaborados, y los errores de almacenamiento ocurren cuando la memo­ ria se agota. Las tareas pueden estar ejecutándose de manera concurrente. El error más común aquí ocurre cuando dos o más tareas intentan comunicarse sin éxito. Estudiaremos más acerca de esto en el capítulo 6. Sólo fines educativos - FreeLibros

142

PARTE II: Lenguajes imperativos

Los diseñadores de Ada extendieron la utilidad de excepción de dos maneras. Primero, uno puede definir, suscitar y manejar sus propias excepciones. Segundo, las excepciones pueden ser propagadas a través de la cadena dinámica de ejecu­ ción hasta que se encuentre un manejador. Éstas se demuestran en el listado (3.4.19). (3.4.19)

Blockl:

declare M: Integer;

functlon F return Integer 1s E: exceptlon; N: Integer;

begln raíse E;

— la excepción ocurre aquí

return N; end F; begln M

B1oquel llama F

F;

-continua

exceptlon when E «> begln

■manejador de excepción aquí

Putí"Problema con E!");

return 0; end exceptlon; end Blockl;

Blockl comienza su ejecución llamando a la función F, donde se suscita una excep­ ción E. Las excepciones pueden ser suscitadas por el mismo programa así como también llegar a ocurrir de manera automática. Puesto que F no tiene un manejador de excepciones para E, F se termina y el control regresa a B1 ockl. Puesto que E fue propagada hasta B1 ockl, su manejador de excepciones se encarga de la excepción. Observe que el manejador incluye un enunciado return de modo que Mtendrá un valor y la ejecución puede continuar en la línea de código marcada —conti nua. Por supuesto, el manejador podría haber sido incluido al final de la función F misma, donde siempre sería manejada en la misma forma. En este ejemplo, se maneja como se especificó por el B1 ockl, puesto que F fue llamada desde B1 ockl. Si es llamada desde un entorno diferente, el manejo podría haber sido diferente. El entorno de soporte para programación en Ada (APSE) Además de los documentos sucesivos de requerimientos del lenguaje (Strawman, Woodenman, Tinman, Ironman y Steelman), el DOD publicó Stoneman en 1980, el cual especificó requerimientos para un entorno de soporte para programación en Ada (APSE; Ada Programming Support Environment). El propósito del APSE era "dar soporte al desarrollo y mantenimiento del software de aplicaciones en Ada a

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

143

lo largo de su ciclo de vida, con énfasis particular en el software para aplicaciones de computadora incrustadas" [Booch, 1986]. El modelo más común para este ciclo es el modelo de cascada (waterfall model) mostrado en la figura 3.4.4, el cual fue presentado por primera vez en [Royce, 1987]. Cada fase puede involucrar diferente personal, herramientas de programación y de depuración, maquinaria, etcétera. Fue el deseo del DOD estandarizar todas es­ tas actividades tanto como fuera posible para reducir costos y m ejorar la transportabilidad tanto de programas como de programadores. La figura 3.4.5 ilustra las diversas partes del APSE. El anillo más interno, pa­ sando el sistema operativo anfitrión, es el KAPSE, o kemel (núcleo) de APSE. EJ KAPSE tiene interfaz con la máquina anfitrión y diferirá de una máquina a otra. Teóricamente, un nuevo KAPSE será todo lo que se necesite para transportar soft­ ware a una máquina diferente. El MAPSE es el mínimo APSE, que proporciona herramientas comunes, incluyendo un editor, un compilador, un ligador, interfaces de periféricos y diversas herramientas para análisis en tiempo de ejecución. Cual­ quier sistema Ada debe proporcionar estas herramientas. El APSE completo no está definido con precisión, pero incluirá herramientas para administrar bases de datos, hacer interfaz con pantallas gráficas y mantener software, entre otras. E J E R C I C I O S 3.4 1. Los requerimientos del paquete Standa rd sugieren que se proporcionen tipos reales adicionales, tales como Sh ort _Fl oat y Long_Float. Muchos implementadores tam­ bién proporcionan un tipo R e a l. Si usted estuviera escribiendo un compilador Ada, ¿qué sugeriría para el tipo Real ? ¿Por qué? 2. ¿Por qué el resultado de multiplicar o dividir dos reales fijos da un valor universal fijo, en lugar de un valor del mismo tipo que uno de los operandos? 3. Ada permite sobrecarga de operadores para varios tipos. Podríamos definir la inter­ sección de conjuntos utilizando como se muestra en el listado (3.4.20).

FIGURA 3.4.4 Modelo de cascada para el ciclo de vida del software

Sólo fines educativos - FreeLibros

144

PARTE n: Lenguajes imperativos

FIGURA 3.4.5 Reproducida con permiso de Wolf, M. I., Babich, W., Simpson, R. Tholl, R. y Weissman, L. (1981). El sistema de lenguaje Ada. Computer 1 4 (6). © IEEE.

function

(A, B:

Color)

return

Color

1s

(3.4.20)

begln return A and B; end

¿Cómo podría definir un operador para diferencia de conjuntos en Ada haciendo uso de not, and, or y xor? (Por ejemplo, {1,2,3} - {2} = {1,3}). 4. Describa las diferencias entre, y racionales para, las tres diferentes maneras de inte­ rrumpir la ejecución secuencial: exit, goto y return. ¿Cómo difieren estos procesos de una excepción? 5. Suponga que deseamos volver a definir conjuntos como listas ligadas en vez de como arreglos. ¿Por qué nos serviría mejor haber elegido un tipo privad o 1 im ita do (1 i mi ted p r i v a t e ) para un conjunto, en lugar de un t i p o p riv a d o ( p r i v a t e ) ? 6. Siguiendo las declaraciones del listado (3.4.14), podemos crear el primer nodo de Grocery_List con el enunciado Grocery_Li st :« new List'C" Suponga que Node ha sido declarado como en el listado (3.4.21).

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

type Node is record

145

(3.4.21)

Item: String(l .. 4); Next: List; end record;

Podríamos haber asignado GroceryJ_ist :« new List;, seguido por Grocery_List. Item ¿Por qué este método de asignación habría sido inválido con Item declarado S t rin g? 7. ¿Puede usted pensar en un ejemplo donde la forma de cortocircuito or else funcio­ ne mejor que el or usual, donde ambas expresiones se evalúan siempre? Cuando se usa A or else B, B no se evalúa si A se evalúa como True. 8. Considere el programa recursivo del listado (3.4.22), donde se suscita una excepción [ANSI-1815A, 1983]: function Factorial(N: Positive) return Float is

(3.4.22)

begin if N = 0 then return 1.0; else return Float(N) * Factorial(N-l); end if; exception when N u m e r i c E r r o r => return Float'Safe_Large; end Factorial;

Silallam adaes Factorial (100) y F l o a t ’Sa feL arg e - 231 - 2 147 483 648.0, ¿cuán­ tas veces se alcanzará un e rror n u m é r i c o (Numeric Error)? ¿Q uévalor se devolverá finalmente? 9. Un programa transportable es aquel que puede ejecutarse en varias máquinas. ¿Qué se entiende por programador transportable?

L A B O R A TO RI O 3.1: BLOQUES: ADA /PA SCA L O bjetivos (Los laboratorios pueden encontrarse en el Instructoras Manual.) 1. Explorar los diferentes bloques disponibles, sin incluir módulos o paquetes (nom­ brados, sin nombrar, enunciados en bloque, procedimientos, funciones y sistema suministrado). 2. Intentar diferentes esquemas de variables locales/globales en bloques anidados. 3. Trazar un procedimiento o función recursivo simple que utilice tanto variables loca­ les como globales. 4. Observar y manejar excepciones que ocurran en un bloque interno, pero propagadas al bloque exterior si es posible. (En Pascal, esto será un manejador de interrupción dependiente de la implementación.)

3.5 C Como vimos en la figura 3.0.1, C tiene un linaje diferente de otros lenguajes tipo ALGOL. Aquí examinaremos más de cerca la ramificación CPL-BCPL-C. C++ y Java se analizarán en el capítulo 4.

Sólo fines educativos - FreeLibros

146

PARTE n: Lenguajes imperativos

El lenguaje de programación combinado (CPL; Combined Programming Language) fue ideado en la década posterior al Informe ALGOL 60 para proporcio­ nar un lenguaje más cercano al hardware de cómputo. Se pretendió que fuera un medio para resolver todo tipo de problemas: numéricos, no numéricos y sistemas. En contraste con los principios de Pascal destinados a fomentar programas estructurados confiables, CPL fue destinado a permitir un intervalo de aplicacio­ nes tan amplio como fuera posible. Su sucesor, C, se mantuvo pequeño y flexible de manera que pudiera ejecutarse en gran variedad de máquinas, con característi­ cas no implementadas tales como E/S y procesamiento de cadenas desarrolladas fácilmente en el sitio. Mientras que los lenguajes tipo ALGOL son fuertemente tipificados, C viene del lenguaje sin tipos BCPL, donde la memoria de alma­ cenamiento es vista como cadenas de bits, en lugar de enteros, reales, caracteres, etcétera.

VIÑETA HISTÓRICA

El dúo dinámico: Dennis Ritchie y Kenneth Thompson C (junto con su extensión C++) ha llegado a ser uno de los lenguajes de programa­ ción más populares en todo el mundo. Es famoso por su sorprendente dualidad. Es tanto un lenguaje de programación de alto nivel como uno de bajo nivel. Tam­ bién es tanto para propósitos especiales como de propósito general. A diferencia de algunos antes que él, como los creadores de ALGOL, Dennis Ritchie no intentaba desarrollar un lenguaje de programación popular. Él quería diseñar un mejor siste­ ma operativo. Remontémonos una vez más a los sesenta: Ritchie era un importante físico de Harvard. Después de completar su trabajo universitario, se especializó en el estudio de las matemáticas, como la mayoría de los pioneros en la ciencia de la compu­ tación. En 1968, llegó a trabajar para los Laboratorios Bell (actualmente Lucent Technology) e hizo equipo con Ken Thompson. Thompson, quien había crecido en­ tre radios y ajedrez, recibió sus títulos universitario y de posgrado en ingeniería eléc­ trica de la Universidad de California en Berkeley. A los dos se les encargó una tarea apremiante: pensar acerca de problemas interesantes en la ciencia de la compu­ tación. El dúo comenzó a pensar acerca de los SO (sistemas operativos). En aquel tiempo, los científicos de Bell estaban experimentando con un siste­ ma operativo llam ado MULTiplexed Inform ation and Com puting Service (MULTTCS). Este sistema multiusuario de tiempo compartido llegó a ser el amigo instantáneo de los programadores que estaban acostumbrados a hacer las cosas de la manera difícil. En lugar de dar una pila de tarjetas perforadas a un operador y esperar una hora o más por una impresión de los resultados, MULTICS permitía a los usuarios escribir comandos en un teclado y obtener una respuesta instantánea. Sin embargo, había un gran problema: era muy caro ejecutar MULTICS. Todos es­ taban utilizándolo, y eso costaba dinero. Los Laboratorios Bell, para la consterna­ ción de muchos, decidieron abandonar MULTICS. Pero Ritchie y Thompson no podían hacerse a la idea de dejar de usarlo para hacer las cosas otra vez a la manera

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

147

antigua. Decidieron diseñar un sistema justo para ellos mismos y sus colegas pro­ gramadores en el laboratorio. Este sistema operativo pronto sería conocido por el mundo como UNIX, como parodia del nombre MULTICS. Thompson, emocionado con el nuevo proyecto, lo propuso a sus superiores. Después del desastre financiero de MULTICS, se mostraban cautelosos acerca de otros proyectos de SO, por lo que fue rechazado. Rehusando desalentarse, él en­ contró una vieja DEC PDP-7 obsoleta y comenzó a trabajar con Dennis Ritchie. El trabajo no era fácil, pero los dos pronto tuvieron un SO completo en sus manos. Sabían que era improbable que su trabajo fuera utilizable por otros "en tanto se ejecutara solamente en una anticuada computadora de Ja que sólo existían unas cuantas " [Slater, 1987]. Para tener en sus manos una computadora moderna y actualizada, Thompson emitió una propuesta para desarrollar un sistema de edi­ ción para tareas de oficina. Fue aprobada, y Ritchie y Thompson tuvieron una PDP-11 para trabajar con ella. En 1971, UNIX fue completado, y su uso dentro de los Laboratorios Bell comenzó a crecer, comenzando con el departamento de pa­ tentes. Sin embargo, aparecieron algunos problemas. UNIX había sido escrito en lenguaje ensamblador, lo que significaba que no podía transportarse a otras má­ quinas que no fueran la PDP-11. En los sesenta había dos tipos de lenguajes. Los lenguajes ensambladores de bajo nivel permitían a un programador controlar una computadora en particular, puesto que él o ella podían manipular los bits individuales de la memoria. Los lenguajes de alto nivel eran más fáciles de utilizar y estaban implementados en gran variedad de hardware. Un programador no necesitaba preocuparse acerca de los detalles desordenados de bajo nivel y podía concentrarse en un buen diseño algorítmico. Un comité conjunto de la Unidad de Cómputo de la Universidad de Londres y el Laboratorio de Matemáticas de la Universidad en Cambridge decidie­ ron diseñar un lenguaje que fuera tanto de alto como de bajo nivel. Sería lo sufi­ cientemente alto como para no estar atado a una computadora en particular, pero lo suficientemente bajo para permitir la manipulación de bits específicos. El len­ guaje resultante fue llamado lenguaje de programación combinado (CPL; Combined Programming Language). Nunca fue popular, puesto que era un lenguaje muy ex­ tenso y difícil, pero una versión recortada, Basic CPL (BCPL) atrajo la atención de algunos usuarios. De regreso en los Laboratorios Bell, Thompson creó una versión aún más pe­ queña de BCPL, llamada B (tal vez simbolizando que él sólo necesitaba parte del BCPL). Ritchie transformó posteriormente a B en C al restaurar algunas de las ca­ racterísticas del CPL, tales como la rica tipificación de datos. UNIX fue entonces vuelto a escribir en C. La transportabilidad resultante hizo a UNIX un estándar de la industria de la computación a mediados de los ochenta. Ritchie se niega a resol­ ver el misterio del nombre C. Él deja a nuestro criterio decidir "si fue siguiendo a Thompson en extraer la siguiente letra de nombre BCPL o al tomar C como la si­ guiente letra en el alfabeto después de B" [Barón, 1986]. C, como su ancestro CPL, es tanto de bajo como de alto nivel. Es un lenguaje de propósito específico diseñado para la programación de sistemas; es decir, UNIX, y también de propósito general. Ritchie señala que "C es un lenguaje de programa­ ción de propósito general... Aunque ha sido llamado un 'lenguaje de progra­ mación para sistemas' debido a que es útil para escribir sistemas operativos, ha Sólo fines educativos - FreeLibros

148

PARTE II: Lenguajes imperativos

sido empleado igualmente bien para escribir importantes programas numéricos, de procesamiento de textos y de bases de datos" [Kemighan, 1978]. C es conocido como un lenguaje de programadores, escrito por un programa­ dor para programadores. Esto es evidente cuando se examinan algunas de las ca­ racterísticas de C, las cuales son breves en lugar de ser bonitas. Por ejemplo, en lugar de begin...end, se utilizan paréntesis de llave {...}. Esto se hace para una programación más rápida, pero también crea un código menos legible. Otro ejem­ plo de la orientación de C hacia los programadores experimentados es su tipificación de datos permisivo. Si se cometen errores, no se obtendrán mensajes claros de error. Probablemente usted tendrá que rastrear sus propios errores; un reto nada peque­ ño. Sin embargo, las versiones más recientes incluyen un programa depurador ("lint") que realiza verificación de errores. Ritchie y Thompson han colaborado en varias ediciones del siempre cambian­ te UNIX. Considerando sus éxitos pasados, a los dos se les otorgó una libertad casi ilimitada en los Laboratorios Bell. Uno no puede sino preguntarse qué nos traerán después.

Tipos de datos en C C tiene dos tipos numéricos, Int y f loat. Un real puede ser double o long double y u n l n t puede ser short, long o unslgned. Existe un tipo de carácter char, pero no un tipo booleano. En C, cualquier valor distinto de cero se considera verdadero (true) y el 0, falso (false). Puesto que C es cercano a la máquina, ciertas constantes carácter no imprimibles se encuentran disponibles, tales como \n para una nueva línea o \b para un retomo de carácter (backspace). Los tipos derivados de los tipos simples anteriores son: Arrays:

<element type> <array name>[size]

Example:

char

Pointers: Example:

*<pointer name> int

ñame [25]

*pn

Structures: struct

Example: or:

[<structure name>](

}

typedef struct {int day, month, year;} date; struct hire_date {int day, month, year;};

Haciendo uso del typedef para date, podemos entonces declarar: date hi re_date; y asignar sus campos como sigue: hire_date.day = 25; hire_date.month * 9; (&hire date) - > year = 1990;

*ve aquí la combinación - > es un símbolo especial que significa "el o de la estructura (unión) señalado por la variable de la izquierda". Los parénon necesarios alrededor de ( H a t o l t-> r» rrn H o n o n r o r í i r l o n r i a c r fc h tt Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

149

unión [

]{list of variants}

U nions:

Las uniones (unions) siempre son discriminadas, así que no pueden ocurrir ambigüedades. Por ejemplo: ( i n t iarg; f l o a t farg;} numeric_const; numeric_const p i , zero; (&pi) - > farg = 3.141592; zero.iarg = 0; ty p e d e f unión

Las uniones y estructuras se declaran de manera similar, pero en una estructu­ ra (struct; registro de C), el almacenamiento es asignado para todos los campos, mientras que en una unión (unión), el almacenamiento es asignado para la varian­ te más grande, y sólo uno es asignado a una variable de unión. El registro variante de Pascal puede ser creado en C si se desea, puesto que una unión puede ser un campo de un registro (y viceversa). Functions:

(parameter 1 i st) parameter definí tions;

{ local declarations; statements;

Un valor funcional puede ser de cualquier tipo excepto otra función o arreglo. En las funciones que devuelven enteros, el tipo valor puede ser omitido. Una función que no devuelve valor es del tipo vold. Por ejemplo: void

swap(px.py) float

*px;

float

*py;

(...)

Una diferencia importante entre las funciones de C y de Pascal es que no ocu­ rre verificación de tipo sobre el número o sobre el tipo de parámetros cuando una función es llamada, si la función es definida como se ve anteriormente en el deno­ minado estilo clásico. Versiones más modernas de C incluyen un estilo moderno, donde la información del tipo de parámetro se incluye en la lista de parámetros, y puede ocurrir verificación de tipo. Por ejemplo: void

sw ap(float

*px,

float

*py)

(...)

Otra diferencia es que esos parámetros siempre son pasados por valor, excepto para arreglos, f (a) pasará un apuntador al primer elemento del arreglo a, a[01 Las llamadas por referencia se consiguen bastante fácilmente mediante direcciones de paso. C está organizado comúnmente en módulos de tres tipos: constantes manifies­ tas (macros), variables extemas (inicializaciones de arreglos y cadenas) y definicio­ nes de funciones. Éstos pueden organizarse para una compilación por separado, pero también pueden residir en el mismo archivo. Cuando un programa se organi­ Sólo fines educativos - FreeLibros

150

PARTE II: Lenguajes imperativos

za en varios módulos por separado, es importante que tengan declaraciones idén­ ticas para elementos comunes. Para mantener esta consistencia, tales declaraciones se colocan generalmente en un archivo de encabezado (digamos prog.h), que mar­ ca como extern aquellos elementos a los que se hará referencia mediante otro mó­ dulo. Los otros módulos pueden obtener acceso a estas declaraciones incluyendo al principio: #include <prog.h>

Las implementaciones de C también proporcionan la obtención de memoria extra cuando es necesario, utilizando la función cal 1oc( n , s ), donde n es el número de elementos de tamaño s por ser asignados, calloc devuelve un apuntador a la primera palabra de memoria extra. Esto también puede ser liberado empleando f r e e ( * p t r ), donde ptr apunta al principio del almacenamiento que será liberado.

Conversiones de tipo y representaciones C permite un pequeño número de conversiones de tipo automáticas. Como Kernighan y Ritchie dicen, “Las únicas conversiones que pasan de manera auto­ mática son aquellas que tienen sentido" [Kernighan, 1978]. Los tipos char e 1nt pueden ser intercambiados libremente, con los caracteres siendo convertidos a sus valores 1nt en ASCII. El valor de la expresión, (c + 'a' - 'A')

es un carácter en minúsculas si c contiene un carácter en mayúsculas. Los ti­ pos float e 1nt pueden ser combinados, como en fa rg+i arg, con el 1nt converti­ do a float. En general, la conversión siempre es para el “tipo más alto". Cualquier tipo no estructurado puede ser convertido en cualquier otro a través del uso de una conversión (cast). Si n es un 1nt, podemos convertirlo explícitamente a float mediante (float) n. La terminología de C es que n está convertido como float. En la práctica, convertir los apuntadores de un tipo de apuntador a otro no siempre funciona, aunque cualquier apuntador tipo p puede ser convertido como (chart) p. La conversión es práctica cuando se llaman funciones, donde los pará­ metros pueden ser de un tipo diferente. Por ejemplo, sqrtí (double) n) convertirá n en un double antes de enviarlo a la función sqrt. Por supuesto, podríamos haber empleado un bloque de tres enunciados, {double x; x = n; sqrt(x);}

para lograr casi el mismo efecto. En la primera llamada a s q r t , n permanecerá como un double, mientras que en el bloque permanece como un 1nt. Los tipos enteros son muy flexibles en C, y pueden ser utilizados para varia­ bles aritméticas, lógicas o de bits. Como se mencionó antes, cualquier valor numé­ rico distinto de 0 (entero o real) es verdadero ( t r u e ) mientras que el 0 es fal so (fal se). Puesto que C está basado en expresiones, el enunciado del listado (3.5.1) es perfectamente válido. Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

1f ( m- =l } {

151

(3.5.1)

/* execute if m decremented by 1 is not 0 */ statementjnl; if ( m -= 1) statementjn2;

} else statement__m3;

Obsérvese el uso de la expresión (m — 1) donde podríamos anticipar una expre­ sión booleana. C es un lenguaje magro, que no carga equipaje extra. No hay cons­ tantes predefinidas verdaderas (true) y falsas (false). Si uno desea esta característica, puede definirse una macro al principio de un programa (o colocarla en un archivo de encabezado): #define false 0 #define true 1

Las relaciones de C devuelven los valores de 1 o 0, de modo que las expresiones booleanas usuales, tales como (x < y), serán evaluadas como 1 si son verdaderas (true) y como 0 si son falsas (false). Operadores de C Una noción útil que se hace operacionalmente explícita en C es la de valores iz­ quierdos (left) y derechos (right) (valores 1 y valores r). Cuando hacemos una asig­ nación b = a, a y b se tratan de manera diferente. Un valor se calcula para a, y luego se ubica una dirección para b. Finalmente, el valor de a se copia en la locali­ dad de almacenamiento para b. Aquí b (o cualquier otro identificador para este propósito) es un valor 1, puesto que la expresión se refiere a un objeto que puede ser examinado o modificado, mientras que otras expresiones se consideran valores r. Una expresión tal como 2 * x + 5 puede tener un valor derecho, pero no un valor izquierdo. C tiene dos operadores que extienden esto: el operador de dirección & puede ser aplicado a un valor 1 (o a un designador de función) y devuelve un apun­ tador a su operando; y el operador de indirección *, el cual se aplica a un apuntador y produce un valor r (o un designador de función, si el apuntador apunta a una función). Considere las asignaciones mostradas en la figura 3.5.1. En la segunda, la di­ rección de a se coloca en b. En la tercera, tomamos el contenido de b, luego lo trata­ mos como una dirección para obtener el contenido 5. La última asignación puede parecer un poco extraña. *a selecciona el valor derecho asociado con a, pero a está a la izquierda, de modo que este valor es una dirección. El valor 1036 es almacena­ do en una celda con dirección almacenada en la localidad asociada con a. C tiene, además de * y &, los cuatro operadores aritméticos: +, *, /; y comparadores aritméticos: <, >, ==, <=, >=, != ( "not“" ). También tiene dos operadores de desplazamiento, desplazamiento izquierdo << y desplazamiento derecho >>. 12 << 3 produce 96, y 26 >> 2 es 6 (véase la figura 3.5.2).

Sólo fines educativos - FreeLibros

152

PARTE II: Lenguajes imperativos a =5 b = &a

-Q

a

_Q II O 5 b

c *a = 1036 5

---- --------- >

1036

Celda con dirección = 5 FIGURA 3.5.1 Valores 1 y valores r en C

Si bien su predecesor carente de tipos BCPL estaba orientado a las declaracio­ nes, C es un lenguaje de expresiones. Una expresión válida, tal como x + y, siempre tiene un valor. La asignación se trata como un operador =, donde la expresión Cx = 3 + 5) da el valor 8. Como un efecto colateral, a x se le asigna el valor 8. Esto también permite escribir asignaciones tales como x = y - 0, puesto que el valor de (y = 0) es otra vez 0, lo cual permite que ese valor sea colocado en x. Considere un bloque de programa C para contar el número de caracteres de la entrada, como se muestra en el listado (3.5.2). {

(3.5.2)

n = 0; ((c = getchar ( ) ) ! = EOF) c != \0 || c != \n ? ++n : n;

w hile

) Examinemos las dos líneas en el enunciado while. Recuerde que en un lenguaje de expresiones, toda expresión proporciona un valor. Primero, la asignación de un ca­

1

1

1

o

1

o

1

1

1

1

26

12

1

12 « 3

26 » 2

FIGURA 3.5.2 Operadores de desplazamiento en C

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

153

rácter a c y la comparación para EOF pueden ser todas hechas en la misma expresión. El valor de la expresión es verdadero o falso, pero la variable c tiene asignado un valor en cualquiera de los casos como un efecto colateral. La segunda expresión es una expresión condicional, señalada por?. La expresión (e ? a : b) nos da el valor a si e es verdadero, y de otro modo da b. Primero, comparamos c con \0 (el carácter nulo) y con \n (nueva línea). Si no es igual (! = ) a ninguno, el valor de la expresión condicional es n+1 (++n). Si es igual a uno o al otro, el valor es n. E l ; se utiliza para convertir la expresión en un enunciado. En lenguajes de expresión, los enunciados no tienen valor, sólo efectos colaterales. El valor de la expresión ya no es necesario, de modo que se desecha. Sin embargo, el efecto colateral de incrementar n ha ocurri­ do de todas maneras, así que n tiene la cuenta deseada hasta la terminación del ciclo. Los operadores de C se muestran en la tabla 3.5.1, agrupados en orden de prece­ dencia, donde los enumerados primero tienen precedencia sobre los que están des­ pués en la figura. Puesto que C es un lenguaje tipificado, el uso está restringido a tipos particulares. El nuevo operador "coma" se analizará posteriormente en esta sección. C tiene un enunciado 1f y uno 1f... el se, así como también enunciados repeat, whlle, do...wh1le ..., swltch y for. Un ejemplo de un enunciado for de C se muestra en el listado (3.5.3). for (i =0; i<5; i++) x=i;

(3.5.3)

La primera expresión da un 0 al comienzo delciclo, y termina cuando i ==5, mien­ tras que i se incrementa en1 después de que se utiliza ( i ++). x se asignará sucesiva­ mente: 0 ,1 ,2 , 3 y 4.

Un ejem plo de operaciones de bits de bajo nivel En esta sección incluiremos un ejemplo de un programa de base de datos simple que ofrece un poco del sabor de las manipulaciones de bits. Supongamos que los registros de estudiantes de un pequeño colegio están almacenados en disco, en registros definidos mediante el listado (3.5.4). #def1ne Ln 35 typedef struct t char nameCLn+11; long ID; char year; char gender; 3 std_type;

(3.5.4) /* /* /* /*

nombre del estudiante */ identificación del estudiante */ año de la escuela: 1 .. 4 */ género: 'M' o 'F' */

Cuando se leen los registros del disco y se colocan en memoria, el nombre (ñame) e identificación (ID) tendrán el mismo formato, pero utilizaremos operaciones de bajo nivel para empacar tanto el año como el género (gender) en un solo campo. Los re­ gistros empacados tendrán la forma mostrada en el listado (3.5.5), y formaremos nues­ tra base de datos de estudiantes st_db como un arreglo global de tales registros. Sólo fines educativos - FreeLibros

154

PARTE II: Lenguajes imperativos

TABLA 3.5.1 Operadores del lenguaje C Primarios

Relacional

paréntesis valor del elemento y-ésimo del arreglo x valor del campo y de la estructura señalada por x valor del campo y de la estructura x

0 x[y] x-> y

x*y

Unitario

++x(—x) x++(x--) -X *X

&x sizeof x

x 1y

x&&y

xl ly

producto (cociente) de x e y x MOD y

x?y:z

y si x es distinta de cero, z en caso contrario Asignación

suma (diferencia) de x e y

x~y xop=y

D esplazam iento

x obtiene el valor de y x obtiene el valor de xopy, donde op puede ser +,

%, » , « , &, Ao I.

x « y ( x » y ) x es desplazado a la izquierda (derecha) en y lugares

Coma

x,y

typedef struct I char nameíLn]; long ID; char year_gender;

1 si tanto x como y son distintos de cero, 0 en caso contrario 1 si x o y son distintos de cero, 0 en caso contrario Condicionales

Adición o suma x+y (x-y)

and en modo de bit de x e y, 1& 1=1,0 en caso contrario xor en modo de bit de x e y, 1A0=0A1=1, en caso contrario or en modo de bit de x e y, 010= 0,1 en caso contrario Lógicos (en orden de precedencia)

M ultiplicación x*y(x/y) x%y

x igual (distinto) a y

M odo de bits (en orden de precedencia)

X

~x

x==y (x!=y)

x&y

x negada; !x = 0 si x es distinta de cero, 1 en caso contrario complemento de 1 de x. Los 0 se convierten en 1 y los 1 en 0. x se incrementa (decrece) antes de su uso x se incrementa (decrece) después de su uso negación aritmética de x valor en la dirección x dirección de x # de bytes en x

x menor que y, etc. 0 si es falso, 1 en caso contrario Igualdad

<

!x

x

,<=,>=)

x, luego y, son evaluadas, la expresión obtiene el valor de y

(3.5.5)

/* año 0 .. 3; Kmascul i no). O(femenino) */

3 packed_std_type;

extern packed_std_type st_dbC];

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

155

Primero examinaremos las funciones pack y unpack, las cuales convierten re­ gistros estándar en registros empacados, y viceversa. Estos se muestran en el lista­ do (3.5.6), en el que se agregaron números de línea para ayudar al análisis. (3.5.6)

1) void pack(packed_std_type *packed_std, std_type *std) 2)

(

int isjnale = std->gender == 1M 1 ? 1 : 0;

3)

strncpy(packed_std->name9 std->name, sizeof

packed_std->name);

4)

packed_std->ID = std->ID;

5)

packed_std->year_gender = (i s_male«2) | (std->year-l);

6)

void unpack(std_type *std, packed_std_type *packed_std)

7)

{

8)

strncpy(std->name, packed_std->name. sizeof packed_std->name); std->ID = packed_std->ID;

9)

std->year = ((packed_std->year_gender) & 3) + 1; /*unpack year_gender*/

10)

std->gender = (packed_std->year_gender»2) == 1 ? 'M' : 'F 1;

} Considere un registro estándar de un Júnior (año = 3) masculino (gender = 'M'). En ese caso, is jn a le obtiene el valor 1 (verdadero). En las líneas 3 y 4, el nombre (ñame) y la identificación (ID) se copian al registro empacado (packed). En la línea 5, yea r -1 desplaza los valores 1 .. 4 al intervalo 0 .. 3, de modo que se ajus­ tarán a dos bits. Al desplazar i sjnal e dos lugares a la izquierda, el bit 1 (male) se coloca en la tercera posición desde la derecha. Aplicando el operador I (or en modo de bit) entonces se empacan ambas informaciones en el campo year_gender, como se muestra en la figura 3.5.3. Cuando un registro se desempaca, al aplicar el operador & (and en modo de bit) con el valor 3 = 0000 0000 0000 0011 en la línea 9 se enmascarará todo menos los dos bits de la derecha. Al agregar 1 se desplazan entonces a los valores de dos bits 0 .. 3 de regreso al intervalo original 1 .. 4. Entonces el operador » (desplaza­ miento a la derecha) coloca el bit de género (gender) de vuelta al bit del extremo derecho.

¡s m ale« 2

0 ..

..00100

std->year-1

..

..0 1 0

packed_std->year_gender

..

..00110

F I G U R A 3.5.3

Empacando el campo year_gender Sólo fines educativos - FreeLibros

¡s male = 1

Júnior = 3

campo empacado

156

PARTE n: Lenguajes imperativos

Una vez que una base de datos st_db de registros empacados se almacena en memoria, se necesitarán funciones para agregar, eliminar y editar registros (entre otras). Considérese la forma de la función add mostrada en el listado (3.5.7).

/* Agrega un estudiante a la base de datos */ /* RETURNS; 0 si no pudiera llegar a realizarse, en caso contrario 1 */ Int add() í Int location; packed_std_type packed_std; std_type std;

(3.5.7)

1f (current_size — MAX_db) /* si la base de datos estállena */ C pr lntfCLa base de datos está llenaVn” ); return 0;

} íf (getIDÍ&std.ID) <*= 0) /* obtiene y verifica una ID válida */ return 0; If (findístd.ID, &1ocatlon)> /* obtiene la localidad de inserción */ í pr int f C N o se puede agregar; el estudiante ya existe.\n” ); return 0; 3 I f (getinput(&std) — return 0 ;

0) /* obtiene nombre, año y género*/

pack(&packed_std, &std); /* crea espacio para el nuevo estudiante */ memmove(&st_dbClocation+l], &st_db[locatlon], (current_size-location)*sizeof(st_db[03)); /* inserta nuevo estudiante */ memcpy(&st_db[locatlon], &packed_std, slzeof(st_db[location])); ++current_size; return 1; 3

Ningún parámetro está enumerado, puesto que st_db es global. A la llamada de f i nd, se determina la localidad de inserción destinada. Aquí getID obtiene el nú­ mero de identificación (ID) del estudiante y verifica su validez, mientras que geti nput obtiene los campos restantes. Después de empacar el registro, se realiza un memmove, el cual mueve todos los registros desde location hasta el final sobre un registro. Luego, finalmente, se utiliza un memcpy para colocar el registro empacado en la base de datos st_db. Los estudiantes tendrán la oportunidad de investigar este ejemplo de manera adicional en el Laboratorio 3.2.

Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

157

L A B O R A T O R I O 3 . 2 : C O M B I N A C I Ó N DE C A R A C T E R Í S T I C A S DE B A J O Y ALTO N I V E L : C

Objetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Familiarizar a los estudiantes con la sintaxis del lenguaje C. 2. Combinar tanto las características de bajo nivel como las de alto nivel de C. 3. Ilustrar los ahorros en espacio que se ganan mediante el uso de las características de bajo nivel.

A rreglos, apuntadores y el operad or com a Puesto que C no permite procedimientos anidados, y todos los parámetros excepto los arreglos son pasados por valor, la compilación y la ejecución son rápidas. Los programas tienden a estar compuestos por multitud de pequeñas funciones. Cuan­ do un arreglo se pasa como un parámetro, es este apuntador el que se pasa; el arreglo no se copia. Un ejemplo directo, semejante a uno del libro The C Puzzle Book [Feuer, 1989] se muestra en el listado (3.5.8). Int a[] = CO,1,2,3); 1nt *p[] = Ca,a+l,a+2,a+3);

/*arreglo con elementos, 0-3 */

(3.5.8)

/* arreglo con elementos de apuntador */

Int **pp=p; malnOC printf("a=%p, *a-%d, p-fcp, *p=%p, **pp=%d\n\ a,*a,p,*p,**p); /* "..." es una directiva de formato */

} La salida impresa es a *

, *a - 0, p - , *p * , **p * 0. ¿Usted ve por qué? Aunque es un tanto inconsistente, ayuda a recordar que el arreglo a es 4a CO], la dirección del elemento 0 del arreglo a, y que a [0] es *a, el valor del elemento 0 (véase la figura 3.5.4). C también soporta aritmética de apuntador. Haciendo uso de las variables de la figura 3.5.4, pp-p«=0, puesto que la variable arreglo p es un apuntador al arreglo. Además, ++pp-p=”l, puesto que ++pp hace que pp apunte al segundo elemento del arreglo p, p[1 ]. Así el valor de pp esp+l,ypp-p — (p+l)-p — 1.

F I G U R A 3.5.4

Identificadores en el listado (3.5.8)

Sólo fines educativos - FreeLibros

158

PARTE n: Lenguajes imperativos

La mayoría de las expresiones y enunciados de C están directamente traídas desde BCPL. Sin embargo, un operador es nuevo para C: el operador , (coma) (comma). ( a , b) es una expresión que evalúa a y que tiene el valor de b. Es particu­ larmente útil para inicialización; por ejemplo, for(s=Ot i=l;i<=TO;s+=i,i++);

(3.5.9)

terminará con s~“ 55, la suma de los primeros 10 enteros. Existen dos usos para , aquí: primero, en la parte de la inicialización, s=0, i =1; y segundo, en la parte de la reinicialización, s +- i ,i++. El ciclo for evalúa s, pero utiliza el valor de la expresión i durante cada iteración.

L A B O R A T O R I O 3 .3 : D I V E R S I Ó N CON T R U C O S PA RA C: C Objetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Demostrar los efectos de la tipificación débil de C. 2. Redirigir la salida desde un programa en C a un segundo programa. C y UNIX Como hemos visto, C está íntimamente relacionado con el sistema operativo UNIX, el cual está casi completamente escrito en C. UNIX está compuesto de un núcleo o kemel, una o más capas y un gran conjunto de rutinas de servicio. El núcleo es pequeño, aproximadamente 10 000 líneas de código, lo que crea una máquina vir­ tual que: 1. 2. 3.

Calendariza, coordina y administra la ejecución del proceso. Proporciona servicios del sistema tales como E/S. Maneja operaciones de hardware dependientes de la máquina [Silvester, 1983].

Todo excepto el conjunto de primitivas de máquina adaptadas a la computadora particular en la que UNIX está ejecutándose está escrito en C. El usuario rara vez ve el kemel, pero interacciona con el conjunto de procedimientos comprendidos en una de las capas o shells. Un sistema UNIX proporciona una variedad de utilidades tales como editores, depuradores y preprocesadores así como también compiladores para BASIC, FORTRAN, RATFOR, Pascal (por lo menos en la versión de Berkeley), C y ensamblador. El código fuente en cualquiera de estos lenguajes primero se traduce a código intermedio C antes de ser traducido a ensamblador, objeto relocalizable y finalmente código ejecutable en lenguaje de máquina. Puesto que todos los progra­ mas primero son traducidos a C, los nuevos compiladores son particularmente fá­ ciles de escribir. Todo lo que se necesita es diseñar un traductor para C, sin escribir nada de código ensamblador. Este traductor también permite la mezcla de código escrito en diferentes lenguajes fuente y hace interfaz con aplicaciones tales como bases de datos, hojas de cálculo electrónicas y programas gráficos. Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

159

El C estándar El estándar defacto para C ha sido el libro de Kemighan y Ritchie [Kemighan, 1978]. Sin embargo, hay ahora un estándar del Comité Técnico X3J11 del American National Standards Institute [ANSI/ISO-9899, 1990]. Ahora se esperará que todos los compiladores de C concuerden. Cuando fue adoptado por la International Standards Organization (ISO), era básicamente idéntico al estándar ANSI. Puesto que había algunas debilidades en la previsión para características dependientes del ámbito local (por ejemplo, coma o punto para separación decimal, mes/día/año contra día/mes/ año, o una secuencia alfabética diferente), ISO adoptó la Enmienda 1 en 1994 [ANSI/ISO-9899,1994], ía cual es ahora parte del estándar. El comité X3J11 fue guiado por varios principios, siendo el más importante, "no hacer obsoleto el código que funciona actualmente". Es decir, los programas escritos en código Kemighan/Ritchie correcto todavía deberían compilarse y eje­ cutarse. Otros abogaban tanto por la transportabilidad como por la dependencia del sistema C. El comité intentó preservar a C tan atractivo como ya existía, y no "arreglarlo". Ventajas y desventajas La principal desventaja de C es la dificultad de depuración de los programas debi­ do a las limitantes de tipo automáticas, aritmética de apuntador y efectos colatera­ les dentro de expresiones. También fomenta un estilo de programación suave que es en ocasiones difícil de leer para cualquiera que no sea el diseñador del progra­ ma. De este modo, con frecuencia no es el lenguaje preferido para aplicaciones científicas o de negocios. Sin embargo, su cercanía a la máquina lo hace ideal para escribir sistemas operativos y compiladores. También es muy flexible para la programación interac­ tiva, debido a la variedad de facilidades de E/S. E J E R C I C I O S 3.5 1. ¿Cuáles son los valores de 114»3? ¿De 9 6 « 2 ? ¿De 8 » 4 ? ¿Cuál es la relación entre » y la división entre potencias de 2? ¿Entre « y la multiplicación? 2. ¿Qué valores se asignarán a x si cambiamos el ciclo del listado (3.5.3) por fo r (1-0; 1<5; ++i) x-1;? 3. Pascal permite conversión automática de enteros a tipos flotantes, pero no de carac­ teres a enteros. Ni Modula-2 ni Ada permiten ninguna, mientras que C permite am­ bas. ¿Cómo manejan Pascal, Modula-2 y Ada una expresión como (r+i), donde r es real e i es un entero? 4. ¿Por qué uno siempre puede representar un apuntador para que sea un apuntador a un carácter, pero posiblemente no un apuntador para un Int o f loat? 5. Considere los siguientes valores de m a la entrada para el código del listado (3.5.1). ¿Cuál(es) enunciado(s) será(n) ejecutado(s)? (== es un comparador de C, mientras que = es el operador de asignación.) a. m==3 b. m==2 c. m==l d. m==0 Ahora, elimine los delimitadores de bloque ( y }, y responda desde a hasta d otra vez. 6. ¿Cuál será el patrón de bits del campo year_gender empacado para una estudiante femenina, siguiendo el ejemplo de la figura 3.5.4? Sólo fines educativos - FreeLibros

160

PARTE H: Lenguajes imperativos

7. Suponga que el campo yea r_gender empacado tiene el patrón de bits 0000 0000 0000 0100. Siga el listado (3.5.6) para hallar los campos year y gender sin empacar. 8. Suponga que invertimos las últimas dos expresiones en el ciclo del listado (3.5.9) a: for(s-0, 1-1; i <=10; i++, s+-i);. ¿Qué valor tendrá s a la terminación del ciclo? 9. Discuta las diferentes secuencias de cotejo, además del alfabeto estadounidense de 26 letras y el punto decimal, que puedan utilizarse en versiones estándar no estado­ unidenses de C. LABO RATO RIO 3.4: HERRAM IENTAS IDE: PASCAL/C

Objetivos (Los laboratorios pueden encontrarse en el Instructor's Manual) 1. Investigar las herramientas de programación proporcionadas con la versión de Pascal o C disponible, especialmente cadenas y paquetes gráficos, el editor, el depurador, el rastreador y el navegador. 2. Utilizar y emplear estas herramientas para sus propósitos destinados. LA BO R A TO R IO 3.5: H ERR AM IEN TA S APSE: ADA

Objetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Investigar las diversas herramientas proporcionadas con el paquete de Ada que se esté utilizando. 2. Llegar a familiarizarse con los paquetes proporcionados del APSE. En particular, examinar los diversos paquetes de E/S incluidos en la implementación que se está utilizando.

3.6

RESUMEN En este capítulo, hemos considerado los lenguajes estructurados en bloques, los cuales implementan bloques anidados y procedimientos (recursivos), comenzando con ALGOL en 1957. Seguimos este desarrollo a través de su primo ortogonal, ALGOL 68; a través de su versión simplificada, Pascal, hasta llegar a Ada. Esta línea de len­ guajes también fue definida sintácticamente en forma cuidadosa a través de una formalización llamada la forma Backus-Naur (BNF, por sus siglas en inglés). Asimismo, examinamos el desarrollo de C desde ALGOL 60, a través de CPL, BCPL y B. A medida que las reglas se hacían más estrictas y los lenguajes de mayor nivel en el primer grupo, las cosas se iban relajando en C de modo que un progra­ mador podía manipular el almacenamiento en la máquina directamente. Los procedimientos pueden tener parámetros formales a los cuales se pasan los valores de parámetros reales. En el capítulo 2 consideramos cinco mecanismos de paso de parámetros: por valor, referencia, resultado, resultado-valor y nombre. Pascal implementa los primeros dos bajo el control del programador, C pasa todos los parámetros excepto los arreglos por valor, y Ada proporciona 1n y out, que se comportan como parámetros por valor o resultado-valor, respectivamente, pero pueden ser implementados de manera diferente, dependiendo del escritor del compilador. El soporte de Ada para los parámetros 1n out fue discutido adicional­ mente en la sección 2.3. Sólo fines educativos - FreeLibros

CAPÍTULO 3: Estructura en bloques

161

Las funciones son procedimientos que devuelven un solo valor. Este valor ha sido restringido a tipos particulares por algunos lenguajes. ALGOL 68 fue el pri­ mero en permitir funciones con valores de cualquier tipo. Ada también incluye esta característica e impone parámetros por valor. La tipificación fuerte, donde los valores de una variable permanecen fieles al tipo a lo largo de su uso, han sido impuestos en Pascal y en Ada, pero no en C. Las uniones libres en Pascal son una excepción a esta noción, pero las dificultades re­ sultantes han sido minimizadas tanto en C como en Ada a través de la insistencia sobre las uniones discriminadas solamente. La noción de procedimientos y funcio­ nes genéricos, donde los tipos tanto de parámetros como de valores funcionales pueden variar dependiendo del uso, ha sido proporcionada en Ada. Los lenguajes estructurados en bloques que examinamos también proporcio­ nan variables dinámicas de dos maneras. La primera de éstas son las variables locales, que son creadas a la entrada y destruidas a la salida de un bloque. La se­ gunda clase son las variables por referencia, que mantienen direcciones de locali­ dades de almacenamiento. Éstas son llamadas apuntadores en Pascal y C y variables access en Ada. Las estructuras (registros) pueden ser definidas recursivamente en Pascal, Ada y C al incluir un apuntador a una estructura similar como uno de los campos. Si p es un apuntador a una estructura de este tipo, el almacenamiento puede ser ubicado para una nueva instancia a través de las funciones new (Pascal, Ada) o al loe (C). Pascal y C también proporcionan las funciones dispose y f ree, respectivamente, para liberar memoria de almacenamiento previamente asignada. El control del programador sobre las excepciones fue presentado por primera vez en PL/I y expandido en Ada. PL/I también incluye arreglos de bits, los cuales han sido explotados de manera más completa en C. Tanto Ada como Modula-2 han proporcionado módulos de mayor nivel, donde las variables y los procedimientos pueden ser agrupados en unidades autocontenidas. En Ada, éstos son paquetes, y en Modula-2, módulos. El soporte de Ada 95 para la programación orientada a objetos se describirá en el capítulo 4. Ada también incluye tareas para implementar la concurrencia. Modula-2 ha implementado corrutinas, y UNIX tiene operaciones de bifurcación y de unión para implementar programas de C concurrentes. Exami­ naremos todo esto con más detenimiento en el capítulo 5.

3.7

NOTAS SOBRE LAS REFERENCIAS Para lograr un entendimiento completo de los lenguajes de procedimientos estructurados en bloques, uno haría bien en estudiar ALGOL 60 y ALGOL 68. [Naur, 1963] proporciona un buen análisis y el inform e completo de 17 páginas. [Tanenbaum, 1976] es un tutorial de ALGOL 68. [Branquart, 1971] proporciona un análisis legible sobre semántica de ALGOL. La lectura de estos tres artículos, más el resumen de Knuth de cuestiones todavía ambiguas en ALGOL 60 [Knuth, 1967] daría también al lector una buena idea de qué tan difícil es ser preciso. Entre los libros de investigación, [Barón, 1986] describe los lenguajes de pro­ gramación para legos de una manera superficial, pero interesante y competente. Historias más técnicas son las de [Sammet, 1969] y [Wexelblat, 1981]. [Horowitz, Sólo fines educativos - FreeLibros

162

PARTE n: Lenguajes imperativos

1987] es una colección de artículos importantes y de fácil lectura, originalmente escritos para publicaciones tan dispares como el IBM Journal o f Research and Development y BYTE Magazine. Esta colección ha sido revisada cada dos años desde 1983, pero una llamada a los editores nos hizo saber que no se ha planeado una nueva edición desde 1987. El artículo de [Feuer, 1982] en la serie Computing Surveys de la ACM compara Pascal y C, mientras que [Smedema, 1983] considera Pascal, Modula, Chill y Ada. Se mencionaron tres documentos del ANSI (American National Standards Institute) en este capítulo: [ANSI-1815A, 1983], que define Ada 83, [ANSI/ISO8652, 1995], que describe Ada 95, y [ANSI/ISO-9899, 1990], el estándar para C. Pascal también tiene un estándar estadounidense, [ANSI/IEEE-770X3.79, 1983]. Éste fue ideado conjuntamente por el Comité X3J9 del ANSI y el Proyecto P770 del Instituto de Ingenieros Eléctricos y Electrónicos (IEEE, por sus siglas en inglés). Un estándar internacional de Pascal, [ISO-DP7185,1980], difiere en alguna medida del 770X3.97. Los programas que siguen el estándar estadounidense se ejecutarán en Pascal ISO, pero el estándar ANSI/IEEE no incluye el arreglo conformante de DP7185. En Pascal ISO, procedure ProcesotA: array [inicio..final] of AlgunTipo);

es perfectamente válido, con s t a r t y f i n i s h conformando los límites del paráme­ tro real pasado al parámetro A en Process. Los documentos estándar, los cuales son muy concisos, no son convenientes para aprender un lenguaje. La "biblia" de Ada es [Booch, 1986]. Los manuales de Pascal abundan, siendo [Cooper, 1983] el que describe el estándar para programadores experimentados. [Kemighan, 1978] ha sido actualizado para incluir el C de ANSI y todavía es el manual de C más ampliamente utilizado. [Plauger, 1996] in­ cluye información acerca de la Enmienda 1 de 1994 para el C estándar. Libros nue­ vos que incluyen tanto C como C++, tales como [Stoustrup, 1991] y [Deitel, 1994], ahora vuelven a estar disponibles.

Sólo fines educativos - FreeLibros

CAPÍTULO 4 LENGUAJES PARA PROGRAMACIÓN ORIENTADA A OBJETOS (POO) 4.0 En este capítulo 4.1 Programación con objetos

166 167

Mensajes, métodos y encapsulamiento Primeras nociones de objetos en Simula Objetos en Ada 83 y Ada 95 Ejercicios 4.1

169 172 175 179

4.2 Clases y polimorfismo

180

Procedimientos y paquetes genéricos en Ada Clases en Object Pascal Clases en C++ Implementación de clases heredadas Ejercicios 4.2

181 183 189 192 193

4.3 Smalltalk

194

Viñeta histórica: Smalltalk: Alan Kay

195

4.4 Herencia y orientación a objetos

Tipos y subtipos en jerarquías de herencia

Herencia múltiple Ejemplares de lenguaje Más de Object Pascal Herencia en C++ Ligadura dinámica Ejercicios 4.4

201 205 206 209 214 217

4.5 Java

217

220

196

Construcciones del lenguaje Java Object, la superclase de todas las demás clases Una clase elemental de Java Las Interfaces de Programación para Aplicaciones de Java (APIs) Compilación y ejecución de un programa Java Hotjava y Applets Tipos de programa Diferencias entre Java, C y C++ Ejercicios 4.5

224 225 225 226 228

200

4.6 Resumen 4.7 Notas sobre las referencias

228 229

Sólo fines educativos - FreeLibros

220 221 222

CAPÍTULO

4

Lenguajes para programación orientada a objetos (POO)

En la división de los lenguajes de programación en dos paradigmas, imperativo y declarativo, cada uno con tres subparadigmas, los lenguajes orientados a objetos se colocaron en el paradigma imperativo, puesto que fue en el lenguaje imperativo Simula en el cual comenzaron estas nociones. Un objeto está definido como "un grupo de procedimientos que comparten un estado" [Wegner, 1988]. Recuerde que un programa escrito en un lenguaje imperativo involucra una secuencia de coman­ dos de transición de estado. De manera informal, un objeto es un elemento o cosa, con sus comportamientos asociados bien definidos. Definiremos un objeto como una colección de datos, denominada su estado, y los procedimientos capaces de alterar ese estado. Si un objeto es un simple robot consistente de un brazo móvil y una tenaza, su estado incluirá su posición en el cuarto donde está localizado, el ángulo del brazo, y si su tenaza está abierta o cerra­ da. Un objeto robot debe tener un nombre para distinguirlo de otros robots. La colección de todos los robots es conocida como una clase. Se puede pensar en una clase como un tipo, aunque algunos lenguajes hacen una distinción, y utili­ zan los tipos para datos y las clases para definiciones de objetos. Nosotros definire­ mos una clase como una colección de objetos que comparte los mismos atributos; donde un atributo es el tipo de un miembro de datos o un método para manipular esos datos. Un atributo de un objeto puede ser otro objeto, así como también datos, o un método. Todo en Smalltalk es un objeto, con la clase obje c t siendo la superclase de to­ dos los demás objetos; es decir, todos los objetos tienen los atributos de object, más otros adicionales, posiblemente. Los objetos tienen asociados operaciones y va­ lores. Por ejemplo, si Queue (cola) es la clase de todos los objetos de cola, como se discutió en la sección 2.1, y un objeto llamado q se encuentra en la clase, entonces las operaciones sobre q incluyen newQueue, add(q,i), front(q), remove(q) e isEmpty(q). Sólo fines educativos - FreeLibros

166

PARTE n: Lenguajes imperativos

q puede tener un estado predeterminado, y representar la cola vacía. De otro modo, el estado de q incluirá la lista de elementos que hayan sido agregados a q, de modo que las relaciones del listado (2.1.6) se mantengan. En el lenguaje de obje­ tos, newQueue es un constructor que produce la existencia del objeto q. Un destruc­ tor, el cual destruye un objeto, se incluye también a menudo en las operaciones de un objeto. Blair asegura que no existe un consenso real acerca de lo que se entiende por un sistema orientado a objetos, y propone que la característica clave de cualquier elemento llamado un objeto sea que esté encapsulado. "Un objeto está encapsulado si las nociones de un conjunto de operaciones y un conjunto de datos están incor­ poradas en una sola entidad (es decir, el objeto). Además, debería restringirse el acceso de los clientes al objeto sólo a través de una interfaz operacional externa, bien definida" [Blair, 1989]. Esto parece ser similar a un tipo de datos abstractos (ADT), el que discutimos en el capítulo 2, y en realidad un ADT puede implementarse como un objeto. Noso­ tros consideraremos, a continuación, otros atributos de objetos y su utilidad.

4.0 EN ESTE CAPÍTULO Un lenguaje basado en objetos soporta: • • •

Encubrimiento de información (encapsulación). Abstracción de datos (la encapsulación del estado con operaciones). Paso de mensajes (polimorfismo).

Un lenguaje que sea orientado a objetos también implementa: •

Herencia, incluyendo ligadura dinámica.

La herencia, la organización de objetos dentro de una jerarquía de clases donde un objeto puede estar dando las propiedades de su clase padre sin redeclaración, es la característica distintiva del enfoque orientado a objetos. Esto incluye liga­ dura dinámica, donde los tipos de datos y/o procedimientos pueden estar ligados a nombres en tiempo de ejecución. Discutiremos esta característica en la sección 4.4. Como ejemplos de lenguaje, consideraremos Ada, Object Pascal, C ++, y el nuevo lenguaje de Sun Microsystems, Java™. Aunque la noción de estado, que es sólo otro nombre para el almacenamiento de un objeto individual, no es un enfoque de los lenguajes declarativos, la orientación a objetos ha hecho impacto allí, así como en los lenguajes imperativos. Dejaremos la discusión del Sistema orien­ tado a objetos SCHEME (SCHEME Object-Oriented System [SCOOPS]) y el Sis­ tema de objetos LISP común (Common LISP Object System [CLOS]) para el capítulo 8, el cual introduce los lenguajes funcionales como parte del paradigma decla­ rativo. Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

167

4.1 PROGRAMACIÓN CON OBJETOS En el mundo real, un objeto es una entidad dinámica. Puede cambiar, pero todavía mantenerse el mismo objeto. Un objeto muy complejo es un ser humano. Un objeto algo más simple es una chequera. Puede (o no puede, según qué tan cuidadoso sea su propietario) representar el estado de una cuenta bancaria. Ya sea que pueda estar equilibrada o no, todavía será la misma chequera. Existen muchas chequeras, con cualquier ejemplo representando un objeto en la clase de todas las chequeras.1 Enviar un mensaje a una chequera para que libre cheques por sí misma, no tiene sentido, pero solicitarle un balance o que procese una transacción de $500, sí lo tiene. Un programador orientado a objetos enfoca un problema mediante su división en agentes interactivos, llamados objetos, los cuales pueden realizar funciones e interactuar con otros agentes. Cuando se utiliza un estilo descendente, uno proce­ de de manera algorítmica, delega responsabilidades para cada paso de un procedi­ miento. El proceso de visitar una ATM (máquina de cajero automático, por sus siglas en inglés), o bien, depositar o retirar fondos se muestra de manera algorítmica en la figura 4.1.1 y en un estilo orientado a objetos en la figura 4.1.2. La figura 4.1.1 representa un algoritmo descendente típico, con el procedimiento principal o "manejador" descompuesto en tres subprocedimientos: dos para la

FIGURA 4.1.1 Análisis algorítmico del programa Chequera 1 Booch [Booch, 1994] considera intercambiables los términos ejemplo y objeto; sin embargo, no todos los autores están de acuerdo. En Object Pascal, un objeto (object) es una plantilla para ejemplos par­ ticulares.

Sólo fines educativos - FreeLibros

168

PARTE II: Lenguajes imperativos

entrada, Obtener ID Usuario y Obtener Transacción; y uno para la salida, Pon $$. Este tercer procedimiento se descompone en otros dos, los que realizan el trabajo principal del problema: Ajustar Balance Bancario y Dar Recibo. Ajustar Balance Bancario se descompone además en dos tareas, Enviar Estado Cuenta y Balan­ ce Chequera. Un problema puede ser descompuesto de manera algorítmica o de modo orien­ tado a objetos, pero no puede mezclar los dos enfoques. Son por completo diferen­ tes. Los objetos son independientes entre sí, pero más fáciles de verificar, transportar (trasladar a una máquina diferente) y mantener que los procedimientos interdependientes. También facilitan la reutilización de código probado sin tener que recompilarlo. Robert Moskowitz afirma que la previsión de objetos preprogramados y modificables por el usuario "permite a los usuarios que entienden muy poco acerca de las computadoras obtener y manipular características, funciones y opera­ ciones de computadora tan fácilmente como obtienen y manipulan objetos tangi­ bles en el mundo real" [Moskowitz, 1989]. Existen muchas definiciones para la palabra "objeto", además de la del CLU "contenedor para datos". Quizá la más simple es la de [Cox, 1984], en la que "los objetos son datos privados y las operaciones soportadas por esos datos". Los obje­ tos se comunican mediante paso de mensajes, los cuales son "solicitudes para que un objeto realice una de sus operaciones". Un mensaje no es más que una llamada a un procedimiento, llamado un método, que pertenece a un objeto y puede estar oculto para el usuario. Así, un mensaje debe hacer referencia a un objeto en parti­ cular así como también al nombre del método que es invocado. En este capítulo, cuando hagamos referencia a un objeto, nos referiremos al par (datos, métodos), y

FIGURA 4.1.2 Descomposición orientada a objetos del problema de la Chequera

Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

169

no sólo al contenedor de datos. Los datos pueden ser visualizados como tipos de datos, variables, o valores (estado), según el contexto.2 En nuestro ejemplo de la chequera de la figura 4.1.2, existen cuatro objetos: ATM, Chequera, CuentaBancaria y Usuario. La comunicación de un Usuario a la ATM, como se indica mediante las flechas, es a través de los mensajes ID y Tran­ sacción. La ATM responde con el envío de Dinero o un Recibo impreso. El objeto ATM es concebido como una entidad activa incluyendo datos, y también capaz de enviar mensajes, recibos, o dinero, mientras que los algoritmos manipulan datos pasivos. Un objeto Usuario solicitará a un objeto ATM que responda a una solici­ tud. La ATM tiene sus propios datos o puede solicitarlos de otro objeto, y puede responder a la solicitud. El mensaje Transacción (Sacar, LibretaDeJuan, 500.00), debería resultar en que Juan tenga $500 en efectivo (Dinero) descargados de la ATM y esta misma cantidad restada a su balance de cuenta corriente, a través de un método perteneciente a CuentaBancaria. BalanceCuenta (que no aparece an la fi­ gura 4.1.2) podrían ser datos pertenecientes a CuentaBancaria, pero Usuario no necesita conocer esto. Si hubiese habido una epidemia de latrocinios en esa ATM recientemente, el banco podría querer el nombre de Juan registrado en una lista de usuarios de la ATM. Esto podría realizarse al volver a escribir e instalar el método Transacción en las oficinas centrales, sin que los usuarios se diesen cuenta del cam­ bio. Los programas Cliente (aquellos que utilizan objetos) no son afectados por los cambios a la implementación de las clases de objetos. Observe que muchos de los procedimientos enumerados en la figura 4.1.1 no están mencionados en la figura 4.1.2. Estos procedimientos serían métodos inter­ nos al objeto en el que funcionan. Por ejemplo, ImprimeRecibo sería un método utilizado en el objeto ATM, y EnviarEstado pertenecería a CuentaBancaria.

Mensajes, métodos y encapsulamiento El paso de mensajes proporciona un medio para que los objetos se comuniquen con un programa cliente, y entre sí. Un mensaje se envía a un objeto, donde un método para responder es seleccionado de los que se tenga disponibles. Un método en un objeto no puede invocar un método en otro, como un procedimiento que llama a otro procedimiento. Un método en ATM no puede tener acceso en forma directa a un método en CuentaBancaria; sin embargo, debe enviar un mensaje a CuentaBancaria (por ejemplo, OK?), el cual responderá utilizando sus propios métodos (por ejemplo, Autoriza). Del mismo modo que un tipo de datos es una plantilla para variables, una clase es una plantilla para objetos. Discutiremos esto en forma adicional en la sección 4.2. Supóngase que declaramos en sintaxis C++ las clases Square y T r i a n g l e como se muestra en los listados (4.1.1) y (4.1.2).

2 La terminología varía de un autor a otro y de un lenguaje a otro. C++ utiliza el término función miembro para denominar un método; por otro lado, Ada no modifica las declaraciones de función y procedimiento usuales cuando trata con objetos.

Sólo fines educativos - FreeLibros

170

PARTE n: Lenguajes imperativos i typedef Int n u m S i d e s : t typedef Int sideLength; # Include <math.h>

(4.1.1)

//para la función sqrt

class Square

{ publlc: //se puede tener acceso a estos métodos desde cualquier lugar en un programa Sq uareísideLength side): s(side), n {4) C3; //constructor sideLength g e t S i d e ( Kreturn s;} sideLength per im e t e r ( K return n * s;3 double a rea C Kreturn (double) s * s;3 p rív a te ://sólo se puede tener acceso a través de métodos de un objeto Square sideLength s; const numSides n; 3:

class T r ia ng l e

(4.1.2)

C publlc: TriangleísideLength side): s(side), n (3) C3: sideLength getSideí) Creturn s;3 sideLength p e r i m e t e r O (return n * s;3

//constructor

double a re a íK re tu rn s q r t (3.0 ) * g e t s i d e O * getSideí) / 4 .0 ;3 prívate: sideLength s: const numSides n;

3: Las partes públicas tanto de Square como de T r i a n g l e enumeran declaraciones para métodos (llamados funciones miembro en C++), mientras que las secciones privadas contienen variables para datos. Estos datos y métodos miembros son los atributos del objeto. Un procedimiento es controlado por los tipos de sus paráme­ tros; mientras que un método también puede usar información contenida en sus datos miembro del objeto (estado) y llamar métodos ya sea públicos o privados para el objeto. Para cualesquiera cuadrado (Square) o triángulo (Triangle), los da­ tos privados incluyen sus longitudes de lado s i deLengths así como el número de lados n . Ni Square ni T r i a n g l e tienen funciones miembro privadas ni datos públi­ cos, aunque otros objetos pueden tener uno o ambos. Un usuario necesita elegir una longitud de lado para cada objeto. Un Square s q u a r e l es construido en sintaxis C++ con un lado de longitud 5, mediante la de­ claración Squa re squa r e í ( 5);. En C++, los objetos son creados cuando se declaran. De este modo un constructor de objeto es un método que tiene el mismo nombre que la clase de objetos por ser construidos. s q u a r e l . p e r i met e r ( ) invocará un método para calcular el perímetro de squar el, mientras que s q u a r e l . a r e a ( ) activa el método para calcular el área. Puesto que am­ bos n y s están ocultos de un cliente ( pr 1vate ), también incluimos ion método públi­ co ge t S i d e entre los atributos públicos de S q u a r e para habilitar un acceso del cliente a s. Esto proporciona un acceso de sólo lectura a s desde el exterior de Square. Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

171

El conjunto de valores de los datos miembro del objeto, o el estado del objeto, persiste entre invocaciones de métodos. Esto significa que si un objeto particular t r i a n g l e l de clase T r i angl e tiene una longitud de lado de uno y tres lados, estos valores permanecerán mientras t r i a n g l e l permanezca en el entorno activo del programa en ejecución. Esto no es verdad con respecto a las variables y constantes locales de procedimiento. Los métodos también pueden tener acceso a datos globales. Éstos pueden estar disponibles para varios procedimientos o métodos y no son parte de ningún estado del objeto. Los datos pueden estar encapsulados junto con operaciones asociadas en un módulo (Modula-2), tipo de datos (CLU) o paquete (Ada). Entonces ¿cómo difiere el encapsulamiento en el sentido orientado a objetos, de lo que hemos visto? Una manera es que un objeto puede incluir datos persistentes y diversos tipos de datos con sus operaciones asociadas. Las figuras 4.1.3 y 4.1.4 más adelante pueden ayu­ dar a hacer más clara la diferencia entre métodos y procedimientos. En la figura 4.1.3, estamos usando el procedimiento per i meter para calcular el perímetro de un triángulo regular con lado de longitud s. Actúa sobre cualquier operando que esté presente con él, en este caso s - 3. El modelo mensaje/objeto de la figura 4.1.4 supone una capa de estructura, el objeto, entre el mensaje y los datos. El mensaje per i meter puede ser enviado a un objeto, que se comporta de acuerdo con su propio método para manejar el mensaje. En la figura 4.1.4, squarel representa un objeto activo con sus dos atributos de datos, n y s, teniendo valores. Los objetos de tipo T r i angl e o S q u a r e tienen cada uno tres métodos: per i meter, a r e a y g e t S i d e . Cada mensaje puede ser significativo para una variedad de objetos diferentes, así que el envío de un mensaje debe in­ cluir el nombre del objeto receptivo. También se puede notar que en las clases T r i a n g l e y S q u a r e se dupliquen definiciones de método y datos. Nosotros seremos capaces de eliminar estas redundancias como se muestra en la figura 4.4.1 cuando examinemos clases y herencia. El término "mensaje" es algo confuso, pero está tan bien establecido en la lite­ ratura orientada a objetos que es probable que permanezca. Un mensaje sugiere que los objetos están actuando de manera independiente y concurrente; y en reali­ dad, los lenguajes actor, tales como Pract y Acore [Agha, 1987], hacen estas suposi­ ciones. En los lenguajes que consideraremos aquí, un método es una función o procedimiento que tiene estado y se encuentra asociado con-una clase de objetos. Un mensaje es el nombre de un método e inicia una llamada a un método.

Llamada

O perandos

perimeter (4, 3 ) ----- ► 4, 3

O perador ►

FIGURA 4.1.3

El modelo operador/operando Sólo fines educativos - FreeLibros

172

PARTE n: Lenguajes imperativos C lases E stado

M étodos

FIGURA 4.1.4 El modelo mensaje/objeto

Primeras nociones de objetos en Simula Simula se originó en el Centro de Cómputo Noruego en 1961, en las manos de Kristen Nygaard y Ole-Johan Dahl. Sus propósitos fueron describir sistemas y pro­ gramar simulaciones [Nygaard, 1981]. Su desarrollo fue motivado por el deseo de: • • • • •

Expresar procesos que son permanentes y activos. Crear y destruir tales procesos como sea necesario. Extender un lenguaje existente para incluir procesos. Proporcionar a los procesos un mecanismo de ejecución en forma concurrente. Agrupar procesos sujetos a los mismos procedimientos en clases.

Los lenguajes de procedimientos separan un problema en datos pasivos y pro­ cedimientos no conectados que los manipulan y que se activan sólo cuando es ne­ cesario. Los procesos (u objetos, como fueron llamados posteriormente) contendrían cualquiera procedimientos relacionados a sus datos, de modo que podrían manipu­ larse ellos mismos como fuera necesario. Un sistema, tal como el de salidas de aeropuerto, se concibió como consistente de componentes de las dos diferentes clases: objetos activos permanentes y objeti­ Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

173

vos pasivos, que actuaban mediante los activos. Los pasajeros son ejemplos del primer tipo, "tomando y manteniendo los dependientes de contador pasivos, co­ lectores de costo, etcétera" [Nygaard, 1981]. En la figura 4.1.5, hay cuatro pasajeros, P., y tres dependientes, C. El pasajero PQestá por cambiarse de la fila de espera del dependiente Cxa la fila más corta del dependiente CQ.Los objetos abstractos fueron pensados como nodos en una red. El trabajo posterior sugiere que los objetos son pensados mejor como de un tipo, que son en ocasiones activos y en ocasiones pasivos, y que esos procesos de interacción forman una mejor noción para objetos que hacen una red, Al principio, Simula era un preprocesador para ALGOL 60, con el código Si­ mula traducido a ALGOL. Esta idea de los objetos que son implementados en la cima de los lenguajes existentes ha sido utilizada en un preprocesador para Ada (InnovAda [Simonian, 1988]) y en extensiones para Pascal (Object Pascal [Tesler, 1985]) y para C (C++ [Stroustrup, 1986]). Los procesos de Simula (objetos) son dinámicos, es decir, pueden ser creados cuando se necesiten y posteriormente ser destruidos. Los procedimientos (méto­ dos) en un proceso difieren del bloque de procedimientos usual. Ellos pueden eje­ cutarse casi concurrentemente3 y contienen declaraciones que solicitan retardos de tiempo. El antiguo operador de Simula pause(<expresión booleana>), que solicita­ ba la suspensión de un proceso actualmente activo hasta que la expresión booleana sea verdadera, causaba tantos problemas que fue abandonada en versiones poste­ riores de Simula por las cuatro directivas passl vate, actívate, hold y cancel. El sucesor de Simula I, Simula 67, tiene clases de objetos como su concepto básico. Dahl y Nygaard habían estado trabajando en una simulación de un puente con una caseta y una cola de camiones, autobuses y automóviles. Ellos notaron que

Po

FIGURA 4.1.5

Red Pasajero (R)/Dependiente(C)

3 Dos o más procedimientos son casi-concurrentes si ellos pueden estar activos al mismo tiempo, y uno no es subprocedimiento del otro. La casi-concurrencia puede ser implementada a través de alguna forma de CPU de tiempo compartido simple, o a través de múltiples CPU con ejecución en paralelo.

Sólo fines educativos - FreeLibros

174

PARTE n: Lenguajes imperativos

un proceso necesario para un camión incluía en gran parte los mismos procedi­ mientos que los de un autobús o automóvil. Desarrollaron una clase de objeto que incluía todas las operaciones de cola; hicieron de Vehí cul o4 una subclase de Col a, y Camión y Autobús subclases de Ve hí cul o. Aunque los objetos V e h í c u l o contienen todos los atributos de los objetos Col a, mientras que Col as no contiene todos los de Vehí cul os, la literatura de objetos llama a una clase mayor en la jerarquía una superclase, y aquellas derivadas de las superclases, subclases. De este modo una Col a es una superclase de las subclases Ve hí cu l o, Camión y Autobús como en la figura 4.1.6. Col a también se conoce como la clase base de Vehí cul o, y este último como la clase base para Autobús y Cami ón, donde una clase base para una clase está inmedia­ tamente arriba de ella en la jerarquía de clase. El concepto de clases de procesos proviene de la noción de Hoare de clases de registros, con procedimientos así como también campos de datos. Cada subobjeto hereda los procedimientos de la superclase. En la figura 4.1.6, Cami ón hereda todos los procedimientos de Col a y todos aquellos de Vehí cul o, con la excepción de C as e­ ta, el cual se redefine para cada Autobús y Camión.

FIGURA 4.1.6 Jerarquía de clase para una simulación de puente de compuerta móvil

Cola new empty? enQueue deQueue timeln timeOut

a

Vehículo toll #passengers license destination a

Autobús size toll

Camión #wheels toll cargo

4 Dahl y Nygaard titularon la clase que hemos llamado Cola, l i g a , y V e h í c u l o , obra han sido renombrados conforme al uso más común de los términos.

Sólo fines educativos - FreeLibros

carro. En esta

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

175

O bjetos en Ada 83 y Ada 95 Dos de las metas principales en el desarrollo de software orientado a objetos son la reducción en el costo y la seguridad. El desarrollo de objetos como unidades de software reutilizable ayuda en cuanto a la reducción en el costo, mientras que el ocultamiento de información promueve la seguridad. Cuando los objetos son crea­ dos y destruidos dinámicamente, la seguridad requiere de arquitecturas de máqui­ na especiales. Las especificaciones de diseño para Ada 95 no requieren de orientación orientada a objetos; sin embargo, sí requiere que toda la verificación de tipo y asig­ nación (ligadura) del almacenamiento para procedimientos sea hecho en tiempo de compilación. Esto es conocido como ligadura estática. De este modo no encon­ traremos ligadura dinámica ni herencia en Ada 83 (véase la sección 4.4). Por el tiempo en que apareció el estándar revisado para Ada 95 [ANSI-1815A, 1983], la experiencia con otros lenguajes orientados a objetos así como con Ada sugería que las cuestiones de seguridad y confiabilidad, importantes para las aplicaciones del departamento de la defensa de Estados Unidos, no serían comprometidas por agre­ gar herencia y ligadura dinámica al lenguaje. La ligadura dinámica fue definida en la sección 1.2, pero tiene un significado extendido cuando se aplica a objetos. Retra­ saremos la discusión adicional de esto hasta la sección 4.4. El ocultam iento de inform ación y la abstracción de datos están ambos implementados en Ada 83. También se proporciona un estado oculto en un objeto. De este modo un procedimiento de objeto en Ada puede rigurosamente ser llama­ do método, lo que haremos aquí, aunque usted no encontrará ninguna mención de métodos en la literatura de Ada. Un objeto se implementa en Ada a través de un paquete (package). Si se recuerda el capítulo 3, un paquete de Ada tiene dos par­ tes: la especificación visible y el cuerpo del paquete oculto. Consideremos el objeto robot de Buzzard y Mudge [Buzzard, 1985]. Examina­ remos sólo los esquemas para declarar los objetos en el listado (4.1.3), dejando los detalles para el Laboratorio 4.1. (4.1.3)

packige Robot 1* typa RobotArm Is llalted prívate; type ArmModel 1s (ASEA,PUMA); type P o s l t i o n Is array ( 1 . . 4 . 1 . . 4 ) of Fl oat :

--almacena l a p o s i c i ó n del RobotArm (Brazo de robot) en el es paci o, y l a o r i e n t a c i ó n — t r i d i m e n s i o n a l de l a pinza en r e l a c i ó n a l as coordenadas del brazo

procedure In111a 11zeArm (x: out RobotArm;

K1nd: In ArmModel}; — pone l os v al or e s i n i c i a l e s en l o s campos de RobotArm para su — p o s i c i ó n en el e s p a d o , si el estado de su pinza está a b i e r t o o cerrado, — y qué c l a s e es

procedure Move ( x ; fn out RobotArm; Oe s ti nat i on ; in P o s l t i o n ) ; — Reubica el Robot al Oe s ti nat i on (Destino)

procedure Open (x: In out RobotArm); — Abre l a pinza

Sólo fines educativos - FreeLibros

176

PARTE II: Lenguajes imperativos procedure Closeíx: In out RobotArm); functlon GetPos i t ion Cx : RobotArm) return Position; — Devuelve la posición actual del Robot prívate — No visible al exterior del paquete type RobotArm 1s record Pos: Position; — Posición del Robot en el espacio O p e n : Boolean; — True (Verdadero) si la pinza está abierta Kind: A r m M o d e l ; — Tipo de modelo de brazo end record; end Robot;

Un Robot se compone de un brazo simple que puede abrir (Open) o cerrar (C i ose) su pinza. Un objeto robot tendrá valores para Pos, Open y Kind, como su estado. Sus métodos para alterar el estado son Ini t i al i zeArm (inicializar el brazo), Move (mo­ ver), Open (abrir), Glose (cerrar) y G e t P o s i t i o n (obtener posición). Supondremos junto con Buzzard y Mudge que las únicas dos clases soportadas por este paquete son ASEA y PUMA, aunque pueden agregarse más. Esta especificación puede ser todo lo que un usuario verá y puede ser compilada en forma separada ya sea de su cuerpo o de un programa que utilice el paquete. Que RobotArm (brazo del robot) sea United prívate (privada limitada) no significa que el usuario no pueda ver la estructura de su tipo. Los nombres del campo de registro (Pos para P o s i t i o n , Open para el valor boolean que indica si la pinza está abierta o cerrada, y Ki nd indicando el modelo del robot) estarían enume­ rados en la especificación, pero un usuario no tendría acceso a ellos excepto a tra­ vés de los cuatro procedimientos I n i t i a l izeArm, Move, Open, C i ó se y la función G e t P o s i t i o n mencionada antes. El estado de este robot simple indica dónde se encuentra, si su pinza está abierta o cerrada y su modelo. Se le solicitará en la se­ gunda parte del Laboratorio 4.1 agregar métodos para que también gire la pinza. El estado de la orientación de la pinza se mantendrá en una submatriz de 3 X 3 de Pos. En la extensión orientada a objetos para Turbo Pascal, la declaración sería como la mostrada en el listado (4.1.4). (4.1.4)

un1t Robot; interface type ArmModel = (ASEA,PUMA); Position = array [1..4.1..4] of real; Arm = record Pos: Position; Open:

boolean;

Kind: ArmModel;

end; RobotArm =

object

A: Arm;

procedure Init(Kind: ArmModel); procedure Move(Destination: Position);

Sólo fines educativos - FreeLibros

CAPITULO 4: Lenguajes para programación orientada a objetos (POO)

177

procedure Open; procedure Cióse; function GetPosition: Position; end; Implementatíon end;

La declaración para un objeto se parece mucho a una declaración de registro, y en realidad lo es. Un objeto de Pascal es un registro, con procedimientos y funciones así como también datos permitidos como campos. Cada uno de los procedimientos en el objeto RobotArm funciona implícitamente sobre el campo Arm, A. Si decla­ ramos: MyASEARobot: RobotArm;

podem os inicializarlo utilizando MyASEARobot. I n i t ( A S E A ) ; m overlo con MyASEARobot.Move(. . . ) ; etcétera. Aunque éstos son llamados procedimientos en sintaxis de Pascal, en realidad son métodos. Sólo pueden ser usados con variables de tipo Arm, que no han sido declaradas como parte de un objeto de tipo RobotArm, y sólo pueden ser activas a través del nombre del objeto, en este caso, MyASEARobot. Object Pascal no tiene facilidades para restringir el acceso a RobotArm. Un usua­ rio puede asignar valores a los campos de una variable de tipo de Arm sin emplear ninguno de los métodos del objeto. Los implementadores suponen que aquellos que programan en un estilo orientado a objetos se autodisciplinarían para usar ejemplos de objetos sólo a través de los métodos incluidos en la definición de obje­ to. La inclusión de un método Ini t fomenta esto. En C++ podríamos declarar un Robot: #1nclude <string.h>

//encabezados para

#1nclude

//módulos de l i b r e r í a s para E/S

en u i boolean [ f a l s e , t r u e l ;

/ / f a l s o = 0, verdadero = 1

enuR armModel [ASEA, PUMA!;

/ / t i p o enumerado

s t r u c t armPositionO C a r m P o s i t i o n ( );

//constructor

prívate: f l o a t pos Í4] C43; f r f e n d ostream & operator << (ostreamO s. const armPositionO pos); f r l e n d i st ream & operator »

íistreamO s, armPositionO pos);

1; c l a s s Robot [ publlc: RobottarmModel

kindln);

//constructor

RobotO movetarmPositionO d e s t i n a t i o n ) ; RobotO c l o s e G r i p p e r O ; RobotO openGri pper(); armPosi ti on g e t P o s i t i o n O ;

Sólo fines educativos - FreeLibros

(4.1.5)

178

PARTE H: Lenguajes imperativos prívate: armPos1t i o n p os l t t o n ; armModel klnd; boolean open;

3; Estas declaraciones serían almacenadas en un archivo de encabezado, r o b o t. h , y también incluidas en el archivo r o b o t . cpp,5 que contiene definiciones para los mé­ todos move, c l o s e G r i pper y openGripper, así como también para los constructores Robot y a r m P o s i t i o n . Las declaraciones del listado (4.1.5) describen objetos que serán miembros de la class Robot. Una class ( el ase) es una plantilla de objeto que tiene variables de datos miembro así como funciones miembro. Uno de los miembros de datos de la c l a s e es una struct ( e s t r u c t u r a ) llamada p o s i t ion del tipo a r m P o s i t i o n . Una struct es notación C++ para un registro. Examinaremos esta struct con más detalle a continuación. C++ tiene tres niveles de protección de miembro. Los elementos publlc de struct o clase son conocidos por los clientes y heredados por subestructuras públicas. Los elementos protected no son conocidos por los clientes, pero son co­ nocidos en las subestructuras. Los elementos prívate, los más restringidos, son conocidos sólo dentro de la clase o struct en las cuales están declarados, y me­ diante frlends de esa struct o clase. En C++, la única diferencia entre una struct y una clase es que la protección predeterminada en una struct es publ 1c, y en una clase es prívate. El mensaje para construir un robot PUMA señalado por rl sería: Robot* rl ■ new Robot(PUMA);

(4.1.6)

Si nosotros no queremos un robot construido dinámicamente, podríamos declarar uno al utilizar: Robot r2(PUMA);

(4.1.7)

El constructor Robot (armModel kindln) es llamado automáticamente cuando rl se declara como en el listado (4.1.6), o r2 en el listado (4.1.7). La definición de la función constructora se encontrará en el archivo C++ robot. cpp. La construcción del robot asigna memoria e inicializa los datos miembros. Como en las declaracio­ nes de Ada y Object Pascal, los detalles pueden ser hallados en la versión C++ del Laboratorio 4.1. La struct armPosition contiene un constructor de su propiedad, que será llamado de manera automática cuando la memoria es asignada para el miembro privado, positlon, de Robot. El cuerpo del constructor será definido en el archivo robot. cpp. Un f rlend no es un miembro de una clase o struct, pero tiene acceso

5 Ambos programas C y C++ casi siempre separan las declaraciones de funciones de las definicio­ nes. El C++ estándar de ANSI pone las declaraciones en archivos de encabezado con la extensión . h. La extensión para el código fuente de las definiciones de funciones depende de la implementación, aquí es .cpp.

Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

179

a sus miembros privados. Las clases i stream y ostream son clases externas para entrada y salida que son declaradas en el archivo i ostream. h. La clase i stream tiene un operador » , que está sobrecargado aquí para tener acceso directo al miem­ bro privado pos de a rmPosi t i on. De manera similar, ostream tiene un operador « , que está sobrecargado para elementos de salida del tipo a rmPos i t i on. Discutiremos la sobrecarga más adelante cuando consideremos otra característica de los lengua­ jes orientados a objetos, el polimorfismo. S i g e t P o s i t i o n está definido apropiada­ mente en r o b o t . cpp, podríamos imprimir la posición del robot en la terminal (cout) en una declaración, como en el listado (4.1.8). cout «

r.getPosition();

(4.1.8)

En ambos casos, a rmPos i t i on& pos significa que los valores para pos sonaccesados por referencia. Estos frlends sirven como intermediarios para i o s t r e a m y a rm P o si t i o n . L A B O R A T O R IO 4.1: O B JET O S, E N C A P S U L A M IE N T O Y M ÉTODOS: OBJECT PASCAL / ADA / C++

Objetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Completar el paquete, unidad o clase Robot a través de definiciones adecuadas para los procedimientos asociados en Ada, Object Pascal y /o C++. 2. Considerar las diferencias entre la implementación del objeto y una en la que se utilice tipos de datos y procedimientos. Los estudiantes deberán poner particular atención a los lenguajes en los que ellos no programan. E J E R C I C I O S 4.1 1. Describa qué se entiende por: a. Ocultamiento de información (encapsulamiento) b. Abstracción de datos (el encapsulamiento del estado con operaciones) c. Paso de mensajes d. Herencia 2. Los procedimientos tienen variables y constantes locales. ¿Cuál es la diferencia entre estas entidades locales y los datos privados para un objeto? 3. Nombre dos maneras en que los mensajes difieren de procedimientos o funciones. 4. ¿Por qué es ventajoso imponer el acceso a un objeto sólo a través de sus métodos? 5. ¿Qué significado puede dar usted a la posición del objeto pasajero, P^ en la figura 4.1.5? ¿Por qué no podría un objeto de dependiente, tal como C,, estar "fuera de línea"? 6. a. En la interfaz para la unidad Robot del listado (4.1.4), ¿Qué representa el estado de un objeto de tipo RobotArm? b. ¿Qué corresponde a la Interface de Pascal en Ada? ¿Y a la lapleaentatlon de Pascal? c. Una especificación de paquete Ada puede ser compilada por separado del cuer­ po del paquete, donde se implementa los procedimientos. Una ventaja de esto es que un programa principal que utilice un paquete sólo necesita la especificación para compilar en forma apropiada; así, el trabajo sobre un programa cliente pue­ de proceder mientras un paquete está siendo completado. Object Pascal no tiene

Sólo fines educativos - FreeLibros

180

PARTE II: Lenguajes imperativos esta facilidad. La interfaz y la implementación pueden ser compiladores por separado desde otro programa, pero no a partir de cada uno. ¿Cómo podría usted conseguir la ventaja establecid a anteriormente de la compilación separada de Ada usando Pascal? 7. a. Si usted desea poner el robot del listado (4.1.4) a funcionar y ya se siente compe­ tente para moverlo, orientarlo y abrir o cerrar su pinza, ¿qué otros objetos podría definirse de modo que realmente pudiera levantar cosas? b. ¿Cómo podrían comunicarse los objetos entre sí?

4.2

CLASES Y POLIMORFISMO Hemos dado un buen fragmento de consideración a los módulos; es decir, coleccio­ nes de tipos de datos relacionados, datos y procedimientos. También discutimos lenguajes que son fuertemente tipificados, donde cada variable es exactamente de un tipo. Una clase es una descripción para objetos aún por instanciarse, de la mis­ ma manera que un tipo es una descripción para variables aún por declararse. Para nuestros propósitos aquí, no se equivocará al pensar en una clase como un tipo abstracto para un objeto conteniendo datos y métodos. El concepto de clase difiere del de un módulo en que permite la existencia de subclases que contienen atributos comunes. Consideraremos más adelante las subclases en la sección 4.4. La noción de clase proviene de la lógica matemática. Una clase es un conjunto, pero estructurado con más precisión. La noción de Georg Cantor de un conjunto como una colección de objetos que comparten ciertos atributos condujo a varias paradojas. Una paradoja es una proposición que es tanto cierta como falsa. Una definición atribuida a Bertrand Russell que conducía a una paradoja es el conjunto A^Cx | x g x). Las matemáticas, por encima de todo, deben ser consistentes y no conducir a paradojas. Así se dio cuenta de que algunos conjuntos no eran válidos. La teoría de clases fue desarrollada para construir conjuntos que eliminarían por lo menos las paradojas conocidas. En los lenguajes orientados a objetos, una clase es una colección de objetos, donde cualquier objeto de la clase incluye los mismos métodos y variables, pero puede incluir diferentes valores de datos. En las declaraciones de tipo anteriores, type RobotArm (listado (4.1.3)), RobotArm = o b j e c t (listado (4.1.4)) y class RGbot (listado (4.1.5)) son descripciones de lo que será semejante un RobotArm (en Ada o Pascal) o Robot (en C++) una vez que una instancia fue creada. De este modo una clase es una plantilla o descriptor para objetos específicos en la clase. La palabra clave de C++ témplate tiene un significado específico, el cual se describe en el MiniManual de C++. También véase el listado (4.2.15) más adelante. Si la paradoja de Russell fuera cierta en clases de objetos, la teoría sería inconsistente. De este modo, ningún sistema orientado a objetos permite una clase que se contenga a sí misma como un miembro. Usted ahora está familiarizado con un objeto, como métodos y datos encapsulados, y con una clase de objetos, todos tienen los mismos atributos. Squa re en el listado (4.1.1) y Ci r c l e en el listado de (4.1.2) son las implementaciones en C++ de dos clases. También hemos examinado cómo los objetos se comunican entre sí a través de mensajes. Como hemos visto, un mensaje puede interpretarse de manera Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

181

diferente si es recibido por diferentes objetos, como s q u a r e l o t r i a n g l e l . E l men­ saje dr aw, que puede invocar diferentes métodos, es llamado polimórfico ("muchas formas"). Usted ya está familiarizado con funciones que hacen cosas diferentes cuando se confrontan con distintos tipos de datos. Un ejemplo soportado por la mayoría de los lenguajes es el operador aritmético +. (1.5 + 3.246) se maneja de forma distinta que (1 + 3) o (1.5 + 3). De este modo, + es un operador genérico o polimórfico. En términos de objetos, + es un mensaje, y existen diferentes métodos para calcular la suma en cada una de las tres expresiones. 1.5 y 3.246 son valores de estado de objetos reales, lo cual incluye un método +. De manera semejante, 1 y 3 son valores de estado de objetos enteros, incluyendo +. El 1.5 y el 3 de la tercera expresión son instancias de objetos mixtos, entre ellos también un operador +. Para conservar todo claro, un lenguaje podría designar los tres diferentes signos de suma como real.+, int.+ y mixto.+, en lugar de solicitar al sistema gue elija el + correcto mediante la verificación de los argumentos en la expresión. Esa sería una manera en forma de procedimientos para enfocar el problema. Cuando un símbolo (token) que representa un operador, por ejemplo +, tiene significados diferentes según el contexto, se dice que está sobrecargado. También exhibe polimorfismo, lo que sig­ nifica que la definición para el operador + tiene una forma diferente, según los datos sobre los que actúa. Procedimientos y paquetes genéricos en Ada Ada 83 y Ada 95 proporcionan procedimientos y funciones polimórficos o genéri­ cos así como también paquetes genéricos. Veamos primero los procedimientos, ya que son algo más simples que los paquetes. Elevar al cuadrado un elemento es un buen candidato, a medida que el proceso se aplica a varios tipos de objetos. Elevar al cuadrado X es X * X, donde * puede interpretarse de manera diferente para ente­ ros, reales, números complejos o vectores. Un subprograma Ada genérico comien­ za con la palabra reservada generlc, como se muestra en el listado (4.2.1). (4.2.1)

generic type Item is private; with function

(x, y: Item) return Item is <>;

function Squaring (x: Item) return Item;

Squaring tiene dos parámetros genéricos, los que deben suministrarse antes que una función real instancia sea construida. El primero es el tipo del Item (elemento) que será elevado al cuadrado, y el segundo es la función de multiplicación, *. La caja < > indica que * será emparejada con una función previamente definida cuan­ do la instancia Squari ng sea construida, como se muestra en el listado (4.2.2).

function Squaring(x:

Item) is

begin return x * x; end;

Sólo fines educativos - FreeLibros

(4.2.2)

182

PARTE n: Lenguajes imperativos

Cuando un compilador Ada encuentra un cuerpo de subprograma genérico, lo ela­ bora, lo cual, en general, no tiene otro efecto que establecer que el cuerpo puede ser utilizado por otras unidades de programa para obtener instancias (véase el listado (4.2.3)).

type Vector Is array (Integer range o ) of Real; functlon CrossProduct (u,v: Vector) return Real Is begin...end;

(4.2.3)

functlon Square 1s new Squaring(Item => Vector, => CrossProduct); functlon; Square 1s new Squaring (Integer); — del listado (4.2.2) — usado como predeterminado functlon Square is new Squaring (Real); S q u a r i n g contendría ahora los elementos S q u a r e ( I n t e g e r ) , S q u a r e ( R e a l ) y S q u a re ( Ve c to r ). Note que la instancia de S qu ar e( Ve ct or ) supone la existencia de la función Cr os sPr oduct . Las instancias del listado (4.2.3) pueden ocurrir sólo en una

sección declarativa del programa, donde las declaraciones de procedimientos y funciones son permisibles. Ada permite la sobrecarga de nombres de procedimien­ to, como podemos ver de los tres diferentes usos de Square anteriores. Puesto que Squari ng nombra una función generlc (polimórfica), para evitar ambigüedades, puede no estar sobrecargada. Los paquetes genéricos son declarados y las instancias son creadas de manera similar. Un ejemplo abreviado para un paquete de pila (stack) genérico se muestra en el listado (4.2.4), y un cuerpo de paquete, Stack, se muestra en el listado (4.2.5). generlc

(4.2.4)

Size: Positive := 100; type Item is prívate; package Stack is procedure Push (1: in Item); procedure Pop

(I: out Item);

Overflow, Underflow: exception; end Stack; package body Stack 1s

(4.2.5)

type Table 1s array (Positive range <>) of Item; MyStack: Table(l..Size); Index: Natural :* 0; procedure Push(E in Item) Is begin...end Push; procedure Pop(E out ITEM) is begin...end Pop; end Stack;

Recuerde que un cuerpo de paquete puede estar oculto para el usuario. Stack es un ejemplo de un ADT, con el tipo de MyStack conocido sólo en el cuerpo (oculto) del paquete. Nótese también que Index se inicializa a cero en la declaración. Las instancias pilas podrían ser creadas utilizando: Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO) package IntStack25 Is new StacktSize -> 25, Item ■*> I n t e g e r ) ; package IntStack 1s new S t a c k d t e m => Integer); — utiliza el valor predeterminado de 100 para Size (Tamaño) package RealStackSO 1s new Stack(50, Real);

183

(4.2.6)

Los diseñadores de Ada tenían como uno de sus objetivos principales la seguridad, de manera que una especificación de paquete genérico compilado no puede ser utilizado por otro programa hasta que el cuerpo del paquete también haya sido compilado. Ada requiere que todos los tipos sean establecidos antes del tiempo de ejecución, de modo que cualquier extravagancia de un paquete genérico debe re­ solverse antes de incorporarse en otra unidad de programas. Para llevar I n t S t a c k a un procedimiento, usaríamos una cláusula wlth para vincular el paquete a un programa cliente, como se muestra en el listado (4.2.7). wlth IntStack;

--se hace visible la especificación IntStack

(4.2.7)

procedure SomeSubprogram Is — declaraciones b e gln,..end SomeProgram;

En el lenguaje Pascal, wlth nos permite omitir la referencia explícita a registros. En Ada, esto se realiza con una cláusula use. Podemos emplear Pus h o Pop en lugar de I n t S t a c k . Push e I n t S t a c k . Pop al anteceder el código con use I n t S t a c k ; . Clases en Object Pascal Object Pascal proporciona clases6de objetos dinámicos. Para los ejemplos en este libro hemos usado la extensión orientada a objetos para Turbo Pascal. Los objetos son de tipo first-class (primera clase); es decir, pueden ser pasados a procedimien­ tos como parámetros y devueltos como valores funcionales. Esto se realiza a través de apuntadores a objetos, llamados referencias. En Object Pascal todo acceso a un objeto es a través de referencias, mientras que en Turbo Pascal 7.0, los objetos pue­ den pasarse como objetos o como referencias a objetos. Una entidad de tipos object puede no ser devuelta por una función, pero una referencia a un objeto, sí puede. Esto no debería causar sorpresa, debido a que los apuntadores, pero no los tipos estructurados, pueden ser valores funcionales en Pascal. Una declaración de Turbo Pascal para una pila podría ser como la que se mues­ tra en el listado (4.2.8). Proporciona pilas con elementos de un solo tipo, en este caso, enteros. 6 Algunos autores han criticado a Pascal debido a que no impone el encubrimiento de información (encapsulación). Los datos de un objeto pueden ser accesados en forma directa; también a través de sus métodos. Un segundo cuestionamiento es que los objetos no son automáticamente construidos durante la declaración de variable. El usuario debe llamar un procedimiento por separado, llamado un cons­ tructor.

Sólo fines educativos - FreeLibros

184

PARTE II: Lenguajes imperativos unit Stacks;

(4.2.8)

Interface const MaxSize type Item = Range = Table =

[visible}

= 1000; Integer; 1, .MaxSize; array [Range] of Item;

Stack = object MyStack Index, Size

: Table; :R a n g e ;

procedure InitCS: R a n g e ) ; procedure P u s h (E : Item); procedure Popfvar E: Item); end; iRplesentatlon

C [

[oculto]

-------

}

Implementaciones de método de Stack

}

[ ---------------------------------------------------------------------- } procedure Stack.InitCS: Range); begln Size ;= S; Index := 0; end; procedure St ac k . P u s h {E : Item); begln 1f Index >» Size then w rlteln ('Error; Pila (Stack) l l ena’ ) else begln Index := Index + 1; MyStacktIndexl E end end; procedure S t ack . P o p (var E: Item); begln 1f Index = 0 then wrlteln ('Error: Pila (Stack) v a cia .’) else begln E MyStack[Indexl; Index := Index - 1 end; end;

Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

185

Object Pascal también proporciona objetos genéricos a través de sus facilida­ des virtuales y de herencia. Para ver cómo se hace esto, primero debemos propor­ cionar una plantilla para I tems con el fin de que sean elementos de la pila, como se muestra en el listado (4.2.9). (4.2.9)

un1t Items; Interface type ItemPtr = A Item; Item - object procedure Display; v irtu a l: end: RealPtr = A RealItem; Real Item = objecttItem) R ; real; constructor Ini t C X : r e a l ); constructor CueR; procedure Display; v irtu a l:

{Solicita el valor de R al usuario}

end; IntPtr - Untlten); Intltem - objectCItem) I ; integer; constructor InitCJ: integer); constructorC u e l ; procedure Display; v irtu a l; end;

(Solicita el valor de I al usuario}

lipleientation

Hay varias cosas que observar aquí. Realltem e Intltem son del tipo objectí Item). Así, los objetos del tipo Realltem o Intltem heredan el método Display del objeto Item. Cada clase de objeto, excepto Item, tiene un constructor, que es necesario para llamar a los métodos virtuales, virtual es una palabra reservada de Object Pascal y Turbo Pascal que indica que un mensaje es polimórfico. Puede haber dife­ rentes métodos para procesamiento de objetos de distintos tipos, pero el mensaje es el mismo. Consideraremos lo que hace el constructor en la figura 4.2.1. Un objeto Real Item es del tipo Item, como lo es de Intltem. Cada uno tiene una Tabla de Método Virtual (VMT; Virtual Method Table), la cual incluye la dirección del constructor para el objeto, llamado Init; también tiene las direcciones para cualquier método virtual, por ejemplo, un método Di spl ay para cada subobjeto. Cuando se enfrenta con una llamada a Display, el compilador de Object Pascal verifica para ver qué tipo de objeto está involucrado en Di spl ay y luego selecciona

Sólo fines educativos - FreeLibros

186

PARTE n: Lenguajes imperativos Item (Elemento)

F I G U R A 4.2.1

Jerarquía de los objetos Item (Elemento)

el método apropiado.7 Item es una clase que no tiene constructores. De este modo no puede haber solicitudes del tipo Item. Una clase de este tipo es conocida como una clase abstracta y sirve como una clase base para Real Item e Intltem. Observe qué diferente es esto de los genéricos de Ada. Los paquetes polimórficos de Ada son declarados en tiempo de compilación, haciendo uso de la función new. No hubo jerarquía de paquetes en Ada 83, y los apuntadores a los paquetes no podían ser pasados como parámetros. Sin embargo, veremos un ejemplo de la adi­ ción de subtipos a Ada 95 en el listado (4.4.1). Cuando utilizamos new con un objeto en Pascal, estaremos creando un apunta­ dor a un nuevo objeto. No obstante, el objeto mismo no existirá hasta que el cons­ tructor sea llamado. Recuerde que un nuevo apuntador de registro Pascal es creado apuntando a un nuevo registro vacío cuando new ( RecordPtr) es llamado, pero no se inicializa. new (ObjectPtr) también reserva espacio, pero el constructor también debe ser llamado, para establecer el VMT y para inicializar cualquier variable con­ tenida en el objeto. Las implementaciones del método I tem se muestran en el lista­ do (4.2.10). (4.2.10)

Iip leie n tatlo n

t -

----------------------------------

t Implementaciones del método de Item c----------------------------------

■3 3 -3

procedure Item.Display; begln end;

[ -------------------------------------------------------------- } í Implementaciones

c

del método de Rea 1 1tem

3

}

7 El método Di spl ay está incluido en el objeto Item, aun cuando no podemos construir una instan­ cia de Item. Esto es necesario debido a que los dos descendentes tienen métodos llamados Di spl ay.

Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

187

constructor Real Item.In1t(X: real); begin R := X end; constructor Real Item.CueR; begin write ('Introduzca un real simple y presione la tecla Enter: ’); readln(X) end; procedure Real Item.Display; begin wr iteln(R:5:2) end;

c--------------------------------------------------------------- 3 {

Implementaciones del método de Intltem

}

C--------------------------------------------------------------- } constructor Intltem.InittJ: integer); begin I := J end; constructor Intltem.Cuel; begin w r i t e { ‘Introduzca un real simple y presione la tecla Enter: ’); readln(I) end; procedure Intltem.Display; begin wr1t e l n ( 1 :5 ) end; end.

*

Ahora estamos listos para ver cómo Items (los elementos) pueden ser incorpo­ rados en Stack (una pila), como se muestra en el listado (4.2.11). (4.2.11)

un1t Stacks; Interface uses Items; const MaxSize - 100; type Range = 0. .MaxSize;

Sólo fines educativos - FreeLibros

188

PARTE n: Lenguajes imperativos Stack - record Table: arrayERange] of ItemPtr; (ItemPtr se declara en Items]

Max: integer end; var Index: Range; procedure InitíM: Range; var S: Stack); procedure Push(var S; Stack; E: ItemPtr); procedure Pop (var S: Stack; var E: ItemPtr); lapleaentatlon procedure In1t(M: Range; var S: Stack); var I: Range; begin

S.Max:- M; Index;- 0; end; procedure P u s h (var S: Stack; E: ItemPtr); begin trlth S do begin I f Index - Max then writelní'Error: Pila (Stack) Llena') else begin Index :- Index + 1; TableEIndex] E end end end; procedure Popí var S: Stack; var E: ItemPtr); begin I f Index - 0 then writelnf'Error: Pila (Stack) Vacía’) else begin E S.TableEIndex]; Index :- Index - 1 end end end.

Las pilas (Stacks) no contienen otros objetos más que aquellos importados de Items. Tiene sentido incluir Stacks en una un1t para encapsular procedimientos y datos de pila. Deberíamos mencionar que las unidades Pascal no imponen oculta­ miento de información de ADTs u objetos. Uno puede tener acceso a la pila directa­ mente, en vez de sólo a través de Ini t, Push y Pop. Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

189

Y por último, el listado (4.2.12) muestra un programa Pascal que utiliza tanto una pila real como una entera. pr ogr ai StackDemo;

(4.2.12)

uses Stacks. Items; var RealStack IntStack AReal Anlnt ItemP

Stack; Stack; RealPtr; IntPtr; ItemPtr;

begln (Un ejemplo de RealStackl S t a c k s . I n i t U O , Rea 1 S t a c k ); newíAReal, CueR); P u s h (R e a l S t a c k , AReal); newíAReal,CueR); P u s h (R e a l S t a c k , AReal); Popí R e a l S t a c k , ItemP); I t e m P * .Di s p l a y ; Pop(RealStack, ItemP); I t e m P * .Di s p l a y ; (Un ejemplo de IntegerStack) Stacks.Init(5, IntStack); newíAnlnt, Cuel); C... con cambios apropiados) end.

Ésta no es en realidad una manera muy orientada a objetos para implementar una unidad de pila (stack), puesto que Stack misma no es un objeto. En la sección 4.4 veremos cómo implementar un objeto Stack, después de haber discutido la he­ rencia.

Clases en C++ Como un ejemplo de clases en C++, utilizaremos el código de la figura 4.4.1, que aparece en la sección 4.4. Ya hemos visto un ejemplo de declaraciones para clases C++ en el listado (4.1.5), y ahora examinaremos otra clase, Polygon, la cual incluye las subclases Squa re yTriangle. Polygon tiene cuatro métodos: uno para calcular el perímetro de un objeto poligonal regular, otro para calcular su área, y dos para hacer el número de lados y la longitud del lado visibles para un cliente. Square y Tri angl e utilizarán el mismo método de perímetro, pero tienen métodos de área

Sólo fines educativos - FreeLibros

PARTE II: Lenguajes imperativos

190

más eficientes. La clase Polygon soporta polimorfismo, puesto que sus métodos son diferentes pero apropiados para tres distintos tipos de objetos. #1nclude <math.h> #def1ne PI 3.1415926536 typedef 1nt n u m S i d e s ; typedef Int s i d e L e n g t h ; class Polygon í numSides n; sideLength s;

(4.2.13) //número de lados //longitud del lado

//privado

protected: //puede utilizarse por Polygon y cualquiera de sus subclases double sqr(sideLength) consttreturn ((d o u b l e ) t ) * t ;} publlc: Polygon (numSides m, sideLength t) : n(m), s(t) (1; sideLength per i meter O const Creturn n * s;3 virtual double a r e a O t return n * sq r(s)/4.0/tan((double)(n-2)/(2*n)*PI);3 sideLength g e t S i d e O const Creturn s ;} numSides g e t N u m S I d e s C ) const Creturn n ;} virtual ~Polygon()(}; 3; class Square: publlc Polygon C publlc: Square (sideLength side): Polygon(4,s)C3: double a r e a O const C return sqr(get$ide());3 ~Square()C3; 3; class Triangle: publlc Polygon C publlc: TrianglefsideLength side): Polygon(3,s )(3; double a r e a O ; ~Triangle()C3; 3;

//constructor //función de área redefinida //destructor

//constructor //función de área redefinida //destructor

Hay unas pocas construcciones C++ nuevas que notar en el listado (4.2.13) que no estaban en el listado (4.1.5). Primero está la designación virtual que precede la de­ claración de función miembro (método) a rea. Cualquier función puede ser redefinida en una clase derivada (subclase) si se desea, pero una función virtual es aquella que está disponible para cualquier clase derivada dentro de una jerarquía de objetos, haya o no sido redefinida dentro de esa clase, y que está disponible para fijación dinámica, si es necesario. Discutiremos tiempos de ligadura en la sección 4.4. Una función (y también la clase en la que está declarada) es v1 rtua 1 pura si está declara­ da, y definida como 0 en una superclase. No puede ser llamada, por supuesto, hasta que es redefinida, lo cual sería necesariamente en una clase derivada de la superclase.

Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

191

También hemos agregado una sección protected, que contiene una función, sqr. Esta función sqr es llamada desde a rea ya sea en Polygon o Square, pero no del exterior de las clases. Ambas clases Square y T r i a n g l e tienen acceso a todas las declaraciones y métodos no privados de su superclase Polygon, entre ellos per i meter.

Como se discutió en la sección 4.1, las propiedades declaradas como prívate están accesibles sólo dentro del objeto en que se hallan declaradas, mientras que las pro­ piedades protected también son accesibles para las subclases del objeto donde están declaradas. De este modo, si necesitamos el valor del atributo s privado para un objeto Square o T r i angl e, debemos obtenerlo mediante el envío del mensaje getSide O.

Si hubiésemos declarado a rea como en el listado (4.2.14), a rea habría sido una función miembro virtual pura, que no puede ser llamada hasta ser redefinida en una subclase. virtual double area() = 0;

(4.2.14)

La asignación de 0 a a r e a ( ) indica que es una función nula en ese punto y será ligada en forma dinámica a una definición cuando un objeto en una de las subclases definidas sea creado. Cualquier clase que contenga una función miembro virtual pura es también virtual, así que no puede haber objetos de tipo Polygon. Una clase virtual también es llamada una clase abstracta y sirve como una clase padre para miembros objeto de sus subclases. Discutiremos funciones y clases virtuales de manera adicional cuando consideremos ligadura dinámica, a rea es definida como una función en línea en la clase Square y se deja ser definida en cualquier lugar en Triangle.

Ahora examinaremos el listado (4.2.15) para ver cómo puede ser declarada una pila genérica en C++ con el fin de realizar el mismo trabajo que hemos dirigido en Object Pascal en los listados (4.2.11) y (4.2.12). //intstack.h

(4.2.15)

//Define la clase genérica Stack empleando un arreglo de tamaño 10 témplate

class Stack í pub Tic: Stack ( unslgned Int sizeln - 10): top(0), size(sizeln), itemsínew T tsizel) (}; ~Stack() tdelete □ items;}; vold pushCconst T &item); T popí); Int isEmptyí) const Creturn top 0;} prívate: const unslgned Int size; T *items; unslgned 1nt top;

}; //stack.cpp //definiciones para los métodos push y pop

Sólo fines educativos - FreeLibros

192

PARTE H: Lenguajes imperativos te ip la te

vold S t a c k < T > : :push(const t &item) C 1f (top >=* size) C cerr « “ Pila (Stack) llena\n"; ex1t(EXIT_FAILURE);

3; items[top++] * item;

3; te ip la te

T St a c k::p o p ( ) { If (isEmptyO) C cerr « “ Intentando extraer y vaciar pila (Stack)\n” ; extt(EXIT_FAILURE);

3; return ItemsC— t o p l ;

3:

Aquí hemos utilizado la construcción teap 1ate de C++, donde una copia por sepa­ rado del código teiplate se hace para cada objeto que utilice Stack. Una pila de enteros sería declarada por typedef Stack

IntStack; y una pila de reales por typedef Stack<double> Real Stack;. En este punto, la plantilla sería copiada con 1nt o double sustituidas para T, dondequiera que se presente.

Im plem entación de clases heredadas

En C++, la mayoría de las implementaciones utiliza una tábla-v (v-table) para loca­ lizar código para métodos. Es definida para cada objeto cuando es creado y contie­ ne una lista de apuntadores hacia funciones virtuales. De este modo la tabla-v para un objeto triángulo (figura 4.4.1) enumeraría la dirección donde el código para la función a rea pueda hallarse. Puesto que a rea es una función en línea en la clase Squa re (listado (4.2.13)), no es necesaria una entrada en la tabla-v puesto que sería tratada como una macro y expandida en línea cuando fuera encontrada. Aquí el balance comparativo es la velocidad contra espacio. La tabla-v es similar al VMT de Pascal como fue mencionado antes. Smalltalk utiliza un diccionario de mensajes para búsqueda de método. Cuan­ do un mensaje es enviado hacia un objeto Smalltalk, el objeto busca el mensaje en su diccionario. Si el método es encontrado, se invoca. Si no, la búsqueda continúa en la jerarquía de herencia hasta que se encuentra un método para llevar a cabo el mensaje. Esto puede hacer que Smalltalk se ejecute con lentitud, ya que la búsque­ da de un método hace un promedio de 1.5 veces el tiempo que se toma para una llamada de un subprograma [Booch, 1994]. En Ada 95, la herencia es implementada mediante registros tagged. La declara­ ción: Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

193

package Polygons 1s type Polygon 1s tagged record S : Float; N : Integer; end record; functlon Per i m e t e r ( P : 1n Polygon) return Float; functlon AreatP: In Polygon) return Float Is abstract; end Polygons;

proporciona una clase base para las clases derivadas, T r i angl e y Square. La desig­ nación tagged anuncia que la clase Polygon puede ser extendida y que el tipo de instancias del tipo Polygon puede distinguirse a través de la etiqueta oculta en tiempo de ejecución. La función Area es declarada como abstract, e impone la redefinición en cada clase derivada. En este caso, las clases derivadas son Squa re y Triangle.

Una clase Square se define como: type Square is new Polygon with record N := 4; end record; function Area(Sq: 1n Square) return Float;

En la terminología de Ada, Square es llamado una extensión t ype de P o l y g o n . Square es una extensión publ 1c, pero podría ser declarada también para ser prívate. La versión de los métodos, Per i meter o A r e a que se llamará, se determina ya sea en forma estática (en tiempo de compilación) o dinámica (en tiempo de ejecución) a través de la etiqueta de control. En el caso dinámico, un mensaje se despacha al cuerpo del método a través de vínculos dinámicos dentro del código new accesado por medio de una etiqueta dinámica, que es un atributo oculto del tipo Square.

L A B O R A T O R I O

4.2: P O L I M O R F I S M O : A D A / C + +

O B J E C T

P A S C A L

/

O hjetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Utilizar los mecanismos disponibles (paquetes, unidades, clases, objetos) para encapsular un ADT. Al hacer este laboratorio, deberá enfatizarse el ocultamiento de información, aun cuando el lenguaje no lo imponga. 2. Programar un método ya nombrado en otro objeto, de modo que actúe de forma diferente sobre el nuevo objeto. Se ejemplificará métodos de sobrecarga y /o virtuales.

E J E R C I C I O S 4.2 1. Explique las diferencias entre las nociones de polimorfismo y clases. 2. ¿Por qué sería ilegal la siguiente definición de clase en C++?

Sólo fines educativos - FreeLibros

194

PARTE

n: Lenguajes imperativos

class Robot

{ public: Robot(ArmModel kindln);

//constructor

Robot& move(const ArmPosition & destination); Robots close(); Robot& open(); armPosition getPosition(); prívate: ArmPosition position; ArmModel kind; Boolean open; Robot babyRobot;

Js 3. Escriba declaraciones genéricas y un cuerpo de función para Vector y CrossProduct del listado (4.2.3), de modo que Squa re tome vectores de cualquier tipo, no sólo vectores reales. Tenga cuidado con la secuencia de creación de instancias. 4. Complete la codificación para Pus h y Pop del paquete Stack del listado (4.2.5). 5. En Object Pascal, un constructor es necesario para establecer la VMT para un obje­ to. Suponga que tenemos tres ejemplos de un objeto de tipo Real Item. ¿Contendrá cada uno una tabla de apuntadores a las dos funciones InitCX) y Di spl ay? Si no, ¿qué contendrá la VMT para una instancia? Si es así, ¿serán estas tablas idénticas o diferentes? 6. Termine el ejemplo IntStack del listado (4.2.12). 7. En el programa StackDemo del listado (4.2.12), ¿por qué teníamos que calificar Stacks. Init, Real Item.Init e IntItem. Init, perono Push o Pop?

4,3 SMALLTALK En principio, "Smalltalk" parece como un nombre extraño para cualquier lenguaje de programación. En sociedad, "small talk" ("charla informal") es la materia de la mayoría de las reuniones. La palabra hace evocar una conversación que está abier­ ta a cualquiera. Puede ser comprendida y comprometida por la gente de varios niveles y orientaciones intelectuales, puesto que trata con temas que son universal­ mente conocidos y acordados, tal como el clima. La charla informal es confortable y fácil con su formato tradicional. No ahonda en detalles. Se desliza sobre la super­ ficie de las ideas. Cuando Alan Kay desarrolló Smalltalk como un lenguaje y una filosofía de programación, su finalidad fue tomar la idea de una "charla informal" y llevarla a la esfera de la computación. Esta viñeta enterará al lector de algunas de las motivaciones de Alan Kay para diseñar Smalltalk, el primer lenguaje desarro­ llado por completo en el estilo POO. Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

195

VIÑETA HISTÓRICA Smalltalk: Alan Kay La historia de Smalltalk™, el lenguaje de computadora, comienza cuando Alan Kay estaba en la universidad de Utah a finales de los sesenta. Él era un hombre con una visión, la de desarrollar una computadora portátil del tamaño de un cuaderno con una capacidad de almacenar miles de páginas de información y ejecutar mi­ llones de instrucciones por segundo. Kay concibió que esta máquina tendría que estar programada en un lenguaje que los no especialistas podrían comprender, uti­ lizar y aprender, a diferencia de otros lenguajes de programación de los sesenta, que fueron orientados hacia especialistas; y aplicaciones que no serían empleadas por aquellos que no fueran especialistas. La computadora debía tener gráficas de alta calidad que podrían hacerla más accesible al usuario. Tendría un teclado, una pan­ talla CRT y un ratón que harían posible que áreas de la pantalla funcionaran como un teclado. Como Barón observó, "para apreciar cuán radical era el componente de hard­ ware de esta visión en esa época, considere el estado de la computación en los sesenta. Todavía no se había escuchado acerca de la computadora personal. Los teclados y las pantallas de CRT eran todavía novedades en un mundo de tarjetas perforadas, y las capacidades gráficas de la mayoría de las macrocomputadoras (mainframes) estaban limitadas a imprimir imágenes de Snoopy a base de patro­ nes de X V ' [Barón, 1986]. Kay previo el uso de su computadora y lenguaje como una herramienta que podría reformar la educación con su habilidad para ayudar a los estudiantes a comprender conceptos y crear otros nuevos. Esta visión educacio­ nal era tan radical como las visiones de hardware de Kay. En los sesenta, el único uso proyectado de la computadora para educación involucraba ejercicios prácticos y de habilidades. Kay comenzó a trabajar en un lenguaje de programación llamado FLEX, un "lenguaje flexible y extensible". Él incorporó ideas del recientemente desarrollado LOGO de Seymour Papert y sus colegas en el MIT; estaba siendo utilizado para enseñar conceptos de programación a los niños. Como LOGO, FLEX mantenía un diálogo abierto e interactivo entre el usuario y la máquina y permitía al usuario crear nuevas discusiones dondequiera que fuera necesario. Después de obtener grados avanzados en la universidad de Utah, Kay fue a trabajar para el Centro de Investigación Xerox en Palo Alto (PARC; Xerox Palo Alto Research Center). Allí continuó trabajando rumbo a su visión. Organizó el Grupo de Investigación sobre Aprendizaje (Leaming Research Group) para trabajar en el desarrollo de su computadora, llamada el "Dynabook", puesto que estaba basada en la recuperación dinámica de información. Su software fue llamado Smalltalk. Un sistema completo se desarrolló al incorporar el hardware y software especiales. La primera versión de Smalltalk fue completada e implementada en 1972. El año de 1973 vio un Interim Dynabook terminado para propósitos de investigación. Smalltalk-72 y este Dynabook fueron empleados en forma experimental con cerca de 250 niños, con edades de los 6 a los 15 años, y 50 adultos. La experiencia con Smalltalk ha conducido a varias revisiones, entre ellas Smalltalk-74, -76, -78 y -80. Sólo fines educativos - FreeLibros

196

PARTE

n: Lenguajes imperativos

El trabajo actual está procediendo a un estándar ANSI para Smalltalk. Junto con la atención que se está dirigiendo a la POO en general, el interés en Smalltalk ha ido en aumento. Smalltalk está destinado como un lenguaje para todos. Sin embargo, existe un problema. Es muy diferente de la mayor parte de los otros lenguajes. Es una pesa­ dilla para los programadores perezosos, puesto que el aprendizaje de un lenguaje basado en conceptos únicos es más difícil que aprender un lenguaje similar a los otros que ya se conocen. Kaeler comenta: "Como un lenguaje, Smalltalk ofrece una metáfora uniforme y poderosa: procedimientos y datos que pertenecen juntos y empaquetados en un 'objeto'. Un objeto interactúa con el resto del sistema al sepa­ rar otro objeto y enviarle un mensaje. La combinación de Smalltalk con buenos editores, una modularización natural del código y un lenguaje basado en una idea poderosa, forma un sistema que está en su mejor momento durante la construcción y evolución de un gran programa de aplicación" [Kaeler, 1986], En 1980, la corporación Xerox comenzó a distribuir Smalltalk-80. Las compa­ ñías que decidieron revisar el lenguaje fueron Apple Computer, Digital Equipment Corporation, Hewlett-Packard y Tektronix. Xerox quiere expandir las comunida­ des de los programadores así como de investigadores de Smalltalk; influir a diseñadores de hardware para mejorar el desempeño de Smalltalk; y establecer un estándar para Smalltalk como un lenguaje de programación orientado a objetos, basado en gráficos [Krasner, 1983]. En 1982 el proceso de revisión fue completado y pudo ser posible publicar material acerca del sistema Smalltalk. En retribución por su ayuda, se dio a las compañías involucradas el derecho de utilizar Smalltalk-80 en sus proyectos de investigación y desarrollo de hardware. Cuando Alan Kay dejó Xerox a principios de los ochenta para trabajar en Apple, rebautizó su grupo de investigación como el Software Concepts Group, reflejando un cambio del enfoque educacional original. Smalltalk como un lenguaje de producción nunca ha despegado, pero ha teni­ do influencia en otros sistemas. El sistema de iconos controlados por el ratón de la Macintosh® de Apple y las ventanas que se traslapan fue un trabajo iniciado por vez primera por Kay para Smalltalk. A diferencia de la charla informal en la con­ versación, el Sistema Smalltalk ha probado ser todo menos algo trivial.

4.4

HERENCIA Y ORIENTACIÓN A OBJETOS Los lenguajes orientados a objetos soportan objetos, clases de objetos y la herencia de los atributos por una subclase de una clase mayor en la jerarquía. Smalltalk es un lenguaje orientado a objetos puro, en el cual todo es un objeto descendiente de una clase abstracta llamada Object. Object (Objeto) no tiene variables de instan­ cia, pero tiene 66 métodos, que son heredados por todos los otros objetos. Estos definen métodos predeterminados para visualizar, copiar y comparar objetos, así como para informar de errores. Ya hemos visto en el listado (4.2.13) un ejemplo de herencia en las declaracio­ nes de nuestras clases C++, Square y Tri angl e, las cuales heredan las funciones Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

197

per i meter, getSide y getNumSides, y los dos miembros de datos s y n, de la clase Polygon. La figura 4.4.1 muestra el mensaje per i meter (perímetro) que se envía hacia el objeto squa re2 en la clase Polygon, el cual corresponde con este método para calcu­ lar el perímetro. square2 ha sido previamente inicializado con una longitud de lados = 3. Todos los Squa re tienen el mismo número de lados, n - 4. Puesto que el objeto en cuestión, square2, es un Squa re, el mensaje p e r i m e t e r debería ser respon­ dido de acuerdo a un método definido en la clase Square. El método per i meter es un atributo de un Squa re, no porque fue declarado allí, sino porque es heredado de la superclase Polygon. Cuando un objeto recibe un mensaje, verifica para ver si existe un método para responder el mensaje. Si no lo hay, verifica la jerarquía de herencia de clase mediante apuntadores, tanto como sea necesario, para encontrar uno. En la figura 4.4.1, sólo existe una superclase para Square, y es allí en Polygon que se encuentra el método pe ri me te r. Clases Polygon (Polígonos) Estado

Métodos

Mensaje

perimeter (square2)

F I G U R A 4.4.1 El modelo mensaje/objeto incluyendo métodos heredados de la clase Polygon

Sólo fines educativos - FreeLibros

198

PARTE II:

Lenguajes imperativos

En nuestro código C++, hay métodos constructor y destructor, que hemos omi­ tido en la figura. Los constructores y destructores no son heredados, como lo son otras funciones miembros, de modo que deben ser proporcionados en cada subclase. La definición: Square (sideLength side): Polygon(4,side){};

//constructor

indica que el constructor para Polygon está por ser llamado para llenar el valor 4 para n y el valor s i d e suministrado por el cliente para s. Aquí, un cuadrado se considera un polígono con cuatro lados. Hereda todos los atributos de un polígono y proporciona su propio método para calcular área. Cuando discutimos herencia y la jerarquía de los tipos de objetos, se utiliza a menudo un ejemplo de animales, como en la figura 4.4.2. La estructura de árbol para la herencia de objetos es semejante a la utilizada para sistemas de clasificación en las ciencias naturales y con la cual muchos usuarios están familiarizados. Esta estructura demuestra las relaciones isA (esUn) y hasA (tieneUn). Un Ave esUn Ani­ mal, que tieneUn EstadoVuelo de verdadero o falso. Puesto que un Ave esUn Animal, también tieneUn hábitat. Hereda este atributo de la clase Animal. Los atri­ butos pueden ser de tres tipos: 1.

2.

3.

Redefinido: un atributo que tiene el mismo nombre que el de uno en una superclase, pero es definido en una subclase. Hábitat es redefinido en la clase Ballena. El hábitat predeterminado sería la constante, "tierra", mientras que el hábitat redefinido de la ballena podría ser uno de los siete mares. Específico: un atributo que es definido en forma única en una subclase. EstadoVuelo, vocabulario, y muchosLadridos son específicos del Ave, Loro y Perro, respectivamente. Nótese que EstadoVuelo es heredado tanto por Loro como Avestruz, puesto que son Aves. Heredado: un objeto posee un atributo que se define sólo en una de sus superclases. Nombre e imagen son heredados a lo largo de la jerarquía. EstadoVuelo es heredado, pero sólo en subclases de Ave.

Los atributos enumerados aquí son variables de instancia, pero los métodos tam­ bién pueden ser heredados, redefinidos o específicos. En nuestro ejemplo Item de

Animal (nombre, hábitat, imagen)

Ave (EstadoVuelo)

Loro (vocabulario)

Avestruz ()

Mamífero ()

Perro (MuchosLadridos)

F I G U R A 4.4.2

Jerarquía de objetos animales [Digitalk, 1986]

Sólo fines educativos - FreeLibros

Ballena (hábitat)

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

199

los listados (4.2.10) hasta (4.2.12), Di spl ay es redefinida en cada subclase, mientras los constructores y variables son específicos. El estilo de la programación orientada a objetos involucra estar bien enterado con las clases que ya se encuentran disponibles, y luego extenderlas para crear otras clases y objetos especializados para la tarea de programación a la mano. Smalltalk-80 (de ParcPlace Systems) está embarcado con más de 240 clases, mien­ tras que Smalltalk/V (de Digitalk) incluye 110. Objective-C (Stepstone) incluye 20. El C++ original no proporciona ninguna, pero el nuevo estándar ANSI prescribe 43. Java define 251 clases organizadas en 8 paquetes. Ada 83 carece de herencia y ligadura dinámica de objetos y no era considerado un lenguaje orientado a objetos. Sin embargo, en Ada 95, la herencia se ha implementado al utilizar tipos etiquetados (tagged types). Sólo los tipos privados y registros pueden estar etiquetados. Como se sugiere, tales tipos estarán discrimi­ nados al tener una etiqueta asociada. Como ejemplo muy simple, volveremos a nuestras figuras geométricas [Ada 9x, 1993]. type Shape is tagged with nuil record;

(4.4.1)

function Size (S: in Shape) return Float is <>; type Rectangle is new Shape with record Length: Float; Width: Float; end record; function Size (R: in Rectangle) return Float is begin return R.Length * R.Width; end Size; type Cuboid is new Rectangle with record Height: Float; end record;

En el listado (4.4.1), Shape es un tipo abstracto, que contiene un registro vacío. S i z e es un subprograma abstracto, que no tiene body (cuerpo), como se indica por la caja < >. Cada una de las subclases R ec tangl e y Cuboid definirá S i z e como sea apropia­ do. S i z e (S: Shape) es la notación de Ada para lo que llamamos un método virtual en Object Pascal. En la función S i z e ( R : R e c t a n g l e ) , R es la etiqueta tanto en R.Length como R . Wi dt h . Puesto q u e C u b o i d es un new Rectangle, hereda Length, Width y la función Si ze de la clase Rectangl e. También tiene Hei ght. Si queremos redefinir S i z e a algo más apropiado para un Cubo i d, podríamos redefinirlo como se muestra en el lista­ do (4.4.2). function Size (C: in Cuboid) return Float is begin return Size(Rectangle (C)) * C.Height; end

Size;

Sólo fines educativos - FreeLibros

(4.4.2)

200

PARTE n :

Lenguajes imperativos

Nótese que el Cuboi d C del listado (4.4.2) se convierte en su tipo padre Rectangl e cuando se calcula Size(Rectangle (C)).

Tipos y subtipos en jerarquías de herencia

Hagamos referencia una vez más a la jerarquía de objetos animales en la figura 4.4.2. Supóngase que declaramos en C++ un objeto particular sal ty (salado) para que sea un Loro. Entonces sal ty esUn Loro. También esUn Ave y esUn Ani mal. // Asignación de memoria para un objeto Loro, salty Parrot salty;

(4.4.3)

// Declaración de a y b como tipo Ave y Animal, respectivamente. Animal a; Bird b;

Entonces podemos hacer las asignaciones: a * salty; b = salty; a = b;

(4.4.4)

Pero el inverso, salty = a; salty = b; b = a;

(4.4.5)

señalaría errores. Esto se conoce como el principio de subtipo, el cual establece que un objeto de un subtipo puede ser usado dondequiera que su supertipo sea legal. Se supuso que un objeto de un supertipo no puede ser utilizado en cualquier lugar donde un subtipo es legal. En el listado (4.4.4), suponemos que a y b son supertipos legales, así que los subobjetos, sal ty y b, también pueden ser utilizados. La situación con los apuntadores a objetos es algo diferente. Supongamos que hacemos las siguientes declaraciones en C++: Loro * s a l t y P t r ; Ave *bPtr; Animal *aPtr;

(4.4.6)

La memoria está asignada para tres apuntadores. La memoria para los objetos Ani mal, Ave y Loro puede ser asignada empleando el operador new. new aPtr; new bPtr; new saltyPtr;

Las asignaciones siguientes entonces serán legales: aPtr

= saltyPtr;

aPtr

= bPtr;

bPtr

= saltyPtr;

(4.4.7)

Sin embargo, sólo aquellos miembros de sal tyPtr* que también son miembros de a* pueden ser accesados a través de a Pt r. Esto es, un apuntador a una clase base puede tener acceso sólo a los miembros de la clase derivada que también son miem­ bros de la clase base. Si deseamos un apuntador hacia una clase base para tener acceso a todos los miembros de la clase derivada, debe hacerse una conversión explícita si la clase base es virtual, como se muestra en el listado (4.4.8). Sólo fines educativos - FreeLibros

C A P ÍT U LO

4: Lenguajes para programación orientada a objetos (POO) Novela (Novel)

Historia (Story)

t

t

201

i Libro (Book)

F I G U R A 4.4.3 Herencia múltiple s a l t y P t r - dynan1c_cast

( aP t r ) ;

(4.4.8)

Lo que sigue es ilegal: saltyPtr = aPtr; bPtr = aPtr; saltyPtr * bPtr;

(4.4.9)

Las asignaciones del listado (4.4.7) establecen que las clases derivadas, Ave y Loro, tienen clases base publ 1c. Si la clase base es prívate en la clase derivada, como ocurre en el listado (4.4.10), entonces a P t r = b P t r no sería legal, puesto que los miembros públicos de Animal no son públicos en Ave. c l a s s Animal C / * . . . * / 3 :

(4.4.10)

c l a se Ave: Animal t/*nombre, h á b i t a t , imagen y miembros no pú bl ic os de Ave*/}

Herencia múltiple Hasta ahora hemos visto clases que proporcionan herencia estructurada en forma ramificada, con descendientes que exhiben una relación del tipo esUn con un pa­ dre. Algunos objetos podrían heredar en forma apropiada desde múltiples padres; por ejemplo, un Libro esUna Novela (Novel) y un Libro esUna Historia (Story) tiene buen sentido, así que Libro podría heredar atributos y métodos de Novela así como de Historia. La estructura de herencia sería como la que se ilustra en la figura 4.4.3.

F I G U R A 4.4.4 M s g S q u a r e h e r e d a d o d e Squa re y Message

Sólo fines educativos - FreeLibros

202

PARTE II:

Lenguajes imperativos

El lenguaje Eiffel [Meyer, 1988], el Common Lisp Object System (CLOS) y la versión 2.0 y superiores de C++ soportan, todas, herencia múltiple. Se debe ser conceptualmente cuidadoso al diseñar clases que se hereden de múltiples padres. Antes que nada, la relación <descendente> esUn <padre> debería mantenerse. EsUn restringe sus descendentes para que sean del mismo tipo de objeto como cada uno de sus padres, mientras que un descendente puede extender el tipo padre para incluir nuevas variables y métodos. Estas nociones deben preservarse para mante­ ner los diseños comprensibles y claros. Un problema potencial para un objeto descendente son los conflictos de nom­ bres entre métodos. ¿Qué ocurre si Novel y Story tienen cada uno un método ListPlot? Esto puede ser manejado en el descendente, pero no es difícil. Eiffel lo resuelve al introducir un operador renome como se muestra en el listado (4.4.11). class Book export...Inherlt Novel; Story renaae ListPlot as StoryLine

(4.4. 11)

♦ ••

end

— clase Libro (Book)

Examinaremos una adición a las declaraciones para nuestra clase Square de C++ de modo que podamos escribir mensajes dentro de cuadrados, como se mues­ tra en la figura 4.4.4. Un programa C++ utilizando MsgSquare se muestra en el listado (4.4.12). #1nclude

#1nclude <string.h> #1nclude #include <math.h> # de f 1ne PI 3.1415926536 typedef int sideLength; typedef int n u m S i d e s ;

//Declaraciones de librería de gráfi cos8 //Funciones de librería de cadenas //para la consola de E/S

(4.4.12)

class PolygonI numSides n; sideLength s; public: Polygon (numSides m, sideLength t): n(m), s ( t K 3 ; //otras declaraciones como en el listado (4.2.13) virtual void s h o w O í l : ~Polygon() II; //destructor

3; class Square: public PolygonI public: 8 Las librerías están suministradas con Turbo C++®. La clase Message está adaptada de una similar que se localiza en [Turbo C++, 1992].

Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

Square (sideLength s): Polygon(4,s) C 3; //otras declaraciones vold s h o w O C ) ; ~$quare(K3;

203

//constructor //método para dibujar un cuadrado //destructor

class MessageC char *msg; //mensaje que será exhibido 1nt font; //fuente de gráficos declarada en graph.h Int field; //tamaño del campo de mensaje Int x, y; //ubicación del mensaje publlc: Me ssageílnt startX, Int startY, Int msgFont, Int fieldSize, char *text): msg(text), f o n t ( m s g F o n t ) t field(fieldSize), x(startX), y (s t a r t Y ) C 3: //constructor vold show()C3; //exhibición del mensaje

3; class MsgSquare: Square, MessageC //hereda tanto de Square como de Message publlc: Ms gSq uareCsideLength side, Int x, Int y, Int font, Int size, char *m): Sq u a r e ( s i d e ) , M e s s a g e C x , y, font, size, m ) C 3: //constructor vold M s g S q u a r e : :s h o w ( K S q u a r e ::s h o w ( ); //dibuja el cuadrado M e s s a g e ::s h o w ( ); //muestra el mensaje 3: 3; ■alnO C initgraphí. . //Inicializa el controlador gráfico MsgSquare mSquare (5, 10, 20, GOTHIC.FONT, 5, “HI ! M ); //declara un cuadrado de lado = 5 y el mensaje comenzando en (10, 20) mSquare.showí): return 0 ; 3:

Observe que la clase MsgSquare del listado (4.4.12) hereda Squa r e así como de Message. Cuando se construye mSqua re, utiliza el constructor de Squa re para estable­ cer el s i d e L e n g t h (longitud de lado) que, a su vez, utiliza el de Polygon para es­ tablecer el número de lados. También hace uso del constructor de Message para localizar dónde van a estar el mensaje y el cuadrado, la fuente, tamaño de campo y el mensaje mismo. Cuando el mensaje mSquare .show se envía, se invoca el método de MsgSquare. Éste llama los de Message y Square, y utiliza el operador de resolu­ ción de alcance : : para decidir cuál método show emplear. Hemos dejado Square :: s h ow y Me s sa ge :: show con definiciones vacías aquí, a medida que ellos requie­ ran de familiaridad con las clases de gráficos. Shopiro [Shopiro, 1989] discute clases implementadas de herencia múltiple desde la librería iostream, la cual suministra utilidades de E/S. Diez clases Sólo fines educativos - FreeLibros

204

PARTE

n: Lenguajes imperativos

interconectadas han sido diseñadas para especializar las clases base mostradas en la figura 4.4.5 para archivos. Esto proporciona un buen ejemplo del uso de herencia para restringir la E/S general para archivos y con el propósito de extender clases al suministrar métodos especializados para archivos. i ostream hereda tanto de i stream, que contiene métodos de entrada, como de ostream, que tiene métodos de salida, como se muestra en la figura 4.4.5. No tiene variables o funciones en absoluto, pero hereda todos sus atributos de i o s :: i s t re a m 0 de i o s :: ostream. i o s es una clase abstracta, que contiene sólo métodos virtuales, los que están implementados en uno de los i s t r e a m u ostream. s tr eambuf también es una clase, a la que *str eambuf apunta. La mayor parte del trabajo de E / S real está incluido en str ea mbuf o en otras clases especializadas, i os decide si se hace entrada o salida, y efectúa la conexión a streambuf a través de un apuntador, bp. 1stream contiene una función de entrada llamada bp->get(), y ostream tiene un método de salida bp->put(c). De modo que, ¿cuál es la ventaja de ser capaz de pensar en i os t r ea mya sea como un flujo de entrada o un flujo de salida? Antes de la herencia múltiple, había sólo una clase de flujo en C++. Era solamente en tiempo de ejecución que una operación inapropiada, tal como intentar escribir a un flujo de entrada, podía identificarse. La herencia múltiple permite que las dos clases de flujos sean separados en i s t r e ams y ostreams. Esto podría haberse hecho sin herencia múltiple al copiar en forma única sobre código compartido por los dos diferentes tipos de flujo. Cuando estos objetos están especializados para archivos, la utilidad real surge, puesto que hay más código común, como muestra la figura 4.4.6. Shopiro mencio­ na que el código C++ que implementan los objetos de las figuras 4.4.5 y 4.4.6 "no es un ejemplo lo suficientemente práctico de herencia múltiple en C++, porque la fa­ cilidad con que describe es demasiado simple para ser útil" [Shopiro, 1989], ¡Que los programadores orientados a objetos estén prevenidos!

¡os

FIGURA 4.4.5 Herencia múltiple en C++ Sólo fines educativos - FreeLibros

streambuf

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO)

205

Ejemplares de lenguaje Ahora estamos listos para examinar lo que se conoce como lenguajes orientados a objetos: aquellos que soportan ocultamiento de información, abstracción de datos, paso de mensajes y herencia. Objetos + clases + herencia = orientación a objetos. Del comentario de Shopiro que se expuso líneas atrás, podría parecer que la programación orientada a objetos no es en realidad el problema. Es la herencia la que desmiente esta conclusión. Aun cuando tengan trucos para escribirse, las clases son reutilizables. Una vez que han sido verificadas, crear nuevas clases y objetos a través de la herencia sería más fácil que comenzar desde un borrador cada vez. Sin embargo, aprender un lenguaje orientado a objetos no será una tarea breve, porque se debe aprender y comprender librerías de clase a fin de elegir cuáles cons­ truir en forma efectiva. Los lenguajes POO son, o bien puros, tal como Smalltalk, o híbridos, como C++ y Object Pascal. Los lenguajes híbridos han sido construidos en la cima de los lenguajes existentes y atraen una camarilla de programadores

streambuf

ios *streambuf A

A

^

ostream

fstreambase

istream

A

A

t

A

i

A

ofstream

¡fstream

iostream

k fstream

F I G U R A 4.4.6 Especialización de 1os para archivos

Sólo fines educativos - FreeLibros

A

filebuf

206

PARTE

n: Lenguajes imperativos

experimentados en el uso del lenguaje base. Los hábitos mueren con lentitud, y los programadores orientados a objetos llevarán a cabo poco mejoramiento en produc­ tividad si únicamente lanzan unos cuantos objetos dentro de un programa estruc­ turado en bloques. Se debe aprender a pensar en términos de objetos en lugar de procedimientos. La POO tiene algunas desventajas de eficiencia. Las clases utilizan espacio ex­ tra para conservar las tablas de método virtual (VMTs; Virtual Method Tables). Los apuntadores desde instancias de objetos dentro de la VMT también deben ser man­ tenidos. El acceso a los métodos a través de al menos dos apuntadores hace los programas de POO ejecutarse con algo más de lentitud que sus contrapartes de procedimientos. Los investigadores también han notado algo llamado el "efecto de yo-yo", en el cual la ejecución que involucra un objeto que hereda métodos de sus clases antecesores se mantiene rebotando hacia arriba y hacia abajo de la jerar­ quía de clase para encontrar cuál método utilizar. Como un ejemplo elemental, considere la jerarquía de objetos de la figura 4.4.4 y el listado (4.4.12). Aquí, cada clase de objeto tiene un método llamado por el mensaje show. Si un objeto de tipo MsgSquare recibe el mensaje para show mismo, la versión de s how definida en la clase MsgSquare primero llama la definida enMessage seguida por la de Square. Esto involucra seguir el apuntador de MsgSquare hacia Message, de regreso a MsgSquare, de vuelta a Square, que puede o no (dependiendo de la implementación) referirse a Polygon, que nos dirige de regreso hacia Square, puesto que show como definido en Polygon es virtual. La implementación es refe­ rida, por último, de regreso a MsgSquare para terminar. Este comportamiento su­ giere árboles de ancestro de pequeño tamaño o alguna clase de optimización, de modo que el árbol entero no necesita ser atravesado cada vez que un método dis­ tante es accesado.

M ás de O bject P a sca l Reescribamos la unidad Stack (pilas) del listado (4.2.11) en un estilo más de POO. Comenzaremos muy al principio y dejaremos a una clase Stack heredar algunos de sus métodos desde una clase List más general. El estilo de programación orien­ tado a objetos incluye utilizar clases que ya han sido probadas y depuradas. El listado (4.4.13) proporciona una clase List de Pascal llamada, obviamente, List. (4.4.13)

uses Items; type NodePtr * ANode; Node = record Item: ItemPtr; Next: NodePtr end;

Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

207

ListPtr * A List; List = object Nodes: NodePtr; constructor Init; destructor Done: virtual; procedure A d d A t F r o n t d : ItemPtr); procedure AddAtRearíI: ItemPtr); procedure A d d A f t e r U , Loe: ItemPtr); tAgrega I después del nodo al que apunta Loe) procedure DeleteFromFront; procedure D e l e t e F r o m R e a r ; procedure DeleteAfterCLoc: ItemPtr); (Elimina el nodo después del que apunta Loe] procedure Report; end; var ItemList: List;

Una Li st, entonces, es una lista de apuntadores hacia nodos. En este caso, cada nodo contiene dos apuntadores, uno para el Item (elemento) en la lista y el segun­ do hacia Next (siguiente), como se muestra en la figura 4.4.7. Como hemos visto, los objetos Item pueden ser polimórficos; así la Li st ante­ rior, apuntada por Nodes, puede incluir Items, Real Items e Intltems, o cualquier otra clase de elementos si deseamos crear más descendentes de Item. H e m o s visto ya constructores, pero un destructor es nuevo. C o m o el nombre lo sugiere, destruye un objeto existente después que pasamos por él. Del mismo modoquenew era extendido para inicializar objetos, como en new( Real Item, CueR), dlspose puede ser utilizado con dlsposeCItemList, Done). Un destructor limpia cualquier campo de apuntador en un objeto y cualquier apuntador heredado des­ de objetos antecesores. También dispone de los apuntadores VMT y llama a dlspose para liberar almacenamiento ocupado por el objeto. La implementación para un destructor de Li st en Pascal 7.0 se muestra en el listado (4.4.14).

FIGURA 4.4.7 Una Li sta circular de apuntadores a objetos Sólo fines educativos - FreeLibros

208

n: Lenguajes imperativos

PARTE

(4.4.14)

destructor List.Done; var N: NodePtr; begin while Nodes <> nil do begin N :* Nodes; dispose (NA .Item, Done); Nodes:* NA .Next; dispose (N); end end;

En la segunda línea de la declaración while, llamamos d1spose(NA .Item, Done). Este Done se refiere a un destructor, Item. Done, no para Li s t . Done, así que debe­ mos agregarlo al object (objeto) Item. En este caso, el destructor no necesita hacer nada, sólo las operaciones invisibles de eliminación de apuntadores VMT (véase el listado (4.4.15)). (4.4.15)

type ItemPtr = A Item; Item = object procedure Display; virtual; destructor Done; end; destructor Item.Done; begin...end;

List.Add agrega un nuevo Item al frente de la List, mientras que List.Report toma a su cargo la salida de Items. Esto se dejará para el Laboratorio 4.3. La clase Li st es una plantilla para Items de cualquier objeto de tipo. Un pro­ grama cliente debe tener acceso sólo al tipo de Node: Node

= record

(4.4.16)

Item: ItemPtr; Next: NodePtr end;

Podemos no querer cambiar este registro, así que podemos nombrar una clase abs­ tracta encabezando los objetos que queremos incorporar en nuestra lista Item. La otra alternativa es cambiar el primer campo de Node a SomeOtherltem : SomeOtherPtr, lo que puede requerir hacer cambios a los métodos de List. Apuntara SomeOtherltem con un apuntador llamado ItemPtr es quizá lo más flexible que podemos ser. Por último, nuestro objeto pila se muestra en el listado (4.4.17). (4.4.17)

uses List; StackPtr = A Stack; Stack

= object(List)

constructor InitStack; destructor Done; procedure Push(Item: ItemPtr);

Sólo fines educativos - FreeLibros

CAPÍTULO

4; Lenguajes para programación orientada a objetos (POO)

209

procedure Pop; procedure Report; implementation constructor Stack.InitStack; begin List.Init end; destructor List.Done; begin List.Done end; procedure Stack.Push(Item: ItemPtr); begin List.AddAtFront(Item) end; procedure Stack.Pop; begin Li s t .Del eteFromFront end; procedure Stack.Report; end; var S: Stack;

H erencia en C++ C++, como Object Pascal, está construido sobre un lenguaje existente. Es decir, cual­ quier programa en C, después de cambios menores, se ejecutaría sobre un compilador C++. Con unas cuantas excepciones, C es un subconjunto de C++. Una de las metas al escribir C++ era la eficiencia. En parte, C fue escrito para eliminar la necesidad del código en lenguaje ensamblador. Las manipulaciones de bits están incluidas justo en el lenguaje, y proporciona compilación y traducción rápida y la eliminación de llamadas a procedimientos en lenguaje ensamblador. Los progra­ mas en C++, aunque incluyen características de mayor nivel que C, pueden utilizar la misma librería en tiempo de ejecución desarrollada para C. C++ agrega el tipo class a los tipos derivados y simples de C. Continuando con nuestro ejemplo de pila, examinaremos una declaración para una lista ligada en C++, que sirve como una clase abstracta [Stroustrup, 1986]. Primero, necesita­ mos definir un tipo de elemento, una clase de nodo para objetos manteniendo un elemento, y un apuntador a la liga siguiente (listado (4.4.18)). Como en nuestra implementación de Pascal, el primer campo de liga es un apuntador hacia algún objeto que se definirá más adelante. (4.4.18)

typedef vold* itemPtr; class Node C frlend class List; frlend class Listlterator; pub 1fe Node* next;

//tiene acceso a miembros privados de itemPtr

Sólo fines educativos - FreeLibros

210

P A R T E n: Lenguajes imperativos itemPtr e; NodeíitemPtr a, Node* p) Ce - a; next - p: };

3; vold sirve como el tipo base para un apuntador. De este modo vold* itemPtr

declara a itemPtr como un apuntador a cualquier tipo que necesitemos utilizar más tarde. Apuntar a un tipo no especificado a través de vold* es idiomático para C++ y puede conducir a errores; de este modo no está soportado por lenguajes como Ada o Smalltalk. next apunta a Node. La función Node es el constructor para un objeto de tipo Node y asigna a a la variable de instancia e, y p al nodo apuntador next. Cuando una variable es declarada para ser de tipo Node, el constructor es llamado en forma automática. double* x; node a(x, O);9

/ /x mantiene un apuntador a un real

Esto inicializa a de la siguiente forma:

Hasta ahora nada parece diferente de nuestra implementación en Pascal pero, y esto es importante, los campos de datos e y next y el constructor Node no son información pública, e y next son conocidos sólo a través de sus frlend, List y Li st lt er a to r. El constructor es llamado bajo la declaración de una variable Node. El listado (4.4.19) muestra las declaraciones para una clase de objetos de lista ligada, mientras que el listado (4.4.20) define las funciones. //Ust.h

(4.4.19)

typedef vold * 1 t e m p t r ; class Node C frlend class List; frlend class Li s t l t e r a t o r ; Node *next; ItemPtr e; N o d e d t e m P t r a, Node *p): next(p), e(a) C3; 3; class List (

9 En C, el token 0 se utiliza para el apuntador nulo (nuil); también para el número cero. Su uso lo determina su contexto.

Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

211

frlend class Listlterator; Node *1 a s t ;

publlc: vold i n s e r t ( i t e m P t r a); vold appendfitemPtr a); i t emPt r g e t ( ); L istO :

1astCO) O ;

L 1 s t ( it emPt r a): l as t í n e w Node(a.O)) Clast ->next - l a s t ; }

); class L i s t l t e r a t o r

O;

/ /Necesi ta ser d e f i n i d a para manejar la l i s t a //Dejamos estas d e f i n i c i o n e s como e j e r c i c i o

//List.cpp

(4.4.20)

tnlin e votd L i s t :: f n s e r t í i t e m P t r a) t l a s t -> n ex t - new Node(a,l as t - >next ); } vold L i s t :: appendí i temPtr a)

C l as t - > n ex t - new Nodeía, l as t- >n ex t ); l a s t * l as t ->n e xt ;

} i temPtr L i s t

:: g e t ( )

£

I f ( l a s t - » 0)t c e r r << "Intentando e l i m i n a r un elemento de una l i s t a v a cí a ” ;

return 0;

) Node *head - las t ->ne xt ; i temPtr r e t - head->e;

I f ( l a s t — head) l a s t = 0; else l as t- >n ex t - head->next; delete head; return r e t ;

) La clase List, como se muestra en el listado (4.4.20), no es muy útil como es, porque todo lo que podemos hacer con ella es crear listas ligadas de apuntadores void. Sin embargo, proporciona una clase padre reutilizable para otras estructuras útiles. El estilo C++ incluye combinar muchos archivos pequeños como entrada para otros programas. De este modo almacenaremos declaraciones de lista en un archivo, "listh ". Lo incluiremos (Include) en cualquier programa que requiera sus métodos. El listado (4.4.21) muestra la clase Stack (class stack) derivada y la struct r e a l Stack, derivada de Stack. //St acks. h

(4.4.21)

linclude " l i s t . h "

Sólo fines educativos - FreeLibros

212

PARTE II:

Lenguajes imperativos

class Stack: private List { publi c: Stack(): List() {} Stack(itemPtr a ) : List(a)

{}

void push(itemPtr a)

{insert(a); }

itemPtr pop()

{return g e t (); )

}; struct realStack: private List { public: Stack myStack; real$tack(): m y S t a c k Q

{}

realStack(double * r ) : myStack((itemPtr) r) {}

void push(double *a) {myStack. push((itemPtr) a); } double* pop() {return (double*) myStack.pop();}

}; Stack es una subclase derivada de List (Stack: prívate List). Aquí, todos los atributos de List son prívate en Stack. El código: {realStack rs; {rs.push(l.0); rs.push(2.0); } );

producirá la pila de la figura 4.4.8.10 Note que la clase Li st tiene métodos públ Icos, que están así disponibles para cualquier cliente. Sin embargo, los clientes de Stack o realStack hallarán los méto­ dos de List prívate (class Stack: prívate List). Los métodos de List están sólo disponibles a través del uso de la clase Stack o real Stack. La variable auxiliar myStack fue declarada en real Stack para facilitar el uso de los constructores de Stack. Para devolver el valor de tipo correcto desde real Stack, el tipo de retomo en pop fue representado a double utilizando (double*). H erencia m últiple en C++. Aunque las primeras versiones de C++ no soportaban la herencia múltiple, las versiones 2.0 y superiores lo hacen. Los primeros problemas

último

F I G U R A 4.4.8 re al St ac k rs

10 Si se mantiene una lista como una lista circular se habilita el fácil acceso ya sea al nodo frontal o posterior. Para más detalles, véase [Stroustrup, 1986].

Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

213

involucraban almacenamiento para apuntadores a funciones virtuales; es decir, aquellas que eran elegibles para ligadura tarda. C++ utiliza una implementación similar a VMT de Pascal para almacenar apuntadores a sus funciones virtuales. Considere las cuatro clases A, B, C y Ddel listado (4.4.22). class A { 1nt h; public: virtual f l ( ) ; virtual f2();

(4.4.22)

}; class 8 { int i; public: virtual f 2 ( ) ; virtual f3();

}; class C: public A { int j ; public: f2(); f4();

}; class D: public A f public B { int k; public: f2(); f4();

}; En el compilador de C++ Tau Metric [Ball, 1989], una clase derivada de sólo un antecesor usaría únicamente una tabla virtual. Por ejemplo, class C: A se almace­ naría como en la figura 4.4.9. Si queremos que class D herede tanto de A como de B, se usarán dos tablas virtuales, la primera para D: A y la segunda para D: B, como se muestra en la

h vptr

Código para A:: f1 -

Código para C:: f2

i

Código para C:: f4

F I G U R A 4.4.9

c la s s C derivada de la cla ss A

Sólo fines educativos - FreeLibros

214

PARTE n :

Lenguajes imperativos

figura 4.4.10. La herencia de más de dos clases puede ser manejada en forma simi­ lar, con una tabla virtual adicional agregada para cada nueva clase de antecesor. Ligadura dinámica Por ahora, la noción de ligadura tarda no debería ser demasiado mitificada. Cuan­ do un archivo fuente se analiza y compila, el código máquina para un procedi­ miento estático se almacena al principio en una dirección de memoria particular. Las llamadas de procedimientos encontradas dentro del programa son reemplaza­ das con instrucciones de transferencia hacia esa dirección, como se discutió en la sección 1.2. La llamada está ligada a esa dirección de comienzo. A esto se le llama ligadura temprana, porque la llamada está ligada al tiempo más temprano posible. En contraste, una llamada de procedimiento, tal como ItemPA . Di spl ay [véase el listado (4.2.10)], no puede estar ligada en tiempo de compilación, porque no se conocerá hasta el tiempo de ejecución si ItemP está apuntando a un I t e m , a Real Item, o a u n I nt lt em . De este modo, la ubicación donde el código para Di spl ay (el servi­ dor) será encontrado debe estar ligado posteriormente para el programa de llama­ da o cliente. Si un objeto padre tiene muchos descendentes, y no se conoce cuáles serán construidos durante una ejecución de programa, habrá muchos procedimientos y funciones virtuales que nunca son llamados. Sin embargo, existen compiladores optimizados orientados a objetos que eliminan procedimientos que nunca son lla­ mados por el programa, y generan código sólo para aquellos que serán potencial­ mente utilizados. Si un lenguaje incluye objetos que pueden ser creados en tiempo de ejecución, entre ellos información acerca de sus tipos de datos, este lenguaje soporta el con­ cepto de ligadura dinámica. Como se definió en el código C++ del listado (4.2.13), si un pentágono con lados de longitud 3 de tipo Polygon se declara (Polygon p o l y g o n l ( 5 , 3 ) ;) y envía el mensaje p o l y g o n l . a r e a ( ), el método para definir a rea, incluido en la clase Polygon, se utilizará, y la ubicación para el código dirigido a ese método a rea será ligada en forma estática a la llamada. Pero suponga que estemos utilizando polígonos, cua-

F I G U R A 4.4.10

c l a s s Dderivada de la cla ss A y la class B

Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

215

drados y triángulos para decorar los artículos de tarea de los estudiantes. Los artículos deficientes obtienen triángulos, los mejores obtienen cuadrados, y los trabajos en verdad extraordinarios obtienen un polígono. No se sabrá cuál usar hasta que los artículos estén entregados, evaluados y el grado se haga llegar al programa que produce las decoraciones de mérito. Si agregamosun método vir­ tual show para la jerarquía de polígonos, será bastante posible decidir cuál figura dibujar en el momento que la calificación se determine, como se muestra en la figu­ ra (4.4.23). Polygon* p;

(4.4.23)

char x;

cout << “ ¿Cuál figura desea dibujar? Introduzca P, S o T: cin >> x; swi tch (x) C case ‘ P ’ : Polygon p o l y g on l ; p - &pol ygonl ; bréale; case

Square s quarel ;

p = &squar el ; bréale;

case ‘T*:

T r ia ng l e t r i a n g l e l ;

p = & tr1a n g l e l ;

3; p->show;

Un lenguaje tal como Ada 83, en el cual todos los tipos de datos deben ser determinados en tiempo de compilación, está ligado estáticamente. Ada 95 incluye la noción de tipos de clase amplia (class-wide types). Con referencia al listado (4.4.1), la clase Shape es el tipo de Shapes, Re cta ngl es y Cuboids. Un procedimiento Ada aplicable a cualquier objeto en la clase podría ser como en el listado (4.4.24). procedure ProcessShapesíA: S h a p e ‘Class) 1s

(4.4.24)

S: Float; begin •i #

S := S i z e ( A ) ;— despacho de acuerdo con la etiqueta end P r o c e s s S h a p e s ;

Cuando se compila Proces sShapes, no hay manera de saber si será enviado un Rectangl e o un Cuboi d. Cuando el parámetro A toma un valor en tiempo de ejecu­ ción, será etiquetado, de modo que el cuerpo de función correcto pueda ser ligado a Si ze. A esto se le llama, en Ada 95, despacho dinámico (dyamic dispatch) o examen de método dinámico (dynamic method look up). La decisión de buscar un método particu­ lar es hecha durante tiempo de ejecución, en qué tiempo el sitio de la llamada es ligado a la dirección del código para el método elegido, y el control es despachado o enrutado a esa ubicación de memoria. Para hacer concreta la noción de ligadura dinámica en Object Pascal, agregue­ mos otra clase de Item alaciase Items del listado (4.2.11), como en el listado (4.4.25). Sólo fines educativos - FreeLibros

216

PARTE II:

Lenguajes imperativos (4.4.25)

uses L i s t : TrianglePtr = ATr i a n g l e I t e m ; Tr iangleltem = obj e c t (Item) T: Triangle;

Cse supone que el tipo Triangle ha sido definido con an terio ridad! constructor InitCT: Triangle): constructor CueT; procedure Display; virtual: end;

Entonces nuestra lista mostrada en la figura 4.4.7 podría incluir Items del tipo I n tl t e m, Real Item y T r i a n g l e l t e m . Supongamos que hemos construido una lista de este tipo, llamada My Li st. Cada objeto de elemento contiene un procedimiento Di spl ay, el cual, por supuesto, será diferente para un triángulo que para ya sea un número entero o un real. Podemos Di spl ay (visualizar) cada Item (elemento) en My Li s t a través de la llamada T r a v e r s e A n d D i s p l a y ( M y L i s t ) , como se definió en el listado (4.4.26). procedure TraverseAndDi splay;

(4.4.26)

var N: NodePtr;

begin while Nodes <> ni 1 do begin N := Nodes; Items(Item).Display; Nodes := N^.Next;

end end;

Si Object Pascal fuera ligado estáticamente, la ubicación de cada procedimien­ to Di spl ay estaría ligada en tiempo de compilación al nombre Di spl ay. Esto, por supuesto, no puede pasar si los tipos de los nodos de My Li s t son desconocidos hasta el tiempo de ejecución. Así, sólo los procedimientos Di spl ay que van a ser invocados se determinan dinámicamente en tiempo de ejecución. Ya hemos visto un ejemplo de C++ llamando a ligadura dinámica en los lista­ dos (4.4.20) y (4.4.21). El tipo para un Item no es conocido hasta que una variable se declara para ser del tipo re a l Stack, que puede ocurrir dondequiera en un progra­ ma C++. La clase Stack también podría ser compilada por separado de la estruc­ tura (struct) re a l Stack. Deestemodo, las definiciones para push así como pop no pueden ser ligadas hasta el tiempo de ejecución, cuando el tipo de Items que será insertado o extraído se conoce. La palabra clave virtual señala que el nombre de procedimiento o función que sigue va a ser ligado para una definición en tiempo de ejecución. L A B O R A T O R I O 4.3: CLASES Y H E R E N C I A: OBJECT PASCAL Y C + + O bjetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Completar las declaraciones para un objeto de List, incorporando apuntadores para otros objetos.

Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

217

2. E scrib ir alg u n as de las u tilid ad es de L ist, tales com o D eleteFro m F ron t, DeleteFromEnd, DeleteAfter, DeleteBefore, AddAfter, AddBefore, etcétera. 3. Completar la implementación de una pila de objetos. 4. Esquematizar la implementación de una cola de objetos. 5. Ver cómo la herencia contribuye al valor del código reutilizable.

E J E R C I C I O S 4.4 1. Vuelva a dibujar la figura 4.4.1 de modo que se conforme a la estructura para los objetos Item, Real Item y Intltem del listado (4.2.9). Etiqueta encada clase y método con su nombre de mensaje. 2. Considere cada una de las variables y métodos definidos para las clases 11 em, Rea111 em e I n111 emdel listado (4.2.9) y clasifique cada uno como editado, redefinido o específico. 3. ¿Satisface la clase IntegerArray la relación esUn con sus ambas superclase, Integer y Array? 4. Conecte las entidades (figura 4.4.1) de la lista del lado izquierdo que sigue con las del lado derecho de la lista mediante la relación esUn y /o tieneUn. perimeter sideLength NoOfSides polygon area

triangle polygon square

5. Escriba código C++para las funciones necesarias en la clase L i s t l t e r a t o r del lista­ do (4.4.19). Éstas deberán incluir al menos f i ndNode, f i ndAf t e r , f i nd Befo r e y cualquier otra que usted piense que sería útil. 6. En el código C++ del listado (4.4.21), ¿por qué hay paréntesis rodeando (i temPtr) en las definiciones de push y pop? 7. Vuelva a dibujar los diagramas de la figura 4.4.9 para representar clase c: b, y de la figura 4.4.10 para representar c: b, a. 8. Supongamos que cambiamos la declaración para una List en el listado (4.4.13) a List = objectí Real Item);, eliminando uses Items;, y se declaran las siguientes variables: R:

Realltem;

L:

List; RPtr: ^Realltem; LPtr: ''List; Data:

real;

¿Cuáles declaraciones son legales y cuáles no lo son? a. L : = R; b. R : = L ; c. R.CueR;

d. L.CueR; e. LPtr^.CueR; f. RPtr^.CueR;

g. R := RPtr^.Item; h. Data := RPtr^.R; i. Data := LPtr^.R;

4.5

JAVA El lenguaje de POO más novedoso es Java™ de Sun Microsystems de Mountain View, California. Como asegura la gente de Sun, es un "lenguaje de programación simple, orientado a objetos, distribuido, interpretado, robusto, seguro, de arquitec­ Sólo fines educativos - FreeLibros

218

PARTE II:

Lenguajes imperativos

tura neutral, portátil, de alto desempeño, de multihilos, dinámico, dócil y de pro­ pósito general" [Sun, 1995]. Java soporta programación para Internet en la forma de applets Java independientes de la plataforma. Los applets son aplicaciones de Java que son cargadas y se ejecutan en el entorno de tiempo de ejecución de Java. De este modo, Java incluye dos productos por separado: el propio Java, el cual es un lenguaje de programación orientado a objetos, con todas sus características, y Hotjava™, un navegador para el World Wide Web (WWW) que habilita a los usua­ rios de la Web para descargar o bajar applets escritos en Java y ejecutarlos en su propio sistema. Cualquier navegador con capacidad para applets, como Netscape, puede bajar y ejecutar applets así como también Hotjava. Los términos originales en los que el Libro Blanco (White Paper) de Java está basado se enumeran a continuación. Proporcionan una descripción bastante buena de lo que justamente es Java. Simple. La sintaxis de Java está tan cercana a C como es posible, de manera que los programadores de C pueden hacerse expertos con rapidez. Puesto que C++ es una extensión de C, algunas características no necesarias para POO han permanecido, y causan confusión. De este modo, la sobrecarga de operador de C++, la herencia múltiple y las conversiones automáticas extensivas han sido omitidas de Java. Lo más importante, Java no incluye apuntadores, los cuales son quizá la causa de la mayoría de los errores de programación en programas de C así como de C++. Puesto que los arreglos de C son accesados a través de apuntadores y las cadenas de C son arreglos de caracteres, la provisión de ambos era necesaria en Java. Esto se realiza mediante la provisión tanto de un objeto de cadena como de uno de arreglo. Orientado a objetos. Las facilidades orientadas a objetos de Java son, en esen­ cia, las de C++; es decir, datos y métodos encapsulados en un módulo llamado un objeto, clases de objetos, herencia e interfaces entre objetos a través de mé­ todos. Java tiene la ventaja de estar desarrollado como un lenguaje orientado a objetos, de modo que no se encuentra cargado con estructuras, implementado en un lenguaje anterior que ya no es necesario, como es C++ que creció fuera de C. Así, usted no verá varias struct (registros de C) en Java ni tenplate (plantillas) (métodos de C++ para crear clases polimórficas). Distribuido. Java tiene una librería extendida de rutinas para copiarse fácil­ mente con los protocolos: TCP/IP (Transmission Control Protocol/Intemet Protocol), HTTP (HyperText Transfer Protocol) y FTP (File Transfer Protocol). Las aplicaciones de Java pueden abrir y accesar objetos a través de Internet por medio de URL (Uniform Resource Locators) con la misma facilidad que aque­ llos que usan los programadores cuando accesan a sistemas de archivos locales [Sun, 1995]. Robusto. Algo de la floja verificación en tiempo de compilación heredada por C++ de C ha sido apretada en Java. Éste implementa arreglos verdaderos, en vez de arreglos manipulados a través de aritmética de apuntador, en la que la verificación de los subíndices es imposible. La representación de apuntadores a enteros también se eliminó. Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

219

Seguro. Puesto que los applets de Java fueron diseñados para funcionar sobre Internet, con muchos usuarios accesando a los mismos archivos, lo relacionado con sistemas libres de virus y corrupción fue direccionado a través de encripción de clave pública. Los applets están restringidos precisamente en lo que pueden hacer; por ejemplo, ellos no pueden escribir o eliminar en los archivos del cliente. La filosofía detrás de la implementación de Java para Internet es no confiar en nadie. Arquitectura neutral. El compilador de Java genera un archivo objeto de ins­ trucciones en código de bytes que no tiene nada que ver con una computadora en particular. Estos archivos pueden ejecutarse en cualquier sistema capacita­ do con e] sistema en tiempo de ejecución de Java, ya sea una PC compatible con IBM o una Macintosh de Apple. Portátil. El sistema Java está escrito en el propio Java, y el sistema de tiempo de ejecución se encuentra escrito en C ANSI. Los tipos de datos simples están implementados de manera uniforme a través de todas las plataformas; es de­ cir, los enteros ( 1nt ) son de 32 bits y los números largos ( 1ong ) son de 64 bits. La desventaja de esto es que Java no se ejecutará en una máquina que sólo soporte palabras de 16 bits. Interpretado. El compilador Java, javac, genera código de bytes, en vez de código de máquina, que puede ejecutarse en forma directa en cualquier má­ quina para la cual el intérprete de Java haya sido transportado. El código fuen­ te de Java ( <nombreArchi vo>. j av a ) que haya sido compilado en código de bytes ( < n o m b r e A r c h i v o > . e l a s s ) se ejecuta entonces por el intérprete de Java ( java<nombreArchivo>).

Alto desempeño. Si se desea un alto desempeño, los códigos de bytes interpre­ tados pueden trasladarse en tiempo de ejecución a código de máquina para el CPU particular en que se esté ejecutando Java. Las pruebas en Sun muestran que los códigos en bytes convertidos a código de máquina se comparan en forma favorable en rendimiento con código C/C++. Multihilo. Un conjunto de primitivas de sincronización basadas en monitores, que se discutirá en el capítulo 5, están integradas en Java. Esto permite a las aplicaciones de Java para ejecutarse de manera concurrente, limitadas sólo por las capacidades del sistema operativo subyacente. Dinámico. Java incluye conceptos de interfaz de Objective-C similares a las clases, en los cuales una interfaz es un listado de métodos a los que ion objeto responde. Estas interfaces pueden ser multiheredadas, a diferencia de las cla­ ses derivadas de Java, que solamente pueden heredar de una sola clase base. Se puede buscar una clase de Java al dar una cadena que contenga este nombre y tener su definición vinculada en forma dinámica en el sistema en tiempo de ejecución. Sólo fines educativos - FreeLibros

220

PARTE II:

Lenguajes imperativos

Construcciones del lenguaje Java Mientras que las estructuras de control de Java son más semejantes a las de C, sus estructuras de datos y módulos no lo son. Java, como Smalltalk, considera que casi todo es un objeto. Los tipos numéricos simples, de carácter y booleanos, son las únicas excepciones.

Object, la superclase de todas las demás clases Una clase, 0bj ect, la superclase de todos los demás objetos, está incluida en el pa­ quete dependiente de la implementación java. 1ang, descrito en el listado (4.5.1). Object no tiene campos de datos, pero incluye los métodos siguientes, que son heredados por cualquier otro objeto: publlc class Object C publlc O b j e c t O ; / /c o n s t r u c t o r

(4.5.1)

/ / métodos de i n s t a n c i a públ icos

publlc boolean eq u al s ( O b je ct . ob j ) ; /* debería ser r e d e f i n i d o en l as cl ases derivadas para probar l a igualdad de obj et os , en los cuales o l . eq ua l s ( o2 ) s i g n i f i c a l os val ores de todos l os campos de ol son los mismos que l os correspondient es en o2 */

publlc fin a l Class g e t C l a s s O ; publlc Int hashCodeí); // proporciona un código de d i s p e r s i ó n (o c á l c u l o de d i r e c c i ó n ) cuando se almacena un objeto en una tabl a de d i s p er s i ón (o tabl a de c á l c u l o de d i r e c c i ó n )

publlc S t r i n g t o S t r i n g O ; / / c o n v i e r t e un Objeto en una cadena // métodos p úb l ic os para l a s i n c r o n i z a ci ó n de h i l o s

publlc fin a l vold n o t i f y (); // a r r o j a l a excepción I l l e g a l M o n i t o r S t a t e (Estado de monitor i l e g a l )

publlc publlc publlc publlc

fin a l fin a l fin a l f in a l

vold vold vold vold

n o t i f y A l 1();

waltdong timeout); waltdong timeout, Int nanos); waltO;

// métodos de i n s t a n c i a protegi dos

protected Object c l o n e O ; // hace una copia de un Objeto protected vold copy (Object s rc ); // copia src en el objeto actual (este) protected f i n a l i z e O ; // l i b e r a recursos del sistema de memoria

3 Vale la pena comentar varias cosas con respecto a la clase O b j e c t . En primer lugar, Java es sensible al tamaño de las letras, y todas las palabras reservadas (aquí en tipo negritas) están en minúsculas. La convención de Java es que los nombres de las clases comiencen con letras mayúsculas, mientras que la primera letra de los nombres de métodos o variables estén en minúsculas. De este modo, el método g e t C l a s s () devuelve un objeto de definición de clase del tipo Class, no una class. Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

221

El objeto almacena información acerca del nombre de la clase, nombre de la superclase, interfaces y otra información acerca de un objeto, y envía el mensaje getClassO. Si un objeto llamado trianglel de tipo Triangle envía el mensa­ je trianglel.getClassC) .getNameí), la cadena “Triangle” es devuelta. A continuación se encuentran los modificadores de método publ fe y protected. Java tiene cinco niveles de seguridad, como se ilustra en la tabla 4.5.1, en contraste con los tres de C++. Los dos adicionales, default y prívate protected, son necesa­ rios debido a colecciones de clases relacionadas residentes en los package (paque­ tes). El nivel default permite la accesibilidad entre las clases dentro de un paque­ te, pero no entre paquetes. Una clase protected permite la herencia entre las subclases en diferentes paquetes, pero no la accesibilidad. Los métodos clone y copy son protected, porque los objetos sólo pueden copiarse en otros objetos del mismo tipo. Si estuviesen en diferentes paquetes, serían necesariamente de tipos diferentes. Un método fina 1 es uno que no puede ser redefinido en una subclase, así que todos los métodos en la clase Object para sincronización de procesos concurren­ tes (hilos) son del tipo final. Un subobjeto en ejecución no puede terminar o espe­ rar sin que todos sus superobjetos terminen o esperen también. Laclase (class) Object es un miembro del package java.lang, el cual pro­ porciona las funciones básicas necesarias para programadores en el nivel más bajo. Discutiremos los paquetes (packages) estándar más adelante.

Una clase elem ental de Java Puesto que casi toda entidad de Java es una clase, consideremos la clase Polygon definida en el listado (4.5.2) siguiendo la jerarquía de la tabla 4.5.1.

TABLA 4.5.1 Niveles de seguridad de clases, campos de datos o métodos Java [Flanagan, 1996]

Situación Accesible para: ¿Ninguna subclase del mismo paquete? ¿Subclase del m ismo paquete? ¿Ninguna subclase de diferente paquete? ¿Subclase de diferente paquete? Heredada por: ¿Subclase en el mismo paquete? ¿Subclase en diferente paquete?

publ1c

default

protected

prívate protected

prívate

Yes Yes

Yes Yes

Yes Yes

No No

No No

Yes Yes

No No

No No

No No

No No

Yes Yes

Yes No

Yes Yes

Yes Yes

No No

Sólo fines educativos - FreeLibros

PARTE II: Lenguajes imperativos

222

publlc abstract class Polygonfint n, int s) {

(4.5.2)

int n; int s; int perimeter()

{return (this.n * this.s);};

abstract double area();

} public final class Triangle(int sideLngth) extends Polygon { int n = 3; super(int n, int this.sideLngth); double area()

//constructor

{

return (Math.sqrt(3) * Math.sqr(this.sideLngth)/4.0;

} public final class Square(sideLngth) extends Polygon { int n = 3; super(this.n, this.sideLngth);

//constructor

int area() { return this.sideLngth * this.sideLngth;

} Polygon es una clase abstracta porque tiene un método abstracto (así como toda­ vía indefinido) a rea. Tanto T r i a n g l e como Square son del tipo final, de modo que

ninguno puede tener alguna subclase. Nótese también que Java tiene un modifica­ dor this, que se refiere al objeto que llama el método.

Las Interfaces de Programación para Aplicaciones de Java (APIs) Precisamente, Ada no incluye características dependientes de la implementación en su especificación oficial de lenguaje, ni lo hace Java, en la cual la independencia de la plataforma es una característica clave. No obstante, Sun tiene, como también lo tienen los desarrolladores de Ada, interfaces proporcionadas con un conjunto apropiado de APIs para paquetes estándar de utilidades. A un usuario se le pro­ porcionará un adecuado conjunto de APIs al adquirir un Paquete de Desarrollo de Java (JDK; Java Development Kit) para el sistema operativo en el que vaya a ser instalado. Cada API incluye una interfaz para el SO (Sistema Operativo), una co­ lección de clases de Java y una colección de excepciones que pueden levantarse cuando una de las clases esté activa. Un paquete es j a v a . l a n g , que ya se mencionó antes como incluido en la superclase O b j e c t . Algunas de las clases incluidas son: j a v a . l a n g . C l a s s , j a v a . l a n g . C o m p i l e r , j a v a . l a n g . M a t h (una librería de funciones matemáticas estándar), j a v a . l a n g . R e f (utilizada por el recolector de basura de Java), j a v a . l a n g . S e c u r i t y M a n a g e r , j a v a . l a n g . S t r i n g (para cadenas de texto constantes) y j a v a . 1 ang. S t r i n g B u f f e r (soporte para cadenas mutantes). Las envolturas (wrappers) de tipos, que son clases, mantienen información acer­ ca de los tipos básicos, que no lo son. j av a . 1 ang. Number es una clase abstracta que es la superclase de las envolturas de tipo j a v a . 1ang. I n t eg er (para enteros de 32 bits), j ava. 1 ang. Long (enteros de 64 bits), j a v a . l ang. F1 oat y j av a. 1 ang. Double. Las Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

223

otras dos envolturas en j a v a , lang.* son j av a. l a ng . Boolean y j av a. l an g . Cha r á e t e r. Una variable podría declararse boolean b l ; o Boolean b2;, pero no ambas. Un valor Boolean puede ser ya sea TRUE o FALSE, mientras que un valor boolean e s o true o false. La clase Bool ean proporciona métodos útiles para trabajar con datos con valores lógicos, tal como t o S t r i ng (), que convierte un valor Bool ean a una cadena de manera que pueda ser impresa. El paquete puede ser importado en una aplicación al incluir en su código fuen­ te la declaración que se muestra en el listado (4.5.3). import java.lang.*;

(4.5.3)

Excepciones tales como A r i t h m e t i c E x c e p t i o n , Arrayl nde xOu tOf Bou ndsExc epti on, IOE xc ept ion y F i le N o t F o u n d E x c e p t i o n se encuentran en este paquete. LaAPI j a v a . ú t i l incluye objetos tales como Date y L i n k e r , j a v a . i o adminis­ tra el flujo de E/S y archivos de acceso aleatorio. j a va . awt (por las siglas de Abstract Window Toolkit) incluye cerca de 6 0 clases e interfaces para crear interfaces gráficas de usuario (GUIs; Graphical User Interfaces). Utilizaremos la subclase j a va . awt. graphi es en el Laboratorio 4. 4 para crear una aplicación con animación, y la combinaremos con la clase j a v a . a p p l e t para hacer nuestra aplicación en un applet, lo que podremos transferir a la World Wide Web. Las APIs para Hotjava son java .browser, j a v a . b r o w s e r . a u d i o , java . ne t (para interactuar con Internet), j a v a . n e t . f t p (para interactuar con FTP), j a v a . n e t . nntp (para tener acceso a grupos de noticias en la red), java . n et .www. html (para admi­ nistrar documentos HTML) y j ava. n e t . www. h t t p (para administrar el protocolo de Transferencia de HiperTexto (HTTP) en la World Wide Web). Cada uno de estos paquetes es importado dentro de una aplicación al incluir la declaración del listado (4.5.4): import <PackageName>.*;

(4.5.4)

en el código fuente. Los usuarios también pueden escribir interfaces. Estas interfaces proporcionan colecciones de declaraciones de métodos sin implementación de cuerpos. Por ejem­ plo, considere la interfaz mostrada en el listado (4.5.5). public interface PolygonGraphMethods {

(4.5.5)

public void setColor(); public void setLocation(int x, int y); public void Draw(DrawWindow dw);

} Una clase S q u a r e P i c t u r e debería heredar los métodos tanto de Square como de P o l y g o n G r a p h M e t h o d s , pero Java no permite la herencia múltiple. A sí que implementamos S q u a r e P i c t u r e como una subclase de Square como en el listado (4.5.6).

Sólo fines educativos - FreeLibros

224

PARTE n: Lenguajes imperativos public class S quarePi ct ure extends Square lapleients PolygonMethodsí // las definiciones para setColor, setLocation y Draw van aquí

(4.5.6)

) Compilación y ejecución de un programa Java Cada clase es compilada por separado y debe localizarse en el directorio apro­ piado de modo que una aplicación Java pueda hallar el código. El código fuente para la clase Polygon podría estar localizado en java\polygon\Polygon.java. Squa r e y T r i angl e estarían ubicados en java\polygon\square\Square.java y en java\ polygon\triangle\Triangle.java. El código fuente para S q u a r e P i c t u r e está en java\polygon\square\SquarePícture.java. (En UNIX, los separadores de directorios serían / en lugar de \). Nótese que las extensiones de archivo son de cuatro caracte­ res de largo, lo que puede ser soportado por sistemas operativos tales como Windows NT, Windows 95 y UNIX. Cuando estos archivos se compilan en código de bytes independiente de la plataforma, los archivos son almacenados en los mismos direc­ torios en archivos * . c 1a s s. Se puede poner múltiples clases en un solo archivo * . j av a , pero cada clase será com pilada en un archivo separad o * . c l a s s . java\polygon\square\SquarePicture es compilado a través de: ja v a c

S q u a r e P ic tu re .ja v a

el cual crea el archivo java\polygon\square\SquarePicture.class. Para ejecutar una aplicación Java, es necesario un método uln. Un posible método de este tipo para incluirse entre los métodos de SquarePi c t u r e se muestra en el listado (4.5.7). I ip o r t j a v a . a w t / / para Col or public s t a t lc void aaln (String a r g v ü ) C SquarePicture sp * new S q uar e P i c t u r e ( l O ) ; Sistem.out.println ("El área es: " + s p . a r e a O ) ; Si stem.out.print (“y el pe rímetro es: ” + s p . p e r i m e t e r í )); sp:color - Color.red; s p .setLocati o n ( 100»50); s p . d r a w f );

(4.5.7)

) El método aaln O no devuelve nada (void), y es statlc; es decir, accesible a lo largo de la clase e independiente de cualquier instancia tal como sp anterior. El único argumento para aaln O es un arreglo de cadenas, argv[], que son cualquier directiva incluida en la línea del intérprete de comandos. Note que el compilador Java es javac y el intérprete es java. Para ejecutar el programa Java anterior des­ pués de compilarlo en código de bytes con javac, utilizamos la línea de comando java SquarePicture. Una directiva útil en esta aplicación que eliminaría la necesidad de calificar métodos y variables de instancia se muestra en el listado (4.5.8). Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

-classpath java SquarePicture

225

(4.5.8)

Sin embargo, se debe estar seguro que no haya conflictos de nombre entre paquetes cuando se utilice - c l a s s p a t h . Otras directivas facilitan la depuración, estilo de in­ forme de ejecución, tamaño de la pila, etcétera. El método naln se ejecuta al utili­ zarse el comando j av a \ p o l y g o n \ s q u a r e \ S q u a r e P i c tur e. Hotjava y Applets Uno de los objetivos del diseño para Java fue crear aplicaciones que puedan ser transportadas a través de Internet y ejecutadas en la máquina del cliente, con acce­ so remoto así como también a archivos locales. Como se expuso antes, estas aplica­ ciones son llamadas applets. Las aplicaciones tales como el navegador de web, Hotjava, puede estar habilitado para Java al darles acceso para el intérprete en tiempo de ejecución de Java. Un applet simple, que es presentado en casi todas las referencias, entre ellas [Gosling, 1996] y [Flanagan, 1996], se muestra en el listado (4.5.9). Import java.applet.* // clase base para applets (4.5.9) 1»port java.awt.* // conjunto de he rramientas para gráficos en ventana: incluye gráficos publlc class EasyApplet extends Applet C publlc vold paintfGraphic s g) C g. Dra w S t r i n g ( “Hola Mundo", 25, 50); 3 3

Puesto que EasyAppl et va a ser llamado desde un archivo HTML que hace referen­ cia a él, lo necesitamos también. El código para el archivo HTML se muestra en el listado (4.5.10). <APPLET code="EasyApplet.class" width=150 height=100>

(4.5.10)

Un navegador tal como Hotjava que comprende la etiqueta <APPLET> puede lla­ mar a EasyApplet. Un navegador que no esté habilitado para Java simplemente ignorará la etiqueta <APPLET>. Investigaremos los archivos HTML en el Laborato­ rio 4.5, y crearemos y ejecutaremos un applet en el Laboratorio 4.6. Tipos de programa Java tiene cuatro tipos de programas: • • • •

Aplicaciones Applets Manejadores de contenido Manejadores de protocolo Sólo fines educativos - FreeLibros

226

PARTE II:

Lenguajes imperativos

Los manejadores de contenido se encuentran en clases del paquete j ava. ne t . *. El j a v a. n et . URL permite que los datos encontrados en un URL (Uniform Resource Locator) sean descargados hacia el sistema del usuario. Al utilizar esta interfaz, una secuencia de páginas puede cargarse de manera automática, dando el efecto de una película, java .net.Socketlmpl proporciona métodos para implementar la comunicación de redes a través de conexiones. Cuando se utilizan con java. ne t . Datagr amSocket, los paquetes no confiables de diagramas de datos pueden enviarse y recibirse a través de la red. Los manejadores de protocolo para HTTP, FTP y Gopher están incluidos en el navegador de Web Hotjava, que fue escrito como una aplicación Java. A medida que se encuentran disponibles nuevos protocolos, los usuarios pueden escribir sus propios manejadores. Los manejadores se colocan en el directorio java\classes\net\www\*, con los manejadores de contenido en el subdirectorio \content\*, y los manejadores de protocolo en el subdirectorio \protocol\*.

Diferencias entre Java, C y C++ Java no tiene preprocesador (cpp) como lo tiene C, el cual es capaz de macrosustitución (#defIne Pl 3,14159), compilación condicional (#1fdef CYBER #def1ne BYTESIZ 10 #else #def1ne BYTESIZ 8 #end1f ) y la inclusión de archivos nombrados (#1nclude <polygon.h>). No hay variables globales Java, pero se puede definir una variable statlc de clase amplia, que persiste a través de varias instancias de una clase. Las constantes son creadas al declarar una variable como statlc final y luego asignarle un valor. Los archivos nombrados son Importados dentro de una clase. Java no requiere de compilación condicional porque es independiente de la plata­ forma. Java no tiene facilidad de macros, cuyos diseñadores pensaron que era innece­ sario, por el avanzado estado de la tecnología de compiladores. Java agrega boolean y byte a sus tipos de datos simples. Los arreglos y clases de Java son tipos de referencia en el sentido que son pasados por referencia, pero no es posible manipular sus direcciones (mediante el uso del operador &) ni desreferenciarlos a través de -> y *, como en C. Los tipos simples son pasados por valor. Puesto que no pueden manipularse las referencias a las variables, Java no tiene al tipo apuntador. En C, el apuntador nulo es 0. En Java, nul 1 es el valor predeterminado para tipos de referencia; es decir, clases o arreglos. Puede ser asig­ nado a cualquier variable de cualesquiera de estos tipos. Las cadenas en Java son de dos clases: cadenas de texto constante ( j a v a . l a n g . S t r i n g ) y cadenas variables ( j a v a . l a n g . S t r i n g B u f f e r ) . Las cadenas constantes se comportan casi como tipos simples y son pasadas por valor, mientras que las cadenas de la clase S t r i ngBuf f e r son pasadas por referencia. Java no soporta el &, * o slzeof de C, puesto que no incluye un tipo apun­ tador. Sin embargo, agrega algunos nuevos operadores, mostrados en el listado (4.5.11). Sólo fines educativos - FreeLibros

CAPÍTULO 4: Lenguajes para programación orientada a objetos (POO) + 0 instanceof C »> &

227

Concatenación de cadenas (4.5.11) Devuelve true si el objeto o es una instancia de clase C Desplazamiento a la derecha con 0 para la extensión de signo AND en modo de bit para enteros; AND para tipos boolean

1 && II

OR en modo de bit para enteros; OR para tipos boolean AND abreviado (no evalúa el segundo argumento si el primero es f a 1se) OR abreviado (no evalúa el segundo argumento si el primero es true)

La declaración for de Java es algo diferente de la correspondiente en C, en el sentido que permite la declaración de variables de ciclos locales en la sección de inicialización. No permite el operador coma (,) de C en la sección de prueba de un for, pero lo hace en la sección de inicialización y de incremento, como en el listado (4.5.12). for (Int 1-0, String s="count";1nt j=s.length;

(4.5.12)

i<j; 1++, s=*s.s u b s t r i n g C O ,j - i );) ( Sy stem.out.println(s); S y s t e m . o u t . p r i n t ( “ ¡Todavía co ntando!’'); S y s t e m . o u t . p r i n t (j - i , “ caracteres por s a lir” ;);

) La salida sería: count iTodav í a contando! ¡5 caracteres por salir! coun ¡Todavía contando! ¡4 caracteres por salir! cou ¡Todavía contando! ¡3 caracteres por salir! co ¡Todavía contando! ¡2 ca racteres por salir! c ¡Todavía contando! ¡1 ca racteres por salir!

C++ tiene varias características no soportadas en Java. Éstas incluyen: • Herencia múltiple • Plantillas para implementar polimorfismo • Sobrecarga del usuario de operadores • La definición de funciones de conversión que automáticamente determinan un constructor cuando se asigna un valor a una variable de clase Además, los objetos C++ son manipulados por valor; mientras que los objetos de Java, por referencia [Flanagan, 1996]. Sólo fines educativos - FreeLibros

228

PARTE II:

Lenguajes imperativos

L A B O R A T O R I O 4.4: OBJ ETOS Y P R O G R A M A C I Ó N O R I E N T A D A A O B J E T O S : J AV A O bjetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Completar una aplicación Java con el uso de técnicas orientadas a objetos. 2. Utilizar la clase java. awt para implementar una aplicación automatizada simple.

L A B O R A T O R I O 4 . 5 : H T M L P A R A U T I L I Z A R S E E N EL W O R L D W I D E W E B C O N J AVA O bjetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Proporcionar al estudiante experiencia en la construcción de una aplicación simple HTML.

L A B O R A T O R I O 4 . 6 : U N A P P L E T : J AV A O bjetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Utilizar las técnicas HTML exploradas en el Laboratorio 4.5 y la aplicación Java del Laboratorio 4.4 para construir un applet de Hotjava. 2. El estudiante desempeñará el papel del servidor, y hará uso de un intermediario de la World Wide Web para enviarlo a los clientes.

E J E R C I C I O S 4.5 1. ¿Por qué usted piensa que cada una de las palabras que describen Java fueron adop­ tadas como un objetivo de diseño? 2. a. Los métodos del listado (4.5.2) no tienen asignados modificadores de nivel de seguridad. ¿Cuál es el nivel de seguridad? b. Dada su respuesta al inciso a, ¿cómo deberían ser empacadas las tres clases defi­ nidas: en uno solo o en diferentes paquetes? 3. Java no proporciona herencia múltiple, excepto como se muestra en el listado (4.5.6). Sin embargo, permite la importación de las in t e r f ace múltiples a través de la inclu­ sión de declaraciones diversas de la forma mostrada en el listado (4.5.4). Si se pre­ senta un método Draw en más de uno de los paquetes importados, ¿cómo haría la diferencia entre ellos en el código para una aplicación Java? 4. ¿Cuál es la diferencia entre redefinir un método, M(), en una subclase en la cual M() ya ha sido definida en una superclase, y definir un método en una subclase que ha sido declarada abstracta en una superclase? ¿Cuándo querría usar cada una? 5. Java no incluye plantillas para implementar clases polimórficos, como lo hace C++. Sin embargo, incluye una clase Stack genérica (en el paquete java.útil .Stack) la que extiende (extend) la clase genérica Vector (java. uti 1 .Vector). a. ¿Qué tipo, según sus conjeturas, podría utilizarse para el elemento genérico de un vector? b. Vector hereda de Object así como de C1 oneable. ¿Cómo podría declararse Vector para obtener esto?

4.6

RESUMEN En este capítulo se examinó lenguajes basados en objetos y orientados a objetos, ambos soportando objetos que encapsulan datos, con el estado y operaciones sobre Sólo fines educativos - FreeLibros

CAPÍTULO

4: Lenguajes para programación orientada a objetos (POO)

229

esos datos llamados métodos. Los objetos se comunican entre sí a través de paso de mensajes, en el cual un mensaje es el nombre de un método de objeto. La herencia implica una jerarquía de clases, con los objetos en una subclase heredando méto­ dos y/o datos de una clase. La herencia puede ser simple o múltiple, en cuyo caso una subclase puede heredar de más de una superclase. Además, los lenguajes orien­ tados a objetos soportan ligadura dinámica, en el cual un objeto y sus métodos pueden ser creados o destruidos en tiempo de ejecución, y un mensaje no necesita estar ligado/vinculado a un método hasta el tiempo de ejecución, cuando el objeto para el cual se dirige es determinado. Ada 83 es un lenguaje basado en objetos, mientras que Ada 95, Object Pascal, C h—Hy Java están orientados a objetos. Los primeros lenguajes orientados a objetos fueron Simula y Smalltalk. Este último es un lenguaje de objetos puro, en el que cada tipo de datos o agregado es un objeto; como es también el caso en el más reciente lenguaje orientado a objetos, Java. Los objetos han sido agregados a los lenguajes interactivos existentes: como en el caso de Simula, Ada 95, Object Pascal y C++. Smalltalk y Java se diseñaron como lenguajes orientados a objetos y son así algo más simples y claros que los otros. La programación con objetos involucra un estilo bastante diferente de los mé­ todos descendentes de procedimientos. Aquí un problema es visto como una colec­ ción de objetos en interacción. Uno de los fines de la programación orientada a objetos es mantener librerías de objetos probados reutilizables, con especificacio­ nes que pueden ser comprendidas con facilidad por los clientes y combinadas en aplicaciones que satisfa*gan sus necesidades particulares.

4.7 NOTAS SOBRE LAS REFERENCIAS Un excelente, aunque breve, resumen de los lenguajes orientados a objetos es [Saunders, 1989]. Él discute y proporciona vendedores de 16 lenguajes categorizados por tipo como Actor, concurrentes, distribuidos, basados en marcos, híbridos (ba­ sados en C o LISP), lógicos, basados en Smalltalk, extensiones ideológicas, y misceláneos. Ada 83 y otros lenguajes basados en objetos no están incluidos. Diversos periódicos y revistas intentan publicar la información más reciente en este campo de rápido desarrollo. Cuatro de éstos son The Journal o f Object- Oriented Programming (JOOP), que publica diez números al año; Hotline on Object-Oriented Technology (HOOT), de aparición mensual; el C++ Report, con diez números al año; y Java Report, el más reciente y de publicación bimestral. JOOP publica artículos y columnas regulares acerca de los lenguajes Eiffel, Smalltalk, Actor, Common LISP Object System (CLOS), Objective-C y C++. La creación de clases reutilizables que sean prácticas y útiles es un trabajo duro. [Johnson, 1988] sigue un debate de herramientas y librerías orientadas a objetos con trece buenas reglas para práctica de programación. [Krasner, 1983] presenta una buena colección de artículos que describe el fon­ do de Smalltalk-80, experiencias con su implementación para varias computado­ ras, resultados de prueba y propuestas para desarrollo futuro. Sólo fines educativos - FreeLibros

230

PARTE II:

Lenguajes imperativos

Una comprensión profunda del diseño orientado a objetos puede obtenerse mediante la lectura del libro de Grady Booch [Booch, 1994]. Algunos lo consideran la “Biblia" de los conceptos y aplicaciones de la orientación a objetos. [Sun, 1995] proporciona información acerca del lenguaje Java. Está disponible en Internet a través de [emailprotected], o a través de la World Wide Web por medio de http://java.sun.com. Otra fuente de información acerca de Java es el SunSITE en la Universidad de Carolina del Norte (http://sunsite.unc.edu/pub/ languages/java). Es fácil encontrar su propio camino por tutoriales, código fuente, código descargable, etcétera, a través de transferencias de hipertexto, puesto que ambos sitios tienen referencias a su contenido bien especificadas. Otra fuente en la web que proporciona applets de ejemplo que usted puede ejecutar es http:// www.gamelan.com/. Java ha capturado la imaginación del mundo de la computación, y los editores se apresuran a imprimir libros y manuales para enterar a los usuarios con este nuevo lenguaje. Nosotros examinamos varios disponibles a partir de abril de 1996, y encontramos [Flanagan, 1996], de O'Reilly & Associates, Inc., el más útil y bien organizado.

Sólo fines educativos - FreeLibros

CAPÍTULO 5 CONSTRUCCIONES DE LENGUAJES PARA PROCESAMIENTO EN PARALELO 5.0 5.1 5.2 5.3

En este capítulo El paradigma Procesos múltiples Sincronización de procesos cooperativos Semáforos Monitores Rendezvous (Punto de reunión) Paso de mensajes Ejercicios 5.3 5.4 Algunas soluciones de sincronización Semáforos en ALGOL 68, C, y Pascal S ALGOL 68 C Pascal S

233 234 236 238 240 243 244 245 247 247 247 247 247 249

Tipos de proceso y monitor en Concurrent Pascal Rendezvous (Punto de reunión) en Ada y Concurrent C Ada Concurrent C Paso de mensajes en Occam Ejercicios 5.4 5.5 Tupias y objetos El espacio de tupias de Linda Objetos como unidades de paralelismo 5.6 Administración de fallas parciales 5.7 Resumen 5.8 Notas sobre las referencias

Sólo fines educativos - FreeLibros

251 253 253 257 259 262 262 263 266 267 268 269

CAPÍTULO

5

Construcciones de lenguajes para procesamiento en paralelo

S i se reparte un trabajo entre dos o más trabajadores, por lo general se hará más rápido y en ocasiones mejor. Pero demasiados cocineros en realidad pueden derra­ mar el caldo. Los proyectos conjuntos necesitan coordinación. En este capítulo, exa­ minaremos los lenguajes que soportan más de un procesador trabajando sobre un problema. Los procesadores pueden trabajar en forma independiente y luego co­ municarse los resultados parciales entre sí; o todos pueden trabajar en el mismo proyecto. El trabajo puede hacerse de manera simultánea o alternada.

5.0 EN ESTE CAPÍTULO En este capítulo examinaremos: • • •

Modelos de memoria compartida con sincronización a través de semáforos o monitores Paso de mensajes Procesos para accesar una memoria asociativa común llamada espacio de tupias

Los módulos escritos para ejecutarse en paralelo reciben la denominación de procesos, que se ejecutan, al menos en forma potencial, en procesadores separados. Veremos ejemplos de procesos escritos en: • • • • •

Pascal S Concurrent C Ada Occam C-Linda Sólo fines educativos - FreeLibros

234

PARTE II: Lenguajes im p erativos

En todos los Laboratorios, excepto en Concurrent C, se dejará que usted com­ plete los detalles en los programas con la implementación de los problemas del productor-consumidor, en los que elementos son producidos concurrentemente y consumidos a medida que llegan a estar disponibles. El término paralelo implica las posiciones de procesadores múltiples, mientras que concurrente sugiere que los pro­ cesos se están ejecutando en forma simultánea. Los términos con frecuencia son intercambiables.

5.1 EL PARADIGMA Cualquiera que sea el sistema, tres factores distinguen la programación concurren­ te de la programación secuencial [Bal, 1989]: 1. 2. 3.

El uso de procesadores múltiples Cooperación entre los procesadores El potencial para falla parcial; es decir, uno o más procesos pueden fallar sin hacer peligrar todo el proyecto

La figura 5.1.1 ilustra los dos modelos para procesamiento en paralelo. Las etiquetas se omitieron de la figura en forma intencional, debido a que no hay una­ nimidad en la literatura para nombrar lo que cada uno de los dos diagramas repre­ senta. Sin embargo, todos coinciden en que ambos involucran dos o más CPU que se comunican entre sí. El diagrama superior muestra cada uno con su propia me­ moria y un canal de comunicación entre ellos. Aquí la memoria así como los CPU

Red

FIGURA 5.1.1 Modelos físicos para procesamiento en paralelo

Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

235

están distribuidos. El diagrama inferior no ilustra un canal de comunicación, sino memoria compartida. Sólo los CPU están distribuidos. Bal, Steiner y Tanenbaum [Bal, 1989] llaman al sistema de arriba distribuido, y no al de abajo; mientras que Shatz y Wang [Shatz, 1989] consideran ambos como distribuidos, puesto que más de un CPU en comunicación está involucrado y el trabajo puede distribuirse entre ellos. Más complicado aún, Shatz y Wang conside­ ran el sistema de arriba como débilmente acoplado, y el de abajo fuertemente aco­ plado. Bal y sus colegas denominan acoplada sólo a la configuración superior. Para ellos, un sistema débilmente acoplado es aquel en el cual los CPU que operan están físicamente apartados y la comunicación puede ser no confiable. Si el canal de co­ municación es una red, esto se denomina una red de área amplia o WAN (Wide Area Network). El sistema fuertemente acoplado es una red de área local o LAN (Local Area Network). En este capítulo examinaremos ambas situaciones y no nos preocuparemos demasiado acerca de los nombres. Cuando la memoria no está compartida, los CPU se comunican mediante el envío y la recepción de mensajes. Cuando sí lo está, cada CPU en cooperación puede inicializar y/o actualizar las mismas localidades de memoria. Una tercera configuración para procesadores múltiples proporciona paso de mensajes así como compartición de memoria. En la figura 5.1.2, la memoria compartida se representa medíante un cuadro de líneas interrumpidas para reve­ lar la memoria lógica. Puede ser físicamente Memoria #1, Memoria #2, o algún tercer bloque de Memoria. Cuando muchos procesadores están involucrados, en ocasiones miles de ellos, es posible una variedad de arquitecturas. La sociedad de cómputo de la IEEE (IEEE, Computer Society), proporciona un tutorial muy breve, con varios diagramas y guías para lectura adicional, en el número de febrero de 1990 de Computer [Duncan, 1990]. Los sistemas distribuidos son ventajosos porque: • • • •

Pueden acelerar programas al ejecutar diferentes procesos en paralelo. Son capaces de mejorar la confiabilidad si dos o más procesadores duplican el trabajo de cada uno de ellos. Proporcionan una avenida natural para el crecimiento del sistema cuando se agregan procesadores adicionales Facilitan tareas naturalmente distribuidas, tal como el correo electrónico.

Red FIGURA 5.1.2 Localidades de memoria

Sólo fines educativos - FreeLibros

236

PARTE

n: Lenguajes imperativos

Es necesario decir lo fundamental acerca de procesos y procesadores. Hasta ahora hemos hablado acerca de los CPU o procesadores físicos: elementos de hardware que se pueden ver y tocar. En este sentido, un proceso es un simple procedimiento secuencial que se ejecuta en un solo procesador físico. Sin embargo, los procesado­ res también pueden ser lógicos. En una máquina simple, los procesos pueden eje­ cutarse de manera alternada y uno a la vez, compartiendo el mismo CPU. Esto se llama, a veces, tiempo compartido. En una arquitectura de CPU múltiples, un compilador podría distribuir los procesos para diferentes CPU. Esto bien podría ser una función del sistema operativo, sin que el usuario esté consciente de si los procesos se ejecutan en ese momento en paralelo o de manera alternada. Algunos autores reservan el término multiprocesamiento para los procesos que se ejecutan en paralelo, y usan el de multiprogramación para incluir ya sea ejecución en paralelo o alternada de los procesos. En la mayoría de los casos, un programa sería el mismo ya sea que la ejecución fuese a través de tiempo compartido en un solo procesador o de manera concurrente con la utilización de varios procesadores. Ya sea que los procesos compartan memoria o no, o estén acoplados en forma débil o fuerte, la probabilidad de que uno o más procesadores fallen se incrementa cuando varios están funcionando a la vez. Además, si los procesadores están física­ mente apartados también contribuye a la falla del sistema. De este modo, los len­ guajes en este paradigma incluirán algún mecanismo para continuar con los procesadores que aún trabajen y/o se recuperen de la falla parcial. ¿Cómo es posi­ ble levantar y manejar excepciones entre procesos operativos? Nos dirigiremos a cada uno de estos asuntos a continuación.

5.2 PROCESOS MÚLTIPLES Un proceso es un tipo de datos abstracto que puede, aunque no lo necesite, ejecutar­ se en paralelo con otro proceso. Una unidad de proceso es la construcción de lenguaje capaz de encapsular un proceso. Estas unidades también pueden ser llamadas uni­ dades de paralelismo. En Modula-2, la unidad es una corrutina; en Ada, una tarea; y en Concurrent Pascal, un proceso. El lenguaje Occam permite declaraciones indivi­ duales para servir como unidades de proceso. Una secuencia de declaraciones Occam precedidas por PAR se ejecutará en paralelo. Por ejemplo, PAR i = 0 FOR 100 A[i ]

inicializará un arreglo entero a 0 de manera simultánea. Esto puede o no ahorrar tiempo, según la rapidez con la que el sistema operativo pueda conmutar la ejecu­ ción a los 100 procesadores por separado. Los objetos también pueden servir como unidades de proceso en lenguajes que los soporten, tales como Concurrent Smalltalk o Emerald. Los lenguajes funcionales utilizan expresiones como unidades de pro­ ceso, mientras que los basados en lógica usan cláusulas. En alguna literatura, aun­ que no en toda, cualquier unidad de paralelismo se conoce como un proceso. Observaremos esta definición aquí. ALGOL 68 incluye la noción de cláusulas colaterales, tales como: Sólo fines educativos - FreeLibros

CAPÍTULO 5: Construcciones de lenguajes para procesamiento en paralelo begin x :* 3, y

237

av e” end;

Las declaraciones de ALGOL están separadas por signos de punto y coma, pero en la cláusula colateral, por comas. Éstas pueden ser ejecutadas en cualquier orden. Como con la mayoría de los esquemas de ejecución en paralelo, lo que se hace primero no se especifica. Las cláusulas colaterales no se comunican entre sí. Si escribimos: x :« 0; begin x

3, x

x + i, z := “ave” end;

el valor de x estará determinado hasta que la cláusula colateral se ejecute, puesto que no sabemos su orden. ¿Es x = 3, o es x = 4? Incluso es posible que x = 1. Veamos cómo podría ser esto. Para facilitar este asunto, llamaremos x 3 la cláusula el, x x + 1 la cláusula c2 y z “ave” la cláusula c3. Si el se completa antes que c2 comience a ejecutarse, entonces c2 será x : * 3 + 1, o 4. Si el se ejecuta después que c2 se complete, x será 3. Ahora suponga que las cláusulas se ejecutan de manera con­ currente y comparten la localidad de memoria para x. La ejecución de una declara­ ción de asignación en una máquina con registros, por lo regular, involucra tres pasos: 1. 2. 3.

Cargar el valor actual de x en un registro r. Realizar la operación del lado derecho, dejando el resultado en r. Almacenar el valor de r en la localidad para x.

Suponemos que el y c2 tienen registros asociados rl y r2. Ahora supongamos que el comienza a ejecutarse y almacena 0 en rl. Entonces x = rl = 0. c2 también está ejecutándose, con x = r2 = 0. Ahora si el se completa, de modo que x = rl = 3, c2 no sabrá nada acerca de ello. Ya ha efectuado la operación de "leer el valor de x en r2" y procede a incrementar r2. El final "almacenar r2 en x" dejará como resultado x = 1. (Véase la figura 5.2.1.)

r3

r3

ave

ave

Sólo fines educativos - FreeLibros

238

PARTE II:

Lenguajes imperativos

Sin algún método de sincronización, no podemos hacer suposición alguna acer­ ca de cómo los procesos concurrentes se intercalarán. Entendiendo por intercalamiento el proceso que se ejecuta por un momento, luego otro y otro más antes de volver a completar el primer proceso. Con el tiempo, todos los procesos intercala­ dos terminan. Aquí surgen varios asuntos. Nuestras tres cláusulas colaterales ilus­ tran quién tiene acceso a un recurso compartido, por cuánto tiempo, y cuándo. La comunicación entre procesos puede manejarse a través de paso de mensajes o de compartimiento de datos. El paso de mensajes tiende a ser más confiable, pero la experiencia muestra que los programas presentan más dificultad para su escritu­ ra que aquellos con memorias compartidas. Examinaremos primero la sincronización de recursos compartidos. Lo que se verá a continuación hace poca referencia a los lenguajes existentes, pero discute los tres mecanismos de sincronización: semáforos, monitores y rendezvous (punto de reunión). Discutiremos sus implementaciones en la sección 5.4.

5.3 SINCRONIZACIÓN DE PROCESOS COOPERATIVOS Dos o más procesos en ejecución pueden comunicar resultados parciales antes de continuar, o pueden compartir recursos. El reparto de recursos involucra que cada proceso tenga acceso a las mismas localidades de memoria, pero sólo uno a la vez. Los procesos se ejecutan en diferentes velocidades, de manera que no hay garantía de que un proceso tenga sus resultados calculados o abandone los recursos com­ partidos en el momento que un segundo proceso los necesite. De este modo, los procesos necesitan estar sincronizados de alguna manera. La Cena de los Filósofos, ilustrada en la figura 5.3.1, es un ejemplo conocido de problemas potenciales con los procesos de cooperación. Se presenta a cinco filósofos sentados alrededor de una mesa con un tazón de arroz en la medianía de ésta y cinco palillos para comer. Cada uno piensa o come de manera alternada, y los cinco realizan una de estas dos acciones en forma con­ currente. De este modo, hay cinco procesos, P.(i=0..4), en los cuales un filósofo come

/Cr

p4

/Ci

arroz

/a

Pi

10,

¡o.

FIGURA 5.3.1 La Cena de los Filósofos

Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

239

o piensa en forma alternada. Cada uno hace sólo una de estas actividades a la vez. A fin de comer, un filósofo, P. debe dejar de pensar, señalar Hambre y levantar dos palillos, uno a su derecha (C.) y uno a su izquierda (C(i+1)mod5). Éstos son los recursos compartidos. ¿Cómo podemos organizar sus acciones de modo que ninguno deje de pensar ni quede hambriento? Deberán evitarse cinco problemas cuando se organicen procesos cooperati­ vos. Estos problemas son: estado de espera productiva (busy waiting), alternancia (aítemation), inanición (staruation), irracionalidad (urtfaimess) y estancamiento (deadlock). Describiremos cada uno en términos de la Cena de los Filósofos. Espera productiva. Una manera de programar a los filósofos es establecer y probar variables compartidas y probarlas en forma repetida. Por ejemplo, po­ dríamos establecer cinco variables booleanas H (i=0..4), uno para cada filóso­ fo. Si P2está hambriento y se establece H2=verdadero, entonces P2sería requerido para abandonar C2y P3 para abandonar C3 a P2, en una cantidad razonable de tiempo. Esto podría implementarse con facilidad si se utiliza contadores para monitorear cuánto tiempo un filósofo en particular ha monopolizado un pali­ llo particular. Es la prueba repetida de las variables H lo que se conoce como espera productiva. Mientras que un filósofo espera por dos palillos, los procesos para los filósofos que comen están probándose diligentemente para ver si un vecino tiene la señal de Hambre. Alternancia. Una solución simple sería permitir que P0 y P2 coman durante un periodo específico, luego P y P3, P4y P2, etcétera. Sin embargo, la noción de concurrencia incluye respuesta a las peticiones aleatorias dentro de una canti­ dad razonable de tiempo, no una programación rígida del acceso. Cada filóso­ fo deberá ser capaz de pensar cuando hay algo acerca de lo cual pensar, y comer sólo cuando tenga hambre. Inanición. Un esquema posible para programar la comida de los filósofos es dejar que cada uno verifique la disponibilidad de los palillos necesarios y coma sólo cuando estos últimos se encuentren disponibles. Suponga que P0 levanta C0 y Cr Entonces P1 y P4 deben mantenerse pensando, estén o no interesados en pensar. Sin embargo, P2 podría comer. P0 y P2 terminarán de comer más tarde, pero todavía podemos matar de hambre a uno de los otros. Solamente dos pueden comer al mismo tiempo, pero la presencia de cinco filósofos re­ quiere de algo más elaborado que verificar si ambos palillos se encuentran dis­ ponibles. Irracionalidad. Resulta cuando uno o más de los filósofos tienen que esperar un periodo no razonable para pensar o para comer. Por algo, el tiempo de espe­ ra promedio deberá ser el mismo para todos los cinco. Una solución injusta, pero fácil, sería dejar que coman los filósofos (quieran o no) en el orden: P0 P2, P2 P3, P4 Pj, y luego empezar de nuevo. P2 llegaría a engordar, o tendría que ceder su tumo a alguien más. Estancamiento. El estancamiento es una situación en la cual dos o más pro­ cesos se encuentran esperando eventos que nunca ocurrirán. Si cada uno de Sólo fines educativos - FreeLibros

240

PARTE II: Lenguajes imperativos

nuestros filósofos levanta el palillo izquierdo y espera hasta que el derecho esté disponible para comer, todos morirían de hambre. La situación se encuen­ tra en un estancamiento, porque no puede proceder la comida para nadie. Puesto que cada uno indica que tiene hambre, también dejarán de pensar, puesto que se está alternando con el momento de comer para cada filósofo. En todos los esquemas de sincronización y para procesos cooperativos que involucran recursos compartidos, debe hacerse una previsión para la exclusión mu­ tua.. Un filósofo que esté utilizando un palillo debe ser capaz de prevenir que su vecino lo tome hasta que él termine de comer. Esto se realiza en el código a través del uso de una sección crítica (CS, por sus siglas en inglés). Un proceso que ejecuta ahora código en la CS tendrá acceso exclusivo a los recursos compartidos hasta que salga de la CS. De hecho, una vez que el código crítico se introduzca, no puede ser interrumpido por un proceso en competencia hasta que salga del CS. En algunos problemas, los múltiples recursos compartidos pueden agruparse en regiones de datos, con sólo un proceso permitido dentro de una región a la vez. No debe confundirse las regiones de datos, que son colecciones de datos, con las secciones críticas, las cuales son segmentos de código.1No estudiaremos las regio­ nes de datos en forma adicional, pero serán motivo de un examen posterior para una solución menos costosa conocida como un monitor.

Semáforos , Una manera de administrar las CS y eliminar el estado de espera productiva la constituye el semáforo, el cual se implemento por vez primera en ALGOL 68. ,Un semáforo actúa de manera muy parecida a su similar en una vía de tren. Cuando T?stá hacia abajo, la ejecución se detiene; en cambio, si se halla hacia arriba, un pro­ ceso puede seguir. Los trabajos reales de un semáforo, entre ellos su lista de proce­ sos en espera, por lo regular están implementados en forma muy profunda en un sistema operativo, con usuarios que tienen acceso a él a través de sus dos operacio­ nes: espera y señal. Un semáforo es una variable entera S, no negativa, en la cual se define dos ope­ raciones, Wait y Signal.2S se inicializa a 1 (arriba) de modo que algún proceso pueda proceder a realizarse. Walt (S) begln I f S - up then S := down; [Bloquea otros procesos y entra la CS} else [pone el proceso de llamada en la cola de espera} end [Espera};

(5.3.1)

1 El lector deberá estar prevenido de que en alguna literatura, una sección crítica de código puede ser denominada como región crítica. 2 El procedimiento Wait (Espera) al principio se llamó P [Dijkstra, 1968a], la primera letra de la palabra holandesa passeren, "pasar". Signal (señal) era V, que provenía de vrygeven, "liberar".

Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

241

Signal (S) begln ff CI o mas procesos están es perando en S3 then (procede 1 en la CS! else S up end C S e ñ a l 3;

Para los cinco palillos (del caso de los filósofos), necesitaríamos un semáforo S. para cada uno. Si P. tuviera hambre, ejecutaría Wait(S.) y Wait(S(i+1)mod5). Si ambos palillos estuvieran disponibles (S. = arriba y S(i+1)mod5= arriba), podría introducir un CS y comenzar a comer de inmediato. De otro modo, aquél esperará hasta que los procesos que utilizan los palillos señalen su disponibilidad. Por simplicidad, consideraremos el semáforo S binario, el cual toma sólo valo­ res de 0 (abajo) o 1 (arriba), y dos procesos, Processl y Process2. Si un proceso ejecuta un Wait (S) y encuentra el semáforo arriba, S lo pone primero hacia abajo para bloquear otros procesos. Processl entonces ejecuta su código crítico así como una Signal(S), y pone el semáforo de vuelta hacia arriba. Dos procesos, P1 y P2, desean ejecutar código que modifica variables compartidas; podrían ser progra­ mados a utilizar: var S : semaphore;

(5.3.2)

procesa Pl: loop (siempre! Wait (S); CCS -para P13 Signal (S): COtro codigo no critico! end [ciclo! end C P U : process P2: loop (s iempre! Wait (S); CCS para P23 Signal (S): COtro codigo que no es critico! end Cdel ciclo! end CP11:

Note que si process Pl toma primero su CS, y process P2 ejecuta Wait (S), la ejecución de process P2 será suspendida hasta que process Pl ejecute su S i g n a l ( S). La espera productiva se elimina al utilizar semáforos, puesto que un proceso de espera es sacado de la cola de espera cuando los recursos compartidos se hacen disponibles. El procedimiento Wait "pone un proceso a dormir" si otro proceso está empleando los recursos compartidos; mientras tanto, Signal "despierta un pro­ ceso dormido", si existe alguno. Un semáforo puede ser utilizado para un propósito simple. S impone la exclu­ sión mutua. Otro puede usarse para coordinación del tiempo. El problema del pro­ Sólo fines educativos - FreeLibros

242

PARTE n: Lenguajes imperativos

ductor-consumidor es aquel en el que se producen y consumen bienes de manera concurrente. Un consumidor no puede adquirir un recurso hasta que haya sido producido, y los consumidores compiten por los recursos disponibles. El ejemplo más simple involucra un solo productor y uno o más consumidores; en este caso el productor produce un nuevo recurso sólo cuando la reserva de recursos está vacía, y un consumidor consume cuando está llena. La relación productor-consumidor necesita sincronización. Esto requiere dos semáforos: full (lleno) inicializado hacia abajo; y empty (vacío), hacia arriba. full empty

down; up;

[Nada di sponible para el consumidor) [Adelante y produzca algo)

(5.3.3)

Producer: loop (siempre) Wait (empty); Produce somethlng; Signa! (full); end loop; Consumerl: loop (siempre) Wait (full); Consume resource; Signa! (empty); end loop;

Con$umer2:

loop (siempre) Wait (full); Consume resource; Signa! (empty); end loop;

Debido a que empty comienza hacia arriba, con lo cual revela que la reserva de recursos está vacía, el productor puede comenzar a producir. Los dos Consumido­ res tendrán que esperar hasta que la reserva esté llena ( f u l l - up ) para comenzar a consumir. Puesto que ambos Consumidores están en espera de la misma señal de f u l l , se necesita alguna clase de sincronización. Hemos supuesto en ambos ejemplos que un semáforo puede tomar uno de sólo dos valores, arriba o abajo. Esto no es necesariamente el caso. Con variaciones menores, un semáforo puede tener cualquier valor positivo. Nuestros consumido­ res pueden esperar por f u l 1 > 0. Con f u l 1 inicializado a 0, y empty a 1, Wait y S i g na 1 tendrían un aspecto como el siguiente: WaitíS: Semaphore) begin 1f S > 0 then S := $-1; [Bloquea otros procesos y entra en la C$3 else [pone el proceso de llamada a dormir en la cola de espera) end [Espera); Signa! (S :S e m a p h o r e ) begin I f (1 o mas procesos están es perando en S) then (despierta un proceso y lo deja proceder en la CS) else S := S + 1; end ( S e ñ a l 3;

Sólo fines educativos - FreeLibros

(5.3.4)

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

243

Los semáforos no pueden garantizar racionabilidad o prevenir inanición. En el ejercicio 5.3, en su apartado 2, se le solicitará que resuelva el problema de la cena de los filósofos con el empleo de semáforos. Por desgracia, no podemos asegurar que un filósofo rendirá sus dos palillos una vez que comience a comer. ¡Los semáforos no pueden eliminar la glotonería! Monitores Un monitor es una interfaz entre los procesos concurrentes del usuario, y propor­ ciona: • •

Un conjunto de procedimientos que el usuario puede llamar. Un mecanismo para programar llamadas a estos procedimientos si otros pro­ cesos que se ejecutan en forma concurrente solicitan su uso antes que el proce­ dimiento haya terminado. Un mecanismo para suspender un procedimiento de llamada hasta que esté disponible un recurso (delay) y entonces reavivar el proceso (continué).

Un monitor no tiene acceso a las variables no locales y puede comunicarse con otros monitores sólo por medio de procedimientos de llamada en ellos. De este modo, un monitor sirve como un policía intermediario entre dos o más procesos cooperativos. Un monitor puede ser considerado como un tipo de datos abstractos que inclu­ ye una estructura de datos compartidos y todas las operaciones que los diversos procesos (concurrentes) pueden realizar en él. Estas operaciones determinan una operación de iniciación, derechos de acceso y operaciones de sincronización. Los procesos concurrentes F Pn deben ser prevenidos de tener acceso al mismo ele­ mento de datos en forma simultánea. Otras funciones del monitor consisten en evitar la alternancia sin sentido de los procesos o inanición, en los cuales uno o más procesos se ejecutan de manera indefinida mientras que otro nunca se activa. Otras operaciones de sincronización deben evitar el estancamiento, en el que todos los procesos son suspendidos, espe­ rando por algún evento que nunca ocurre. Un monitor tiene la forma que se muestra en el listado (5.3.5). ■onltor CNombreMonitor> var <declara c1one s de variables pe rmanentes> procedure

{<lista-parametrosl>) •••

procedure

(<lista-paranietrosH>) begln end;

Sólo fines educativos - FreeLibros

(5.3.5)

244

PARTE II:

Lenguajes imperativos

Las variables permanentes se mantienen a través de cada invocación del monitor. De este modo, un monitor, como un objeto, tiene un estado. Los procesos pueden llamar las operaciones de un monitor del mismo modo que se haría una llamada a procedimiento. Las variables permanentes se pueden accesar sólo a través de estas llamadas. Dos operaciones, además de las definidas en el monitor, están asociadas con cada monitor. Éstas son delay y continué, que son análogas a las del semáforo Wait y Signal. Un monitor también tiene una cola en la que almacena las solicitudes para accesos. De esta forma, la ejecución de un delay (retraso) implica un proceso en cola, y continué (continuación) extrae de la cola el primer proceso en espera y le permite entrar al monitor. Un monitor puede ser visto como un módulo [Parnas, 1972], con la mayoría de los detalles ocultos al usuario. Su implementación también estará oculta en el siste­ ma operativo, de modo que los usuarios se comportarán como si cada uno fuera el único proceso en ejecución. Un uso antiguo de los monitores fue el que se le dio en uno de los sistemas de tiempo compartido BASIC. Los usuarios del BASIC interactivo no se ejecutan en paralelo, sino uno a la vez, compartiendo un solo CPU. Sus procesos eran suspendidos o se les permitía ejecutarse con el uso de ope­ raciones de monitor asociadas con dos colas, una para los procesos suspendidos y otra para los procesos terminados. Éstos también se incluyen en voto Concurrent Pascal y en Modula. Ben-Ari [Ben-Ari, 1982] demuestra que los monitores pueden ser reemplaza­ dos por semáforos, excepto para la suposición primero en entrar, primero en salir (FIFO, por sus siglas en inglés) en las colas del monitor. Cuando se usa semáforos, lo que el proceso haga a continuación será aleatorio más que ordenado. Pero la -decisión de utilizar un monitor o un semáforo por lo general depende de lo que tenga disponible el programador. El beneficio principal de los monitores radica en la claridad y confiabilidad del sistema que los emplea, no en su operación.

Rendezvous (Punto de reunión) El punto de reunión o rendezvous incluye la sincronización, comunicación y eje­ cución de un bloque de código en uno, dos o más procesos que se ejecutan de manera concurrente. Coordina lo que se conoce como llamadas de procedimiento re­ moto (RPCs; remóte procedure calis); es decir, un procedimiento que se ejecuta en un procesador remoto llama a uno que se localiza en un procesador diferente. El pro­ cedimiento o función que llama es un cliente del procedimiento o función que acep­ ta, el cual se conoce como el servidor. Cada tipo de rendezvous en un proceso de servidor se denomina una transacción. Esta palabra es una sugerencia de lo que ocurre en realidad. Un cliente envía un mensaje hacia un servidor al cual solicita servicio de alguna clase y es bloqueado de ejecución adicional hasta que el servicio se realiza. Un rendezvous puede implementarse con o sin memoria compartida. El rendezvous difiere de un monitor en dos formas fundamentales. La prime­ ra, no es un módulo separado que coordina procesos en ejecución, sino que se con­ sigue a través de los procesos mismos. Un proceso (el cliente) inicia una llamada, y otro (el servidor) la acepta. La llamada es procesada por el servidor, que recibe Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

245

cualquier parámetro transmitido mediante la llamada y devuelve valores de pará­ metro hacia el cliente. La segunda forma en la que difiere es que un proceso (llamado una tarea o task en Ada) puede incluir varias entradas (entry), que otros procesos pueden llamar. Cada entrada (entry) mantiene su propia cola de solicitudes, mientras que un monitor tiene sólo una cola. Un proceso que gana el acceso a un monitor puede llamar cualquiera de sus procedimientos, mientras que un proceso que solicita un rendezvous debe obtener en la cola cada entrada que necesite. Además del lenguaje de producción Ada, los rendezvous están implementados en CSP (Communicating Sequential Processes; procesos secuenciales de comuni­ cación), un lenguaje experimental para explorar las facilidades de la programación concurrente, y en Concurrent C.

Paso de mensajes El paso de mensajes involucra dos asuntos: cómo son designados las fuentes y los destinos y en qué forma son sincronizados los procesos. Una fuente y un destino definen un canal de comunicaciones. La designación más simple es el nombrado directo; es decir, enviar datos a un receptor o recibir datos de un emisario; aquí "receptor" y "emisario" son los nombres de los procesos. Cuan­ do múltiples procesos envían o reciben mensajes en un momento dado, puede ser necesario el almacenamiento intermedio o temporal (buffering) para mantener un mensaje hasta que un proceso de recepción esté listo para él. Un almacén temporal (buffer) de este tipo con frecuencia es llamado un buzón (mailbox). En el caso parti­ cular en el cual sólo existe un receptor pero muchos emisarios, el buzón se conoce como un puerto (port). Un programa particular puede involucrar varios puertos, pero un acuse de recibo designará un solo puerto. La idea aquí es que todas las solicitudes para un servicio particular vayan a un solo buzón o puerto. Otra noción de canal es la de una tubería (pipe), en la que la salida de un proceso es entubada o dirigida como la entrada hacia otro. Ambos procesos pueden estar ejecutándose de manera concurrente, con el segundo proceso recibiendo la entrada de la tubería a medida que la produce el primer proceso. Sin embargo, las tuberías fluyen sólo en un sentido. Ya examinamos la noción de rendezvous, en el cual los mensajes pue­ den enviarse en cualquier dirección. La sincronización del paso de mensajes difiere de aquella para recursos com­ partidos puesto que no es necesario mantener secciones o regiones críticas. Inclu­ so, un proceso que reciba un mensaje debe estar listo para recibirlo, o el proceso que lo envía debe esperar hasta que el receptor esté listo para procesarlo. La espera por lo regular se administra mediante una o varias colas. Existen cuatro modelos básicos de paso de mensajes: Punto a punto. La técnica más simple de paso de mensajes consiste en invo­ lucrar un proceso que envía un mensaje hacia otro, el cual lo recibe. Algunos lenguajes, como SR y Concurrent C, estipulan recepción condicional. Por ejem­ plo, una solicitud en Concurrent C para abrir un archivo, si no está protegido, puede codificarse como sigue [Bal, 1989]: Sólo fines educativos - FreeLibros

246

PARTE II:

Lenguajes

im p e r a t iv o s

accept open (f) suchthat no t _ l o c k e d t f )

C ...proceso abrir co dificado aqui...

} Si el archivo está protegido, la solicitud no será aceptada. Los esquemas punto a punto son simétricos si el proceso que llama así como el que recibe se nom­ bran entre sí. El esquema anterior es asimétrico porque el receptor no nombra al emisario. En este caso, un emisario el cual solicita que un archivo sea abierto está dispuesto a que esto se realice por cualquier proceso que sea capaz de hacerlo así. Los mensajes punto a punto pueden pasarse en forma sincrónica o asincrónica. En el paso sincrónico, el proceso de envío es bloqueado hasta que el receptor está listo para aceptarlo. Si el paso es asincrónico, el emisor conti­ núa la ejecución aun cuando su mensaje no haya sido aceptado. En un sistema sincrónico puede haber sólo un mensaje pendiente de cualquier proceso, mien­ tras pueda haber hasta ahora varios mensajes por ser contestados desde un emisario asincrónico. Occam, el lenguaje ensamblador para trasponedores y un lenguaje derivado de CSP, pasa los mensajes de manera sincrónica; por otro lado, NIL (Network Implementation Language; lenguaje de implementación de red) es implementado en forma asincrónica. Punto de reunión (Rendezvous). Ya se discutió el punto de reunión o rendezvous, basado en los tres conceptos: declaraciones entry, llamadas entry, y declaraciones accept. El punto de reunión es sincrónico por completo e involucra sólo dos procesos: el emisor, el cual es suspendido hasta que es acep­ tado, y el receptor. Llamadas de procedimiento remoto (RPC). Las RPC son muy parecidas a los procesos que se usan para realizar el rendezvous o punto de reunión. Sin embargo, están destinados a tener exactamente el mismo significado que los procedimientos regulares. Cuando esto puede lograrse, permite la codificación de procesos concurrentes en lenguajes de procedimiento tradicionales y deja que los programas convencionales sean transportados al sistema de sincronización. Las RPC han sido consideradas para su uso con Modula-2 e implementadas en el sistema operativo V y en CLU concurrente. Paso de mensajes de tipo uno a muchos. El paso de mensajes de tipo uno a muchos recibe también el nombre de transmisión, en la medida que se compor­ ta de manera muy semejante a una estación de radio en la que todos los recep­ tores escuchan el mismo mensaje. Un tipo es sin memoria temporal (unbuffered), de modo que un mensaje enviado puede ser obtenido sólo por aquellos proce­ sos listos para recibirlo. Si los mensajes tienen memoria temporal (buffered), pue­ den permanecer en esa memoria de manera indefinida de modo que los procesos puedan recibirlos en cualquier momento. Un lenguaje que implementa el paso de mensajes de uno a muchos es el BSP (Broadcasting Sequential Processes; procesos secuenciales de transmisión), otro derivado de CSP. Sólo fines educativos - FreeLibros

CAPÍTULO 5: Construcciones de lenguajes para procesamiento en paralelo

247

E J E R C I C I O S 5.3 1. Una fila de cafetería es una buena aplicación para el procesamiento en paralelo. Los consumidores están uniéndose a la fila al mismo tiempo que otros la dejan. Esta situación tipifica un problema de productor-consumidor en el cual no es posible "consumir" un elemento hasta que haya sido "producido", pero los consumidores y los productores pueden trabajar en paralelo. Escriba un algoritmo informal para si­ mular una fila de cafetería, con dos procesos, HacerAlmuerzo y ComprarAlmuerzo, funcionando en paralelo. Pruébelos con algunos consumidores simulados. Intente evitar: • Inanición: un comprador espera siempre mientras los almuerzos se preparan. • Alternancia: un segundo almuerzo no se prepara hasta que el primero es vendi­ do. • Estancamiento: un preparador de almuerzo espera por una señal para hacer otro almuerzo, mientras que un comprador espera una señal para comprar uno. 2. Utilice cinco semáforos binarios, Palillo0 a Palillo4, y escriba procesos P. Philosopher (filósofo) que se ejecutarán en paralelo para implementar el problema de la Cena de los Filósofos. La sección crítica incluirá la declaración Eat (comer) y deberá estar rodeada por operaciones de Wait (espera) y Signal (señal). En el principio, cada palillo, deberá estar establecido en up (arriba) para indicar su disponibilidad. 3. ¿A qué tiene acceso un proceso cuando "entra a un monitor"?

5.4

ALGUNAS SOLUCIONES DE SINCRONIZACIÓN Diversas soluciones para ejecución en paralelo o compartida en un CPU han sido implementadas. Examinaremos algunas de ellas a continuación.

Semáforos en ALGOL 68/ C y Pascal S ALGOL 68 ALGOL 68 fue el primer lenguaje con un semáforo incorporado, y sus dos opera­ ciones, up (arriba) y down (abajo). El modo sena (tipo) de ALGOL estipula varios procesos para ejecutar en paralelo, con un contador que sigue la pista de cuánto tiempo esperar para la comunicación de otro proceso. Así, ALGOL 68 proporciona la posibilidad de evitar estancamiento de procesos ávidos. Cuando el contador al­ canza algún límite asignado con anterioridad, un proceso en ejecución que haya ejecutado un down sobre un semáforo en particular será forzado a ejecutar un up, que permitirá el siguiente proceso de espera para tener acceso a recursos comparti­ dos. C Cuando C está en ejecución bajo el sistema operativo UNIX tiene un semáforo, y sus operaciones están proporcionadas en una librería en tres archivos de siste­ mas, con encabezados sys/types.h, sys/ipc.h y sys/sem.h. Dos de las operacio­ nes son: Sólo fines educativos - FreeLibros

248

PARTE II:

Lenguajes imperativos

senil = semget ( . . . ) , el cual crea un semáforo llamado senil smectl (seml val), que restablece seml a val Las operaciones semaphore_$end y semaphore_wa i t, las cuales implementan Signal y Wait, son accesadas al utilizar semop. Por ejemplo, semop(semaphore_$end,. . . ) ejecutará una señal. Los semáforos escritos en C pueden utilizarse con las operaciones f o r k , execl y wait de UNIX. C (y UNIX) administran la memoria de diversas maneras, f o r k , execl ywait permiten que un programa en ejecución {padre) sea suspendido mien­ tras que otro programa se ejecuta y utiliza las mismas localidades de memoria. Una llamada a f o r k produce una nueva copia del programa padre, yexecKchil d_name, a l , a 2 , . . . , a n , 0 ) permite que un nuevo programa denominado c hi 1d_n ame se eje­ cute en lugar de esta copia, wai t fuerza al padre a permanecer en suspenso hasta que el hijo se completa. Un usuario puede ordenar a UNIX que ejecute dos o más programas "simultáneamente" con el uso del operador &de UNIX $ payroll hours employee payment & ed

comenzará la ejecución de payroll (nómina), pero permite al editor interrumpirlo si es necesario. Si se tiene disponible más de un procesador, estos operadores pue­ den administrar el procesamiento en paralelo. Un hijo (child) puede ejecutarse en forma concurrente con su padre (parent), y se puede bifurcar ( f o r k ) a su propio hijo también, como se muestra en la figura 5.4.1 . Una llamada a f o r k sin parámetros crea una nueva copia de su proceso padre (process 1), el cual se ejecuta en forma concurrente con el padre. Una llamada sub­ secuente a e x e c l ( p 2, a 1........an,0) reemplaza la copia de p r o c e s s l conp2 einicia el proceso p2 ejecutándose en forma concurrente con el padre, processl. Aquí p2 es un apuntador hacia una cadena de caracteres llamada process2, y an son apuntadores a los nombres de los argumentos deprocess2.

processl (procesol)

processl

execl (p2, a-,,..., an, 0)

FIGURA 5.4.1 Operaciones UNIX fork y execl para iniciar dos procesos ejecutándose en forma concurrente

Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

249

No hay límite (en teoría) para el número de procesos que pueden ejecutarse en forma concurrente en procesadores múltiples o bien intercambiarse dentro y fuera de un solo procesador. En el último caso, UNIX traslapa la memoria de un padre con la que necesita el hijo. De este modo, programas extensos pueden ejecutarse "en una memoria relativamente limitada, previniendo que es posible subdividir el texto y datos del programa de manera tal que cada uno y todos los [procesos] ejecutables se ajusten a las limitaciones de memoria de la máquina" [Silvester, 1984] (véase la figura 5.4.2). Puede establecerse una tubería a través de la capa de entorno (shell) de modo que la salida de un programa se dirija en forma directa como la entrada de otro. Por ejemplo, $ payroll

| Ipr

redirigirá la salida del programa de nómina (payroll) directamente hacia la impre­ sora en línea. Esto realiza la misma acción que los tres comandos: $ payro l1>scratch_fi 1e $ 1pr<scratch_fi 1e $ rm $cratch_file

/* salida de payroll hacia seratch_fi 1e /* envió de ser atch_fi 1e hacia la linea /* elimina scratch_file del sistema */

*/ de impresión */

De manera similar, $ payroll

| sort | Ipr

redirigirá la salida de la nómina hacia el programa s o r t (clasificar), y s o r t la diri­ girá hacia 1pr, para un listado clasificado y ordenado. Los tres programas comen­ zarían a ejecutarse de manera simultánea, con posibles pausas para la salida desde el otro. P a sca l S Pascal S viene de las palabras Pascal Secuencial, y es un intérprete que puede implementarse como un subconjunto aumentado de Pascal. El programa de Pascal

FIGURA 5.4.2 Jerarquía de procesos concurrentes

Sólo fines educativos - FreeLibros

250

PARTE II: Lenguajes imperativos

pascal s compila Pascal S en seudocódigo, llamado código-P, y luego procede a leerlo e interpretar un programa escrito en Pascal S. Niklaus Wirth es autor de Pascal y de Pascal S, que fue modificado más tarde por M. Ben-Ari en la Universi­ dad de Tel Aviv. Los procesos "concurrentes" son intercalados en vez de que se ejecuten de un modo concurrente. Pascal fue previsto como ion lenguaje de ense­ ñanza más que de producción, como lo es Pascal S. Cualquiera con un solo proce­ sador capaz de correr Pascal puede correr Pascal S, pero espere que sea ineficiente en tiempo de ejecución. Los procesos concurrentes P l, P2,...,Pn están señalados por: cobegin Pl; P2;..; Pn coend;

Wirth describe esta declaración como sigue: "la declaración cobegin es una señal al sistema de que los procedimientos encerrados no van a ejecutarse, pero van a mar­ carse para ejecución concurrente. Cuando se llega a la declaración coend, la ejecu­ ción del programa principal se suspende y los procesos concurrentes son ejecutados. El intercalamiento de las ejecuciones de estos procesos no es predecible y puede cambiar de una ejecución a otra. Cuando todos los procesos concurrentes han fina­ lizado, entonces el programa principal se reanuda en la declaración que sigue a la coend" [Ben-Ari, 1982]. El programa Pascal S del listado (5.4.1) es una solución al problema de la Cena de los Filósofos, con una variación. El semáforo del palillo está arriba (1) si un palillo se encuentra disponible, y abajo (0) si un filósofo lo está sosteniendo. El semáforo de hambre está arriba (de 1 hasta 4) si cuatro o menos filósofos están hambrientos; y abajo (lugar = 0) si hay cinco. Esto asegura (mediante el principio del casillero)3 que al menos un filósofo tendrá acceso a dos palillos. En este caso, el número de filósofos hambrientos representa la cantidad de nichos, y el número de palillos representa cinco pichones. El semáforo notHungry (no hambriento) trabaja como sigue: cuando un procedimiento Philosopher (Filósofo) comienza, el primer filósofo espera que notHungry tome un valor de 1 a 4, con lo cual indica que de cero a cuatro filósofos no están hambrientos y, por lo tanto, no están interesados en comer. Esto proporciona al menos una oportunidad de que habrá dos palillos dis­ ponibles. Cuando esto ocurre, wai t ( notHungry ) se ejecuta al decrementar notHungry en 1. Cuando también encuentra sus dos palillos adyacentes disponibles ( C[ i 1=1 y C[ ( i+1) mod 51-1), procede a eat (comer), después de lo cual hace una señal de que los dos palillos se encuentran disponibles para otro filósofo y que él ya no tiene (notHungry + 1) mod 5). hambre (notHungry pr ograi DiningPhiloso phers; const SomeBigNumber - maxint; var C: array[0..4] of tbinary] semaphore; nothungry : semaphore: (Supone valores 0..4.} i : integer:

(5.4.1)4

3 El principio del casillero (pigeonhole) establece que si se tiene n nichos y n+1 pichones, al menos un nicho debe acomodar dos o más pichones. ADe [Ben-Ari, 1982], con modificaciones.

Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

251

procedure Philosopheríi : integer); begin fo r j :™ 1 to So mebigNumber do think; wait(notHungr y); waitCCEil); w a i t C C C i + 1 ) mod 53); eat; signal CCti3); signal (CC(i+l) mod 53): signal (n ot H u n g r y ) end (for); end; (P) begin C m a i n 3 notHungry 4; [todos los 5 filósofos están Si nHambre (n o t H u n g r y )3 fo r i 0 to 4 do C t i 3 1; (todos los palillos están di sponibles) cobegln Ph ilosopher(ü); Ph ilosopher(l); Ph i 1 o s o p h e r (2); Philosopher(3 ); Ph i 1 o s o p h e r (4); coend end.

Tipos de proceso y monitor en Concurrent Pascal Recuerde que una memoria temporal (buffer) es un área de almacenamiento en un disco que se utiliza para guardar datos de entrada y salida de manera temporal. También puede ser implementada en Concurrent Pascal con el uso de un monitor. Existe dos formas para entrar a la memoria temporal, Send y Rece i ve. Send en­ vía tma página hacia la memoria temporal desde un proceso de llamada, y Rece i ve devuelve una página para un proceso desde la memoria temporal. En el monitor Di skBuffer, las entradas son procedimientos y son llamadas desde algún proceso, y controlan un dispositivo de entrada o salida. Estos controladores no pueden te­ ner acceso a un Virtual Di sk en forma directa, sólo a través del procedimiento monitor titulado Entry. La coordinación de los procesos en ejecución quizá concu­ rrentes que llaman a Send y Recei ve se realiza dentro del monitor. type DiskBuffer =* ■o n1tor(Conso leAccess, D i skA cce ss; Resource; Base, Li mi t : integer); var Cshared data) disk: VirtualDisk; Sender, Receiver ; queue; He.ad, T a i l , Length : integer; 5 [Brinch Hansen, 1978.

Sólo fines educativos - FreeLibros

(5 . 4 . 2 ) 5

252

PARTE II:

Lenguajes imperativos

procedure entry Send(Block: Page); (envía una pagina desde un procedimiento de llamada hacia el buffer o memoria temporal del disco) begin I f Length * Lim11 then de lay ( s e n d e r ) : (buffer f u l ! , wait) di sk.writeCBase + Tal 1, Block); Tail(Tail + 1) aod Limit; Length:- Length + 1; contlnue(Receiver) [t ransfiere el control a Receive (recibir) si hay algo en su cola, Receiver (r ecibidor)) end; procedure entry Re ceive(var Block: Page); [regresa una pagina para el proceso de llamada) begin end; begin init di sk(ConsoleAccess, DiskAccess): (o peración de iniciación no descrita aqui) Head:- 0; Tai 1 0 : L e n g h t := 0: end.

Si se mira el código en el listado (5.4.2) de manera descendente, primero vemos que DiskBuffer va a ser un tipo nonltor con cuatro parámetros. Los primeros dos, Consol eAccess y Di skAccess, serán variables de un tipo dependiente del sistema, Resource. Los dos segundos estipulan una dirección base en la que el DiskBuffer comienza y un límite (Limit), que fija su tamaño. El tipo Vi rtua 1Di s k es una clase de Concurrent Pascal, que incluye datos así como operaciones asociadas. Dos de estas operaciones son disk.writeydisk.read. Nuestra memoria temporal de da­ tos será de esta clase. Concurrent Pascal incluye un tipo integrado, queue (cola). Una cola (queue) puede estar asociada con un monitor para administrar múltiples procesos en la espera de un recurso que se solicitó en forma mutua. Un proceso de llamada, si no es retrasado en queue, tiene acceso exclusivo a las variables compartidas di s k, He ad, Tail y Length desde el begin (inicio) de la entrada que se llamó hasta que alcan­ za la declaración continué. Head se inicializa a 0, la dirección relativa del comienzo de la memoria temporal. Ta i 1, que es la dirección relativa del final (en páginas) de la memoria temporal o buffer, también se inicializa a 0, lo que indica un buffer vacío. El procedimiento Send incluye tres declaraciones que debe ejecutarse antes que un proceso llame, lo que, con éxito, ha ganado entrada para Send sin ser retra­ sado, dando datos compartidos a un proceso en la entrada del procedimiento Recei ve. Estos escriben a la memoria temporal e incrementan Tai 1 y Length. Aunque el monitor Di s kBuf f e r no lo estipula, los monitores de Concurrent Pascal pueden incluir llamadas a otros monitores.

Sólo fines educativos - FreeLibros

CAPÍTULO 5: Construcciones de lenguajes para procesamiento en paralelo

253

Rendezvous (Punto de reunión) en Ada y Concurrent C A da La unidad de programa de Ada con potencial para ejecutarse en paralelo con otras unidades se denomina una task (tarea). En cuanto a sintaxis, es semejante a un package (paquete); tiene una especificación y un cuerpo. ta sk T 1s

— especificació n

(5 . 4 . 3 ) 6

•♦ •

end T; task body T 1s

— cuerpo

end T;

Veamos un ejemplo simple de tareas que puede ejecutarse de manera concu­ rrente. Supongamos que planeamos una fiesta. procedure Plan_Party i s

(5.4.4)

task Invitations; task body Invitations is begin Write_Invitations; Mail_Them; end Invitations; task Clean; task body Clean is begin Clean_House; end Clean; begin Prepare_Food; end Plan_Party;

P l a n _ P a r t y es la unidad padre para las dos tareas I n v i t a t i o n s y Clean. Cuando

procedure Plan__Party se está ejecutando, y se alcanza el begin para esta unidad

padre, las dos tareas locales comenzarán en forma automática a ejecutarse tam­ bién. El end para Pl an_Pa r t y no puede ejecutarse hasta que todas las tareas locales se hayan terminado. En el esquema anterior, los tres procedimientos, I n v i t a t i o n s , C1 ean y Prepa re_Food, se ejecutan en forma concurrente, pero no en algún orden en particular. No existe comunicación entre ellos. Dependiendo del compilador y del

6 La convención utilizada por el manual de referencia de Ada [ANSI/ISO-8652,1995] implica el uso de letras minúsculas en negritas para las palabras reservadas, y sólo la primera letra versal para otros identificadores.

Sólo fines educativos - FreeLibros

254

PARTE n: Lenguajes imperativos

hardware, estos tres procedimientos podrían ejecutarse en paralelo o en un solo procesador al emplear alguna clase de tiempo compartido. Ahora imaginemos un poco de la fiesta, de manera que las tareas puedan co­ municarse entre sí. En Ada esto se realiza por medio de una tarea (la que llama), accept (aceptando) una entry en ella cuando es llamada por otra tarea. Una entry (entrada) es una llamada de procedimiento, pero lo que hace es determinado por la tarea que acepta la llamada (véase el listado (5.4.5)). procedure Plan_Party is

(5.4.5)

type Name_List is array (i nteger range <>) of S t r i n g ( l . .50); task In vi t a t i o n s is entry Guest_List(Ñames: in Name__List); end I n vi t a t i o n s; task body I n v i t a t i on s is

Guests: Name_List; begin accept Guest_List(Ñames: in Name_List) do

Guests:= Ñames; end Guest_List;

Wr it e_Invi tat i ons ; Mail_Them; end I n vi t a t i o n s; task Clean; task body Clean is begin

Clean_House; end Clean;

G: Name_List; begln

Prepare_Food; Read_List(G); I n vi t at i on s. G u es t_ Li s t ( G ) ; end Plan_Party;

Aquí, P la n _ Pa r t y es el llamador para I n v i t a t i o n s . Puesto que todas las tres tareas, Plan_Party, I n v i t a t i o n s y Clean comienzan en forma simultánea, I n v i t a t i o n s debe esperar para accept (aceptar) la entry G u e s t _ L i s t hasta que sea enviada por el procedimiento principal. Lo que ocurre durante el accept... do... end ; es el rendez­ vous o punto de reunión. Un ejemplo simple pero práctico de tareas es el de una memoria temporal de entrada/salida para un carácter simple, como se muestra en el listado (5.4.6). task Char_Buffer Is entry Read (C: out C h a r a c t e r ); entry Write (C: In Character); end Char_Buffer;

(5.4.6) --Lectura desde la memoria temporal (buffer) — Escritura hacia la memoria temporal

Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

255

task body Charjiuffer 1s F u l l : Boolean False: Ch: Character; loop select when Full -> accept ReadíC: out Character) do C Ch; end Read; Full

False;

or when not Full -> accept WritetC: In Character) do Ch C; end Write; Full True; or te riln a te ; end select; end loop; end Char_Buffer;

Como es usual, existe varias cosas que notar aquí. Antes que nada, Char_Buffer. Read(. . . ) y Char_Buffer.Wri t e ( . . . ) serán llamadas por otras tareas. Cada entra­ da tiene una cola asociada, de manera que si varias tareas están intentando Read (leer) o Wri te (escribir) de manera simultánea, las llamadas serán puestas en una cola y procesadas en orden FIFO (primero en entrar, primero en salir). Por supues­ to, debemos tener algo en una memoria temporal antes de Read (leer) de ella. De este modo accept Read se guarda mediante la expresión when Fu11. La declaración select significa que cualquiera de las declaraciones accept puede elegirse sin un orden particular. Más específico, si la memoria temporal se encuentra vacía y Ful 1-Fal se, la tarea Char_Buffer puede seleccionar (select) una llamada desde la cola Write antes de aceptar un Read. Otra manera de manejar una memoria temporal de carácter de lectura/escritu­ ra puede ser establecer Read_Char y Wr it e_Char como dos tareas y comenzarlas ejecutándose simultáneamente, como se ilustra en el listado (5.4.7).

procedure B u f f e r_ Ta s ks ; Ful 1; boolean

False;

pragia Sh a red ( F u l l ); Ch: Character; pragna Sha r e d (C h ); task Read_Char is entry ReadíC: out Character); end Read_Char;

Sólo fines educativos - FreeLibros

(5.4.7)

256

PARTE II:

Lenguajes imperativos

task write_Char 1s entry Write (C: 1n Character); end Write_Char; task body Read_Char 1s begin loop when Ful 1 -> accept ReacKC: out Character) do C Ch; Ful 1 False; end Read; end loop; end R e a d :C h a r ; task body write„Char 1s begin loop accept Write(C: 1n Character) do Ch C; Ful 1 True; end Write; end loop; end Write_Char; begin — Establece la ejecución de Read_Char asi como de Write__Char end B u f f e r _ T a s k s ;

Un pragma de Ada es una directiva de compilador. El Shared pragma, para implementar memoria compartida, garantiza dos cosas. Primero, si una variable com­ partida, tal como la Ch o Ful 1 anteriores, se lee en una sección crítica (CS) en una tarea, no será actualizada por ninguna otra tarea hasta que la CS termine. En el listado (5.4.7), una CS de este tipo se presenta para Ch entre accept Read y su end. Segundo, si una variable compartida se actualiza en una CS, no será ni leída ni actualizada por ninguna otra tarea hasta que se salga de la CS. Esto ocurre entre accept W r i t e ysuend para Ch, y en ambas declaraciones accept para Ful!. Ada no implementa semáforos ni monitores, de modo que éstos deben progra­ marse si deseamos evitar los problemas de sincronización discutidos en la sección 5.3. Sólo variables simples o access (apuntador) pueden declararse Shared, por tal motivo debemos prever otros medios para la protección de datos estructurados. El estudio se encaminará hacia esto en el Laboratorio 5.1, donde construiremos una memoria temporal de lectura/escritura más grande que un simple carácter. Las tareas de Ada pueden ser dinámicas así como estáticas. Es decir, se les puede crear o destruir a medida que se ejecuta un programa. Supóngase que de­ seamos crear tareas Cha r_Buf f er a medida que van necesitándose en un programa, y también la capacidad de tener más de una memoria temporal de este tipo a la Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

257

vez. Para conseguir esto, agregaríamos la palabra reservada type a la declaración de tarea, y luego crearíamos valores tipo access apuntando a las tareas, como se muestra en el listado (5.4.8). task type Char_Buffer is entry Read

(5.4.8)

(C: out Character);

entry Write (C: in Character); end CharJBuffer; type Buffer_Ptr is access Char_Buffer; P, Q: Buffer^Ptr; begin P := new Buffer_Ptr; Q := new Buffer_Ptr; P := nuil;

Q

:= nuil;

end;

Ada no destruye de manera dinámica los objetos creados a través de un proce­ dimiento como d1 spose. Esto fue una decisión de diseño de Pascal para eliminar apuntadores colgantes que señalen a objetos de datos no existentes. En Ada, asig­ nar a una variable access el valor nul 1 hace inaccesibles a los objetos. Sus localida­ des de memoria serán liberadas cuando la ejecución del programa salga del alcance de los objetos.

Concurrent C El lenguaje C no tiene construcciones para procesamiento concurrente, aunque vi­ mos cómo los procedimientos de ejecución concurrente podrían ser implementados con el uso de directivas hacia el sistema operativo UNIX. Concurrent C se desarro­ lló para proporcionar un tipo process y sus operaciones asociadas como caracterís­ ticas de lenguaje concurrente. No implementa memoria compartida, pero emplea el paso de mensajes sincrónico con la ejecución del programa del cliente y bloquea hasta que el servicio haya sido recibido. Ello difiere de Ada en varias formas, de las cuales mencionaremos cuatro. En primer lugar, las transacciones de C son simila­ res a llamadas de función, mientras que en Ada son como procedimientos. Esto significa que una llamada puede aparecer en cualquier lugar en el que una función sería apropiada; por el contrario, en Ada una llamada es siempre una declaración. C estipula prioridades de proceso especificadas por el usuario; en Ada, siempre son procesadas en orden FIFO. C permite una llamada de transacción con paráme­ tros, de modo que sólo aquellas llamadas que satisfa*gan ciertos criterios serán ser­ vidas. En Ada, cuando se introduce un bloque principal con tareas, sus tareas son activadas también y terminadas cuando se sale del bloque. En Concurrent C se debe activar cada proceso, por ejemplo, Sólo fines educativos - FreeLibros

PARTE n: Lenguajes imperativos

25 8

process buffer b; b = create buffe r(128);

También puede finalizarse mediante c_abort(b);. Otras diferencias más sutiles pueden encontrarse en [Gehani, 1986], Concurrent C tiene un tipo, process, que requiere de una parte de especifica­ ción (spec) y una de body (cuerpo). La spec es visible para otros procesos, mientras que body no. Un proceso de servidor incluye una declaración accept para recibir llamadas de transacción (trans). Veamos el problema de la Cena de los Filósofos implementado en Concurrent C, como se muestra en el listado (5.4.9). process spec C h o p s t t c k ( )

(5.4.9 ) 7

C

trans vold pick_up(); trans vold put.downí);

); process spec Ph i1 os o p h e r ( Int id, process Chopstick left, process Chopstick right); //define

LIFE.LIMIT 100000

process body P h i1o s o p h e r (i d , left, right)

{ Int times_eaten; fo r (ti mes_eaten - 0; times_eaten !- LIFE_LIMIT; times_eaten++) C /* piensa, luego entra al cuarto de la cena */ /* levanta Palillos */ right.pick_up(); left.pick_up( ); /*come*/ p r i n t f C P h i l o s o p h e r % d : * b u r p * / n M , id); /*baja Palillos*/ left.put_down(); ri g t h . p u t _ d o w n ( ); /*se levanta y abandona el cuarto de la cena */

} process body C h o p s t i c k O

C fo r(;;) select

/*siempre */

} accept pick_up(); accept put_down();

7 La Cena de los Filósofos en Concurrent C. Reproducido con permiso de N. H. Gehani y W. D. Roome, Concurrent C, © 1986 por John Wiley & Sons.

Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

259

or terslnate;

] ) mainO

C process Chopstick f[53: in t j;

/*arreglo de cinco palillos*/

/* pr1mero crea los palillos, después procede a crear a los Filósofos */ for (j - 0; j < 5; j++) fCj] - create Chops ti c k { ); fo r (j - 0; j < 5; j++) create Philosopherfj, f[j], f C (j + 1 ) %8 5]);

) Cada filósofo existe sólo hasta que haya comido 100 000 veces. Los palillos siguen siempre. Sin embargo, una vez que todos los filósofos terminaron, los pali­ llos no tienen nada que hacer, y así la opción or de la declaración select se elige y cada palillo termina. Entonces, puesto que todos los procesos se completaron, el programa mai n de Concurrent C también puede terminar. L A B O R A T O R IO 5.1: S IM U L A C IÓ N DE P R O C E S A M IE N T O EN P A R A L E L O : A D A

O bjetivos (Los Laboratorios pueden encontrarse en el Instructor's Manual) 1. Experimentar con diferentes métodos de sincronización una memoria temporal o buffer implementado a través de tareas. 2. Observar qué ocurre cuando la memoria temporal es accesada sólo por una tarea exterior o por varias. 3. Diseñar un esquema de sincronización para dos tareas de clientes con el uso de la memoria temporal de modo que el cód ig o de cadena de los dos clientes no se entre­ mezcle.

Paso de mensajes en Occam Los mensajes en Occam se envían a través de canales que son visibles tanto el pro­ ceso de llamada así como para el proceso de aceptación. Con cada canal está aso­ ciado un protocolo, que describe el tipo de datos capaz de ser enviado a través de cada canal. Un canal y los dos procesos que conecta son establecidos cuando el programa que los declara es compilado, y ni los procesos ni el canal pueden estar asignados en tiempo de ejecución. Un mensaje es salida para el canal por un proce­ so y entrada al otro proceso del canal. Para sincronizar la comunicación del canal, el primer proceso, con el fin de emprender la entrada o la salida sobre un canal,

Recuerde que % es el operador mod en C.

Sólo fines educativos - FreeLibros

260

PARTE II: L en g u ajes im p erativ o s

debe esperar hasta que el otro proceso esté listo ya sea para salida del canal o en­ trada hacia éste. Cualquier operación adicional de un proceso es suspendida du­ rante un tiempo de espera. Si se desea comunicación bidireccional; es decir, que los datos entren al Proceso2 desde la salida del Procesol, y la subsecuente salida de los datos del Proceso2 sean la entrada al Procesol, entonces deben establecerse dos canales. El problema del productor-consumidor consiste de uno o más productores y uno o más consumidores trabajando en forma concurrente con un consumo posi­ ble sólo si un elemento se produjo y se encuentra disponible. La versión más sim­ ple es en la que un productor produce elementos para un solo consumidor, como se muestra en la figura 5.4.3. El productor no debe sacar un elemento hasta que el consumidor esté listo para introducirlo y procesarlo. Un esquema de Occam del problema, es: C U F OCCEXAMP.OCC

(5.4.10)

--archivo oc cexamp.occ //INCLUDE “hostio.inc" PROC pr o d c o n (CHAR OF SP fs, ts, [] INT memor y)9 #USE “hostio.1ib" CHAN OF BYTE input, output, source PROC producer PROC consumer PROC interface PAR interface (fs, ts, input, output) p r o d u c e r (i n p u t , source) consumer(output, source)

}}} Canal de entrada

Canal de salida

A >f Proceso productor

Canal fuente

Proceso consumidor

FIGURA 5.4.3 Sistema productor-consumidor 9 f s y ts son canales desde y hacia el servidor de archivos anfitrión, como se describe en el "Occam 2 Toolset User M anual" ("M anual de Usuario del Conjunto de Herramientas de Occam 2"), que es parte del CSA Transputer Education Kit. El tipo de canal lo indica las palabras clave CHAN OF. El protocolo SP utilizado por los canales del archivo anfitrión está definido en “hosti o . i nc”.

Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

261

Los paréntesis de llave indican un fold (doblez), en el que el texto del programa puede estar oculto. Aquí utilizamos el conjunto de herramientas Occam 2, que se ejecuta en el equipo de educación transponedor de CSA (CSA; Transputer Education Kit) e incluye el editor de doblaje Origami. En un programa completo, los dobleces se convierten en comentarios, precedidos por y con remplazo de Los dobleces pueden estar anidados. Occam tiene una orientación hacia el uso en pan­ talla, de modo que si el texto del programa llega a ser muy largo para una pantalla simple, algo de ella puede ser doblada en un espacio más pequeño. El programa completo es: — archivo oc cexamp.occ #INCL UDE "hostio.inc" PROC prodcon (CHAN OF fs, ts, SP, □ INT memory) #USE “hostio.lib" CHAN OF BYTE input, output, source — CíC PROC producer PROC producerCCHAN OF BYTE input, source.ch) BYTE x: NHILE TRUE SEQ input ? x source.ch ! x —

--procesa se cuencialmente

3]33

— CtC PROC consumer PROC consumeríCHAN OF BYTE output, destination.ch) BYTE y: NHILE TRUE SEQ destination.c h ? y output ! y -333 — CCC PROC interface PROC interface (CHAN OF fs, ts, SP,CHAN OF BYTE to.prod,from.cons) BOOL done: BYTE c h l , ch2, result: VAL end IS •**': SEQ s o . w r i t e . n l ( f s , ts) done FALSE NHILE NOT done SEQ so.getkeytfs, ts, chl, result) to.prod ! chl from.cons ? ch2

— nueva linea

— espera por una clave, sin eco — envia al productor — hace eco en la pantalla

Sólo fines educativos - FreeLibros

(5.4. 11)

262

PARTE II: Lenguajes imperativos IF ch2 - end done :» TRUE TRUE S K I P 10 so.exitífs, ts, sps.success)

--}}} PAR interface ífs, ts, input, output) produceríinpu t, source) consumerfoutput, source)

E J E R C I C I O S 5.4 1. a. Rastree la Cena de los Filósofos en Pascal S (listado (5.4.1)) ungís cuantas veces para convencerse de que en realidad funciona. Recuerde que el orden en que los cinco procedimientos Ph11 osophert i ) son llamados es indeterminado, b. Elimine el ciclo notHungry (wait(notHungry),.signal(notHungry)) y rastree el progra­ ma de nuevo. ¿Es posible para un filósofo morir de hambre sin el semáforo de lugar? 2. SiHead es la dirección relativa del OiskBuffer del listado (5.4.2), ¿Qué es la variable Base? 3. Complete la entry Recei ve para el monitor Di skBuffer del listado (5.4.2). 4. ¿Por qué es necesario incluir una cláusula teralnate en la tarea Char_Buffer del listado (5.4.6), y por qué no está enumerada como una entry? 5. ¿Qué pasaría si dejamos la actualización de Fu) 1 de las declaraciones accept en la segunda implementación de memoria temporal del listado (5.4.7), como lo hicimos en el primer fragmento de Ada del listado (5.4.6)? 6. Escríba el código para una tarea de Ada con dos entradas para implementar el semá­ foro binario de Dijkstra (listado (5.3.1)). 7. El programa de la Cena de los Filósofos del listado (5.4.9) puede conducir a un estan­ camiento. ¿Cuándo ocurrirá esto? Sugiera cómo evitar este problema. Nótese tam­ bién que la salida de los procesos Philosophers pueden mezclarse si más de uno intenta tener acceso al mismo tiempo a la salida estándar. ¿Cómo podría resolverse esto?

5.5

TUPLAS Y OBJETOS Hay unidades de paralelismo además de los procesos sincronizados que hemos visto. Las versiones de los objetos del capítulo 4 pueden también ejecutarse en for­ ma concurrente. Aquí, los métodos actúan como procesos al ejecutarse en paralelo. Un paradigma radicalmente diferente, espacio de tupias, también puede utilizarse.

10Occam requiere de una declaración "else" para cada IF. Aquí se emplea la declaración muda SKIP.

Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

263

El espacio de tupias es un espacio de datos compartidos que no es propiedad de ninguno de los procesos. Las tupias que contienen datos así como procesos son extraídas desde y restauradas hacia el espacio a medida que se necesitan. Conside­ raremos esto a continuación.

El espacio de tupias de Linda No todos los procesos se comunican a través de paso de mensajes o mediante loca­ lidades de memoria compartida. Estos dos últimos sufren de algún grado de no confiabilidad debido a la necesidad de la sincronización administrada por el pro­ gramador. Linda,11 desarrollado en la Universidad de Yale, es un lenguaje para procesamiento en paralelo que implementa el espacio de tupias. Actúa como una memoria asociativa y relaciona una dirección base con una clave en un almacena­ miento rápido. Los mensajes pueden, aunque no lo necesitan, ser pasados con el uso de tupias. El propio Linda no es un lenguaje de calidad de producción desarro­ llado por completo, pero se incrustó en diversos lenguajes, entre ellos Ada y C. A continuación, utilizaremos la sintaxis de C-Linda. Una tupia es una colección ordenada de elementos de datos, por ejemplo, ( “hola mundo", 22, 2.17). El espacio de tupias es la colección de tupias colocada en el es­ pacio de tupias con el uso de los operadores out y eval. Existe cuatro operaciones sobre tupias: out(t), 1n(T), rd(T), eval(t); y dos predicados, 1np() y rdp(). out(t) evalúa la tupia t y la coloca en el espacio de tupias. Por ejemplo, out ( “ hola mundo” , 22, 2.17) crea una tupia y la coloca en el espacio de tupias. 1n (T ) empareja la plantilla (descripción) T con una tupia en el espacio de tupias si existe uno, y la elimina del espacio de tupias. Si no se encuentra una tupia que coincida, el proceso que llama se suspende hasta que una está disponible. Si se encuentra más de una que coincida con T, se elige una de manera arbitraria para ser eliminada. Por ejemplo, 1n ( " h o l a mundo” , ? i , ? f ) elimina una tupia con la primera coordenada igual a “ hol a mundo” ; la segunda, cualquier entero; y la terce­ ra, un número de punto flotante, suponiendo que i y f hayan sido declarados con anterioridad como entero (integer) y punto flotante (float). rd ( T ) funciona de manera muy parecida a 1n, pero la tupia emparejada perma­ nece en el espacio de tupias. Se devuelve una copia de la tupia emparejada, rd permite que el espacio de tupias funcione como memoria sólo de lectura, la que puede ser compartida por cualquier número de procesos en ejecución. Si no se encuentra una tupia que empareja, el proceso que llama se bloquea. eval(t) es similar a out, excepto que la tupia se evalúa después, en vez de antes, y se coloca en el espacio de tupias, eval crea una nueva tupia activa (un nuevo proceso); por ejemplo, eval ( 45 , A l g u n a F u n c i o n ( x ) ) crea un nuevo pro­

11 El nombre "Linda", es una travesura irreverente. Ada fue llamado así en honor de Ada Lovelace, que según se cree fue la primera mujer programadora de computadoras quien trabajó con Charles Babbage en su máquina analítica. En la época en que el lenguaje Linda se encontraba en desarrollo, había una estrella del cine p*rnográfico llamada Linda Lovelace. De esta manera, con juvenil buen humor y en un intento de conservar los lenguajes "entre familiares", David Gelemtner nombró su nue­ vo lenguaje según el nombre de la Lovelace contemporánea: Linda.

Sólo fines educativos - FreeLibros

264

PARTE II:

Lenguajes imperativos

ceso, que se ejecuta en paralelo con el proceso que llamó eval. La tupia (45, A l g u n a F u n c i o n ( x ) ) está activa tanto tiempo como Al gunaFuncion se encuentre eje­ cutándose, y pasiva cuando AlgunaFuncion termine. Las dos operaciones de predicado (que devuelven el valor de verdadero o de falso) son Inp O y rdp(). Se comportan justo como 1n y rd, pero no bloquean el proceso de llamada. Si no se encuentra un emparejamiento, se vuelve falso (0 en CLinda) y el procesamiento puede continuar. La idea, puesta de forma simple, es que un proceso que desee alterar los datos removería una tupia del espacio de tupias al utilizar un tn, procesaría los datos y luego la devolvería al espacio de tupias utilizando out. Cualquier otro proceso será incapaz de tener acceso a esa tupia hasta que sea devuelta. Los mensajes también pueden ser enviados si se usa out y recibirse utilizando 1n. Las implementaciones de Linda incluyen un preprocesador que utiliza colas y semáforos, entre otras técnicas, para acelerar la búsqueda de tupias. Un kernel de Linda está incluido en el sistema operativo QIX, el cual se desarrolló para imple­ mentar el procesamiento en paralelo. Sus creadores afirman que es más eficiente que UNIX, mientras que mantiene una considerable compatibilidad con él. Tam­ bién afirman que hace la escritura de programas en paralelo más fácil, resultando independientes de la arquitectura en particular que se utilice. David Gelemter, el creador de Linda [Markoff, 1992], cree que el procesamien­ to en paralelo con el uso de una red ordinaria de estaciones de trabajo, o incluso de PC, caracterizará la moderna oficina del futuro. Por lo menos una firma de Wall Street conduce sus actividades cotidianas de comercio empleando Linda sobre una red y utiliza también ciclos ociosos de CPU de diversas máquinas a lo largo del día para generar grandes modelos matemáticos de sistemas financieros. Los procesos de Linda son débilmente acoplados; es decir, sin memoria com­ partida, lo que permite al programador ignorar muchos de los problemas de sincronización que se presentan en sistemas de memoria compartida. Un simple programa de C-Linda para simular un juego de ping-pong con dos jugadores, que demuestra la comunicación de procesos, se muestra en el listado (5.5.1). /* PING_PONG. CL - Dos procesos en comunicación */

( 5 . 5 . 1 ) 12

//define NUM_PING_PONGS 1000 real_ia1n()

í Int p i n g O , p o n g O ;

/*los dos procesos co operando */

eval ( p i n g O ) ;

/*

pone ping en el espacio de tupias y se

bi f ur c a a un nuevo proceso para r e a l i z a r la evaluación */

eval

(pongO):

} pingO

/*

definición de ping */

12 Tomado del C-Linda Reference Manual®, Scientific Computing Associates, New Haven, CT.

Sólo fines educativos - FreeLibros

CAPÍTULO 5: C o n stru ccio n es d e len g u ajes p ara p ro cesa m ien to en p a ra lelo

265

[ ín t í ; for (i = 0; i < NUM_PING_P0NGS; ++) { out(“ping” ); /*

evalúa

y

posteri ormente regresa ping al

espaci o de tupi as */

InCpong");

/* quita pong del espacio de tupias

*/

} } pongO

[ lí lt i ; fo r (i = 0; i < NUM_PING_P0NGS; ++) í 1 n ( "p ing” ); /* quita ping del espacio de tupi as */ o u t ( " p o n g ” ); /* evalúa y posteri ormente devuelve pong al espacio de tupias */

} } Este programa no hace más que conmutar el control hacia atrás y hacia adelante entre ping y pong. Ambos procesos comienzan su ejecución en forma simultánea; ping es evaluado después de pong y remueve a este último del espacio de tupias. Después de su ejecución ping remueve a pong del espacio de tupias, el cual puede entonces ser evaluado cuando el proceso pong alcanza out( “ pong” ). Si agregamos una pequeña gráfica en C para cada proceso después de cada out, podríamos ver la "pelota" ir hacia atrás y hacia adelante sobre la pantalla. Quizá ping podría enviar la pelota de izquierda a derecha y pong de derecha a izquierda. C-Linda también tiene una facilidad para módulos de temporización. Esto ac­ túa como un cronómetro y es útil para recolectar la estadística acerca de la ejecu­ ción en paralelo y factores de rendimiento. Existe también varios niveles de rastreo de tupias que pueden ser activados o desactivados. Una estructura de datos, tal como un arreglo, puede estar distribuido en varias tupias; por ejemplo, cada renglón o elemento podría ocupar una tupia por separa­ do. Un programa podría entonces ser escrito con un solo maestro y tantos trabaja­ dores como hubiera tupias de arreglo, todos corriendo de manera simultánea, para procesar el arreglo. Los ciclos del listado (5.5.2) calculan los "cincos" que se repiten en el renglón de tabla, m = [0 ,5 ,1 0 ,1 5 ,2 0 ,2 5 ,3 0 ,3 5 ,4 0 ,4 5 ]. //define FALSE 0 //define TRUE1 1nt 1nt 1nt in t

(5.5.2)

dim - 9; workers = //processors a v a í l ab l e

*mCd1m]; w or k e r O ;

for (i = 0 ; i <= di n; ++i) out( “ f i v e s r o w ” . i , FALSE. mCi]);

/ ^d is t r i b u ye el ar r eg l o en di ez tupi as*/

Sólo fines educativos - FreeLibros

266

PARTE II: Lenguajes imperativos /*start wo rkers*/ for (i - 0; i <» dim; + + i K e v a l ( “f u n ct ion” , i, w o r k e r O ) ;

} w o r k e r t ){ 1nt i , *p; 1n (“f i v es row” ,? i , FALSE, ?p); *p - i * 5; o u t ( “f i v es row” ,i, TRUE, p)

/*manti ene cualquier tupia di sponible sin procesar */ /^calcula el valor de * p [ i ]*/ /*!o pone de vuelta en el espacio de tupias*/

3 Las declaraciones son código C. El primer ciclo distribuye las localidades de ele­ mento del arreglo a tupias separadas. FALSE indica que el valor del elemento aún no se ha calculado. Note que ? i se convierte a i y ? p a p después que se evalúa p y se asigna a m[i ]. Cada uno de los procesos trabajadores iniciados por el segundo ciclo evalúan cualquier tupia sin procesar en el arreglo distribuido. Si hay menos procesadores que el número dim de tupias por ser procesadas, cada procesador tendrá que evaluar la función trabajadora (worker) más de una vez. Antes del pro­ cesamiento, la tercera tupia es ( " f i ves row” , 3, 0, p ), en el que p apunta al cuarto elemento del arreglo mC3]. Después out (t ), *p - 15, y la tupia es ( " f i ves row” , 3, 1, p).

En el Laboratorio 5.2, se le solicitará finalizar el programa productor-consumi­ dor para dos productores y un consumidor, para lo cual involucrará la comunica­ ción entre tres procesos ejecutándose en paralelo. L A B O R A T O R IO 5.2: P R O D U C T O R E S - C O N S U M ID O R E S : PA SC A L S / O CC AM 2 / C -L IN D A

O bjetivos (Los Laboratorios pueden encontrarse en el Instructor's Manual) 1. Ver cómo se implementan tres procesos cooperativos en un lenguaje concurrente. 2. Experimentar con diferentes soluciones para el problema del productor-consumidor utilizando la concurrencia. 3. Intentar un tipo diferente de solución: el espacio de tupias de Linda. Se le solicitará programar dos procesos de Productor y un proceso de Consumidor en los lenguajes seleccionados por su instructor.

Objetos como unidades de paralelismo La programación basada en objetos u orientada a objetos es el área más actual en investigación y en aplicaciones de lenguajes de programación para la presente dé­ cada. Así, utilizar objetos como unidades de paralelismo ha recibido un nivel ele­ vado de atención. Los objetos pueden considerarse como "máquinas abstractas independientes que interactúan en respuesta a mensajes" [Caromel, 1989]. Pero la mayoría de los intentos para implementar objetos de ejecución concurrente no in­ Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

267

cluye paso de mensajes asincrónico temporizado en forma aleatoria. Las llamadas de procedimiento remoto, en las cuales un objeto llama a un procedimiento en otro objeto y espera una respuesta, es el método preferido. Esto ocurre así en lenguajes como Concurrent Smalltalk, ABCL/1 y Orient85. Emerald, desarrollado en la Uni­ versidad de Washington, también soporta procesos concurrentes. No está orienta­ do a objetos, sino basado en objetos, ya que la herencia no está soportada. Caromel [Caromel, 1989,1993] propone un modelo para lenguajes orientados a objetos en paralelo que fomenta los puntos fuertes de los objetos: reutilización, extensibilidad y programas altamente legibles. En Nancy, Francia, la experimenta­ ción con este modelo recibe un seguimiento bajo el uso del lenguaje de objetos Eiffel. Esta implementación usa un objeto llamado PROCESS, con dos métodos, Cr eat e y Live. C r e a t e permite la creación de una instancia de un objeto PROCESS, el cual procede a ejecutar su rutina Li ve. Cuando esta rutina se completa, el PROCESS mue­ re. Eiffel soporta herencia múltiple, en el que un solo objeto puede heredar de más de una superclase. De este modo, cualquier objeto apropiado podría llegar a ser un P ROC ESS al heredar los métodos de PROC ESS. Otra extensión similar, pero algo diferente, de Eiffel [Karaorman, 1993] permi­ te que cualquier objeto herede de la clase CONCURRENCY, la que hereda de ICP (Interprocess Comm unication Primitives), las Primitivas de Comunicación Interprocesos del sistema UNIX. CONCURRENCY requiere que cualquier clase que he­ rede de ella proporcione su propio planificador. El enfoque de la concurrencia orientada a objetos es modelar situaciones en las cuales los objetos actúan juntos, en vez de acelerar la ejecución del programa. Ésta es una nueva y creciente área de investigación, incluyendo los esfuerzos para com­ binar Concurrent C con su primo orientado a objetos C++ como una extensión simple para C.

5.6 ADMINISTRACIÓN DE FALLAS PARCIALES Cuando se pasan los mensajes, varios errores pueden ocurrir: 1. 2. 3.

El mensaje puede ser perdido por la red. La respuesta puede perderse. El servidor puede fallar antes de enviar la respuesta.

Una manera para manejar estas situaciones es que el cliente declare una pausa por un periodo especificado. Si la respuesta deseada no se recibe cuando el tiempo expirarse supone que ha ocurrido una de las tres contingencias y se toma acciones para remediarlo. Esto podría ser tan simple como volver a enviar la petición. Sin embargo, en una situación como la del segundo caso que se enumeró, esto puede no ser lo correcto. A menudo los procesos exhiben persistencia, con lo cual los valo­ res de estado se mantienen de invocación a invocación. Por ejemplo, suponga que el servidor incrementó un contador N y lo recuerda. Al volver a enviar una petición resultaría que N se incrementaría doblemente, aun cuando sólo una respuesta sea devuelta. Una posible solución a este problema es hacer atómico al servidor; es Sólo fines educativos - FreeLibros

268

PARTE n: Lenguajes imperativos

decir, del tipo todo o nada. De este modo, responderá en forma exitosa, o el estado anterior se restaurará. Las tareas de Ada son casi sin restricción, lo que puede conducir a errores de secuencia así como de bloqueo. Un error de secuencia ocurre cuando las tareas se comunican en un orden no anticipado. Una tarea está bloqueada cuando no puede proceder más. Un caso posible es el bloqueo o estancamiento circular (circular deadlock), como se muestra en la figura 5.6.1, en la cual cada tarea ha llamado a la siguiente en el círculo, la que no puede aceptarla hasta que llegue al punto de reunión (rendezvous) con la de adelante. Las tareas en ejecución concurrente son en particular difíciles de depurar debi­ do a que pueden ejecutarse de manera diferente cada vez que son invocadas. Las herramientas de depuración que se hallan en desarrollo se basan en monitores o durante tiempo de ejecución, en el cual una secuencia de "instantáneas" (snapshots) del programa son tomadas para resaltar los estados de los procesos en ejecución seleccionados cuando uno llega a bloquearse. Debido a las inconsistencias que se presentan de una ejecución a la siguiente, un sistema axiomático de demostración o prueba es en particular apropiado para validar los procesos concurrentes.

5.7

RESUMEN La programación distribuida incluye varios modelos. Todos involucran el uso de procesadores múltiples, cooperación entre los procesadores y manejo de las fallas de uno o más procesos en ejecución concurrente mientras otros continúan. Existe cuatro modelos principales: 1. 2. 3. 4.

Los que se basan en memoria compartida Los basados en paso de mensajes asincrónico (sin bloqueo) Los que se basan en paso de mensajes sincrónico (bloqueo) Una combinación de paso de mensajes y memoria compartida

FIGURA 5.6.1

Estancamiento o bloqueo circular Sólo fines educativos - FreeLibros

CAPÍTULO

5: Construcciones de lenguajes para procesamiento en paralelo

269

Ada implementa el cuarto modelo; mientras que Concurrent C, el tercero. No im­ porta cuál modelo se elija, los procesos en operación deben estar sincronizados si la información va a ser intercambiada. Los primeros mecanismos de sincronización fueron el semáforo y el monitor. Un semáforo controla el acceso a una sección críti­ ca de código. Un proceso que comienza a ejecutar este código crítico no puede interrumpirse hasta que haya completado la ejecución de este código. Los monitores contienen variables compartidas y cualquier operación permitida en ellas. Se co­ munican con otros monitores y también pueden ser accesados por procesos coope­ rativos. El punto de reunión o rendezvous es un tercer mecanismo para sincronizar procesos. Está basado en el modelo cliente/servidor, con el que un cliente solicita servicio y entonces espera hasta que se le proporciona. Occam también implementa el punto de reunión, pero aquí se le conoce como paso de mensajes. Las unidades de procesamiento de Ada, llamadas tareas, pueden ser creadas y destruidas en tiempo de ejecución; las de Occam, conocidas como procesos, son estáticas, como lo son sus canales de comunicación. Diversos lenguajes han sido desarrollados para experimentar con la concurren­ cia sobre un solo procesador. En ocasiones esto se denomina multiprogramación. Los procesos se piensan como de operación concurrente, pero en realidad se implementan a través de un intercambio de entrada y salida en un solo CPU. ALGOL 68, C, Modula-2, Pascal S y Concurrent Pascal son ejemplos de tales lenguajes. Un quinto modelo para la concurrencia es el espacio de tupias de Linda. Aquí, los procesos y sus variables asociadas se heredan y destruyen como tupias en una memoria asociativa con, pero separada de, la RAM de cada proceso. Los creadores de Linda hallaron este modelo más fácil de imaginar que el paso de mensajes y las memorias compartidas. Linda se implemento como un preprocesador para C o para Ada. No todos los teóricos coinciden en que los lenguajes orientados hacia procedi­ mientos proporcionen el mejor vehículo para implementar la concurrencia. El cue­ llo de botella de VonNeumann, en el que sólo una palabra es transferida dentro o fuera de la memoria en cada ciclo de procesador, aún causa problemas. En el capí­ tulo 8 examinaremos las nociones para el procesamiento en paralelo con el uso de funciones, después que el lector esté más familiarizado con los lenguajes funciona­ les por sí mismos. Las cláusulas de procesamiento lógico en paralelo serán consi­ deradas en el capítulo 7.

5.8 NOTAS SOBRE LAS REFERENCIAS [Wegner, 1983] proporciona una buen análisis de las diferencias entre monitores y el concepto de rendezvous (punto de reunión). Él usa Ada y CSP como ejemplos de la implementación de lenguajes de punto de reunión. Implementa un monitor en Ada, usando la definición de monitor de Brinch Hansen [Brinch Hansen, 1978]. [Bames, 1994,1996] es una buena introducción en la literatura en Ada y Ada95, escritos por John Bames, uno de los miembros clave en el equipo de diseño de Ada. Su principal línea de trabajo incluye las pruebas.

Sólo fines educativos - FreeLibros

270

PARTE

n: Lenguajes imperativos

Una buena representación de Concurrent C, con muchos programas ejemplo elementales, es [Gehani, 1986]. Describe una versión implementada en plataforma UNIX con un solo procesador. Por el tiempo en que se escribió este artículo, una versión para sistemas distribuidos se estuvo desarrollando en los laboratorios Bell. El apéndice A de [Ben-Ari, 1982] contiene un "paquete de implementación" para Pascal S. El paquete proporciona todos los códigos del programa Pascal nece­ sarios para implementar un interpretador de Pascal S, corriendo sobre un progra­ ma Pascal. La segunda edición de este libro [Ben-Ari, 1990], que se divide en tres partes, no hace referencia al Pascal S. La parte I considera sistemas de memoria compartida, semáforos y monitores. La parte II tiene que ver con paso de mensajes y vistas en los lenguajes Ada, Occam y Linda. La parte III analiza la implementación de estos temas, dando especial atención a sistemas de tiempo real, donde el tiempo de respuesta puede ser un tema. [Kerridge, 1987] es un buen tutorial sobre Occam que proporciona cierta canti­ dad de programas elementales así como también un análisis de cómo trabaja Occam con transputers. Para un breve análisis de Linda, se sugiere al lector consulte a [Leler, 1990], [Markoff, 1992] o [Carriero, 1989], El segundo artículo compara Linda con paso de mensajes, orientación a objetos, lógicos y modelos funcionales para programación concurrente. Sobresalen cuatro buenos tutoriales sobre la programación en paralelo, tres son de ACM y el cuarto de la IEEE. Éstos son [Brinch Hansen, 1978], [Andrews, 1983], [Bal, 1989] y [Shatz, 1989]. Se incluye en Shatz un buen análisis de las prue­ bas de depuración en Ada. La revista Communications ofthe ACM, de septiembre de 1993, se destinó a la programación orientada a objetos concurrentes. Se incluye [Caromel, 1993] y [Karaorman, 1993], así como artículos sobre sistemas operativos que soportan la concurrencia y a un sucesor concurrente de Trellis/Owl llamado DOWL.

Sólo fines educativos - FreeLibros

PARTE III

Autómatas y lenguajes formales

Esta sección contiene un capítulo en el que comenzamos a examinar los lenguajes de una manera más teórica. También incluimos temas acerca de aplicaciones prác­ ticas de esos conceptos teóricos para lenguajes de programación y diseño de compiladores. La jerarquía de Chomsky de los tipos de lenguajes formales proporciona la estructura para el material, desde el Tipo 3 (el más restrictivo) hasta el Tipo 0, que incluye todos los otros tipos. Examinaremos en particular el Tipo 3, gramáticas regulares, debido a su uso común en los compiladores para reconocer los compo­ nentes léxicos o tokens (elementos de programa) de un lenguaje. El Tipo 2, gramáti­ cas libres de contexto, se emplea con frecuencia para describir cómo dichos tokens se combinan para formar construcciones válidas de lenguaje. Las técnicas utiliza­ das ofrecen cierta visión acerca del análisis sintáctico o gramatical (parsing) de un programa y el entendimiento de su semántica (significado).

Sólo fines educativos - FreeLibros

CAPÍTULO 6 LENGUAJES FORMALES 6.0 En este capítulo 6.1 Lenguajes formales Definición de lenguajes formales La jerarquía de Chomsky de los lenguajes formales Viñeta histórica: Clasificaciones de los lenguajes: Noam Chomsky Tipo 3: Gramáticas regulares Tipo 2: Gramáticas libres de contexto Tipo 1: Gramáticas sensibles al contexto Autómatas lineales limitados (LBA) Viñeta histórica: Alan Turing: Lo que las máquinas no pueden hacer Tipo 0: Gramáticas no restringidas Ejercicios 6.1 6.2 Gramáticas regulares Expresiones regulares

273 274 275 277 278 280 281 282 283

286 288 289 290 291

Autómatas finitos (FA, NFA y DFA) Aplicaciones Ejercicios 6.2 6.3 Gramáticas libres de contexto (CFG) Autómatas descendentes (PDA) Árboles de análisis sintáctico Gramáticas ambiguas Aplicaciones Formas normales Forma normal de Chomsky (CNF) Forma normal de Backus (BNF) Diagramas de sintaxis Ejercicios 6.3 6.4 Gramáticas para los lenguajes naturales Ejercicios 6.4 6.5 Resumen 6.6 Notas sobre las referencias

Sólo fines educativos - FreeLibros

292 295 - 296 298 299 303 304 306 306 306 307 309 310 311 313 313 314

CAPÍTULO

6

Lenguajes formales

E l capítulo 2 trata con la abstracción, la destilación de una construcción de lengua­ je a su forma esencial desmenuzada. Como usted podrá recordar, examinamos tres aspectos de la abstracción: datos, control y modularización. Aquí no considerare­ mos las construcciones del lenguaje, sino la sintaxis, la forma escrita que un len­ guaje puede adquirir. Mencionaremos sólo de manera breve la semántica, el significado destinado de la sintaxis, y se dejará un tratamiento más completo para un curso posterior. El propósito de un lenguaje es la comunicación, ya sea con otras personas, con una computadora o con alguna otra entidad. Nuestra comunicación debe ser com­ prendida por la parte que la recibe, así como también por el escritor. Los lenguajes de computadora no son la excepción a este requerimiento. Las personas así como la computadora deben comprender el lenguaje en el que escribe el programador. Puesto que se encuentra involucrada una máquina, los lenguajes de programación deben ser muy precisos. Es necesario ajustarlos a reglas fijas. Aquí estudiaremos las reglas y símbolos de los lenguajes formales convenientes para la comunicación con las computadoras. Este capítulo se dirige a aquellos estudiantes que no planeen incluir un curso completo en diseño de compiladores o teoría de la computación en sus cursos de estudios universitarios. En vez de dirigimos al asunto de cuál lenguaje de progra­ mación es mejor para realizar una tarea en particular, trataremos con las caracterís­ ticas que debe tener cualquier lenguaje de computadora para ser reconocido por una computadora, el modo en que se construyen los lenguajes formales y las má­ quinas para reconocerlos.

6.0

EN ESTE CAPÍTULO Comenzaremos por examinar las maneras formales de especificar una gramáti­ ca. Existe cuatro tipos reconocidos de lenguajes formales (más algunos subtipos),

Sólo fines educativos - FreeLibros

274

PARTE

ni: Autómatas y lenguajes formales

cada uno de los cuales puede lograr diferentes cosas y son útiles para diversas tareas. Siguiendo la jerarquía de Chomsky, comenzamos por el tipo más restrictivo, el Tipo 3, después seguiremos por los Tipos 2 y 1, y por último con el Tipo 0, que incluye a todos los demás. Describiremos en forma tan sucinta como sea posible tres aspectos de cada tipo: • • •

Qué es lo que distingue a un tipo de otro de mayor jerarquía. La forma de las reglas gramaticales para los lenguajes del tipo. La máquina teórica que reconoce las palabras válidas del tipo.

Aún más, examinaremos de cerca el Tipo 3, gramáticas regulares y las expre­ siones regulares relacionadas, puesto que son de uso práctico en la definición de los tokens de un lenguaje. Después consideraremos el Tipo 2, las gramáticas libres de contexto y cómo se les utiliza en el reconocimiento de frases legales en la gramá­ tica. Por último, examinaremos brevemente las gramáticas para los lenguajes natu­ rales.

6.1

LENGUAJES FORMALES La estructura lexicográfica de ion lenguaje es la forma de sus tokens? La sintaxis des­ cribe las declaraciones que serán aceptadas como correctas por un compilador o intérprete para el lenguaje. La sintaxis, o precisamente lo que constituye una decla­ ración válida, está definida por una gramática que genera un lenguaje formal. Un lenguaje form al es el conjunto de declaraciones que en un contexto sintáctico son correctas. Una gramática implica una lista finita de símbolos, llamada un alfabeto, una lista finita de reglas para formar palabras con el alfabeto y quizás otro conjunto de reglas para formar declaraciones a partir de las palabras. Como un ejemplo, consi­ dere el minilenguaje que llamaremos Playa-Alegre. Alfabeto:

Regla:

Sustantivos: {Juan, María} Verbos: (nada, navega) Alfabeto = Sustantivos u Verbos u {"", .}2 S es una declaración de Playa-Alegre si S es de la forma N V., en el cual N £ Sustantivos, seguido por un espacio, seguido por V é Verbos, seguido por un punto.

El lenguaje Playa-Alegre es entonces las cuatro declaraciones {Juan nada., María nada., Juan navega., María navega.} Una computadora, aunque en forma esencial está compuesta de cadenas binarias y operaciones ligadas a éstas, también puede pensarse como una máquina 1Un token es una cadena válida en un lenguaje, tal como un identificador, un indicador de asigna­ ción (:=), una palabra reservada (por ejemplo, If), un comparador (por ejemplo, <), etcétera. 2 ' ' indica un espacio. Los signos de comillas no son elementos del alfabeto.

Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

275

virtual que se comporta como si se hubiera diseñado para reconocer exactamente un lenguaje y realizar las instrucciones codificadas en ese lenguaje. Por ejemplo, un lenguaje estructurado en bloques está implementado como una pila simple, mien­ tras que los lenguajes imperativos concurrentes requieren de varias pilas con colas asociadas. La máquina virtual para el lenguaje estructurado en bloques se compor­ taría como una pila, mientras que para los lenguajes imperativos concurrentes fun­ cionaría como pilas cooperativas con colas asociadas. En este capítulo, examinaremos lo básico de estas máquinas virtuales y comentaremos acerca de sus vínculos esen­ ciales para los tipos de lenguajes. Cuando hablamos de un lenguaje formal, nos referimos a la forma o sintaxis de las palabras válidas en el lenguaje. No nos interesa lo que eso significa; es decir, su semántica. Si "trabajo" y "casa" son palabras válidas, y una de las reglas de forma­ ción de palabras dice que la concatenación de dos palabras es también una palabra, entonces "trabajocasa", "casatrabajo", "casacasa" y "trabajotrabajo" son todas pala­ bras válidas. En español, las dos primeras tienen algún sentido, aunque su semánti­ ca sea diferente, pero las dos últimas no tienen sentido. ¡No importa! En un lenguaje formal con una regla de concatenación, la totalidad de las cuatro son válidas por igual. Los números no son tan ambiguos como las palabras en español. Si 3 y 17 son palabras en un lenguaje con una regla de concatenación, entonces 317,173,33 y 1717 también lo son. Las reglas formales para la generación y reconocimiento de lenguajes de com­ putadora son más sintácticas que semánticas, con la semántica descrita en algún lenguaje natural, en nuestro caso, el español. La sintaxis para la cadena 5 + 3 * 5 es el símbolo 5 seguido por un espacio, luego los símbolos +, espacio, 3, espacio, *, espacio y 5. Su semántica captura algunas reglas comunes de la aritmética de los números naturales. La semántica de cadenas válidas no será de interés aquí, aun­ que mucho de la teoría de los lenguajes trata de la noción de que la sintaxis apro­ piada captura los significados intuitivos, y que las declaraciones que son inexpresivas pueden bien no tener sentido. Los lenguajes formales son útiles al elaborar descripciones estándar, analizar si los lenguajes son correctos y construir generadores de análisis sintáctico o gramati­ cal como parte de un compilador. Como su primera tarea, un compilador debe analizar el código fuente descomponiéndolo en partes más finas; es decir, las decla­ raciones en expresiones, las expresiones en palabras, las palabras en tokens. Si el lenguaje sigue reglas formales, sin ambigüedades, esto puede ser automatizado sin dificultad. Se aplica diferentes niveles de análisis, desde el reconocimiento de erro­ res de sintaxis hasta la determinación de cuáles programas generan ciclos iterativos infinitos y cuáles terminan siempre con soluciones correctas al problema práctico. Los lenguajes formales y las máquinas teóricas que los reconocen son útiles, desde el reconocimiento de elementos del lenguaje hasta los niveles superiores para pro­ bar que un programa es correcto. D efinición de lenguajes formales Para definir un lenguaje formal L, necesitaremos dos cosas: 1.

Un alfabeto X de símbolos individuales. Sólo fines educativos - FreeLibros

276 2.

PARTE III:

Autómatas y lenguajes formales

Un conjunto de reglas para determinar cuáles cadenas o palabras3 de £ son válidas en L. Denotaremos el conjunto de reglas mediante la letra P, y cada una será dada en la forma a —» /?, llamada una producción.

En conjunto, el alfabeto y las reglas para formar palabras válidas son llamados una gramática sobre £. Por consecuencia, una gramática puede ser considerada como un par o 2-tupla (£, P). Si G es una gramática (£, P), generando un lenguaje L, L se escribe L(G). Por ejemplo, el lenguaje L(B), generado por la gramática B = (£, P) en el listado (6.1.1), es de todos los posibles decimales binarios no negativos menores que 1, correctos o aproximados a dos posiciones. L(B) = {0.00, 0.01, 0.10, 0.11), donde

£ = {0,., P = {Rl: R2: R3: R4: R5: R6:

1} S0

-> S2 -> S2 -> S3 -> S3 -»

(6 .1.1) 0ST .S2 0S3 1S3 0 1

Las reglas escritas en la forma a -» p se llaman producciones, debido a que se "produce" una nueva cadena a partir de una antigua mediante el reemplazo de la subcadena a la derecha por la correspondiente a la izquierda. Por ejemplo, la cade­ na 0.11 puede ser producida a partir de 0.1S3, al reemplazar la subcadena S3 por 1, utilizando la regla de producción R6. Cuando se genera una palabra para su inclusión en L(B), siempre comenzamos en S , el símbolo de inicio. La cadena inicial generada cuando se produce una palabra es el símbolo simple S0. Este último símbolo no se halla en el alfabeto de £ de símbo­ los terminales, y se le llama un no terminal. Tendrá que ser eliminado mediante algu­ na regla si va a producirse una cadena válida de L(B). En la gramática B del listado (6.1.1), el conjunto de no terminales es N = {SQ, S , S , S3). La aplicación de la regla R l involucra el reemplazo de SQcon OS . La aplicación de otras reglas implica reemplazo, en la cadena generada hasta ahora, de una ocu­ rrencia de alguna de las no terminales S por x, si S —» x es una regla. Si tanto Si x como S y son ambas reglas, cualquiera puede aplicarse. Por ejemplo, S3 puede reemplazarse ya sea con un 0 o un 1. Una derivación de la cadena 0.01 que represen­ te el decimal binario para V4 es: Rl R2 R3 R6 S0-> 0S1-> 0.S2-> 0.0S3-> 0.01 Un sistema de producción es una gramática G, en la cual las reglas están dadas en la forma de producciones. G = (£, N, P, Inicio) es una 4-tupla que incluye una serie de símbolos terminal, £; un conjunto de no terminales, N; un conjunto de reglas 3 Una palabra en un lenguaje L(G) incluye sólo símbolos de X, entre ellos la palabra vacía e. Este último no es un símbolo terminal ni uno no terminal. Una cadena puede incluir cualquier símbolo.

Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

277

de producción, P; y un símbolo de inicio, Inicio e S U N. En la gramática anterior, E = {0,1,.}, N = {S0, Sj, S2, S3) e Inicio = SQ.G es denominada una gramática estructurada en frases si toda producción resulta ser de la forma s. —> s., en la que s. y s son cadenas de (E U N) y s. contiene al menos una no terminal. (La mayoría de los autores emplea letras mayúsculas para no terminales y letras minúsculas o dígitos para terminales, pero esto es meramente una convención.) Usted se sorprendería al descubrir que existe sólo cuatro tipos de lenguajes formales, según la clase de reglas de producción que se utilice. Esta comunicación a escala fundamental no es tan variada como se podría pensar y ha atraído la aten­ ción de investigadores en diversos campos. Los lenguajes de cada forma tienen propiedades definitivas. De hecho, la relación entre lenguaje y máquina teórica es de uno a uno. Un lenguaje de un tipo particular se reconoce mediante una máquina específica. Lo opuesto se mantiene también; pero más en esto último. Primero ne­ cesitamos ver qué clases de lenguajes formales existe.

L a jerarq u ía de C hom sky de los lenguajes form ales

Muchos investigadores han trabajado en las formas de las reglas de decidibilidad4 para generar lenguajes de tipos particulares; esto plantea varias interrogantes. ¿Cuá­ les restricciones son justamente esenciales y cuáles pueden despreciarse? Con una clase dada de reglas, ¿qué tipo de problemas puede resolverse? Dado un lenguaje, ¿qué máquinas pueden reconocer en forma apropiada las cadenas formadas y re­ chazar aquellas que sean inválidas? Por otra parte, ¿qué es una máquina? ¿Para qué tipo de lenguajes una máquina siempre tendrá éxito al reconocer cadenas váli­ das? ¿Puede una máquina reconocer cadenas potencialmente infinitas?5 ¿Para qué aplicaciones son más apropiados ciertos lenguajes? Aunque los investigadores trabajaban en forma independiente sobre la teoría de los lenguajes formales, sus diversas formulaciones cayeron dentro de las mis­ mas cuatro distintas clases, comenzando con conjuntos de reglas bastante irrestrictas a través de las cuales se volvían cada vez más rígidas. Sólo más tarde se reconoció que cada una de estas formulaciones era equivalente a las mismas cuatro clases de lenguajes. Las máquinas que reconocían los lenguajes formados mediante reglas irrestrictas pueden generar soluciones para una variedad de problemas, pero nin­ guna máquina puede decidir si puede o no resolver cualquier problema arbitrario. Las máquinas que reconocen los lenguajes basados en clases de reglas más estrictas pueden garantizar la generación de soluciones, pero para una clase limitada de problemas. Examinaremos los tipos de lenguajes formales descritos por el lingüista Noam Chomsky. Como se muestra en la figura 6.1.1, estos lenguajes forman una jerarquía en la que cualquier lenguaje de Tipo 3 es también de Tipo 2, cualquier lenguaje de

4Una regla es de decidibilidad si existe un procedimiento efectivo de decisión para generar un sí o un no en un número finito de pasos. 5Una cadena es potencialmente infinita si no conocemos su longitud; es decir, una cadena de la forma an es infinita en forma potencial si no conocemos el valor de n. Para cualquier valor fijo de n, podemos generar una cadena que sea más larga.

Sólo fines educativos - FreeLibros

278

PARTE III: Autómatas y lenguajes formales

Tipo 0: Lenguajes recursivamente enumerables reconocidos por las máquinas de Turing

Tipo 1: Lenguajes sensibles al contexto reconocidos por autómatas lineales limitados Tipo 2: Lenguajes libres de contexto reconocidos por autómatas descendentes Tipo 3: Lenguajes regulares reconocidos por autómatas finitos

F I G U R A 6.1.1

La jerarquía de Chomsky

Tipo 2 es asimismo de Tipo 1, y aquellos que sean de Tipo 1 también lo son del Tipo 0. Este último es el más general, incluye a todos los otros tres tipos. Como veremos, cada tipo de lenguaje está asociado con una máquina de cómputo particular. Las gramáticas estructuradas por frases están asignadas a la jerarquía de Chomsky bajo los fundamentos de las formas de las producciones. Pero primero veamos quién es Noam Chomsky.

VIÑETA HISTÓRICA Clasificaciones de los lenguajes: Noam Chomsky Todos hemos oído hablar de las lenguas romances, pero rara vez del individuo que ha tenido toda una vida de romance con las lenguas o lenguajes, Noam Chomsky es ese individuo. Su profundo interés en el estudio de la lingüística comenzó cuando tenía sólo 10 años de edad. Estaba muy interesado con las demostraciones de una gramática del siglo Xin que leía en escritos de su padre. Ésta se encontraba escrita de un modo informal y no conforme a la escuela estructural tradicional de la lingüística. La introducción informal de Chomsky al estudio de los lenguajes matizó su futura labor en ese campo. Uno no puede más que intrigarse al pensar si él hubiese llega­ do a ser el revolucionario lingüista que ha sido si su primera aproximación al cam­ po hubiese sido más tradicional. En 1945 Chomsky ingresó a la Universidad de Pennsylvania donde se graduó en lingüística. Aquí es donde su interés de toda la vida en cambio político comenzó a surgir. Estaba atento en particular a los sucesos que condujeron al establecimiento del estado de Israel. Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

279

En 1951 recibió el grado de M.A. de la Universidad de Pennsylvania con una tesis llamada "Morfemas del Hebreo Moderno", basada en los esfuerzos para de­ sarrollar un sistema de reglas que podría utilizarse para caracterizar toda estructu­ ra de frases en un lenguaje* Recibió un Ph. D. en lingüística en 1955. Al principio Chomsky pasó por una época muy difícil para publicar alguno de sus trabajos, los cuales resultaron muy revolucionarios para la comunidad lingüís­ tica establecida. No sólo exponía lo inadecuado de las gramáticas estructuralistas, también criticaba la práctica lingüística más moderna. La escuela estructuralista planteaba que el lenguaje es principalm ente función del com portam iento conductista, según la respuesta del individuo a su medio ambiente externo. Chomsky sentía que la explicación estructural no tomaba en cuenta la creatividad lingüística en los humanos. Acerca del conductismo decía que, "el entrenamiento tipo skinneriano* es apropiado sólo para los trabajadores del área industrial que necesitan desarrollar complejas habilidades técnicas. ¿Es el crecimiento y aprendi­ zaje nada más que la forma de los comportamientos? Si eso es todo lo que la educa­ ción significa, figuras autoritarias formando personas, entonces tal vez no necesitemos de ella" [Newsweek, agosto 26 de 1968]. Creía también que la lingüísti­ ca moderna "no ha reconocido explícitamente la necesidad de complementar una 'gramática particular7de un lenguaje con una gramática universal si es para conse­ guir una adecuación descriptiva. De hecho, ha rechazado en forma característica el estudio de la gramática universal por estar desencaminada, y... no ha intentado tratar con el aspecto creativo del uso del lenguaje. De este modo, no sugiere ningu­ na manera de superar la insuficiencia descriptiva fundamental de las gramáticas estructuralistas" [Chomsky, 1965]. Para apoyar sus teorías, Chomsky confiaba fuertemente en las matemáticas, y publicó su primer libro en 1957. En ese tiempo él era profesor de lingüística en el MIT. En Cartesian Linguistics [Chomsky, 1966], dividió el estudio de la lingüística en tres categorías principales: 1.

2.

3.

Investigaciones que se enfocan en forma directa a la naturaleza del lenguaje, incluyendo descripciones de sintaxis, semántica, fonología (el estudio de los sonidos) y sus evoluciones Estudios acerca del uso del lenguaje y las habilidades y organización mental que esto presupone, tal como los procesos de aprendizaje del lenguaje en niños y adultos, y el lenguaje como se le emplea en la literatura. Estudios sociológicos de fondo con el establecimiento de los diversos enfoques para el estudio del lenguaje en configuraciones intelectual e histórica apro­ piadas.

Aunque se le considera un genio en el campo de la lingüística, Chomsky nunca minimizó sus dificultades. En una ocasión afirmó: "Puede estar más allá de los límites de la inteligencia humana comprender cómo trabaja la inteligencia huma­ na" [Time, febrero 16 de 1968]. Los intereses políticos de Chomsky resurgieron alrededor de 1965 con sus pro­ testas por la guerra de Vietnam. Llegó a ser un líder en organizaciones de paz como

N. del T.: De B. F. Skinner, psicólogo estadounidense, padre del conductismo.

Sólo fines educativos - FreeLibros

280

PARTE III:

Autómatas y lenguajes formales

Resist, un movimiento nacional de resistencia a la conscripción. Dio cursos univer­ sitarios sobre cambio político y publicó muchos escritos de sus puntos de vista pacifistas. Una vez más, retó en forma franca a las autoridades. Israel Shenker es­ cribió en el New York Times (octubre 27 de 1968): "En la segunda década de su vida, Noam Chomsky revolucionó la lingüística. Después de sus treinta años, ha estado intentando revolucionar la sociedad." Sus escritos continúan hasta la actualidad. The Culture ofTerrorism [Chomsky, 1988], evidencia las políticas de Estados Unidos en áreas como Centroamérica e Irán; establece que "incluso en una sociedad am­ pliamente despolitizada como la de Estados Unidos, sin partidos políticos o prensa de oposición más allá del reducido espectro del consenso dominado por los intere­ ses mercantiles, es posible que la acción popular tenga un impacto significativo en la política, aunque sea indirectamente. Esa fue una lección importante para la gue­ rra de Indochina. Es de resaltar, una vez más, por la experiencia de los años ochen­ ta en lo que se refiere a Centroamérica. Y debería recordarse para el futuro" [Chomsky, 1988]. Su influencia hasta la actualidad continúa en otros campos además de la lin­ güística y el activismo político. Su legado es muy sobresaliente en la ciencia de la computación. Su desarrollo de una teoría matemática de los lenguajes naturales y la descripción de cuatro diferentes clases de lenguajes ha hecho posible el análisis de la sintaxis y la gramática de los lenguajes de programación. "Esto ha tenido beneficios prácticos importantes puesto que permitieron el desarrollo de genera­ dores de analizadores automáticos, automatizando de esta manera lo que había sido una de las partes más difíciles de la escritura de compiladores" [MacLennan, 1987].

Tipo 3: Gram áticas regulares Una gramática estructurada en frases G = (X, N, P, Inicio) es una gramática regular si sus producciones son de la forma: A —» a, o A —> aB, en el que A,B G N, y a G E.

(6.1.2)

Es decir, el primer símbolo en cada lado derecho debe ser una terminal y puede seguirle una no terminal. Considere, por ejemplo, las siguientes reglas para la creación de un identificador Pascal I: I —» a I ... I z I aL I ... I zL 1aD 1... I zD L —» aL I... I zL I aD I... I zD I a I... I z D —» 0 L I ... 19L10DI... 19D10 1... 19

(6.1.3)

Aquí I significa (OR); esto permite una abreviación para varias reglas. Entonces I es ya sea una letra o bien una letra seguida por una secuencia finita de caracteres alfanuméricos (letras y/o dígitos). Tales gramáticas pueden ser reconocidas por autómatas finitos (FA), que en ocasiones se conoce como autómatas de estado finito (FSA). Si se comienza en el

Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

281

estado inicial S0, a medida que cada nuevo símbolo es leído, hay una transición a otro (o quizás el mismo) estado. Dentro de un número finito de pasos debe deter­ minarse si una cadena es válida o inválida. Las gramáticas regulares son utilizadas a menudo en la fase de análisis lexicográfico de un compilador, que en ocasiones se conoce como el rastreador (scanner), en la cual los tokens válidos de un lenguaje son aceptados. Debido a esta im­ portante aplicación, examinaremos más de cerca las gramáticas regulares y los autómatas finitos en la sección 6.2.

Tipo 2: Gram áticas libres de contexto El tipo siguiente, que es menos restrictivo que el Tipo 3, es el Tipo 2. Los lenguajes Tipo 2, que se conocen también como libres de contexto, son en especial importan­ tes en la ciencia de la computación debido a que todas, excepto algunas, de las características de los lenguajes de programación de alto nivel pueden ser escritas haciendo uso de ellas. Como antes, vamos a caracterizar estos lenguajes al descri­ bir las gramáticas libres de contexto (CFG; Context-Free Grammars) que generan cadenas válidas, y las máquinas teóricas que las reconocen. Una gramática estructurada por frases, G = (X, N, P, Inicio), es libre de contexto si las producciones son de la forma: A —» s, en el cual A G N, el conjunto de no terminales, y s es cualquier cadena de X U N

(6.1.4)

Las gramáticas regulares son, por supuesto, libres de contexto ya que las cade­ nas de la forma 'a' o 'aB' son candidatos para s del lado derecho. Las gramáticas libres de contexto pueden escribirse con el uso de producciones de formas diferen­ tes al ejemplo (6.1.4), pero una gramática así siempre puede mostrarse como equi­ valente a una del tipo citado. Las CFG son llamadas libres de contexto porque pueden hacerse reemplazos dondequiera que se presenten, y no en el contexto de otros símbolos circundantes. Por ejemplo, una regla libre de contexto permitiría el reemplazo del artículo "el" con "este" en una frase en español; es decir, "El perro ladraba" "Este perro la­ draba". Una regla sensible al contexto sería reemplazar (en idioma inglés) "the" con "an" si la palabra siguiente comienza con una vocal, y de otro modo, reempla­ zarlo con "a". "The dog barked" —» "A dog barked" (bark: ladrar), mientras que "The otter barked" —>"An otter barked" (otter: nutria). Aquí el contexto del reem­ plazo es la palabra que sigue al artículo que será reemplazado. Como las gramáticas regulares son reconocidas por FA, un CFG puede ser re­ conocido por un autómata descendente (PDA; Push-Down Automaton). Como su nombre implica, aparte de la cadena de entrada, puede utilizarse una pila para un PDA. Éstos se usan con frecuencia en el analizador de un compilador, el cual toma tokens de la gramática como entrada y reconoce si el programa se encuentra en sintaxis apropiada. En este caso, cuando la forma del lado derecho de una regla está sobre la pila, podemos extraer esas entradas, después introducir la no terminal resultante del lado izquierdo sobre la pila. Examinaremos más de cerca las CFG, PDA y el análisis en la sección 6.3. Sólo fines educativos - FreeLibros

282

PARTE III:

Autómatas y lenguajes formales

Tipo 1: Gram áticas sensibles al contexto Existe lenguajes que no son libres de contexto. Una de las palabras más simples que no puede ser generada por una CFG es anbncn, para una n arbitraria pero fija. La demostración se encuentra más allá del alcance de esta breve introducción, pero puede hallarse en [Cohén, 1991]. Las producciones a —>/?para las gramáticas sen­ sibles al contexto (CSG; Context-Sensitive Grammars) son como las de los lengua­ jes libres de contexto, con las siguientes excepciones: 1. 2.

El lado izquierdo a puede contener más de un símbolo mientras que al menos uno sea un no terminal. La longitud de a es menor que o igual a la longitud de /3.

La segunda regla asegura que no existe producciones vacías, aquellas en las que el lado derecho es la cadena vacía e. El lector puede consultar lo referente a las reglas de borrado en el ejercicio 6.1.3. Esta última restricción evita los finales muer­ tos (dead end), en los cuales lo que se reemplaza, a, puede llegar a ser más extenso que la palabra generada hasta ahora. Una gramática CSG para palabras de la forma anbncn es: 1. 2. 3. 4. 5. 6. 7.

S - » aSBC S -> aBC CB- >BC aB ab bB -» b b bC —>be cC cc

y la producción de a3b3c3 es: 1 1 2 3 3 3 S —^aSBC —>aaSBCBC —) aaaBCBCBC —) aaaBBCCBC —) . . . —^ 4 5 aaaBBBCCC —» aaabBBCCC

5

6 7 aaabbbCCC —» aaabbbcCC —> . . .

7 ^aaabbbccc La única diferencia entre estas reglas de producción y las correspondientes a una CFG es la presencia de dos símbolos en los lados izquierdos de las reglas 3-7 anteriores. Éstos proporcionan los contextos. B puede cambiarse a b cuando esté precedida por una a (regla 4) o cuando lo esté por una b (regla 5). Las gramáticas de Tipo 1 que se utilizan para el procesamiento del lenguaje natural se conocen a veces como gramáticas estructuradas en frases restringidas. Una regla típica estructurada en frase es S: —>NP VP, en la que S denota una oración, NP una frase sustantiva y VP una frase verbal. Se incluye otras restricciones aparte de las enumeradas antes para gramáticas sensibles al contexto, con el fin de eliminar características que no se presentan en los lenguajes naturales. Ejemplos de algunas Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

283

construcciones indeseables de esta clase son NP —» NP S, o VP —>V VP, en el que V representa un verbo. Con las reglas sensibles al contexto, las cadenas de la forma NP NC VP pueden reemplazarse por NP PP VP2VP, en la cual PP es un pronombre personal. Por ejemplo, si NP es "El tigre" y VP es "se comió a la dama", NP PP VP2 VP se reemplazaría por algo como "El tigre, que estaba detrás de la primera puerta, se comió a la dama". Las gramáticas sensibles al contexto pueden ser reconocidas por un autómata lineal limitado, el cual es una Máquina de Turing (MT) con una cinta finita. In­ cluimos una breve introducción a las MT más adelante, puesto que algunos lecto­ res pueden no tener un curso de Teoría de la Computación en sus curricula universitarios.

A u tóm atas lin eales lim itad os (LBA; Linear-Bounded A utóm ata) Dado un lenguaje L, un reconocedor es un programa que se utiliza para determinar si una cadena S dada es o no una cadena válida en el lenguaje. Considere de nuevo el lenguaje L que aparece en el listado (6.1.1), el cual determina ciertos números decimales binarios. Un reconocedor debería decimos que 0.01 £ L y que 1.01 £ L. Un reconocedor para un lenguaje sensible al contexto es una Máquina de Turing (MT) determinística con una cinta finita, llamada autómata lineal limitado o LBA (por sus siglas en inglés). Requiere sólo de una cinta, mientras que el PDA, que reconoce lenguajes libres de contexto, utiliza dos. Una MT requiere de seis cosas [Cohén, 1991]: 1. 2. 3.

4. 5. 6.

Un alfabeto, X, de símbolos de entrada. Una cinta dividida en celdas, llamadas 1 , 2 , 3 , . . . Una cabeza de lectura/escritura que pueda moverse una celda a la izquierda o derecha, leer lo que encuentre y escribir o borrar esa información. No permiti­ remos a la cabeza ir más allá (a la izquierda) de la celda 1, puesto que no existe celdas que la precedan. Un alfabeto, T, de caracteres que pueda escribirse sobre la cinta. F puede in­ cluir a X, pero no lo necesita. Un conjunto finito, S, de estados, entre ellos Inicio (Start) y Alto (Halt). Un conjunto de reglas llamado programa, P. Cada regla es de la forma (estado^ leer-carácter, escribir-carácter, dirección, estado2). Si el estado actual de la MT es estado^ la cabeza lee el valor de leer-carácter en la celda, y escribe un valor de escribir-carácter a la misma celda y la mueve ya sea a la derecha o a la iz­ quierda. El nuevo estado es entonces estado2.

Veamos un ejemplo bastante extensivo de un LBA, LBA(AnBnCn) = (X, T, S, S0, S , P), para reconocer cadenas de la forma anbncn. El lenguaje de entrada X = {a b c #}. Aquí # marca el final de la entrada. F = (A B C T). T es un símbolo temporal utilizado para reemplazar las B. La cadena 'aabbcc' se transformará en 'aabbcc' 'AATTcc' —»AABBCC. Las T cuidan del contexto; la transformación extra es nece­ saria porque las b se encuentran entre las a y las c. La cinta LBA es inicializada para la cadena de entrada, y comienza el procesamiento en el estado SQ.Habrá reconoci­

Sólo fines educativos - FreeLibros

284

PARTE III:

Autómatas y lenguajes formales

do una cadena correcta cuando todas las letras minúsculas hayan sido cambiadas a mayúsculas, el símbolo # haya sido alcanzado y la máquina se encuentre en S5. Los estados son: SQ: Cuando la cabeza de lectura-escritura, *, se encuentra en una celda, y la máquina está en S0, todas las celdas antes de la celda, son correctas y no necesitan procesamiento adicional. Una a acaba de ser cambiada por una A, y buscamos ahora a la derecha una b para compararla. S2: Una b acaba de ser cambiada a una T, y estamos buscando a la izquierda por la siguiente a para cambiarla. S3*. Una T acaba de ser cambiada por una B, y buscamos ahora a la derecha una c para compararla. S4: Una c acaba de ser cambiada a una C, y estamos buscando a la izquierda la T siguiente para cambiarla. S5: HALT La figura 6.1.2 muestra el procesamiento de 'aabbcc'. El * muestra la posición de la cabeza de lectura-escritura. Hemos visto la ejecución de un programa exitoso, de modo que es tiempo de ver el programa mismo, como se ilustra en el listado (6.1.5). Cada instrucción se aplica al (estado actual, leer, escribir, mover y nuevo estado), dándonos los detalles necesarios para que se utilicen en los cambios de estado descritos antes. En la pri­ mera regla, por ejemplo, si estamos en el estado S0 y leemos una a, escribimos una A, nos movemos a la derecha, y pasamos al estado S . En el ejercicio 6.1.4, se le solicitará al lector que siga la secuencia de instrucciones empleadas en la ejecución mostrada en la figura 6.1.2. S0 a A R Sj) s 0 t b r s 4) S0 C C R S0) S0# # R S s) Sj a a R Sj)

(Inicia el procesamiento aqui)

(6.1.5)

{Buscando # para HALT} HALT {Buscando hacia adelante por una b para coincidir con una A}

S j T T R S j)

Sj b T L S2) S2 T T L S2) S2 a a L S2) S2A A R S 0) S3T T R S3) S CCRS) S j C C L S4) S4 C C L S4) S, T T L S.) S4 B B R S q)

{Buscando hacia atras por la siguiente a)

{Buscando hacia adelante por una c para coincidir con una B}

{Buscando hacia atras por la siguiente T}

La última regla (S4 B B R S0) es semejante a las otras, en la cual el símbolo leído, en este caso B, es el mismo que el escrito e indica que la celda que se está rastreando

Sólo fines educativos - FreeLibros

CAPÍTULO

E stado

Cinta

6: Lenguajes formales

Estado

285

Cinta

S 0:

a

a

b

b

c

c

#

A

A

B

T

C

c

#

Si:

A

a

b

b

c

c

#

A

A

B

T

C

c

#

Si =

A

a

b

b

c

c

#

A

A

B

T

C

c

#

S 2:

A

a

T

b

c

c

#

A

A

B

B

C

c

#

S 2:

A

a

T

b

c

c

#

A

A

B

B

C

c

#

S0:

A

a

T

b

c

c

#

A

A

B

B

C

C

#

Sv

A

A

T

b

c

c

#

A

A

B

B

C

C

#

Sv

A

A

T

b

c

c

#

A

A

B

B

C

C

#

S 2:

A

A

T

T

c

c

#

A

A

B

B

C

C

#

S 2:

A

A

T

T

c

c

#

A

A

B

B

C

C

#

S0:

A

A

T

T

c

c

#

s 3;

A

A

B

T

c

c

#

S 3:

A

A

B

T

c

c

#

S 0: Halt (Alto)

F I G U R A 6.1.2

Reconocimiento de aabbcc en LBA(AnBnCn)

permanece sin cambios cuando nos movemos a la siguiente celda y estado. Algu­ nas descripciones de las Máquinas de Turing ofrecen la opción de moverse sin es­ cribir. Elegimos volver a escribir un símbolo sólo para hacer más fácil la presentación. Sólo fines educativos - FreeLibros

286

PARTE ni: Autómatas y lenguajes formales

Un autómata lineal limitado determinístico (LBA) es una Máquina de Turing que se detiene en un lapso finito y que es determinística; es decir, para cada par de instrucciones, IN 1e IN2, s i INÍ = (S1X Y Z S2) e IN2 = (S3A B C S4) y si Sx = S3, X = A, Y = B y Z = C, entonces S2 = S4. Esto significa que el siguiente paso está siempre determinado por completo por el estado y la entrada. Si introducimos una cadena, la MT puede decidir si es una cadena legal o no en periodo proporcional a la longi­ tud de la cadena. Por supuesto, tomará más tiempo procesar a2346b2346c2346que a2b2c2, pero alguna función de la MT nos dirá cuánto tiempo más tomará. Nuestro grupo final de lenguajes no tendrá estas garantías.

VIÑETA HISTÓRICA Alan Turing: Lo que las máquinas no pueden hacer El título de la biografía de Alan Turing escrita por Andrew Hodges [Hodges, 1983], Alan Turing; El Enigma, es un juego de palabras. Enigma, que significa un misterio o problema desconcertante, también es el nombre de una ingeniosa máquina que utilizaron lo alemanes para generar códigos durante la Segunda Guerra Mundial. Turing fue ton genio matemático que descifró el código Enigma en 1942; estuvo convicto por "conducta indecente" (un eufemismo para la hom*osexualidad) en 1952, y se suicidó al comer una manzana empapada con cianuro en 1954. Es de estos asuntos que se hacen las leyendas, y por supuesto, el libro de Hodges fue llevado a una exitosa representación teatral en Broadway en 1988. Alan Turing nació en 1912, de padres en el Servicio Civil Indio de la Gran Bre­ taña. En 1933 ingresó a la Universidad de Cambridge para aprender matemáticas. Éstos eran tiempos vehementes, a medida que dos importantes preguntas acerca de la naturaleza de las matemáticas, cuestionamientos sobre su integridad y/o con­ sistencia, habían sido contestadas en forma negativa, mientras que una tercera per­ manecía abierta. En 1931 Kurt Gódel demostró que cualquier sistema matemático útil no podría ser completo sin ser inconsistente. Un sistema completo es aquel en el cual puede probarse cualquier proposición verdadera, mientras que uno consistente es aquel en que ninguna proposición falsa puede ser probada. En 1931 Gódel había demos­ trado que cualquier sistema matemático con suficiente complejidad para incluir multiplicación y división contiene proposiciones verdaderas que hacen al sistema inconsistente si son probadas. Una proposición de este tipo que puede expresarse en la Teoría de la Aritmética de Enteros (AE) es, G: "La fórmula G no es demostrable"

(6.1.6)

La fórmula G dice entonces de sí misma que no es demostrable; es decir, que no se puede probar dentro de la AE. Si se proporciona una prueba, la proposición es falsa, haciendo inconsistente la teoría, y si la proposición es verdadera, no puede hallarse una prueba, de modo que la teoría estaría incompleta. Tales proposiciones se conocen como "autorreferidas"(se// referent) porque hacen referencia a sí mis­ mas. La fórmula G misma es: G: (x) ~Dem(x,sub(n,13,n)) Sólo fines educativos - FreeLibros

(6.1.7)

CAPÍTULO 6: Lenguajes formales

287

El modo preciso en que la fórmula (6.1.7) expresa la (6.1.6) se encuentra más allá del alcance de este libro. El lector interesado puede consultar a [Nagel, 1958], En 1933, una pregunta aún abierta era si había algún método "mecánico" para determinar cuáles proposiciones eran decidibles o no; es decir, puede tomarse una decisión por adelantado acerca de qué clases de problemas llevarán a respuestas y cuáles conducirán a cálculos infinitos sin una decisión. Un método "mecánico" es aquel que sigue reglas, pero puede o no efectuarse en una máquina física. Alan Turing eligió dirigirse a este problema de la decidibilidad. Primero tuvo que hacer precisiones acerca de lo que se entiende por una "máquina". Esto resultó en una máquina teórica de la que se podría esperar que resolviese cualquier pro­ blema que otra máquina, o ser humano, siguiendo reglas especificadas, podría re­ solver. Él fue capaz de encontrar siete preguntas que una máquina de este tipo no puede contestar. 1. 2. 3. 4. 5. 6. 7.

Dada una máquina M para resolver problemas arbitrarios y un problema arbi­ trario P, ¿puede M resolver P? Dada una máquina particular M, ¿es capaz M de resolver un problema P arbi­ trario? Dada M, ¿puede reconocer la ausencia de problemas cuando los vea? Dada M, ¿es capaz de resolver cualquier problema en absoluto? Dada M, ¿puede resolver todos los problemas? Dadas dos máquinas, M I y M2, ¿son capaces de resolver los mismos proble­ mas? Dada una Máquina de Turing MT, ¿el lenguaje MT acepta regular? ¿libre con­ texto? ¿decidible?

La Segunda Guerra Mundial interrumpió los estudios teóricos de Turing, cuan­ do se le asignó a la Escuela del Gobierno de Códigos y Cifrado (GCCS; Government Code and Cypher School), justo a medio camino entre las universidades de Oxford y Cambridge. Los alemanes utilizaban una máquina con cuatro rotores con el pro­ pósito de generar códigos para las transmisiones hacia, entre otras cosas, sus sub­ marinos. Con 26 caracteres y cuatro rotores, un código podría tener 26 x 26 x 26 x 26 = 456 976 diferentes configuraciones, y los alemanes cambiaban el código diaria­ mente. Por 1940, los británicos tenían diseños de la máquina Enigma, obtenidos por agentes polacos, pero la determinación exacta de cuál era el estado de los rotores permanecía como un problema. Lo que se necesitaba era una máquina para anali­ zar códigos de enigma y descifrarlos con rapidez. Los conocimientos de Turing en teoría de números, lógica matemática y teoría de la probabilidad, más la ingeniería necesaria para construir una máquina en la práctica, rindieron frutos. En 1942 GCCS construyó una máquina, llamada "Bomba" debido a su fuerte sonido, capaz de descifrar los códigos de Enigma. Alan Turing fue el cerebro detrás de este logro. No es una exageración decir que este esfuerzo cambió el curso de la guerra. Como resultado, los británicos fueron capaces de determinar la ubicación exacta de cada submarino alemán en diversos mares. Después de la guerra, Turing volvió a su investigación ligada a las capacidades de la Máquina de Turing en el National Physical Laboratory y en la Universidad de Manchester. Su prometedora carrera se derrumbó con su arresto en 1952. Su muer­ Sólo fines educativos - FreeLibros

288

PARTE ni: Autómatas y lenguajes formales

te en 1954 fue considerada por la mayoría como un suicidio, pero el chapucero trabajo policiaco nunca pudo eliminar la posibilidad de un accidente. Algunos tratadistas creen que esto era un subterfugio, destinado por Turing para ahorrarle a su familia la ignominia de un suicidio. Ellos eran libres de creer lo que quisieran. Durante su breve existencia, Alan Turing se enfrentó a algunas de las interro­ gantes más profundas que planteaban las computadoras. ¿Puede una máquina ser tan "inteligente" como un humano? ¿Es el libre albedrío compatible con una visión mecánica del mundo? ¿Las emociones y la razón son lo mismo o cosas diferentes? ¿Pueden comprender las máquinas las experiencias humanas como el amor, la frus­ tración, el sufrimiento o la desesperación? También intentó combinar las matemá­ ticas, la filosofía y la ingeniería; algo ridiculizado durante su existencia, pero ahora tomado en cuenta, 42 años después de su muerte.

Tipo 0: G ram áticas no restringidas Las gramáticas Tipo 0 son construidas sin restricciones sobre las reglas de reempla­ zo, excepto que una no terminal debe aparecer en la cadena del lado izquierdo. Los lenguajes generados se conocen como lenguajes Tipo 0 o, de un modo más común, recursivamente enumerables (r.e.). Las producciones de Tipo 0 son las mismas que aquellas para los lenguajes Tipo 1, excepto que la regla 2, en la cual el lado izquierdo no debe ser más extenso que el derecho, se elimina. De este modo una gramática Tipo 0 es: 1. 2. 3.

Un alfabeto X de símbolos terminales Un alfabeto T de no terminales, incluyendo un símbolo de inicio Un conjunto de reglas de producción a —>¡i, en el que a y /?son cadenas de X U T, con a conteniendo por lo menos una no terminal, y sin restricciones sobre /?

Los reconocedores para lenguajes Tipo 0 son Máquinas de Turing, las cuales ya fueron presentadas, pero en este caso la cinta puede ser infinita, aunque el número de estados sea finito. Un ejemplo detallado de un lenguaje Tipo 0 que no es también del Tipo 1 está más allá del alcance de esta breve introducción, pero existe. Considere el lenguaje regular CWL (Code Word Language), generado por la gramática regular en el lista­ do (6.1.8). S —> a S l a B B ->bC C —>aC I aD D->bE E -> aF I bF F -» aG I bG G aH I bH H —>al I bl I ->a I b

(6.1.8)

Sólo fines educativos - FreeLibros

CAPÍTULO 6: Lenguajes formales

289

Cohén [Cohén, 1991] presenta una MT = (X, T, S, Sir S2, P), que reconoce pala­ bras de CWL como se muestra en el listado (6.1.9). X = (a, b, A], T = |b}, S - \SV S2, S3} P: (Sj, b, b, R, S}) (S1, a, b, R, S3) (S3, a, b, L, S3) (S3, A, b, L, S2)

(6.1.9)

¡Nada nuevo hay aquí! Puesto que los lenguajes regulares (Tipo 3) también son Tipo 0, pueden ser reconocidos por las MT. De hecho, la MT del listado 6.1.9 es un LBA. De este modo, CWL no es el lenguaje Tipo 0 que estamos buscando; es decir, un lenguaje que no sea también del Tipo 1. Cohén luego codifica las cuatro instrucciones del listado (6.1.9) en cadenas de a y b. Un código de una palabra para la MT entera de cuatro instrucciones es la cade­ na en el listado (6.1.10): ababababbabaaabaaabbaaabaaabaaabaaaabaabbaaba

(6.1.10)

De hecho, cualquier MT sobre X = (a, b) puede ser codificada en una cadena de CWL. Las cadenas de CWL también pueden ser decodificadas en las MT, algunas legales y algunas no (por ejemplo, una MT resultante puede tener reglas duplica­ das). El lenguaje MATHISON (por el apellido materno de Alan Turing) se define entonces: MATHISON = {todas las palabras en CWL que son aceptadas por sus correspondientes MT)

(6.1.11)

Dejamos como un ejercicio demostrar que la cadena del listado (6.1.10), codifican­ do la MT del listado (6.1.9), es aceptada por TM(CWL). Cohén suministra una prueba de que MATHISON es recursivamente enume­ rable (Tipo 0), pero nosotros no lo haremos, para que sigamos en el camino con nuestra cabeza sobre los hombros. Los lenguajes más generales que los del Tipo 1 son creaciones extrañas y maravillosas, de interés teórico, pero no para definir len­ guajes de programación. E J E R C I C I O S 6.1

1. Construya un sistema de producción P para generar cadenas de X = {1,0}, a. terminando en 0 (números pares). b. terminando en 1 (números impares). c. cadenas con cualquier combinación de 0 y 1 con una longitud exacta de 8. 2. Construya sistemas de producción para generar cadenas sobre X = {a, b) de la forma: a. an, n = 0,1,... b. anbn, n = 0,1,... c. anbncn, n = 0,1,..., 4 d. anbncn, n = 0,1,... (difícil) Usted notará diferencias en la forma de las reglas de producción en a, b, c y en d. Sólo fines educativos - FreeLibros

290

PARTE ni: Autómatas y lenguajes formales

3. Las reglas que contiene e son llamadas reglas de borrado. El símbolo e es la letra griega épsilon, y representa la cadena vacía, una palabra sin ningún carácter en ab­ soluto. £no es ni una terminal ni una no terminal. Pero es necesario de algún modo representar nada, así se utiliza e. Si A —» £ es una regla, la A no terminal puede ser borrada. Si S0 —>e es una regla, el lenguaje generado contiene la cadena nula. ¿Qué lenguaje genera el siguiente sistema? Rl: S —>aSb R2: S —» £ 4. Con el programa del listado (6.1.5), siga la secuencia de instrucciones empleadas en la ejecución que se muestra en la figura 6.1.2, la cual reconoce la cadena 'aabbcc'. 5. Sean Ia 1, Ib I y Ic I los indicadores del número de las a, b o c en una cadena de entrada. Utilizando la MT LBA(AnBnCn) descrita en el segmento acerca de los autó­ matas lineales limitados, intente cadenas de prueba para demostrar que: a. La MT se detiene en S0 si Ia I =0. b. La MT se detiene en S2 si Ia I > 0 y Ib I =0. c. Si Ia I > 0 y Ib I > 0, se detiene en S4si Ia I < Ib I, y en S2si Ia I > Ib!. d. Si 0 > Ia I = Ib I, la MT se detiene en St si Ic I > Ia I y en S4si Ic I < Ia I. 6. ¿Qué palabras son generadas por la gramática del listado 6.1.8? 7. Demuestre que la cadena del listado (6.1.10), modificando la MT del listado (6.1.9), es aceptada por MT(CWL). 8. Una palabra de código para la MT(S17b, b, R, S2) de una instrucción es abaabababb. a. Demuestre que la cadena de código no es aceptada por la MT. b. ¿Qué palabras acepta la MT?

6.2

GRAMÁTICAS REGULARES Como lo discutimos en la sección 6.1, una gramática estructurada en frases G = (X, N, P, Inicio) es una gramática regular si sus producciones son de la forma: A —>a, o A -» aB, en el que A,B G N, y a E I

(6.2.1)

Sin embargo, no es necesario que las reglas estén en la forma del listado (6.2.1) para que una gramática sea regular. Si las reglas están en la forma del listado que se citó, se garantiza que la gramática resultante sea regular, pero existe otros sistemas para gramáticas equivalentes. En la formulación de Chomsky, la gramática B7 = (X, N, P', S0) del listado (6.2.2) genera el lenguaje L(B7). Este último es el mismo lenguaje que L(B), el cual fue examinado en el listado (6.1.1). Se dice que dos gramáticas, B y B7 sobre X, son equivalentes (=) si ellas generan el mismo lenguaje, aquí L(B7) = L(B). Las producciones P7son: P': Rl': So R 2 ’: s, — > 0 R 3 ’: s2 — > S3S4 R4': S3 — > R5': s4 S A R6': S5 — » i R7': S4 SA R8': S4— > s5s4 R9': S4 s5s5 Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

291

Nótese que R l’, R31, R5' y de R71hasta R9' no son de las dos formas especificadas en (6.2.1). Pero veremos en forma posterior más de esto en la discusión acerca de for­ mas normales en la sección 6.3. Expresiones regulares Las gramáticas regulares también pueden ser construidas a partir de expresiones regulares, en lugar de utilizar esquemas de producción. Recuerde del capítulo 0 que los tokens son constantes, símbolos especiales, palabras reservadas e identificadores. La forma de estos tokens es con frecuencia bastante simple, de modo que es útil emplear una gramática regular para aceptarlos. Las expresiones regulares involucran una notación en especial bella para definirlas, o por lo general para definir cadenas válidas en una gramática regular. Considere, por ejemplo, un identificador Pascal, que puede ser definido como sigue: identificador —>letra (letra I dígito)* letra —» A I . . . I Z I a l . . . l z dígito —>01 — 19 Comienza con una letra, luego es seguido por (concatenado con) una secuencia de letras y dígitos. Aquí la I es alternativa; significará "o, pero no ambas". Los parén­ tesis se utilizan en agrupación y el asterisco (*) o estrella de Kleene indica cero o más repeticiones. Definimos una expresión regular e sobre un alfabeto X como sigue: 1. 2. 3. 4.

e (la cadena nula) es una expresión regular. Si x £ X, entonces x es una expresión regular Si e1es una expresión regular, entonces así es (ex). Si e 1y e2 son expresiones regulares, entonces así son e 1 e2, e1 I e2 y e * .

El hecho de que £ sea una expresión regular no necesita explicación. La regla 2 dice que cada símbolo de X es una expresión regular. Note que las expresiones regulares están cerradas bajo tres operaciones: concatenación (sin símbolo entre ellas), alternancia ( I) y asterisco (*). Los paréntesis de la regla 3 necesitan una breve explicación. Éstos no son símbolos en X, pero pueden ser empleados con libertad para hacer más claras las expresiones. Símbolos tales como (,), 1 y *, que pueden utilizarse en expresiones pero que no son parte del lenguaje mismo, se denominan meta símbolos.6 Dentro de una expresión, * tiene la precedencia más alta, luego sigue la conca­ tenación y después la alternancia. De aquí, en el ejemplo ab I cd*, el asterisco se aplica sólo a d, y las alternativas son ab y cd* (es decir, c, cd, cdd, cddd, etc.). El lector debe confirmar que las cadenas como abd y acdd no son válidas, pero serían 6En algunos textos los símbolos de £ se escriben en negritas para separarlos de los metasímbolos; por ejemplo, (x)(yy). Sólo fines educativos - FreeLibros

292

PARTE III: Autómatas y lenguajes formales

válidas en a(b I c) d*. Como otro ejemplo, las cadenas de a y b que contienen al menos una 'a' estarían representadas por (a I b)*a(a I b)*. Con las expresiones regulares definidas, estamos listos para enumerar las re­ glas de una gramática regular usadas para construir un lenguaje regular L de una expresión regular e. Describiremos L(e) para indicar el lenguaje L definido por e. 1. 2.

Si e = x, entonces L(x) = {x}. Es decir, la única palabra en el lenguaje L es x. L(e) = {€}. Si L(e1) = Lx y L(e2) = L2, entonces a- L(ej e2) = L jL2 b. L(ei I e2) = L1 I L2 c. L(e/) = V

Algunos ejemplos están en orden. Supongamos que Lx= {x| y L2= (y), en la que ex = x y e2 = y. Esto es, cada lenguaje tiene exactamente una palabra en él. Entonces: 1. L(xy) = (xy), una sola palabra, xy 2. L(x I y) = {x} I {y} = {x, y} 3. L(x*) = L* = (e, x, xx, xxx,...} 4. L(x*y) = L(x*)L(y) = {y, xy, xxy, xxxy,...) Para ser un poco más prácticos, considere el lenguaje B, de decimales binarios entre ellos el 0 pero menores que 1, y aproximados a dos posiciones. El alfabeto, £ = {., 0, 1). B = {0.00, 0.01, 0.10, 0.11). B puede ser construido a partir de los dos lenguajes: L1 = {0.) - L(0.)

y L2 = {00, 0 1 , 10,11¡ = L((0 I 1)(0 I 1)) = L((0 I l) 2)

Entonces B = L((0.)(0 I l ) 2) = LXL2. Bes generado por la expresión regular 0 . (0 I l ) 2. Autómatas finitos (FA, NFA y DFA) Ahora que hemos visto cómo generar un lenguaje regular, nos enfrentaremos con el problema opuesto. Dada la gramática regular B en el listado (6.1.1) y una cadena de su alfabeto, ¿cómo podemos reconocer si esa cadena particular es una palabra de L(B) o no? Queremos una máquina que aceptará palabras válidas y rechazará cadenas inválidas. Para los lenguajes regulares, una máquina de este tipo es llama­ da autómata finito (FA, por sus siglas en inglés). La máquina debe funcionar en forma automática y reconocer o rechazar una cadena de entrada en un número finito de pasos. Dada una cadena para procesar, se procede mecánicamente en cada uno de los símbolos para aceptar o rechazar la cadena como una palabra del len­ guaje para el que el autómata fue construido. Procesará un símbolo a la vez de izquierda a derecha. Si una palabra es procesada comenzando en la flecha de inicio que apunta al estado de inicio S0, se dice que es reconocida o aceptada por el FA si el proceso termi­ na en un nodo terminal o Final, mostrado en la figura 6.2.1 como el nodo en cuadro Sólo fines educativos - FreeLibros

CAPÍTULO 6: Lenguajes formales

293

doble F. Se dice que las palabras que no son reconocidas son rechazadas o fallan. La gráfica con direcciones en el extremo inferior, llamada diagrama de transición, repre­ senta un FA para el lenguaje L(B). La S. y la F son llamados estados. S0 es el estado de inicio, y F, una terminal o estado final. Un FA debe tener exactamente un estado de inicio, pero puede tener uno, ninguno o varios estados finales. Para reconocer la cadena s = '0.01', el procesamiento comenzaría en S0, en el extremo izquierdo de s. Puesto que la lectura del primer símbolo es 0, la máquina cambiaría al estado Sx, en el que se lee el segundo símbolo, El FA cambiaría entonces al estado S2, en el cual se lee 0, y luego a S3, en el que se lee el 1. El FA cambiaría entonces al estado F y se detendría, puesto que habría alcanzado el final de la cadena de entrada. Ya que F es un estado terminal, s ha sido reconocida. De manera formal, un autómata finito es una 5-tupla (S, E, T, Inicio, FS) con: 1. 2. 3.

4. 5.

Un conjunto de Estados S = {SQ, Sv ... Snl). Para el ejemplo de la figura 6.2.1, S = { S 0, S 1, S 2, S 3,F). Un alfabeto E. En nuestro caso, E = {0,., 1). Un conjunto de transiciones T. En la figura 2.1.1, las transiciones están repre­ sentadas por flechas; por ejemplo, una transición se hace desde el estado S: al S2 si el procesamiento está en el estado S1y se reconoce en la cadena que se encuentra en proceso. Inicio es el estado de inicio. En la figura 6.2.1, Inicio = SQ. Un subconjunto de los S. (posiblemente vacío) es designado como estados fina­ les, de parada o terminales FS. Aquí, FS = {F}.

Un FA es finito porque el alfabeto y el número de estados son finitos. Un FA se encuentra en el estado SQsi el procesamiento acaba de comenzar o si hay alguna transición de regreso a S0. El FA hace una transición al estado S. al leer un símbolo x. El FA, entonces, se encuentra en el estado Sf El conjunto de transiciones también puede representarse como una tabla de transición. Se puede utilizar con libertad cualquiera que sea el más claro. La tabla de transición para el FA de la figura 6.2.1 se muestra en la figura 6.2.2. Una tabla tal como la de la figura 6.2.2 puede ser construida a partir de una gramática regular G = (S, E, T, Inicio, FS) como sigue: 1.

Si m es el número de elementos en E, y n es el número de no terminales (S - Sn 2) utilizadas en T, construya una tabla de n x m con renglones titulados

FIGURA 6.2.1 Diagrama de transición para L(B) incluyendo estados

Sólo fines educativos - FreeLibros

294

PARTE III: Autómatas y lenguajes formales Entradas 0

So Si

s2 S3

*F

1

.

Si —

S2

S3 F —

S3

F

FIGURA 6.2.2 Tabla de transición para el lenguaje B

2. 3. 4.

S. (0 i n-1). Si hay cualquier producción de la forma N —>t, agregue un renglón titulado F. El renglón F está marcado con * para indicar que es un estado termi­ nal. Las columnas son tituladas con los símbolos de terminal m de X. Para cada regla de la forma S. —» tS., introduzca la transición desde S. hasta S. escribiendo "S " en Tabla(S., t). Para cada regla de la forma N —» t, escriba "F " en Tabla(N, t). Marque todas las otras celdas de la tabla con "— ", representando la ausencia de una transición.

Las no terminales de la gramática, con la posible adición de F, se convierten en los estados para la FA. En forma similar, un sistema de producción para una gramática regular puede construirse a partir de una tabla de transición, T-Table(X, y), como se explica a continuación: 1. 2.

Para cada entrada S. en T-Table(S., t), escriba la regla S. —» tS.. Para cada entrada F* en T-Table(S., t), escriba la regla S. —> t.

Si existe exactamente una transición posible de cada estado, dado un posible símbolo de entrada, un FA se denomina determinístico o un DFA. Si se ofrece múlti­ ples selecciones de un estado Si para algún símbolo t, el FA se llama no determinístico, o un NFA. En este caso, si el FA se encuentra en el estado S¡ y t es leído de la cadena de entrada, lo que debería hacerse no está determinado. La figura 6.2.3 representa un NFA porque existe dos elecciones de transición desde S0 al reconocer la letra b, resultando en cualquiera de losestados SQo Sr Resulta entonces que es más conveniente mostrar la entrada de latablacomo el Entradas

50

{S0}

{Sq.S ú

51

FIGURA 6.2.3 Diagrama y tabla de transición para un NFA

Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

295

conjunto {S0/ S J . De manera correspondiente, todas las otras entradas de la tabla indican el conjunto de los estados resultantes. ¿Puede usted deducir cuál lenguaje reconoce este NFA? Las transiciones SQ-^ SQy S0-^>S0 se conocen como ciclos. Es posible ciclar cero o más veces generando repetidas a o b. La relación para expresiones regulares ahora puede haber llegado a ser más clara. Las expresiones regulares (a I b)*b están re­ presentadas en la figura 6.2.3. La alternancia se indica mediante múltiples ramifi­ caciones desde SQhacia otro estado (en este caso, de regreso a S0). La concatenación se indica mediante la secuencia de estados desde S0 hasta Sr El asterisco de Kleene resulta en un ciclo desde un estado y de regreso a sí mismo. El trabajo de Kleene y otros muestra que cualquier lenguaje que puede recono­ cer un NFA también es capaz de reconocer un (probablemente más complicado) DFA. Un método mecánico para producir el DFA en la figura 6.2.4 a partir del NFA de la figura 6.2.3 se deja para los ejercicios. Cuando el procesamiento ha progresado hasta el estado de terminación (SQ,S J, puede o bien detenerse, repetirse durante 0 u otras b, o regresar a {S0} si una a se ha leído. El DFA reconoce infinitamente más palabras puesto que se puede ir a través de los ciclos cualquier número de veces. Un importante resultado de la teoría de los autómatas dice que cualquier lenguaje reconocido por un FA, como el lenguaje reco­ nocido por los FA de las figuras 6.2.3 y 6.2.4, es regular. También se ha demostrado que cualquier lenguaje con un número finito de palabras puede ser reconocido por un autómata finito. Lo inverso, por supuesto, no es verdad, como se ejemplifica me­ diante el DFA de la figura 6.2.4. Recomendamos al lector interesado a [Cohén, 1991]. Aplicaciones Hemos considerado un grupo de lenguajes, llamados regulares, que se generan mediante gramáticas regulares y cuyas palabras pueden ser reconocidas por los DFA (NFA). Vimos un ejemplo de cómo construir un FA desde la gramática regular B, y una gramática de un FA expresada en la tabla de la figura 6.2.2. Cada lenguaje con un número finito de palabras puede generarse a partir de una gramática regular; los procesadores de texto pueden ser escritos utilizando los DFA. La compilación de un lenguaje involucra varios pasos, el primero de los cua­ les, como se mencionó antes, es el análisis lexicográfico (rastreo) o el reconocimien­ to de tokens y símbolos válidos. Un lenguaje especial denominado LEX [Lesk, 1975] se implemento para producir un DFA a partir de código fuente. Algunos compi­ ladores también emplean un DFA para implementar el primer paso sobre el código fuente. Para un ejemplo de cómo LEX produce un rastreador DFA para declaracio­ nes aritméticas de FORTRAN, véase [Aho, 1986]. a {S0}

b ...

{S0,Si}

a b FIGURA 6.2.4 Equivalente DFA para el NFA de la figura 6.2.3

Sólo fines educativos - FreeLibros

296

PARTE III: Autómatas y lenguajes formales LABORATORIO

6.1: E X P R E S I O N E S R E G U L A R E S : grep

O bjetivos (Los Laboratorios pueden encontrarse en el Instnictor's Manual) 1. Usar la utilidad grep o egrep para investigar la forma y notación de expresiones regulares. 2. Volver a escribir una expresión regular como un autómata finito (FA). 3. Crear un diagrama y una tabla de transición para el FA generado en el punto 2 de este ejercicio.

E J E R C I C I O S 6.2 1. ¿Cuál de los sistemas que usted construyó en los ejercicios 6.1.2 anteriores son regu­ lares? 2. Construya una gramática regular para generar palabras sobre (a, b¡ que contenga la cadena 'abab'. 3. ¿Por qué la regla S —» aSb no está permitida para producir un lenguaje regular? 4. Suponga que un lenguaje con palabras anbnestá restringido a n < 1000. Llámelo L1000. Puesto que cualquier lenguaje con un número finito de palabras es regular, L1000 es regular. ¿Cómo podríamos construir reglas de producción para generar palabras de L1000 y un FA para reconocerlas? 5. ¿Es la gramática B' del listado (6.2.2) una gramática estructurada en frases? 6. Construya una tabla de transición y un FA para un lenguaje L con dos símbolos, X = {a bj, en el que L contiene palabras que contienen una cadena de por lo menos dos a. Algunas palabras de L son aa, aaab, abba, baa y bbaaaaa. 7. Usted quizá construyó un NFA para el ejercicio 6. Si fue así, construya el DFA equi­ valente. Existe una manera automática para hacer esto. Demostraremos el método; por ejemplo, con el uso de la gramática y NFA de la figura 6.2.3, la cual se incluye a continuación. a

Entradas

C*0

b

S, 1

So Si

a

b

{Sol 0

{So-Sú 0

b

Paso 1: sea S = {S0, S}} los estados del NFA en la figura 6.2.3, y sea P(S) = { 0 , {S0|, {SJ, {S0,S1}} el conjunto de potencia de S. Sea P(S) el conjunto de estados para el DFA relacionado con la figura 6.2.4. Paso 2: construimos transiciones de P(S) mediante: • • • •

{S0} es el estado de inicio, en el que S0 era el estado de inicio para el NFA. 0 ^ 0 para cualquier entrada. £ —> 0 para la entrada x, si no hay transición en el NFA de S para x. P. —> P. para la entrada x, si P =syp T(S.x)tal que existe un estado, S. en P., y existe una transición en el NFA, S —» T(S.,x), el conjunto de entradas de tabla de transi­ ción para el estado S. y la entrada x. • Marcar cualquier terminal de estado (*) que contenga un estado que era terminal en el NFA.

Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

297

Demuestre que este proceso siempre produce un DFA a partir de un NFA para un número finito de entradas que puedan hallarse en él [Johnsonbaugh, 1993]. La tabla de transición resultante es: Entradas UOlClvIvw 0

a

b

{S0}

{S0}

{So, S-J

*{Sd *{s0, Sd

{S0}

{So, S-,}

El DFA mostrado en la parte superior de la página siguiente representa esta tabla.

a

w

b

{Sol ^

*

{S0, S,}

a b

a

b

Las transiciones de (SJ y 0 pueden ser eliminadas, puesto que no se les puede alcan­ zar desde el inicio {S0}, y queda el diagrama de la figura 6.2.4. 8. Construya un DFA a partir del siguiente NFA:

a

a

b

Sólo fines educativos - FreeLibros

298

PARTE III: Autómatas y lenguajes formales 9. Construya sistemas de producción para sus lenguajes de los puntos 6 ,7 y 8 de estos ejercicios. 10. Escriba una gramática para generar palabras que contengan una sola vocal con una sola consonante a cada lado, o la consonante simple a la derecha seguida por una s. Usted puede limitar sus consonantes a {b, d, m, n) para simplificar el asunto. Algu­ nas palabras (en idioma inglés) en el lenguaje generado podrían ser "bed", "beds", "dad", "m an", "m uns", etcétera. 11. Escriba una gramática, tabla de transición y FApara generar palabras con cualquier número de letras a y exactamente tres b.

6.3

GRAMÁTICAS LIBRES DE CONTEXTO (CFG) Existe lenguajes que no pueden ser reconocidos por los FA y no son regulares. Uno de los más simples es el lenguaje: LN = [anbn: n = 1, 2, ...) = {ab aabb aaabbb ...)

(6.3.1)

La demostración de que LN no es regular utiliza el hecho que si lo fuera, habría un autómata finito que reconocería cadenas legales y rechazaría cadenas ilegales, y que la existencia de un FA de esta clase conduce a una contradicción. La demostra­ ción puede encontrarse en [Cohén, 1991]. De este modo, existe lenguajes de otro tipo aparte del Tipo 3 de Chomsky. Recuérdese de la sección 6.1 que para las CFG el lado izquierdo debe ser un no terminal, mientras que el lado derecho puede ser cualquier cadena de terminales y no terminales. Como un ejemplo, examinaremos la CFG para LN. Aquí X = (a, bj, N = {S} y el símbolo de inicio es S. Las reglas de producción para cadenas de la forma anbn son: P = {Rl: S

aSb, R2: S -» £}

Como hemos visto previamente en la notación, estas dos reglas pueden ser combi­ nadas en S —>aSb I £. Podemos producir la palabra a2b2 empleando la derivación: Rl Rl R2 S —» aSb —» aaSbb —» aabb en la cual la producción final borra la S. Nótese que puesto que S —>e es una produc­ ción válida, la cadena vacía ese encuentra en el lenguaje LN. El borrado puede con­ ducir a problemas, de modo que se desarrollaron varias estrategias para eliminar las reglas de borrado. Aquí, podríamos haber utilizado, en lugar de las reglas en P: P2 = (S -> aSb I ab)

(6.3.2)

Denotemos LN7 = L(P2). Las producciones de P2 no producen el lenguaje LN = L(P), ya que £ G LN, pero £ g LN7. Hay pruebas de que cualquier lenguaje que no incluya la palabra vacía £ puede ser generado por una gramática sin reglas de borrado. Sólo fines educativos - FreeLibros

6: Lenguajes formales

CAPÍTULO

299

Autómatas descendentes (PDA; Push-Down Autómata) Del mismo modo que las palabras de un lenguaje regular pueden ser reconocidas por un autómata finito (FA), las palabras de un lenguaje libre de contexto son reco­ nocidas por un autómata descendente (PDA). Los inversos también son verdaderos: cualquier lenguaje reconocido por un FA (o PDA) es regular (o libre de contexto). Un PDA se compone de dos cintas (quizá de longitud infinita). La primera es una cinta de entrada que contiene la palabra por reconocerse. Se agregó el símbolo # al final de la cinta de entrada para dar a entender el final de la entrada particular que se intenta reconocer. La segunda cinta funciona como una pila descendente, y contiene al principio el símbolo de inicio S y el símbolo de terminación #. Veamos cómo funciona esto en la figura 6.3.1, cuando reconozcamos a*b2 de nuestro len­ guaje LN' del listado (6.3.2). La acción es apilar las a y extraerlas a medida que encontremos las b correspondientes. El * debajo de la cinta de datos marca el apun­ tador de posición para el símbolo que se leerá a continuación. ¿Por qué no necesita­ mos un apuntador de posición para la pila? Un PDA puede ser definido como un conjunto de reglas para dos cintas: una que contiene una cadena de entrada y la otra para ser empleada como una pila. Una regla es de la forma:

Entrada a

Pila

a

b

b

#

s

#

a

b b

#

a

S

b

a

b b

#

S

b

#

b

b

#

a

b b

b

b

#

b b

b

#

b

* a

#

*

a

*

a

a *

a

a

#

*

a

a

b

#

*

a

a

b

b

#

¡Éxito!

F I G U R A 6.3.1

Operación de un PDA para reconocer a2b2 de LN'

Sólo fines educativos - FreeLibros

#

300

PARTE

ni: Autómatas

y

lenguajes formales

[r, s j -> [x, s2],

(6.3.3)

en la que r es un carácter simple leído de la cinta de entrada, s1enumera lo que es­ tá en la parte superior de la pila y s2 reemplaza a en la pila. El apuntador de lectura avanza un carácter si x es > en el lado derecho de la regla, o permanece en el último carácter leído si x es No estamos confinados a considerar sólo un símbolo de la pila, pero puede extraerse tantas veces como sea necesario. Por ejemplo, la regla [a,S] —» [-,aSb] significa que si a ha sido leída de la cinta de entrada, el apun­ tador de lectura permanece donde está, S se extrae de la pila y aSb se desplaza. Por claridad, cr puede emplearse para representar elementos arbitrarios de la cinta de entrada. Las reglas del PDA son, de este modo, algo parecidas a las reglas de pro­ ducción, en el sentido de que el lado izquierdo representa los estados actuales de las dos cintas, mientras que el lado derecho muestra los estados después de que se toman las acciones apropiadas. Las reglas de producción para un CFG pueden utilizarse para construir reglas de PDA con el uso del algoritmo no selectivo descendente o de arriba hacia abajo (NTB; Non-selective Top-to-Bottom) de Griffiths y Petrick [Griffiths, 1965] que se muestra en el listado (6.3.4). NTB: condición CFG 1. A —» s1s2...sn 2. a £ X

regla PDA ( a ,A ) - » ( - ,s 1s2...Sn) (^/^) —^ (^/ £)

(6.3.4)

NTB expresa (1) reemplazar en la pila un lado izquierdo de una producción CFG con su lado derecho, o (2) extraer un símbolo terminal hallado en ambas cintas de la pila y avanzar el apuntador de lectura. Las reglas para el PDA de LN' en el listado (6.3.2) se muestran en el listado (6.3.5). La primera regla fue descrita con anterioridad. La segunda regla enuncia: "Si S se encuentra en el tope o parte superior de la pila, extraiga S, y después des­ place ab sobre la pila." La tercera regla es: "Si usted lee una a en la cinta de entrada y observa una a en la pila, extraiga la a de la pila y haga avanzar elapuntador de lectura." La cuarta regla es similar a la tercera, con b en lugar de a; la quinta regla nos permite conocer cuando la cadena de entrada ha sido reconocida por el PDA. La pila es iniciada con S# y la cinta de entrada con la cadena que será reconocida seguida por #. La lectura de la cadena de entrada comienza al frente de la cinta y es de izquierda a derecha. Siga estas reglas a lo largo de la ejecución del PDA que se muestra en la figura 6.3.1 para a2b2. R l: R2: R3: R4: R5:

[a, S] -> aSb] [cr, S] -> [-, ab] [a, a] —^ [>, £] [b, b] -> [>, e] [#, #] -> ¡Éxito!

(6.3.5)

Note que R l y R2 son no determinísticos. Cualesquiera de ellos puede utilizarse para reemplazar S en la pila. Esto es la causa de que el algoritmo no sea selectivo: no hay directivas para elegir cuál regla de PDA emplear si se puede aplicar dos o más. Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

301

¿Por qué puede este PDA reconocer cadenas que no podrían ser reconocidas por un FA? Exploraremos esto para una n fija en los ejercicios 6.3.1 y 6.3.2. Un FA no tiene la habilidad para contar. Si una cadena anbnse envía a un FA para su reco­ nocimiento, en el cual un ciclo ha sido integrado para reconocer las a, el FA no puede recordar cuántas a ha visto (cuántas veces han circundado por el ciclo), de modo que un ciclo reconociendo las b podría no ser recorrido un igual número de veces. Como hemos visto antes, la pila puede realizar esta función de conteo. Más formal aún, un PDA es una 6-tupla (X*, N U X*, Inicio, #, {>, R], en el que: 1. 2. 3. 4. 5 6.

X* is X U {#}, el conjunto de símbolos de la cinta de entrada N U X* es el de los símbolos de la pila Inicio es el símbolo de inicio # es el símbolo de terminación {>,-} indica movimiento del apuntador de lectura R es el conjunto de las reglas del PDA

Como un ejemplo más práctico en lenguajes de programación, consideremos una versión simplificada de una expresión aritmética de Pascal. La gramática es ArithExp = (X, N, P, Inicio), en la cual X = {0,1, +, *, (,)}, N = {EXP, FAC, TERM}, el símbolo de inicio es EXP y las reglas de producción son: P: R l: EXP -> TERM I EXP + TERM R2: TERM -> FAC i TERM * FAC R3: FAC - > 0 1 1 1 (EXP)

(6.3.6)

Tres palabras válidas de L(ArithExp) son 1*0,1+0+1 y (1+1)*0. Con el uso del algoritmo NTB del listado (6.3.4), un PDA, para reconocer L(ArithExp), tiene: X* = {0,1,+,*,(,),#} N U X* = {0,1,+,*,(,),#, FAC,TERM,EXP) Start = EXP R: A E la-b: [<x, EXP] -+ [-, TERM] AE2a-b: [<7, TERM] -> [-, FAC] AE3a-c: [o, FAC] -+ [-,0] AE4a-f: [0,0] I [1,1] I [+,+] I [*, *] ->[>,£] AE5: [#,#] -> ¡Éxito!

(6,3.7)

I[-, EXP + TERM] I[-, TERM * FAC] I[-, 1] I [-, (EXP)] I [(, (] I [),)]

Los espacios entre los elementos de pila son para mayor legibilidad, y la I (o) es para ahorrar espacio. Las primeras dos reglas, AEla-b y AE2a-b, representan dos reglas cada uno, AE3a-c representa tres reglas y AE4a-f representa seis. Advierta que las reglas AE1-AE3 son no determinísticas. Ahora veamos el funcionamiento del PDA del listado (6.3.7) sobre la cadena de entrada (1 + 1) * 0. Esto se ilustra en la tabla 6.3.1. Sólo fines educativos - FreeLibros

302

PARTE

ni: Autómatas y lenguajes formales

TABLA 6.3.1 Reconocimiento de (1 + 1) * 0 por PDA(ArithExp) Entrada

Pila

Regla que se aplicará

(1+1)*0#

EXP#

A E la

(1+1)*0#

TERM #

AE2b

(1+1)*0#

TERM * FAC #

AE2a

(1+1)*0# *

F A C * FA C#

AE3c

(1+1)*0#

( E X P ) * FAC #

AE4e

(1+1)*0#

EXP) * FAC)

A E lb

(1+1)*0#

EXP + TERM) * FAC) #

A E la

(1+1)*Ü#

TERM + TERM) * FAC) #

AE2a

(1+1)*0#

FAC + TERM) * FAC) #

AE3b

(1+1)*0# *

1 + TERM) * FAC #

AE4b

(1+1)*0# *

+ TERM) * FAC #

AE4c

(1+1)*0# *

TERM) * FAC #

AE2a

(1+1)*0# *

FAC) * FAC #

AE3b

(1+1)*0# »

1) * FAC #

AE4b

(1+1)*0# *

) * FAC #

AE4f

(1+1)*0# *

* FAC #

AE4d

(1+1)*0#

FAC#

AE3a

(1+1)*0# *

0#

AE4a

(1+1)*0# ¡Éxito!

#

AE5

* * *

* * *

* *

*

El lector que haya trabajado a lo largo de la tabla 6.3.1 se habrá dado cuenta de que el indeterminismo de las reglas AE1-AE3 es una seria desventaja, y que el PDA puede tomar muchas rutas equivocadas antes de reconocer una cadena válida. Griffiths y Petrick presentan un algoritmo selectivo descendente que utiliza una matriz de precedencia generada en forma automática para ayudar a la determina­ ción de qué regla de PDA elegir cuando más de una se puede aplicar. También se ha dado otros algoritmos e informado de sus medidas de eficiencia. Algunos de éstos son ascendentes, la pila iniciada a #, e informan de un éxito cuando la cadena de entrada es agotada y la pila sólo contiene los símbolos de inicio y terminación. Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

303

En un PDA descendente, trabajaríamos a través de FACtores, TERMinos y EXPresiones hasta que una cadena de entrada sea reconocida como una EXPresión simple. Cohén [Cohén, 1991] proporciona un tratamiento extensivo de los PDA, rela­ cionándolos con diagramas de flujo en vez de pares de reglas.

Árboles de análisis sintáctico El análisis sintáctico (parsing), o el reconocimiento de palabras, se concibe a menudo como un árbol. Las ramas del árbol reflejan cuáles reglas de producción fueron aplicadas en el reconocimiento de la cadena. El análisis de a*b2 en la figura 6.3.1 puede ser retratado por el árbol de análisis mostrado en la figura 6.3.2. El primer renglón indica la aplicación de S —» aSb, y la segunda de S ab. Un transversal inorder (izquierda-raíz-derecha), del árbol resulta en aabb como el or­ den resultante de los terminales, como se desea. De manera semejante, el árbol de análisis sintáctico para (1 + 1 )* 0, siguiendo la ejecución del PDA(ArithExp) que se expone en la tabla 6.3.1, se muestra en la figura 6.3.3. Cuando un compilador realiza los pasos del análisis sintáctico sobre EXP

TER M

TERM

FAC

FAC

EXP

EXP

TERM

+

TER M

FAC

S FAC

a

b

S

a

b

FIGURA 6.3.2 Árbol de análisis sintáctico para a2b2

FIGURA 6.3.3 Árbol de análisis sintáctico en ArithExp para (1 + 1) * 0

Sólo fines educativos - FreeLibros

PARTE ni: Autómatas y lenguajes formales

304

un programa, un método común involucra la creación de un árbol de análisis de este tipo. Un árbol así, indica algo acerca de la semántica. Implica precedencia de operadores (que el operador + y la evaluación de las expresiones entre paréntesis vienen primero, y la multiplicación viene después). De aquí, es claro que el valor de la expresión es 0. Además, ¿qué hay acerca de la interrogante de la asociatividad de los operado­ res? Examinemos el árbol de análisis para 0 + 1 + 1, como se muestra en la figura 6.3.4. Nótese que se implica que el operador + izquierdo se aplicará primero, y el + derecho se aplicará más tarde. Como resultado, + se dejará asociativo. De aquí, se evalúa como (0 +1) +1. Se dejará como ejercicio demostrar que * es también asocia­ tivo por la izquierda en ArithExp.

Gramáticas ambiguas Supóngase que en lugar de utilizar las reglas para LN' en el listado (6.3.2), genera­ mos las cadenas de {anbn} empleando las reglas en el listado (6.3.8). S -+ aS2 I Sxb S„ —» a I aSab aS2b

(6.3.8)

El lenguaje generado es el mismo que el de LN', pero dos árboles diferentes re­ presentan análisis de a2b2, como se ilustra en la figura 6.3.5. Cuando una o más cadenas producen dos diferentes árboles de análisis, se dice que la gramática es ambigua.

EXP

E>

EXP

TERM

+

h

TERM

TERM

FAC

FAC

1

FAC

FIGURA 6.3.4 Asociatividad del operador + en 0 + 1 + 1

a

So

b

FIGURA 6.3.5 Dos análisis sintácticos para a V en una gramática ambigua

Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

305

Considere otra vez nuestra gramática ArithExp del listado (6.3.6). Supónga­ se que intentamos simplificar las producciones a la forma mostrada en el listado (6.3.9). EXP -> TERM T E R M - > 0 I 1 I TERM + TERM I TERM * TERM

(6.3.9)

Por desgracia, ahora son producidos dos árboles de análisis para 1 + 1 * 0 utili­ zando estas reglas sin paréntesis, como se ilustra en la figura 6.3.6. Si evaluamos la expresión del árbol izquierdo al usar aritmética ordinaria de base 2 y un recorri­ do transversal inorder del árbol, obtendríamos 1. El resultado para el árbol dere­ cho es 0. Los programas tienen semántica así como sintaxis, y el significado de una de­ claración (en este caso, el valor de la expresión) no debe ser ambiguo. Existe dos maneras comunes para hacer una expresión aritmética no ambigua: insistencia en paréntesis completos en la sintaxis del lenguaje, o el uso de una precedencia de opera­ dor que está integrada en el lenguaje. Se analiza la sintaxis de una expresión y des­ pués se evalúa en orden (de izquierda a derecha), con operaciones realizadas en el orden de su precedencia, con las de mayor jerarquía ejecutándose primero. En Ada, la jerarquía desde lo más alto a lo más bajo es: ** | abs | not * | / | lod | re» + | + | -| & - | /- |< | <= | >| >= and |op | xor

(exponenciación, valor absoluto, negación lógica) (m ultiplicación, división, m ódulo, residuo) (más o menos unitario) (sum a, resta, concatenación de arreglos) (operadores relaciónales) (operadores binarios lógicos)

EXP

EXP

FIGURA 6.3.6

Dos análisis sintácticos para 1 + 1 * 0 en un lenguaje de expresión ambigua Sólo fines educativos - FreeLibros

306

PARTE ni: Autómatas y lenguajes formales

Aplicaciones Las gramáticas libres de contexto tienen muchos usos prácticos, en la medida en que la sintaxis de los lenguajes de programación puede especificarse con el uso de ellas. El primero en utilizar CFG para la definición del lenguaje fue Algol 60, segui­ do por FORTRAN, Pascal, BASIC, PL/I, y por último, Ada, entre otros. Sin embar­ go, cada uno de estos lenguajes no tiene construcciones libres de contexto. Una de éstas es que en los lenguajes tipificados, un tipo de variable debe declararse antes de que sea utilizada en un programa. La descripción BNF por lo regular enuncia una sección de declaración de variable como opcional, para incluir (sub)programas sin variables. Una descripción de lenguaje oficial incluirá algún otro método aparte de BNF para describir tales características. Los compiladores son en particular re­ ceptivos a las CFG con sus implementaciones como pilas, de modo que tantos len­ guajes como sean posibles se definen con el uso de una CFG.

Formas normales Las formas normales son métodos de descripción de lenguaje que siguen ciertas reglas. Uno de sus usos importantes es en la construcción de pruebas acerca de propiedades de lenguaje. Para muchos lenguajes, podemos suponer que se en­ cuentran especificados en forma normal y limitar nuestra prueba a estas construc­ ciones. Las formas normales pueden no ser en particular fáciles de leer o de com­ prender, pero son más sencillas de analizar que descripciones de lenguaje más informales. Cualquier lenguaje libre de contexto puede ser descrito mediante cualquiera de las formas normales que se menciona a continuación.

Forma normal de Chomsky (CNF; Chomsky Normal Form) Se dice que una gramática está en forma normal de Chomsky si todas sus reglas de producción son de una de dos formas: 1. 2.

Nj —>N2N3, en el que N. es una no terminal N —» t, en el cual N es una no terminal y t es una terminal simple

Describamos nuestra CFG para LN' en CNF. Recuerde del listado (6.3.5) que LN' es el lenguaje de palabras de la forma anbn, y su gramática libre de contexto se compone de las dos reglas: (1) S —»aSb y (2) S ab. Una CNF equivalente para esta gramática es: C l) C2) C3) C4) C5)

S -»A C C SB S AB A a B —>b Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

307

Y una derivación de a3b3 es: CI C2 CI C2 C3 C4 C4 S -» AC -> ASB AACB AASBB AAABBB -» aAABBB

.

C5 —> aaabbb En la notación anterior una flecha indica cuál regla fue utilizada para producir el lado derecho a partir del izquierdo. La CNF hace el análisis de lenguaje particularmente fácil, debido a que sólo se tiene que preocupar de las palabras producidas a través de producciones de dos clases. La CFG de dos reglas para LN' fue reescrita en cinco reglas CNF. Esto ocurre con regularidad, de manera que las gramáticas CNF tienden a ser más largas. Forma normal de Backus (BNF; Backus Normal Form) Una forma normal más legible es la Forma Normal de Backus (BNF). La BNF tam­ bién es conocida como Forma Backus-Naur (Backus-Naur Form); aquí se reconoce las contribuciones de Peter Naur como el editor del informe de ALGOL 60, el cual fue escrito en BNF. Como se describió en el capítulo 0, BNF es un metalenguaje utilizado para describir sistemas de producción para generar lenguajes libres de contexto. Cada lenguaje que se crea con el uso de BNF incluye un conjunto de terminales, un con­ junto de no terminales y una lista de producciones. Las terminales de BNF se indi­ can de diversas maneras en diferentes referencias de lenguaje. Utilizaremos una cadena en minúscula y en negritas. Como se muestra a continuación, las no termi­ nales se encuentran encerradas entre picoparéntesis. Los metasímbolos de BNF (como se utilizan en este texto) se muestran en el listado (6.3.10). Sím bolo ::= I

algo

Significado se define como alternativamente no terminal terminal

(6.3.10)

A través del tiem po, BNF se ha extendido a EBNF para hacer más legibles las descripciones del lenguaje al reemplazar algunas definiciones recursivas con otras iterativas, como se muestra en el listado (6.3.11). Sím bolo [algo] {algo} (esto I eso)

Significado cero o una ocurrencia de algo; es decir, opcional cero o más ocurrencias de algo agrupación; ya sea de esto o eso

(6.3.11)

Haremos referencia al capítulo 0, BNF y EBNF, por los ejemplos de uso de cada uno de estos símbolos. Sólo fines educativos - FreeLibros

308

PARTE III:

Autómatas

y

lenguajes formales

Una definición BNF recursiva para un identificador de Pascal es:

::= I I En EBNF podríamos escribir de manera no recursiva: ::= I {letra I digito} La definición EBNF de Ada para una declaración 1f se muestra en el listado (6.3.12):7 if_statement ::= if condi tion then sequence_of_statements {elseif condition then sequence_of_statements} [else sequence_of_statements]

(6.3.12)

end if;

En CNF, esto puede ser bastante largo. Podríamos comenzar como en el listado (6.3.13): IS - > I T P I

TP etc.

(6.3.13)

-> 1f

C TS

Examinaremos LN' = {anbn I n = 1,2 ...} del listado (6.3.2), definido en BNF.

::= ab I ab Note el uso recursivo de . Para derivar a3b3, usaríamos la definición tres veces: -» ab —» aabb —> aaabbb siendo ab la sustitución final para . BNF tiene una ventaja más aparte de hacer definiciones precisas y ayudar al análisis del lenguaje. Impone una estructura sobre las palabras que ayudan a la construcción de un reconocedor, como se ilustra en la figura 6.3.7. Los métodos para el recorrido de los árboles están bien desarrollados. Para reconocer a3b3, po­ dríamos recorrer el árbol desde la parte inferior aaabbb hacia las superiores , o de arriba hacia abajo.

7En el Manual de Referencia del Lenguaje Ada las terminales son escritas con un tipo en negritas minúsculas, y las no terminales con tipo simple.

Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

309

i a

b

ab

i

i aa

bbaabb

i

i aaa

bbb

aaabbb

FIGURA 6.3.7 Árbol de análisis sintáctico de BNF

LABORATORIO 6.2:EBNF: PAPEL Y LÁPIZ

Objetivos (Los Laboratorios pueden encontrarse en el Instructor1s Manual) 1. Utilizar la forma EBNF como un generador de lenguaje; para esto, debe usarse las definiciones EBNF para lenguajes existentes como Pascal o Ada. 2. Reescribir las definiciones EBNF como una gramática libre de contexto. 3. Construir un autómata descendente (PDA) para reconocer los fragmentos de len­ guaje generados por el punto 1 de este segmento. 4. Programar el punto 3, si el instructor lo desea y se dispone de tiempo.

D iagramas de sintaxis Las formas normales todavía pueden ser difíciles de leer y comprender por las personas no entrenadas en la lógica matemática. Diagramas de sintaxis equivalentes a las formas pueden incluso ser utilizadas por programadores novatos. Un diagra­ ma de sintaxis para anbn se ilustra en la figura 6.3.8. Nótese la recursión aquí. Aquí definimos AnBn, como se lista en el encabezado para el diagrama, y AnBn se pre­ senta en el diagrama mismo. No es difícil ver que AnBn toma el lugar del símbolo de inicio S correspondiente a la regla de producción S —» ab I aSb.

AnBn a

AnBn

FIGURA 6.3.8 Diagrama de sintaxis para anbn

Sólo fines educativos - FreeLibros

310

PARTE ni: Autómatas y lenguajes formales

E J E R C I C I O S 6.3 1. Las cadenas de la forma anbn (por ejemplo, a2b2, a3b3, etc.) no pueden ser generadas desde una gramática libre de contexto (CFG). Vea si usted puede deducir el porqué al intentar utilizar el NFA. 2. a. Diseñe un FA para reconocer cadenas de la forma anbn (n 3). ¿Funcionará este método para cualquier cadena anbn en la cual (n k) para una k fija? b. ¿Por qué el método no funcionará para una n arbitraria? 3. a. Sea A = (X, N, P, S) en el que X= { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , +, - , =, X},N = {NUM, VAR, NEGNUM}. Escriba reglas libres de contexto para producir palabras que son ex­ presiones de adición o suma tales como X = - 2 + 6. (Nótese que utilizamos el símbolo - de dos maneras: un - unitario, como en -2 , y un - binario, como en 5 - 2 . Los espacios son para facilitar la lectura, pero usted no necesita incluirlos en sus reglas.) b. ¿Podría producirse el mismo lenguaje usando una gramática regular? ¿Por qué o por qué no? c. Vuelva a escribir su gramática para incluir espacios de modo que X = 2 + 6 sea una palabra legal. 4. a. Extienda su gramática del ejercicio 3a para incluir números de cualquier longitud tales como -1230, 546682, etcétera. Asegúrese de eliminar los 0 al comienzo; es decir, su gramática no deberá producir 0053 como un NUM. b. ¿Piensa usted que este lenguaje podría ser producido a partir de una gramática regular? ¿Es esta respuesta diferente de 3b? ¿Por qué o por qué no? 5. Extienda su gramática del ejercicio 4 para incluir sumas de más de dos números. Intente esto en dos formas: con y sin paréntesis. 6. Extienda la gramática del ejercicio 5 para incluir multiplicación. Dos palabras pro­ ducidas bien podrían ser X = 4 + (2 * 3) y X = (4 + 2) * 3. 7. Considere la siguiente gramática libre de contexto para generar frases en inglés. ({the, a, man, boy, ball, hit, saw, said, believed), {S, NP, VP, DET, N, VT, VS), P, S), P: S -» NP VP NP -» DET N VP VT NP VP -> VS S

DET N VT VS

-> the ! a -» man I boy I ball —> hit I saw -4 said I believed

¿Es "The ball believed the man hit the boy" (La pelota creyó que el hombre bateó el niño) una frase válida? Si es así, vuelva a escribir la CFG para eliminarla. 8. Considere la gramática GI = ({a, b, c, d}, {S, A, B¡, P, S): P: 1. S -» A B 2. A —> a 3. A -> ABb

4. B be 5. B —> Bd

a. Demuestre que abed e L(G1). b. Pruebe que GI es libre de contexto. c. Construya un PDA = (X*, N u X*, Inicio, #, {>, -) , R), que reconozca palabras de L(G1) utilizando el algoritmo NTB del listado (6.3.4). d. Demuestre que el PDA que construyó en 8c reconoce abed. 9. Demuestre que l + l * 0 e L(ArithExp) utilizando el PDA del listado (6.3.7).

Sólo fines educativos - FreeLibros

CAPITULO 6: Lenguajes formales

311

10. Verifique que la multiplicación * es asociativa por la izquierda para la gramática ArithExp del listado (6.3.6). 11. Suponga que reemplazamos las reglas de producción de ArithExp del listado (6.3.6) con: P': 1. EXP -> TERM I TERM + TERM 2. TERM -> FAC I FAC * FAC 3. FAC - > 0 1 1 1 (EXP) Llame a esta gramática ArithExp2. Con el uso de árboles de análisis: a. ¿Está (1 + 1) * 0 en L(ArithExp2)? b. ¿Se evalúa (1 + 1) * 0 del mismo modo que en ArithExp? c. ¿Está 0 + 1 + 1 en L(ArithExp2)? d. ¿Está 0 + (1 + 1) en L(ArithExp2)? 12. Termine la definición CNF del listado (6.3.13) para una if_statement (declaración_if) de Ada. Con el fin de que sea breve, omita las definiciones CNF para las condiciones y para una sequence_of_statements (secuencia_de_declaraciones). 13. Construya un diagrama de sintaxis para una if_statement (declaración_if) de Ada. La EBNF se muestra en el listado (6.3.12).

6.4

GRAMÁTICAS PARA LOS LENGUAJES NATURALES Los seres humanos se comunican con lenguajes naturales, pero pocos de estos len­ guajes se pueden caracterizar con suficiente precisión para definir una gramática que genere todas las oraciones o frases válidas. Las declaraciones arbitrarias no siempre pueden ser analizadas para facilitar su comprensión. Sin embargo, ade­ más de las gramáticas estructuradas en frases, tanto las técnicas libres de contexto como las sensibles al contexto prueban ser útiles en la comprensión de los lengua­ jes naturales en algunas configuraciones bastante restringidas. Es la ambigüedad del lenguaje cotidiano sin restricciones lo que impide el desarrollo de dispositivos prácticos de reconocimiento de voz. Relacionados muy de cerca a los autómatas finitos (FA), que describimos como reconocedores para lenguajes regulares, son las redes de transición recursiva (RTN; Recursive Transition Netiuorks). Éstas pueden usarse como generadores de lenguaje libre de contexto equivalentes a CFG. Difieren de los FA en que permiten etiquetar arcos que hacen referencia a otras redes. Por ejemplo, la CFG para generar cadenas de la forma anbnes: S —>ab I aSb. Un RTN equivalente se muestra en la figura 6.4.1.

s F I G U R A 6.4.1 RTN para generar anbn

Sólo fines educativos - FreeLibros

312

PARTE III:

Autómatas y lenguajes formales

La diferencia aquí de un NFA es que el arco etiquetado S se presenta a la mitad de la RTN. Eso significa que la red completa S está por ser insertada para la etique­ ta S. Se le solicitará a usted investigar un NFA relacionado en el ejercicio 6.4.2. Una simple CFG para generar frases es la siguiente: S <- NP VP SUSTANTIVO <- cat(s) I rat(s) ! dog(s) VP ^ VERBO NP ARTICULO <- the la NP SUSTANTIVO VERBO ate I meowed NP ARTICULO SUSTANTIVO en el que S (Sentence) significa declaración u oración, VP (Verb Phrase) significa frase verbal, NP (Noun Phrase) viene de frase sustantiva y ART es por artículo. Oraciones tales como "The cat ate a rat" (El gato comió una rata) pueden generarse con ella, pero también se puede "A cats ate a rats" (Un gatos comió un ratas) o "Cats meowed dogs" (Los gatos maullaron perros). Se necesita un poco de más información correcta para generar oraciones significativas. Aquí, las RTN aumen­ tadas pueden ser de ayuda. Nótese que las flechas en las reglas de gramática están al revés de las que hemos visto antes. Esto sugiere un análisis de oración de abajo hacia arriba, el cual es uno de los primeros pasos en la comprensión del lenguaje natural (véase la figura 6.4.2). Un método que ha probado su utilidad en la automática comprensión de len­ guaje es la red de transición aumentada (ATN; Augmented Transition Network). Aquí la RTN es aumentada mediante pruebas de cada arco, lo que garantiza el acuerdo entre sustantivos y adjetivos, sujetos y verbos, verbos y auxiliares, etcétera. Una red de transición también debe ser aumentada para manejar complementos de ver­ bo, tal como frases con adverbios e infinitivos. Las ATN pueden ser utilizadas para generar y reconocer oraciones gramati­ calmente correctas, pero su significado es otra actividad compleja. Considere los diversos significados de rompió:

NP

ARTICULO (ART)

VP

SUSTANTIVO (NOUN)

I

I

The

cat (gato)

(El)

V ER BO (VERB)

NP

ate I (comió) ARTICULO (ART) a (una)

FIGURA 6.4.2 Árbol de análisis sintáctico de oración

Sólo fines educativos - FreeLibros

SUSTANTIVO (NOUN) rat (rata)

CAPÍTULO

1. 2. 3.

6: Lenguajes formales

313

Alex rompió el vidrio con una roca. La roca rompió el vidrio. El vidrio se rompió.

Agregue un adverbio al número 1: "Alex rompió intencionalmente el vidrio". ¿Tie­ ne sentido "La roca rompió intencionalmente el vidrio" o "El vidrio intencionalmente se rompió"? Sólo las entidades animadas tendrían "intención". Las redes semánticas han sido desarrolladas para tratar con preguntas tales como éstas y muchas otras más. Nos enfrentaremos con algunas de estas clases de relaciones cuando discuta­ mos los lenguajes declarativos en la parte VI de este texto. E J E R C I C I O S 6.4 1. A continuación tenemos tres frases ambiguas. ¿Puede hallar al menos dos significa­ dos para cada una? ¿Los dos significados se analizan de manera diferente? a. I hate visiting relatives" (Odio visitar relativos/parientes) b. " 5 * 3 + 2" c. "H ere's to my last wife" (¡Aquí está/tiene para mi última esposa!) 2. Analice las tres oraciones utilizando el verbo "broke" (rompió) (discutido antes) de una manera similar a la de la figura 6.4.2. 3. ¿Cuál es el lenguaje reconocido por el NFA mostrado en la figura siguiente? ¿Cómo difiere de la RTN de la figura 6.4.1? ¿Qué tipo de lenguaje es reconocido?

6.5

RESUMEN Se examinaron ya cuatro tipos de lenguajes formales. Las gramáticas para generar lenguajes de Tipos 0 a 3 de Chomsky son llamadas estructuradas por frases. Una gramática para generar un lenguaje es un sistema G = (X, N, P, Inicio), en el que X es el conjunto de símbolos terminales a partir del cual las cadenas se construyen, N es un conjunto de no terminales, S es un símbolo de N denominado el símbolo de inicio, y P es un conjunto de reglas de producción de la forma a —»/3, en el cual a (o parte de ella) se va a reemplazar por fí (o parte de ella). El Tipo 0 es el más general, e incluye a todos los otros. Las producciones para estas gramáticas no están restringidas, excepto que si or-+ fi es una producción, alfa debe contener por lo menos una no terminal. Los lenguajes Tipo 0 también son llamados recursivamente enumerables (r.e.), lo que significa que existe alguna fun­ ción f(n) de los números naturales para el alfabeto X, el cual generará cadenas váli­ das. Las cadenas pueden ser de longitud infinita. Los matemáticos sostienen en forma amplia que cualquier función que pueda ser efectivamente calculada es r.e. Las cadenas de los lenguajes Tipo 0 pueden ser reconocidas por Máquinas de Turing (MT). Sólo fines educativos - FreeLibros

314

PARTE III:

Autómatas y lenguajes formales

Los lenguajes del Tipo 1 son generados por gramáticas sensibles al contexto (CSG). Las producciones están restringidas en la longitud de a pero deben ser me­ nores que o iguales a la longitud de /?. Ellas pueden ser reconocidas mediante autó­ matas lineales limitados (LBA), los que se detendrán en un tiempo proporcional a la longitud de una cadena de entrada. Las gramáticas libres de contexto (CFG) son del Tipo 2 de Chomsky. Pueden ser generadas al utilizar reglas de producción con una sola no terminal en el lado izquierdo. Los lenguajes libres de contexto son reconocidos mediante autómatas descendentes (PDA). Estos son los más importantes para la construcción de analizadores para compiladores, puesto que muchos lenguajes de computadora puede ser casi definidos por completo mediante una gramática libre de contexto. Aunque no lo hemos discutido aquí, la construcción de árboles de análisis sintáctico a partir de PDA es directa. Las formas normales de Backus (BNF) y sus extensiones son equivalentes a las gramáticas libres de contexto. Las BNF y EBNF (BNF exten­ dida) son ampliamente utilizadas para especificar reglas de lenguaje libre de con­ texto. Los estándares de Ada y Pascal están escritos en EBNF, con aquellas partes que no son libres del contexto descritas en lenguaje natural ordinario. Los lenguajes regulares son libres de contexto, pero pueden ser generados por las reglas de producción de la forma N —> t, o N 1 tN2, en el que N. es una no terminal. Cualquier lenguaje con un número finito de palabras es regular y puede ser reconocido mediante un autómata finito (FA). Algunos analizadores están ba­ sados en FA para una verificación inicial para la validación de tokens, PDA para una verificación secundaria de la sintaxis de la declaración, y algún otro proceso para declaraciones que no son ni regulares ni libres de contexto.

6.6

NOTAS SOBRE LAS REFERENCIAS En el texto se ha tratado a los lenguajes formales en una forma muy breve. Un curso en ciencia de la computación teórico consideraría pruebas de gran parte del material que se ha mencionado aquí. Nos hemos referido al legible texto elemental de Daniel Cohén [Cohén, 1991]. Sería una buena elección para agregar a su biblio­ teca. El texto de Lewis y Papadimitriou [Lewis, 1981] trata el tema en un contexto un poco más avanzado. También Hopcroft y Ullman [Hopcroft, 1979] proporciona un buen tratamiento. El libro clásico acerca de la teoría de funciones recursivas es [Rogers, 1967]. Aquellos interesados en el uso práctico de estas técnicas en lenguajes de pro­ gramación se beneficiarían de un libro sobre diseño de compiladores. Aho, Sethi y Ullman [Aho, 1986] es el estándar en esta área. Pittman y Peters [Pittman, 1992] es en particular legible y ofrece un hermoso tratamiento de los rastreadores y analizadores. Las Máquinas de Turing se consideran en muchos textos de lógica matemática; por ejemplo [Mendelson, 1979]. El uso de las Máquinas de Turing para construir reconocedores libres de contexto continúa recibiendo atención. Véase [Griffiths, 1965] para los primeros trabajos, y [Graham, 1980] para algo más reciente. Ambos artículos contienen extensas bibliografías. La biografía de Hodges [Hodges, 1983] Sólo fines educativos - FreeLibros

CAPÍTULO

6: Lenguajes formales

315

de Alan Turing es notable por su combinación de comentarios sensibles acerca de una vida problemática con una correcta exposición científica. Para una breve pero meritoria reseña de Hodges, véase [Hofstadter, 1985b]. Un libro pequeño bien escrito para no especialistas es Gódel's Proof(La prueba de Gódél) [Nagel, 1958]. Como dice una nota en la portada interior: "En 1931 Kurt Godel publicó un artículo revolucionario: uno que desafió ciertas nociones básicas subyacentes en mucha investigación tradicional en matemáticas y lógica. En la ac­ tualidad su exploración de esa térra incógnita ha sido reconocida como una de las mayores contribuciones al pensamiento científico moderno... Ofrece a cualquier persona educada con un gusto por la lógica y la filosofía la oportunidad de satisfa­ cer su curiosidad intelectual acerca de un tema antes inaccesible." La comprensión del lenguaje natural implica con frecuencia a los lingüistas. Un buen lugar para comenzar con la literatura es a través de la Synthese Language Library publicada por D. Reidel. Una colección que inspecciona las tendencias ac­ tuales es el volumen 15 de The Nature ofSyntactic Representation (La naturaleza de la representación sintáctica) [Jacobson, 1982].

Sólo fines educativos - FreeLibros

P AR T E IV

Lenguajes declarativos

A diferencia de un lenguaje imperativo, que nos permite escribir una secuencia de comandos en una computadora, un lenguaje declarativo facilita la escritura de de­ claraciones, o verdades. En contraste con los cuatro comandos, mostrados en la página de presentación de la parte II, que se usaron para almacenar “Jack el D e s t r i pador” en una localidad de memoria particular, la declaración simple, (cons 'Jack ( U s t ‘el *Des tr i p a d o r ))

establece que la función cons construye una expresión de la literal ‘ Jack y una lista, ‘ ( el D e s t r i pador), producida por la función Ust. El sitio donde se almacena el resultado en la memoria se deja al lenguaje particular que se utilice. Los lenguajes declarativos se consideran de más alto nivel que los lenguajes imperativos, debido a que un programador que utiliza lenguajes declarativos ma­ neja conceptos en lugar de localidades de memoria en la máquina misma. En el capítulo 7 examinaremos los lenguajes basados en la lógica, mientras que en el capítulo 8 consideraremos aquellos basados en la noción matemática de una función, la que opera sobre sus argumentos para producir un solo valor. En el ejem­ plo anterior, los argumentos son ' J a c k y ' ( e l D e s t r i p a do r), y el valor producido por la función cons es ‘ ( Jack el D e s t r i pador). El capítulo 9 hace una breve consi­ deración acerca de los lenguajes para bases de datos, basados en la manipulación de tupias ordenadas, llamadas relaciones.

Sólo fines educativos - FreeLibros

CAPÍTULO 7 PROGRAMACIÓN LÓGICA 7.0 En este capítulo 7.1 Sistemas lógicos formales Viñeta histórica: Aristóteles Demostraciones o pruebas Resolución Unificación Búsqueda Retroceso Hechos, metas y condiciones Encadenamiento hacia atrás y hacia adelante Representación de hechos negativos Ejercicios 7.1 7.2 PROLOG Viñeta histórica: PROLOG: Colmerauer y Roussel Conversando en PROLOG: hechos, reglas y consultas

319 320 320 322 322 328 329 330 332 332 333 334 337 337

Sintaxis Estructuras de datos Operadores y functors integrados Control Implementaciones de PROLOG Una máquina teórica Arquitecturas paralelas Recolección de basura Tipos y módulos Aplicaciones Inteligencia artificial Bases de datos relaciónales La quinta generación Fortalezas y debilidades Ejercicios 7.2 7.3 Resumen 7.4 Notas sobre las referencias

338

Sólo fines educativos - FreeLibros

339 340 341 345 349 349 352 353 353 354 354 355 355 356 357 359 361

CAPÍTULO

7

Programación lógica

La lógica es la ciencia del razonamiento, y como tal, incluye metodologías forma­ les que son útiles para resolver problemas aparte de los que se resuelven por intui­ ción, asunto de fe o compromiso. Tales métodos no lógicos son empleados para llegar a soluciones aceptables para muchos problemas sociales, pero con frecuencia no pueden ser traducidos en algoritmos utilizables para programas de compu­ tadora. Durante el primer cuarto del siglo XX, se creía que todas las matemáticas así como el razonamiento verbal formal podrían ser expresados en un sistema formal de lógica. Bertrand Russell y David Hilbert trabajaron de manera independiente para demostrar que esto era así, pero ambos investigadores se decepcionaron con el tiempo. No obstante, un sistema lógico incluye suficientes matemáticas para ha­ cerlo un razonable fundamento teórico para un lenguaje de programación.

7.0 EN EST E C A PÍT U LO Suponemos que el lector de este capítulo conoce algo acerca del cálculo de declara­ ciones que se denomina en ocasiones el cálculo proporcional, y también el cálculo de predicados, que incluye variables cuantificadas en las proposiciones. Si no fuera así, el Apéndice A contiene suficiente material acerca de estos cálculos para com­ prender el material que presentamos a continuación. Los temas principales del capítulo incluyen: • • • • • •

Demostración por el método de resolución Unificación de variables Administración de una búsqueda de base de datos a través de retroceso Razonamiento a través de encadenamiento hacia atrás o hacia adelante El lenguaje PROLOG Inteligencia artificial con el uso de PROLOG Sólo fines educativos - FreeLibros

320

PARTE IV:

Lenguajes declarativos

Bases de datos relaciónales en PROLOG Implementaciones de PROLOG

7.1

SISTEMAS LÓGICOS FORMALES El primer sistema lógico conocido, el cual se le atribuye a Aristóteles durante el siglo IV a. de C., incluía leyes de deducción basadas en proposiciones de cuatro formas posibles. "Todos los estudiantes trabajan duro" y "algunos estudiantes co­ men mucho" son ejemplos de dos de las formas. Las reglas de deducción, entonces, nos permiten establecer que "algunos que comen mucho trabajan duro".

VIÑETA HISTÓRICA Aristóteles Fue estudiante de Platón, tutor de Alejandro el Magno, y un prolífico autor que escribió acerca de virtualmente todos los campos de estudio conocidos en esa épo­ ca, Su trabajo trataba acerca de temas tan diversos como lógica, política, economía, biología, física, meteorología, ética, psicología y teología. Su nombre es Aristóteles, y fue uno de los más grandes filósofos del mundo griego antiguo. Aristóteles nació en la colonia jónica de Stagira en Macedonia en el año 384 a. de C. Randall dice que "todos los principales filósofos griegos, con la excepción de Sócrates y Platón, habían sido jónicos" [Randall, 1960], Aunque el padre de Aristóteles es conocido como Nicomacus, médico del Rey Amintas, se rumoraba que su progenitor real era el dios de la sanación y la medicina y que su abuelo era Apolo, dios de la razón y del Sol [Randall, 1960]. Aristóteles fue enviado a la edad de diecisiete años a Atenas para estudiar en la Academia. Allí recibió el adiestramiento de Platón y con rapidez se estableció a sí mismo como "la mente de la escuela" y "el lector". Él no estaba de acuerdo con algunas de las doctrinas de Platón, pero fue influenciado en mucho por el trabajo de este filósofo, en especial durante sus primeros años. Sus escritos durante este periodo reflejan la influencia de Platón en temas tales como la inmortalidad del alma, la retórica, la justicia y la idea de la bondad pura. Sin embargo, Aristóteles era crítico severo del maestro Sócrates. "Él compartía el desprecio de Platón hacia la pobreza de pensamiento de Sócrates y por su eleva­ ción del éxito oratorio sobre la búsqueda de la verdad" [Ross, 1923]. Esta crítica lo hizo enemigo de la Escuela socrática. Cuando Platón murió en 348-7 a. de C., dejó la dirección de la academia a su sobrino, Espeusipo, aun cuando consideraba que Aristóteles era su mejor estudiante. Algunos dicen que ésta fue la razón que llevó a Aristóteles a abandonar la acade­ mia. Otros afirman que fue debido a la presión de Espeusipo sobre las matemáti­ cas, mientras que otros incluso plantean la hipótesis de que Aristóteles no había sido aceptado por completo en la sociedad. Sólo fines educativos - FreeLibros

CAPÍTULO 7: Programación lógica

321

Aristóteles pasó varios años estudiando plantas y animales a lo largo de la costa de Asia Menor antes de viajar a Macedonia para asumir el puesto de tutor del hijo de trece años de edad del Rey Filipo, Alejandro. Cuando el muchacho llegó a ser rey, Aristóteles regresó a Atenas para establecer su propia escuela en el Liceo. La escuela fue llamada "la Peripatética" debido a que Aristóteles a menudo caminaba y habla­ ba con sus estudiantes en los jardines del Liceo. Las mañanas eran dedicadas a la lógica; las tardes, a la retórica, política y ética. Aristóteles estableció que la lógica, originalmente llamada analítica, "no es una ciencia sustantiva, sino una parte de la cultura general que todos deberían experimentar antes de estudiar ciencias; y que sola capacita para saber para qué clase de proposiciones se debería demandar una prueba y qué clase de pruebas se deberían exigir para ellas" [Ross, 1922]. La inspiración de Aristóteles para desarrollar la lógica fue ion deseo para esta­ blecer o formular el patrón matemático presente en todas las ciencias. Él definió la ciencia como "una serie de proposiciones incontestablemente verdaderas de las que puede afirmarse que caen dentro de dos clases. A la primera clase pertenecen los principios básicos o axiomas; es decir, las proposiciones notables cuya verdad es tan evidente que no son capaces o no tienen necesidad de una prueba. A la se­ gunda clase pertenecen las proposiciones o teoremas; es decir, las proposiciones cuya verdad puede ser demostrada basándose en la verdad de los axiomas" [Scholz, 1961]. El modelo para estas proposiciones fue la geometría griega. El trabajo más grande de Aristóteles en el campo de la lógica es el Organon, que se compone de varios volúmenes. El cuerpo principal del trabajo trata con diferentes tipos de de­ claraciones y sus propiedades y relaciones lógicas. Cuando Alejandro murió en el 323 a. de C., se propagaron sentimientos antimacedónicos. Las conexiones macedónicas de Aristóteles, junto con la enemis­ tad de los socráticos, lo hicieron una probable víctima. Se retiró a Chalcis donde falleció el siguiente año a la edad de 62 años. Se le recuerda no solamente por su genio intelectual, sino también por su carácter y naturaleza afectuosos.

Las reglas de deducción de Aristóteles se encontraron inadecuadas para muchas proposiciones, y un sistema lógico algo diferente fue formalizado más tarde. Es conocido como el cálculo proposicional (CP) porque proporciona reglas para calcular los valores de verdad de las proposiciones, que son simplemente frases declarativas o declaraciones. En este sistema, a cualquier proposición debe ser asignado el valor TRUE (verdadero) o FALSE (falso). No hay MAYBE (quizá). Por ejemplo, de las dos proposiciones, p: Bruto asesinó a César q: Casio asesinó a César. podemos construir la proposición, r: P o q expresando la noción que Bruto asesinó a César, o Casio asesinó a César, o posible­ mente ambos. Entonces el valor de verdad de r sería calculado en CP como TRUE (Valor(r) = TRUE, si Valor(p) = TRUE o Valor(q) = TRUE). Sólo fines educativos - FreeLibros

322

PARTE IV: Lenguajes declarativos

Demostraciones o pruebas Una teoría es un conjunto de axiomas, suposiciones y tesis o teoremas demostrables a partir de ellos. Revisaremos los axiomas lógicos y las teorías de los cálculos proposicional y de predicados en el Apéndice A. Los axiomas lógicos por lo regular se aceptan sin nuevo planteamiento para una teoría. En ocasiones las suposiciones también son llamadas axiomas, como en los axiomas de Peano para la teoría de la aritmética de todos los números. Una declaración que se expresa en una teoría se conoce como una hipótesis, hasta que se prueba su veracidad, cuando es renom­ brada como una tesis de la teoría. Existe varios métodos de demostración. Algunos comienzan con axiomas y suposiciones que se suponen ciertas para la situación práctica, y se procede a tra­ vés del uso repetido de reglas de inferencias para otras tesis, las cuales son enton­ ces verdaderas dadas las suposiciones. En el cálculo de predicados (CP), utilizando los axiomas lógicos de Principia Mathematica (PM), subsisten dos reglas de inferen­ cia, modus ponens y reemplazo uniforme. El Modus Ponens establece que si las dos proposiciones, a y p, son verdaderas, donde p es "si a es verdadera, entonces impli­ ca b ", por lo tanto b es verdadera. El reemplazo uniforme requiere que si en una proposición s reemplazáremos una variable libre X con alguna otra, digamos Y, entonces todas las ocurrencias libres de X en s también deben ser reemplazadas con Y. De nueva cuenta, se remite al lector al Apéndice A para las explicaciones de estas reglas. Otros métodos comienzan con la hipótesis que debe ser probada y proceden en sentido inverso hacia los axiomas y las suposiciones. Otro método es probar o de­ mostrar por contradicción, donde la hipótesis p que debe probarse se supone falsa. Los axiomas del CP nos aseguran que si p es falsa, entonces la negación de p será verdadera. Una cadena de inferencias procede desde la negación de p hasta que se llega a una contradicción. Una proposición es contradictoria si puede probarse que es al mismo tiempo verdadera como falsa, en cuyo caso se ha demostrado una contradicción. Puesto que se supone que una teoría es consistente, es decir, libre de contradicciones, y ya que la suposición de la negación de p nos lleva a una contra­ dicción, p debe ser verdadera. Existe, sin embargo, otro método que es más ade­ cuado para su solución por computadora. Resolución En 1965, J. Alan Robinson publicó un artículo en el Journal o f the Association fo r Computing Machinery demostrando un nuevo principio llamado resolución, el cual es un simple proceso que no incluye otros axiomas que reglas lógicas, y que es un sistema lógico de primer orden completo y consistente. El orden de un sistema depende de cuáles sustituciones sean permitidas para variables. "X asesinó a Cé­ sar" es una proposición de primer orden si sólo constantes (individuales) o expre­ siones que evalúan a constantes y no otras proposiciones, pueden ser sustituidas por X. La resolución sostiene similitudes con el método de reductio de demostra­ ción por contradicción en el Apéndice A. En su modo más simple, la resolución trabaja de esta forma: suponga que esta­ mos interesados en p o q: "Bruto asesinó a César o Casio asesinó a César." Esta Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

323

proposición puede ser derivada o resuelta para inferir p: "Bruto asesinó a César" si también tenemos el hecho ->q:x "-«Casio asesinó a César", p: "Bruto asesinó a Cé­ sar" se denomina el resolvente. Esta resolución puede ser escrita de manera simbó­ lica en tres formas equivalentes, como se ilustra en el listado (7.1.1). poq 2E

q

-,q - » P 2P

(p o q ) y ->p

(7.1.1)

q

q

Decimos que q es una consecuencia lógica de los hechos pt, p2, p n si cualquiera de todas las p. son interpretadas como verdaderas, así como es q. Esto no es nada más que modus ponens. El teorema aplicable a la resolución como se usa para programación lógica es: Teorema de resolución: q es una consecuencia lógica de px, p2,. . pn, si (-«q y p: y p2 y ... y pn) es FALSE (falsa). En la notación del listado (7.1.1), y utilizando la forma "o " de (-«q y pt y ... y pn), q es verdadero si se mantiene la derivación del listado (7.1.2). q o -.pj o -.p2 o ... o ->pn

(7.1.2)

2 3 ____________________ FALSE Por ejemplo, suponga que queremos probar que q: "Harry es hermano de Larry" es una consecuencia de: Px: P2: P3: P4: P5:

Joe es el padre de Harry. Mary es la madre de Harry. Joe es el padre de Larry. Mary es la madre de Larry. Dos muchachos son hermanos si tienen la misma madre y el mismo padre.

De manera formal, queremos probar que q 4-p1& p2 & p3 & p4 & p5,2y luego utilizar modus ponens para derivar q de la suposición de la verdad de p4& p2 & p3 & p4 & p5. El teorema de resolución sugiere que nuestra estrategia es demostrar que: - q & P l &p2 &p3 &p4 &p5

(7.1.3)

es falsa; es decir, supondremos que ellos (Harry y Larry) no son hermanos.

1 Utilizaremos la abreviación "~>p" para representar la negación de (p). 2Escribir la flecha de implicación al revés ha llegado a ser una costumbre en la programación lógica para enfatizar el objetivo o meta de una demostración o prueba; es decir, lo que va a probarse a la izquierda mediante lo que implican las premisas de la derecha. De este modo A B & C debería leerse, "A es verdadero si tanto B como C son verdaderos".

Sólo fines educativos - FreeLibros

324

PARTE IV: Lenguajes declarativos

Esto es fácil de hacer si volvemos a escribir p5 sustituyendo Harry y Larry por los dos chicos. Entonces tenemos: P5': Harry y Larry son hermanos si tienen la misma madre y el mismo padre. Que ellos tengan la misma madre y el mismo padre se demuestra utilizando desde p 1hasta p4. De modo que tenemos q <—p1 & p2 & p3 & p4 & p5'. El teorema de resolución dice que: Pj & p2 &p3 & p4 & p5* -iq

tesis suposición

FALSE

contradicción

q

resolución

(7.1.4)

Una proposición tal como (7.1.3) puede ser escrita en la forma normal, o también conocida como disyuntiva, como exploraremos en el ejercicio A.2 del Apéndice A. Así, q <—pj & p2 & p3 & p4 & p5' puede escribirse de manera equivalente como: q o i p j o -.Pj o -npj o - P 4 o

--P s '

(7.1.5)

Esta última proposición, que contiene seis disyuntivas, en la que sólo una de las cuales es positiva, se conoce como Cláusula de Horn. Si además permitimos el cuantificador universal FORALL (para todos), se dice que las proposiciones están en la forma de Cláusula de Hom extendida. Es la resolución de las Cláusulas de Horn extendidas la que forma la base de la programación lógica pura, de la cual PROLOG es una implementación mejorada. La resolución no necesita ser confinada a encontrar una simple disyunción o hecho como se muestra en el listado (7.1.2). El principio de resolución general esta­ blece que: SI o q o S2 S3 o “>q o S4

(7.1.6) se resuelve para:

S I o S2 o S3 o S4,

para cualquier SI, S2, S3 y S4.

Como un ejemplo, suponga que tenemos en nuestra base de datos las inferencias: en_prision <—crimen_cometido & es_capturado. ->en_prision -ies_capturado.

(7.1.7)

Éstas pueden ser escritas como las cláusulas de Horn: Cl:en__prision o -


(7.1.8)

Utilizando dos veces el principio de resolución del listado (7.1.6), C1 y C2 se resuelven para inferir la cláusula C3: Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

Cl:en_prision o -icrimen_cometido o -<es_capturado. C2:-ien_prision o es_capturado.

325

(7.1.9)

C3:-«crimen_cometido o *-<es_capturado o es__capturado. C3 no agrega nueva información porque es siempre verdadera (¿por qué?), de modo que no la agregamos a la base de datos. Suponga que agregamos C4: crimen_cometido, C5:es_capturado, y C6:->en_prision a la base de datos. Cl:en_prision o ->crimen_cometido o -ies_capturado. C4:crimen_cometido.

(7.1.10)

C7:en_prision o ->es_capturado. C5:es_capturado. C8:en_prision. C6:"ien_prision. FALSE Nuestra base de datos es ahora: Cl:en„prision o --crimen_cometido o -<es_capturado. C2:->en_prision o es„capturado. C7:en_prision o ->es_capturado. C4:crimen_cometido. C5:es_capturado. C6:->en„prision. C8:en_prision.

(7.1.11)

El listado (7.1.11) muestra una base de datos inconsistente. (Es obvio aquí que la inconsistencia es la inclusión de las dos cláusulas C6 y C8. Nadie puede estar en prisión y fuera de ella al mismo tiempo. El listado (7.1.10) muestra su resolución como falsa. La resolución es refutación (contradicción) completa lo que significa que falso siempre es derivable de una base de datos inconsistente. La resolución es también correcta, lo que significa que falso solamente se derivará de una base de datos inconsistente. Estas propiedades conducen a un proceso de consulta de la base de datos. Suponga que enviamos la consulta, crimen_cometido. Esto es equi­ valente a agregar en forma temporal la cláusula -*crimen_cometido a la base de datos. Esto se resuelve para el listado (7.1.12). crimen_cometido -icrimen_cometido

(7.1.12)

FALSE De este modo, estamos buscando una derivación vacía (falsa) de una base de da­ tos consistente para responder a una consulta. ¿Cuando consultamos la base de datos por crimen_cometido?, la estrategia de resolución es agregar temporalmente ->crimen_cometido a la base de datos. Si esta última entonces llega a ser inconsisSólo fines educativos - FreeLibros

326

PARTE IV:

Lenguajes declarativos

tente —lo que ocurrirá si crimen_cometido es derivable de la base de datos consis­ tente original puesto que la resolución es completa— sabemos que fue hecho me­ diante esta adición, puesto que la resolución es también correcta. Nuestra estrategia de resolución es buscar a lo largo de una base de datos, buscando dos cláusulas de Hom, una de las cuales contiene una disyunción d y la otra contiene ->d. Recuerde que: d -'d

se resuelve en

FALSE Sólo somos capaces de derivar falso si la base de datos es inconsistente. Si sabemos (o suponemos) que nuestra base de datos era consistente, y que al agregar la cláu­ sula ->d se hace inconsistente, podemos concluir que la cláusula d debe haber sido verdadera en primer lugar. Esto se sigue del principio del medio excluido (principie of excluded middle), el cual se le solicitará demostrar en el ejercicio A.5 del Apéndice A. Este principio dice que si d es una cláusula, entonces debe ser verdadera o falsa, pero no quizá. Además, no puede ser al mismo tiempo verdadera y falsa. Veamos un ejemplo debido a Doug DeGroot [DeGroot, 1984], de una base de datos de cláusulas de Hom y sus resoluciones con una consulta a esa base de datos. Para facilitar la comprensión, escribamos primero las cláusulas de la base de datos en la forma "&/<—". La base de datos es nuestra teoría y supondremos que cada cláusula o proposición en ella sea verdadera. C l: C2: C3; C4: C5: C6: C7:

feliz(tomas) viendo(tomas,fútbol) & tiene(tomas,alimentos) (7.1.13) tiene(tomas,alimentos) <—tiene(tomas,cerveza) & tiene(tomas,pretzels) viendo(tomas,fútbol) <—esta_encendido(tv) & jugando(vaqueros) esta_encendido(tv) jugando(vaqueros) tiene(tomas,cerveza) tiene(tomas,pretzels)

Si deseamos deducir si Tomas es feliz, tenemos que demostrar que feliz(tomas) es una consecuencia lógica de C l hasta C7. Sabemos del teorema de resolución que si C8 es -*feliz(tomas) y que C l & C2... & C7 & C8 se resuelven a falso, entonces la consulta feliz(tomas) es verdadera. Así, nosotros agregamos a nuestra base de da­ tos la negación de la consulta (C8) y resolvemos la base de datos. En cada paso en la cadena de resolución, las dos proposiciones que fueron resueltas están enumera­ das a la derecha del resolvente, que se agrega entonces a la base de datos; por ejemplo, C8 y C l1fueron resueltos para producir C9. C l’ hasta C4' son cláusulas de Hom equivalentes a C l hasta C4. C8: C l': C2': C3': C4!:

-

tiene(tomas,alimentos) tiene(tomas,alimentos) o ~>tiene(tomas,cerveza) o -4iene(tomas,pretzels) viendo(tomas,fútbol) o -<esta_encendido(tv) o -ijugando(vaqueros) esta„encendido(tv) Sólo fines educativos - FreeLibros

CAPÍTULO

C5: C6: C7: C9: CIO: C ll: C12: C13: C14:

7: Programación lógica

jugando(vaqueros) tiene(tomas,cerveza) tiene(tomas,pretzels) -*viendo(tomas,fútbol) o ->tiene(tomas,alimentos) -

esta_encendido(tv) o ^jugando(vaqueros) o atiene (tomas,cerveza) o -jugando(vaqueros) o -tiene(tomas,pretzels) -»tiene(tomas,pretzels) FALSE

327

C8, C l’ C9, C3' CIO, C2' C ll, C4' C12, C5 C13, C6 C14, C7

Examinaremos la primera resolución, siguiendo el listado (7.1.6). C8: -ifeliz(tomas) C l': feliz(tomas) o -iviendo(tomas,fútbol) o -4iene(tomas,alimentos) C9:

->viendo(tomas,fútbol) o ->tiene(tomas,alimentos)

La segunda resolución es: C9: -iviendo(tomas,fútbol) o ->tiene(tomas,alimentos) C3’: viendo(tomas,fútbol) o ->esta_encendido(tv) o -ijugando(vaqueros) CIO: ->tiene(tomas,alimentos) o -<esta_encendido(tv) o -«jugando(vaqueros) La tercera es: CIO: -*tiene(tomas,alimentos) o -iesta_encendido(tv) o -ijugando(vaqueros) C2': tiene(tomas,alimentos) o -

jugando(vaqueros) o -itieneítomas,cerveza) o -4iene(tomas,pretzels) La cuarta es: C ll: ->esta_encendido(tv) o -ijugando(vaqueros) o ->tiene(tomas,cerveza) o -itiene(tomas,pretzels) C41: esta„encendido(tv) C12: ->jugando(vaqueros) o -tiene(tomas,pretzels) La quinta es: C12: -«jugando(vaqueros) o -tiene(tomas,pretzels) C5: jugando(vaqueros) C13: ->tiene(tomas,cerveza) o -
328

PARTE IV:

Lenguajes declarativos

La sexta es: C13: -4iene(tomas,cerveza) o -■tiene(tomas,pretzels) C6: tiene(tomas,cerveza) C14: -'tiene(tomas/pretzels) Y por último, tenemos dos cláusulas, una de las cuales niega a la otra: C14: ->tiene(tomas,pretzels) C7: tiene(tomas,pretzels) FALSE Puesto que hemos derivado falso de -ifeliz(tomas) y de C1 hasta C7, podemos concluir que Tomas es en realidad feliz siguiendo el listado (7.1.14). Un intérprete PROLOG interactivo nos anunciaría el éxito (SUCCESS!) o algo parecido al alcan­ zar la contradicción. Nótese el orden en el cual la cadena de resolución procede. Primero, la negación de la consulta se resuelve con la primera cláusula de la lista. Si esto no es posible, la resolución -iQUERY (C8), con C2, C3, etcétera, se intentaría en orden. En cada paso después del primero, la resolución de la nueva cláusula, lla­ mada el resolvente, se intenta con la siguiente cláusula que sigue de la lista.

Unificación Justamente como se deseaba para extender el cálculo proposicional al cálculo de predicados con el fin de incluir fórmulas generales conteniendo variables y cuantificadores, las pruebas mediante la resolución deberían aplicarse a tales pro­ posiciones también. Esto requiere un proceso conocido como unificación. Suponga que cambiamos la primera cláusula C1 de nuestra base de datos Feliz a: DI: feliz(X) <—viendo(X,fútbol) & tiene(X,alimentos) Esta nueva proposición sugiere que cualquiera, no sólo Tomas, que esté viendo el fútbol y tenga alimentos, es feliz. Un primer paso para resolver D I, C2 hasta C7, con la consulta negada -ifeliz(tomas)?, es para unificar la consulta con D I. Necesitamos encontrar sustituciones (ligas) para cualquier variable en las dos expresiones, lo que las hará parecerse, excepto por el signo. Si se sustituye "tomas" por X en D I, las dos expresiones coinciden. La sustitución debe ser uniforme, y resulta en: D8: feliz(tomas) <—viendo(tomas,fútbol) & tiene(tomas,alimentos) D8 y D I están unificadas por los conjuntos de sustitución {}3 y {X/tomas). 3 La consulta -ifeliz(tomas)? no tiene variables libres, y se denomina una cláusula base. Su conjunto de reemplazo cuando se le unifica con D I es {}. D I tiene una variable libre X, reemplazada por tomas, de modo que su conjunto de reemplazo es {X/tomas}, leído como "X se reemplaza por tomas".

Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

329

Volvamos a escribir otra vez nuestras condiciones para el predicado feliz, y agreguemos imas cuantas opciones más. Aquí, no hemos reescrito nuestras propo­ siciones como cláusulas de Horn, dejándolas como un ejercicio. E l: E2: E3: E4: E5: E6: E7:

feliz(X) 4—viendo(X,futbol) & tiene(X,Y) tiene(X,alimentos) tiene(X,Y) & tiene(X,Z) tiene(X,cerveza) tiene(X,pretzels) viendo(X,futbol) <—esta„encendido(tv) & jugando(Y) esta__encendido(tv) jugando(vaqueros)

(7.1.15)

La unificación y resolución, si se consulta con feliz(tomas)?, procedería como se muestra en el listado (7.1.16) (como antes, las explicaciones se encuentran en la columna del lado derecho): E8: -'feliz(tomas) E9: -iviendo(tomas,fútbol) o ->tiene(tomas,Y) E10: ->tiene(tomas,Y) o ->esta_encendido(tv) o ->jugando(Yl) E li: ->tiene(tomas,Y) o ->jugando(Yl) E12: -itiene(tomas,Y) o ->tiene(tomas,Y2) o -ijugando(Yl) E13: -ijugando(Yl) E14: ->jugando(vaqueros)

(7.1.16) {X/tomas} en E l {Y/Yl}4 en E9, {X/tomas¡ en E5 E10, E6 (Y/alimentos) en E li, {X/tomas} en E2 {Y/cerveza, Y2/cerveza} en E12, E3 {Y1/vaqueros} en E13 en E7

FALSE En esta resolución nunca usamos la cláusula tiene(X,pretzels), porque susti­ tuimos "cerveza" tanto para X como para Y en E12. Para Tomas, la cerveza parece ser suficiente. Usted tendrá una oportunidad para pensar acerca de esto en el ejer­ cicio 7.1.6. Búsqueda La demostración o prueba a través de la resolución involucra la búsqueda a través de una base de datos de cláusulas para términos confiables y para cláusulas que pueden ser resueltas. Hasta aquí, en nuestros ejemplos comenzábamos en la parte superior de la lista de cláusulas y satisfacíamos exitosamente la consulta con la primera cláusula, y así en forma sucesiva a lo largo de la lista hasta que se obtenía

4 Y1 debe sustituirse por Y en E9 debido a que no puede representar el mismo valor como la Y en D5.

Sólo fines educativos - FreeLibros

330

PARTE IV:

Lenguajes declarativos

un falso. Las cosas no siempre funcionan así de bien. Suponga que agregamos la cláusula C0 enfrente de C1 hasta C7 de la base de datos Feliz del listado (7.1.13), donde C0 es: C0: feliz(tomas)

viendo(tomas,fútbol) & tiene(tomas,cena)

Un intento para resolver la consulta feliz(tomas)? es: (7.1.17) C15: -

tiene(tomas,cena) o ->esta_encendido(tv) o -jugando(vaqueros) C15,C3 C17: -itiene(tomas,cena) o ->jugando(vaqueros) C16,C4 C18: -*tiene(tomas,cena) C17,C5 FAIL Es claro que nuestra falla no puede significar que probamos que Tomas es infeliz. Ya sabemos que la consulta se resuelve con C1 hasta C7, así que debería resolverse seguramente con C0 hasta C7. Necesitamos deshacer las resoluciones hechas ya y comenzar a intentar resolver C8 con C1 en lugar de C0. Esto se realiza a través de una técnica llamada retroceso, que se discute en la siguiente sección. Es importante comprender la diferencia entre una cadena de resoluciones que conduce a una contradicción, es decir, falso, y una que falla. Puesto que buscamos una contradicción para la negación de nuestra consulta, una resolución final para falso significa que probamos que la consulta es en realidad verdadera, y puede ser agregada a la base de datos sin inconsistencia. Si la consulta falla, significa que, dados los hechos en la base de datos, no podemos probar que la consulta sea verda­ dera o falsa. Así la base de datos está incompleta. Podemos agregar ya sea la con­ sulta que falló o bien su negación a la base de datos si lo deseamos, sin introducir una contradicción.

Retroceso La situación de nuestro ejemplo puede ser manejada como un árbol, como se ilus­ tra en la figura 7.1.1. Eliminamos C18 y retrocedemos a C17. Puesto que C17 podría resolverse sin otra cláusula que C5, las eliminamos y vemos C16, nuestro punto de selección más cercano. Estamos listos para escoger una cláusula diferente de C4 para resolver con C16. La única otra cláusula posible es C5, que también falla. No hay otras opciones para C16, de modo que eliminamos la rama izquierda bajo C16 y otra vez retrocedemos al punto más cercano de selección hacia arriba del árbol, C8. Nuestra siguiente elección para resolución con C8 sería C l, que sabemos resul­ tará en un éxito. Una situación relacionada con la carencia de pretzels para ir con la cerveza de Tomas ocurre si consultamos D I hasta D7 con la pregunta, tiene(tomas, X)?, y espe­ ramos como una solución una lista de todas las cosas que Tomas tiene. Nuestro árbol de resolución sería el que se ilustra en la figura 7.1.2. Las unificaciones ocurren de izquierda a derecha en el orden representado en la figura 7.1.2, suponiendo que examinaremos las cláusulas en orden desde la parte Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

C8

\C1

A CO

\

/

C9, C3

C15

\

I

C3

C10, C2

\

C11.C4

C16

\

C12, C5

\

C13, C6

\

C14, C7

FALSE

J FIGURA 7.1.1 Árbol de resolución con retroceso para feliz (toma s )

itiene(tomas,X)

C2 X=al¡mentos

C6

Y=cerveza & Z=cerveza FALSE

Y=cerveza & Z=pretzels FALSE

FIGURA 7.1.2 Unificación y resolución para t i e ne (to mas .X )?

Sólo fines educativos - FreeLibros

C7 X=pretzels FALSE

pretzels & Z=pretzels FALSE

331

332

PARTE IV: Lenguajes declarativos

superior de la lista hasta la parte inferior. Ésta fue la suposición de Robinson al presentar el método de resolución, pero son posibles otras optimizaciones. Véase, por ejemplo, [Genesereth, 1985], Nótese también que se utiliza tiene(tomas,alimen­ tos) tres veces por separado cuando se retrocede, y que tiene(tomas,cerveza) se repite cuatro veces. Sin embargo, hallar todas las soluciones que se aplican no es por lo regular automático. Se tiene que utilizar un predicado especial, por lo gene­ ral llamado "hallartodo"; por ejemplo, hallartodo(X: tiene(tomas,X)).

Hechos, metas y condiciones Como anotamos antes, las proposiciones pueden escribirse como condicionales en la forma "si A entonces B" o "B si A ", donde B es una proposición o cláusula sim­ ple, y A es cero o más cláusulas. Si A no tiene cláusulas, entonces B es un hecho; es decir, verdadero bajo cualquier condición posible. Estos hechos son llamados, en lógica, axiomas propios y funcionan del mismo modo que los axiomas lógicos del Apéndice A. Si A contiene una o más cláusulas, entonces B se llama la meta, y las condiciones de A, submetas. A medida que nos movemos a lo largo de una cadena de resolución, cada submeta llega a ser una meta. Cuando todas estas submetas han sido resueltas con los hechos, la meta principal B ha sido probada.

Encadenamiento hacia atrás y hacia adelante Cuando hallamos una cadena de resolución con una meta B, como al demostrar la meta feliz(tomas), encadenamos hacia atrás desde la meta hasta los hechos. Como ocurre con la prueba por contradicción, comenzamos con lo que va a probarse que es contradictorio, en vez de lo que es conocido; es decir, los axiomas o hechos. Cuando hay sólo una o quizá unas cuantas opciones para cláusulas por resolver con una meta, el encadenamiento hacia atrás es efectivo. Sin embargo, si existe muchas opciones (dos o tres se muestran en algunos de los puntos de decisión en las figuras 7.1.1 y 7.1.2), el encadenamiento hacia atrás puede llegar a ser ineficiente debido a que todo el retroceso necesario para deshacer las trayectorias es resolu­ ción vana. Para ciertos problemas, el encadenamiento hacia adelante de hechos y reglas hacia la meta principal es más eficiente. Si hay más hechos que reglas, el encadena­ miento hacia adelante probablemente lo hará mejor. Esto es, si encadenamos desde el conjunto de proposiciones más pequeño hasta el más grande (el más fácil de encontrar), podemos hallar menos rutas equivocadas. Otra situación en la que el encadenamiento hacia adelante prueba ser preferible es cuando existen pocas opciones en cada punto de decisión cuando se razona desde los hechos. Por su­ puesto, puede ser difícil saber esto al principio. El encadenamiento hacia adelante también puede ser más efectivo cuando un usuario necesita ver una justificación para cada paso en una prueba, y piensa naturalmente desde lo que se conoce hasta lo desconocido. Para satisfacer la meta feliz(tomas) se procedería como sigue. Primero, volve­ ríamos a arreglar el orden de la base de datos del listado (7.1.13) de modo que los hechos precedan a las reglas, como en el listado (7.1.18). Sólo fines educativos - FreeLibros

CAPÍTULO

C4: C5: C6: C7: C l: C2: C3:

7: Programación lógica

333

esta_encendido(tv) (7.1.18) jugando(vaqueros) tiene(tomas,cerveza) tiene(tomas,pretzels) feliz(tomas) <—viendo(tomas,fútbol) & tiene(tomas,alimentos) tiene(tomas,alimentos) <—tiene(tomas,cerveza) & tiene(tomas,pretzels) viendo(tomas,fútbol) esta_encendido(tv) & jugando(vaqueros)

Entonces consultamos y resolvemos la base de datos como en el listado (7.1.19). C8: -ifeliz(tomas) C9: viendo(tomas,fútbol) CIO: tiene(tomas,alimentos) C ll: feliz(tomas) FALSE

(7.1.19) C4, C5, C3 C6, C7, C2 C9, CIO, C l C8, C ll

Note el orden de las resoluciones. C9 se resuelve primero, porque C4 y C5 vienen primero en la base de datos del listado (7.1.18). El lenguaje PROLOG, que discutiremos en la sección 7.2, utiliza resolución con encadenamiento hacia atrás como su estrategia para resolución de problemas. Otros lenguajes, tales como OPS-5, confían en el encadenamiento hacia adelante. Sin embargo, el propio PROLOG puede ser utilizado para implementar un intérprete que utilice encadenamiento hacia adelante en lugar de hacia atrás [Malpas, 1987, sección 5.3].

Representación de hechos negativos Las proposiciones negativas con frecuencia han causado problemas a los lógicos y matemáticos. En lógica formal, si p es falso, entonces ->p es verdadero. Esto no es el caso en PROLOG, donde el éxito de una consulta (su negación resuelta como falso o FALSE) y la falla (su negación resuelta como falla o FAIL) no son mutuamente exclusivas; es decir, (not FAIL) no es lo mismo que SUCCESS. Cuando ocurre una falla, todas las variables quedan sin unificación, puesto que las ligaduras no fun­ cionan. Suponga que c o m e _ j a r a b e _ c a 1 i e n t e _ d o s _ v e c e s _ d i a r i a s ( s a 1 l y ) y not(come_jarabe__cal i'ente_dos_veces_di a r i a s ( o l a f )) son ambos hechos en nuestra base de datos. Entonces, come_jarabe_cal i ente_dos_veces_di a r i a s ( s a l l y ) . n ot t no t t come_j a rabe_cali ente_dos_veces_di a r i as (sal l y )).

(7.1.20)5 (7.1.21)

resuelve la misma cosa. Cuando se intenta resolver (7 .1.21), se encuentra el primer not, y se hace un intento para demostrar que not ( come_j a r abe_ca 1i en t e_dos_veces_

5 Nótese que hemos modificado nuestra fuente para el código para informar al lector que lo que está observando es código PROLOG. Advierta también que las cláusulas finalizan con puntos.

Sólo fines educativos - FreeLibros

334

PARTE IV:

Lenguajes declarativos

d i a r i a s ( s a l l y ) ) tiene éxito. Se resuelve a falso con la base de datos, así que no es exitosa. El segundo not resultará en un intento para demostrar que (7.1.20) tiene éxito, lo cual sucede. De este modo n o t ( c o m e _ j a r a b e _ c a l i e n t e _ d o s _ v e c e s _ d i a r i a s ( s a l l y ) ) f a l l a (FAILs), y (7.1.21) tiene éxito. Ahora supongamos que se reemplaza sal ly con X. co m e _ j a r a b e _ c a l i ente_dos_veces_di a r i a s ( X ) . not(not(come_jarabe_cali ente_dos_veces_di a r i a s ( X ) ).

(7.1.22) (7.1.23)

Cuando intentamos demostrar (7.1.23), seguimos la misma cadena de resolución que hicimos para (7.1.21). Cuando (7.1.20) tiene éxito, X se instancia con sa l l y , de manera que X—s a l l y . El resultado para n o t ( c o m e _ j a r a b e _ c a l i e n t e _ d o s _ v e c e s _ d i a r i a s ( s a l l y ) ) falla. Además, X pierde su valor de sally. El listado (7.1.23) tiene éxito, pero X - s a l l y no se devuelve, ya que X es ahora una variable no instanciada. Una característica de PROLOG es que cuando un nuevo hecho se agrega a una base de datos, los valores de cualquier variable antes libres se fijan y no pueden reem­ plazarse. De este modo si una consulta falla, cualquier unificación que haya ocurri­ do se deshace. n o t (come_j ara b e _ca1 i ente_dos_veces_di a r 1 a s (m o h a m m e d )) .

(7.1.24)

Si la cláusula del listado (7.1.24) es un hecho, durante el retroceso PROLOG lo en­ contraría y haría la instancia de X a mohammed. Esta ligadura se perdería otra vez cuando el segundo not es probado. De este modo, la doble negación tiene poco uso. not(p) sólo puede tener éxito o fallar. Si p tiene cualquier variable libre, no serán instanciadas, aun cuando eran unificadas con valores apropiados durante el curso de la cadena de resolución. L A B O R A T O R I O 7. 1: I N T R O D U C C I Ó N AL L E N G U A J E :

PROLOG Objetivos (Los laboratorios pueden encontrarse en el Instructor's Manual) 1. Acostumbrarse al ingreso y ejecución de programas PROLOG. 2. Emplear algunas de las herramientas proporcionadas con el PROLOG que ya esté utilizando, en particular EDIT, TRACE y DEBUG. 3. Ver en acción la ruta que su PROLOG retrocede. 4. Obtener alguna experiencia con las diferencias que tiene el orden cuando se introdu­ cen las diversas instancias de una relación. E J E R C I C I O S 7. 1

1. Recordando que r a. r<—p & q -ir c. s <- p & q & r P

q

p & q = ro->po -iq, resuelva lo siguiente: b. q o r -q o ->r d. s o ->p o ->q o ->r P

q

2. Considere las cláusulas siguientes y utilice el mecanismo de resolución para respon­ der las consultas que a continuación se presenta: Sólo fines educativos - FreeLibros

CAPÍTULO

Cl: C2: C3: C4: C5: C6 : C7: C8 :

7: Programación lógica

335

en_prision(maria) 4- cometio(maria,crimen) & atrapar(policia,maria) atrapar(policia,maria) <—vio(policia,crimen) vio(policia,crimen) 4- en_servicio(policia) cometio(maria,crimen) 4—tomo(maria,cartera) & pertenece_a(cartera/juana) tomo(maria,cartera) 4—tuvo(maria,oportunidad) en_servicio(policia) tuvo(maria,oportunidad) pertenece_a(cartera,juana)

Consulta 1, C9: cometio(maria,crimen)? Consulta 2, CIO: en_prision(maria)? 3. Supongamos que en una cadena de resolución el primer predicado a la derecha de una " 4- " debe resolverse antes de intentar satisfacer cualquiera de aquellas a la dere­ cha. Por ejemplo, en el punto 2 de este ejercicio, cuando -*Q2 es resuelto con Cl para ->cometió(maria,crimen) o -icapturó(policía,maria), la siguiente resolución necesa­ riamente involucraría el predicado cometió, incluso si una instancia de "atrapó'' se encontrara antes en la lista. ¿Qué método de los dos siguientes crearía un algoritmo más eficiente?: 1) primero, resolver el predicado más a la derecha o 2 ) verificar todos los predicados en una cláusula y resolver con el primer emparejamiento que se en­ cuentre en la lista? ¿Puede esto depender de la longitud de la lista de cláusulas? Vuelva a hacer la consulta feliz(tomas)?, siguiendo cada estrategia de emparejamiento. ¿Hace esto alguna diferencia? 4. Unifique los términos siguientes o establezca por qué no pueden ser unificados: a. a(X,3); a(2,3) b. a(X,3);a(Y,Y) c. madre(rea,X); madre(Y,jupiter) d. padre(satumo,X); padre(Y,Y) e. hijo(jupiter,saturno); hijo(Y,Y) f. p(X,Y); p(Z,Z) 5. Escriba un algoritmo para una función unificar(Terml,Term2), que regresa Term3 o FAIL. Un término es definido en forma recursiva: a. Si C es una constante, entonces C es un término. b. Si X es una variable, entonces X es un término. c. Si pNes un símbolo de predicado en el lugar N, y t1,...,tNson términos, entonces PN(V es un término. 6 . La unificación y resolución de El hasta E13 del listado (7.1.16) supone que hacemos uso de E3 dos veces: una vez para unificar y resolver con tiene(tomas,Y) y después para unificar y resolver con tiene(tomas,Z). Suponga que deseamos que el proceso continúe como se muestra a continuación: E12: -itiene(tomas,Y) o -»tiene(tomas,X) o -»jugando(Z) E13: -»tiene(tomas,X) o ->jugando(Z) {Y,cerveza) en E3,E12 E14: ->jugando(Z) (X,pretzels) en E13,E4 FALSE (Z,vaqueros) en E14,E7 a. Sugiera dos maneras para rechazar la sustitución de "cerveza" para X así como para Y en E12. Una manera podría modificar E2 y la otra podría modificar la regla de búsqueda para cláusulas de resolución. b. Si se intenta siempre unificar y resolver una cláusula con E2 antes de intentar E3 o E4, ¿qué podría pasar cuando consultara El hasta E7 con -Teliz(tomas)? ¿Cómo podría evitarse esto? Sólo fines educativos - FreeLibros

336

PARTE IV:

Lenguajes declarativos

7. El listado (7.1.6) proporciona la regla de resolución para dos cláusulas en lo que se llama forma normal de disyunción. Esto es, los únicos conectivos lógicos son "o" y "no" (not). Los ejemplos de resolución que hemos visto involucran cláusulas escritas en la forma Af - B.1 n a. Pruebe que una cláusula en la forma A <- B1 &...& Bnes una Cláusula de Horn como se definió en el listado (7.1.5). b. Establezca una regla de resolución equivalente al listado (7.1.6) para Cláusulas de Horn en la forma A &.. .& Bnque no traduce primero cláusulas a la forma "o". c. Use la regla para b y sugiera una forma de cláusula para una consulta a las cláu­ sulas en la forma A <- B.l &.. .& Bn . 8. Considere el siguiente conjunto de reglas y hechos: C1: Norte-de(Xl, X2) <- Ubicacion(Xl, Yl, 21) & Ubicacion(X2, Y2, Z2) & Menos(Y2, Yl) C2: Ubicacion(NuevaYork, 41,74) C3: Ubicacion(Chicago, 42,88) C4: Ubicacion(Tokio, 35,140) C5: Ubicacion(Oslo, 60,11) C6: Ubicacion(Quito, 0, 80) C7: Ubicacion(Cairo, 30,30) a. Construya un árbol de resolución tal como el de las figuras 7.1.1 y 7.1.2, comen­ zando con la consulta, Q: Norte-de(Chicago,Nueva York)? b. Ahora construya un árbol comenzando con la consulta, Q1: Norte-de(X,Nueva York)? Asegúrese que su retroceso explore todas las posibles sustituciones para X. 9. Use la regla p <—q & r, la cual puede ser reescrita como una Cláusula de Horn equiva­ lente, p o -iq o ~>r, para reeescribir las cláusulas del listado (7.1.15) como Cláusulas de Horn. 10. Construya una cadena de resolución para feliz(tomas); emplee para esto las cláusu­ las C0 hasta C7 de las secciones "Resolución" y "Búsqueda", pero encadene hacia adelante desde los hechos C4 hasta C7, en vez de hacerlo hacia atrás como en los ejemplos mostrados. 11. a. ¿Por qué sería preferible el encadenamiento hacia adelante cuando se intentara determinar una ruta de viaje desde el hogar hacia un destino desconocido (las metas)? b. Suponga hechos que determinan cuáles palabras son verbos, sustantivos, adjeti­ vos, etcétera, y reglas que describen qué comprende una oración en idioma in­ glés. Para oraciones simples, las siguientes tres reglas bastarán: Rl: Oracion(NP, VP) <- FraseSustantiva(NP) & FraseVerbal(VP) R2: FraseSustantiva(A, N)Articulo(A) & Sustantivo(N) R3: FraseVerbal(V, NP) <—Verbo(V) & FraseSustantiva(NP) Si nuestra meta es analizar una oración o frase dada, ¿sería preferible encadena­ miento hacia adelante o hacia atrás? Intente descubrir con unas cuantas oraciones cuál de ellos parece más natural. c. En un problema de diagnóstico médico, los hechos son síntomas, y la meta es hacer coincidir esos síntomas con una enfermedad. ¿Sería más razonable aquí el encadenamiento hacia adelante o hacia atrás? Sólo fines educativos - FreeLibros

CAPÍTULO 7: Programación lógica

337

d. En un juego de gato, las metas son ganar las configuraciones de la red de 3 x 3. ¿Cuántas configuraciones existen para un solo jugador? (Tenga cuidado con este cálculo; recuerde que una configuración involucra todos los nueve cuadrados, no sólo el renglón, columna o diagonal ganadores.) ¿Cuántas configuraciones en to­ tal hay aquí? (¡Una red vacía es una de ellas!) ¿Un encadenamiento hada adelante o hada atrás encontraría una soludón ganadora con más facilidad? ¿Por qué? ¿Importa esto? 7.2

PROLOG VIÑETA HISTÓRICA

PROLOG; Colmerauer y Roussel Una mirada a la historia de PROLOG es otra mirada a la historia de la lógica mis­ ma, y a su futuro. En el principio desarrollado por Alain Colmerauer, Philippe Roussel y sus colegas del Greupe dTntelligence Artificielle (Universidad de Marse­ lla) para ser un lenguaje para prueba de teoremas, PROLOG contará, inmerso en la cuarta generación, como un buen lenguaje para administración de bases de datos, y en la quinta en el campo de la inteligencia artificial. Los principios de PROLOG nos remontan veintidós siglos atrás hacia la lógica tradicional de Aristóteles. Uno de los problemas de ese sistema es que es entera­ mente estático. Una proposición puede tener sólo un valor, verdadero o falso, y una vez que se establece nunca puede ser cambiado. Las primeras insatisfacciones surgieron durante el siglo XIX, cuando DeMorgan, un matemático inglés, comenzó el desarrollo de un sistema formal, más representativo del razonamiento matemá­ tico que el lenguaje natural. Las contribuciones de Gottlob Frege en la última mitad del siglo establecieron con firmeza la lógica simbólica como una rama de las mate­ máticas, en ocasiones solamente restringida a la filosofía. Durante la década de los sesenta hubo gran interés en la demostración auto­ mática de teoremas. Robert Kowalski, en su trabajo en la Universidad de Edimburgo, se concentró en la programación lógica; el uso de computadoras para hacer inferencias lógicas controladas. Colmerauer y Roussel, un estudiante canadiense, desarrollaron con otros el primer lenguaje de programación lógica. Lo llamaron PROLOG, una abreviación de programmation en logiqne, siguiendo una sugerencia de la esposa de Roussel, Jacqueline. Debido al cercano vínculo con la lógica matemática y demostración de teore­ mas, puede parecer sorprendente que PROLOG sea conocido como un lenguaje de inteligencia artificial (IA). Incluso la lógica fue diseñada originalmente para aclarar el discurso ordinario, no para investigar matemáticas. Hubo poco interés en PROLOG, tanto en Estados Unidos como en Europa, hasta principios de los ochen­ ta, cuando el Instituto para la Tecnología de la Nueva Generación de Japón anunció planes para producir una quinta generación de hardware de computadora que acep­ taría entrada de lenguaje natural y procesaría grandes cantidades de información. Sólo fines educativos - FreeLibros

338

PARTE IV:

Lenguajes declarativos

El lenguaje elegido por ellos fue PROLOG. La primera reacción de los cinetíficos estadounidenses respecto al movimiento japonés fue la burla, suponiendo rápida­ mente que se cometía un gran error, pero las risas terminaron cuando se difundió los informes del éxito japonés con sus proyectos de quinta generación. Los noventa han visto un Japón todavía interesado en llegar a ser el centro de una red de infor­ mación a escala mundial. PROLOG permanece como una poderosa herramienta para el desarrollo de sistemas, con el producto final, sin embargo, implementado en C o C++. En la actualidad PROLOG es utilizado en Estados Unidos así como en Japón para demostración de teoremas, diseño de bases de datos relaciónales, ingeniería de software, procesamiento del lenguaje natural, representación del conocimiento en inteligencia artificial y programación de sistemas expertos. Quizá la característi­ ca más importante de PROLOG es que está un paso adelante de la programación de no procedimiento, en la que menos programación está involucrada a medida que se obtiene más código hecho de manera automática. El usuario puede concen­ trarse más en lo que necesita hacer que en cómo hacerlo. El futuro de PROLOG y de la programación lógica no está claro. PROLOG es para la programación lógica lo que FORTRAN fue para la programación de compu­ tadoras modernas: un comienzo. Los japoneses han desarrollado un nuevo lengua­ je de programación lógica llamado KL, reemplazando las duplicaciones de PROLOG. En la actualidad, muchos programadores de PROLOG son entusiastas de la compu­ tadora que quieren aprender más acerca de la programación de IA. Se tiene dispo­ nibles varias versiones para microcomputadoras. Quizá PROLOG será dejado atrás como un lenguaje de producción y permanezca como una herramienta de ense­ ñanza de AI a medida que se desarrollen lenguajes más novedosos.

Conversando en PROLOG: hechos, reglas y consultas PROLOG ha sido descrito como relacional [Malpas, 1987], descriptivo [Genesereth, 1985] y declarativo. Tanto los puntos de vista relacional como descriptivo conside­ ran la organización de la base de datos, o conjunto de hechos y reglas de PROLOG, supuestos verdaderos para la aplicación práctica. PROLOG se considera declarati­ vo en el sentido que el usuario describe lo que quiere realizar; por ejemplo, "clasificar([5,3,7,2],Respuesta)!", con poco interés en el procedimiento para efec­ tuar la tarea de clasificación, la que devuelve "Respuesta = [2,3,5,7]". Por supuesto, debemos describir además precisamente lo que entendemos por "clasificar", si no se ha definido en forma previa en la implementación. PROLOG también es llamado un lenguaje para programación en lógica [Calingaert, 1988; Ghezzi, 1987]. Esto último puede ser la clasificación más precisa, pero PROLOG mismo está basado sólo en la lógica y no produce tQdas las pruebas posibles de los métodos que utilizan toda la potencia del cálculo de predicados. PROLOG está presentado en diversos dialectos. La versión original de Colmerauer y Roussel es la sintaxis de Edimburgo, también llamado PROLOG DEC10®, debido a su implementación inicial en computadoras DEC-10 ejecutando el sistema operativo TOPS-10. Otro dialecto, Micro-PROLOG, está disponible para Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

339

microcomputadoras, aunque "Core PROLOG", un subconjunto de la versión DEC10, parece haber llegado a ser el estándar de fa d o para micros, minis y macrocomputadoras. Se utilizará aquí la sintaxis de Core PROLOG, puesto que se en­ cuentra más ampliamente disponible tanto para máquinas de 32 bits como de 16 bits, aunque el aprendizaje de PROLOG por parte del usuario es todavía más fácil de realizar si se emplea el manual de Clark y McCabe para micro-PROLOG [Clark, 1984]. La diferencia principal entre las versiones Edimburgo y micro-PROLOG está en la forma de una cláusula. fel i z (t o m a s )

v i e n d o tt oma s. fut bol), ti e n e ( t o m a s ,alimentos)

es una cláusula de Edimburgo, mientras que fel i z ( t o m a s ) (viendo tomas fútbol) (tiene tomas alimentos)).

es la misma cláusula en la sintaxis de micro-PROLOG. Cada una significa que To­ mas [está] feliz [si] Tomas [está] viendo el fútbol [y] Tomas tiene alimentos. Hay algunas diferencias en la evaluación de expresiones aritméticas y algunas otras también. Una vez que usted ha dominado una, no es difícil cambiarse al otro dialecto. Cada sintaxis es bastante fácil de aprender. Sin embargo, escribir progra­ mas eficientes en PROLOG requiere de una comprensión bastante sofisticada tanto de lógica como de la ejecución de una máquina PROLOG abstracta. Sintaxis Un programa de PROLOG es una lista de declaraciones, llamadas hechos y reglas, que es ingresada a través de una consulta. La forma general de una declaración es C A B E Z A C U E R P O , donde CABEZA es una estrudura simple y CUERPO está com­ puesta de cero o más estructuras, llamadas submetas, separadas por comas que sig­ nifican "y " o puntos y comas que representan "o". Un hecho es una declaración sin cuerpo (body), mientras que una regla contiene tanto un encabezado (cabeza) como un cuerpo (cuerpo). Una consulta es un hecho precedido por ? -, y devuelve ya sea verdadero (TRUE) o falso (FALSE). Una consulta, hecho o regla se termina mediante un punto. Si una consulta que contiene variables es exitosa, se imprimen los valores constantes para las variables que hacen la consulta verdadera. La forma de una estructura es la de un hecho de PROLOG, functor(terml7..., termn). Un término (term) puede ser una constante, variable o estructura. Los functors son símbolos de predicado, operadores o nombres de relación. Un predicado pue­ de tomar los valores verdadero o falso. -< (2,4) es verdadero, mientras que -<(4,2) es falso. Aquí6 -< es un functor y 2 y 4 son términos constantes. Un operador es un functor escritor en forma infija en lugar de prefija; por ejemplo, 2 -< 4. Algunos operadores integrados en PROLOG son los de aritmética entera; por ejemplo, X+Y y 6 Los manuales de PROLOG tienden a rodear los signos utilizados como operadores o functors con comillas simples. La designación completa es '=<'/2, lo que significa que =< requiere de dos argumentos.

Sólo fines educativos - FreeLibros

340

PARTE IV:

Lenguajes declarativos

X+Y*Z. Un usuario de PROLOG puede declarar functors como operadores al espe­ cificar el nombre del functor, precedencia y tipo, donde los tipos pueden ser infijos ( X+Y), prefijos (- 2) o posfijos (5!), donde ' ! ' es el operador factorial. La preceden­ cia y la asociatividad también deben ser especificadas para operadores. Aquellos aritméticos obedecen las reglas estándar; por ejemplo, * precede a +, y los operado­ res se asocian de izquierda a derecha, por ejemplo 2 + 3 + 4 s ( 2 + 3) + 4. Una constante se piensa como nombrar un objeto específico o relación y es o un átomo o un entero. Un átomo constante es una cadena de letras y dígitos que co­ mienzan con una letra minúscula y no contiene otros signos más que el de subraya. john_alden, x, yymap2 son todos constantes, pero 2X, Mary, y gambier-ohio no lo son. Sin embargo, cualquier carácter puede ser utilizado para formar una constan­ te entre comillas simples. De esta manera 'Gambier-Ohio* es una constante. Un átomo también puede estar compuesto enteramente de signos, pero éstos son reservados para propósitos especiales. Dos de estos átomos especiales son :-, que significan "if" ("si") y ?-, que señaliza una consulta. Los signos son: {+ - * \ / A < > ~:.? # @ $ &}. Un nombre de relación es también un átomo; por ejemplo, el < en <(2,4), o el tiene en t i ene( tomas, cerveza). Una variable es cualquier cadena comenzando ya sea con una letra mayúscula o el signo de subraya. Who, Sal ary_Amt, X y _2_hermanos son variables, mientras que nombre-final y 2daBase no lo son. PROLOG también tiene una variable anónima especial, ' J . La consulta, ?- t i e n e ( t o m a s , _ ) . , es exitosa si cualquier átomo que satisface la relación t i e n e , con tomas como el primer término, se unifica con la variable _. Ya sea que t i ene (tomas ,_) tenga éxito o falle, no sabemos precisamente lo que tomas tiene, aun cuando, como vimos en la sección "Resolución", cerveza, pretzels y alimentos satisfacen la relación t i ene para tomas. La variable anónima _ debe ser unificada cuando se resuelva t i e n e ( t ornas, _ ), pero sus valores serán des­ cartados.

Estructuras de datos La única estructura de datos integrada en PROLOG es una lista, implementada como el functor '. '/2. El punto está sobrecargado y es aquí un nombre de functor, donde lo utilizamos previamente para terminar una cláusula. El 2 representa su orden (arity), o el número de términos esperados como argumentos. Cuando se uti­ liza cualquier functor, se omite el orden. Los dos argumentos para el punto son el encabezado y la cola. . (brocol i , []) es una lista con un elemento simple, brocol i . El [ ] es un símbolo especial que representa la lista vacía, que marca el final de cualquier lista. . (brocol i ,. (patatas, . ( leche, [ ] ) ) ) es una lista de tres elementos. Por conveniencia, PROLOG permite escribir esta mi sma lista com o [brocol i .patatas,leche] o como [brocoli |[patatas,leche]]. Aquí el operador ' I '/2 se emplea para agregar el segundo argumento, que debe ser una lista, al pri­ mer elemento, que es el encabezado de la lista. Una lista de longitud indetermina­ da puede escribirse como [brocol i |X], donde X es una variable que representa la cola de la lista. A continuación presentamos un programa de PROLOG para agregar dos listas. Sólo fines educativos - FreeLibros

CAPÍTULO append([]»L,L) . append([X|Ll],L2,Y)

7: Programación lógica

341

(7.2.1) append(Ll,L2,L3), Y = [X|L3].

El listado (7.2.1) contiene dos predicados: un hecho y una regla. La regla es recursiva en append, puesto que append aparece tanto a la izquierda como a la derecha de la regla. Como hemos visto con anterioridad, PROLOG examina las cláusulas en una base de datos desde arriba hasta abajo, de modo que la relación no recursiva que detendrá la recursión se enumera primero. Ahora suponga que consultamos, ?- append ( C1, 2] , [3 ] , Y ) . PROLOG devol­ verá: Y = [1,2,3] . No.

El No indica que no hay soluciones más que las enumeradas. Si nombramos las cláusulas del programa del listado (7.2.1) C1 y C 2 :- C3, C4 y nombramos la consul­ ta como Q, nuestra resolución procede como se muestra en la figura 7,2.1. Hubo sólo tres llamadas a C1 y dos a C2, de modo que la recursión fue breve en este ejemplo. L1 y L2 habían sido más largas, podrían haber sido muchas llamadas recursivas hacia atrás y hacia adelante para C l, C2, C3 y C4. La recursión es por lo general implementada utilizando una pila con cláusulas sin resolver insertadas, y extraídas de la pila hasta que ocurra un éxito o una falla. La acción de la pila recursiva es como se muestra en la figura 7.2.2. Cada "in­ serción" representa un intento para unificar una cláusula (flecha hacia abajo), y una de "extracción" corresponde a un retroceso (flecha hacia arriba) en el árbol de la figura 7.2.1. Note que el procedimiento append (listado (7.2.1)) enumera como su primera cláusula append ([ ] , L, L). Esta cláusula se utiliza en la parte inferior de la recursión, antes del retroceso hacia la pila recursiva para unificar variables no instanciadas. La omisión de una cláusula de terminación de recursión al principio de un procedimiento recursivo conduce a ciclos infinitos. A PROLOG no le intere­ sa, pero usted obtendrá probablemente una nota "no espacio disponible" (no space left) después de que un procedimiento así se ejecute durante un momento. Intente invertir el orden de las dos cláusulas y vea lo que ocurre con su versión de PROLOG. Core PROLOG no tiene estructuras integradas para arreglos, conjuntos o cade­ nas, pero algunas implementaciones están extendidas para incluir cadenas y ope­ radores de manipulación de cadenas. Varias contienen extensiones para escribir analizadores descendentes.

Operadores yfunctors integrados Además del predicado not (que fue mencionado antes en la sección "Representa­ ción de hechos negativos" y también será discutido de nuevo más adelante en este capítulo) y los operadores aritméticos, PROLOG proporciona varios comparadores, operadores para control de ejecución y depuración y para determinados tipos. Aquí examinaremos en forma breve algunos.

Sólo fines educativos - FreeLibros

342

PARTE IV:

Lenguajes declarativos Q

append([],L1,L1) FAIL

append([2IL21],L22,Y2). {X2=2,L21=[]IL22=L2=[3]}

SU CCESS

F I G U R A 7.2.1

Cadena de resolución para ?-append( [ 1 , 2 ] , [ 3 ] , Y).

Sólo fines educativos - FreeLibros

Y2=[2IL23] L23=Y3

CAPÍTULO

7: Programación lógica PUSH C12 PUSH C21

PUSH C11

PUSH C3

C11

PUSH C2

C3

C3

C1

C2

C2

Q

Q

Q

PUSH C1 C1 Q

Q

FAIL

FAIL

343 PUSH

C41

SUCCESS C12 C41 C12

C21

C21

C21

C21

C3

C3

C3

C3

C3

C2

C2

C2

C2

C2

C

Q

Q

O

Q

Q

G

C11

SUCCESS C41 PUSH SUCCESS C21 C21 C4 SUCCESS SUCCESS C4 C3 C3 C4 C3 SUCCESS C2 C2 C2 C2 C2 Q Q

G

G

Q

G

G

F IG U R A 7.2.2 Pila recursiva para resolución de la figura 7.2.1

PROLOG no incluye la igualdad en el sentido usual. Si el término X = Y se encuentra, PROLOG intenta unificar la X con la Y. De este modo mantequi l i a = m a n t e q u i l l a es un éxito, y m a n t e q u i l l a * p i s t o l a falla, m a n t e q u i l l a = W tiene éxito como en X = Y. Como un efecto colateral, W tendrá el valor mantequ i l l a y Y el mismo valor de X, o la variable X misma si X no está instanciada. Se dice que una variable está instanciada si le ha sido asignado un valor. En la figura 7.2.1, X fue instanciada con el valor 1, L1 con [2], L2 con [3] y (eventualmente) Y c o n [ l , 2 , 3 J . La única manera en que una instancia de una variable particular puede ser cambia­ da es si el predicado que la contiene falla. Si un predicado falla, PROLOG deshace sus fijaciones de variable y busca una diferente manera de unificarlas y/o resolver­ las. Este proceso se llama retroceso, el cual ya hemos discutido. El operador '=='/2 es el comparador de PROLOG. X=Y no intentará unificar Y con X. De este modo si X o Y es una variable sin instancia, X==Y fallará. Sin embargo, si seguimos X“ Y por X==Y, tanto X=Y como X==Y tendrán éxito. X * gato.

X ahora tiene el valor ‘g a t o ’.

?-X==Y.

puesto que Y no esta instanciada.

no ?-X«Y.

Y ahora también tiene el valor ‘g a t o ’

Sólo fines educativos - FreeLibros

(7.2.2)

344

PARTE IV:

Lenguajes declarativos

si ? - X~Y.

si

Dos estructuras son equivalentes (==) si tienen el mismo functor y número de ar­ gumentos, y todos los argumentos son iguales (==). Un operador muy útil es '= .. 7 2 , llamado univ. Si consultamos: ?-append([l,2],

[3], Y) =.. L.

PROLOG devolverá: L = [append,

[1,2],

[3], Y].

De manera similar, ?-T=.. [append, [1, 2] , [ 3 ] , Y]. devuelveT = append([l,2], [3], Y). Tal cambio entre listas y términos permite la modificación de los programas mientras están en ejecución, puesto que los términos pueden agregarse con facili­ dad o eliminarse de las listas. Así puede hacerse que los programas aprendan mien­ tras se ejecutan. Un uso de univ es en la construcción de la función mapca r, que es una de las funciones construidas dentro de LISP (véanse "Funciones como objetos de primera clase" en la sección 8.1). Nótese que hemos agregado comentarios, pre­ cedidos por el signo % para mayor claridad en el listado (7.2.3).7 (7.2.3)

mapcar(_, [],[]). mapcar(Foo,[X|Args], [Y|Answers] Foobar =.. [Foo,X,Y],

% Foobar is Foo(X,Y)

c a l i (Foobar),

% Y is Foo(X).

mapcar(Foo,Args,Answers).

LaaplicacióndemapcarCfunc, L l , L2). regresará como L2, el resultado de aplicar func de manera sucesiva a los elementos de Ll. Aquí func es un nombre de fun­ ción, L l es una lista y L2 es un identificador de variable. ? -mapear(mayúsculas,

[ a , b , c , d ] , X).

regresará X = [ ‘ A*, ‘ B ’ ( ‘ C V ‘ D’ ], suponiendo que mayuscul as haya sido apro­ piadamente definido. Los elementos de la lista X son mayuscul a(a), mayuscul a(b), mayuscul a( c) y mayuscula(d). Se puede probar los tipos de los términos utilizando los predicados ‘ var V I , ‘ n o n v a r ’ / l , ‘ i n t e g e r ’ / l y ‘ a t o m V l . atom(X) es verdadero para constantes no enteras. 7 Los functors foo y foobar han sido utilizados tradicionalmente por los programadores como comodines, foo viene de "fouled up" ("dañado"), y foobar es por "fouledupbeyond allrepair" ("daña­ do más allá de toda reparación"). Usted verá estos acrónimos dispersos a través de muchos textos y artículos. Véase [Raymond, 1993] para una discusión adicional.

Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

345

A ritm ética. Si PROLOG encuentra X = 1+2, intentará unificar X con el término 1+2. Para tener asignado el valor 3 a X, debemos utilizar Ms ’ /2. X 1s 1+2, realiza la aritmética deseada e instancia X a 3. Si queremos evaluar y probar la igual­ dad aritmética, consultamos ? - x “ : = 1+2. El operador \= V2 pruébala desigualdad aritmética. ‘ <'/2, ‘>’ /2, ‘=>’ /2 y


Control Debido a que PROLOG realiza exhaustivas búsquedas de profundidad primaria cuando intenta unificar sus variables, la ejecución del programa puede ser muy poco eficiente tanto en velocidad de ejecución como en uso de memoria. Así, se le exige al programador escribir procedimientos que minimizan tanto el tiempo de búsqueda como el uso de la memoria. R ecursión de cola. La recursión de cola fue mencionada en la sección 2.2, "Recur­ sión". El procedimiento para agregar del listado (7.2.1) no es recursivo de cola. Sin embargo, append2([],[_,!_). append2([X|Ll],L2,[X|L3])

(7.2.4) append2(Ll,L2,L3).

sí lo es. Sigamos a través de ? - a ppend2 ( [ 1 , 2 ] , [ 3] ) utilizando las figuras 7.2.3 y 7.2.4. La operación de la pila para append2 de la figura 7.2.3 se muestra en la figura 7.2.4. Nótese que en las transiciones desde la cuarta a la quinta, desde la séptima hasta la octava y de la octava a la novena pilas, C2, C21 y C22 no necesitan ser mantenidas, a medida que cualquier variable de instancia haya sido copiada a los conjuntos de unificación (marcados por {...)) para C21, C22 y C12, respectivamente. De esta forma, los requerimientos de memoria son como se ilustra en la figura 7.2.5. Sólo fines educativos - FreeLibros

346

PARTE IV:

Lenguajes declarativos

append2([1,2],[3],Y]

C1 append2([],L,L])

append2([1 IL1 ],[3],[1 I L 3 ] ) C 2 1 {X=1 ,L1=[2],L2=[3],Y=[1 IL3]}

{[]=[1,2]} FAIL C1

C21

C11

C22 append2([2]![]],[3],[2IL13]) {L3=[21L13],Y=[ 11L3]}

append2([2],L3,L3) FAIL

C12 append2([],[3],L13) SUCCESS {L13=[3],L3=[2,3],Y=[1,2,3]}

FIGURA 7.2.3 Árbol de resolución para append2([ 1 , 2 ] , C3 ] , Y ) .

Una submeta recursiva de cola puede ser reconocida por su forma en el mo­ mento que es llamada, y algunas implementaciones de PROLOG aplican de mane­ ra automática optimización recursiva de cola, como se mostró antes. Un procedi­ miento recursivo de cola ahorra espacio de pila, puesto que los resultados intermedios no necesitan ser guardados en la pila recursiva. Note que el valor par­ cial para Y fue llevado a través de las submetas en la figura 7.2.3. Una submeta recursiva de este tipo se caracteriza por lo siguiente: 1. 2.

En el momento que es llamada, todas las submetas anteriores han sido deter­ minadas. No hay submetas adicionales después de la submeta recursiva. Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

347

PUSH

C12 PUSH C11 PUSH

Q

PUSH

PUSH

C1

C2

PUSH C22

C12

C21

C11

FAIL C11

C22

C22

C21

C21

C21

C21

C21

C1

FAIL C1

C2

C2

C2

C2

C2

C2

Q

Q

Q

Q

Q

Q

Q

Q

SUCCESS C12 Y = [1,2,3]

FIGURA 7.2.4 Pila recursiva de cola para la figura 7.2.3

El procedimiento append del listado (7.2.1) no es recursivo de cola, porque cuando la llamada a la submeta C3 se efectúa la primera vez, todavía hay una submeta C4 restante por ser satisfecha. Cuando se escribe una regla, se garantiza que sea recursiva de cola si es de la forma: R(t17. . ./tn) C 1,C2, . .

,.. .,tn).

donde cada una de las C. son submetas satisfechas por una sola solución, o si C m es un corte !, lo que interrumpe el retroceso. Discutiremos el efecto del corte de predi­ cados en la siguiente sección. Si no hay C., como en el procedimiento append2, la condición, por supuesto, es satisfecha. Para ejemplos de cómo cambiar procedi­ mientos en unos equivalentes recursivos de cola, véase [Clark, 1984]. Una forma de recursión que siempre debe ser editada es la recursión izquier­ da. Considere la regla y el hecho siguientes: R: F:

a n e e s t r o ( X ,Z ) :- a n c e s t r o ( X ,Y ) & aneestro CY, Z). ancestroí gas t o n , f e r d i n a n d ) .

Si consultamos Q: ? - a n c e s t r o ( g a s t o n , A ) . para hallar los ancestros de Gastón, PROLOG emparejará Q con el encabezado de R, utilizando el conjunto de unifica-

PUSH C11

PUSH C1

Q

PUSH C2

PUSH C21

C11

FAIL C11

PUSH C22

PUSH C12

C21

C21

C22

C12

Q

Q

Q

C1

FAIL C1

C2

C21

Q

Q

Q

Q

FIGURA 7.2.5 Pila recursiva de cola optimizada para la figura 7.2.4

Sólo fines educativos - FreeLibros

SUCCESS C12 Y =[1,2,3]

348

PARTE IV: L en g u a jes d eclarativ o s

ción {X=gaston, Z=A), y activa la primera submeta, ancestro(gaston,Yl). [Y1=A] unificará esta submeta, lo cual de nuevo empareja con el encabezado de R. Una nueva submeta, a n c e s t r o ( g a s t o n , Y 2 ) será activada, que emparejará con R, y así en forma sucesiva. Este círculo infinito a través de R continuará hasta que aparezca un error tipo "no espacio disponible" (no space left). Nuestro problema es que mante­ nemos la recursión desde la derecha hacia la izquierda de R, con metas idénticas para ser satisfechas, y nunca alcanzamos el hecho F. En este ejemplo, la recursión izquierda puede ser reconocida mediante la apariencia de cláusulas idénticas (ex­ cepto para nombres de variable) tanto en el lado izquierdo como en el derecho de la regla R. Corte (Cut)r falla (fal 1) y negación (not). El predicado integrado corte o cut ( U * i 0) siempre tiene éxito, y evita la reevaluación de cualquier cláusula que lo precede. Si su versión de PROLOG no proporciona optimización recursiva de cola, usted puede hacer algo del trabajo utilizando el corte. PROLOG busca por todas las posi­ bles soluciones para una consulta. Si usted sabe que sólo hay una, cortar la búsque­ da adicional después de la única solución que haya sido encontrada ahorra tanto tiempo como espacio. Puesto que append2 se detiene cuando la primera cláusula tiene éxito, coloca­ remos un corte allí. append3 es idéntico a append2, excepto por el corte agregado. (7.2.5)

append3([],L,L) :- !. append3([X|Ll],L2,[X|L3])

append2(Ll,L2,L3).

PROLOG detendrá la búsqueda la primera vez que satisfa*ga a p p e n d 3 ( [ ] f L ,L ). Un procedimiento de una solución de esta clase podría ser útil si nosotros siempre fuéramos a utilizarlo con dos listas de cláusulas base como los primeros dos argu­ mentos, como e n ? - a p p e n d 3 ( [ l , 2 , 3 ] ( [ 4 , 5 , 6 ] f L). Sin embargo, si queremos encon­ trar todas las posibles sublistas, como en: ?-append3(X,Y,[l,2,3,4,5,6]).

(7.2.6)

PROLOG devolvería solamente una respuesta, X—[ ] ; Y - [ l , 2 , 3 , 4 , 5 , 6 ] . El corte evitaría cualquier búsqueda adicional. El predicado f a l l es uno que siempre falla. Suponga quequeremos determi­ nar si un individuo es un ciudadano británico, unatarea nada fácil en una nación colonial. Un individuo, tal como Guy Burgess,8 quien ha renunciado a su ciudada­ nía en el Reino Unido, claramente no es un ciudadano británico. Así otros podemos tener una regla: ciudadano(X) ciudadano(X)

;- re nun cia (X. UK ), 1, fal 1 na cid o _ e n (X ,U K ) ;...

(7.2.7)

donde los ... indican todas las múltiples condiciones que permiten la ciudadanía. Entonces ?- c i u d a d a n o ( B u r g e s s ) . devolvería fa 11. 8 Guy Burgess fue un ciudadano británico que realizó labores de espionaje para la Unión Soviética durante la Segunda Guerra Mundial, y posteriormente desertó hacia ese país.

Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

349

Necesitamos aquí el corte para evitar cualquier búsqueda adicional por una regla que daría la ciudadanía a Burgess, pero también necesitamos de fall para devolver una respuesta negativa a nuestra consulta. La combinación corte/faf 1 siempre puede ser reemplazada por el uso de la negación not. Nuestra definición anterior sería: ciudadano(X) ciudadano(X)

not(renuncia(X,UK)). na c i d o _ e n ( X , U K ) ;...

(7.2.8)

En definiciones más complicadas, el uso de not puede requerir paréntesis pro­ fundamente anidados, lo que hace un programa menos legible para algunos. P rogram as que se m odifican a s í m ism os. PROLOG tiene predicados que pueden eliminar o agregar cláusulas a la base de datos mientras que un programa se en­ cuentra en ejecución. A continuación tenemos un programa que hace una consulta por el usuario acerca de alergias a las drogas y agrega la información a la base de datos. droga(penicilina). drogaCsulfatiazina). d r o g a (aspi ri n a ). d r o g a í c a r b r o m o l ). examen_drogas

(7.2.9)

w rlte ( ‘Por favor introduzca su apellido: '), read (Paciente), w rlte ( ‘Después de que se enumere cada droga, responda s i ’), w rlte ( ‘ o no si usted es alérgico a ella o no.'), ni, drug NombreDroga , w rlte NombreDroga , ni, read si .assert (alergico(Paciente.NombreDroga)), f a ll.

El fall se utiliza aquí para forzar a PROLOG a retroceder a través de todas las drogas en la base de datos para verificar otras alergias a las drogas. La ni provoca un retomo de carro en el flujo de salida. PROLOG también puede agregar o eliminar hechos y/o reglas de la base de datos mientras que un programa se encuentra en ejecución utilizando los predica­ dos assert, retract y abol 1sh. assertC C1) agrega la cláusula C1 a la base de datos, retract( C2) elimina la cláusula C2 y abol 1sh( N/A) elimina todas las cláusulas con nombre de predicado N y orden A de la base de datos. Dejaremos los ejemplos del uso de estos predicados para el MiniManual y los laboratorios de PROLOG. Implementaciones de PROLOG Una m áquina teórica La ejecución de un programa PROLOG puede ser descrita mediante la máquina teórica de la figura 7.2.6. Colmerauer [Colmerauer, 1985] la llama "el Reloj PROLOG", puesto que su función principal es mantener un registro del tiempo. Sólo fines educativos - FreeLibros

350

PARTE IV:

Lenguajes declarativos

FIGURA 7.2.6 El Reloj PROLOG

Un reloj de computadora, no confundirse con un reloj en tiempo real (RTC; Real-Time Clock), que mide el tiempo cotidiano, cuenta los ciclos de ejecución. Aquí el valor del reloj es el de la variable i de arriba y comienza en 0. El reloj PROLOG tiene la capacidad de correr hacia atrás así como también hacia adelante, de mane­ ra que tenemos dos relojes en uno. El círculo exterior representa el reloj que corre hacia adelante, mientras que el círculo interno representa el que corre en sentido inverso. Las C. son restricciones y representan intentos de igualar términos PROLOG Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

351

T con otros en la base de datos. R representa la regla que estamos probando para un emparejamiento para el reloj=i/y las s y las tik son los términos que todavía serán emparejados en los tiempos sucesivos. Sigamos a través de la ejecución de ?-append(A,B,[l,2]). para los diversos tiempos i del reloj como se ilustra en el listado (7.2.10). (7.2.10)

Rl: append([] ,L,L). R2: append([X|L1],L2,[X|L3]).

i := 0 C0 = 0 T0 = {append(Al,Bl,[1,2])} R0 := Rl Cl = {append(AlfB l # [l,2]) =? appendí[],L1,L1)}

Tl = 0 1 := 1 Al =[ ], B1=L1, Ll = [1,2]

print answerl:

A

= [], B = [1,2]

i := 0

R0 := R2; C, = {append(A2,B2,[l,2]) =? append([Xl|Lll],L21,[Xl|L31])} Tj = {append(Lll,L21.L31)}

i

1

A2=[l|Lll], 1=X1, B2=L21, [2]=L31 R, := Rl C2 = Cj u

{append(L11,L21,[2]) =? append([].L2.L2)

- 0 i := 2 Lll=[], L21=[2], [2] = L2

print answer2:

A

= [1], B = [2]

i := 1 Rt := R2 C2 = {Cj v

{append(L12tL22,[2]) *? append([X2|L13],L23,[X2|L33]}

l z = {append(L13,L23tL33)}

i := 2 L12=[2], L22=L23, 2=X2, []=L33 R2 := Rl C3 = C2 u

{append(L13,L23,[]) =? append([],L 3 »L3)}

t3- 0 i := 3 L13=[] , L23=[] , [] = L3

print answer3:

A

= [1,2], B = []

i := 2 R2 := R2 C3 = C2 u

{{append(L14, L24,[]) =? append([X3|L15],L25,[X3|L35])}

T3 = {append(L15,L25,L35)}

Sólo fines educativos - FreeLibros

352

PARTE IV: Lenguajes declarativos i

:= 3

d ead end

i := 2 i := 1

i := 0 end

Otras máquinas teóricas han sido diseñadas para interpretar o compilar pro­ gramas PROLOG, pero el reloj PROLOG de Colmerauer fue el primero y todavía es comúnmente utilizado.

Arquitecturas paralelas PROLOG está admirablemente organizado para el procesamiento en paralelo. Si una regla es tO ti, t 2 ,..., tn con meta tO, y tenemos disponibles n procesadores, ¿por qué no resolver todas las n submetas de manera simultánea? Esta clase de ejecución en paralelo se conoce como paralelismo y, porque en una cláusula tal como A B , C, intentaremos probar B y C de manera concurrente a fin de probar A. Por desgracia, los programas PROLOG trabajan de izquierda a derecha, y con frecuen­ cia el orden de los términos es importante. Por ejemplo, supongamos que defini­ mos una relación descendente como en el listado (7.2.11). Rl: descendiente(Y.X) padre-de (X.Y). R2: descendiente(Y,X) :- padre-de (X,Z), descendiente(Y.Z).

(7.2.11)

Esto funciona muy bien, pero ahora consideremos: R2': descendiente(Y,X) :- descendiente^,Z), padre-de(X,Z).

R2* contiene la misma información que R2, pero invierte el orden de las dos submetas. Como vimos en la sección "Recursión de cola", R2* es recursiva por la izquierda y producirá un ciclo infinito. La resolución de descendienteCY,Z) requiere que padre-de(X,Z) ya esté resuelta. El paralelismo y requiere que las submetas sean independientes entre sí, o que algún otro método sea diseñado para evitar colisiones de variables. Por ejemplo, en Concurrent PROLOG, la cláusula A B ( X ) , C ( X ? ) restringe la asignación a X de un valor en B(X). C(X?) sólo puede leer X, no escribir en él. Sin embargo, existe una ventaja oculta en las restricciones de asignación. Una variable puede ser utilizada como un canal de comunicación con procesos tales como C(X?) esperando hasta que X haya recibido un valor. El paralelismo o, el cual elimina el retroceso, es sustancialmente más fácil. Si observamos de nuevo el listado (7.2.10), y si todo el procesamiento a un tiempo dado ocurre en paralelo (es decir, la asignación,i := i-1 del reloj interno nunca pasa), tendríamos un ejemplo de paralelismo o. Y el paralelismo involucra procesamien­ to concurrente a través de diferentes niveles de tiempo. El paralelismo o trabaja de manera concurrente sobre cláusulas tales como: A A

B. C.

Sólo fines educativos - FreeLibros

CAPÍTULO 7: Programación lógica

353

Aquí podemos probar que A es verdadero ya sea al probar B o C. Trabajamos de manera concurrente tanto en B como en C, deteniéndonos cuando B o C se resuel­ van. La prueba de A es entonces indeterminada. Cuando PROLOG anuncia que A es verdadero, podemos no saber o no importa si B o C es también verdadero. El no determinístico "no importa" es una característica de los sistemas cerrados, en los que sólo las resoluciones exitosas son visibles al usuario. PROLOG está con fre­ cuencia implementado como un sistema reactivo, donde el usuario puede ver re­ sultados parciales a medida que progresa un cálculo. Esto requiere un "no importa" no determinístico, también llamado indeterminismo. Una submeta puede fallar, pero a la aplicación no le importa mientras que la meta primaria pueda ser demos­ trada. El desarrollo de procesadores en paralelo y compiladores para sacar ventaja de ellos recibe en la actualidad mucha atención de investigación. Por ejemplo, véase [ProcSLP, 1986]. Una buena exposición de las complejidades de la ejecución en pa­ ralelo utilizando Parlog86 puede encontrarse en [Ringwood, 1988]. Recolección de basura La ejecución en paralelo puede acelerar el procesamiento, pero los programas en PROLOG todavía consumen grandes cantidades de memoria. Para mantener sepa­ radas las variables, cada vez que una regla es invocada, sus variables deben estar renombradas. Cuando una resolución particular se haya completado, estas varia­ bles pueden todavía existir. El reclamo de las localidades de memoria que ya no son necesarias a través de una reorganización de la memoria se conoce como reco­ lección de basura. La máquina teórica de la figura 7.2.6 es una forma simplificada de un compilador PROLOG real. La máquina abstracta de Warren (WAM; Warren Abstract Machine) [Warren, 1988], es un fundamento ampliamente aceptado para las implementaciones. Los datos se mantienen en tres áreas: el área de código que contiene el programa; el área de control formada de los registros de máquina; y tres pilas. La primera pila, que puede o no ser recursiva, mantiene el seguimiento de la cadena de las cláusulas llamadas y las variables simples; la segunda contiene las variables de lista y de estructura; la tercera, llamada el rastreo (trail) se refiere a las variables que tienen que deshacerse durante el retroceso. Se han propuesto va­ rios métodos para la recolección de basura durante la ejecución de la WAM; por ejemplo, véase [Appleby, 1988]. Tipos y módulos Aparte de la ineficacia en espacio y tiempo, PROLOG tiene otras desventajas [Genesereth, 1985]. Una de ellas es que la lógica utilizada se basa en sistemas com­ pletos (mundo cerrado) y no está adecuada para aplicaciones que generalizan el conocimiento más allá de la base de datos, razonamiento por analogías, o hacer deducciones a partir de datos inciertos. La capacidad para escribir, depurar y man­ tener programas grandes también está limitada debido a una carencia de tipificación de datos y modularidad, aunque los módulos son soportados en algunas implementaciones. Cuando hablamos de tipificación de datos, no queremos decir sólo Sólo fines educativos - FreeLibros

354

PARTE IV:

Lenguajes declarativos

enteros, variables o de carácter, sino la capacidad del usuario para definir y mante­ ner tipos de datos abstractos, junto con sus operaciones asociadas. Las extensiones orientadas a objetos, tales como IPW en el IBM PROLOG, han sido implementadas para este propósito. Goguen y Meseguer [Goguen, 1984] han sugerido una revisión y extensión de PROLOG, llamada Eqlog. Incluye igualdad genuina, como en 3 + 4 * 5 = 23; tipos definidos por el usuario, llamados sorts (clases); módulos, tales como conjuntos de enteros con definiciones para membresía y unión; y un mecanismo para definir módulos genéricos. No discutiremos aquí precisamente lo que es un módulo gené­ rico, pero daremos, en su lugar, el ejemplo de Goguen para un conjunto casi-ordenado. (una casi-ordenación es reflexiva y transitiva, pero no cualesquiera dos ele­ mentos necesitan ser comparables). theory QUOSET is sorts e l t preds _=<_ : e l t . e l t vars A,B,C : e l t axioms

(7.2.12)

A *< A. A *< C :- A =< B, B =< C endtheory QUOSET

Un usuario puede entonces solicitar tener un objeto Eqlog, X, "certificado" para ser de clase genérica QUOSET. Si tal certificación es exitosa, X debe ser casi-ordenada. Un módulo, incluyendo predicado, función y definiciones de variable, y un grupo de axiomas utilizando la clase QUOSET podría ser: module

INTS0RT[T::QU0SET]

endmodule

using

INTSET - SET[INT]

(7.2.13)

INTSORT

Aplicaciones

Inteligencia artificial La inteligencia artificial es un término definido en forma vaga que comprende acti­ vidades, llevadas a cabo por computadoras, que se piensa que ordinariamente re­ quieren de alguna clase de inteligencia humana. Estas incluyen: la comprensión del lenguaje natural hablado y escrito; aprendizaje de nueva información; recuer­ do de hechos previamente aprendidos; análisis científico, planeación y resolución de problemas; y diversas hazañas físicas tal como desplazarse en un cuarto sin chocar contra los muebles. PROLOG está siendo utilizado en todas estas áreas. Otra área es la también llamada sistemas expertos. Para algunos sistemas, tal como MYCIN, que se utiliza para diagnosticar y recomendar una terapia para en­ fermedades infecciosas, un ingeniero de conocimiento entrenado extrae informa­ ción de expertos médicos y luego la incorpora dentro de un programa de computadora que podría proporcionar el saber combinado de los expertos. MYCIN no fue escrito en PROLOG, sino en LISP, el cual es reconocido generalmente por ser más difícil de aprender. Algunos desabolladores creen que la ingeniería del cono­ Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

355

cimiento ya no será necesaria cuando las interfaces de usuario para sistemas exper­ tos, llamadas entornos (shells), lleguen a ser más fáciles de utilizar. Bases de datos relaciónales Sin duda, el uso más frecuente de las computadoras es en la construcción y mante­ nimiento de bases de datos. Cada empresa con más de unos cuantos empleados tiene que conservar registros para propósitos de pago de nómina e impuestos. Cada negocio de fabricación o distribución al detalle debe tener un control de inventario. La cantidad de datos conservados por los gobiernos local, estatal y federal es mo­ numental, entre ellos información acerca de salud, criminalidad, etcétera. De esta manera no es sorprendente que el desarrollo de nuevas y mejores formas de admi­ nistrar esta información haya sido, y continuará siendo, un área de interés. Una vez que se establece una base de datos, es bastante difícil volver a organi­ zaría o cambiarse a un nuevo y mejor administrador de base de datos. La base de datos relacional, con su base teórica en el álgebra relacional (operaciones para la manipulación de relaciones), ha llegado a ser el estilo más prometedor. PROLOG, que está basado en el concepto de relación, es así un lenguaje ideal para estas apli­ caciones. La quinta generación Durante la década de los ochenta, la adopción de PROLOG por los japoneses como el lenguaje central para su esfuerzo de quinta generación impulsó un interés seme­ jante en la industria y las universidades. Japón planeó empacar y vender conoci­ miento, así como otras naciones comercian con el vino o la ropa. Para hacer esto, sus computadoras necesitan ser inteligentes; es decir, capaces de "aprender, aso­ ciar, hacer inferencias, tomar decisiones y algún otro comportamiento en maneras que siempre hemos considerado del dominio exclusivo de la razón humana" [Feigenbaum, 1983]. Según el investigador japonés Ichikawa, citando a Shigeru Watanabe, "la IA es una tecnología que analiza el conocimiento y el juicio utilizado por los seres humanos, e intenta emplearlos en la computadora" [Ichikawa, 1991]. Los japoneses contemplaron un sistema de computadoras utilizando PROLOG como su lenguaje central. PROLOG estaba por ser diseñado dentro del hardware mismo. Aunque los planes presentes no contemplan máquinas PROLOG per se, el Proyecto de Quinta Generación (Fifth Generation Project) movilizó cerca de 50.5 mil millones de yenes (472 millones de dólares) del año fiscal 1982 al 1991, inclu­ yendo el desarrollo del lenguaje de programación lógica en paralelo tipo PROLOG, KL-1. Incluye algunas funciones de sistema operativo así como también modularidad y procesamiento concurrente. Como un experimento, 64 computadoras fue­ ron conectadas en paralelo bajo el sistema operativo PIMOS (Parallel Inference Machine Operating System) ejecutando KL-1. Las eficiencias de tiempo fueron medidas en 5-8 mega LIPS (inferencias lógicas por segundo, por sus siglas en in­ glés). El objetivo de este proyecto fue el desarrollo de capacidades de procesamien­ to de inform ación de conocimiento. La investigación y el desarrollo están continuando, con vina nueva meta de conectar mil computadoras de inferencia en Sólo fines educativos - FreeLibros

356

PARTE IV:

Lenguajes declarativos

paralelo, con una velocidad de inferencia de 200 mega LIPS. Los japoneses también han desarrollado un lenguaje basado en PROLOG llamado PROLOG Auto-conte­ nido Extendido (ESP; Extended Self-Contained PROLOG) para programar muchos proyectos de quinta generación en PC y estaciones de trabajo. ESP se ejecuta bajo UNIX. Las aplicaciones abarcan desde la exploración de recursos, pasando por el diag­ nóstico médico y funciones de biblioteca, hasta sistemas de armamento. Las áreas de investigación están aproximadamente organizadas bajo inferencias y resolu­ ción de problemas, bases de conocimiento, interfaz humano-máquina, soporte de desarrollo y sistemas básicos de aplicaciones. Una característica extraordinaria del esfuerzo japonés fue su plan de implementación a diez años, con cooperación interindustrial, universitaria y del gobierno. El uso de máquinas especiales para proyectos de inteligencia artificial está dis­ minuyendo en Japón, llevándose a cabo casi la mitad de ellos en computadoras personales. LISP es el lenguaje utilizado por cerca del 35 por ciento de proyectos de IA, 33 por ciento de C, y un mero 5 por ciento de PROLOG. La área más activa de aplicaciones de IA es el desarrollo de sistemas expertos. L A B O R A T O R I O 7. 2: C A N Í B A L E S Y M I S I O N E R O S : P R O L O G Objetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Ver ejemplos de programas PROLOG bien escritos y bien documentados. 2. Ver un ejemplo apropiado de un programa que resuelva un problema no determinístico interesante. 3. Ver un buen uso de la suma y/o eliminación de relación a través del uso de assert y re tra ct.

4. Observar la flexibilidad y utilidad de las listas de PROLOG. Fortalezas y debilidades PROLOG tiene varias características cuya combinación es única con respecto a las encontradas en otros lenguajes [Cohén, 1985]. Éstas son: 1. 2. 3.

Cada parámetro para un procedimiento puede ser tanto entrada o salida para cada invocación, como desee el usuario, Los procedimientos pueden devolver resultados con variables no ligadas, pre­ sentando así soluciones parcial o genérica para un problema. Pueden encontrarse soluciones múltiples haciendo uso del retroceso inte­ grado.

Cohén también elogia la base lógica de PROLOG en los intereses de la especifica­ ción efectiva del problema, el potencial para procesamiento en paralelo y lo conci­ so de los programas PROLOG: estimados como cinco a diez veces más pequeños que aquellos escritos en un lenguaje de procedimientos. Sin embargo, existen desventajas reconocidas, además de las mencionadas en [Goguen, 1984; Cohén, 1985; Feigenbaum, 1983]. No es muy fácil para el no inicia­ do leer o escribir información utilizando el cálculo de predicados de primer orden Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

357

(PC; predícate calculus). Pero una vez que se hace, PROLOG toma el control y hace inferencias por cuenta propia. Algunos ven esto, también, como un defecto serio, puesto que los programadores experimentados pueden mejorar la eficiencia de la ejecución del programa si pueden controlar el método de solución. Como existe ahora, PROLOG no tiene mecanismo para especificar la ejecución en paralelo, ni estructura de bloques, ni metodología para documentación ni verificación de tipos.

E J E R C I C I O S 7.2 (Si es posible, es mejor hacer la mayoría de estos ejercicios en una computadora. Sin embargo, piense acerca de cada uno primero y luego vea lo que ocurre realmente en la computadora.) 1. Lea "PROLOG Dialects: a deja va BASICs" [Sosnowski, 1987], y compare los méritos de las versiones de Edimburgo, Turbo y micro PROLOG. 2. ¿Qué respuestas esperaría usted para las consultas: a. ?-append2(X,[2],Cl,2]). b. ?-append2(X,Y,[ 1 , 2 ] ) . c. Intente la consulta del listado (7.2.6) empleando append2 tanto con corte como sin corte. d. Considere ? -append2 ( comanche, [ ] , Z). ¿Podría usted arreglar esto de manera que PROLOG no acepte esta entrada? 3. Escriba una consulta PROLOG para agregar la dirección, el estado y el código postal para el nombre "Donald Trump". 4. Considere un procedimiento PROLOG para invertir una lista: i nver so( [ ] , [ ] ) . 1nversoíCX|Y],2)

inverso (Y.Zl), append2(Zl,[X],Z).

a. Lleve a cabo la construcción de un árbol de resolución para ? - i nver so ( [ 1 ( 2] f Z). b. Construya pilas para X, Y y Z. ¿Es este procedimiento recursivo de cola? 5. Ahora considere un procedimiento diferente para invertir una lista: inverso2(Ll,L2) inverso3 (L1, [], L2). inverso3([ ] , L1, Ll ) . inverso3([X|Ll],L2,L3) i nverso3(Ll, [X|L2], L3). (inverso3 se introduce con el solo propósito de hacer la llamada más natural. La segunda variable de inverso3 se emplea para acumular los resultados parciales). a. Trace la ejecución de ? - i nv e r s o 2 ( [ l , 2 ] , R) . b. ¿Es este procedimiento recursivo de cola? 6. La consulta ? - ha 11 artodoíX , p(X), Li s t a ) . devolverá la Lista de todos los valores que satisfa*gan p(X). ¿Cuál valor esperaría que PROLOG devolviese para Lista si p( X) es ti ene (tomas ,X) (de la base de datos en la sección "Resolución" de la sección 7.1)? 7. Una función PROLOG para el cálculo de N! es:

Sólo fines educativos - FreeLibros

358

PARTE IV:

Lenguajes declarativos

factorial(0,1). factorial(N,M)

NI is N - 1, factorial(NI,MI), M is N * MI.

a. Construya el árbol de resolución y la pila para ?- factor i al (3,X). b. ¿Es este procedimiento recursivo de cola? c. Si su respuesta al inciso b es no, ¿puede escribir un procedimiento factorial 2CN,M) que sea recursivo de cola? 8. La serie de Fibonacci es (1, 1, 2, 3, 5, ...), donde cada término después del 0-ésimo y el primero es la suma de los dos términos anteriores; es decir, Fib(i) = Fib(i-l) + Fib(i-2). Un procedimiento de PROLOG para Fib es: Fib(O.l). Fib(l.l). Fib(N,M)

9. 10.

11. 12.

13.

NI is N - 1, N2 is N - 2, Fib(NI,MI),Fib(N2,M2), M is M1+M2.

Este procedimiento no es recursivo de cola. ¿Puede usted hacer un nuevo procedi­ miento Fib2(N,X,M), que utilice X para almacenar resultados parciales y que sea recursivo de cola? Escriba en PROLOG lo que pueda ser una definición para el predicado /2. ¿Debe ser recursivo? ¿Por qué? ¿Qué puede ocurrir si permitimos la sustitución (Y/f(Y)) en una implementación PROLOG? Considere la consulta ? - Y-f (Y). Intente esta consulta, utilizando cualquier functor para f, con su versión PROLOG y vea qué es lo que pasa. ¿Por qué la lista [ ' A’ , ' B\ ‘C'« ‘D ’] se devuelve cuando se emplea m a p c a r (listado (7.2.3)) con el functor mayuscul as, en lugar de [A, B, C, D]? Lea "The British Nationality Act As a Logic Program" [Sergot, 1986] y escriba un resumen y /o informe para su clase acerca de este muy interesante uso de PROLOG para obtener un complicado punto de la ley británica. Considere el siguiente conjunto de hechos acerca de jardinería: flor(flox). flor(petunia). flor(rosa). f l o r (margar i t a ).

t i p o t f l o x , p e r e n n e ). ti po ( p e t u i n a , a n u a l ). tipo(rosa mata), ti poí mar g a r i t a . p e r e n n e ) . t i po ( m a r g a r i t a .a n u a l ).

a. Haga uso de las utilidades debug y /o trace de PROLOG para "trazar" o "ras­ trear" la ejecución de ? - j a r d i n _ p e r e n n e ( X ) . , si j a r d i n _ p e r e n n e es la regla: jardin_perenne(F) flor(F), tipo(F, perenne). b. Agregue un corte a la regla en a y vuelva a rastrear de nuevo. c. ¿Cuál regla podría producir una lista para todo el jardín perenne? ¿Cuál es más rápida? 14. Considere la siguiente definición utilizando corte [Clocksin, 1984]: nu mero_de_padres(adan,0) numero_de_padres(eva,0) n u m e r o _ d e _ p a d r e s (X ,2).

!. !.

Sólo fines educativos - FreeLibros

CAPÍTULO

7: Programación lógica

359

a. Cuál será la respuesta de PROLOG para: ?-numero_de_padre$(beatriz,N). ? - n u m e r o _ d e _ p a d r e s ( X , Y ). ?- n u m ero _de _p adr est eva ,2). b. Clocksin y Mellish arreglan esto como: numero_de_padres(adan,N) numero_de_padres(eva,N) numero_de_padres(X,2).

!, N » 0. !, N - 0.

¿Puede usted pensar en otra manera de hacer esto, mediante la modificación de la última cláusula, en vez de las dos primeras? c. ¿Funcionará cualquiera de éstos para: ?-numero_de_padres(X,Y). 15. Reescriba la regla siguiente utilizando c u t / f al 1 para una regla equivalente utilizan­ do not. matrimoniable(X.Y)

(primos_primeros(X,Y); mi $ m o _ s e x o ( X , Y ) ; he r m a n o s (X ,Y ))*

¡.rail. matrimoniable(X,Y)

!.

16. Considere la siguiente base de datos Hamburguesa: c o n d i m e n t o ( c a t s u p ). vegetal es(cebolla), condi m e n t ó ( m o s t a z a ) . vegetal e s (1e c h u g a ). quesoC cheda r ). queso(suizo), hamburgesaconqueso c o n d i m e n t o ( X ) , vegetal e s (Y ), queso(Z). Rastree la ejecución de ?-hamburguesaqueso. a travésdel reloj PROLOG de la figura 7.2.5. 17. Programe el procedimiento append en un lenguaje de procedimientos que usted co­ nozca, tal como Pascal, C, Ada o FORTRAN. Comente las diferencias entre este pro­ grama y el de PROLOG con respecto a: a. facilidad de programación b. velocidad de ejecución c. versatilidad d. diferencias de E /S 18. El problema de los Caníbales y los Misioneros involucra tres misioneros, tres caníba­ les, una canoa y un río. El problema es lograr que los seis crucen el río sin que en algún momento estén más caníbales que misioneros en cada lado del río. La canoa sólo tiene cupo para dos personas. Examine la solución de Eqlog a este problema en [Goguen, 1984, pp. 204-206] y rastree su ejecución.

7.3

RESUMEN El fundamento para la programación lógica es el cálculo de predicados, una exten­ sión de los sistemas lógicos de Aristóteles. La lógica aristotélica se usa para encon­ Sólo fines educativos - FreeLibros

360

PARTE IV:

Lenguajes declarativos

trar nueva información de una base de datos dada siguiendo las reglas de la deduc­ ción. Una de estas reglas es llamada reducción al absurdo (reductio ad absurdum), donde suponemos que la proposición que va a demostrarse es falsa, y se deriva de esto una contradicción. Una versión del método de reductio para demostración es la base del lenguaje de programación lógica, PROLOG. El Teorema de Resolución establece que: q es una consecuencia lógica de p17 p2, . . . , pn si (- rel="nofollow">q & px & p2 & . . . & pn) es FALSE (falsa). T P i ' P2' * •*' Pn (Tue significa que la veracidad de p 1y p2. . . y pn implica también la veracidad de q) se resuelve en PROLOG al probar primero todas las submetas de px hasta pn, y luego derivando una contradicción de la inclusión de la negación de q (not(q)). Si las metas incluyen variables; por ejemplo, p.(x) y pj(y), se busca sustituciones para unificar las dos metas, haciendo ambas verdaderas. Esto puede ser sustituyendo z tanto para x como para y, (p.(z) y p (z)), y por último ReyTut por z (p.(ReyTut) y p.(ReyTut)). PROLOG, existente en varios dialectos, se basa en la lógica y hechos, reglas y consultas. Las reglas producen derivaciones de nuevos hechos a partir de otros antiguos, mientras que una consulta pregunta si una proposición dada es cierta o falsa de acuerdo con los hechos existentes; por ejemplo, la consulta ? - ( c ual (x(vi ve_en(México,x)) ) ) . verificaría la relación de los dos lugares vi ve_en para ver cuáles personas (los valores de x) en la base de datos viven en México. El retroceso es el método de deshacer el camino o pista de una derivación que ha llevado a un punto muerto antes de alcanzar una solución, y volver a intentar otra trayectoria. El retroceso también puede utilizarse con el fin de hallar más de una solución para una consulta. El retroceso es el método natural para el encadenamiento hacia atrás; es decir, razonamiento desde una meta regresando a través de las reglas hasta los hechos subyacentes. Esto involucra demostrar primero los lados derechos de las reglas. El encadenamiento hacia adelante también es posible en PROLOG, donde iniciamos con la meta y exploramos todas las posibles reglas o hechos que podrían conducir a ella. Esto involucra el razonamiento desde los lados derechos de las reglas. En general, intentamos movemos desde el conjunto de estados más pequeño hacia el más grande (fácil de encontrar). Por ejemplo, si existen muchos teoremas y sólo unos cuantos axiomas, razonamos desde los axiomas hacia los teoremas; es decir, hacia atrás rumbo al teorema meta. PROLOG, en su forma actual, es ineficiente tanto en el uso de tiempo como de almacenamiento o memoria, pero es el primer lenguaje funcional basado en lógica, con otros que están siendo desarrollados rápidamente. Algunos de éstos incluyen facilidades para la ejecución en paralelo. PROLOG formó el fundamento inicial para el Proyecto de Quinta Generación japonés con el fin de mecanizar y distribuir información con rapidez. PROLOG es empleado para inteligencia artificial, en especial donde el razona­ miento formal, tal como el involucrado en la demostración de teoremas, es necesa­ rio. También es un lenguaje natural para las bases de datos relaciónales, puesto que cada hecho se expresa como una relación en una base de datos. Sólo fines educativos - FreeLibros

CAPÍTULO 7: Programación lógica

361

PROLOG ha sido implementado de diversas formas, entre la que destaca por su uso más común la Máquina Abstracta de Warren (WAM; Warren Abstract Machine), que trabaja como un reloj que corre hacia adelante (estableciendo nue­ vos hechos) y hacia atrás (retrocediendo y deshaciendo trayectorias de derivación inútiles).

7.4

NOTAS SOBRE LAS REFERENCIAS J. Alan Robinson escribe bellamente mientras explica la teoría básica. Su presenta­ ción original [Robinson, 1965] de la resolución es bastante entendióle. Para su pun­ to de vista sobre el futuro, véase [Robinson, 1983]. Una discusión extendida acerca 4 e las relaciones de la lógica con la programación puede hallarse en Hoare y Shepherdson [Hoare, 1985]. En diversas revistas se publican con regularidad artículos acerca de PROLOG. Una guía de estas publicaciones puede encontrarse en [Cohén, 1988] y [Poe, 1984]. También examine números del PROLOG Digest para controversias de actualidad. El número de enero de 1988 de la Communications o f the Association fo r Computing Machinen/(CACM) contiene buenos artículos históricos acerca de PROLOG [Cohén, 1988]; [Kowalski, 1988]. Un número más antiguo (diciembre de 1985) también está dedicado a PROLOG. Para una buena introducción a las ideas subyacentes, áreas de aplicación y un manual del mismo PROLOG DEC-10, véase [Malpas, 1987]. Clocksin y Mellish [Clocksin, 1984] así como Clark y McCabe [Clark, 1984] son las referencias estándar de principiantes para Edimburgo PROLOG y micro PROLOG, respectivamente, y con frecuencia se incluyen en la adquisición de un compilador o intérprete. La Máquina Abstracta de Warren (WAM, por sus siglas en inglés) no es el úni­ co modelo para la compilación de PROLOG. Éste ha sido compilado en lenguajes intermediarios que son conocidos por ser razonablemente eficientes y están implementados en un gran número de máquinas. Se ha hecho trabajo en Pascal y C, entre otros [Weiner, 1988]. Los sistemas expertos son únicamente una aplicación de lo que por lo general se conocen como sistemas basados en reglas o RBS (Rule-Based Systems). Para un repaso, véase [Hayes-Roth, 1985]. Si usted no está familiarizado con la serie Computing Surveys, ahora es tiempo de que sea así. Estas publicaciones trimestrales son escritas por estudiantes y proporcionan un estudio de importantes áreas de investigación, o tutoriales. Para un excelente tratamiento de la lógica y las bases de datos en esta serie, véase [Gallaire, 1984]. El número especial de ACM Computing Surveys sobre paradigmas de lenguajes de programación [Wegner, 1989] discute el paralelismo en la programación lógica en dos artículos. El primero [Bal, 1989] es fácil de comprender y discute tanto el paralelismo "y" como el paralelismo "o " en el contexto de la concurrencia en gene­ ral. El segundo [Shapiro, 1989] se dedica por completo al paralelismo lógico, y es difícil de leer, pero completo. Incluye una discusión de las implementaciones de más actualidad: GHC, Parlog, FGHC, P-PROLOG, ALPS, FCP, Concurrent PROLOG y CP. Sólo fines educativos - FreeLibros

X

CAPÍTULO 8 PROGRAMACIÓN FUNCIONAL (APLICATIVA)

8.0 En este capítulo 8.1 Características de los lenguajes funcionales

365

Composición de funciones Funciones como objetos de primera clase Ausencia de efectos colaterales Semántica limpia

365 366 367 368

8.2 LISP

369

Viñeta histórica: LISP: John McCarthy El lenguaje LISP (dialecto SCHEME) Tipos de datos Método para almacenamiento de datos Funciones integradas Formas funcionales apply,eval y operadores aritméticos Recursión y control Efectos colaterales Una función automodificante Otras características no funcionales Iteración Vectores y cadenas Objetos y paquetes

365

369 371 371 373 376 378 380 381 383 385 388 388 389 389

Dialectos Common LISP Ejercicios 8.2

394 394 395

8.3 Implementación de lenguajes funcionales

396

Evaluación débil (lazy evaluation) contra evaluación estricta (strict evaluation) Alcance y ligaduras Los problemas funarg Recolección de basura Ejercicios 8.3

398 399 401 404 405

8.4 Soporte de paralelismo con funciones 8.5 Otros lenguajes funcionales

405 407

APL ML Tipos de datos Tipos de datos polimórficos Módulos Excepciones Definición semántica de ML Otros Ejercicios 8.5

407 408 408 412 412 413 413 417 417

8.6 Resumen 8.7 Notas sobre las referencias

417 418

Sólo fines educativos - FreeLibros

CAPÍTULO

8

Programación funcional (aplicativa)

Una función es una "asociación de cierto tipo de objeto (u objetos)1de un conjunto (el rango) con cada objeto de otro conjunto (el dominio). Por ejemplo, una función puede ser definida como la edad de cada persona cuando se especifica la persona, se diría entonces que "la edad de una persona es una función de la persona, y que el dominio de esta función es el conjunto de todos los seres humanos, y que el rango es el conjunto de todos los enteros que son las edades de las personas con vida actualmente" [Glenn, 1959]. Una función es entonces una expresión y sus va­ lores asociados, donde la expresión proporciona un método o regla para hacer la asociación entre los valores de dominio y de rango. Las funciones pueden tener nombres, sin embargo, no lo necesitan. Si edad es el nombre de una función, enton­ ces edad(Amalia) = 7 es una manera de indicar que el valor de la expresión edad, edad(Amalia), cuando se asocia con Amalia, es 7. Otra sintaxis, que es común a la mayoría de los LISP, uno de los lenguajes funcionales que serán considerados en este capítulo, es (edad Amalia), la cual se evalúa a 7. Una tercera forma es la utiliza­ da por PROLOG, (edad Amalia 7). Sin importar cómo se escriba, la función edad, al ser aplicada al parámetro Amalia, devuelve el valor de 7. La palabra aplicativa incluye la noción de alguna clase de proceso o regla para construir el valor de una función a partir de los valores de parámetros que se le presentan. Una función por lo regular puede aplicarse a diferentes valores de pará­ metros en distintas invocaciones. Al definir una función como una expresión parametrizada que devuelve un solo valor, ello implica que existe algún método, posiblemente más complicado, de llegar al valor simple dados valores de paráme­ tros particulares. Una expresión funcional, cuando se aplica a un conjunto de pará­ metros, devuelve el valor de la expresión.

1 Una de las características distintivas de una función es que sea de valor único; es decir, para cada valor del dominio, existe exactamente un valor asociado del rango. Una función puede tener valores múltiples y tod a v ía satisfacer este requerimiento si ponemos los valores en una tupia simple, por ejem­ plo: (objj, obj2, .. v objn). El rango debería ser entonces un conjunto de tupias de objetos de otro conjunto. Los valores de una función no necesitan incluir todos los elementos de su conjunto rango, pero cada elemento del dominio debe estar asociado con algún valor en el rango.

Sólo fines educativos - FreeLibros

364

PARTE IV:

Lenguajes declarativos

Las características distintivas del paradigma funcional son: • • • •

Los programas se construyen como la composición de funciones. Las funciones son soportadas como objetos de primera clase. No hay efectos colaterales (bueno, quizá unos pocos). Es posible una semántica limpia y sencilla.

El control se consigue por lo regular a través de recursión en vez de los mecanis­ mos de ciclo iterativo utilizados con frecuencia en los lenguajes imperativos. Entre las ventajas de los lenguajes funcionales se encuentra la simplicidad.2En un lenguaje de procedimiento, un bloque principal puede comprender tres llama­ das de procedimientos: begln ObtenerDatosí...); ProcesarDatosí...): SalIdaResultados end.

En un lenguaje funcional, esto se realiza con una sola expresión (imprime(proceso-datos(obtener-datos(...)))).

(8.1.1)

Aquí el valor de la expresión o bt e n e r - d a t o s (. . .) se utiliza como entrada a la función pr o ce s o- d at o s. La función imprime toma entonces el valor de procesodatos como su argumento.3Empleando una función como un argumento para otra función, o como el valor de una variable, es lo que caracteriza las funciones como primera clase. Pueden ser utilizadas en cualquier lugar en que otro objeto pueda, en particular, como el valor de una variable. Tal vez la característica más impactante de las funciones siendo objetos de primera clase y los programas siendo funciones es que los programas pueden tratarse como datos y modificarse en tiempo de eje­ cución. Los defensores del lenguaje funcional afirman que los programas pueden ser escritos rápidamente, están más cercanos a la notación matemática tradicional, son más sencillos de verificar y pueden ser ejecutados con más facilidad en arquitectu­ ras en paralelo que los lenguajes imperativos tradicionales [Hudak, 1989]. El primer lenguaje funcional LISP (de Procesamiento de LIStas, por sus siglas en inglés), fue implementado en la década de los cincuenta por John McCarthy. Su descripción original de LISP [McCarthy, 1960], cuyo contenido incluía un prefacio motivacional y una descripción de un intérprete para la IBM 704, necesitó sólo de 12 páginas. Sin embargo, más importante que un económico manual del lenguaje, es que la semántica o significado de las expresiones es muy simple. De este modo, las pruebas de corrección son bastante posibles para muchos programas. La nota­

2Aquí empleamos la palabra "simplicidad" en su sentido matemático como sinónimo de elegancia, no de facilidad. Una expresión simple se considera más simple que un bloque conteniendo tres propo­ siciones. 3 En términos funcionales, un parámetro es llamado un argumento para una función.

Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

365

ción de LISP está basada en la teoría de las funciones, como se escribió en el cálculo lambda (cálculo X) de Alonzo Church [Church, 1941], el cual se discute en el Apén­ dice B. Del mismo modo que PROLOG, el lenguaje de programación basado en la ló­ gica que vimos en el capítulo 7, efectúa algunos acomodamientos prácticos para el cálculo de predicados, la implementación de los lenguajes funcionales no sigue el cálculo lambda de manera exacta. Un programador funcional encontrará algu­ nas características no funcionales en el lenguaje utilizado, así como también la dis­ ponibilidad de algunos efectos colaterales; por ejemplo, entrada y salida.

8.0

EN ESTE CAPÍTULO Discutiremos las características principales del paradigma funcional como se enu­ meraron anteriormente. En su trabajo, Church demostró que todo lo que se necesita teóricamente para expresar todas las partes demostrables de las matemáticas es el cálculo lambda. De este modo un lenguaje de programación que implemente la mayoría del cálculo lambda puede ser bastante poderoso. Existen consideraciones prácticas que deben tomarse en cuenta cuando se transforme una teoría matemática en un lenguaje que pueda ser interpretado para controlar una computadora digital. De esta forma, LISP tiene, además de las reglas notacionales, o sintaxis: • • • •

Un método para almacenamiento de datos Un conjunto de funciones integradas Un conjunto de formas funcionales Operadores para la aplicación de una función a los parámetros y para la eva­ luación de los resultados

Como un ejemplo, examinaremos el dialecto SCHEME de LISP sin tipos. Tam­ bién revisaremos en forma breve un lenguaje funcional más moderno, ML (acrónimo de Meta Lenguaje)4 el cual es fuertemente tipificado.

8.1

CARACTERÍSTICAS DE LOS LENGUAJES FUNCIONALES Composición de funciones En el último capítulo presentamos las relaciones como tupias ordenadas. Una fun­ ción también puede pensarse como una clase especial de relación, f = (\ , x2, . . . , xn, y) donde la última coordenada y, llamada su valor, está determinada únicamente por los valores de xl7 x2, .. . xn. Una relación funcional se escribe a menudo como

4 Un metalenguaje es un lenguaje utilizado para discutir algún otro lenguaje o sistema simbólico. ML se emplea para discutir la teoría de las funciones en el contexto de un lenguaje de programación de computadoras.

Sólo fines educativos - FreeLibros

366

PARTE IV:

Lenguajes declarativos

f(xx, x2, .. ., xn) = y, donde las x. se conocen como los argumentos de f, mientras que y es el valor, cuando f se aplica a x2, x2, . . xn. Aquí las x. son las variables indepen­ dientes de f, y la y es la variable dependiente, puesto que su valor depende de los valores de las x.. Una función también puede considerarse como una expresión f ( x , x2, . . xn), que puede ser evaluada. Estamos bastante acostumbrados a expresiones del tipo de print(xl + x2), donde se imprime 8 si xl = 5 y x2 = 3. En PROLOG, M + N = Sserepresentaríacomolarelación(SUM M N S), la cual es verdadera sólo si S = M + N. SCHEME hace uso de la expresión (+ m n), siendo devuelto el valor de m+ n. Como una relación, la notación ( SUM 2 N 6) en PROLOG tiene sentido puesto que a Nse le puede asignar el valor 4, pero como una función, no lo tiene. La relación SUM puede utilizarse tanto para suma como para resta, pero la regla para una función puede realizar sólo una tarea, y devolver únicamente un valor. Necesitaríamos una segunda función para calcular (- 6 2) y regresar el valor 4. Nótese también aquí la distinción entre una relación y una función. Una relación es una asociación ordenada de elementos frecuentemente enumerados como una tupia, mientras que una función devuelve un valor, dada una tupia ordenada o lista de argumentos de los elementos. Sin embargo, como se advirtió antes, una función puede ser implementada como una clase especial de relación, con una co­ ordenada (por lo general la última) siendo reservada para el valor funcional. El poder expresivo del paradigma funcional proviene de la composición de dos o más f unciones. (+(* w x)(- y z)) representa la composición de las funciones * y - con +. Puesto que la evaluación de una función involucra primero la evalua­ ción de cada uno de sus argumentos, ambos valores de (* w x ) y ( - y z) deberían regresarse a la función +, la que invocaría entonces la regla +. Tal función está com­ puesta de las tres funciones, *, - y +. Existe tres razones por las que se está poniendo atención a la programación funcional [Eisenbach, 1987]. Primero, la notación funcional es concisa, y permite escribir programas más breves y elegantes. En segundo lugar, la teoría de las fun­ ciones matemáticas está bien desarrollada, y permite que los programadores escri­ ban programas que parezcan como especificaciones con sistemas de transformación automática que convierten las especificaciones en programas de ejecución eficien­ te. Por último, los programas funcionales pueden ejecutarse en paralelo en proce­ sadores múltiples. Los dos argumentos para nuestra función anterior, (+(* w x)(- y z)), que son llamadas de función a * y -, podrían evaluarse en paralelo, y luego regresarse a +.

Funciones como objetos de primera clase Una función es de primer orden si toma individuos como argumentos; es decir, cosas tales como números, cadenas, registros, etcétera, y devuelve un valor in­ dividual. En términos funcionales, un individuo es una función de orden 0. Hay dos pasos para la evaluación de una función como f(x). Primero, debe susti­ tuirse un valor conveniente para x; por ejemplo, la sustitución de 2 por x produce f(2). A continuación, f(2) se evalúa de acuerdo a alguna regla de definición para f. Si f(x) es la regla x + 3, entonces f, cuando se aplica a 2, se evalúa como 5; es decir, f(2) = 5. Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

367

Si un dominio D y un rango R son conjuntos de individuos, se dice que una función de D en R es de primer orden; por ejemplo, si tanto D como R son conjuntos de enteros, x e D y e R, entonces (+ x y) es de primer orden. Una función de orden mayor puede tomar otras funciones, también individuos, como argumentos y de­ volver ya sea funciones o individuos como valores. Una función de segundo orden puede tomar funciones de primer orden como argumentos y devolver ya sea fun­ ciones de primer orden o individuos como valores. En general, una función de orden n puede tener funciones de orden n - 1 o menor como argumentos y devol­ verlos también. Las funciones de orden n son importantes para la teoría de las funciones, en la medida en que proporcionan la estructura para pruebas recursivas acerca de funciones. Una función que puede tener funciones de cualquier orden como argumentos y que puede devolver funciones de cualquier orden se denomi­ na de primera clase. Las funciones LISP son potencialmente de primera clase, ya que aceptan funciones de cualquier orden como argumentos y devuelven funciones de cualquier orden como valores. Ya examinamos una función de primera clase ma pea r en el capítulo 7. (mapear fun 1 i s), cuando se suministra con los argumentos fun '+ ' y )is - ((1,1),(1,2),(1,3)), devuelvelalista (2,3,4). mapear aplica la función + sucesivamente a cada elemento de 1i s y devuelve una lista de estos valores. Uno de los factores interesantes acerca de LISP es su punto de vista de los datos como el valor de una expresión. Si (imprime(proceso-datos(obtener-datos ( . . . ) ) ) ) es un programa, puede ser considerado como un segmento de código ejecutable, y este código puede considerarse como los datos mismos. Si nombra­ mos el programa (define hacer-el-trabajoíimprime(proceso-datos(obtener-datos ( - . . ) ) ) ) ) , entonces hacer-el - trabajo tiene el valor (imprime (proceso-datos (obtener-datos ( . . . ) ) ) ) , que puede ser visto ya sea como una función ejecutable o como una lista de cadenas encerradas entre paréntesis. Estas cadenas pueden cam­ biarse por otras funciones, como veremos a continuación.

Ausencia de efectos colaterales Como se anotó en el capítulo 2, se dice que una función (f x y z) tiene un efecto colateral si los valores de x, y, y/o z cambian en el entorno de llamada durante la aplicación de la función a sus argumentos, o si alguna otra acción, como la de im­ primir, ocurre mientras se evalúa f. La mayoría de los lenguajes imperativos implementan el paso de parámetros por valor o por referencia. Una localidad de memoria asociada con un parámetro real en el entorno desde el cual se hace una llamada de procedimiento o de función no es cambiada si la llamada es por valor. De este modo, una función definida con todos los parámetros por valor y donde no se hacen asignaciones a las variables globales, no tiene efectos colaterales. Pero con frecuencia tendremos el efecto colateral después de que pasemos un parámetro por referencia. Un procedimiento en un lenguaje imperativo llamado ObtenerDatos (x , y, z ) muy probablemente será utilizado para proporcionar valores para x, y, y z, y para comunicar esta información a otras partes del programa. Estamos seguros de que los argumentos de variables para una función sin efectos colaterales tienen los mismos valores a la salida de una función que los que tenían a la entrada. Así Sólo fines educativos - FreeLibros

368

PARTE IV:

Lenguajes declarativos

que, ¿cómo estos argumentos obtienen algún valor? En un lenguaje puramente funcional, la respuesta es que ellos son los valores de otras funciones. Puesto de manera diferente, unObtenerDatos funcional sería algo parecido a (ObtenerDatos ObtenerX, ObtenerY, ObtenerZ),dondeObtenerX,ObtenerY yObtenerZ sonfunciones que devuelven valores. La mayoría de las implementaciones de LISP incorpo­ ran algunos efectos colaterales y tipos de datos integrados. Éstos han sido incluidos para hacer más sencillo un código fácilmente legible y las implementaciones efi­ cientes.

Semántica limpia Algunas de las características que hacen que un lenguaje sea útil y confiable son en las cuales el lenguaje significa lo que dice —no es ambiguo— y los resultados de un programa pueden verificarse. En un lenguaje funcional f(3) siempre devolverá el mismo resultado, mientras que en un lenguaje imperativo, como Pascal, éste pue­ de no ser el caso. Considere la función de Pascal: function f(I : 1nteger):integer; begln C o n t e o C o n t e o + I: f Conteo end;

(8.1.2)

Si Conteo es una variable global inicializada convenientemente, f(3) devolverá un resultado diferente cada vez que sea llamada. Éste es sólo un simple ejemplo de las dificultades que pueden ser encontradas cuando se intenta probar lo que son las semánticas de un programa imperativo. No hay caso en reinventar la rueda si una está disponible y es adecuada para la tarea en cuestión. Por lo tanto, los autores de los lenguajes funcionales utilizaron la teoría matemática de Alonzo Church, llamada el cálculo lambda, mejorada mediante el cálculo de combinatorias de Haskell B. Curry y R. Feys. Se puede codi­ ficar un programa y frecuentemente probar su corrección utilizando la misma notación en la que estas dos teorías están escritas.5 Un científico computacional puede confiar en el trabajo matemático existente y usar estas teorías para de­ sarrollar algoritmos correctos para tareas específicas. Hemos incluido un breve examen del cálculo lambda en el Apéndice B para aquellos lectores que estén inte­ resados.

5 No todos los programas pueden ser probados como correctos o incorrectos, sin importar el méto­ do que se utilice. Un resultado conocido como el problema del paro muestra que si hubiera una función H(f), que devolviera el valor verdadero (TRUE) si f fuera una función que finalizara y devolviera un valor, y falso (FALSE) si f fuera a ejecutarse de manera infinita, entonces H llevaría a una paradoja en la teoría de funciones.

Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

369

8.2

LISP VIÑETA HISTÓRICA

LISP: John McCarthy La inteligencia artificial (IA) es la parte de la ciencia de la computación interesada en el diseño de sistemas computacionales inteligentes; es decir, sistemas que exhibirán las características que asociamos con la inteligencia en el comportamiento humano: com­ prensión del lenguaje, aprendizaje, razonamiento, resolución de problemas y otras más [Jackson, 1986]. LISP es un lenguaje de programación con un propósito. Se desarrolló en forma específica para programación de la IA a finales de los cincuenta por John McCarthy, profesor en el colegio de Dartmouth. Las semillas de LISP fueron sembradas en la mente de McCarthy durante el verano de 1956 cuando asistió al primer taller im­ portante sobre IA en Dartmouth. Se dio cuenta de que los lenguajes existentes no iban a satisfacer las necesidades de los programadores de LA. Los lenguajes como FORTRAN procesaban números. Un lenguaje de IA, si fuese a imitar en verdad el cerebro humano, necesitaría codificar palabras y conceptos. McCarthy trabajó durante los siguientes dos años para desarrollar LISP. El len­ guaje LISP es una combinación de cuatro elementos: dos lenguajes existentes, las matemáticas y el último elemento del propio McCarthy. LISP tomó prestada la sin­ taxis algebraica de FORTRAN y métodos de manipulación de símbolos de IPL (Information Processing Language; Lenguaje de Procesamiento de Información). En las matemáticas, McCarthy encontró dos sistemas equivalentes, la teoría de fun­ ciones recursivas de Kleene y el cálculo lambda, una notación conveniente para las funciones anónimas de LISP. El inventor del cálculo lambda, Alonzo Church, había sido el asesor de tesis de McCarthy en Princeton. Aunque el cálculo lambda influenció a McCarthy, no lo siguió servilmente. Los últimos elementos son de su propiedad: el uso de listas para representar información, la representación de pro­ gramas como datos y la creación de recolección de basura para colectar y hacer disponibles localidades de memoria que ya no son necesarias. Como FORTRAN, la primera implementación de LISP fue para la IBM 704. Tenía sólo unas cuantas primitivas y utilizaba tarjetas perforadas en modo de lote. Un sistema LISP interactivo desarrollado en 1960 tiene el honor de ser uno de los ejemplos más antiguos de computación interactiva. Sin embargo, el crecimiento del uso de LISP fue lento. La IA era un campo relativamente nuevo que necesitaba grandes computadoras con memorias masivas. El interés en la IA creció con el de LISP, el cual llegó a ser el principal lenguaje experimental de IA. "Es una característica de las aplicaciones de inteligencia artifi­ cial que el problema no esté bien comprendido. En realidad, con frecuencia una meta de la investigación es comprender mejor el problema.... LISP está muy [bien] adecuado para esta clase de problema" [MacLennan, 1987]. LISP es bueno para Sólo fines educativos - FreeLibros

370

PARTE IV:

Lenguajes declarativos

problemas ambiguos debido a su sistema de tipo dinámico y estructuras de datos flexibles, las cuales fomentan un enfoque experimental para la resolución de pro­ blemas. Por supuesto, no todos los aspectos de la imagen de LISP son de color de rosa. Todos los sistemas iniciales eran interpretados, en vez de compilados, haciendo la ejecución de los programas muy lenta* En la actualidad, la mayoría de los sistemas LISP proporcionan compiladores con optimizadores de velocidad, pero su repu­ tación como un lenguaje lento ha permanecido. También, LISP hace mucho uso de la recursión, que muchos programadores encuentran difícil de aprender. Por últi­ mo, los programas de LISP requieren memorias centrales muy extensas para ejecu­ tarse. De este modo, el desarrollo de mejores recolectores de basura es todavía un área activa de investigación. La IA puede dividirse en tres áreas: procesamiento del lenguaje natural, robóticá*e ingeniería del conocimiento. Es en la segunda y tercera áreas donde LISP sobresale. "La ingeniería del conocimiento se enfoca tanto al desarrollo de soft­ ware para sistemas expertos y en el análisis de formas en las cuales los expertos humanos resuelven los problemas. La ingeniería del conocimiento interactúa con expertos humanos para ayudarlos a describir sus conocimientos y estrategias de inferencias en términos que permitirán codificar el conocimiento. Así, un ingeniero del conocimiento combina niveles altos de psicología cognitiva con técnicas de pro­ gramación simbólica para desarrollar sistemas expertos" [Harmon, 1985]. Los sistemas expertos se enfocan en dos tipos de conocimiento. El primero, conocimiento público, es la clase que se encuentra en libros de texto. Un experto hu­ mano en un campo tiene un firme sostén de información objetiva. Los sistemas expertos pueden superar a los expertos humanos en la derivación de información pertinente, dada una base de datos adecuada. El segundo, es el conocimiento priva­ do, el cual puede ser llamado intuición o sentido común. "Este conocimiento priva­ do consiste fundamentalmente de reglas prácticas que han venido a ser llamadas heurística. La heurística capacita al experto humano para hacer adivinanzas educa­ das cuando sea necesario, para reconocer enfoques promisorios para los proble­ mas y para tratar en forma efectiva con datos erróneos o incompletos. Elucidar y reproducir tal conocimiento es la tarea central en la construcción de sistemas ex­ pertos" [Hayes-Roth, 1983]. Cuando se llega al conocimiento privado, los huma­ nos por lo regular derrotaron a las máquinas. Ejemplos de sistemas expertos escritos en LISP y que se encuentran en uso actualmente son DENDRAL, MACSYMA, EXPERT y MYCIN. DENDRAL se utili­ za para analizar datos de espectrografía de masas, nucleares, de resonancia mag­ nética y químicos experimentales, para inferir la estructura plausible de un compuesto desconocido. MACSYMA realiza en forma simbólica cálculos diferen­ ciales e integrales y sobresale en expresiones simbólicas simplificadas. EXPERT se emplea para construir modelos de consulta en endocrinología, oftalmología y reumatología. MYCIN diagnostica enfermedades infecciosas de la sangre y pres­ cribe el tratamiento. El interés por LISP ha descendido a medida que la comunidad de programadores se ha enfocado más en las técnicas orientadas a objetos. Afínales de los ochenta se hizo un intento para estandarizar una mezcla de Common LISP y SCHEME. El esfuerzo para mezclar los dos lenguajes fracasó, pero la IEEE produjo un estándar Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

371

para SCHEME en 1989. El documento en borrador de Common LISP, producido por el grupo de trabajo de la IEEE X3J13 en 1992, es de cerca de 1 000 páginas de extensión; terriblemente lejano del original de 12 páginas de McCarthy.

El lenguaje LISP (dialecto SCHEME) Tipos de datos LISP tiene un tipo de datos simple, el átomo, que es un número o una cadena co­ menzando con una letra, llamada átomo literal. Los números se autoevalúan y utili­ zan el hardware integrado para aritmética de enteros y real. Un átomo literal puede tener un valor y ser evaluado, o puede permanecer sin evaluarse. Un átomo literal también puede tener una lista de propiedad asociada, propUst, la cual es origi­ nalmente una lista vacía O. Se pueden agregar propiedades a la propl Ist de un átomo utilizando (putprop<nombre>

<prop1edad>), o eliminarse haciendo uso de (renprop<nombreXpropiedad>). (getprop<nombreXprop1edad>) regresa el valor de una propiedad del átomo <nombre>. Expresión SCHEME (propUst ‘ballena-azul) (define mar ‘océano (putprop ‘ballena-azul ‘plancton ‘come) (putprop ‘ballena-azul mar 'mora-en) (propUst ‘ballena-azul) (getprop ‘ballena-azul ‘mora-en) (reaprop ‘ballena-azul ‘mora-en) (propUst ‘ballena-azul)

Valor O

(8.2.1)

valor no especificado valor no especificado valor no especificado (MORA-EN OCEANO, COME PLANCTON) (OCEANO)

valor no especificado (COME PLANCTON)

Después de que se evalúan las primeras seis expresiones del listado (8.2.1), el átomo bal 1ena-azul sería representado en la memoria como en el diagrama de la figura 8.2.1. El nombre ballena-azul también sería una entrada en una tabla llamada la lis ta de objetos, que es semejante a una tabla de símbolos. La entrada en esta lista de objetos bajo bal 1ena -azul es el apuntador para la estructura de la figura 8.2.1. El átomo n1l representa el final de una lista. La estructura de la figura 8.2.1 represen­ ta la lista ( bal 1e n a -azul, propUst), donde propl Ist es ((come plancton) (mora-en oc é ano)).

El tipo estructurado de LISP es la lista, que puede estar vacía o contener objetos ordenados (objaobj„. . . obj ). Algunos LISP, como Common LISP, también incluyen arreglos utilizando las facilidades de arreglo de una máquina en particular. Ya hemos visto los tipos de datos abstractos en los capítulos 2 y 3. Esta ligadu­ ra de operaciones para datos también puede implementarse en lenguajes funciona­ les. Lo que es necesario son funciones llamadas constructores, que construyen instancias de un tipo de datos compuesto particular; y selectores, que seleccionan características del agregado. Como un ejemplo, los números complejos pueden reSólo fines educativos - FreeLibros

372

PARTE IV:

Lenguajes declarativos Entrada de apuntador para ballena-azul en la lista de objetos

rl

ballenaazul

nil

Lista de propiedades

i yf

1f

nil

nil

come

nil

>f plancton

r nil

mora-en

t océano F I G U R A 8.2.1

Representación en memoria para el átomo, bal lena-azul

presentarse de dos maneras, forma rectangular (ParteReal, Partelmaginaria) o for­ ma polar (Magnitud, Ángulo), como se muestra en la figura 8.2.2. Una función constructor devuelve un número complejo, y los operadores de aritmética compleja devuelven números complejos, sin importar la forma. Tales operadores deben ser funciones genéricas que puedan efectuar la operación desea­ da de manera correcta, sin importar los tipos de parámetro. LISP, incluyendo el dialecto SCHEME que examinaremos más adelante, no es fuertemente tipificado, pero podemos construir dos formas diferentes de números complejos a través de funciones definidas por el usuario llamadas, quizá, rectan­ gular y polar. El programador debe tener cuidado de tipos que no coincidan, por­ que el sistema no lo hará por él. En SCHEME, podemos definir la función Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

373

• x,y

forma rectangular x = parte real y = parte imaginaria

forma polar r = radio e = ángulo

FIGURA 8.2.2 Dos formas para los números complejos

rectangular(r, i) = (r, i). Dados dos números reales r e i como argumentos, rectangu­ lar devuelve una lista de dos elementos que contienen r como la parte real e i como la parte imaginaria de un número complejo. De manera similar, polar(m, 0) podría devolver una lista con m como la magnitud y 0 como el ángulo. Las funciones para aritmética compleja pueden entonces ser definidas para que tomen como argu­ mentos listas de dos elementos de reales y que conviertan números complejos de una forma a otra. Es interesante notar que en el lenguaje LISP, el par (1.0,0.0) podría representar un número complejo ya sea en forma rectangular o polar, puesto que empareja el patrón complejo para cualquier representación: una lista de dos elementos de nú­ meros reales. LISP no devuelve un tipo junto con un valor de lista. Es labor del programador hacer cualquier verificación de tipo que sea necesaria. Esta invita­ ción para programar errores ha sido remediada en el lenguaje funcional, ML, que discutiremos en forma breve al final del capítulo.

Método para almacenamiento de datos La sintaxis original de McCarthy para LISP fue la expresión-S (S-expression o sexpr), que viene de "symbolic expression" (expresión simbólica). Una expresión-S se de­ fine de manera recursiva como: 1. 2.

Un símbolo atómico es una expresión-S. Si ex y e2 son expresiones-S, entonces también lo es ( e ^ ) .

Esta última expresión se llama un par punteado. Una lista puede ser implementada como:

(8 .2 .2 )

Sólo fines educativos - FreeLibros

374

PARTE IV:

Lenguajes declarativos

donde n11 es un símbolo atómico para la lista vacía. En la mayoría de LISP, la lista del listado (8.2.2) es abreviada como (exe2 ... en). El almacenamiento de datos fue implementado en la IBM 704 como celdas ató­ micas o como celdas cons (de "construcción") para pares punteados, como se mues­ tra en la figura 8.2.3. Los identificadores car y cdr están relacionados con la IBM 704, donde una palabra de memoria incluía el contenido del registro de dirección (car; contents of the address register) y el contenido del registro de decremento (cdr, contents of the decrement register). Los nombres continúan siendo usados en la actualidad para indicar la cabeza y la cola de una lista, donde (car 1) devuelve el primer elemento de la lista 1, y ( cdr 1) devuelve toda la lista excepto su primer elemento. Si 1 - (a b c) es una lista, (car D e s a y (cdr l ) e s ( b c). (b c) es llamado la cola o extremo de 1. Una lista es entonces como la que se muestra en la figura 8.2.4. En esta figura, la cabeza, o car, de la lista es ex, y la cola o extremo es el resto de la lista, que se localiza en la dirección contenida en el cdr de la primera celda cons. L istas com o estructuras de d a to s. Las listas son sorprendentemente flexibles para desarrollar estructuras de datos. Un árbol binario ordenado el cual se muestra en la figura 8.2.5 es (( (0) 1 (2)) 3 ((4) 5 (6))) en forma de lista. Nótese que cada subárbol es una lista en sí mismo. El subárbol izquierdo aparece a la izquierda del nodo raíz y el derecho, a la derecha. Hemos visto gran variedad de estructuras de datos, todas las cuales serán ex­ presadas como listas en LISP. Por ejemplo, un arreglo unidimensional es tan sólo una lista simple, y un arreglo de n x m de dos dimensiones es una lista de listas, (RENGLONxRENGLON,. . . RENGLONJ, donde cada RENGLON es una lista mdimensional. Un arreglo de tres dimensiones es (AxA , . . . Ap), donde cada A. es un arreglo de dos dimensiones, etcétera. Supóngase que A = ((12 3)(4 5 6)(7 8 9)) repre­ senta un arreglo de 3 x 3. SCHEME (y muchos otros LISP) tienen funciones especia­ les para hallar rápidamente elementos individuales, (car A) es la lista (1 2 3). Pero

car

cdr I

FIGURA 8.2.3 La celda binaria o cons para un par punteado

FIGURA 8.2.4 Lista implementada como pares punteados. El ex puede ser celdas cons o átomos.

Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

2

4

375

6

F I G U R A 8.2.5 Árbol de búsqueda binaria

suponga que queremos A [l,l] = 1 o (car(car A)). Esto se abrevia (caar A).A[2,1] = 4es (car(car(cdr A))) o (caadr A) yA[3/l] = 7es (caaddr A),o (car(car(cdr(cdr A)))), el cual es (car(car(cdr ((4 5 6) (7 8 9 ) ) ) ) ) , quees (cartear ((7 8 9 ) ) ) ) , o(car(7 8 9)) - 7. Todas estas abreviaturas comienzan con c, finalizan con r y tienen tantas a o d como necesitemos car o cdr.6 ¿Se entendió? L istas com o program as. Una form a de LISP es una expresión-S cuyo significado está por evaluarse. El número 2 es una forma que se evalúa a 2. (+ 3 5) es una forma que se evalúa como 8, como lo es ((1 ambda (x)(+ 3 x)) 5). Esta última se denomi­ na una expresión lambda, con la palabra I ambda precediendo la lista de argumentos, en este caso, (x). Suponga que usamos (use)(define pl us3(1 ambda(x ) (+ 3 x ))). Cuando plus3 se aplica al valor 5, empleando ( (pl us3 x) 5), tiene el mismo efecto que ( lambda (x) (+ 3 x ) ) 5), pero también nombra la función así definida. Las formas tales como ( + 3 x) están integradas en el sistema SCHEME. Cada forma podría ser considerada como un programa. En la práctica, un programa se guarda para su uso repetido y contiene una colección de definiciones de función. Una llamada de función comienza el proceso de ejecución. Por ejemplo, el programa para la facilidad de Ayuda (Help) de SCHEME comienza con la definición (ligeramente modificada aquí) de una fun­ ción para tener acceso a los diversos aspectos de la ayuda. (define ayuda (lambda tema

(8.2.3)

(If (nuil? tema) (muestra-temas-ayuda) (consigue-ayuda (cap tema)))

)) (1f

<parte-if> <parte-else>) es una forma especial que toma tres argumentos. Si es verdadera, se evalúa la <parte-if>; de otro modo, la <parte-else> es evaluada. Existen dos fundones auxiliares para hel p, show-helptopics y fetch-help. Éstas invocan todavía otras funciones, las cuales invocan otras, etcétera, hel p acepta cero o un argumento, s ubj ect. Si no hay argumentos, se llama a show-hel p-topics. Si el sujeto es una lista, como en (hel p( edi t or)), se 11a­

6 La mayoría de los LISP limitan la longitud de un operador c . . . r. En PC-SCHEME, es cxxxxr, donde cada x puede ser d o r.

Sólo fines educativos - FreeLibros

376

PARTE IV:

Lenguajes declarativos

ma a f etch - hel p con el primer elemento de subj ect como argumento; en este caso, se exhibiría la ayuda al utilizar el editor. Cuando se carga hel p, la evaluación une el cuerpo de cada una de las funciones del sistema Help (de ayuda) en el entorno actual y las pone a disposición para responder a las llamadas de función desde la terminal. La facilidad Help está com­ puesta de 22 funciones definidas, con acceso inicial a través de la llamada de fun­ ción ( hel p).7 Un programa SCHEME también podría incluir otras expresiones para ser eva­ luadas. Por ejemplo, podríamos terminar el archivo de ayuda con la función llama­ da de ( hel p). Luego cargar el archivo resultaría en los temas de ayuda enumerados de manera automática. Probablemente no sea una buena idea, pero es posible. Las abstracciones de datos, aunque no están integradas en SCHEME, pueden ser implementadas al elegir nombres sugerentes para las funciones. Por ejemplo, una implementación para un ADT para números racionales en el dialecto SCHEME de LISP podría definirse con las ocho funciones mostradas en el listado (8.2.4) [Abelson, 1985]: make-rat (n d) number (x) denom (x) +rat (x y) -rat (x y) *rat (x y) /rat (x y) =rat (x y)

devuelve (n-d) (8.2.4) devuelve n, donde x es (n*d) devuelve d, donde x es (n-d) devuelve x + y, donde x y y son racionales devuelve x - y, donde x y y son racionales devuelve x * y, donde x y y son racionales devuelve x / y, donde x y y son racionales devuelve true (verdadero) si x = y; en caso contrario, de­ vuelve fal se (falso), donde tanto x como y son racionales.

Un aspecto de LISP, no compartido por otros lenguajes, es que los programas (expresiones-S) y los datos son indistinguibles. Así como un programa puede mo­ dificar los valores de las variables, del mismo modo puede modificar otros progra­ mas o incluso a sí mismo. Las definiciones de funciones, entornos,8 programas y archivos son objetos LISP de primera clase que pueden ser pasados a funciones y devueltos como valores. Funciones integradas Había sólo seis funciones integradas en el LISP original de McCarthy: cons, cond, car, cdr, eq y atoa.9 En la actualidad, la mayoría de los LISP en uso proporcionan 7 Una función por lo regular tiene argumentos, como en la llamada (+ 1 2). Si no hay argumentos, como en hel p, la llamada ( hel p) todavía requiere paréntesis. 8 Un entorno es una secuencia de tablas que contienen ligaduras de variables. Es similar a las liga­ duras en bloques anidados en un lenguaje como Pascal. En SCHEME, un entorno puede ser devuelto como el valor de la forma especial, make-environment. 9 En el LISP original, los átomos estaban escritos en letras mayúsculas, por ejemplo CAR, debido a que las computadoras antiguas no reconocían los caracteres en minúsculas. No lo hemos hecho así aquí debido a que la práctica ha sido abandonada.

Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

377

lyst

FIGURA 8.2.6

headylyst antes de la operación cons otras, entre ellas operadores aritméticos, entrada y salida. Todos los átomos, fun­ ciones y formas de los LISP antiguos están incluidos en las versiones más recientes, pero muchas más han sido agregadas para conveniencia del programador. (cons a b) devuelve el par punteado (a • b ) . S i l i s t e s u n a l i s t , (cons ‘head l y s t ) devuelve una lista, con head agregado a lyst como el primer elemento, como se muestra en las figuras 8.2.6 y 8.2.7. (car lyst) devuelve el primer elemento de lyst, y (cdr lyst) devuelve la totalidad de lyst excepto el primer elemento, como se discutió en la sección "M é­ todos para almacenamiento de datos". De este modo, si lyst es (a b c), (car lyst) devuelve a, (cdr lyst) devuelve (b c ) , y (cons 'head lyst) devuelve (head a b c). (cons 9 O) se evalúa para (9 • nll ), lo cual es abreviado (9), la lista con un solo elemento, 9. Nótese que en la figura 8.2.7, la lista producida por la operación cons no tiene nombre. Finuras tales como las funciones nombradas han sido agregadas a todas las implementaciones de lista. Sin embargo, existen características no funcionales. En un lenguaje puramente funcional, los valores no son asignados a localidades de almacenamiento. Si queremos construir la lista (1 2 3) y luego encontrar su primer elemento, podríamos conseguir esto en una manera funcional utilizando ( car ( cons (1 (cons 2(cons 3 n1l))))).Si(l 2 3) tuviera el nombre 11, podríamos utilizar (car 11). Consideraremos expresiones-S y átomos nombrados en la sección "Efectos colaterales". (eq A B) prueba si los átomos Ay B son los mismos o no. ( atom A) devuelve #T (True; verdadero) si Aes un átomo, y #F (False; falso) en caso contrario. En algunos LISP, #T, #F y n11 son constantes autoevaluadas, como son los números. En SCHEME, #T, #F y n1l son símbolos ordinarios que están acotados en el entorno global a valo­ res apropiados, nll representa la lista vacía, aunque también puede usarse (). No todos los LISP utilizan #T y #F, pero pueden emplearse t y f , t r u e o f a l s e , o también TRUE y FALSE.

lyst

head

FIGURA 8.2.7

Después de la operación cons Sólo fines educativos - FreeLibros

PARTE IV: Lenguajes declarativos

378

El control es proporcionado a través de la expresión condicional cond. Discutire­ mos cond con más detalle en la sección "Recursión y control", pero lo mencionare­ mos brevemente aquí ya que no podemos ir muy lejos en LISP sin él. La forma de cond <expresión> se ilustra en el listado (8.2.5). (cond (

<eL>)

(8.2.5)

(

<e2>) ( <er>)

) La ejecución comienza en la parte superior, evaluando las c , o guardas (guards), hasta que una se evalúe como verdadera. El valor de la e correspondiente se de­ vuelve. Si ninguna de las c. resulta ser verdadera, el cond devuelve un valor de falso (n 11 o #F). Una expresión cond que devuelve el valor absoluto de su argumento es: (cond ((> x 0) x) ((eq x 0) 0)

(8.2.6)

(#T (- x))

) La última expresión-S, (#T (- x )), se evalúa sólo si las primeras dos guardas, (> x 0) y (eq x 0), son falsas (#F). Su guarda #T es siempre verdadero. Form as fu n cion ales Existe sólo una forma funcional integrada en el LISP original, la expresión lambda, (lambda (<parametros formales >) <cuerpo>), que proviene del cálculo lambda de Church como se discute en el Apéndice B. (1 ambda (x y) ( * x y )) representa una función de dos variables, la cual devuelve su producto. ((1 ambda (x y )(* x y )) 2 3) devuelve 6, después de fijar 2 para x y 3 para y, y posteriormente aplicar el ope­ rador de multiplicación a 2 y 3. El ámbito de x y de y es la expresión lambda. Para implementar la recursión, LISP utiliza una expresión lambda etiquetada. (labe!' (factorial (lambda(n) (cond ((eq n 0) 1) (#T (* n (factorial (- n 1))))

(8.2.7)

) ))) labe! no liga una definición de función del átomo f a c t o r i a l , pero suministra un nombre temporal a la función de modo que pueda ser llamada de manera recursiva como en la tercera línea de la expresión en el listado (8.2.7). En SCHEME, podemos ligar el átomo factorial a su definición haciendo uso de: (define factorial (lambda(n) (cond ((eq n 0) 1) (#T (* n (factorial {- n 1))))

Sólo fines educativos - FreeLibros

( 8 .2 .8 )

CAPÍTULO

8: Programación funcional (aplicativa)

379

En el listado (8.2.8), f act or i a1 está ligado a su definición lambda y puede entonces ser llamado repetidas veces, (define

<expresion-S>) es una for­ ma especial de SCHEME que sirve para el propósito de label (etiqueta) en el LISP antiguo para implementar la recursión de la última línea. Además, como un efecto colateral, liga a <expresión-S>. Como un ejemplo de una forma funcional construida en LISP utilizando una expresión lambda, consideraremos la función mapca r descrita anteriormente en la sección "Funciones como objetos de primera clase". En matemáticas, un mapa es un conjunto de pares ordenados (x, y), con la x "mapeada" sobre su valor y. (mapca r f une l y s t ) transforma en forma repetida el car de la lista lyst en un valor funcio­ nal. Si lyst tiene 25 elementos, et, . . . , e ^ (mapear fun lyst), devolverá otra lista de 25 valores, ( f un( e l ) fun(e25)). Por ejemplo, addl es una función de una variable que agrega add 1 a su argumento, (mapear addl (1 2 3)) devuelve la lista (2 3 4). mapear puede ser definida utilizando sintaxis de SCHEME como se muestra a continuación en el listado (8.2.9). (define mapear (lambda(fun l y s t )

(8.2.9)

(cond ( ( nu i l ? l y s t ) n1l) (#T (cons (fun (car l y s t ) )

(mapear fun (edr l y s t ) )

)

)

) )) Supongamos que se evalúa la expresión (mapear (lasbda ( x)(* x x ) ) ( l 2 3)), donde ( 1anbdft (x ) (* x x )) es la forma que coincide con el parámetro, fun, y (1 2 3) sustituye a lyst. (danbda ( x)(* x x)) 2) se evalúa como 4 puesto que se sustituye 2 por x antes que la multiplicación tenga lugar. La recursión es como se muestra en el listado (8.2.10). (cons ((lambda (x) (* x x)) 1)

(8.2.10)

(mapear ((lambda (x) (* x x)) (2 3)) (cons ((lambda (x) (* x x)) 2) (mapear ((lambda (x) (* x x)) (3)) (cons ((lambda (x) (* x x)) 3) (mapear ((lambda (x) (* x x)) ()) (nuil? ()) ni1) (9) (4 9) (1 4 9)

Nótese que la recursión llama repetidamente a mapear hasta que se encuentra la lista nula en la línea 7, en cuyo momento la cadena entera puede desenvolverse, construyendo la lista de valores. Esto se denomina con frecuencia "consignar una lista" (consing up a list). Este comportamiento causa problemas al programador principiante de LISP, quien en ocasiones crea llamadas recursivas infinitas.

Sólo fines educativos - FreeLibros

380

PARTE IV:

Lenguajes declarativos

apply, eval y operadores aritméticos

McCarthy utilizó las cinco funciones básicas para definir apply, eval y eval quote, que en efecto construyen un intérprete para LISP. Las primeras dos están integra­ das en la mayoría de los LISP modernos. Éste es otro ejemplo de un intérprete o compilador escrito en el lenguaje que es para traducir a código de máquina. Ya hemos examinado otro, el lenguaje C. apply toma dos entradas, evalúa cada uno de sus argumentos, y luego aplica el primero, que es una función, al segundo, el cual es una lista de argumentos, (apply car (quote ((a b c) ) ) ) 10devuelve a, el primer elemento de la lista (a b c);es decir, aplica la función car al argumento simple (a b c ). eval toma una expresión y un entorno, e, como valores. Suponga que los valo­ res a, b y c en el entorno e son 1, 2 y 3, respectivamente, (eval (car ' (a b c ) ) devuelve el valor de a, 1. e va 1 incluye una llamada a a pp 1y después de evaluarse a, b y c . De este modo (eval (car '(a b c)) invoca a (apply (car(quote ((1 2 3)))) después de evaluar a, b y c. Usted verá una aplicación tanto de eval como de apply en la sección "Una función automodificante". Precisamente para advertir al usuario de diversos dialectos de LISP, hemos escrito hasta ahora nuestras funciones LISP como expresiones-S, encerradas entre paréntesis; por ejemplo, (cons a (b c)). Cuando se utilice algunos intérpretes, esto podría introducirse en notación más funcional como cons (a (b c)). Esto es, f(x y) en lugar de (f x y). Esta práctica ha sido sumamente abandonada, debido a que es inconsistente con la mayoría de la sintaxis de LISP. Sin embargo, usted puede ver cons (a (b c)), (cons a (b c)),(cons a, (b, c)),cons (a; (b; c ) ) , u otras variaciones en diferentes intérpretes o compiladores. Ninguna función aritmética fue integrada en el LISP antiguo, aunque operado­ res aritméticos tanto enteros como reales están integrados en SCHEME y la mayo­ ría de otros LISP modernos. Al principio, éstos así como todos los demás, tenían que ser implementados por el programador. Por ejemplo, suponga que los enteros no negativos son definidos utilizando el 0 y una función sucesor succ como: (define

zero

n1 1)

(define (succ n)(cons n n))

Entonces los números 0, 1, 2 y 3 son: n1l, (nil), ((nil), n1l) y (((nil), n1l), (n11), n1l).

En el ejercicio 8.2.3, se le solicitará escribir la función p red (predecesor), donde ( pred n ) devuelve el número antes de n si n no es 0, y ( pred 0) devuelve un error. (define

plus

(lambda (cond

(numl num2) ((zero? numl) num2) (#T (plus (pred numl)(succ num2)))

(8.2.11)

) )) En el listado (8.2.11), si numl es 0, entonces se devuelve el valor de num2. En caso contrario, procedemos a la segunda alternativa, e2= (plus (pred n u m l H s u c c num2)). Evaluamos (plus 2 3). 10 La función (quote ((a b c ) ) ) , q u e también puede escrib irse'((ab e)) devuelve ((a b c ) ) c o n a , b y c sin evaluarse.

Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

(zero? 2) = #F

381

(8.2.12)

#T (plus 1 4) (zero? 1) = #F #T (plus 0 5) (zero? 0) return 5

En SCHEME, la forma especial 1f es una cond abreviada: (1f e l Cj c2) = (cond (ex c j

(8.2.13)

(#T c2))

En la práctica, un programa LISP es una colección de definiciones de funcio­ nes, con una de ellas suministrando el acceso dentro del programa al llamar a otras, las que a su vez llaman a otras, etcétera.

Recursión y control Como vimos en el capítulo 2, las abstracciones de control son ramas, interacciones y procedimientos. En LISP, la expresión cond, como se definió en el listado (8.2.5), controla las ramificaciones, con (cond ( b o o l e a n l e x p l ) ( b o o l e a n 2 exp2) . . . (booleanN expN)) implementando tanto proposiciones If...then. •.else como de declaraciones (case). El control de procedimientos en SCHEME, como en todos los LISP, es a través de llamadas de función, las cuales son por lo regular recursivas. Cada llamada establece un nuevo conjunto de ligaduras, llamado un entorno o marco, dando como resultado el paso del entorno inferior de una recursión todo el camino de regreso hacia la parte superior, como un valor para la función original. Utilizando la ya conocida fu nción factorial, veam os una vez más cóm o funciona, implementando su definición como: factorial

(n) =

if (n =

0)

then 1

(8.2.14)

else (n * factorial(n - 1))

Definimos factorial en el listado (8.2.8), pero lo volveremos a definir aquí en có­ digo SCHEME, el cual es ligeramente diferente, como se muestra en el listado (8.2.15). (8.2.15)

(define factorial (lambda (n) (if(zero? n)

1 (* n (factorial(- n 1)))

))) Una llamada a (factorial 3) producirá cuatro entornos anidados antes de llegar a cualquier valor, y los resultados parciales deben pasarse todo el camino de regreso hacia factorial. En el listado (8.2.16), que muestra la acción recursiva, usaremos subíndices para indicar los entornos. Sólo fines educativos - FreeLibros

382

PARTE IV:

Lenguajes declarativos

(factorial^ 3) (* 3 (factorial 2)) (factorial2 2) => (* 2 (factorial 1)) (factorial3 1) => (* 1 (factorial 0)) (factorial4 0) => 1 (factorial3 1) => (* 1 1) = 1 (factorial2 2) => (* 2 1) = 2 (factoría^ 3) => (* 3 2) = 6

(8.2.16)

Un cálculo más eficiente es N * (N -l) * (N-2) * .. .* 1. Podemos realizar esto en SCHEME utilizando un marco para la función hel per como se expone en el listado (8.2.17), que contiene las variables temporales a, actuando como un acumulador para las sumas parciales, e i, contando en forma descendente hasta 0. (define factorial2

(8.2.17)

(lambda(n) (define helper (lambda (i a) (if (zero? i) a (helper ( - i

1) (* a i))

)) (helper n 1)

)) Aquí una llamada a factorial2(3) primero definiría helper y luego llamaría (helper 3 1), que multiplicaría de manera recursiva 3*2*1, devolviendo 6. Esto también puede ser implementado sin el uso de helper con el let de SCHEME, el cual inicializa i a n y a al valor 1 como en el listado (8.2.18). (define

(8.2.18)

factorial3

(lambda (n) (let f ((i n) (a 1)) (if (zero? i)

a (f (- i l)(* a i)))

))) Aquí la acción es: (factorial3 3) => ? ( f . 31) =>(f2 3) (f2 2 3) =>(fl 6) (f3 1 6) =>(f0 6) =>6 (f4 0 6) =>6 (f3 1 6) (f2 2 3) =>6 (f, 31) => 6 factorial3 => 6

(8.2.19)

Nótese que el valor final, 6, es obtenido en la parte inferior de la recursión, en entomo4. Este comportamiento es recursivo de cola, como fue mencionado en el capítulo anterior. Una implementación óptima para factorial sería: Sólo fines educativos - FreeLibros

CAPÍTULO 8: Programación funcional (aplicativa)

(factorial3 3) =» ? (fi 3 1) => (f 2 3) (f223) =>(fl6) (f3 1 6) => (f 0 6) (f4 0 6) =>6 (factorial3 3) => 6

383

(8.2.20)

Los intérpretes para SCHEME, así como también para Common LISP, están construidos para reconocer llamadas recursivas de cola y evaluarlas de manera interactiva, como se hizo en el listado (8.2.20), aun cuando la función misma es recursiva. Efectos colaterales En SCHEME, las funciones integradas que producen efectos colaterales finalizan con ! de modo que son obvias. Como un ejemplo, considere la función append, que es definible en todos los sistemas LISP y construye una nueva lista de sus dos argu­ mentos, (append (hada m a r i a M p e q u e ñ o cordero)) devuelve como su valor la nue­ va lista, (hada maria pequeño cordero), append no tiene efectos colaterales. En SCHEME, se puede también definir una función append !, que devuelve el mismo valor que append, pero tiene el efecto colateral de alterar la primera lista, (hada maria). Quizás algunos diagramas harán la diferencia más clara. La figura 8.2.8 muestra las dos listas originales antes de la aplicación de cualquiera de los dos append o append!. Para la expresión (append 11 12) en la figura 8.2.9, 11 ha sido copiada, y la 11 original no ha sido alterada. Como se ilustra en la figura 8.2.10, después de un append ! la primera lista, 11, ha sido alterada de manera que su última celda cdr apunta a 12. Hacer esto no sólo ahorra espacio, sino que también el tiempo requerido para copiar 11. Sin embargo, hemos violado la regla de no efectos colaterales del cálculo lambda. La alteración de estructuras de listas requiere dos funciones primitivas, set-cari y set-cdrl. Para efectuar el cambio de la figura 8.2.8 a la figura 8.2.10 se requeriría una llamada a set-cdrl, que cambia el apuntador en el cdr de la última celda cons de 1 1

FIGURA 8.2.8 Dos listas originales, 11 y 12

Sólo fines educativos - FreeLibros

384

PARTE IV:

Lenguajes declarativos

FIGURA 8.2.9 Después (append 11 12)

FIGURA 8.2.10 Después (append! 11 12)

hada

nil

a

hada

12 -

define fue mencionado como una forma de ligadura especial cuando se definió la

función mapca r anteriormente. También puede ser usada para ligar valores a varia­ bles, como en (define x 2). define realiza dos tareas. Asigna almacenamiento para una variable x, y asigna 2 como el valor de x. Para cambiar el valor de x, utilizamos set!, que realiza sólo una tarea, la reasignación del valor de x. De este modo, defi­ ne y set l son más bien mapeos imperativos que funcionales. Recuerde que los len­ guajes imperativos proporcionan la asignación explícita de valores a las localidades de memoria, (define x 2) es lo mismo que la secuencia:

Sólo fines educativos - FreeLibros

CAPÍTULO 8: Programación funcional (aplicativa)

385

var x:

; begin x := 2;

con dos diferencias. La primera, LISP es carente de tipos, de modo que x contiene solamente un apuntador hacia una localidad de almacenamiento; y la segunda, no necesitamos dos proposiciones, una desempeñándose como una declaración en una sección especial y la otra como una asignación, (seti x 23) es equivalente a la declaración simple, x 23. Una función automodificante Mencionamos en la sección "Listas como programas", que el código para una fun­ ción LISP puede ser considerado como datos, y puede modificarse durante el tiem­ po de ejecución del mismo modo como cualquier otro objeto de datos. El listado (8.2.21) muestra una función, courses, que se modifica a sí misma. (define courses (lambda O ;¡Hace la llamada a coursesl mas fácil. (apply (eval coursesl) n1l)

)) (define coursesl '(lambda O ;; Función que se modifica en tiempo de ejecución (let ((course '())) (display “¿Cual curso estudiara?”)(setl course (read)) (cond ((eq? course ‘none) ‘must-be-summer) ((eq? course 'calculus) (setl coursesl (no-more-school-subj coursesl)) (courses)) (#T (apply (eval school) course))

)))) (define school ‘ (lambda(subj) (write subj)(wr1teln “ es un tema escolar” )(courses)

)) (define (college subj) (write subjKwriteln " es un tema universitario” )(courses)

) (define no-more-school-subj (lambda (p) ;; 1) elimina el segundo par condicional de p ;; 2) redefine la lista para la función, school, para la lista vacia ;; 3) sustituye college en la expresión final (#T . . .) del cond en p

Sólo fines educativos - FreeLibros

(8.2.21)

386

PARTE IV:

Lenguajes declarativos

(letC Cnew-func (delete-one ‘ ((eq? course 'calculus) (seti coursesl (no-more-school-subj coursesl)) (courses)) p))) (write ‘calculusMwriteln “ es un curso universitario” ) (setl school ‘()) (subst ‘college ‘school new-func)

))) Esta función es una versión en limpio de un programa similar programado por primera vez por Laurent Siklóssy y publicado en 1976 [Siklóssy, 1976]. Una ejecu­ ción de muestra para c ours es está contenida en el listado (8.2.22). Las respuestas del usuario están en cursiva. [1] (courses) ¿Cual curso estudiara? algebra ALGEBRA es un tema escolar ¿Cual curso estudiara? calculo* CALCULO es un tema universitario ¿Cual curso estudiara? algebra ALGEBRA es un tema escolar ¿Cual curso estudiara? ninguno DEBE-SER-VERANO

(8.2.22)

C2] Cuando el usuario responde con un curso que estudiará, se supone que es un tema escolar hasta que la respuesta del usuario es "cálculo". Aquellos con aptitudes para el cálculo estudiarán cursos colegiales desde entonces, y el sistema responde así. Introduciendo "ninguno" cuando se le pida ¿Que t i p o de c ur s o e s t u d i a r a usted? se detendrá la recursión y el programa finalizará. Después que el usuario haya introducido "cálculo" en el * del listado (8.2.22), la función coursesl del listado (8.2.21) quedará alterada para ser: 1 (laibda O Función que se modifica en tiempo de ejecución

(8.2.23)

(le t ((course '())) (dlsplay "¿Cual curso estudiara? ” )(set! course (read)) (condííeq? course 'none) ‘must-be-summer) (#T (apply (eval college) course))

))) y la función school ( escuel a) será ‘ (), la lista vacía. Algunos comentarios son pertinentes. 1.

Existen dos fruiciones de utilidad llamadas en n o - m o r e - s c h o o l - s u b j , ( s u b s t new o í d 1 is ) y ( d e l e t e - o n e expr 1 is),lasquesedejancom oejercicios. La primera sustituye todas las ocurrencias de new para o í d en 1i s, mientras que la segun­ da elimina la primera ocurrencia de expr en 1i s. Sólo fines educativos - FreeLibros

CAPÍTULO 8: Programación funcional (aplicativa)

38 7

La definición para coursesl está "señalada" (está precedida por una sola comilla). Si solicitamos verlo en SCHEME: [1] coursesl

la lista ( LAMBDAÍ) . . . (#T (apply (eval school) cour s e ) )) será devuelta como su valor. Nótese que coursesl devuelve el valor (en este caso, código) para coursesl. Si entonces solicitamos: [2] (eval coursesl)

#

será devuelto, indicando que coursesl es el nombre de un proce­ dimiento. Coursesl tiene una lista como su valor que, cuando es eval (evaluada), regresa en un procedimiento que puede ser apply (aplicado) a una lista de ar­ gumentos. En este caso, la lista de argumentos está vacía. (apply (eval coursesl))

Note también que la definición de la función courses no está entre comillas, y que puede ser aplicada sin utilizar el operador apply al llamar (courses). Esto es justamente un atajo de SCHEME, que no era parte del LISP original. La primera vez el valor de cour se es igual al átomo ‘ calculus,las dos expre­ siones que siguen el predicado, (eq? course ' cal cul us) son evaluadas. En primer lugar, el valor de coursesl se establece con la función setl al valor de (no-more-school -subj - coursesl); es decir, la lista new-func, que es el valor de la última expresión en no-more-school-subj, (subst ‘ college 'school new-func). En segundo, courses se llama otra vez. no-more-school-subj mo­ difica la definición de coursesl. Para hacer esto, debemos tratar a coursesl como una lista, no como un procedimiento, new-func es la antigua definición para coursesl cambiada en dos formas. Primero, ha eliminado la expresión condicional usando una llamada a del ete-one: ((eq? course 'calculus)

(8.2.24)

(set! coursesl (no-more-school-subj coursesl course)) (courses))

Y en segundo lugar, ha cambiado el identificador ‘ school por ‘col 1ege al lla­ mar (subst 'college 'school new-func). De este modo cuando coursesl es llamado otra vez, la expresión: (#T (apply (eval school) course))

habrá sido reemplazada por: (#T (apply (eval college) course))

no-more-school -subj también cambia la definición déla función school por la lista vacía, * O. Si es llamada, school no hará nada. Sólo fines educativos - FreeLibros

388

PARTE IV:

Lenguajes declarativos

La siguiente vez que c o u r s e s l sea llamado desde courses, las modificaciones tendrán efecto. L A B O R A T O R I O 8.1: P R A C T I C A N D O CON LISP: SCHEME

Objetivos (Los laboratorios pueden encontrarse en el Instructor's Manual) 1. Familiarizar a los estudiantes con el sistema SCHEME (u otro LISP) que tengan dis­ ponible, incluso la facilidad de ayuda (Help). 2. Introducir y ejecutar expresiones SCHEME simples de manera interactiva, y adver­ tir cuántos errores así como evaluaciones exitosas son reportadas. 3. Utilizar el editor de SCHEME, Edwin, para introducir y guardar un breve programa. 4. Ejecutar un programa que produzca un ciclo infinito e interrumpir la ejecución. 5. Escribir, guardar y ejecutar un programa recursivo simple, dirigiendo la salida a una impresora. L A B O R A T O R I O 8 . 2 : U N A F U N C I Ó N DE P A L Í N D R O M O S : SCHEME

Objetivos 1. Diseñar, escribir, guardar y ejecutar un programa SCHEME más extenso que involucre varias funciones. 2. Diseñar, escribir, guardar y ejecutar un programa SCHEME que trate con entrada y /o salida de archivos así como también E /S de pantalla.

O tras características no funcionales

El cálculo lambda como se discute en el Apéndice B es bastante escaso, usando sólo variables, paréntesis, comas y el símbolo especial lambda, más cuatro reglas de formación y tres de transformación. Así, los diseñadores de lenguajes basados en él insertan abreviaturas en lugar de todos esos paréntesis. La programación del cálculo lambda puramente funcional también es lenta, confiando en la recursión y sin tomar ventaja de economías de ahorro de espacio o de tiempo. Iteración La recursión es el medio mediante el cual LISP maneja las estructuras de datos. El LISP puro no proporciona iteradores. SCHEME tiene la forma especial, do. do espe­ cifica un conjunto de variables por ser asignadas, cómo son iniciadas al comienzo y cómo son actualizadas en cada iteración. Cuando se encuentran una condición de terminación, el ciclo sale con un valor especificado. (do

((i 0 ) ( + i i)

(8.2.25)

(sum 0 (+ sum i))) ((= i 10) sum))

Como un ejemplo, el ciclo do del listado (8.2.25) suma los números del 0 al 10, devolviendo 55. Tanto sum como i son locales a la expresión do, y cada uno es Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

389

inicializado a 0. En cada iteración, i se incrementa de 1, (+ i 1), y posteriormente sum se incrementa de i , (+ sum i ). La condición de terminación es ( - i 10). El valor de sum es devuelto cuando la condición se hace verdadera. L A B O R A T O R IO 8.3: P R O G R A M A C IÓ N U T IL IZ A N D O CICLOS ITERATIVOS: SCHEME

O bjetivo (Los laboratorios pueden encontrarse en el Instructor's Manual) 1. Emplear el ciclo iterativo do de SCHEME.

L A B O R A T O R IO 8.4: RA ST R EO Y D E P U R A C IÓ N : SCH EM E

O bjetivos 1. Investigar las herramientas de SCHEME. 2. Cuando se presente un programa con errores, utilizar las diversas herramientas para hallar y eliminar esos errores. 3. Monitorear la ejecución de un programa con y sin el PCS-DEBUGGER-MODE acti­ vado.

Vectores y cadenas Una desventaja de la lista es que sólo se puede tener acceso a la cabeza. Para tener acceso al n-ésimo elemento, tenemos que aplicar cdr hacia abajo n-1 veces y luego tomar el car de la lista sobrante. Los LISP modernos han agregado otros tipos de datos, en particular vectores y cadenas. En SCHEME, un vector es como un arreglo de longitud fija, comenzando los índices en 0. Sin embargo, es como una lista en la que sus elementos pueden ser de cualquier tipo. Se puede tener acceso a un ele­ mento en particular empleando la función vector-ref y modificarlo utilizando vector-setl.

Las cadenas SCHEME son de la longitud especificada y son creadas utilizando ■ake-strlng. Los caracteres individuales pueden ser modificados con strlng-setl, o accesados con str1ng-ref. Estas funciones son no funcionales, en el sentido que

hacen referencia y modifican directamente localidades de memoria. Fueron inclui­ das en el lenguaje con el interés de la eficiencia de tiempo. SCHEME también incluye una función 11st-ref que encuentra el n-ésimo ele­ mento de una lista. Se comporta semánticamente como vector-ref, pero está implementado de manera diferente, vector-ref calcula la dirección del elemento deseado y devuelve el elemento en esa dirección, mientras que 11st-ref hace uso de n-1 operaciones cdr para hallar el n-ésimo elemento.

Objetos y paquetes LISP ha sido extendido para soportar programación orientada a objetos, en forma notable a través de los lenguajes Flavors [Moon, 1986] y LOOPS [Bobrow, 1983]. Sólo fines educativos - FreeLibros

390

PARTE IV: Lenguajes declarativos

Recuerde del capítulo 4 que un lenguaje orientado a objetos soporta: • • • •

Ocultamiento de información (encapsulamiento) Abstracción de datos (la encapsulación del estado con operaciones) Paso de mensajes y polimorfismo Herencia

SCHEME también tiene una extensión orientada a objetos, SCOOPS. En esta sec­ ción, examinaremos un ejemplo de una jerarquía de objetos SCOOPS. Éste se en­ cuentra implementado por completo a través de macros de SCHEME. Una macro es una expresión-S comenzando con el átomo na ero, seguido por un nombre que será la palabra clave de una nueva forma especial. Cuando el intérprete de SCHEME encuentra una expresión macro, su expansión se copia directamente dentro del pro­ grama SCHEME, donde es evaluada. La manera precisa de cómo una expresión se expande depende del intérprete o compilador. Un método es traducirlo en expre­ siones lambda, incluyendo una expansión para cada llamada recursiva, si existe alguna. El usuario no será enterado de esta expansión cuando se ejecute SCOOPS, pero las expansiones pueden aparecer en una salida impresa de un programa que haya sido ejecutado. En cualquier caso, una vez definidas, las macros se comporta­ rán como cualquier otra forma especial. Como un ejemplo de una macro de SCHEME, considere el listado (8.2.26). (macro sqr (lambda(sexpr) (llst '* (cadr sexpr)

(8.2.26) (cadr sexpr)) ))

Cuando la expresión que contiene la palabra clave sqr es encontrada, por ejemplo, (sqr 3), la expresión se reemplaza por la expansión de la macro. En este caso, la expansión es la lista (* 3 3), que entonces es evaluada. Definamos una función implementando la regla de Pitágoras y veamos cómo trabaja. (define P ita górico (lambda (a b)

(8.2.27)

(sqrt (+ (sqr a) (sqr b ))) ))

Una llamada a Pitagori co y las evaluaciones resultantes son: => (Pitagórico 3 4) (sqrt (+(apply(* a a) 3) (apply(* b b) 4)))

(8.2.28)

(sqrt (+ 9 16)) (sqrt 25) =>5. Cuando usted ejecuta SCHEME, no estará enterado de la sustitución de (* a a ) y (* b b) por ( s q r a) y ( s q r b). El texto real de la macro se sustituye cuando la palabra clave es encontrada, y luego evaluada. Una macro no es llamada del mis­ mo modo que una función. Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aphcativa)

391

SCOOPS implementa clases, instancias de clases u objetos, variables de clases, variables de instancia, métodos y mixins. Las mixins son superclases heredadas por una clase siendo definida. SCOOPS incluye macros para definir las palabras clave def 1ne-class,c1assvars, Instvars,deflne-method, make-Instance y mixins. Las pri­ meras tres implementan ocultamiento de información y abstracción de datos; deflne-method define clases, malee-Instance crea objetos y mixins implementa herencia. Definamos las tres clases de S C O O P S , poi nt, 1 i ne y r e c t a n g l e, con 1 i ne he­ redando de p o i n t , y r e c t a n g l e heredando de ambas, como se ilustra en la fi­ gura 8.2.11. El listado (8.2.29) muestra las definiciones de SCHEME para las tres clases. (define-class point

(8.2.29)

(classvars (origin-x 0) (origin-y 0)) (instvars (x

(active 0

() move-x))

(y

(active 0

() move-y))

(color (active 'yellow () change-color))) (options settable-variables inittable-variables)) (complle-class point) (define-class U n e (instvars (len (active 50 () change-length)) (dir (active 0

() change-direction)))

(mixins point) (options settable-variables)) (compile-class U n e )

Variables de clase: origin-x, origin-y Variables de instancia: x, y, color Métodos: set-origin-x, set-origin-y set-x, set-y move-x, move-y change-color erase, draw, redraw Variables de instancia: len, dir Métodos: change-length change-direction draw Hereda de: point Variables de instancia: height Métodos: change-height draw

F I G U R A 8.2.11 L a s clases poi nt, 1 i ne y rectangl e

Sólo fines educativos - FreeLibros

392

PARTE IV: Lenguajes declarativos (define-class rectangle (instvars (height (active 60 () change-height)) (mixins line) (options settable-variables)) (compile-class rectangle)

Los significados de los nuevos átomos, classvars, Instvars, active, settable e i ni 11 ab1e, se darán más adelante. La jerarquía de herencia es establecida mediante llamadas a la función compile-class. 1 ine hereda de point, y rectangle de 1ine, debido al orden en el cual compile-class es llamado. Tres objetos, pl, 11 y rl son creados mediante: (define pl (make-instance point)) (define 11 (make-instance line)) (define rl (make-instance rectangle))

El estado local para pl tendrá los valores x = 0 e y = 0. Debido a que point es inittable, podemos definir (define p2 (make-1nstance point) 3 42) con valores iniciales x = 3 e y = 42. Una instancia de un objeto (también llamado un objeto) también puede compartir un estado con todas las instancias de su clase. Esto se realiza mediante classvars. Cualquier poi nt tiene origin-xyori gi n-y con valores iniciales de 0. A consecuencia de que las classvars de point son settable, se definen cuatro métodos de manera automática: set-origin-x, set-origin-y, set-x y set-y. Si deseamos que todos los puntos sean relativos a otro origen distinto de (0,0), podríamos establecer set-origin-x y set-origin-y a los valores deseados. Los métodos move-x y move-y están todavía por definirse, x e y son variables activas, lo que significa que cuando cualquiera es accesada, no ocurre nada; pero cuando un valor se cambia, move-x o move-y se invocan automáticamente. (define-method (point draw)

()

(8.2.30)

(draw-point x y)) (define-method (point erase) (set-pen-color!

()

’black)

(draw)) (define-method (point redraw)

()

(set-pen-color! color) (draw)) (define-method (point move-x)(new-x) (erase) (set! x new-x) (redraw) new-x

) Una 1 i ne tiene los dos métodos automáticamente definidos, s e t - l e n y s e t di r, los cuales invocan a change-1 ength y a change-di r e c t i o n cuando son invoca­ dos. l i n e hereda todos los métodos del listado (8.2.30), excepto para draw, que será redefinida como define-method (1 ine draw). No tenemos que redefinir redraw para Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

393

1 i ne, y si el mensaje draw es enviado a una instancia de 1 i ne, se llamará al método draw de 1 i ne, no el de poi nt. De este modo draw es una función polimórfica, res­

pondiendo con un procedimiento apropiado para el receptor de este mensaje. Si queremos dibujar (draw) pl, llamamos a (send pl (draw)). El resto de los métodos para poi nt y los correspondientes a 1 i ne y r e c t a n g l e no se darán aquí, pero pueden hallarse en el archivo de demostración de PC SCHEME, SCPSDEMO.S [TI, 1987].

SCOOPS soporta herencia múltiple, así como también simple, como se mues­ tra en la figura 8.2.12. Aquí la clase polygon hereda tanto de ci r c l e como de 1 i ne, con todavía otra redefinición de draw. Si draw no ha sido redefinido, polygon here­ daría cualquier método de draw que se encuentre primero entre los mlxlns. La je­ rarquía es buscada con profundidad primero desde la parte superior para métodos. Aquí poi nt se encuentra en la parte superior. Esto depende del orden en el que las clases fueron compiladas. En este caso, el método draw de ci r c l e sería utilizado, porque fue compilado en la jerarquía más cercana a la compilación de polygon que la hecha para line.

L A B O R A T O R IO 8.5: P R O G R A M A C IÓ N SCHEME

EN S C O O P S :

Objetivos (Los laboratorios pueden encontrarse en el Instructor's Manual) 1. Ejecutar un programa de demostración SCOOPS interactivo, y enviar mensajes de objeto a objeto. 2. Definir e incluir nuevas clases como mixins. 3. Modificar clases dadas para servir a diferentes propósitos.

Variables de clase: origin-x, origin-y Variables de instancia: x, y, color Métodos: Set-x, Set-y move-x, move-y change-color erase, draw, redraw Variables de instancia: len, dir Métodos: change-length change-direction draw Hereda de: point Variable de instancia: height Métodos: change-height draw

FIGURA 8.2.12 Una jerarquía de herencia múltiple

Sólo fines educativos - FreeLibros

394

PARTE IV: Lenguajes declarativos

Dialectos SCHEME fue desarrollado como parte de los esfuerzos de investigación y ense­ ñanza del Laboratorio de Inteligencia Artificial del MIT en 1975. En 1981 fue cons­ truido un chip SCHEME que incorporaba un compilador innovador. El lenguaje fue desarrollado en forma adicional para cursos especiales en Yale y la Universi­ dad de Indiana, y las variantes del lenguaje o dialectos comenzaron a confundir a los usuarios. Un estudiante que aprendía SCHEME en, digamos, Indiana y luego iba al MIT para trabajo de graduación no podía siquiera ser capaz de leer progra­ mas escritos allí. De este modo, los creadores de SCHEME, Guy Steele y Gerald Sussman, junto con una docena de asistentes, tomaron la tarea de definir el lengua­ je [Rees, 1987]. SCHEME fue el primer LISP con ámbito lexicográfico, lo que signi­ fica que el ámbito de una variable es la expresión-S en la que está declarada; el primero en tratar los procedimientos como objetos de primera clase; y el primero en confiar solamente en llamadas de procedimientos para expresar la iteración, en lugar de confiar en ciclos no funcionales y goto's. También incorpora procedimien­ tos de escape de primera clase. Algunas de estas características han sido incorpora­ das en el lenguaje de producción Common LISP. Existen muchos otros dialectos de LISP, donde fueron agregadas características cuando los problemas se hacían apa­ rentes. SCHEME partió desde el comienzo, y utilizó la definición del LISP puro, superando muchas de las anteriores desventajas de LISP. Los LISP experimentales más comunes utilizados durante la década de los se­ tenta fueron MacLISP (MIT) y sus primos de la costa oeste, Franz LISP (Universi­ dad de California en Berkeley) y UCI-LISP (Universidad de California en Irvine). InterLISP es un producto comercial de Bolt, Beranek y Newman, Inc., y Xerox. Zeta LISP también fue desarrollado en el MIT para tomar ventaja de una máquina LISP especial. Estas diferentes localidades fueron todas organizaciones de investigación, de modo que los lenguajes cambiaban para ajustarse a intereses particulares de los investigadores involucrados. Uno podía por lo regular, con algo de esfuerzo, vol­ ver a escribir programas de un dialecto hacia otro, pero no siempre. Esto es exce­ lente si los programas rara vez son transportados fuera de su máquina anfitrión.

Common LISP Common LISP de [Steele, 1984] es un extenso producto comercial, que incorpora todas las características que nadie querría. Guy Steele, uno de sus 63 desarrolladores, lo puso así, "Common LISP es a LISP como PL/I fue a FORTRAN y COBOL"; es decir, la clase de lenguaje donde usted bajaría al vestíbulo y preguntaría a alguien cómo hacer lo que usted quiere hacer, en lugar de intentar encontrar lo que sea necesario en el inmenso manual. Por lo regular no es la mejor elección para el pri­ mer encuentro de alguien con LISP. Alrededor de 1980, las implementaciones de LISP habían comenzado a ser di­ vergentes debido a sus entornos: Zeta LISP y Spice LISP para computadoras perso­ nales, NIL para computadoras de tiempo compartido comerciales, y S -l para supercomputadoras. Common LISP está destinado a ser compatible con Zeta LISP, MacLISP e InterLISP, en ese orden. Es decir, un programa escrito en el núcleo de Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

395

CommonLISP debería ejecutarse en cualquiera de los otros sistemas, con las carac­ terísticas no Common LISP consideradas como extensiones. Common LISP tam­ bién tiene extensiones, tales como una implementación de paquetes, pero éstas no son parte del núcleo. Common LISP está destinado a ser portátil, eliminando características que no pueden ser implementadas en un gran número de máquinas. Se han hecho esfuer­ zos para hacerlo consistente, expresivo, eficiente y poderoso. También está destina­ do a ser estable de modo que posteriores implementaciones serán extensiones para un núcleo sin cambio. Una ventaja real para Common LISP ha sido el interés mostrado por el Depar­ tamento de la Defensa de Estados Unidos en él, o en alguna extensión de él, como la base para un lenguaje de muy alto nivel para desarrollar prototipos [Gabriel, 1989]. Si, como ha sido sugerido, el DOD acepta programas escritos sólo en Ada o en Common LISP, estos lenguajes seguramente prosperarán, puesto que el DOD es el más grande consumidor de software en Estados Unidos. Aunque el núcleo para Common LISP es un pequeño lenguaje, incorpora mu­ chas extensiones. Una de éstas son los paquetes, que examinamos en Ada. Los pa­ quetes de LISP fueron primero desarrollados para Zeta LISP e incorporados en Common LISP. Un paquete LISP es en esencia un espacio de nombre. Si NewAdd es una función en pakcagel y también en package2, no ocurrirá un conflicto de nom­ bre. Uno puede pensar en NewAdd como el packagel.NewAdd y package2.NewAdd (muy parecido como en Ada). Los paquetes deben ser manipulados con cuidado para evitar errores sutiles, pero ellos proporcionan modularización y una base para implementar objetos, lo que permite a varios programadores trabajar en un siste­ ma extenso sin invadir el espacio de cada uno de los otros. EJERCICIO S

8.2

1. Considere la función mapcar del listado (8.2.9). Suponga que 11 es una lista circular donde la última celda cons apunta al principio de la lista. Demuestre cómo (mapcar sqrt 11) funcionará si 11 es:

2. a. Dibuje figuras similares a las de la figura 8.2.5 para las listas: 1)

((0) I) M (M (Y)))

2)

(((Mi) Perro) tiene (pulgas))

b. Los árboles de la figura 8.2.5 y los dos anteriores están enumerados en orden. ¿Cuál sería la representación de la lista para las mismas listas si se atravesaran en orden previo? ¿y en orden posterior? 3. a. Escriba la función predecesora para enteros no negativos como la usada en el listado (8.2.11). b. Siguiendo el patrón del listado (8.2.11), escriba una función LISP para times. 4. a. ¿Por qué pasar todos los parámetros por valor evita efectos colaterales? b. ¿Por qué leer e imprimir funciones provoca efectos colaterales? 5. ¿Cuáles abreviaturas c . . . r utilizaría para encontrar los segundos elementos de cada uno de los renglones en A - ((1 2 3)(4 5 6)(7 8 9))?

Sólo fines educativos - FreeLibros

396

PARTE IV: Lenguajes declarativos 6. ¿Cómo podríamos implementar una pila como una lista, y cómo serían escritas sus operaciones en LISP? 7. Defina una función SCHEME (subst new oíd 1 i s ) que sustituya todas las ocurren­ cias de ol d en 11 s con new. Usted puede encontrarlo más fácil si permite que subst llame a una función de ayuda (subst2 new ‘ O 11 s ). subst2 divide la lista en una parte frontal y una posterior y las une después que se ha hecho una sustitución. La parte parametrizada de la definición es: (define ( s u b s t 2 new o í d f r o n t r e a r ) . . . )

8. Defina una función SCHEME (delete-one expr 1 i s ) que elimine la primera ocu­ rrencia de expr de 1i s. (delete

1 ( a b)

' (a b

(c

d (a b ) )

(a

b))

debería devolver (A B (C 0 ) ( A B)). Como en el ejercicio 7, es posible que usted quiera utilizar una función de ayuda, (delete-one2 item front rear).

8.3 IMPLEMENTACIÓN DE LENGUAJES FUNCIONALES Los compiladores portátiles para lenguajes funcionales son fáciles de implementar puesto que los programas pueden ser traducidos a un lenguaje intermedio para el código de máquina, como en la figura 8.3.1. De este modo, cualquier compilador puede ser construido si el segundo paso, traducción desde el código intermedio, ha sido especificado para una máquina particular. La manera en que se traduce un lenguaje funcional de más alto nivel en el código intermedio está más allá del al­ cance de este libro, y se remite al lector a [Peyton Jones, 1987]. Como vimos anteriormente, LISP se basa en la expresión-S y está implementado mediante pares punteados. La implementación del par punteado de la figura 8.2.3 no toma en cuenta la tipificación, y LISP es en realidad carente de tipos. Sin embar­ go, algunas implementaciones agregan una tercer celda en cada par con el tipo (implícito) del elemento (véase la figura 8.3.2). tipo (type)

Código de lenguaje funcional

car

cdr

F I G U R A 8.3.2

Tipo agregado a una celda binaria

Código intermedio

_______ y _______ Código de máquina

F I G U R A 8.3.1

Un posible esquema de compilación para código funcional.

Sólo fines educativos - FreeLibros

CAPÍTULO etiqueta apuntador

8: Programación funcional (aplicativa)

397

tí DO .

(ptrtag) (type)

car

>f

etiqueta (,ag>

tipo . (type)

C í ar

N

1020

,,

,

cdr

cdr

FIGURA 8.3.4 Gráfica para (+ 2 4)

FIGURA 8.3.3 Celda cons tipificada etiquetada como datos

La entrada en la celda de tipo será un código para un número (N); función integrada (P); aplicación de una función (@); una estructura, tal como una lista no vacía, llamada una celda cons (:); o una abstracción lambda, (X). Además, algunos lenguajes, de manera notable SKIM (1980) y NORMA (1985), agregan a cada par punteado un bit que lo marca como un apuntador o celda de datos (véase la figura 8.3.3). Una posible repesentación gráfica para (+ 2 4) se ilustra en la figura 8.3.4. Su representación como celdas cons es representada en la figura 8.3.5. Como puede verse en las figuras, los datos LISP, entre, ellos definiciones de funciones, por lo regular se referencian a través de apuntadores. De este modo, el almacenamiento está en la pila, más que a través de registros de activación, y se le hace referencia a partir de una lista de objetos de átomos literales y sus apuntado­ res asociados, en vez de mediante una tabla de símbolos y sus localidades de me­ moria asociadas.

N

2

nil

N

4

nil

>f P

+

nil

FIGURA 8.3.5 La gráfica de la figura 8.3.4 implementada como celdas cons

Sólo fines educativos - FreeLibros

398

PARTE IV:

Lenguajes declarativos

La lista de objetos es visible para un programador de LISP y varían amplia­ mente en cada implementación. En SCHEME, tres procedimientos, object-hash, object-unhash y ge (de garbage collection; recolección de basura), permiten a un usuario asociar un objeto con ion entero único, basado en una función de disper­ sión (hashing function). La lista de objetos SCHEME es entonces una tabla de disper­ sión de objetos (tabla de cálculo de dirección de objeto).11 Los objetos a los que ya no se hace referencia son eliminados de la tabla de dispersión de objetos durante la reco­ lección de basura, la que puede ser controlada por el usuario mediante la llamada a ge, o en forma automática, ge se discutirá después en la sección acerca de recolec­ ción de basura. (object-hash

) asigna un entero a y registra la relación en la tabla de dispersión de objetos. Los objetos que son idénticos (en el sentido de eq?) se les asigna el mismo entero. (object-unhash) devuelve el objeto asocia­ do con , y proporciona con esto alguna otra referencia para que el objeto exis­ ta. Si no existe asociación, se devuelve #F. Un objeto sin otra referencia que el entero asociado con él en la tabla de dispersión de objetos se elimina de la tabla durante la recolección de basura. Evaluación débil (lazy evaluation) contra evaluación estricta (strict evaluation) Ya hemos mencionado la evaluación débil como el cálculo de valores de argumen­ tos sólo si son necesarios. Por ejemplo, en la expresión (IF p THEN q ELSE s), q necesita solamente ser evaluada si p es verdadero (TRUE). De manera similar, s es evaluada sólo si p es falso (FALSE). En una evaluación estricta, p, q y s serían todas evaluadas antes de ejecutar la expresión condicional. La evaluación débil también involucra la evaluación de una expresión tan pocas veces como sea necesario. Por ejemplo, ((lambda ( x ) ( + x x)) 2*10) se reduce a (+ (2*10) (2*10)) suponiendo una reducción de orden normal (de izquierda a derecha). Para completar el cálculo, (+ 20 (2*10)) (+ 20 20) —> 40, involucraría dos cálculos de 2*10. Si utilizáramos reducción de orden aplicativo, donde la reducción más interna se efectúa primero, tendríamos: ((lambda (x) (+ x x)) 2*10) -> ((lambda (x) (+ x x)) 20)

(+ 20 20) -» 40

eliminando un cálculo. Como se menciona en el Apéndice B, las reducciones en orden aplicativo no garantizan alcanzar una forma normal que ya no pueda ser reducida, si existe una. Como es usual, todo tiene su precio. La evaluación débil es implementada en muchos lenguajes funcionales. Un ejemplo simple es el siguiente. Supóngase que una función f se define como se expresa a continuación: (define (f x pl p2)(if (> x 0) pl p2))

Los argumentos pl y p2 no necesitan ser evaluados hasta que la veracidad o false­ dad del predicado, (> x 0), haya sido determinada. La evaluación podría ser débil; 11La tabla de dispersión (hash table) de objetos está referenciada por las direcciones de los objetos. Una función de dispersión, h(identificador objeto) = dirección asocia un objeto con su dirección de memoria.

Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

399

es decir, retrasada hasta que haya sido determinado cuál de entre pl y p2 fuera necesario. El caso de la evaluación débil incluye más que la eliminación de cálculos innecesarios. Otra ventaja es que pueden ser implementadas estructuras de datos potencialmente infinitas, con sólo esa parte necesaria siendo evaluada. El argu­ mento en contra de la evaluación débil es la velocidad de ejecución. La determina­ ción exacta de cuáles cálculos pueden ser editados o pospuestos es costosa, e involucra la implementación eficiente de "thunks", que discutimos en el capítulo 3. Los lenguajes funcionales como ML y Hope han hecho un compromiso, siendo la norma la evaluación estricta a menos que la evaluación débil sea llamada por el programador. SCHEME tiene dos operadores integrados, (delay<exp>) y (force<exp>), para implementar la evaluación débil. Una expresión retrasada (de 1ayed ) es descartada (como en un "thunk") y no es evaluada hasta que se ejecu­ ta un forcé. Lo que del ay y forcé de SCHEME hacen, como en otros esquemas de evaluación débil, es desemparejar la ejecución de la estructura aparente de un pro­ grama. Otro método para acelerar la evaluación débil es a través de la reducción de gráficas con evaluación normal, en lugar de la reducción de cadena usual utilizada en el cálculo lambda. Nuestro ejemplo anterior, si se redujera mediante métodos gráficos, nos llevaría al resultado que se muestra en la figura 8.3.6 [Hudak, 1989]. Note que esto toma el mismo número de pasos que la reducción de orden aplicativo, donde el (2*10) fue evaluado primero, pero de otro modo es una resolu­ ción de orden normal. Alcance y ligaduras LISP se parece al cálculo lambda, como se describe en el Apéndice B, y permite expresiones lambda para representar funciones sin nombre. Una expresión lambda como ((1 aabda (x)(+ x x ) ) ( * 2 10)) nunca puede ser usada otra vez puesto que no hay un nombre para hacer preferencia a ella. Para ahorrar cálculo de una expresión más de una vez, podemos utilizar una cláusula let, como se muestra en el listado (8.3.1). (8.3.1)

(let <(f ((laibda(x)(+ x x))(*210)))) <cuerpo Involucrando f>...

) (Claabda (x)(+ x x)) 2*10) (+ . .)

i

(*2 10)

(+ . .)

i

20

FIGURA 8.3.6 Reducción de gráficas de la expresión lambda (laabda (x)(+ x x)) 2 * 10)

Sólo fines educativos - FreeLibros

400

PARTE IV:

Lenguajes declarativos

El valor de la expresión lambda, 2*10 + 2*10 = 40, será empleado como el valor de f a lo largo de la expresión let; es decir, dondequiera que f se presente en <cuerpo involucrando f> . . . . El let anterior es de este modo una manera eficiente y fácilde-leer para evaluar y retener el valor de una expresión lambda. También podríamos realizar esto utilizando dos expresiones 1et, con la segun­ da dentro del alcance de la primera, como se muestra en el listado (8.3.2). (le t

((x (* 2 10)))

(8.3.2)

(let ((y (la«bda(x)(+ x x))) <exp Involucrando y>...)))

Un let también permite la asignación de varias variables, como en el listado (8.3.3): (le t

(laibda(x)(+ x x))(* 2 10))) (w 22) (z (- 16 3)) <expresion involucrando y. w, z>))

((y

(8.3.3)

En esta expresión let, a wle será asignado 22 y a la z, 13. Este comportamiento conforma a la independencia evaluativa de argumentos funcionales, w, y, y z son todos parámetros para el 1et, y no podemos hacer una suposición acerca de cuáles se evalúan primero. Si <expresión> es (+ y w z), el let anterior es equivalente a la expresión de cálculo lambda: (Az.Aw.Ay.<exp involucrando z, w, & y » ( A x . ( + x x)(* 2 10) 22 (- 16 3)).

Cuál es más fácil de leer es asunto de preferencia. SCHEME permite el uso de let* para asegurar una evaluación ordenada de modo que las variables asignadas previamente puedan ser usadas en expresiones siguien­ tes, como en el listado (8.3.4). (let*

((x (* 2 10))

(8.3.4)

(y (lambda(x)(+ x x))) <exp> ...)

Esto es equivalente a los dos let del listado (8.3.2). l et r ec (let recursivo) puede ser utilizado en lugar de 1et*, y es más poderoso. El ejemplo del listado (8.3.5) muestra la definición de dos funciones mutuamente recursivas, even? y odd?, ambas con recursión descendente hasta 0, seguida por una llamada de función a even?. (letrec ((even? (lambda(n)

(8.3.5)

(if (zero? n) (odd? (- n 1)))

)) (odd?

(lambda(n) (if (zero? n)

Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

401

#F (even? (- n 1)))

))) (even? 88))

El entorno de 1etrec se extiende para ligar la primera expresión lambda con el nom­ bre e ve n? y la segunda con odd?. La expresión (even? 88) se evalúa luego y devuelve un valor de #T para 1etrec. Si esta expresión hubiera sido (even? x),#T o #F habrían sido devueltas según el valor de x en el entorno circundante de 1etrec. Las funcio­ nes mutuamente recursivas no están permitidas en expresiones let o let*. Como hemos visto, las variables pueden estar ya sea libres o ligadas en una expresión lambda. Lo que es de interés para nosotros aquí es cómo las variables libres están ligadas a valores. El ámbito dinámico puede conducir a errores, en tanto que el seguimiento de los nombres de variable y entornos puedan llegar a confundir sin remedio a un programador. También viola la noción de la "caja negra" (black box) para un proce­ dimiento, lo que no nos entremetemos con sus trabajos internos, y resultados co­ rrectos son garantizados si pasamos los parámetros reales apropiados. Examinare­ mos dos de estos problemas, conocidos como los problemas funarg. Los problemas funarg LISP fue el primer lenguaje en tratar las funciones como objetos de primera clase que pueden ser pasados o devueltos como valores de otras funciones. Por ejemplo, la función LISP (mapear func a r g s ), como se define en el listado (8.2.9), produce una lista de valores para f u n c ( a r g ) cuando el nombre de una función se pasa a func, y una lista de argumentos para args. No hay problemas con el argumento func puesto que mapear no involucra variables libres. Sin embargo, existen dos problemas discutidos en la literatura de LISP cuando están presentes variables libres, el problema funarg descendente y el problema funarg ascendente. El primero de ellos ocurre cuando un procedimiento captura variables libres de otro entorno. El siguiente ejemplo es de Abelson y Sussman [Abelson, 1 9 8 5 ] y exhibe el problema funarg descendente. Primero, definimos un b

función s um que devuelve X f(x) cuando a y b se pasan a los x=a

límite superior y uno inferior para x, term es pasado a la función f, y next es pasado a una función para incrementar x. (define (sum term a next b)

(8.3.6)12

(if > (a b)

0 (+ (term a) (sum term (next a) next b))))

12 Al definir las funciones SCHEME hasta ahora, hemos utilizado expresiones lambda tales como

(define sumdaBbda (term a next b) . . . )). Una alternativa SCHEME es (define (sum term a next b) . . . ) .

Sólo fines educativos - FreeLibros

402

PARTE IV:

Lenguajes declarativos

S i ( s q r x) se define para devolver el cuadrado de x, una llamada de (sum s q r 1 1+3)

devolverá l 2 + 22 + 32 = 14. Acto seguido, definimos una función más especializada, suma-pot enci as, que devuelve^ x n.

(define (suma-potencias a b n)

(8 .3 .7 )

(define (potencia-n x) (expt x n)) (sum potencia-n a 1+ b ))

Una llamada de ( sum-powers 1 3 2) devolverá la misma suma de 14 que fue de­ vuelta de (sum sqr 1 1+ 3). Aquí 1+ es una función que incrementa su único argumento en 1, y (expt x n), como se define en el listado (8.3.1) devuelve xn. La figura 8.3.8 muestra los entornos y ligaduras durante el primer paso de la recursión, cuando a=l y b=3. Aquí todo funciona como fue previsto, devolviendo el valor correcto de 14. Ahora supongamos que s umse ha definido usando n como un nombre de varia­ ble en lugar de next.

(define (sum term a n b)

(8.3.8)

( i f (> a b)

0 (+ (term a) (sum term (n a) n b ))))

La situación es como en la figura 8.3.8, con la variable libre n de nth-power refirién­ dose a la n de s um, puesto que está donde nt h - powe r es llamada. Esto debería causar un error, puesto que (2 a), cuando se sustituye por (n a), no es una llamada de función (véase la figura 8.3.8). El problema ascendente se presenta cuando un procedimiento se devuelve como un valor y pierde las ligaduras de sus variables libres. El listado (8 .3 .9) ilustra el problema [Abelson, 1985]. (define (make-adder increment)

(8.3.9)

(lambda (x) (+ x increm ent)))

(let ((add3 (make-adder 3 ) ) ) . . .) fijará el nombre de la función add3 a (lambda (x ) (+ x 3 )). Una llamada subsecuente de ( add3 4), dentro del ámbito de la expre­ sión let, devolverá 7. Sin embargo, si intentamos evaluar make-adder directamen­ te, como en ((make-adder 3) 4) en un LISP con alcance dinámico, el 3 se perderá si ya existe una variable nombrada i ncrement en el entorno de llamada. Esta vez, la

Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

403

FIGURA 8.3.7 Ligaduras para (suma-potencias 1 3 2 )

FIGURA 8.3.8 La variable siguiente de la figura 8.3.7 renombra­ da como n

1+

Sólo fines educativos - FreeLibros

404

PARTE IV:

Lenguajes declarativos

variable existente "capturará" el incremento de make-adder y sustituye su valor por el destinado 3.

Recolección de basura Los lenguajes funcionales requieren más almacenamiento que los que son estructurados en bloques, por varias razones. En primer lugar, necesitan el almace­ namiento extra para el método de reducción de gráficos o la implementación y el paso de funciones. Como se ilustró en la figura 8.3.5, la reducción de gráficas de la expresión (+24) requeriría 5 celdas para su implementación. Si la expresión era (+ x y), serían creadas celdas adicionales para implementar la evaluación de x y de y, a las cuales otras referencias ya estarían apuntando, como se ilustra en la figura 8.3.9. Tales celdas son creadas en forma dinámica a medida que las expresiones son encontra­ das, así que de alguna manera necesitan estar incluidas en un compilador para que un lenguaje funcional las devuelva a almacenamiento disponible cuando ya no sean necesarias. Las celdas a las que no se puede tener acceso desde un programa debido a que no hay referencias (apuntadores) activas para ellas se conocen como basura (garbage). En un lenguaje como Pascal, el almacenamiento está dividido en una pila de recursos de memoria (heap) y una pila de estructura de datos (stack). La asigna­ ción de almacenamiento o memoria de la pila (heap) se realiza utilizando el proce­ dimiento new, y se devuelve haciendo uso de dlspose. Las variables locales y vínculos para el procedimiento llamado son automáticamente extraídas de la pila (stack) cuando termina un procedimiento. Esto puede no ser así en un lenguaje funcional, donde las funciones pueden ser pasadas como valores de parámetros, porque las referencias a las variables locales de una función pueden persistir incluso después que la propia función ha terminado. De este modo todo el almacenamiento se loca­ liza en una pila (heap) sin desasignación automática o celdas innecesarias.

FIGURA 8.3.9

Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

405

Los métodos para recolectar y devolver celdas sin referencia son llamados re­ colección de basura. Pueden ser consumidores de tiempo, de manera que se ha desarrollado mucho trabajo en la implementación de recolectores eficientes. Un método, llamado mark-scan, se ejecuta de manera automática cuando el almacena­ miento está próximo a agotarse. Cada celda debe contener un bit extra para el mar­ cado. Durante la fase de marcado, la estructura gráfica es recorrida totalmente, y marca cada celda que se encuentre. Si una celda permanece sin marcar, no es referenciada en la estructura presente, y de este modo es basura. En la fase de ras­ treo (sean), todas las celdas sin marcar son devueltas al almacenamiento. Otros métodos bien conocidos son el copiado (copying) y el conteo de referencia (reference counting). Un procedimiento de copiado divide la memoria disponible en dos secciones llamadas desde-espacio y hacia-espacio. Un programa en ejecución asigna la memoria en desde-espacio. Cuando se invoca el algoritmo de copiado, toda la estructura es recorrida, pero no se marca en la fase de marcado (mark) de mark-scan. Durante el recorrido, cada celda es copiada de desde-espacio hasta ha­ cia-espacio. Aquello que sea inaccesible permanece en desde-espacio y entonces es basura. Cuando el copiado se finaliza, desde-espacio y hacia-espacio son intercambiados. Un método de conteo de referencia requiere un campo de conteo extra en cada celda para contar referencias a la celda. Cuando una celda es creada, el conteo se establece a 1. Si es además referenciada, el conteo se incrementa en 1 y cuando es derreferenciada, disminuye en 1. Cuando el conteo alcanza 0, la celda se devuelve al almacenamiento disponible. No discutiremos aquí los méritos de los diversos métodos, pero remitiremos al lector interesado a [Peyton Jones, 1987]. E J E R C I C I O S 8.3 1. Evalúe las expresiones siguientes utilizando: • Evaluación normal (de izquierda a derecha) • Evaluación aplicativa (la expresión más interna primero) • Evaluación libre • Reducción de gráficas Mantenga la pista del número de sustituciones. a. (laabda (x) (laabda (y) (+yy) x) 3*20) b. (laabda (x) (laabda (y) x)) (laabda (x) x) (lambda (s) (s s)) (laabda (s) s)) 2. ¿Cómo vería un lenguaje, como Pascal, el ámbito ilustrado en la figura 8.3.7? 3. Dibuje un diagrama de entorno para la función del listado (8.3.9) e incluya la llama­ da a ( (m ake -ad der 3) 4) desde otro entorno conteniendo una variable increment = 25. ¿Cuál es el resultado de esta llamada?

8.4

SOPORTE DE PARALELISMO CON FUNCIONES Los lenguajes funcionales puros, donde no son permitidos efectos colaterales, han sido pensados para ser naturales para el procesamiento en paralelo. Una función f(ev e2, . . . , en) podría ser procesada al asignar cada uno de sus n parámetros a un Sólo fines educativos - FreeLibros

406

PARTE IV:

Lenguajes declarativos

diferente procesador y devolver sus valores al procesador que trabaja sobre f. La investigación ha procedido a lo largo de las líneas de detección automática y asig­ nación de procesos en paralelo por un compilador. No habría necesidad para de­ claraciones PAR compuestas como en Occam (véase el capítulo 5) para indicar que una secuencia de declaraciones estaba por ser ejecutada en paralelo. El compilador comprendería cuáles parámetros funcionales podrían ser evaluados en forma si­ multánea. En la sección 8.3, "Implementación de lenguajes funcionales", discutimos la evaluación débil contra la evaluación estricta y proporcionamos un breve ejemplo de la primera utilizando reducción de gráficas. Esta última es el método principal para introducir paralelismo dentro del procesamiento de lenguaje funcional, don­ de se supone (al menos inicialmente) la evaluación estricta. Supongamos que tene­ mos una función (+ e: e2). El + es estricto (no débil), debido a que ambos argumentos deben ser evaluados. Su gráfica se ilustra en la figura 8.4.1. Las @ marcan los nodos en la gráfica. Un compilador detectando nodos que son candidatos para iniciar procesos en paralelo hallaría los dos nodos que están marcados en la figura 8.4.1 con el símbolo #. No debería haber problemas al evaluar e 1 y e2 de manera concu­ rrente, puesto que ellos no pueden afectarse entre sí o a cualquier variable global. Las condicionales son expresiones donde la evaluación débil sería apropiada, ( i f t e s t - e x p t h en- ex p e l s e - e x p ) es la proposición if-then-else en SCHEME. Exis­ ten tres expresiones, todas las cuales podrían ser evaluadas en paralelo; sólo una de ellas, t e s t - e x p , es estricta. Un compilador conservador evaluaría sólo t e s t - e x p , y posteriormente uno de the n- ex p o el se - exp. Un compilador especulativo evalua­ ría la totalidad de los tres en paralelo, y utilizaría el que fuera necesario. Hay varias cuestiones aquí. No todas las expresiones pueden terminar, de modo que el proce­ samiento en paralelo especulativo podría usar en forma innecesaria tiempo de CPU. Algunos SCHEME comienzan el procesamiento en las tres expresiones, pero elimi­ nan aquellas que eventualmente llegan a ser innecesarias. Aunque los lenguajes funcionales son por lo regular concurrentes por el com­ pilador más que el programador, las mismas preguntas que planteamos en el capí­ tulo 5 están involucradas. ¿Deberíamos distribuir expresiones en cada nodo posible o sólo en segmentos principales del programa?13 ¿Qué expresiones deberían asig­ narse a qué procesadores? ¿Debería la memoria ser distribuida o compartida?

13Justamente cuanto se consigue hacer mediante cada uno de los procesadores trabajando en para­ lelo se conoce como granularidad. El paralelismo de grano fino divide una expresión en muchos segmen­ tos pequeños para ser evaluados en paralelo, mientras que un grano grueso asigna segmentos de programa más extensos a menos procesadores.

Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

407

Las ventajas reclamadas por el enfoque funcional, con el paralelismo detecta­ do por un compilador inteligente, están basadas en programadores que necesitan producir sólo expresiones matemáticas, en vez de preocuparse acerca del paralelis­ mo. Los programas producidos son completamente transportables de una máqui­ na a otra, más conducente para una verificación formal que los programas imperativos tradicionales, y más fáciles de depurar. Los programas también son más cortos y más elegantes y, por tanto, más fáciles de comprender para aquellos que se sientan a gusto con métodos matemáticos. D.A. Tumer escribe que una difi­ cultad básica de los lenguajes de programación que no son funcionales es que "son muy extensos y enredados, en términos de la cantidad que se tiene que escribir para obtener un efecto dado" [Tumer, 1982]. El problema es que el programador no tiene control sobre la granularidad (véase la nota al pie número 13) del paralelismo. Los programadores experimentan a me­ nudo con diferentes versiones de un programa para hacerlo más eficiente. Por lo regular, el paralelismo de grano grueso se ejecuta con más rapidez que el de grano fino, a medida que el gasto de la sincronización es minimizado. Un compilador inteligente no puede decidir por cada programa si es más ventajoso dividir una función en muchas expresiones pequeñas, asignando cada una a un procesador diferente, o asignar menores pero más extensos segmentos de programas a pocos procesadores. Existen varios programas experimentales para desarrollar reducción de gráfi­ cas en paralelo. Uno es el proyecto Rediflow que se desarrolla en la Universidad de Utah, el cual distribuye la memoria sobre una colección de unidades procesador/ memoria/conmutador llamadas Xputers. Una función gráfica es distribuida sobre todos los Xputers involucrados. Otro es ALICE (Applicative Language Idealized Computing Engine; Máquina de Cómputo Idealizado de Lenguaje Aplicativo) en el Imperial College de Londres. Aquí la gráfica entera se mantiene en memoria compartida, aunque los procesadores también tienen memorias individuales. Un grupo en Yale está trabajando en el proyecto DAPS (Distributed Applicative Parallel Systems,) y un grupo en el University College de Londres está desarrollando GRIP (Graph Reduction in Parallel). Las referencias para estos sistemas se pueden en­ contrar en [Peyton Jones, 1987].

8.5

OTROS LENGUAJES FUNCIONALES APL APL no es un lenguaje puramente funcional, pero es un ejemplo de un lenguaje con características funcionales que no está sustentado en el cálculo lambda. Del mismo modo que la estructura de datos primaria de LISP es la lista, la de APL es el arreglo. Ha tenido influencia sobre otros lenguajes funcionales, notablemente FP [Backus, 1978], donde la estructura de datos primaria es la secuencia. FAC [Tu, 1986], la calculadora de arreglo funcional de Tu y Perlis, está basada directamente en APL, pero incluye arreglos infinitos así como también finitos. FAC confía mucho en la evaluación débil para conseguir esto. Sólo fines educativos - FreeLibros

408

PARTE IV:

Lenguajes declarativos

ML El FP de John Backus fue uno de los primeros lenguajes funcionales aparte de LISP. Backus, el diseñador de FORTRAN, escribió un elocuente tratado acerca de las ventajas de la programación funcional en su Turing Award Lecture de 1978 [Backus, 1978]. En él discutió lo poco adecuado de los lenguajes imperativos para las ne­ cesidades de cómputo del futuro. El problema esencial es que la ejecución del programa procede mediante la alteración del almacenamiento, una palabra de computadora a la vez. No hay previsión para acciones concurrentes múltiples en algún instante simple de tiempo. FP no está basado en el cálculo lambda, sino que sobre unas cuantas reglas para combinar formas funcionales. Backus creyó que el poder del cálculo lambda para expresar todas las funciones calculables era más extenso que necesario y podría conducir con facilidad al caos. Al mismo tiempo que FP estaba siendo desarrollado en Estados Unidos, ML apareció en el Reino Unido. ML viene de las siglas en inglés de Meta Lenguaje, lo que significa un lenguaje que habla acerca de otro lenguaje, en este caso, matemá­ ticas. A diferencia de LISP, ML es fuertemente tipificado, aunque un usuario no siempre necesita declarar tipos, debido a que el compilador puede en ocasiones determinarlos por inferencia. El ML estándar es principalmente un lenguaje funcional, pero también tiene poderosas características de los lenguajes imperativos, entre ellas un mecanismo de manejo de excepciones. Los lenguajes funcionales tienen la reputación de ser lentos en su ejecución, pero los escritores de compiladores para ML han tomado ventaja de los recientes avances en emparejamiento de patrones para mejorar su eficiencia. Sus ventajas sobre LISP son: • • • • •

Tipos de datos concretos, de unión y recursivos Funciones y tipos de datos polimórficos Módulos paramétricos Excepciones Ejecución de programa de dos fases: una fase estática donde se verifica la sono­ ridad del programa y una fase dinámica en la que el programa puede ejecutar­ se sin verificación adicional

Tipos de d a to s En la figura 8.2.2, consideramos dos representaciones para números complejos, y mencionamos dos funciones, rectangul ar y polar, en SCHEME. MLha reservado palabras, datatype y con (constructor), para definir éstas. Los números complejos rectangular y polar pueden construirse como: - datatype RECT - Rect of real * real ;(* entrada del usuario *) > datatype RECT * Rect of real * real (* respuesta ML *) con Rect = fn : real * real -> RECT (* función constructor *) - datatype POLAR - Polar of real * real; > datatype POLAR - Polar of real * real con Polar = fn : real * real -> POLAR

Sólo fines educativos - FreeLibros

(8.5.1)

CAPÍTULO 8: P ro g ram ació n funcional (aplicativa)

409

Cuando las líneas comienzan con el indicador de petición de entrada (o prompt) se fija RECT o POLARa un tipo de datos (datatype) compuesto de dos números del tipo integrado, real . Nótese que RECT es un tipo de datos, mientras que Rect es una función devolviendo un RECT. Cuando RECT se declara en el p r o m p t R e c t se de­ fine automáticamente. Un objeto de cualquier tipo puede ser construido a partir de dos reales mediante la función constructor apropiada, Rect o Pol a r. El símbolo "> " indica que esta fijación ha tenido lugar. Si introducimos de manera subsecuente: - Polar (1.0, 0.5):

(* entrada *)

Polar (1.0, 0.5) : POLAR

(* respuesta ML

(8.5.2) *)

la respuesta ML es el número complejo con radio = 1, ángulo0 = 0.5 radianes y también su tipo, POLAR. Un valor polar puede asignarse a la variable, x, utilizando: (8.5.3)

val x = Polar (1.0, 0.5);

Un tipo rectangular RECT puede definirse de manera similar. Una función de con­ versión puede definirse entonces como: (8.5.4)

- fun to-polar (Rect (x,y)) = Po.lar(sqrt (x * x + y * y),arctan (y/x)); > val to-polar = fn : RECT - > POLAR

Nótese que los paréntesis están siendo utilizados en tres maneras en el listado (8.5.4). El primer conjunto, ( Rect (x , y)), liga el tipo Rect con los parámetros (x , y ) a la función to-polar siendo definida, (x, y) determina un par, y (x * x + y * y) y (y/ x ) llaman para los cálculos aritméticos. La definición de to- rect se dejará como un ejercicio. Una vez que podemos convertir fácilmente las coordenadas rectangula­ res a polares y viceversa, la definición de la aritmética compleja es directa. - fun plus-rect ((xl,yl),(x2,y2)) :

Rect =

(8.5.5)

Rect(xl + x2, yl + y2); - fun plus-polar ((rl,al),(r2,a2)) : Polar = to-polar(plus-rect(to-rect((rl,al)),to-rect(r2,a2));

Podemos colocar esto junto en un tipo unión, COMPLEX. (8.5.6)

- datatype C0MPLEX = Polar | Rect; > datatype COMPLEX = Polar | Rect con Polar = fn : POLAR - > COMPLEX con Rect

= fn : RECT - >

COMPLEX;

Las funciones aritméticas podrían definirse sobre tipos complejos, es decir: - fun plus-complex (Rect (rl, r2))

= Complex(plus-rect (rl, r2)))

(8.5.7)

| plus-complex (Polar (pl, p2)) = Complex(plus-polar (pl, p2))); > val plus-complex = fn : COMPLEX - > COMPLEX

compl ex es entonces un tipo polimórfico y plus-complex una función polimórfica, debido a que la función permite parámetros ya sea de tipo Rect como Pol ar.

Sólo fines educativos - FreeLibros

410

PARTE IV:

Lenguajes declarativos

ML define una lista, que está encerrada entre corchetes, como una secuencia ordenada de objetos de datos, todos los cuales son del mismo tipo, a diferencia de LISP, donde los elementos de lista pueden ser de cualquier tipo. Si queremos com­ binar objetos de diferentes tipos debemos utilizar tupias de longitud fija, encerra­ das entre paréntesis. La lista: - [ 6, 1, 2 , 3] ; [6.1.2.3]

( 8 .5 .8 ) : int list

que es del tipo i nt lyst, difiere de: - (6,1,2,3)

(8.5.9)

(6.1.2.3) : int * int * int * int

la que es una tupia de longitud fija 4, yde tipo i nt * i n t * i n t * i n t . El tipo i n t * i n t es un par, con funciones estándar, funf s t ( x , _ ) * x (como sedefinió en el listado (8.5.10)), fun s n d ( _ , y ) - y; fun p a i r x y - (x, y ) : , y fun swap(x, y) = (y, x ) ; . ( s q r , 3) es una tupia de longitud 2, de tipo fn * i n t y no se permite como una lista. ¿Por qué? Si estuviéramos interesados en el primer elemento de ( s q r , 3), podríamos definir una función f st: - fun fst (x, y) = x;

(8.5.10)

> val fst = fn : 'a * ’b - > 'a

La * a * ' b -> ‘ a indica que x y que y pueden ser de cualquier tipo, y que el valor devuelto será del mismo tipo que el de la primera coordenada, x, del par de argu­ mentos. snd puede ser definido de manera semejante. - fst (sqr, 3);

(8.5.11)

sqr : fn - snd (sqr, 3); 3 : int

Si queremos aplicar sqr al entero 3, podríamos introducir: - fst (sqr, 3) snd (sqr, 3);

(8.5.12)

> 9

ML también proporciona tupias con o sin nombre, generalmente llamadas re­ gistros. - {ñame = "Boole",

al ive = fal se};

> { ñ a me = " B o o l e 11,

a liv e = fal s e }

(8.5.13) :

{ ñ a me :

string,

- type MORTALITY = {ñame : string, alive

; bool};

> type MORTALITY = {ñame : string, alive

: bool}

alive

:

bool}

- val x = Mortality {ñame = "McCarthy", alive = true};

Sólo fines educativos - FreeLibros

CAPÍTULO 8: P ro g ra m a ció n fu n cio n a l (ap licativ a)

411

Recuerde que una de nuestras primeras funciones recursivas LISP calculaba la función factorial utilizando el algoritmo: factorial (n) = if (n = 0) then 1 else (n * factorial(n - 1 ) ) En ML esto se define como: - fun factorial 0 = 1

(8.5.14)

| factorial n = n * factorial

(n - 1);

> val factorial = fn : int - > int

Nótese aquí que no se mencionan tipos ni para los parámetros de, o valores devuel­ tos desde, f actor i al . ML tiene un "intérprete inteligente" que implica los tipos de datos cuando es posible. Los tipos de datos también pueden ser definidos de manera recursiva, como los correspondientes a los números naturales y pilas mostrados en el listado (8.5.15). - datatype

NAT = Zero | Succ14 of NAT;

> datatype

NAT = Zero | Succ of Nat

(8.5.15)

con Zero : NAT con Succ = fn : NAT - > NAT - datatype

'a STACK = Empty | Push of 'a * 'a

STACK;

> datatype

'a STACK = Empty | Push of 'a * 'a

STACK

con Empty : STACK con Push

'a * 'a STACK - > STACK

Una pila es exactamente lo mismo que una lista de tipo !a. 'a puede amperejarse con cualquier tipo, pero todos los elementos de la pila deben ser del mismo tipo, del mismo modo que en una lista ML. Una de las características de los lenguajes funcionales es el soporte de funcio­ nes de orden mayor. Las funciones de ML siempre toman exactamente un argumen­ to. Los parámetros múltiples son pasados como una tupia; por ejemplo, add (x y); no ( add x y ;). Sin embargo, podemos escribir funciones parcialmente aplicables que toman un argumento después de otro, devolviendo una función como el resultado parcial. - fun add

x

* fn y : int

->

x

+ y;

> val add = fn : int - > int - > int

Esto está abreviado como add * fn : int -> (fn : int -> int). y es devuelta como la función identidad, y entonces x se le agrega. Una función de tal tipo se denomina una función de Curry, por el lógico Haskell B. Curry. En ML, la función de Curry add puede ser definida:

14 succ o pred pueden ser definidos en M L como add losubstract 1. add (x y ) se define como x + y; es decir, int -> int int (x->y -» x+y). add 1 agrega 1 a cualquier argumento que se proporcione, succ 10 devuelve 11, mientras que pred 10 devuelve 9.

Sólo fines educativos - FreeLibros

412

PARTE IV:

Lenguajes declarativos

- fun add x y : int = x + y;

Esto ahorra un poco de código. Mucho más importante aún, existe un gran cuerpo de investigación que utiliza funciones de Curry, las cuales soportan la bús­ queda de poderosos medios de abstracción. La función de Curry no tiene, como parece, dos argumentos, pero devuelve y primero como el valor de la función iden­ tidad aplicada parcialmente y posteriormente agrega x a ella. Tipos de datos polimórficos Ya hemos visto ejemplos de funciones polimórficas en f s t y snd, que devuelven el primero y el segundo miembros de un par, sin importar el tipo. Cuando se define fst, ML devuelve > val fst = fn : 'a * ’b — > 1a

la notación ' a * 1b -> 1a indica que a y b son politipos; es decir, cada uno puede ser cualquier tipo que quiera. Tales funciones pueden ser escritas por el usuario para eliminar la necesidad de escribir una versión separada de una función particular para cada tipo involucrado. Módulos Los conceptos principales para los módulos de ML son estructuras, firmas yfunctors, que no tienen similar en la mayoría de los otros lenguajes de programación. Una estructura resulta de ejecutar una declaración y encapsular su entorno. Una estruc­ tura simple del Commentary on Standard ML [Milner, 1991] es como la del listado (8.5.16). structure lamp =

(8.5.16)

struct datatype bulb = ON | OFF fun switch(ON) = OFF | switch(OFF) = ON end

Más adelante en el programa, se puede hacer disponible a 1amp para su uso, empleando: open lamp

La firma APPLIANCE resume el contenido de la estructura 1amp, y es una descrip­ ción abstracta de todas las cosas que tienen al menos unbul byunswi tch como se define en el listado (8.5.17). signature

APPLIANCE

(8.5.17)

sig type bulb val switch : bulb - > bulb end

Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

413

Esto abre posibilidades para el ocultamiento de información, puesto que ML permite que una estructura sea vista sólo a través de su firma. Un functor es un mapeo de una estructura hacia otra. Si ML fuera perfecta­ mente ortogonal, no necesitaría functors, puesto que las propias funciones podrían mapear estructuras sobre estructuras. Sin embargo, éste no es el caso. Un functor puede ser concebido como una clase especial de función con dominio y rango en el conjunto de estructuras. Una función de ML señalada por la palabra clave f un no puede mapear estructuras. Los functors también pueden tener firmas, de modo que sus trabajos internos pueden estar ocultos. Excepciones Un paquete de excepción, [e], contiene ya sea un nombre de excepción, en, o un nombre de excepción enparejado con un valor, (en, v). Cuando se construye una excepción, se le asigna un nuevo nombre único. El levantamiento de excepciones es una característica imperativa de ML, en el sentido que el orden en el cual las evaluaciones han sido hechas es de importancia; es decir, cuando la excepción ocu­ rre hace una diferencia en el estado resultante. En un lenguaje funcional puro, el orden de evaluación no es de importancia, pero cuando se elevan las excepciones, el sistema debe saber cuáles evaluaciones ya han sido hechas al momento que una secuencia de cálculos se interrumpe. Una excepción simple que devuelve 0 en un intento de división entre 0 se mues­ tra en el listado (8.5.18). exceptlon divO :int * int handle divO with (x, 0) - > 0 I (x, y) - > x div y

(8.5.18)

Nótese en el listado (8.5.19) la diferencia en las respuestas de ML para di v y di vO. di v está integrada en ML para realizar división entera y tiene su propio manejador de excepciones integrado, mientras que nosotros declaramos uno diferente para di vO. (8.5.19)

- 5 div 0 Failure : div - 5 divO 0 0 : int

En cualquier caso, ML solicitará una nueva entrada por parte del usuario después que la excepción se haya alcanzado. Definición semántica de ML ML es inusual en el sentido que su semántica y su sintaxis estaban, y aún lo están, siendo desarrolladas de manera formal y simultánea. Hemos visto la EBNF utiliza­ da para definir la sintaxis de un lenguaje, y ahora es tiempo de hacer un breve examen de cómo se podría ir definiendo la semántica del lenguaje al mismo tiem­ Sólo fines educativos - FreeLibros

414

PARTE IV:

Lenguajes declarativos

po. En un apéndice de The Definition o f Standard ML [Milner, 1990], los autores afir­ man que una de las fases más difíciles en el desarrollo de ML ha sido la interacción entre diseño y descripción semántica. En la opinión de los involucrados, esto con­ duce a un alto grado de confianza tanto en el lenguaje como en el método semántico. ML se desarrolló al principio como un lenguaje para demostrar teoremas y ha sido empleado para desarrollar prototipos ejecutables para diseño de hardware, así como para propósitos más generales. El uso destinado original influenció la elección del estilo funcional para el propio ML y un método denotacional, llamado Semántica Natural, para describir su significado. El método semántico está basado en afirmaciones acerca de la evaluación de la siguiente forma: B bP=>M que dice, "en el contexto B, la frase P se evalúa en el significado M ". El propósito de la definición semántica de ML es probar cuáles afirmaciones de esta forma son verdaderas acerca de ML, y cuáles no. Quizás un ejemplo del Commentary on Standard ML [Milner, 1991] nos ofrecerá algunos matices de este esfuerzo. Digamos que s representa un estado (mem, ens), donde mem es su componente de memoria, y ens es el conjunto de nombres para excepciones. Sea A la representación de un objeto semántico. Un objeto semántico describe el significado de un objeto sintáctico, y si es estático o dinámico, simple o compuesto. Los objetos semánticos estáticos simples están en el listado (8.5.20). • Variables de tipos • Nombres de tipos • Nombres de estructuras

a E TyVar t e TyName m E StrName

(8.5.20)

Subsecuentemente, en la definición semántica, en cualquier lugar que a se presen­ te, representa una variable de tipo. TyVar es el conjunto de todas las variables de tipo. Los objetos dinámicos simples se encuentran en el listado (8.5.21). • • • • •

Direcciones Nombres de excepciones Valores básicos Valores especiales Falla

a E Addr en E ExName b E BasVal sv E SVal {FAIL}

(8.5.21)

Los objetos compuestos están construidos de los más simples, mediante unión; por ejemplo, {x} U {y} = {x, y¡; producto cartesiano, {x} x {y} = {(x, y)}; subconjunto finito, {x, y} C (x, y, z}; o mapeo finito, x -> int. Entonces la frase, s,A |- phrase => A’, s' significa que, cuando el estado de contexto s y el objeto semántico A son sujeto a una frase ML, A se transforma en el objeto A’, y s en el estado s’. Una frase ML es una instancia de una de las dieciséis Core Phrase Classes (clases de frase núcleo), expresiones o ligaduras de valores, o de las Module Phrase Classes (clases de frases módulo), expresiones de firma, descripciones de tipo de datos o declaraciones de functor. Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

415

La definición del estándar de ML (Definition o f Standard ML) [Milner, 1990] está compuesta de definiciones de objetos semánticos como las anteriores y de un con­ junto de 196 reglas de inferencias y teoremas probados acerca de ellas. Como un ejemplo, veamos la definición sintáctica del listado (8.5.22) para un tipo registro (denominado un renglón de tipo en el núcleo Core de ML), seguida por su defini­ ción semántica en el listado (8.5.23). i [patrow] }

registro

lab - pat <, patrow>

comodín renglón de patrón

patrow

atpat longcon atpat longexcon atpat patl con pat2 patl excon pat2 pat : ty var [;ty] as pat

pat

atpat scon var longcon longexcon t [patrow] 3 ( pat ) tyrow ty

: :•

(8. (8.5.22)

patrón atómico constructor de valor; es decir, A.x constructor de excepción construcción de valor infijo construcción de excepción infijo tipificado comodín constante especial variables constantes constante de excepción registros

lab15 : ty <, tyrow>

expresión de tipo registro

tyvar C [tyrow] 3 tyseq16 longtycon17 ty ty'

variable de tipo expresión de tipo registro construcción de tipo expresión tipo función (asociativa por la derecha)

( ty )

1.

2. 3. 4. 5.

p e RecType = Lab —> Type = TyVar U RecType U FunType U ConsType T E Type VE e VarEnv = (Var U Con U ExCon) Constructor de valor Con Constructor de excepción ExCon

(8. (8.5.23)

En el listado (8.5.23), 1 significa que p se establece para un identificador de tipo registro de la forma etiqueta —>tipo. En 2, r representa cualquier variable de tipo.

15lab G Lab, el conjunto de etiquetas de registros. 16Tyseq = ty (secuencia singleton) I (secuencia vacía) 17tycon es un identificador utilizado como un constructor de tipos; longtycon es un tycon discrimi­ nado tal como TuModulo.MiTipo.

Sólo fines educativos - FreeLibros

416

PARTE IV:

Lenguajes declarativos

Un valor constructor 4 es una función tal que (dos (succ(succ(cero))))/donde cero e SCon, el conjunto de constantes especiales. Un constructor de excepción construye de manera dinámica una excepción, incluyendo su nombre, cuando se satisfacen ciertas condiciones. Ahora examinemos las dos reglas de inferencia que tratan con renglones de patrones, listado (8.5.24).

Rule 40:_______

(8.5.24)

C h . . . =» (0,P)

Rule 41(a): C (- pat =» (VE, x) C |- lab = pat

(VE {lab (- x)

Rule 41(b): C |- pat => (VE, x), C 1- patrow => (VE’, p), lab * Dom p C |- lab = pat, patrow => (VE u VE', {lab bx], p) La regla 40 dice que, en cualquier situación (sin premisas) y cualquier contexto C, es un teorema que un comodín para un renglón patrón,. .., produce un estado con un entorno de variable vacía {}, y alguna variable p de tipo registro sin nombre. La regla 41(a) muestra que si una frase patrón "pat" produce un estado con el entorno de variable VE y la variable xde tipo, entonces en el mismo contexto C, la frase "lab = pat" también producirá el estado VE y, además, la ligadura "lab 1- x". Recuerde que "lab" es una etiqueta (labe1) de registro. La regla 41(b) indica que una etiqueta puede identificar un patrón compuesto "patrow", compuesto de un patrón "pat" y un patrón "row" (renglón), "lab" estará ligado tanto al patrón como al renglón de patrón. Las reglas de formación general aseguran que los entornos VE y VE' son disjuntos. Teorema (Determinancia) Sean las dos frases s, A h frase => A',s'; s, AI- frase

A ”, s"

siendo ambas inferidas. Entonces (A", s") solamente difiere de (A', s') por un cambio uno-a-uno de direcciones y nombres de excepciones, lo que no ocurre en (s, A). Prueba: La prueba es una extensa inducción sobre los diversos objetos semánticos y frases que pueden presentarse. (Mostraremos después un ejem­ plo de una de éstas.) Pero primero, se necesita probar un teorema auxiliar, co­ nocido como un Lema en Matemáticas. Lema: Si s, A h frase s', y A’ pueden ser inferidas, y cambiamos las direc­ ciones y nombres de excepciones que se presentan en (s, A), la frase puede ser todavía inferida si hacemos los mismos cambios en (s', A'). Este teorema dice que si la misma frase es aplicada al mismo objeto semántico A, su evaluación siempre será la misma (determinada), excepto quizá para las di­ Sólo fines educativos - FreeLibros

CAPÍTULO 8 : P r o g r a m a c ió n f u n c io n a l ( a p li c a t iv a )

417

recciones de memoria donde los valores son almacenados, o los nombres de las excepciones. Esta segunda diferencia ocurre porque cuando se construye una ex­ cepción, se le asigna un nuevo y único nombre. Estos nombres pueden diferir de una ejecución a otra. Y ahora para nuestro ejemplo de parte de la prueba. Supongamos que la frase es la expresión x + y, el estado s es í í 5 l- x, 6 1- y ,} , í }}, y A es el nombre de la variable de tipo, int. Entonces s'será CC5 b x, 6 b y , l l b a l ] , ( ] } , donde al es un registro para el cálculo de la frase, s" puede ser CC5 b x, 6 b y , l l b a2 1 , 0 ] , puesto que las direcciones del registro están determinadas cuando sea necesario. Una ex­ cepción puede elevarse si x o y no son del tipo int. El nombre de excepción en puede estar ligado a la variable donde la excepción fue levantada. Si en una ejecu­ ción no se eleva ninguna excepción, y en la segunda ejecución se generó el nombre en, el objeto semántico A 1reflejaría este hecho, mientras que A no lo haría. Este muy breve examen a una prueba semántica de construcciones ML puede llegar a ser pesada para los lectores que no estén acostumbrados a las demostracio­ nes formales. La incluimos para indicar el matiz de la definición de dos partes de ML. Otros Entre los lenguajes basados en el cálculo lambda se encuentran SASL, KRC, Haskell y Miranda, el cual es quizás el único lenguaje funcional comercializado en el mer­ cado. Se remite al lector a [Hudak, 1989] para un resumen de las características de estos lenguajes y para una extensa bibliografía. E J E R C I C I O S 8.5

1. Escriba una definición ML para la función to - rect, que convierta un número com­ plejo de tipo Polar a un número de tipo Rect (véase el listado (8.5.4)). s i n x y eos x son funciones estándar de ML. 2. Escriba definiciones para times-rect, minus-rect, div-rect, times-polar, minus-po1ar y di v-pol ar semejantes a la de pl us-rect del listado (8.5.5).

3. Escriba una función polimórfica de ML, swap(x, y) = (y, x).

8.6 R ESU M EN Los lenguajes funcionales están basados en la noción de las funciones matemáticas, las cuales, dada una lista de parámetros reales, devuelven un valor simple de acuerdo con alguna regla. Los lenguajes funcionales puros no permiten efectos colaterales; es decir, los valores de los parámetros nunca son cambiados durante una llamada de función. De este modo, los parámetros nunca son pasados por referencia, por nombre o retorno de valor, únicamente por valor. Los lenguajes funcionales forman una buena base para la ejecución en parale­ lo, puesto que un programa no es más que una función simple p(ax, a2, . . ., a j, donde cada parámetro a. es también una función, devolviendo un valor para p. Sólo fines educativos - FreeLibros

418

PARTE IV: Lenguajes declarativos

Cada una de las a. puede asignarse a un procesador diferente y ser evaluada de manera independiente de otra a.. El primero, y todavía el más común lenguaje funcional, es LISP, basado en el cálculo lambda de Alonzo Church. La simplicidad de definición puede llevar a expresiones complicadas que involucran paréntesis profundamente anidados. De este modo, las implementa­ ciones de LISP tales como Franz LISP, Zeta LISP, InterLISP y Common LISP pro­ porcionan muchas extensiones y abreviaciones. El dialecto SCHEME está más cercano al cálculo lambda que los otros. SASL, KRC, Haskell y Miranda son otros lenguajes que están basados en el cálculo lambda. ML y Miranda han agregado tipificación de datos al estilo funcional. Tienen más características imperativas que las de LISP, pero el programador es capaz de captar errores con mayor facilidad y pueden construirse intérpretes más eficientes. ML es en particular notable en que su semántica ha sido formalizada a medida que el lenguaje fue desarrollado. Un segundo grupo de lenguajes funcionales está basado más en la notación matemática común que en el cálculo lambda. El pionero de éstos es APL. Su tipo de datos fundamentales es el arreglo, con sus operaciones asociadas, en lugar de la lista. El sucesor más prometedor de APL es el lenguaje FP. Los defensores del estilo funcional afirman que producen programas más bre­ ves que se pueden depurar con mayor facilidad y verificar que los lenguajes de procedimientos. Las matemáticas y sus métodos de prueba han estado desarrolla­ dos durante siglos. Los lenguajes funcionales, que se construyen directamente so­ bre esta experiencia, pueden tomar ventaja de este gran cuerpo de investigación.

8.7

NOTAS SOBRE LAS REFERENCIAS Douglas Hofstadter redactó una deliciosa serie de tres artículos acerca de LISP para Scientific A m erican, cuando escribía la colum na de "Temas m etam ágicos" (Metamagical Themas). Éstos se encuentran reproducidos en [Hofstadter, 1985a] y proporcionan un ameno jugueteo mediante funciones LISP tales como HOTPO se­ guida por TATO. La columna final presenta una solución al problema de las Torres de Hanoi. Otra introducción "sin dolor" a LISP es The Little LISPer [Friedman, 1987], el cual incluye muchos diagramas y programas humorísticos. Existen varios manuales de SCHEME, entre ellos [Dybvig, 1987]. El Tutorial y Manual de Referencia [TI, 1987] es bastante adecuado si se está utilizando PC SCHEME. El informe de 40 páginas, extraído por el MIT define el lenguaje [Rees, 1987]. Abelson, Sussman y Sussman [Abelson, 1985] es un extraordinario primer curso en programación, que utiliza SCHEME a lo largo de él. Según se informa, funciona bien para los novatos del MIT, pero es duro de llevar para la mayoría de los otros principiantes. El volumen de verano de 1989 de Computing Surveys [Marzo, 1989] está dedica­ do a los paradigmas de lenguaje de programación. El artículo de Paul Hudak [Hudak, 1989] proporciona una buena, aunque no elemental, discusión acerca de la historia y el futuro posible de los lenguajes funcionales. Sólo fines educativos - FreeLibros

CAPÍTULO

8: Programación funcional (aplicativa)

419

Un interesante libro de la Universidad del estado de Colorado por Robert Mueller y Rex Page es Symbolic Computing with Lisp and Prolog [Mueller, 1990]. Los autores discuten la programación declarativa a través de aplicaciones típicas, con soluciones ya sea en LISP, PROLOG o ambos. Es bastante adecuado para auto­ didactas. ML se presenta en dos volúmenes compañeros, The Definition o f Standard ML [Milner, 1990] y Commentary on Standard ML [Milner, 1991]. Un texto simple es el de [Wikstróm, 1987]. Sin embargo, no incluye algunas de las características más inte­ resantes de ML tales como los módulos.

Sólo fines educativos - FreeLibros

CAPÍTULO 9 LENGUAJES PARA BASES DE DATOS

9.0 En este capítulo 9.1 Modelos jerárquicos y de red

422 422

Ejercicios 9.1

423

9.2 El modelo relacional

424

Manipulación de bases de datos relaciónales El álgebra relacional El cálculo relacional SQL

425 426 428 429

Sistemas basados en lógica utilizando PROLOG Ejercicios 9.2

433 434

9.3 Modelos de datos semánticos

434

Ejercicios 9.3

437

9.4 Modelo de base de datos orientado a objetos 9.5 Resumen 9.6 Notas sobre las referencias

437 438 439

Sólo fines educativos - FreeLibros

CAPÍTULO

9

Lenguajes para bases de datos

Una base de datos es un archivo más o menos permanente con una estructura. En su forma más simple, es un archivo de registros o entidades, tal como un catálogo de tarjetas de biblioteca. Es persistente en el sentido que tanto sus entidades como las relaciones entre ellas son preservadas de un uso al siguiente. Casi todos los lengua­ jes soportan la persistencia en la forma de archivos, pero muy pocas estructuras permanecen fuera de línea después que un programa ha terminado. Pascal, por mencionar uno, soporta archivos de datos tipificados en su declaración f i l e of, pero no relaciones entre los objetos de datos. Los lenguajes para manipulación de bases de datos deben soportar una des­ cripción de estas relaciones y entidades y también medios para cambiar ambos. Éstos son llamados en ocasiones lenguajes de sistemas de datos o DSL (data system languages). Los DSL soportan a menudo dos sublenguajes: el lenguaje de definición de datos, o DDL (data definition language) y el lenguaje de manipulación de datos, o DML (data manipulation language). El DDL describe la estructura y relaciones entre las entidades de datos, mientras que el DML soporta (al menos) operaciones para exa­ minar, insertar, eliminar y modificar datos. Además, a menudo el DML tiene un lenguaje de consulta, el cual es amigable con el usuario, orientado a la pantalla, interactivo y relativamente fácil de utilizar. Tanto el DDL como el DML pueden estar integrados en un lenguaje anfitrión, tal como Pascal (Pascal /R), COBOL (SQL) o FORTRAN (DL/I). Una base de datos puede ser visualizada en diversas formas, como se observa en la figura 9.0.1. En el nivel más bajo está la vista física, que describe los discos o tambores físicos reales donde están almacenados los datos. En el siguiente nivel superior de abstracción está la vista de almacenamiento, que proporciona una estruc­ tura a los propios datos físicos. La estructura de almacenamiento más común para bases de datos extensas es el árbol-B (árbol de altura balanceada), con índices, índi­ ces a índices, etcétera. Los programadores y administradores de bases de datos, pero no el usuario, pueden interactuar con esta vista. El siguiente nivel de abstracción superior es la vista conceptual, que describe cómo se organizan los datos. Por último, hay posiblemente varias vistas externas Sólo fines educativos - FreeLibros

422

PARTE IV: Lenguajes declarativos FIGURA 9.0.1 Niveles de abstracción en un sistema de base de datos

Vistas externas

Vista conceptual

para una base de datos. Estas vistas son observadas y empleadas por el usuario, con frecuencia a través del lenguaje de consulta. Al tomar todo en conjunto, el len­ guaje anfitrión más el DSL, así como las vistas externa, conceptual, de almacena­ miento y física, conforman en su totalidad un sistema de administración de base de datos o DBMS (database management system).

9.0 EN ESTE CAPÍTULO Los modelos básicos para la vista conceptual son: • • •

Modelo jerárquico Modelo de red Modelo relacional

Cada uno será considerado, pero ya que el paradigma de base de datos es princi­ palmente relacional, este modelo se presentará con mayor profundidad. Además, examinaremos las relaciones de bases de datos representativas utilizando modelos semánticos.

9.1 MODELOS JERÁRQUICOS Y DE RED Históricamente, la primera de las vistas conceptuales es el modelo jerárquico, don­ de los datos se visualizan como un árbol. Un DDL para el modelo jerárquico es el sistema de administración de información de IBM (IMS; Information Management System), con su DML acompañante, DL/I. En la librería, el DDL podría describir la jerarquía mostrada en la figura 9.1.1. Una típica consulta de base de datos para una lista de todos los editores de libros que fueron escritos por Kurt Vonnegut es: get all PUBLISH.NAME where AUTHOR = 'Kurt Vonnegut1.

Sólo fines educativos - FreeLibros

CAPÍTULO

9: Lenguajes para bases de datos

423

FIGURA 9.1.1 Je ra rq u ía d e p u b lica c io n e s

La dificultad con el modelo jerárquico es que el acceso a los registros de datos es siempre de manera descendente. Encontrar el nombre del editor para un particular AUTHOR ÑAME involucra poder recorrer hacia abajo el árbol a través de AUTHOR hasta ÑAME y luego ascender de vuelta hasta el nivel BOOK, y atravesar de regreso a través de los niveles SUBJECT y PUBLISH hasta ÑAME. Existen maneras de conectar una jerar­ quía a través de niveles, pero no es fácil para los usuarios de bases de datos em­ plear estos métodos. La solución obvia es modelar la base de datos como una gráfica, donde pueden hacerse las conexiones entre cualquier número de nodos en cualquier dirección. Esto se denomina el modelo de red. Un ejemplo se ilustra en la figura 9.1.2. El Grupo de Trabajo de Bases de Datos (DBTG; Data Base Task Group) de la Conferencia sobre Lenguajes de Sistemas de Datos (CODASYL; Conference on Data Systems Languages), el cual fue responsable de la estandarización del lenguaje de negocios COBOL, ha hecho una serie de propuestas para un lenguaje de red estándar. Se ha propuesto tres lenguajes, comenzando en 1971: un DDL, DML y un lenguaje para definir vistas diferentes del DDL. La manipulación de la base de datos se encuentra todavía en el nivel de registro, como en el modo jerárquico, pero hacer conexiones es algo más fácil. E J E R C I C I O S 9. 1 1. C o m p le te la ram a J RNL en la jera rq u ía d e p u b lica cio n es. a. ¿E n realid ad se n ecesita re p etir los ca m p o s AUTHOR, TITLE y SUBJECT?

Sólo fines educativos - FreeLibros

424

PARTE IV:

Lenguajes declarativos

FIGURA 9.1.2 Red de publicaciones b. ¿Cómo se podría idear un registro JRNL para evitar las redundancias menciona­ das en el inciso a anterior? c. Aparte del desperdicio de espacio, ¿por qué es una mala idea mantener más de una copia de los datos? Intente pensar en dos razones.

9.2 EL MODELO RELACIONAL Ni las jerarquías ni las redes proporcionan mucha estructura a la base de datos misma. Una forma estructurada se agrega en el modelo relacional. Como hemos men­ cionado, las bases de datos son utilizadas con frecuencia por aquellos con poco conocimiento de computadoras o de matemáticas. Cuando la mayoría de las per­ sonas piensa en datos, lo hace en la forma de una tabla con renglones y columnas. Un registro es entonces un renglón en una tabla. Algunas relaciones posibles de nuestra base de datos PUBLI CATION se ilustran en la figura 9.2.1. Nótese que tenemos campos integrados para conectar una relación con otra. El propio BOOK está compuesto de tres claves que nos remiten a las subrelaciones. La clave de autor AKEY y la clave de tema SKEY se denominan claves externas, puesto que son claves a otras relaciones aparte de BOOK. ISBN# es tanto una clave primaria para BOOK como una clave externa, puesto que también es la clave primaria para la reíaSólo fines educativos - FreeLibros

CAPÍTULO

425

FIGURA 9.2.1

BOOK AKEY

9: Lenguajes para bases de datos

ISBN#

SKEY

ÑAME

PKEY

ÑAME

AKEY

Relaciones de publicaciones

AUTHOR AKEY

TITLE ISBN#

PUBLISH PKEY

ÑAME

ción TITLE. Existen diversos lenguajes relaciónales, de los cuales el más influyente es el SQL (Structured Query Language). Manipulación de bases de datos relaciónales Como en otros lenguajes que hemos visto, los sistemas matemáticos permanecen subyacentes en la estructura de los lenguajes de consulta relaciónales. En realidad no hay nada nuevo bajo el sol. Aquí veremos una base de datos como un conjunto de relaciones, donde una relación es un conjunto de tupias (una tabla). Un ejem­ plo de una relación es la conocida como k-tupla, donde k es el orden de la relación. El símbolo t(k) se emplea para indicar una k-tupla arbitraria. Para la relación PUBLI SH de la figura 9.2.1, k = 2, y para las otras tres, k = 3. Hasta ahora hemos examinado las descripciones de datos, pero no los datos mismos. Los registros individuales que conforman una descripción particular son llamados instancias, y una colección de instancias es una base de datos. Usaremos la base de datos de biblioteca de muestra en la figura 9.2.2 en nues­ tros ejemplos para presentar las posibles manipulaciones sobre una base de datos

PUBLISH

AUTHOR--1

TITLE

1001

Smith

MH

MH

McG-Hill

0-013

Gatos

1002

1002

Jones

MH

BA

Bantam

1-025

Perros

1002

1003

Cohén

BA

0-036

Aves

1003

1-324

Vacas

1001

2-066

Ovejas

1003

AUTHOR--2 1003

Cohén

BA

1004

Brown

MH

FIGURA 9.2.2 Base de datos de biblioteca

Sólo fines educativos - FreeLibros

426

PARTE IV:

Lenguajes declarativos

de relaciones. La relación AUTHOR-1 tiene tres 3-tuplas, o instancias; AUTHOR-2 tiene dos 3-tuplas; PUBLISH tiene dos 2-tuplas, y TITLE tiene cinco 3-tuplas.

E l á lg e b ra re la c io n a l

Un álgebra es un conjunto con operaciones definidas respecto a ésta. El álgebra relacional se define por las operaciones permitidas sobre conjuntos de relaciones. El álgebra para el ejemplo en la figura 9.2.2 es: <{AUTH0R-1, AUTHOR-2, PUBLISH, TITLE},Unión,Diferencia-Conjuntos, Producto cartesiano, Proyección, Selecciónx S = {AUTHOR-1, AUTHOR-2, PUBLISH, TITLE} es el conjunto de relaciones, mientras que Union, Diferencia de Conjuntos, Producto Cartesiano, Proyección y Selección son las operaciones sobre S. Definiremos estas operaciones posteriormente. Unión. La Union (A, B) es el conjunto de tupias que se presentan en A o en B o bien en ambas. Union (AUTHOR-1, AUTHOR-2) es: 1001

Smith

MH

1002

Jones

MH

1003

Cohén

BA

1004

Brown

MH

Diferencia de conjuntos. La d i f e r e n c i a - c o n j u n t o s de dos relaciones A y B es A - B , el conjunto de relaciones en A pero no en B. Di f e r e n c í a - c o n j u n t o s (AUTHOR1, AUTH0R2) es: 1001

Smith

MH

1002

Jones

MH

Producto cartesiano. El product o ca r t e s i ano de dos relaciones A y B es la rela­ ción A x B, cuyas primeras coordenadas son las correspondientes a A, y las últimas, las pertenecientes a B. De este modo, si A tiene orden k xy B tiene orden k2, entonces A x B tiene orden (k2 * k2). AUTHOR-2 x PUBLISH es: 1003

Cohén

BA

MH

McG-Hill

1004

Brown

MH

MH

McG-Hili

1003

Cohén

BA

BA

Bantam

1004

Brown

MH

BA

Bantam

El producto cartesiano no es muy útil, porque obtenemos columnas duplica­ das y algunas relaciones sin sentido. Por ejemplo, el editor de Cohén es Bantam, no Sólo fines educativos - FreeLibros

CAPÍTULO

9: Lenguajes para bases de datos

427

McGraw-Hill. Dos variaciones se implementan para bases de datos relaciónales. La primera es la junta de igualdad, donde sólo se unen aquellas relaciones que tie­ nen entradas iguales en una columna especificada. Por ejemplo, la junta de igual­ dad para PKEY (equijoinpKEY) de AUTHOR-2 y PUBLISH es: PKEY

PKEY

1003

Cohén

BA

BA

Bantam

1004

Brown

MH

MH

McG-Hill

La junta natural (natural join) elimina la columna duplicada de la junta de igual­ dad. La junta natural de AUTHOR-2 y PUBLISH es: PKEY 1003

Cohén

BA

Bantam

1004

Brown

MH

McG-Hill

Proyección. Una proyección produce una nueva relación a partir de una ya existente, con sólo un subconjunto de los componentes o con componentes rearreglados. Por ejemplo, 3 , 2 ( TITLE) es: 1002

Gatos

1002

Perros

1003

Aves

1001

Vacas

1003

Ovejas

Solamente las columnas 2 y 3 permanecen, rearregladas del orden 2,3 al 3,2. Selección. La sel ecci on a, como su nombre lo implica, selecciona aquellas tupias que satisfa*gan alguna condición dada. Por ejemplo, cr[NAHE_ .Gatos. 0RHAHE. (TITLE) es: 0-013

Gatos

1002

1-324

Vacas

1001

Las operaciones aparte de la junta de igualdad y la junta natural pueden definirse también a partir de estas operaciones. Intersección. A n B es la abreviación para A-(A-B). De este modo, AUTHOR-1 n AUTHOR-2 es: 1003

Cohén

BA

Sólo fines educativos - FreeLibros

428

PARTE IV: Lenguajes declarativos

Cociente. A B es la relación que factoriza las tupias de B que se presentan en A. Por ejemplo, para las relaciones A y B mostradas aquí, A -í- B selecciona aquellas tupias en las cuales todas las tupias en B emparejan los extremos de tupias en A que tienen los mismos elementos de inicio. (1,2) aparece en el cociente debido a que (1,2,a,b) y (1,2,c,d) aparecen en A. A

B

A-B

1

2

a

b

a

b

1

2

c

d

c

d

3

4

a

b

3

4

b

c

1

2

Un lenguaje relacional puramente algebraico es el Lenguaje de Base de Sistema de Información (ISBL; Information System Base Language), desarrollado por IBM en Gran Bretaña para su uso en un sistema experimental, el Peterlee Relational Test Vehicle. Sus mejores características ya han sido combinadas en el lenguaje SEQUEL (también conocido como SQL), las cuales examinaremos a continuación, combi­ nando tanto el álgebra relacional como el cálculo relaciona!. El cálcu lo relacion al El cálculo relacional es en realidad dos cálculos: el cálculo de tupias y el cálculo de dominio. Ya se trató un poco acerca del cálculo de tupias desde el capítulo 7 y en el Apéndice A se expondrá también, ya que no es nada más que el cálculo de predica­ dos aplicado a las tupias. Las variables representaban tupias. Una fórmula tal como (EXISTE (t


R(t), donde R es una relación y t es una tupia t[i] u[j], donde es un operador de comparación tal como =, <, o bien >. t[i] representa el i-ésimo componente de la tupia t. t[i] C, donde C es una constante.

Daremos como ejemplos fórmulas de cálculo relacional representando las ope­ raciones del álgebra relacional. Éstas se encuentran dadas como conjuntos. Al con­ junto le será asignado un valor TRUE, justo en el caso de que sus miembros satisfa*gan Sólo fines educativos - FreeLibros

CAPÍTULO 9: L en g u a jes p a ra b a ses de d ato s

429

la condición utilizada en su descripción. Las letras mayúsculas tales como R o S representan relaciones, mientras que las letras minúsculas tales como t o u repre­ sentan tupias, u G R significa que la tupia u pertenece a la relación R. Unión: R U T = jt I R(t) OR S(t)} Diferencia: R - S = {t I R(t) ANDNOT(S(t))} Producto cartesiano: R x S = {t(r+s) I EXISTS(u G R) EXISTS(v G S) (t[l]=u[l] AND ... AND t[r]=u [r] AND t[r + l]= v[l] AND ... AND + [r + s]=v[s])} Proyección: , ik(R) = {t


430

PARTE IV:

Lenguajes declarativos

Institute y la International Standards Organization, siendo la versión actual la SQL/ 92 [ANSI/ISO-X3.135,1992]. La publicación de un estándar tiene muchas ventajas, principalmente en que el personal entrenado en una localidad puede ser capaz de utilizar sus mismas habi­ lidades si cambian de trabajo; las aplicaciones son transportables de una máquina a otra y serán utilizables por mucho tiempo; los sistemas pueden comunicarse de uno a otro, y los clientes pueden elegir la versión completa o un subconjunto del mismo lenguaje, según sus necesidades. C. J. Date [Date, 1993], sin embargo, pre­ viene de las numerosas deficiencias del SQL tal y como existen en la actualidad. La más seria es que nunca fue en realidad diseñado de conformidad con el álgebra relacional o el cálculo relacional y está lleno de numerosas restricciones difíciles de recordar, construcciones ad hoc y reglas especiales. En otras palabras, SQL está lejos de ser ortogonal. Además, [Date, 1995] advierte que SQL está alejándose del mode­ lo relacional. Él también expresa que algunas características que deberían ser parte del estándar han sido dejadas como definidas por la implementación o dependien­ tes de ésta. No obstante, “los vendedores están amontonándose para darle soporte, y los clientes están demandando dicho soporte" [Date, 1993]. Continuaremos usando la base de datos de biblioteca de la figura 9.2.2, inclu­ yendo las tablas PUBLISHyTITLE, pero utilizaremos la unión deAUTH0R-lyAUTH0R-2 y la llamaremos AUTHOR. También dejaremos el tercer campo nulo en el registro de Cohén, indicando que su libro, Sheep, no tiene editor todavía. La discusión posterior se basa en la obra de Date, Guide to the SQL Standard [Date, 1993]. En el Laboratorio 9.1, usted encontrará algunas diferencias, puesto que la implementación no es es­ trictamente el estándar SQL. AUTHOR 1001

Smith

MH

1002

Jones

MH

1003

Cohén

1004

Brown

MH

SQL incluye tanto un DDL como un DML. A fin de tener una base de datos para trabajar con ella, primero debemos definirla. Definiremos nuestra base de datos de la biblioteca mediante un esquema, como se ilustra en el listado (9.2.1). (9.2.1)

CREATE SCHEMA AUTH0RIZATI0N VANDEKOPPLE CREATE TABLE PUBLISH ( PNO

CHAR(2)

NOT NULL,

PNAME

CHAR(8), PRIMARY KEY ( PNO ) ) CREATE TABLE AUTHOR

( ANO

CHAR(4)

ANAME PNO

NOT NULL,

CHAR(10), CHAR(2),

PRIMARY KEY ( ANO ), FOREIGN KEY ( PNO ) REFERENCES PUBLISH )

Sólo fines educativos - FreeLibros

CAPÍTULO CREATE TABLE TITLE

( ISBN TNAME ANO

CHAR(8)

9: Lenguajes para bases de datos

431

NOT NULL,

CHAR(8), CHAR(4), PRIMARY KEY ( ISBN ), FOREIGN KEY ( ANO ) REFERENCES AUTHOR )

AUTHORI Z A H O N significa que VANDEKOPPLE creó este esquema. Advierta que la defini­ ción de datos incluye la entrada con formato. Cada tabla tiene un campo designado como una clave primaria (P R IH A R Y K E Y ), que no puede ser nula. Esta designación debe ser única para un renglón y es la forma primaria para examinar un registro. AUTHOR y TITLE también son claves externas FOREIGN K E Y s , que facilitan la referencia de tablas relacionadas. El hecho de que Cohén no tenga PNO no provoca problema alguno, puesto que no aparece en la tabla PUBLISH. El DMLde SQL tiene cuatro operaciones básicas: IN S E R T , UPDATE, DELETE y S EL E C T. Nuestro siguiente trabajo sería introducir los datos en las tres tablas definidas en el esquema. Por ejemplo, INSERT INTO AUTHOR (ANO, ANAME) VALUES (1003, 'Cohén')

Cuando el libro de Cohén es realmente aceptado por Bantam, podemos: UPDATE AUTHOR SET WHERE

PNO = 'BA1 AUTHOR.ANAME * 'Cohén'

La declaración S E L E C T es por lo general de la forma S E L E C T X F R O M Y NHERE <expresión>. Un uso es implementar la junta de igualdad que vimos cuando describimos el álgebra relacional. Utilizaremos nuestras FOREIGN K EY en el listado (9.2.2). (9.2.2)

CREATE TABLE AP AS SELECT AUTHOR.ANAME, PUBLISH.PNAME FROM AUTHOR , PUBLISH WHERE AUTHOR.PNO - PUBLISH.PNO

La siguiente tabla será el resultado: AP Smith

McG-Hill

Jones

McG-Hill

Cohén

Bantam

Brown

McG-Hill

El SQL estándar no es en particular adecuado para seleccionar un número de ren­ glones y realizar alguna operación en ellos, como se destina primordialmente para Sólo fines educativos - FreeLibros

432

PARTE IV:

Lenguajes declarativos

la incrustación en los lenguajes de procedimiento, en particular COBOL y PL/I, los cuales no están orientados para la manipulación de tablas. Se puede conseguir esta clase de iteración al declarar un cursor, que se mueve en los elementos de la tabla de la misma manera que un cursor controlado por el ratón se mueve a través de la pantalla. Supongamos que Bantam tiene ventas a alguna compañía misteriosa, para leerse de algún archivo secreto, y queremos actualizar todos los renglones en PUBLISH donde ‘ Bantam* es el PNAME. Mientras estemos en él, podríamos actualizar la clave BA con las primeras dos letras del nuevo nombre. Este código necesita estar incrus­ tado en un lenguaje anfitrión para leer el nombre misterioso y extraer los primeros dos caracteres. El código en el listado (9.2.3) es un esquema de un programa PL/I para efectuar el trabajo. EXEC SQL señala al compilador de PL/I que conmute a SQL. Xy también Y son variables PL/I que se escriben : Xy : Yen el código incrusta­ do de SQL de manera que no haya confusión con las variables SQL. EX EC SQL D EC LA R E

c

(9.2.3)

CURSOR FOR

publish.pname, publish,pno FRON publish HHERE pno » ‘ba’ X C H A R O ); Y C H A R Í2 ); S ELEC T

D EC LA R E D EC LA R E

/* declaraciones PL/I */

EX EC S Q L OPEN C ;

para todos los renglones accesibles via el cursor * / C; EX EC SQL FETC H C IN T O :X, :Y; /* lee el nuevo nombre en X y pone las dos primeras letras en Y */ EX EC SQL UPDATE PUBLISH S ET PNAME “ :X; AND PNO - :Y;

DO / *

NHERE CURRENT OF C ; EN D; EX EC SQL C LO S E C ;

En el listado (9.2.3), hay cinco operaciones con cursores: OPEN, CURRENT, FE T C H , S E T y CLO SE. OPEN establece el cursor en la parte superior de PUBLISH y comienza con S E L E C T pasando sobre todos los renglones accesibles por el cursor C. CURRENT es el renglón al que apunta actualmente C. S E T lee los valores a los que apunta el valor actual de C, mientras que FETCH lee y posteriormente mueve el cursor al siguiente renglón definido por él. C LOSE deja de declarar el cursor. Existen dos medidas de seguridad en SQL, uno que utiliza una V I EN y otro llamado GRANT. Una V I EN (vista) puede utilizarse para ocultar algunos datos a los usuarios, mientras que las operaciones son otorgadas (GRANTed) a ellas. A la ma­ yoría de los usuarios quizá no se les otorga (GRANT) privilegios de actualización (U PTA D E). Podemos crear una vista (V IE N ) de autores de McGraw-Hill ("McG-Hill") empleando: CREATE VIEW MH-AUTHORS AS SELECT * FROM AUTHOR WHERE AUTHOR.PNO = 'M H '

Sólo fines educativos - FreeLibros

CAPÍTULO

9: Lenguajes para bases de datos

433

Muchos lenguajes de bases de datos, entre ellos la versión del Sistema R (R System) de SQL, incluyen una función para la creación de un índice; por ejemplo, CREATE INDEX AUTHOR-INDEX ON (ANO [order, either ASCending or DESCending]) AUTHOR

CREATE IN D EX funciona directamente sobre la base de datos física y proporciona direcciones de renglones de datos para acelerar las consultas o búsquedas. Esto ha sido eliminado del SQL Standard, puesto que los programas son para ser portátiles entre una máquina y otra. Los índices son creados en SQL Standard haciendo uso de la función T A B L E ; es decir, CREATE TABLE AUTHOR-INDEX AS SELECT ANO FROM AUTHOR

Dos restricciones de integridad han sido por lo regular consideradas como de­ seables en los DBMS. La primera es la integridad de entidad, la cual insiste que una K EY , sea primaria o externa, no puede ser nula. La segunda es la integridad referencial, que insiste en que cada relación tenga al menos una clave (key) externa para permi­ tir enlazar dos o más relaciones. El sistema R (System R) no impone ninguna regla, mientras que SQL Standard impone la integridad de entidad pero no la integridad referencial. SQL hace provisión para la concurrencia a través de transacciones, que garanti­ za su independencia unas de otras. Una transacción termina normalmente mediante la ejecución de COMNIT WORK. ROLLBACK WORK controla una transacción no exitosa y regresa la base de datos a su estado anterior antes de la ejecución de la transacción. Un ROLLBACK debe ser llamado por medio de una transacción, y el estándar no pro­ porciona guía para transacciones ejecutándose en el momento que un sistema se caiga o las transacciones que terminan sin haber ejecutado COMNIT KORK. De modo que estas situaciones anormales deben ser manejadas caso por caso con una implementación particular. L A B O R A T O R I O 9. 1: S Q L : d B A S E IV O bjetivos (Los laboratorios pueden encontrarse en el Instructor's Manual.) 1. Familiarizarse con la codificación SQL para definir y establecer una base de datos para la base de datos PUBLISH. 2. Utilizar las facilidades de escritura del informe de algún paquete popular basado en SQL para producir un informe breve de la base de datos.

Sistemas basados en lógica utilizando PROLOG Debido a la asociación de las bases de datos relaciónales con la lógica de predica­ dos de primer orden, PROLOG es una selección natural como un lenguaje de con­ sulta. Como vimos en el capítulo 7, los hechos y las reglas forman una base de datos interna de PROLOG. Este puede usarse como el lenguaje para la interfaz con la base de datos relacional extema, la cual se considera como parte del sistema

Sólo fines educativos - FreeLibros

434

PARTE IV: Lenguajes declarativos

PROLOG. Al mantener separada la base de datos externa, los datos todavía.pue­ den ser utilizados por otras aplicaciones. A fin de ver el potencial de la sintaxis de PROLOG, considere el ejemplo del operador de junta de igualdad en la consulta del listado (9.2.2). SELECT AUTHOR.ANAME, PUBLISH.PNAME FROM AUTHOR, PUBLISH

WHERE AUTHOR.PNO = PUBLISH.PNO

Aquí preguntamos por el autor y el nombre del editor para aquellas instancias en las cuales coinciden los campos PNO. En PROLOG, esto puede escribirse como la consulta mostrada en el listado (9.2.4). ?-author(_,Aname,Pno).publishíPno.Pname), write(Aname,Pname)>nl ,fa1l.

(9.2.4)

Mientras que las versiones anteriores de PROLOG eran lentas y limitadas, se hicieron mejoras en la eficiencia que lo han hecho sensible para tomar ventaja de su potencial como un lenguaje de bases de datos. E J E R C I C I O S 9. 2 1. Utilizando la base de datos de la biblioteca de la figura 9.2.2, ¿qué tabla resulta de la junta de igualdad con AKEY (equijoinAKEY) de A U T H O R - 1 y TI TLE ? ¿Y de la junta de igualdad con AKEY (equijoinAltEY) de AUTHOR-2 y TITLE? 2. ¿Cuál es la diferencia de conjuntos (A - B) si A = T I TLE y B = a [(UHE. ,Gat0S. 0> NJBE. (TITLE)?

3. ¿Qué es P U BLI SH x T I TLE ? ¿TITLE X PUBLISH? ¿Y también T I TLE x P U BLI SH x AU THO R-2 ? 4. ¿Cuál es la junta natural con AKEY (joinA I(Ey)de TITLE x AU THOR-2? 5. Si quisiéramos agregar ACMPress a la base de datos PUBLISH, ¿utilizaríamos el INSERT o UPDATE de SQL? ¿Por qué? 6. Haga uso de proposiciones de SQL para crear la junta natural de AUTHOR y PUBLISH. 7. Cree una vista (VIEH) SQL de TITLE dando sólo aquellos títulos para autores como Smith o Jones. Primero tendrá usted que extraer de AUTHOR justamente cuáles son esos títulos. 8. Utilice una declaración SQL para borrar (DELETE) todos los autores cuyos libros estén

publicados por Bantam. 9. ¿Por qué se utiliza el predicado fal 1 en la consulta PROLOG del listado (9.2.4)?

9.3 MODELOS DE DATOS SEMÁNTICOS Un modelo relacional es ciertamente más fácil de utilizar que cualquier otro mode­ lo jerárquico o de red, pero sus tablas están todavía más cercanas a la máquina que a muchas de las relaciones naturales que se encuentran en el mundo de los nego­ cios. Los modelos semánticos fueron introducidos por primera vez como herra­ mientas de diseño de esquema. Un esquema sería diseñado y luego traducido a uno de los otros tres modelos. Examinemos un modelo semántico para la base de datos de la biblioteca en la figura 9.3.1.

Sólo fines educativos - FreeLibros

CAPÍTULO

Clave:

, x

9: Lenguajes para bases de datos

__ entidad

v.____' tipo imprimible ► función con valor simple

subtipo

>- función con valor en conjunto — ►► función con valor múltiple

tipo construido

FIGURA 9.3.1 Modelo semántico para la base de datos de biblioteca

Sólo fines educativos - FreeLibros

435

436

PARTE IV:

Lenguajes declarativos

Los modelos semánticos se distinguen por tres cosas. La primera es la repre­ sentación directa de tipos de objetos, llamados entidades. Muchos modelos se dis­ tinguen entre tipos abstractos e imprimibles o representables. Las entidades abstractas están representadas en el diagrama con triángulos; las subentidades, con círculos con flechas dobles apuntando al tipo padre. El segundo m ecanismo fundam ental que se encuentra en los modelos semánticos es la noción de atributos, o funciones entre tipos. Por ejemplo, vi ve-en mapea a AUTHOR en ADDRESS (domicilio), mientras que es - res i denci a - de mapea ADDRESS de regreso a AUTHOR. Estos atributos se piensan a menudo en el sentido relacional: AUTHOR v i v e - e n ADDRESSy ADDRESS e s - r e s i d e n c i a - d e AUTHOR.

El tercero es la habilidad de representar relaciones esUn (isA) entre supertipos y subtipos. Aquí tenemos que ACADEMIC esUn AUTHOR y EDITOR esUn AUTHOR. Como subtipos, tanto ACADEMIC como EDITOR heredan todos los atributos de un AUTHOR, incluyendo ADDRESS, ANAME y BOOK. Una diferencia entre un subtipo de modelo semántico y una subclase, en el sentido orientado a objetos, es que los atributos no pueden ser redefinidos. Una subentidad hereda sin cambios todos los atributos de la entidad padre, mientras que las relaciones esUn del modelo de la base de datos de biblioteca definen un subconjunto de AUTHORs. En esencia, hay dos clases de modelos semánticos, relaciones de entidad (ER, por sus siglas en inglés) y modelos de datos funcionales (FDM, también por sus siglas en inglés). La ER tiende a enfatizar tipos de datos abstractos, mientras que los modelos FDM están más interesados en atributos relacionados con entidades a través de funciones. La figura 9.3.1 representa una combinación de técnicas tanto ER como FDM; ADDRESS y PUBLISHER son aquí tipos abstractos y AUTHOR está relacio­ nado a sus atributos mediante funciones. Los lenguajes de consulta para bases de datos semánticas pueden parecerse mucho a SQL, como se muestra en el listado (9.3.1). (9.3.1)

for each X in AUTHOR such that Y = 'Tampa' and X lives-at ADDRESS.Y and X has-name Z print Z

Los subtipos pueden ser creados en el momento de la ejecución del programa, como en el listado (9.3.2). create subtype SCIENCE-EDITOR of EDITOR

(9.3.2)

where EXPERTISE includes SCIENCE for each X in SCIENCE-EDITOR where X has-name Y print Y

Si agregamos el record SCIENCE-EDITOR, el subtipo será agregado a la base de da­ tos. Esto se conoce como un subtipo derivado, puesto que se deriva de las propieda­ des ya existentes en la base de datos. Existen diversos modelos de datos semánticos implementados, en especial como interfaces (front ends) o componentes frontales para otros administradores de bases de datos. La mayoría de ellos se ejecuta en sistemas VAX o estaciones de trabajo Sólo fines educativos - FreeLibros

CAPÍTULO

9: Lenguajes para bases de datos

437

bajo los sistemas operativos UNIX o VMS. Hull y King [Hull, 1987], enumeran éstos como: Nombre DAPLEX FQL TAXIS Semdal GEM™ ARIEL Galileo™

Interfaz DBM S Extensión ADAPLEX Modelo de datos funcional Componente frontal o interfaz relacional SEMBASE Componente frontal o interfaz INGRES Componente frontal o interfaz relacional GALILEO

Lenguaje de implementación Ada Pascal, CODASYL Pascal R C Lenguaje de interfaz relacional Pascal Código de máquina VAX

Los lenguajes enumerados antes están enfocados para aplicaciones que mane­ jan datos intensivamente dentro de un lenguaje de procedimentos estándar. Otros lenguajes experimentales proporcionan también interfaces gráficas. E J E R C I C I O S 9.3 1. En el modelo semántico de la figura 9.3.1, ¿cuál(es) función(es) probablemente debería(n) ser "total 1-1" aparte de t i e ne -no mbr e? 2. a. En la figura 9.3.1, ¿por qué hay una flecha de doble sentido desde A U THO R hasta BOOK? b. ¿Qué significaría si la flecha desde AUTHO R hasta BOOK también fuera de doble senti­ do? 3. ¿Cuál es la diferencia entre una función valuada de conjunto (------ >-) y una valuada múltiple ( — ►►)? ¿Cuándo debería emplear cada una? 4. ¿Por qué A D D R E S S está representada como un tipo construido, en vez de una entidad? (Piense acerca de esto. Su respuesta depende de su concepción de la diferencia entre una entidad y un atributo.) 5. Escriba consultas para la base de datos semántica de la biblioteca con el fin de pro­ ducir: a. Todos los autores que traba jan-en CiudadU b. Una lista de libros publicados por McGraw-Hill (McG-Hill) 6. Vuelva a hacer el ejercicio 5 de una manera diferente; es decir, si usted no lo ha hecho así ya, cree un subtipo para lo que usted quiera.

9.4 MODELO DE BASE DE DATOS ORIENTADO A OBJETOS Los lenguajes descriptivos ofrecen un mejor enfoque para las bases de datos que los lenguajes imperativos. Por tanto, ha habido gran interés en los lenguajes basa­ dos en la lógica, los cuales discutimos anteriormente, y los sistemas de bases de datos orientados a objetos. Como lo hemos visto, los objetos están bastante cercanos a las entidades de base de datos. Los sistemas de bases de datos orientados a objetos deberían incluir las características siguientes: Sólo fines educativos - FreeLibros

438 1. 2. 3. 4.

PARTE IV:

Lenguajes declarativos

Tipos, clases y métodos Encapsulación y abstracción de datos Subtipos y herencia Identidad de objetos

Las primeras tres fueron discutidas en los capítulos 2 y 4. Las declaraciones de tipos incluirían tipos de conjunto y de registro. Un subtipo tendría métodos y cam­ pos adicionales definidos en esa subclase, pero heredaría operaciones definidas en el tipo padre. Debido a la posibilidad de sobrecarga del operador, el sistema tam­ bién debería soportar ligadura dinámica. La identidad de objetos indica que cada objeto tiene una identidad aparte de su valor. De hecho, dos objetos con los mismos valores podrían ser todavía dis­ tinguibles. Suponga que nuestra base de datos de autor sólo tiene campos para ANAMEy PNO: AUTHOR Smith

MH

Jones

MH

Cohén

BA

Brown

MH

Si tuviéramos otro autor cuyo apellido fuera Jones y el editor fuera McGrawHill, no sería posible representar esa información en esta base de datos. Puesto que una relación es un conjunto que no permite elementos duplicados, la identidad de objetos no está soportada por el modelo relacional. El campo adicional ANO sería necesario para distinguir los dos. Es interesante notar que tanto el modelo jerárqui­ co como el modelo de red soportan identidad de objetos. Un bello ejemplo de un sistema de base de datos orientado a objetos es el siste­ ma GemStone, el cual es comercializado por Servio Logic Corp. Tienen un DDL/ DML común llamado OPAL, que está relacionado a Smalltalk. El sistema puede tener interfaz con lenguajes como C o C++ cuando se escriben otras aplicaciones.

9.5

RESUMEN El paradigma de base de datos es relacional, y está relacionado de manera muy cercana a los lenguajes basados en lógica. Difiere de éstos en que los lenguajes de base de datos soportan la persistencia. Por persistencia queremos decir que las re­ laciones entre entidades de base de datos son preservadas fuera de línea. Las bases de datos relaciónales y sus lenguajes son ahora los más comunes, pero las basadas en jerarquías (estructura de árbol) o redes (estructura gráfica) aún existen. Un sistema de administración de base de datos (DBMS; database management system) incluye por lo regular dos lenguajes, un lenguaje de definición de datos (DDL; data definition language) y un lenguaje de manipulación de datos (DML; Sólo fines educativos - FreeLibros

CAPÍTULO

9: Lenguajes para bases de datos

439

data manipulation language). El DML con frecuencia se encuentra incrustado en otro lenguaje de alto nivel; por ejemplo, un segmento SQL dentro de un programa PL/I. SQL es el lenguaje de base de datos más utilizado, si no es el mejor. Tiene la capacidad de concurrencia a través de transacciones independientes.

9.6

NOTAS SOBRE LAS REFERENCIAS Los tres textos teóricos más utilizados acerca del diseño de bases de datos y lengua­ jes son [Wiederhold, 1983], [Ullman, 1988] y [Date, 1995]. El segundo se usa con más frecuencia como un texto universitario, quizá debido a que tiene un tamaño más pequeño. Tanto Ullman como Date enfatizan los modelos relaciónales, siendo Ullman más teórico, mientras que Date combina aplicaciones con la teoría. El texto de Wiederhold está dividido en tres secciones: "Estructuras y diseño de archivos", "Estructuras y diseño de bases de datos" y "Seguridad y operaciones". La serie de la ACM Computing Surveys proporciona diversos tutoriales acerca de sistemas de bases de datos. Uno de éstos es un volumen completo [Atkinson, 1987] que trata de tipos y persistencia en lenguajes de bases de datos. Otros dos, [Hull, 1987] y [Peckham, 1988], están interesados en los modelos de datos semánticos. Hull es un tutorial particularmente accesible sobre nociones semánticas, incluyen­ do implementaciones así como áreas de interés en investigación. [Lucas, 1988] proporciona un tratamiento accesible de un sistema basado en la lógica utilizando PROLOG; incluye ejemplos. La información acerca de sistemas de bases de datos orientados a objetos, incluyendo el sistema GemStone, se en­ cuentra disponible tanto en [Ullman, 1988] como en [Vossen, 1991].

Sólo fines educativos - FreeLibros

APÉNDICE A

Cálculos lógicos (para el capítulo 7)

Los cálculos lógicos son sistemas diseñados para calcular los valores de verdad de proposiciones de acuerdo a reglas particulares. El cálculo proposicional se refiere al razonamiento formal acerca de la verdad de las proposiciones. El cálculo de predica­ dos incluye el cálculo proposicional y también las variables dentro de las declara­ ciones. "Beto es un niño" es una proposición, o declaración; "X es un niño" es un predicado con una variable, X. Las proposiciones son predicados con cero varia­ bles. Una declaración o predicado es verdadero o falso, nunca un término medio, como "tal vez". ¡Pero en algún instante particular en el tiempo, podríamos no ser capaces de determinar cuál es el valor de un predicado!

EL CÁLCULO PROPOSICIONAL El cálculo proposicional, también conocido como el cálculo de declaraciones, es un sistema para calcular los valores de verdad de nuevas declaraciones suponiendo la veracidad de las otras. Por ejemplo, supongamos que tenemos dos proposiciones: "Fido es un perro" y "Fido es un gato". Para ahorrar tinta, asignaremos "Fido es un perro" a la variable p, y por otra parte, "Fido es un gato" a la variable q. Una nueva proposición sería r = p OR q. Si tanto Valor(p) como Valor(q) son verdaderos, en­ tonces Valor(r) es en forma automática verdadero. Esto es, si sabemos que Fido es un gato, entonces ciertamente Fido es un gato o Fido es un perro. O podríamos hacer la asignación Valor(r) = verdadero, sin conocer cuál de p o q es verdadera. Esto no es tan absurdo como puede parecer. Es cierto que la declaración p' OR q1es verdadera si p' = "El Mississippi desbordará el malecón mañana" y q’ = "El male­ cón aguantará mañana". Esto refleja el significado en el lenguaje natural de or (o). Las variables para declaraciones simples serán p, q, r, s , . . . . Las declaraciones compuestas pueden formarse de éstas haciendo uso de los conectivos lógicos OR, AND —» y p —» q significa que si p entonces q, y ->p significa NOT p. A cada declaración se le asignará un valor de verdad de 0 (FALSE), [falso] o 1 (TRUE) [verdadero] de acuerdo a la tabla A.l. Sólo fines educativos - FreeLibros

APÉNDICE A: Cálculos lógicos (para el capítulo 7)

442

TABLA A.l Valores de verdad de los conectivos lógicos

q i 0 i 0

p

1 1 0 0

p OR q

p AND q

1 1 1 0

1 0 0 0

p->q i 0 i i

-,p 0 0 1 1

OR se llama el or inclusivo puesto que Valor(p OR q) = 1 incluye el caso donde Valor(p) = 1 y Valor(q) = 1, así como también esos casos donde sólo una de las opciones p o q sea verdadera. Ya sea que Bruto o Casio, o ambos, hayan asesinado a César, se conoce como una instancia de la proposición p OR q, con "Bruto asesi­ nó a César" siendo sustituida por p, y "Casio asesinó a César" sustituida por q. Podríamos precisamente sustituir también "El gorrión mató al petirrojo" en lugar de p. "El gorrión mató al petirrojo o Casio asesinó a César" sería entonces una instancia diferente de la proposición p OR q. Una interpretación de p OR q es una asignación de valores de verdad a las variables proposicionales, p y q. Las diferen­ tes interpretaciones posibles de p OR q se muestran en la tercera columna de la tabla A .l, dados los valores de p y q mostrados en la primera y segunda columnas. La tabla A .l se conoce como una tabla de verdad. Es necesario hacer un comentario acerca del operador condicional p —» q. Aquí p es la premisa y q la conclusión de la proposición. La noción es que las premisas verdaderas no conducen a conclusiones falsas, de modo que a TRUE —» FALSE se le asigna el valor de FALSE. De este modo el asignar tanto a (FALSE -> TRUE)

(A.l)

como a (FALSE -> FALSE)

(A.2)

el valor de TRUE es con frecuencia confuso. La justificación aquí es que si comen­ zamos con premisas falsas, no podemos decir mucho acerca de las conclusiones. Y todavía se debe asignar a (A.l) y (A.2) valores de verdad. No tiene sentido decir que no es verdadero que "las premisas falsas le dan conclusiones falsas", de mo­ do que asignamos a (A.2) un valor de TRUE. Puesto que por lo regular estamos interesados en el valor de la conclusión más que en el de las premisas, también asignamos a (A.1) un valor de 1, puesto que la conclusión es verdadera. Otra manera de ver el operador de implicación, — es escribir p —> q en su forma disyuntiva (OR), -

p es falso; es decir, p es verdadero. La tabla de ver­ dad para estas dos formas es: P 1 1

q T 1 0 0 0

1

1

1

1 1

P ~>9 1 0

T OR q 1 o

1

1 Sólo fines educativos - FreeLibros

APÉNDICE A:

Cálculos lógicos (para el capítulo 7)

443

Nótese que las últimas dos columnas son idénticas, lo que significa que las dos proposiciones son equivalentes. Cuando se formaliza la lógica, el primer paso es describir lo que constituye una proposición válida. Esto se hace de manera recursiva mediante cinco reglas de formación como sigue: FR1: FR2: FR3: FR4: FR5:

Una sola letra del alfabeto es una proposición Si p es una proposición, también lo es -

q. Si p y q son proposiciones, también lo es p AND q.

(A.3)

Como vimos anteriormente, FR4 no es necesario, en la medida que —> puede ser reemplazado por -> y OR. Se le solicitará a usted en el ejercicio A.2 que demuestre que FR5 tampoco es necesario. De hecho, sólo se necesita tres reglas. Podemos ex­ presar todas las declaraciones del cálculo proposicional usando FR1, FR2 y alguna de las tres restantes FR3, FR4 o FR5. Una teoría lógica incluye todas las declaraciones verdaderas. Algunas de éstas se denominan axiomas y se supone que son ciertas sin prueba. Una tesis es una declaración verdadera que es o bien un axioma, o derivable de los axiomas hacien­ do uso de las reglas de inferencia de la teoría. Se ha hecho muchas formulaciones de la teoría para el cálculo proposicional (CP), que se ha demostrado que son equi­ valentes. Se dice que una teoría es completa si todas las declaraciones que son ver­ daderas (TRUE) son tesis. Es consistente si ninguna declaración que sea falsa (FALSE) puede ser derivada. Puede demostrarse que el CP es tanto completo como consis­ tente [véase para un ejemplo: Mendelson, 1979]. Uno de los conjuntos de axiomas mejor conocidos para CP es el de los Principia Mathematica (PM) de Whitehead y Russell [Whitehead, 1910]. PM1: PM2: PM3: PM4:

(pORp)->p q —> (p OR q) (p OR q) —» (q OR p) (q r) ((p OR q) -» (p OR r)).

(A.4)

Existen dos reglas de inferencia en PM. Estas son: •

R l (Sustitución Uniforme): Si p es una tesis y q es una declaración derivada de p, al sustituir todas las ocurrencias de una letra, x, por otra letra, y, entonces q es una tesis. R2 (Separación, o modus ponens): Si p y p —» q son ambas tesis, entonces q tam­ bién es una tesis.

Empleando R l y PM1, tendríamos que (s OR s) —» s es una tesis de PM, al sustituir de manera uniforme la letra s por la p. Sin embargo, (s OR p) —>s puede no ser una tesis puesto que no hemos sustituido s para todas las ocurrencias de p. La sustitu­ ción no fue uniforme. Podemos probar la validez de las declaraciones usando tablas de verdad, o mediante la construcción de una prueba enumerando todas las declaraciones verSólo fines educativos - FreeLibros

444

APÉNDICE A:

Cálculos lógicos (para el capítulo 7)

TABLA A.2 Derivación de q —» (p OR q) p

q

(p OR q)

q —> (p OR q)

1 1 0 0

i 0 i 0

1 1 1 0

1 1 1 1

daderas como premisas y luego derivando la declaración que deseamos probar mediante el uso repetido de las dos reglas de inferencia. Una tabla de verdad para PM2: q —¥(p OR q), utilizando los valores de la tabla A .l, se muestran en la tabla A.2. Puesto que la última columna conteniendo valores de verdad para PM2 contiene todos los 1, q —>(p OR q) es una tesis. Una tesis lógica en ocasiones se denomina una tautología, lo que significa que es verdadera para cualquier asignación de valor de verdad para las variables. Probemos la declaración verdadera p OR -p , haciendo uso de PM. Caso 1: Suponga p p - » (p OR -.p) p OR -p

Suposición Instancia de PM2 R2: modus ponens

Caso 2: Suponga -p -p —> (-p OR p) -p O Rp p OR -p

Suposición Instancia de PM2 R2: modus ponens Instancia de PM3

Demostración por contradicción Sin embargo, existe otro método de demostración que es más aplicable a una solu­ ción por computadora, conocido como reductio ad absurdum (reducción al absurdo). Aquí suponemos que la declaración que va a probarse es falsa (FALSE) y llegamos a una contradicción. Demostraremos este método para exhibir la validez de PM4 al suponer que es falsa; es decir, Valor(PM4) = 0. Paso 1: (q —» r)

((p OR q) -A (p OR r)) 0

Colocamos un valor de FALSE (0) bajo la —>, indicando que se supone que esta implicación sea falsa. Dado el significado de — esto solamente ocurre cuando el antecedente es verdadero (TRUE) y el consecuente, falso (FALSE). De este modo: Paso 2: (q —> r) —> ((p OR q) —» (p OR r)) 0 1

Sólo fines educativos - FreeLibros

APÉNDICE A:

Cálculos lógicos (para el capítulo 7)

445

En este momento no podemos hacer más con el antecedente (q —>r), así que consi­ deramos cómo inferir al consecuente FALSE. Esto ocurre una vez más con un ante­ cedente verdadero y un consecuente falso. Paso 3: (q —> r) —» ((p OR q) —» (p OR r)) 0 1 0 1 0 Ahora (p OR r) tiene un valor de verdad de 0, sólo cuando Valor(p) = 0 y Valor(r) = 0. Paso 4: (q —» r) —» ((p OR q) —> (p OR r)) 0 1 0 1 0 0 0 Ahora debemos hacer Valor(p) = Valor(r) = 0 de manera uniforme. Paso 5: (q —> r) —> ((p OR q) -> (p OR r)) 0 1 0 1 0 0 0 0 0 A continuación, asignamos los valores necesarios para q. Valor(q) debe ser 0, así que Valor((q r) = 1), como se determina en el paso 2. Paso 6: (q -» r) -» ((p OR q) -> (p OR r)) 0 1 0 1 0 0 0 0 0 0 Ahora Valor(p OR q) = 1 (paso 3), y esto sólo puede ocurrir con Valor(q) = 1, puesto que Valor(p) = 0 (paso 5). Paso 7: (q -> r)

((p OR q)

(p OR r))

0 1

0 1

0 0

1

Sólo fines educativos - FreeLibros

446

APÉNDICE A:

Cálculos lógicos (para el capítulo 7)

Aquí hemos puesto en negritas los dos valores porque representan la contradicción de q siendo tanto verdadera como falsa. Así hemos "reducido al absurdo" nuestra afirmación original del paso 1, de que Valor(PM4) = 0. Lógicamente, una contradic­ ción es una declaración de que es tanto verdadera como falsa. Las contradicciones no se presentan en una teoría consistente. Sin embargo, en una teoría completa, toda declaración legal es tanto verdadera como falsa, de modo que PM4 debe ser verdadera y Valor (PM4) = 1. Escribimos la reducción completa en una sola línea para colocar los pasos por encima, y los valores de verdad por debajo, de la declaración. Pasos: Valores de verdad:

625 (q r) 010

1 0

5 3 7 2 ((p OR q) -> 0 1 1 0

4 3 4 (p OR r)) 0 0 0

EL CÁLCULO DE PREDICADOS El cálculo de predicados no es nada más que el cálculo de declaraciones (proposi­ ciones) con variables y cuantificadores agregados. Los cuantificadores son FORALL (Para todo...) y EXISTS (Existe...). Por ejemplo, (FORALL X)(IF X es un perro THEN X ladra)

y (EXISTS X)(X es un perro) son declaraciones del cálculo de predicados. "Rex ladra" está en el cálculo de de­ claraciones, y no tiene cuantificadores. Examinaremos estas diferencias con más detenimiento a continuación. R elaciones y predicados

El cálculo proposicional carece de poder expresivo, en el sentido que las declara­ ciones son indivisibles. No podemos utilizar la misma declaración en diferentes instancias. Por ejemplo, no podríamos saber quién mató a César, sino saber que está muerto y necesitar una declaración que establezca "alguien mató a César". Entonces podríamos sustituir alguien por Bruto o Casio. Bruto o Casio están rela­ cionados con César mediante la relación ASESINÓ. ASESINÓ(Bruto, César) y ASESINÓ(Casio, César) son dos instancias del predicado de dos argumentos ASE­ SINÓ. (Nótese que no sabemos todavía quién lo hizo. Eso depende de una inter­ pretación o asignación de valores de verdad para las dos instancias del predicado ASESINÓ.) En matemáticas, una relación de argumentos (o binaria) es simplemente un conjunto de 2-tuplas ordenadas. Un ejemplo es la relación MENOR-QUE, <, que incluye (2,3) y (5,7), pero no incluye (2,2). Otras relaciones pueden ser funciones, tales como VECES, *, que es una relación de tres lugares o conjunto de 3-tuplas. (3,2,6) pertenece a VECES, pero (3,2,5) no. VECES también se denomina una fun­ ción binaria porque tiene dos argumentos; en el caso de (3,2,6), los argumentos son Sólo fines educativos - FreeLibros

APÉNDICE A:

Cálculos lógicos (para el capítulo 7)

447

el 3 y el 2. Las funciones o relaciones binarias con frecuencia son escritas en forma apropiada con sus nombres entre los argumentos; por ejemplo, 2 < 3, Bruto ASESI­ NÓ César, o 3 * 2 = 6. Un predicado es una relación a la que (potencialmente) puede asignarse un va­ lor de verdad. Se compone de un símbolo de predicado, tal como VECES, <, o ASESINÓ, y argumentos, los cuales pueden ser variables, constantes o functors. Un functor es una expresión funcional que no ha sido evaluada. VECES(3,2,X) es un predicado falso si 5 se sustituye para X, y verdadero si sustituimos 6. También podemos escribir VECES(3,2,1+Y))

(A.5)

si permitimos que una expresión tal como 1+Y, sea sustituida para las variables. ¿Cuál sustitución para Y hace verdadera esta última relación? Si definimos una función, PLUSl(Y) = Y + 1, (A.5) puede escribirse VECES(3,2,PLUS1(Y)). Aquí PLUSl(Y) es un functor. Necesitamos un valor para Y que haga el valor de la fun­ ción PLUSl(Y) igual a 6. Que (3,2,6) pertenezca a VECES constituye una prueba de la declaración "allí existe una X y VECES(3,2,X)". En PROLOG, esta declaración existencial podría escribirse CUALÍX : VECESÍ3,2,X)) cual (_x (times 3 2 _ x ) )

o ?- VECESÍ3.2.X)

dependiendo del dialecto que se esté utilizando. Es el trabajo del intérprete o compilador de PROLOG probar que la variable X puede ser reemplazada por 6, Para agregar predicados a declaraciones de CP, podemos emplear las reglas de formación FR1-FR3, y agregar: FR6: Si p es una fórmula y X es una variable individual, entonces (FORALL X)p es una fórmula.1 Del mismo modo que fuimos capaces de definir p AND q como “,(_,p OR ->q) (véase el ejercicio A.2a), podemos definir (EXISTS X)p, como ->(FORALL X)(-«p). Con el fin de decir "Existe un gato amarillo", podríamos utilizar la declaración convolucionada, "No es verdad que todos los gatos no son amarillos". (Usted fue advertido acerca de la negación doble por sus maestros de español de secundaria en cuestión de estilo, no de significado.) En ES-UN( X , A m a r i l l o , Gato), se dice que X es "libre". En CUAL(X: ES- UN( X , Amar i l l o , Gat o) ) , X está "ligada"2por el cuantificador de PROLOG WHICH (EXISTS). 1 Nótese que hemos cambiado la designación de una declaración de "proposición" a "fórm ula". En muchas discusiones acerca de lógica, las declaraciones escritas en forma correcta de acuerdo a las reglas FR1-FR3 más FR6 son llamadas fórmulas bien formadas, o wffs (well-formed formulas). 2 Usted ya ha pugnado con las ligaduras cuando programó funciones y procedimientos. Las varia­ bles son o globales o locales para un procedimiento y, si son locales, están "ligadas" al procedimiento donde están declaradas.

Sólo fines educativos - FreeLibros

448

APÉNDICE A:

Cálculos lógicos (para el capítulo 7)

Las reglas de inferencia R l y R2 todavía se aplican al cálculo de predicados, y agregamos dos nuevas reglas para manejar las variables: •

R3 (Sustitución uniforme de variables): Si X es una variable individual, y p es cualquier fórmula legal, y si q es una fórmula que difiere de p sólo en que todas las ocurrencias libres de X han sido reemplazadas por Y, entonces (FORALL X)p —»q es una tesis, proveyendo que la Y no llegue a estar ligada en q cuando reemplace X. R4 (Generalización universal): Si X es cualquier variable individual y p y q son fórmulas, y si p —» q es una tesis, entonces así es p —» (FORALL X)q.

En R4, la proposición p puede estar vacía, de modo que una instancia de R4 nos da: si q es una fórmula, entonces también lo es (FORALL X)q.3 Hughes y Cresswell [Hughes, 1968] demostraron el problema al que puede llegarse si se ignora la estipulación en R3, de que la Y no llegue a estar ligada en q. Supongamos que p es la declaración (EXISTS Y) (X es-un-niño-de Y). Esto, en efec­ to, establece que cualquiera que sea X, tiene un padre (la mayoría probablemente verdadero). Note que X es libre en p, mientras que Y se encuentra limitada por (EXISTS Y). Ahora si q es (EXISTS Y)(Y es-un-niño-de Y), q es falsa, al menos tanto como sabemos. Esta sustitución de Y para X viola la estipulación, puesto que per­ mitió la libre ocurrencia de X para llegar a estar ligada en q por (EXISTS Y). La declaración, (FORALL X)(EXISTS Y)(X es-un-niño-de Y) -> (EXISTS Y)(Y es-un-niño-de Y) seguramente no puede ser una tesis, puesto que el antecedente es verdadero, pero el consecuente, falso. Por fortuna, los programadores lógicos regularmente no ne­ cesitan interesarse ellos mismos con tales problemas, sino que los desabolladores de un lenguaje basado en la lógica, tal como PROLOG, se preocupan acerca de ligaduras y alcances.

El cálculo de predicados de primer orden El orden de un sistema lógico depende de qué valor pueden tener las variables individuales, tales como X y Y. Es decir, cuando decimos (FORALL X) p, ¿cuáles X precisamente tenemos en mente? En una teoría de primer orden podemos reem­ plazar X con un nombre de variable diferente como se prescribe en R3, o con una constante, llamada un individuo, o con un functor que evalúa un individuo. Es decir, en la declaración ES-UN(X, amarillo, gato), X puede ser reemplazada sola­ mente por otra variable, tal como Y, por el individuo gato Esponjoso, Tigre o MagnifiCat, o por alguna expresión funcional f(X) que se evalúa a un individuo (no por alguna otra relación que evaluaría TRUE o FALSE).

3(FORALL, X)p puede escribirse como (X)p o (VX)p, y (3X)p significa (EXISTS X)p. De modo seme­ jante, ->(VX)p quiere decir -«(FORALL X)p y ~,(3X)p o ;3Xp significa --(EXISTS X)p.

Sólo fines educativos - FreeLibros

APÉNDICE A:

Cálculos lógicos (para el capítulo 7)

449

EJERCICIOS A 1. Construya una tabla de verdad para definir el conectivo lógico XOR. p XOR q signi­ fica que ya sea p o q es verdadera, pero no ambas. 2. a. Utilizando la identidad, p AND q = _i(‘ip OR ->q), haga uso de tablas de verdad para demostrar que no es necesario FR5 del listado (A.3). b. Empleando la identidad, p —» q = - p OR q, vuelva a escribir los axiomas PM (A.4) usando sólo y OR. c. Vuelva a escribir los axiomas PM haciendo uso únicamente de -• y AND. d. Reescriba los axiomas utilizando sólo -« y 3. Utilice PM para demostrar la regla del triángulo:

A —>B, B —» C I - A~ > C La notación I- significa que si se supone que tanto A —» B como B —» C son verdade­ ras, entonces A —> C se puede probar que es verdadera; es decir, A -» C es una tesis. Usted puede encontrar con más facilidad la demostración si cambia cada una de las implicaciones (-») en la forma OR del ejercicio 2b. 4. Sustituya la multiplicación ordinaria (*) por AND, adición (+) para OR. Demuestre que cada uno de los axiomas PM tiene un valor > 0. Aquí definiremos la negación como:

p

-p

1

1

Aquí se tiene un ejemplo: PM2: q —» (p OR q) = ->q OR (p OR q)

1

q 1

1

p

-iq OR (p OR q)

->1 +(1+1) 0+ 2 2 ->0 + (1 + 0) 1+ 1

2

1

-i1 + (0 + 1) 0+ 1 1

-.0 + (0 + 1+ 1

1)

2

5.

Use el método de reducción al absurdo de la sección sobre "Demostración por con­ tradicción" para demostrar la validez de: a. p OR ->(p) (Principio del medio excluido) b. -»(p AND — '(p)) (Principio de consistencia) c. (p -» q) -> ((p r) -> (p -» (q AND r))) (Composición)

Sólo fines educativos - FreeLibros

APÉNDICE A:

Cálculos lógicos (para el capítulo 7)

<*• ((p q) ((q r) (P -> r)) e. ((q r) -> ((p q) (p -»■r))(d y e son las Leyes del Silogismo) (-,(-,(p))) P (*"►significa "si y sólo si" (iff). Una demostración involucra dos partes: una prueba de (->(_,(p))) p y una para p -> (-,(_,(p))). 6. Empleando las tesis CP del inciso 5 de estos ejercicios haga las sustituciones apropia­ das para las letras de declaración p, q y r, para hacer el modelo en CP de las siguien­ tes declaraciones: a. Se puede morir de gripe o mejorarse, pero no hay otra alternativa acerca de esto. b. Si paso este curso, significa que en realidad estudié. c. Todos los hombres son mortales. Sócrates es un hombre, por lo tanto Sócrates es mortal. 7. Distinga las ocurrencias libres y ligadas de las variables en: a. (VX)p(X,Y) b. p(X,Y) -> (VX)q(X,Y) c. ((VX)(3Y)p(Y, X, f(X, Y)) OR -(VY)q(X, f(Y)) d. ((VX)((3Y)p(Y, X, f(X, Y)) OR -(VY)q(X, f(Y))) e. ((VX)((3Y)(p(Y, X, f(X, Y)) OR -(VY)q(X, f(Y))) 8. Traduzca las declaraciones siguientes en declaraciones cuantificadas: a. Todos los perros, excepto los de pelea, son cariñosos con los niños. b. Algunos perros no son cariñosos con los niños. c. Nadie debería fumar cigarrillos. d. Alguien debería reducir el déficit. e. Si todos hicieran su parte, alguien puede reducir el déficit.

Sólo fines educativos - FreeLibros

El cálculo lambda (para el capítulo 8)

E l cálculo lambda (cálculo A) fue desarrollado por Alonzo Church para formalizar las nociones intuitivas acerca de las funciones [Church, 1941]. Después del fracaso de la teoría de conjuntos y de la lógica formal para representar todas las matemáti­ cas, Church intentó ver qué verdades matemáticas podrían estar contenidas en la teoría de funciones. Su notación, llamada el cálculo lambda, ha probado ser muy útil en la teoría de funciones, y es la base para la sintaxis de diversos lenguajes de programación funcionales, entre ellos LISP, SCHEME y ML. Si escribimos la expresión (x + 1), se puede interpretar como una regla para cálculo, o como una expresión numérica variable. Como un número, (x + 1) = (y + 1) será verdadero sólo en el caso que x = y. Sin embargo, si deseamos expresar la noción de que la regla es la misma, sin importar cuáles valores puedan ser sustitui­ dos para x o para y, necesitamos una nueva notación, Ax(+ x 1) = Ay(+ y 1). La notación Ax indica que x está ligada en la expresión que sigue, (+ x 1), la cual es una función de una variable, x. Puesto que Ay(+ y 1) difiere solamente en que la varia­ ble ligada simple ha sida renombrada de x a y, las dos expresiones representan la misma función. En SCHEME, la expresión es (1 anbda (x ) (+ x 1 )). Una expresión lambda (AxE) es llamada una abstracción, puesto que generaliza la expresión E para cualquier valor sustituido para x. La otra clase de expresión lambda se denomina una aplicación. Por ejemplo, (Ax(+ x 1) 4), que en ocasiones se escribe Ax.(+ x 1) 4, indica que estamos por aplicar la función (+ x 1) con el 4 sustituyendo la variable ligada x. Cuando (+ 4 1) es evaluada, el resultado será 5. En la segunda notación, el toma el lugar de los paréntesis circundantes de la primera notación.

SIN TAXIS Y SEM Á N TICA La sintaxis del cálculo lambda es la simplicidad misma. Tiene tres símbolos impro­ pios: A, (,); y una lista infinita de variables, a, b, c , ..., x, y, z, aJ7 b v ..., a., b .,... . Una fórmula es cualquier combinación finita de símbolos impropios y variables. Exis­ ten cuatro reglas para combinar los símbolos y variables en fórmulas bien forma­ Sólo fines educativos - FreeLibros

452

APÉNDICE B: E l cá lcu lo lam b d a (p ara el ca p ítu lo 8)

das (wffs; well-formed formulas), y tres reglas de transformación. Las reglas wff son: 1. 2.

3.

4.

Una variable x es una wff, y la ocurrencia de x en esta wff es libre. Si F y A son wff, entonces también lo es (F A). Una ocurrencia de una variable ya sea en F o A, o en ambas, es libre (o ligada) en (F A) si es libre (o ligada) en F o en A. Un ejemplo de esta regla es (Ax(x) 2), que significa que 2 está por ser sustituido en lugar de x. Si F está bien formada, y contiene al menos una ocurrencia libre de una varia­ ble x, entonces (AxF) está bien formada. Todas las ocurrencias de x en (AxF) están ligadas. Si y es otra variable que se presenta en F, y la y no es x, entonces y se encuentra ligada o libre en (AxF), dependiendo de si está ligada o libre en F. En Ax(x+y), x está ligada mientras que y se encuentra libre. Una fórmula es una wff, y las variables que se presentan en ella están libres o ligadas, sólo cuando esto se deriva de las reglas 1-3.

Dadas las reglas de formación anteriores, las expresiones lambda involucran pa­ réntesis profundamente anidados, x+y se escribe como (Ax(Ay(+((x)y)))). Esto pue­ de ser abreviado a (Ax.Ay.+ x y) cuando no ocurra ambigüedad. Cualquier función del cálculo lambda puede ser considerada como una fun­ ción de una sola variable, utilizando uno de dos dispositivos. Supongamos que tenemos una función (Ax. Ay.+ x y) representando aritmética de enteros ordinaria, la función + aplicada a las variables x, y. Esto puede volver a escribirse como ((+ x) y), donde (+ x) es una función que agrega x a su parámetro simple, y. El otro dispositi­ vo es considerar (+ x y) como (+ (x y)), donde el argumento simple para + se com­ pone del par de variables (x y). Esto simplifica las pruebas acerca de las propiedades del cálculo lambda, puesto que podemos suponer que todas las abstracciones lambda tienen parámetros simples. Las reglas de transformación dependen de si las variables están ligadas o li­ bres, como se describió anteriormente en las reglas 1-4. Las tres reglas de transfor­ mación son llamadas conversión alfa (a ), conversión beta (fí) y conversión eta (r¡). La conversión alfa nos permite cambiar nombres de variable para evitar conflictos de nombre. Utilizaremos la notación [y/x] E que significa: "'sustituyan la y para toda ocurrencia libre de la x en la expresión E". Un ejemplo de conversión alfa es: Ax.E <^>Ay.[y/x]E donde y no está libre en E. La conversión beta nos permite aplicar una función (abstracción lambda) a un argumento en particular. Por ejemplo, la expresión (Ax. + x 1)4 se reduce a (+ 4 1). La regla para la conversión beta es:

(foc.Ej) E2y [E2/x]Ej La regla final de conversión es la conversión eta, que en ocasiones puede elimi­ nar abstracciones lambda innecesarias. Por ejemplo: (Ax. + 1 x)

(+ 1),

Sólo fines educativos - FreeLibros

APÉNDICE B: E l cá lcu lo lam b d a (p ara el ca p ítu lo 8)

453

puesto que los lados izquierdo y derecho describen la misma función. Tanto las conversiones beta como eta se denominan reducciones cuando convierten desde el lado izquierdo al derecho. Unos cuantos ejemplos son convenientes. Supongamos que deseamos reducir {Ax.[Ax. + (- x 1)] x 3 } 9 usando la reducción beta. Estamos empleando corchetes [], paréntesis () y llaves {} por claridad. {A,x.[¿,x. + (- x 1)] x 3} 9 —» [kx. + (- x 1)] 9 3¡ -> +(-91)3 —» + 8 3 -> U

(B.l) [9/x] in [...] [9/x] in (. . . )

El ejemplo siguiente muestra cómo tres funciones LISP pueden escribirse como abstracciones lambda. Hay tres funciones LISP fundamentales acerca de listas: car, cdr y cons. ( cons a b ) devuelve el par punteado a •b, como se describió en el capí­ tulo 8. Si lyst es una lista, (cons head lyst) devuelve una nueva lista, con el valor de head agregado a lyst como el primer elemento, (car lyst ) devuelve el primer elemento de lyst, y (cdr lyst) devuelve una lista que contiene todos los elemen­ tos en lyst excepto el primer elemento. De este modo si lyst = (a b c ) y h e a d = 0, el valorde (car lyst) es a, el de (cdr lyst) es (b c) y (cons head lyst)es(0 a b c ). Ahora si usted recuerda nuestra discusión anterior sobre tipos de datos abstrac­ tos, un tipo de datos tal como una lista puede ser definido mediante funciones, que están generalmente relacionadas. Si especificamos que (car(cons head lyst) ) = head, y (cdr(cons head lyst) ) = lyst, cualquier función lo hará mientras estas relaciones se mantengan. Ahora supongamos que definimos abstracciones lambda: cons =

(A,a.Xb.Xf.f a b)

car

=

(Xc.c

(Xa.Xb.a))

(b.2) (b.3)

cdr

s

(Xc.c (Xa.Xb.b))

(B.4)

Verifiquemos que, en realidad, después de las reducciones beta, (cdr(cons head lyst)) - lyst, es como se muestra en el listado (B.5). (cdr (cons head lyst)) - (Xc.c (Xa.Xb.b)) -»

(b.5)

(cons head lyst)

(cons head lyst)(Xa.Xb.b)) (Xa.Xb.Xf.f a b) head lyst (Xa.Xb.b)

(Xb.Xf.f head b)

lyst (Xa.Xb.b)

(Xf.f head lyst) -» -»

(Xa.Xb.b)

(Xa.Xb.b) head lyst (Xb.b)

lyst

lyst

De este modo cdr se encuentra relacionado correctamente a cons. Dejaremos car como un ejercicio. Esto no es sino un ejemplo que muestra que nosotros en realidad no necesita­ mos en absoluto de funciones integradas especiales, pero podríamos obtenerlas mediante abstracciones lambda. De hecho, esto es una de las mayores ventajas del cálculo lambda. La tesis de Church, que es aceptada como verdadera pero no pro­ Sólo fines educativos - FreeLibros

454

APÉNDICE B: E l cá lcu lo lam b d a (p ara el ca p ítu lo 8)

bada, establece que cualquier función efectivamente calculable puede ser repre­ sentada como una abstracción lambda. Sin embargo, lo que se demostró es que las funciones calculables de Turing (véase el capítulo 6) y las funciones recursivas son equivalentes al cálculo lambda, como lo son los sistemas de Markov [Markov, 1954] y de Post [Post, 1943]. Por lo general se cree que cualquiera de estos sistemas inclu­ ye todas las funciones que pueden ser calculadas en tiempo finito. El cálculo lambda tiene otras ventajas. Una de ellas es que la evaluación de las expresiones lambda puede hacerse en cualquier orden. Lo que esto significa es que si una función tiene varios parámetros interrelacionados, digamos f(g (D) g2(D ). . . gn(D)), entonces la g. puede ser evaluada en cualquier orden sin afectar el resultado de f. En particular, todas las gt podrían ser calculadas en forma simultánea, en pa­ ralelo, devolviendo los resultados para f en el cálculo final. Esto se halla en agudo contraste con la ejecución secuencial de los lenguajes imperativos. Lo que hace posible esto es la ausencia de efectos colaterales en los lenguajes funcionales. El parámetro, D, que es compartido por todas las g;, no se verá afectado cuando algu­ na g. se aplique a éste, ni cuando g¡(D) sea evaluada.

COMPUTABILIDAD Y CORRECCIÓN Una función es una regla o método para calcular un valor, dado un (solo) paráme­ tro. Una función es calculable si el método termina con un solo resultado. La tesis de Church sostiene que todas las funciones calculables son parte de la teoría del cálculo lambda. Sin embargo, el inverso de esta tesis no es verdadero. Precisamen­ te debido a que una función particular sea lambda-definible no garantiza que sea calculable. Los intentos para evaluar abstracciones lambda que no son calculables nunca terminan. Un ejemplo de una función sin terminación es la siguiente aplicación lambda: (Ax.x x)(Ax.x x). Si sustituimos (Ax.x x) para cada x en la primera expresión utilizan­ do la reducción alfa, obtenemos el resultado sin terminación: (Ax.x x)(Ax.x x) —» (Ax.x x)(Ax.x x) —» (Ax.x x)(Ax.x x) -» . . .

(B.6)

Se ha demostrado que no hay función de prueba, Halts?(f), que devuelva YES si un cálculo de f termina, y NO en caso contrario. Aun así, el cálculo lambda tiene algunas atractivas propiedades que lo hacen particularmente conveniente como un fundamento para lenguajes de programación funcionales. Estas propieda­ des son: 1. 2. 3.

Cualquier función recursiva que se pueda expresar en el cálculo lambda es equivalente a una abstracción lambda no recursiva. Si dos funciones son equivalentes, pueden ser reducidas mediante reducciones alfa, beta y eta a la misma forma, denominada forma normal. Para cualquier expresión que pueda reducirse a una forma normal, existe una reducción de orden normal que producirá la forma. Sólo fines educativos - FreeLibros

APÉNDICE B: E l cá lcu lo lam b d a (p ara e l ca p ítu lo 8)

455

La segunda propiedad tiene que ver con las formas normales, las cuales son sólo expresiones lambda que no pueden reducirse más. La tercera propiedad nos dice cómo obtener esta forma, si es que existe. Una reducción de orden normal co­ mienza a la izquierda de una expresión, y reduce de izquierda a derecha donde sea posible. Una reducción de orden aplicativo reduce primero la expresión más interna, en cualquier lugar donde pueda ocurrir. Aunque el orden normal puede no ser la reducción más eficiente, se garantiza que producirá una forma normal, si una exis­ te. Las aplicaciones sin terminación, tales como (Ax.x x)(Ax.x x), no tienen forma normal. La existencia de formas normales, así como la eliminación de la recursión, hace de las pruebas semánticas sobre el cálculo lambda particularmente directas. Recuerde que la semántica es el significado de un lenguaje, en contraste con la sintaxis, la cual es lo que parecen las wffs. Dos operadores están integrados en el cálculo lambda: APPLY(E1E2) y EVAL(E), que evalúan una expresión definida por el usuario. Considere la expresión lambda: (Ax+ 1 x)

(B.7)

que debería incrementar el valor de x de 1. APPLY((Ax.+ 1 x) 2)

(B.8)

es ( + 1 2 ) = 3. EVAL(Addl 5) es 6, donde Addl es una funcióndefinida por el usuario para ser la expresión lambda del listado (B.7). Para Addl, el efecto deEVAL sería:

=» =>

EVAL(Addl 5) APPLY((?ix.+ 1 x) 5) ( + 1 5 ) = 6.

(B.9)

En la práctica, una de las primeras extensiones para el cálculo lambda es una lista de constantes que contiene por lo menos 0 y alguna de las funciones comunes, tales como + y *. Dada esta mejora, las expresiones lambda tienen diferente signifi­ cado en distintos entornos. Una variable lambda tiene asignado como máximo un valor en cualquier entorno. Sin embargo, el mismo nombre de variable puede tener diferentes valores en distintos entornos. Usted no se irá demasiado lejos si piensa en un entorno como un bloque de procedimientos, con todos los parámetros pasa­ dos por valor. Así, un nombre x debería pensarse como x , xe2, etcétera, donde el y e2 son entornos diferentes. Dada la existencia de entornos, la semántica de las ex­ presiones lambda puede resumirse como: EVAL(k) EVAL(x) EVAL(E1E2)e EVAL(Ax.E)ea EVAL(E)

= constante k en el entorno e =x = (EVAL (E ^) (EVAL(E2)e) = EVAL(E) donde a es un elemento arbitrario de e = donde E no tiene forma normal1

(B.10)

1 El símbolo -l se conoce como "fondo" (bottom) y se asigna c o m o un valor para funciones no computables.

Sólo fines educativos - FreeLibros

456

APÉNDICE B: E l cá lcu lo lam b d a (para el ca p ítu lo 8)

En la práctica, las constantes y las funciones integradas son las mismas en to­ dos los entornos. Por ejemplo, EVAL(O) = 0, EVAL(+) = +, EVAL(TRUE) = TRUE, EVAL(FALSE) = FALSE y EVAL(IF) = IF. En forma similar, EVAL(IF TRUE ab ) = a y EVAL(IF FALSE a b) = b. También insistiremos que si dos expresiones E1y E2 reducen a la misma forma E, entonces EVAL(E1) = EVAL(E2) = EVAL(E). Por ejemplo, advertimos anterior­ mente que (Ax + x 1) = (Ay + y 1), e introducimos la regla de conversión alfa para sustituir una variable por otra, con lo que se garantiza su evaluación al mismo valor. No obstante, dos funciones pueden devolver los mismos valores, pero no ser reducibles a cada una de las otras. Por ejemplo, (Ax.* x x) y (Ax.expt x 2), donde el término (expt x 2) significa x2, devuelven los mismos valores. De este modo, EVAL(Ax.* x x ...) = EVAL(Ax.expt x 2), pero no hay forma normal común a la cual ambas expresiones se reduzcan. EJERCICIOS B 1. ¿Cuáles de las siguientes son abstracciones lambda legales? a. Ax.x d. Ax(y (Ay(+ x y)) x) b. Ay(Ax.x) e. (Ax.+ x y) c. Ax(Ay.x) f. (Ax.Ay. + x y) 2. Efectúe las siguientes aplicaciones y evalúe la expresión lambda resultante donde sea posible: a. (Ax.x) z d. Ax.(y (Ay.(+ x y)) x) 2 b. (Ay.Ax.x)6 e. (Ax.(+ x y)) 5 c. (Ay.(Ax.x) 6) 2 f. (Ax.Ay. (+ x y)) 2 5 3. Una abstracción, Ax(Ay(E)) puede escribirse como Ax.Ay.E, como hemos visto. Tam­ bién puede ser abreviada como Ax y.E. Reescriba las expresiones de los incisos 1 y 2 de estos ejercicios en dicha forma abreviada. 4. Demuestre que (cartcons head l y s t ) ) beta reduce a head (véase el listado (B.5)). 5. Demuestre que dado un elemento arbitrario a, APPLY(Ax.(* x x)a) = EVAL(expt a 2)). 6. Demuestre que, utilizando la conversión eta, Ac.Aa.a se reduce a Aa.a, y que Ac.Aa.c se reduce a Ac.c. 7. (Un reto). Demuestre que si TRUE = Ax.Ay.x, FALSE = Ax.Ay.y, con el condicional siendo COND = Ap.Aa.Ab.(p a b) entonces EVAL (COND TRUE) = a y EVAL (COND FALSE) = b. De esta manera, COND puede reescribirse como IF p THEN a ELSE b.

Sólo fines educativos - FreeLibros

APÉNDICE

C

Fuentes de software

Este apéndice contiene información acerca de software para computadoras perso­ nales que se ejecuten en el entorno DOS. Puesto que el curso de Lenguajes de Pro­ gramación requiere de diversos compiladores e intérpretes, hemos elegido implementaciones económicas que lo hagan posible. Las asignaciones de laboratorios y el código dentro del texto principal ha sido probado en el software mencionado a continuación. No se hizo el intento de revisar todas las implementaciones disponibles o de hacer un juicio acerca de los méritos de diferentes productos.

C A PÍT U LO S 1 , 2 : VARIABLES, TIPO S D E DATOS Y A B ST R A C C IÓ N Pascal Debido a su popularidad, en muchas universidades se utiliza el Turbo Pascal ver­ sión 7.0 de: Borland Scholar Program P.O. #660001 Scotts Valley, CA 95067-0001 (800) 932-9994, ext. 1373

Ada El código en el texto ha sido probado en PC utilizando el Meridian Ada Compiler, Versión 4.1.1 para sistemas PC DOS, y el Verdix Ada Development System, Versión 5.5 para la familia de computadoras AT&T 3B. Ambos compiladores son productos de la Verdix Corporation cuya dirección se muestra en la página siguiente. Sólo fines educativos - FreeLibros

458

APÉNDICE C: Fuentes de software

Verdix Corporation 205 Van Burén Street Hemdon, VA 22070 (703) 318-5800 o (800) 653-2522 Versiones gratuitas de Ada son GWU-Ada/Ed y NYU Ada/Ed. La informa­ ción para obtener ambas de la red se encuentra disponible a través de FTP anóni­ mos en el directorio/languages/ada/compiler/adaed en wuarchive.wustl.edu. Para sistemas Win95, UNIX y OS/2, existe el traductor GNU Ada, gnat, de la Universidad de Nueva York. Use ftp anónimo para obtener información sobre gnat en el directorio pub/ gnat en cs.nyu.edu. C A PÍT U LO 3: LEN G U A JES ESTR U C TU R A D O S EN BLO Q U ES C Empleamos Turbo C de Borland International, con la dirección enunciada anterior­ mente. C A PÍTU LO 4: LEN G U A JES BA SA D O S EN O BJETO S

Object Pascal Turbo Pascal 7 es una extensión orientada a objetos para Turbo Pascal y lo más cercano que usted puede obtener para una implementación de Object Pascal. Está disponible mediante Borland International. C++ Turbo C++ es una extensión orientada a objetos para Turbo C. Está disponible me­ diante Borland International.

Java Java es un C++ más sencillo, con algunas mejoras también. Se ejecuta en Windows NT/95 y UNIX. The Java Development Kit (JDK) está disponible para los estudian­ tes en forma gratuita de Sun por medio de FTP anónimo en ftp://java.sun.com/ pub/ o desde un sitio interm edio (M irror site), por ejem plo: ftp:// www.blackdown.org/pub/java/pub/ o ftp://sunsite.unc.edu/pub/languages/ java. Existe un índice general para otras fuentes de Java en http://java.sun.com/ about.html. C A PÍT U LO 5: PR O G R A M A C IÓ N D ISTR IBU ID A

Ada Las direcciones para los proveedores de implementaciones de Ada se proporciona­ ron anteriormente. Sólo fines educativos - FreeLibros

a p é n d ic e

C:

Fuentes de software

459

C-Linda C-Linda está disponible a través de: Scientific Computing Associates, Inc. 265 Church Street New Haven, CT 06510 (203) 777-7442

Occam Occam está disponible para su uso con tarjetas de expansión para PC llamadas transputers. The Transputer Education Kit incluye un transputer, un compilador Qccam y documentación para el transputer y los lenguajes C y Occam. Está dispo­ nible con: Computer System Architects 950 N. University Avenue Provo, Utah 84604 (801) 374-2300

Pascal S Para copias de Pascal S véase el Instructor's Manual. El código se encuentra listado en un apéndice en [Ben-Ari, 1982], o en la siguiente dirección: Professor Carol Torsone Computer Science Department St. John Fisher College Rochester, N Y 14618 [emailprotected]

CAPÍTULO 7: PROGRAMACIÓN LÓGICA PROLOG micro-PROLOG está disponible en: Logic Programming Associates, Ltd. 10 Bumtwood Cióse London SW18 3jU England 01-874-0350 En Estados Unidos, LPA PROLOG es distribuido por Programming Logic Systems, Inc., cuya dirección se muestra a continuación. Sólo fines educativos - FreeLibros

460

APÉNDICE C: Fuentes de software

Programming Logic Systems, Inc. 31 Crescent Drive Milford, CT 06460 (203) 877-7988 Una versión compilada de Edinburgh PROLOG se encuentra disponible en una versión educativa de: Arity Corporation 30 Domino Drive Concord, MA 01742 (508) 371-1243 o (800) 722-7489

CAPÍTULO 8: PROGRAMACIÓN FUNCIONAL (APLICATIVA) SCHEME Existen muchas versiones de LISP para microcomputadoras. Elegimos PC SCHEME Versión 3.03 debido a su facilidad de uso, cercanía al cálculo lambda y mínimo costo. Se encuentra disponible con: Richard Weyhrauch Ibuki P.O. Box 1627 Los Altos, CA 94022 [emailprotected] (415) 961-4996/(415) 961-8016 (FAX) PC-SCHEME se encuentra disponible también por medio de la red en el FTP anónimo de altdorf.ai.mit.edu, en /archive/pc-scheme. Una versión más reciente que PC SCHEME es EdScheme para DOS o 3DScheme y el WinScheme Editor para Windows 3.1 o versiones superiores. Se encuentra dis­ ponible con: Schemers Inc. 2136 N.E. 68th Street, Suite 401 Ft. Lauderdale, FL 33308 [emailprotected] (305) 776-7376/(305) 776-6174 (FAX)

CAPÍTULO 9: LENGUAJES PARA BASES DE DATOS SQL El Laboratorio 9 está escrito para su uso con dBASE IV. La implementación es una versión de funcionalidad limitada, disponible para propósitos de entrenamiento y demostración. Las ediciones para estudiantes de dBASE están disponibles con Borland en la dirección mencionada antes. Sólo fines educativos - FreeLibros

Referencias

Abelson, 1985

Abelson, H., Sussman, G. J. y Sussman, J. (1985). Structure and

interpretation o f Computer programs. Cambridge, MA: MIT Press.

Ada 9X, 1993 Introducing Ada 9X: Ada 9X project report. Cambridge, MA: Intermetrics. Adams, 1992 Adams, J. C., Brainerd, W. S., Martin, J. T., Smith, B. T. y Wagener, J. L. (1992). Fortran 90 handbook: Complete ANSI/ISO reference. Nueva York: McGraw-Hill. Agha, 1987 Agha, G. y Hewitt, C. (1988). Actors: A conceptual íoundation for concurrent object-oriented programming. En Research directions in object oriented programming, editado por B. Shriver y P. Wegner, pp. 49-74. Cambridge, MA: MIT Press. Aho, 1979 Aho, A. V. y Ullman, J. D. (1979). Principies ofCompiler Design. Reading, MA: Addison-Wesley. Aho, 1986 Aho, A. V., Sethi, R. y Ullman, J. D. (1986). Compilers: Principies, techniques, and tools. Reading, MA: Addison-Wesley. . Ai't-Kaci, 1983 Ai't-Kaci, H., Lincoln, P. y Nasr, R. (1983). Le Fun: Logic, equations, and functions. En Proceedings ofthe 1987 Symposium on Logic Programming, pp. 17-23. Washington, DC: IEEE Computer Society Press. Andrews, 1983 Andrews, G. R. y Schneider, F. B. (1983). Concepts and notations for concurrent programming. ACM computing suroeys 15(1): 3-43. ANSI/IEEE-770X3.97,1983 American national standard Pascal Computer programming language, Nueva York: IEEE. ANSI-1815A, 1983 Military standard: Ada®programming language. Washington, DC: American National Standards Institute. ANSI/ISO-8652,1995 American national standardfo r the Ada programming language. Washington, DC: American National Standards Institute. ANSI/ISO-9899, 1990 The annotated ANSI C standard, American National Stan­ dard for Programming Languages-C, ANSI/ISO 9899-1990 (anotaciones por H. Schildt). Berkeley, CA: Osbome McGraw-Hill. ANSI/ISO-9899, 1994 ISO/IEC Amendment 1 to C Standard 9899: 1990 (1994). Geneva: International Standards Org. (ISO). ANSI/ISO-X3J11, 1986 Draft proposed American national standard fo r Information systems-Programming language C. Washington, DC: American National Standards Institute. Sólo fines educativos - FreeLibros

462

Referencias

ANSI/ISO-X3.135,1992 Datábase language SQL. Washington, EXT: American Na­ tional Standards Institute, Inc. Así como International Organization for Standardization Document ISO/IEC 9075:1992. Appleby, 1988 Appleby, K., Carlsson, M., Haridi, S. y Sahlin, D. (1988). Garbage collection for Prolog based on WAM. CACM 31(6): 719-741. Atkinson, 1987 Atkinson, M. P. y Buneman, O. P. (1987). Types and persistence in database programming. ACM computing surveys 19(2): 105-190. Auer, 1989 Auer, K. (1989). Which object-oriented language should we choose? Hotline on object-oriented technology. Nueva York: SIGS Publications. Backus, 1978 Backus, J. (1978). Can program m ing be liberated from the vonNeumann style? CACM 21(8): 613-641. Bal, 1988 Bal, H. E. y Tanenbaum, A. S. (1988). Distributed programming with shared data. En Proceedings o f the 1988 International Conference on Computer Languages, pp. 82-91. Washington, DC.: IEEE Computer Society Press. Bal, 1989 Bal, H. E., Steiner, J. G. y Tanenbaum, A. S. (1989). Programming languages for distributed systems. ACM computing surveys 21(3): 261-322. Ball, 1989 Ball, M. S. (1989). Implementing múltiple inheritance. The C++ Report 1(9): 1-6. Bames, 1994 Bames, J. G. P. (1994). Programming in Ada. London: Addison-Wesley. Bames,1996 Bames, J. (1996). Programmingin Ada 95. Reading. MA: Addison-Wesley. Barón, 1986 Barón, N. (1986). Computer languages. Garden City, NY: Anchor Press/ Doubleday. Ben-Ari, 1982 Ben-Ari, M. (1982). Principies ofconcurrent programming. Englewood Cliffs, NJ: Prentice-Hall International. Ben-Ari, 1990 Ben-Ari, M. (1990). Principies o f concurrent and distributed programming. Englewood Cliffs, NJ: Prentice-Hall International. Blair, 1989 Blair, G.S., Gallagher, J. J. y Malik, J. (1989). Genericity vs delegation vs conformance vs... Journal o f object-oriented programming 2(3): 11-17 Bobrow, 1983 Bobrow, D. G. y Stefik, M. J. (1983). The LOOPS manual. Palo Alto, CA: Xerox. Booch, 1986 Booch, G. (1986). Software engineering with Ada. 2a. ed. Menlo Park, CA: Benjamin/Cummings. Booch, 1994 Booch, G. (1994). Object oriented design with applications. 2a. ed. Redwood City, CA: Benjamin/Cummings. Branquart, 1971 Branquart, P., Lewi J., Sintzoff, M. y Wodon, P. L. (1971). The composition of semantics in Algol 68. CACM 14(11): 697-707. Brender, 1981 Brender, R. F. y Nassi, I. R. (1981). What is Ada? En Ada: Programming in the 80's. Reimpreso por Computer, 14(6): 17-24. Brinch Hansen, 1975 Brinch Hansen, P. (1975). The programming language Concurrent Pascal. IEEE transactions on software engineering 1(6): 199-207. Brinch Hansen, 1978 Brinch Hansen, P. (1978). Distributed processes: Aconcurrent programming concept. CACM 21(11): 934-941. Bryan, 1990 Bryan, D. L. y Mendal, G. O. (1990). Exploring Ada. Vol. 1. Englewood Cliffs, NJ: Prentice-Hall. Buzzard, 1985 Buzzard, G. D. y Mudge, T. N. (1985). Object-based computing and the Ada programming language. Computer 18(3): 11-19. Así como Tutorial: Sólo fines educativos - FreeLibros

Referencias

463

Object oriented computing. Vol. 1, Concepis, editado por G. E. Peterson (1987), pp. 115-123. Washington, DC: IEEE Computer Society Press. Calingaert, 1988 Calingaert, P. (1988). Program translation fundamentáis: Methods and issues. Rockville, MD: Computer Science Press. Caromel, 1989 Caromel, D. (1989). Service, asynchrony, and wait-by-necessity. Journal o f object-oriented programming, 2(4): 12-22. Caromel, 1993 Caromel, D. (1993). Toward a method of object-oriented concurrent programming. CACM 36(9): 90-102. Carriero, 1989 Carriero, N. y Gelemter, D. (1989). Linda in context. CACM 32(4): 444-458. Catell, 1994 Catell, R. G. G. (1994). Object data management: Object-oriented and ex­ tended relational database systems. Edición revisada. Reading, MA: AddisonWesley Chamberland, 1995 Chamberland, L. (1995). Fortran 90: A reference guide. Upper Saddle River, NJ: Prentice-Hall. Chomsky, 1965 Chomsky, N. (1965). Aspects o f the theory o f syntax. Cambridge: MA: MIT Press. Chomsky, 1966 Chomsky, N. (1966). Cartesian linguistics. Nueva York: Harper and Row. Chomsky, 1988 Chomsky, N. (1988). The cidture o f terrorism. Boston: South End Press. Church, 1941 Church, A. (1941). The calculi o f lambda conversión. Princeton, NJ: Princeton Univ. Press. Clark, 1984 Clark, K. L. y McCabe, F. G. (1984). micro-Prolog: Programming in logic. Englewood Cliffs, NJ: Prentice-Hall. Clark, 1973 Clark, R. L. (1973). A linguistic contribution to goto-less programming. Datamation 19(12): 62-63. C-Linda, 1990 C-Linda® reference manual. New Haven, CT: Scientific Computing Associates. Clocksin, 1984 Clocksin, W. F. y Mellish, C. S. (1984). Programming in Prolog. 2a. ed. Berlin: Springer-Verlag. Cohén, 1991 Cohén, D. I. A. (1991). Introduction to Computer theory. Edición revisa­ da. Nueva York: Wiley. Cohén, 1985 Cohén, J. (1985). Describing Prolog by its interpretation and compilation. CACM 28(12): 1311-1324. Cohén, 1988 Cohén, J. (1988). A view of the origins and development of Prolog. CACM 31(1): 26-37. Colmerauer, 1985 Colmerauer, A. (1985). Prolog in 10 figures. CACM 28(12): 12961310. Cooper, 1983 Cooper, D. (1983). Standard Pascal user reference manual. Nueva York: Norton. Cooper, 1985 Cooper, D. y Clancy, M. (1985). OH! PASCAL. Nueva York: Norton. Cox, 1984 Cox, B. J. (1984). Message/object programming: An evolutionary change in programming technology. IEEE software, enero de 1984: 50-61. Así como Tutorial: Object oriented computing, Vol. 1, Concepts editado por G. E. Peterson (1987), pp. 150-161. Washington, DC: Computer Society Press. Sólo fines educativos - FreeLibros

464

Referencias

Dahl, 1966 Dahl, O. y Nygaard, J. (1966). SIMULA-An Algol based simulation language. CACM 9(9): 671-681. Date, 1993 Date, C. J. (1993). A guide to the SQL standard. 3a. ed. Reading, MA: Addison-Wesley. Date, 1995 Date, C. J. (1995). An introduction to database systems. 6a. ed. Reading, MA: Addison-Wesley. Dauben, 1979 Dauben, W. (1979). Georg Cantor. Cambridge, MA: Harvard University. Press. December, 1995 December, J. (1995). Presenting Java. Indianápolis, IN: Sams.net. DeGroot, 1984 DeGroot, D. (1984). Prolog and knowledge information processing: A tutorial. Documento inédito. IBM, T. J. Watson Research Center, Yorktown Heights, NY. Deitel, 1994 Deitel, H. M. y Deitel, P. J: (1994). C++: How to program. Englewood Cliffs, NJ: Prentice-Hall. Denning, 1988 Denning, P. J., Comer, D. E., Gries, D., Mulder, M. C., Tucker, A., Tumer, A. J. y Young, P. R. (1988). Report of the ACM task forcé on the core of Computer Science (Order #201880). Baltimore: ACM Order Dept. También re­ sumido en CACM 32(1): 9-23. Digitalk, 1986 Smalltalk/V: Tutorial and Programming Handbook. Los Angeles: Digitalk, Inc. Dijkstra, 1968a Dijkstra, E. W. (1968). Cooperating sequential processes. En Programming languages, editado por F. Genuys. Reimpreso por la Technological University, Eindhoven (1965), pp. 43-112. Nueva York: Academic Press. Dijkstra, 1968b Dijkstra, E. W. (1968). Go to statement considered harmful. CACM 11(3): 147-148. Duncan, 1990 Duncan, R. (1990). A survey of parallel Computer architectures. Computer 23(2): 5-16. Dybvig, 1987 Dybvig, K. R. (1987). The SCHEME programming language. Englewood Cliffs, NJ: Prentice-Hall. Eisenbach, 1987 Eisenbach, S. (ed.) (1987). Functional programming: languages, tools and architectures. Nueva York: Wiley. Ellis, 1990 Ellis, M. A. y Stroustrup, B. (1990). The annotated C++ reference manual. Reading, MA: Addison-Wesley Emery, 1986 Emery, G. (1986). BCPL y C. Oxford, UK: Blackwell Scientific Publications. Falkoff, 1976 Falkoff, A. (1976). Some implications of shared variables. En Formal languages and programming, editado por R. Aguilar. Amsterdam: North Holland. Feigenbaum, 1983 Feigenbaum, E. A. y McCorduck, P. (1983). The Fifth Generation: Artificial Intelligence and Japan's Computer Challenge to the World. Reading, MA: Addison-Wesley Feuer, 1982 Feuer, A. R. y Gehani, N. H. (1982). A comparison of the programming languages C and PASCAL. ACM computing surveys 14(1): 73-92. Feuer, 1989 Feuer, A. R. (1989). The C puzzle book. 2a. ed. Englewood Cliffs, NJ: Prentice-Hall. Flanagan, 1996 Flanagan, D. (1996). Java in a nutshell. Sebastapol, CA: O'Reilly. Friedman, 1987 Friedman, D. P. (1987). The little lisper. Cambridge, MA: MIT Press. Sólo fines educativos - FreeLibros

Referencias

465

Gabriel, 1989 Gabriel, R. R (ed.) (1989). Draft report on requirements for a common prototyping system. SIGPLAN notices 24(3): 93-165. Gallaire, 1984 Gallaire, H. y Minker, J. (1984). Logic and databases: A deductive approach. ACM computing surveys 16(2): 153-185. Gehani, 1986 Gehani, N. H. y Roome, W. D. (1986). Concurrent C. Software-practice and experience 16(9): 821-844. Gehani, 1994 Gehani, N. H. (1994). ADA: An advanced introduction. Summit, NJ: Silicon Press. Genesereth, 1985 Genesereth, M. R. y Ginsberg, M. L. (1985). Logic programming. CACM 28(9): 933-941. Ghezzi, 1987 Ghezzi, C. y Jazayeri, M. (1987). Programming language concepts. 2a. ed. Nueva York: Wiley. Glenn, 1959 Glenn, J. y James, R. C. (eds.) (1959). Mathematics dictionary. Princeton: Van Nostrand. Goguen, 1984 Goguen, J. A. y Meseguer, J. (1984). Equality, types, modules and (why not?) generics for logic programming. Journal oflogic programming 1(2): 179-210. Goldstein, 1989 Goldstein, T. (1989). Tutorial: Part I: Derivation. The C++ report 1(1): 4-6. Gordon, 1979 Gordon, R. (1979). The denotational description o f programming languages. Nueva York: Springer-Verlag. Gosling, 1996 Gosling, J. y McGilton, H. (1996). The Java® language environment: A white paper.http://java.sun.com/ Graham, 1980 Graham, S. L., Harrison, M. A. y Ruzzo, W. L. (1980). An improved context-free recognizer. ACM transactions on programming languages and systems 2(3): 415-462. Gries, 1971 Gries, D. (1971). Compiler constructionfor digital computers. Nueva York: Wiley. Gries, 1981 Gries, D. (1981). The Science o f programming. Nueva York: SpringerVerlag. Griffiths, 1965 Griffiths, T. V. y Petrick, S. R. (1965). On the relative efficiencies of context-free grammar recognizers. CACM 8(5): 289-300. Gruñe, 1977 Gruñe, D. (1977). A view of coroutines. Sigplan notices 12(7): 75-81. Guttag, 1977 Guttag, J. V: (1977). Abstract data types and the development of data structures. CACM 20(6): 396-404. Halmos, 1960 Halmos, P. R. (1960). Naive set theory. Nueva York: Van Nostrand. Harbison, 1995 Harbison, S. P. y Steele, G. L., Jr. (1995). C, a reference manual 4a. ed. Englewood Cliffs, NJ: Prentice-Hall. Harmon, 1985 Harmon, P. y King, D. (1985). Expert systems: AI in business. Nue­ va York: Wiley. Hayes-Roth, 1985 Hayes-Roth, F. (1985). Rule-based systems. CACM 28(9): 921932. Helmbold, 1965 Helmbold, D. y Luckham, D. (1985). Debugging Ada tasking programs. IEEE software 2(3): 47-57. Hoare, 1969 Hoare, C. A. R. (1969). An axiomatic basis for Computer programming. CACM 12(10): 576-583. También en Tutorial: Programming language design, edi­ Sólo fines educativos - FreeLibros

466

Referencias

tado por A. I. Wasserman (1980), pp. 500-505. Los Alamitos, CA: IEEE Computer Society Press. Hoare, 1972 Hoare, C. A. R. (1972). Proof of correctness of data representations. Acta informática 1(1): 271-281. Hoare, 1973 Hoare, C. A. R. (1973). Hints on programming language design. Re­ porte técnico número. CS-73-403. Computer Science Department, Stanford University. Stanford, CA. También en Programming languages: A grand tour. 3a. ed., editado por E. Horowitz (1987), pp. 31-40. Nueva York: Freeman. Hoare, 1985 Hoare, C. A. R. y Shepherdson, J. C. (eds.) (1985). Maíhematical logic and programming languages. Englewood Cliffs, NJ: Prentice-Hall International. Hodges, 1983 Hodges, A. (1983). Alan Turing: The enigma. Nueva York: Simón and Schuster. Hofstadter, 1985a Hofstadter, D. R. (1985). Lisp: Atoms and lists, Lists and recursion, and Recursión and generality. En Metamagical themas, pp. 396-424. Nueva York: Basic Books. Hofstadter, 1985b Hofstadter, D. R. (1985). Revisión de Alan Turing: The enigma. En Metamagical themas, pp. 483-491. Nueva York: Basic Books. Hopcroft, 1979 Hopcroft, J. E. y Ullman, J. D. (1979). Introduction to autómata theory, languages and computation. Reading, MA: Addison-Wesley. HOPL-II, 1993 The Second ACM S1GPLAN History o f Programming Languages Conference (1993, Cambridge, MA). Nueva York: ACM. Horowitz, 1984 Horowitz, E. (1984). Fundamentáis o f programming languages. 2a. ed. Rockville, MD: Computer Science Press. Horowitz, 1987 Horowitz, E. (ed.) (1987). Programming languages: A grand tour. 3a. ed. Nueva York: Freeman. Hudak, 1989 Hudak, P. (1989). Conceptiori, evolution, and application of furictional programming languages. ACM computing surveys 21(3): 359-411. Hughes, 1968 Hughes, G. E. y Cresswell, M. J. (1968). An introduction to modal logic. Londres: Methuen. Hull, 1987 Hull, R. y King, R. (1987). Semantic database modeling: Survey, applications, and research issues. ACM computing surveys 19(3): 201-260. Ichikawa, 1991 Ichikawa, T. (1991). Present situation and future prospects on AI utilization in Japan. Japan Information Processing Development Center (JIPDEC). IEEE-754,1985 Binaryfloating-point arithmetic, IEEE Standard 754. Nueva York: IEEE Press. ISO-DP7185, 1980 Second DP 7185. Specification fo r the Computer Programming Language Pascal, mayo de 1980. Geneva: ISO. Jackson, 1986 Jackson, P (1986). Introduction to expert systems. Reading, MA: Addison-Wesley. Jacobson, 1982 Jacobson, P. y Pullum, G. K. (eds.) (1982). The nature o f syntactic representation. Boston: Reidel. Jensen, 1974 Jensen, K. y Wirth, N: (1974). Pascal user manual and report. 2a. ed. Nueva York: Springer-Verlag. Johnson, 1988 Johnson, R. E. y Foote, B. (1988). Designing reusable classes. Journal o f object-oriented programming 1(2): 22-35. Sólo fines educativos - FreeLibros

Referencias

467

Johnsonbaugh, 1993 Johnsonbaugh, R. (1993). Discrete mathematics. 3a. ed. Nue­ va York: MacMillan. Jonsson, 1989 Jonsson, D. (1989). Next: The elimination of goto-patches? SIGPLAN notices 24(3): 85-92. Kamin, 1990 Kamin, Samuel N. (1990). Programming languages: an interpreter-based approach. Reading, MA: Addison-Wesley. Karaorman, 1993 Karaorman, M. y Bruno, J. (1993). Introducing concurrency to a sequential language. CACM 36(9): 103-116. Kemighan, 1978 Kemighan, B. W. y Ritchie, D. M. (1978). The C programming language. Englewood Cliffs, NJ: Prentice-Hall. Kerridge, 1987 Kerridge, J. (1987). Occam programming: A practical approach. Lon­ dres: Blackwell Scientific. Knuth, 1967 Knuth, D. E. (1967). The remaining troublespots in ALGOL 60. CACM. 10(10): 611-617. También en Programming languages: A grand tour. 3a. ed., edita­ do por E. Horowitz (1987), pp. 61-68. Nueva York: Freeman. Knuth, 1981 Knuth, D. E. (1981). The art o f Computer programming. 2a. ed. Vol. 2, Seminumerical algorithms. Reading, MA: Addison-Wesley. Kowalski, 1985 Kowalski, R. A. (1985). The relation between logic programming and logic specification. En Mathematical logic and programming languages, edita­ do por C. A. R. Hoare y J. C. Shepherdson (1985), pp. 11-27. Englewood Cliffs, NJ: Prentice-Hall International. Kowalski, 1988 Kowalski, R. A. (1988). The early years of logic programming. CACM 31(1): 38-43. Krasner, 1983 Krasner, G. (1983). SMALLTALK-80: Bits ofhistory, words ofadvice. Reading, MA: Addison-Wesley. Kristensen, 1987 Kristensen, B. B., Madsen, O. L., Moller-Pedersen, B. y Nygaard, K. (1987). The BETA programming language. En Research directions in objectoriented programming, editado por B. Shriver y P. Wegner (1987), pp. 8-48. Cambridge, MA: MIT Press. Kuhn, 1962 Kuhn, T. S. (1962). The structure o f scientific revolutions. Chicago: University of Chicago Press. Kuhn, 1970 Kuhn, T. S. (1970). The structure o f scientific revolutions. 2a. ed., aumen­ tada. Chicago: University of Chicago Press. Leler, 1990 Leler, W. (1990). Linda meets Unix. Computer 23(2): 43-54. Lesk, 1975 Lesk, M. E. (1975). LEX-a lexical analyzer generator. CSTR 39. Murray Hill, NJ: Bell Labs. Lewis, 1981 Lewis, H. R. y Papadimitriou, C. H. (1981). Elements ofth e theory o f computation. Englewood Cliffs, NJ: Prentice-Hall. Liskov, 1975 Liskov, B. H. y Zilles, S. N. (1975). Specification techniques for data abstractions. IEEE transactions on software engineering 1(1): 7-19. Liskov, 1977 Liskov, B., Snyder, A., Atkinson, R. y Schaffert, C. (1977). Abstraction mechanisms in CLU. CACM 20(8): 564-576. También en Programming languages: A grand tour. 3a. ed., editado por E. Horowitz (1987), pp. 254-266. Nueva York: Freeman. Liskov, 1986 Liskov, B. y Guttag, J. (1986). Abstraction and specification in program development. Cambridge, MA: MIT Press. Sólo fines educativos - FreeLibros

468

Referencias

Louden, 1993 Louden, K. C. (1993). Programming languages: Principies and practice. Boston: PWS. Lucas, 1988 Lucas, R. (1988). DataBase applications using Prolog. Nueva York: Wiley. McCarthy, 1960 McCarthy, J. (1960). Recursive functions of symbolic expressions. CACM 4(3): 184-195. También en Programming languages: A grand tour. 3a. ed., editado por E. Horowitz (1987), pp. 203-214. Nueva York: Freeman. McCarthy, 1965 McCarthy, J. y Levin, J. (1965). LISP 1.5 programmers manual. Cambridge, MA: MIT Press. También en Programming languages: A grand tour. 3a. ed., editado por E. Horowitz (1987), pp. 215-239. Nueva York: Freeman. MacLennan, 1987 MacLennan, B. J. (1987). Programming languages: Design, evalu­ ation and implementation. 2a. ed. Nueva York: Holt, Rinehart y Winston. Madsen, 1987 Madsen, O. L. (1987). Block structure and object -oriented languages. En Research directions in object oriented programming, editado por B. Shriver y P. Wegner (1987), pp. 113-128. Cambridge, MA: MIT Press. Malpas, 1987 Malpas, J. (1987). PROLOG: A relational language and its applications. Englewood Cliffs, NJ: Prentice-Hall. Mandrioli, 1986 Mandrioli, D. y Ghezzi, C. (1986). Theoretical Computer science. Nueva York: Wiley. Mano, 1982 Mano, M. M. (1982). Computer system architecture. 2a. ed. Englewood Cliffs, NJ: Prentice-Hall. March, 1989 March, S. T. (ed.) (1989). ACM computing surveys 21(3). Edición espe­ cial sobre "Programming Language Pardigms". Marcotty, 1976 Marcotty, M., Ledgard, H. V. y Bochmann, G. V. (1976). Asampler of formal definitions. ACM computing surveys 8(2): 191-276. Markoff, 1992 Markoff, J. (1992). David Gelemter's Romance with Linda. Nueva York: New York Times, 01/19/92, sec. 3 p. 1 c. 2. Markov, 1954 Markov, A. A. (1954). The theory of algorithms. Trudy matematicheskogo instituía imeni V. A. Steklova. 42. (en Rusia); traducción al inglés, Jerusalem: Israel Program for Scientific Translations, 1961. Mendelson, 1979 Mendelson, E. (1979). Introduction to mathematical logic. Princeton, NJ: Van Nostrand. Meyer, 1988 Meyer, B. (1988). Eiffel: Hamessing múltiple inheritance. Journal o f object oriented programming 1(4): 48-51. Michaelson, 1989 Michaelson, G. (1989). An introduction tofunctional programming through lambda calculus. Wokingham, Reino Unido: Addison-Wesley. Milner, 1990 Milner, R., Tofte, M. y Harper, R. (1990). The Definition o f Standard ML. Cambridge, MA: MIT Press. Milner, 1991 Milner, R. y Tofte, M. (1991). Commentary on Standard ML. Cambridge, MA: MIT Press. Moon, 1986 Moon, D. (1986). Object-oriented programming with Flavors. ACM SIGPLAN notices 21(11): 1-16. Moskowitz, 1989 Moskowitz, R. (1989). Object oriented programming: The future is now. PC Times, octubre 2 de 1989, p. 3. Mueller, 1990 Mueller, R. A. y Page. R. L. (1990). Symbolic computing with Lisp and Prolog. Nueva York: Wiley. Nagel, 1958 Nagel, E. y Newman, J. R. (1958). Gódel's Proof. Nueva York: NYU Press. Sólo fines educativos - FreeLibros

Referencias

469

Naur, 1963 Naur, R (ed,) (1963). Report on the algorithmic language ALGOL 60. CACM 6(1): 1-17. También en Programming languages: A grand tour. 3a. ed., edi­ tado por E. Horowitz (1987), pp. 44-60. Nueva York: Freeman. Nygaard, 1981 Nygaard, K. y Dahl, O-J. (1981). The development of the Simula languages, and Transcript of presentation. En History o f programming languages, editado por R. Wexelblat (1981), pp. 439-491. Nueva York: Academic Press. Pamas, 1971 Pamas, D. L. (1971). Information distribution aspects of design methodology. En Proceedings ofthe 1971IFIP Congress, pp. 26-30. Amsterdam: North Holland. Pamas, 1972 Pamas, D. L. (1972). On the criteria to be used in decomposing systems into modules. CACM 15(12): 1053-1058. Pascoe, 1986 Pascoe, G. A. (1986). Elements of object-oriented programming. Byte, agosto de 1986. También en Tutorial: Object oriented computing. Vol. 1, Concepts editado por G. E. Peterson (1987), pp. 15-20. Washington, DC: Computer Society Press. Peckham, 1988 Peckham, J. y Maryanski, F. (1988). Semantic data models. ACM computing surveys 20(3): 153-189. Peterson, 1987 Peterson, G. E. (ed.) (1987). Tutorial: Object oriented computing. Vol. 1, Concepts. Washington, DC: Computer Society Press. Peyton Jones, 1987 Peyton Jones, S. L. (1987). The implementation o f functional programming languages. Hemel Hempstead, Hertfordshire, Reino Unido: Prentice-Hall International. Pittman, 1992 Pittman, T. y Peters, J. (1992). The art ofcompiler design: Theory and practice. Englewood Cliffs, NJ: Prentice-Hall. Plauger, 1996 Plauger, P. J. y Brodie, J. (1996). Standard C: A reference. Upper Saddle River, NJ: Prentice-Hall. Poe, 1984 Poe, M. D., Nasr, R. y Slinn, J. A. (1984). Kwic bibliography on Prolog and logic programming. Journal oflogic programming 1: 81-142. Post, 1943 Post, E. L. (1943). Formal reductions of the general combinational decisión problem. American journal o f mathematics 65:197-215. Pratt, 1975 Pratt, T. (1975). Programming languages: Design and implementation. Englewood Cliffs, NJ: Prentice-Hall. Pratt, 1995 Pratt, T. y Zelkowitz, M. V. (1995). Programming languages: Design and Implementation. 3a. ed. Englewood Cliffs, NJ: Prentice-Hall. ProcSLP, 1986 Proceedings ofthe 1986 Symposium on Logic Programming. Washing­ ton, DC: IEEE Computer Society Press. Randall, 1960 Randall, J. H., Jr. (1960). Aristotle. Nueva York: Columbia University Press. Raymond, 1993 Raymond, E. S. (ed.) (1993). The new hacker's dictionary. 2a. ed. Cambridge, MA: MIT Press. Rees, 1987 Rees, J. y Clinger, W. (eds.) (1987). Revised3 report on the algorithmic language Scheme. Artificial Intelligence Memo 848a. Cambridge, MA: MIT Ar­ tificial Intelligence Lab. Rentsch, 1982 Rentsch, T. (1982). Object-oriented programming. SIGPLAN notices 17(9): 51-57. También en Tutorial: Object oriented computing. Vol. 1, Concepts, edi­ tado por G. E. Peterson (1987), pp. 21-27. Washington, DC: Computer Society Press. Sólo fines educativos - FreeLibros

470

Referencias

Rich, 1991 Rich, E. (1991). Artificial intelligence. 2a. ed. Nueva York: McGraw-Hill. Richards, 1979 Richards, M. y Whitby-Stevens, C. (1979). BCPL— the language and its compiler. Cambridge, Reino Unido: Cambridge University Press. Ringwood, 1988 Ringwood, G. A. (1988). Parlog86 and the dining logirians. CACM 31(1): 10-25. Robinson, 1965 Robinson, J. A. (1965). A machine-oriented logic based on the resolution principie. /ACM 12(1). También en Automation o f reasoning. Vol. 1, Classical papers on computational logic, 1957-1966, editado por J. Siekmann y W. Graham (1983), pp. 397-415. Berlín: Springer-Verlag. Robinson, 1983 Robinson, J. A. (1983). Logic programming—past, present, and future. New generation computing 1:107-124. Rogers, 1967 Rogers, H., Jr. (1967). The theory o f recursive functions and effective computability. Nueva York: McGraw-Hill. Ross, 1923 Ross, D. (1923). Aristotle. Londres: Methuen. Royce, 1987 Royce, W. (1987). Managing the development of large software systems: Concepts and techniques. En Proceedings o f the Ninth International Conference on Software Engineering, pp. 328-338. Washington, DC: IEEE Computer Society Press. Rubin, 1987 Rubin, F. (1987). GOTO considered harmful. CACM 30(3): 195-196. Sammet, 1969 Sammet, J. (1969). Programming languages: History andfundamentáis. Englewood Cliffs, NJ: Prentice-Hall. Saunders, 1989 Saunders, J. H. (1989). A survey of object-oriented programming languages. Journal o f object-oriented programming 1(6): 5-13. Scholz, 1961 Scholz, H. (1961). Concise history o f logic (K. F. Leidecker, trad.). Nue­ va York: Philosophical Library. Sebesta, 1993 Sebesta, R. W. (1993). Concepts of programming languages. 2a. ed. Menlo Park, CA: Benjamín/Cummings. Sergot, 1986 Sergot, M. J., Sadri, R. A., Kowalski, F., Kriwaczek, P. H. y Cory, H. T. (1986). The British Nationality Act as a logic program. CACM 29(5): 370-386. Sethi, 1989 Sethi, R. (1989). Programming languages: Concepts and constructs. Reading, MA: Addison-Wesley. Shapiro, 1989 Shapiro, E. (1989). The family of concurrent logic programming languages. ACM computing surveys 21(3): 412-510. Shatz, 1989 Shatz, S. M. y Wang, J-P. (1989). Tutorial: Distributed software engineering. Washington, DC: IEEE Computer Society Press. Shopiro, 1989 Shopiro, J. E. (1989). An example of múltiple inheritance in C++: A model of the iostream library. SIGPLAN notices 24(12): 32-36. Shriver, 1987 Shriver, B. y Wegner, P. (eds.) (1987). Research directions in objectoriented programming. Cambridge, MA: MIT Press. Shumate, 1988 Shumate, K. y Kjell, N. (1988). A taxonomy of Ada packages. Ada letters 8(2): 55-76. Siklóssy, 1976 Siklóssy, L. (1976). Let's talk Lisp. Englewood Cliffs, NJ: PrenticeHall. Silvester, 1984 Silvester, P. (1984). The Unix system guidebook: An introductory guide fo r serious users. Nueva York: Springer-Verlag. Simonian, 1988 Simonian, R. y Crone, M. (1988). InnovAda: True object-oriented programming in Ada. Journal of object-oriented programming 1(4): 14-23. Sólo fines educativos - FreeLibros

Referencias

471

Slater, 1987 Slater, R. (1987). Portraits i n S i l i c o n . Cambridge, MA: MIT Press. Smedema, 1983 Smedema, C. H., Medema, P. y Boasson, M. (1983). The program­ ming languages Pascal, Modula, Chill, Ada. Englewood Cliffs, NJ: Prentice-Hall. Sosnowski, 1987 Sosnowski, R. A. (1987). Prolog dialects: A deja vu of BASICs. SIGPLAN notices 22(6): 39-48. Steele, 1978 Steele, G. L., Jr. y Sussman, G. J. (1978). The revised report on Scheme, a dialect of Lisp. Artificial Intelligence Memo 452. Cambridge, MA: MIT Artifi­ cial Intelligence Lab. Steele, 1984 Steele, G. L., Jr. (1984). Common LISP: The language. Burlington, MA: Digital Press. Steele, 1993 Steele, G. J., Jr. y Gabriel, R. P. (1993). The evolution of LISP. ACM History o f Programming Languages II, Cambridge, MA: (abril de 1993) SIGPLAN notices 3(28): 231-270. Stefik, 1986 Stefik, M. y Bobrow, D. G. (1986). Object-oriented programming: Themes and variations. Al magazine, invierno de 1986; 40-62. También en Tutorial: Object oriented computing, Vol. 1, Concepts, editado por G. E. Peterson (1987), pp. 182-204. Washington, DC: IEEE Computer Society Press. Stroustrup, 1986 Stroustrup, B. (1986). The C++ programming language. Reading, MA: Addison-Wesley. Stroustrup, 1994 Stroustrup, B. (1994). The design and evolution o f C++. Reading, MA: Addison-Wesley. Stroustrup, 1995 Stroustrup, B. (1995). The C++ programming language. 2a. ed. Reimpreso con correcciones. Reading, MA: Addison-Wesley. Sun, 1995 About fava. Mountain View, CA: Sun;Microsystems. Suppes, 1960 Suppes, P. (1960). Axiomatic set theory. Nueva York: Van Nostrand. Sussman, 1975 Sussman, G. J. y Steele, G. L., Jr., (1975). Scheme: An interpreter for extended lambda calculus. Artificial Intelligence Memo 349. Cambridge, MA: MIT Artificial Intelligence Lab. Tanenbaum, 1976 Tanenbaum, A. S. (1976): A tutorial on ALGOL 68. ACM com­ puting surveys 8(2): 155-190. También en Programming Languages: A grand tour. 3a. ed., editado por E. Horowitz (1987), pp. 69-104. Nueva York: Freeman. Tennent, 1976 Tennent, R. D. (1976). The denotational semantics of programming languages. CACM 19(8): 437-453. Tesler, 1985 Tesler, L. (1985). Object Pascal report. Structured language world 9(3): 10-14. TI, 1987 Revised SCHEME user's guide, tutorial, and reference manual. Austin, TX: Texas Instruments. Torsone, 1993 Torsone, C. (1993). Introducing parallel programming to a program­ ming language concepts course. En Proceedings ofthe Ninth Annual Eastern Small College Conference, editado por J. G. Meinke, pp. 66-70. Tremblay, 1985 Tremblay, J. y Sorenson, P. G. (1985). The theory and practice of compiler writing. Nueva York: McGraw-Hill. Tu, 1986 Tu, H-C y Perlis, A. J. (1986). FAC: A functional APL language. IEEE software 3(1): 36-45. Tukey, 1977 Tukey, J. W. (1977). Exploratory data analysis. Reading, MA: AddisonWesley. Sólo fines educativos - FreeLibros

472

Referencias

Turbo C++, 1992 Turbo C++ versión 3.0 user's guide. Scotts Valley, CA: Borland International. Turbo 7.0,1992 Turbo Pascal 7.0: Programmer's reference. Scotts Valley, CA: Borland International. Turbo 7.0,1993 Turbo Pascal versión 7.0 reference manual. Scotts Valley, CA: Borland International. Tumer, 1982 Tumer, D. A. (1982). Recursión equations as a programming language. En Functional programming and its applications, editado por J. Darlington, R Henderson y D. A. Tumer (1982), pp. 1-28. Cambridge, Reino Unido: Cambridge University Press. Ullman, 1988 Ullman, J. D. (1988). Principies ofdatabase and knowledge-base systems. Vol. 1. Rockville, MD: Computer Science Press. Vossen, 1991 Vossen, G. (1991). Data models, database languages, and database management systems. Reading, MA: Addison-Wesley. Warren, 1977 Warren, D. H. D., Pereira, L. M. y Pereira, F. (1977). PROLOG—The language and its implementation compared with LISP. SIGPLAN notices 12(8): 109-115. Warren, 1988 Warren, D. S. (1988). The Warren abstract machine. En SIGPLAN '88: Advanced implementations tutorial notes, pp. 1-18. Baltimore: ACM Press. Watson, 1987 Watson, S. E. (1987). Ada modules. Ada letters 7(4): 79-84. Wegner, 1976 Wegner, P. (1976). Programming languages—the first 25 years. IEEE transactions on computers, diciembre de 1976:1207-1225. También en Programming languages: A grand tour. 3a. ed., editado por E. Horowitz (1987), pp. 4-22. Nueva York: Freeman. Wegner, 1980 Wegner, P. (1980). Programming with Ada: An introduction by means of graduated examples. Englewood Cliffs, NJ: Prentice-Hall. Wegner, 1983 Wegner, P. y Smolka, S. A. (1983). Processes, tasks, and monitors: A comparative study of concurrent programming primitives. IEEE transactions on software engineering SE-9(4): 446-462. También en Programming languages: A grand tour. 3a. ed., editado por E. Horowitz (1987), pp. 360-376. Nueva York: Freeman. Wegner, 1987 Wegner, P. (1987). The object-oriented classification paradigm. En Research directions in object-oriented programming, editado por B. Shriver y P. Wegner, pp. 479-560. Cambridge, MA: MIT Press. Wegner, 1988 Wegner, P. (1988). Object-oriented concept hierarchies. Tutorial no­ tes: Object-oriented software engineering. International Conference on Computer Languages "88. Tutorial presentado en la IEEE International Conference on Computer Languages, Miami Beach, FL. octubre de 1988. Wegner, 1989 Wegner P. (editor huésped) (1989). Introduction to programming language paradigms (edición especial). ACM computing surveys 21(3): 253-258. Wegner, 1990 Wegner, P. (1990). Concepts and paradigms of object-oriented programming, OOPS messenger 1(1): 8-84. Weiner, 1988 Weiner, J. L. y Ramakrishnan, S. (1988). A piggy-back compiler for Prolog. En Proceedings o f the SIGPLAN '88 conference on programming language design and implementation, pp. 288-296. Baltimore: ACM Press. Wexelblat, 1981 Wexelblat, R. (ed.) (1981). History o f programming languages. Nue­ va York: Academic Press. Sólo fines educativos - FreeLibros

Referencias

473

Whitehead, 1910 Whitehead, A. N. y Russell, B. A. W. (1910-1913, la. ed.; 19231927, 2a. ed.). Principia mathematica. Vols. 1-3. Cambridge, Reino Unido: Cambridge University Press. Wiederhold, 1983 Wiederhold, G. (1983). Database design. 2a. ed. Nueva York: McGraw-Hill. Wikstrom, 1987 Wikstrom, Á. (1987). Functional programming using standard ML. Londres: Prentice-Hall. Wirth, 1971 Wirth, N. (1971). The programming language Pascal. Acta informática 1(1): 35-63. Wirth, 1985 Wirth, N. (1985). Turing award lecture: From programming language design to Computer construction. CACM 28(2): 160-164. Wolfe, 1981 Wolfe, M. I., Babich, W., Simpson, R., Tholl, R. y Weissman, L. (1981). The Ada language system. Computer 14(6): 37-45. Zilles, 1986 Zilles, B. y Guttag, J. (1986). Abstraction and specification in program development. Nueva York: McGraw-Hill.

Sólo fines educativos - FreeLibros

índice

abstracciones, 7,71-106 cálculo lambda y, 451,452, 453,454, 455 de control, 72,83-93 de datos, 72-82 de procedimiento, 72,93-106 ligadura y, 42 objetos y, 103 POO y, 166 variables y, 31,39 Véase también abstracciones de datos abstracciones de datos Ada y, 175 SCHEME y, 376 Véase también abstracciones ACM. Véase Association for Computing Machinery (ACM) Ada, 129-145 abstracciones y, 80-81 ALGOL y, 113,114 análisis sintáctico y, 304 apuntadores y, 38 arreglos y, 54, 81 bloques y, 46 cadenas de caracteres y, 56 caja, 137,139 como ejemplo de paradigma basado en objetos, 10 como ejemplo de paradigma estructurado en bloques, 6,

8 como extensión de lenguaje de tercera generación, 15 Concurrent C (C concurrente) y, 268 Departamento de Defensa (Department O f Defense, DOD) y, 8 ,1 2 ,1 6 , 26,129131

EBNF y, 307 encapsulamiento de datos y, 171 enteros y, 32 estancamiento cíclico en, 268 estándares y, 28 excepciones en, 92,141-142 facilidad genérica, 128,132, 140-141,181-183 fuentes de software y, 457458 gramáticas libres de contexto y, 304 identificadores y, 40 Java y, 222 ligadura y, 42 límites dinámicos y, 119 Linda y, 263 modularización y, 102 números reales y, 34 Object Pascal vs., 185-186 operadores y, 124 palabras clave en, 41 parámetros y, 96 Pascal y, 126,132,136 POO y, 168 pragma, 256 punto de reunión (rendezvous) en, 244,252256 registros y, 57 Simula y, 173 sintaxis de, 84 tipos de datos y, 3 2 ,5 1 ,5 2 tipos unión y, 61 unidades de proceso en, 236 variables de control de ciclos en, 46 verificación de tipo y, 64,67 Ada 83, 9 ,1 0 ,1 3 1 ,175-180,181182,199, 215

Sólo fines educativos - FreeLibros

Ada 95,10, 40,131,175-180 181-182,199, 215 Ada9X, 131 ADT. Véase tipos de datos abstractos (abstract data types, ADT) alcance, 42-45,68 Ada y, 133-134 ALGOL y, 115-116 dinámico, 46 excepciones y, 91 lexicográfico, 44,113 LISP y, 400-404 registros de activación y, 48 tiempo de vida vs., 48 alcance dinámico, 46-47 LISP y, 400 registros de activación y, 48 alcance estático, 4 5 ,4 7 alcance dinámico vs., 45 ALGOL y, 113 registros de activación y, 48 álgebra, 4,426-428,429 PROLOG y, 354 SQL y, 429,431 álgebra relacional, 426-428,429 SQL y, 429,431 ALGOL, 6 ,1 3 bloques y, 110 C y, 145 identificadores y, 40 sintaxis de, 84 verificación de tipos y, 65 ALGOL 58,111,112 ALGOL 60, 9 ,1 6 alcance y, 44 bloques y, 46,109,110-122 gramáticas libres de contexto y, 304 Pascal y, 125

476

índice

puntos problemáticos en, 119-120 Simula y, 173 ALGOL 6 8,9,2 2 ,1 2 2 -1 2 5 bloques y, 110,112-113,114 cláusulas colaterales en, 236 límites dinámicos y, 119 ortogonalidad y, 25 registros y, 57 semáforos en, 240,247-248 tipos unión y, 58 ALGOL-W, 125 algoritmo abstracciones y, 79,85 ALGOL y, 113 de sonido, 369 P O O bs .,1 6 7 programación funcional y, 369 algoritmo no selectivo descendente (Nonselective Top-to-Bottom Algorithm, NTB), 300, 301-302 algoritmo NTB. Véase algoritmo no selectivo descendente (Nonselective Top-to-Bottom Algorithm, NTB) almacenamiento, 5 abstracciones y, 73 paradigmas imperativos y, 9 almacenamiento dinámico, 3738 alternación diagrama de transición y, 294 expresiones regulares y, 292 procesos de cooperación y, 239 ambiente, 44 alcance dinámico y, 46-47 alcance estático o lexical y, 45 ALGOL y, 113,117 cálculo lambda y, 455 iteración y, 88 LISP y, 375 recursión y, 88-89 registros de activación y, 48 ambiente de desarrollo integrado (Integrated Development Environment, IDE), 16 ambiente de soporte para programación en Ada (Ada programming support environment, APSE), 142-143

análisis lexicográfico, 23,281, 295 análisis sintáctico, 22,303 derivación, 276 PDA y, 281 PROLOG y, 343 RTN y, 312 anfitrión, 22 anillo, 74 animación, 223 ANSI. Véase Instituto Nacional Americano de Estándares (American National Standards Institute, ANSI) API. Véase interfaces de programación de aplicaciones (application programming interfaces, API) APL, 13,29 alcance y, 47 arreglos y, 55 ligadura y, 42 límites dinámicos y, 119 tipos de datos y, 52 aplicaciones en tiempo real, interrupción, 21 applets, 2 17,218,223, 224-225 APSE Mínimo (Minimum APSE, MAPSE), 145 APSE. Véase ambiente de soporte para programación en Ada (Ada programming support environment, APSE) apuntadores, 36-38 alcance dinámico y, 46 C y, 157,159 cadenas de caracteres y, 56 Java y, 219 LISP y, 397 objetos y, 183-189 parámetros y, 101 POO y, 197,200,206-213 registros y, 58 árbol binario, 421 árbol de análisis sintáctico, 303311 árbol de sintaxis, 19,20 archivos bases de datos y, 421 LISP y, 376 argumentación, 340,425 argumentos, 364,367,376

evaluación perezosa vs. evaluación estricta de, 398399,405-406 función preparada y, 412 aritmética de enteros, 286 arquitecturas paralelas, 352353,364 arreglos dinámicos, ALGOL y, 118-119 arreglos, 54-56 abstracciones y, 81 Ada y, 138-140 ALGOL y, 118-119 APL y, 407 C y, 157-158 C-Linda y, 266 conformante, 162 corte, 137 implementación de, 68 Java y, 219 límites dinámicos, 119 límites flexibles, 119 LISP y, 371, 374 no restringidos, 55, 64,138 orden mayor de columna, 55 orden mayor de renglón, 55 PROLOG y, 341 registros, 57-58 tipos agregados y, 5 2 ,5 4 unidades de proceso y, 237 verificación de tipos y, 63-64 ASCII. Véase Código Estándar Americano para Intercambio de Información (American Standard Code for Information Interchange, ASCII) asignación de responsabilidad, 93 ,9 4 ,1 0 1 asignaciones procesamiento paralelo y, 236 registros y, 58 tipos de datos subrango y, 51 tipos unión y, 59 verificación de tipos y, 64 asignaciones globales, 11 asociatividad árbol de análisis sintáctico y, 303 PROLOG y, 338 Association for Computing Machinery (ACM), 111, 112

Sólo fines educativos - FreeLibros

índice ATN. Véase red de transición aumentada (augmented transition network, ATN) átomos, 371-373,376 bases de datos relaciónales y, 428 en PROLOG, 339-340 literales, 371,397 SCOOP y, 392 atributos, 10,136 clases y, 179-180 ligadura de, 41-42. Véase también ligadura modelos semánticos y, 336 objetos y, 165,166 subclases y, 179 variables y, 3 1,39 autómata descendente (PDA), 281, 298-303 árbol de análisis sintáctico y, 302 LBA vs., 283 autómata finito determinístico (deterministic finite automaton, DFA), 294298 autómata finito no determinístico (nondeterministic finite automaton, NFA), 294-297, 311 autómata limitado lineal (linear-bounded automaton, LBA), 283-285 autómatas de estado finito (fínite-state autómata, FSA). Véase autómatas finitos (finite autómata, FA) autómatas finitos (finite autómata, FA), 281,292298 gramáticas libres de contexto y, 298-299 PDA y, 299 RTN y, 311 axiomas, 322,443 axiomas de Peano, 322 "azúcar sintáctica", 83 bases de datos, 421-439 consulta, 325-337,421. Véase también lenguajes de consulta definidas, 421 ejemplos y, 425-428

fuentes de software y, 460 modelo relacional para, 425434. Véase también búsqueda en bases de datos relaciónales, 325-337 modelos semánticos y, 390392 PROLOG y, 434 bases de datos estándar, 433 bases de datos relaciónales lógica y, 434 manipulación, 425-429 PROLOG y, 338,355 BASIC gramáticas libres de contexto y, 304 ligadura de tipo y, 42 sistema de tiempo compartido, 244 UNIX y, 158 Basic CPL (BCPL), 146-147,157 basura, 38-39 BCD. Véase decimal codificado en binario (binary coded decimal, BCD) bits, 7 C y, 153-157 conjuntos y, 62 entero y, 32 valores booleanos y, 34 bloques, 43, 45-46, 68,109-162 Ada y, 129-145 ALGOL 60 y, 109,110-122 ALGOL 68 y, 122-125 C y, 145-160 cadenas de caracteres y, 56 excepciones y, 91,92 Pascal y, 124-129 programación funcional y, 364 PROLOG y, 357 punto de reunión (rendezvous) y, 244 registros de activación y, 48 registros y, 58 BNF. Véase forma Backus-Naur (BNF) búferes, 246 Ada y, 253-257 Concurrent Pascal y, 251252 búsqueda, 329-336 buzón de correo, 245 bytes caracteres y, 33 valores booleanos y, 34

Sólo fines educativos - FreeLibros

477

C, 6 ,8 ,1 5 ,1 4 5 -1 6 1 abstracciones y, 95 ALGOL y, 113,145 bloques y, 46 cadenas de caracteres y, 55 caracteres y, 34 confiabilidad y, 21 enteros y, 32 GemStone y, 438 identificadores y, 40 Java y, 218,219,225-227 ligadura y, 42 Linda y, 263 objetos y, 267 operadores en, 151-158 Pascal y, 145-149 PROLOG y, 338, 356 semáforos en, 248-249 Simula y, 172 tipos de datos y, 53,148-151 tipos unión y, 58 UNIX y, 147,158 verificación de tipos y, 67 C++, 10,145 abstracciones y, 81,95 clases en, 181,189-192,196, 198 Concurrent C y, 267 GemStone y, 438 herencia en, 209-214 herencia múltiple y, 202-204, 212-213 Java y, 219,220,225-228 ligadura dinámica en, 216 objetos y, 266 operadores y, 125 POO y, 168,169-170,177-179, 197 PROLOG y, 338 Simula y, 173 cadena de caracteres potencialmente infinita, 277 cadenas de caracteres, 56-57 clases y, 179 lenguajes formales y, 275-276 LISP y, 389 Pascal y, 127-129 potencialmente infinitas, 277 PROLOG y, 341 tipificación fuerte, 64-67 tipos agregados y, 53 ,5 4 caída, procesamiento paralelo y, 267

478

índice

calculador de arreglo funcional (Functional Array Calculator, FAC), 407 cálculo de dominio, 428,429 cálculo de predicados de primer orden, 448-450 cálculo de predicados, 11,19, 322, 441, 446-450 bases de datos relaciónales y, 428 programación funcional y, 364 PROLOG y, 338 tupias y, 428 unificación y, 328-329 cálculo de tupias, 428-429 cálculo lambda, 5,451-456 alcance y ligaduras y, 399400 Backus y, 407 formas funcionales y, 378-379 programación funcional y, 364, 365, 368, 369, 388 SASL, KRC, H askelly Miranda y, 417 cálculo proposicional, 441-446, 447 cálculo relacional, 10, 428-429 SQL y, 429, 430 cálculos lógicos, 441-450 campos, 57-61 canales de comunicación, 245, 258-263 caracteres, 34 abstracciones y, 81 imprimible (alfanumérico), 34 cargador, 23 Cartesian Linguistics, 280 CFG. Véase gramáticas libres de contexto (context-free grammars, CFG) ciclos (bucles) abstracciones y, 87-88 bloques y, 46 tabla de transición y, 294 clase base, 173,185-186,200 clase virtual pura, 190 clases, 9,179-194 abstracciones y, 77 abstractas, 185-186 de procesos, 174 Java y, 220-223 objetos y, 103,165,166,170, 172 SCOOP y, 391-392

Simula y, 174 tipos de datos abstractos y, 102-103 Véase también herencia cláusula base, 329 cláusulas colaterales, 236-237 cláusulas de Horn, 11, 324- 327 claves externas, 424,432 claves primarias, 424,432 claves, bases de datos y, 424 clientes, 245 C-Línda, 264-267,459 CLU, 29,170 CNF. Véase forma normal de Chomsky co-rutina, 236 COBOL, 12,15, 73 bases de datos y, 421,423, 431 extensibilidad y, 26 identificadores y, 40 números reales y, 34 registros y, 57 cociente, bases de datos relaciónales y, 427-428 CODASYL. Véase Conferencia sobre Lenguajes de Sistemas de Datos (Conference on Data Systems Languages, CODASYL) código binario, 23 código de intercambio decimal codificado en binario extendido (Extended Binary Coded Decimal Interchange Code, EBCDIC), 34 código de máquina, 23 comprobabilidad y, 21 Java y, 219 ligadura y, 41 variables y, 31 código ejecutable, detección y recuperación de errores, y, 24 código Enigma, 284, 287 Código Estándar Americano para Intercambio de Información (American Standard Code for Information Interchange, ASCII), 34 código fuente, 23 DFA y, 295 ligadura y, 41

memoria y, 24 ramificación y, 84-86 y código objeto eficiente, 24 código intermedio, 22 código objeto, 22,23 código-P, 249 código reubicable, 22, 41 coerción de tipo, 64 coerción, C y, 159 coincidencia de patrones, 57, 407 colas, 9 abstracciones y, 78-79 apuntadores y, 37 arreglos y, 55 máquina virtual y, 274 monitores y, 243, 244 paso de mensajes y, 246 sincronización y, 245 colector de basura, 38-39 Ada y, 140 LISP y, 369, 370, 398,404-405 PROLOG y, 353 tabla de dispersión (cálculo de direcciones) de objetos y 398 Common LISP, 370, 371, 394396 alcance y, 47 estándares y, 27 recursión y, 91, 382 comparaciones, tipos enumeración y, 53 compatibilidad de tipo, 63 compiladores, 4, 6 ,2 3 abstracciones y, 74 ALGOL y, 113 análisis lexicográfico de, 281 análisis sintáctico y, 303 C para escritura, 159 comprobabilidad y, 22 ejecución concurrente y, 104 enteros BCD y, 32 escritura/diseño, 24, 68 evaluación perezosa vs. evaluación estricta y, 405407 extensibilidad y, 27 gramáticas libres de contexto y 307 IDE y, 15 Java y, 219 lenguajes de muy alto nivel y 14, 24 optimización, 24 PDA y, 281 '

Sólo fines educativos - FreeLibros

índice procesamiento paralelo y, 352-353 programación funcional y, 396,405 recursión y, 89,90 semántica y, 20 subconjuntos y, 26 transportabilidad y, 26-27 comprobabilidad, 20-21 computabilidad, cálculo lambda y, 454-456 concatenación, 57 diagrama de transición y, 294 Concurrent C Ada y, 258 objetos y, 266 paso de mensajes en, 245 punto de reunión (rendezvous) en, 256-258 Concurrent Pascal, 236,244, 251-252 Concurrent PROLOG, 9,352 Concurrent Smalltalk, 267 condicionales, 407 condiciones ON, 91-92 Conferencia sobre Lenguajes de Sistemas de Datos (CODASYL), 423 confiabilidad, 21-22 Ada y, 131,175 monitores y, 244 conjunto de potencia, 62 análisis sintáctico y, 302 precedencia, 123 PROLOG y, 339 conjuntos, 61-62 clases como, 179-180 PROLOG y, 343 conocimiento privado, 370 conocimiento público, 370 cons, 374-378 consistencia, 25, 65 especificación algebraica y, 79 teoría y, 322 constantes, 43 cálculo lambda y, 455 PROLOG y, 339 constructores, 165,371 consulta, 326-337 contenedor de datos, 168 conteo de referencia, 405 continuación (de excepciones), 91 contradicción, 322,444-446

control en LISP y SCHEME, 337-338, 381-382 en PROLOG, 345-349 funciones y, 264 conversión alfa, 4 5 2 ,454,456 conversión beta, 452,453,454 conversión eta, 452,454 conversión, C y, 150-151 conversiones de tipo, C y, 150151 copiado, 405 CPL-BCPL-C, bloques y, 110, 146 CPL. Véase Lenguaje de Programación Combinada (Combined Programming Language, CPL) CPU abstracciones y, 74 estado y, 9 Java y, 219 paradigmas declarativos y, 10 procesamiento paralelo y, 234,235 programación concurrente y, 10

criterio de lenguajes, 16-29 CSA Transputer Education Kit, 260,261 CSG. Véase gramáticas sensibles al contexto (context-sensitive grammars, CSG) CSP. Véase procesos secuenciales de comunicación (communicating sequential processes, CSP) cursor, bases de datos y, 431432 CWL. Véase Lenguaje de Palabra de Código (Code Word Language, CWL) datos, almacenamiento y, 73 DBASE IV, 460 DBMS. Véase sistema de administración de bases de datos (database management system, DBMS) DBTG. Véase grupo de tarea de bases de datos (Data Base Task Group, DBTG)

Sólo fines educativos - FreeLibros

479

DDL. Véase lenguaje de definición de datos (data definition language, DDL) decimal codificado en binario (BCD), 32, 33 declaraciones Ada y, 129 ALGOL y, 114 bloques y, 46 C-Linda y, 265 cálculos lógicos y, 441-450 case, 84 ciclo while, 87 excepciones y, 91 for, 87 if, 84-85 iteraciones y, 88 ligadura de nombre y, 41 ligadura de tipo y, 42 Pascal y, 129 PROLOG y, 338-339 registros de activación y, 48 declaraciones anidadas, ALGOL y, 116 definiciones de lenguaje, 17,19, 29 comprobabilidad y, 21 semántica y, 20 Departamento de Defensa, (Department of Defense, DOD), Ada y, 8 ,1 2 ,1 5 , 26,129-132 Ada 95 y, 175 APSE y, 142-146 Common LISP y, 395 lenguaje, 8 ,1 2 ,1 5 ,2 6 Pascal y, 127 depuración C y, 159 confiabilidad y, 22 datos tipo subrango y, 52 Java y, 224 modularización y, 102 procesamiento paralelo y, 267 PROLOG y, 341 traducción y, 24 derreferenciación, 37 desbordamiento aritmético, 22 descripciones de lenguajes, 1518 descriptores del arreglo, 5 5 ,5 6 despachar, 193,214 despacho dinámico, 214 destructores, 165

480

índice

detección de errores y recuperación, traducción y, 24 diagrama de contorno, 45 diagrama de transición, 293294 diagramas de ferrocarril, 18 diagramas de sintaxis, 18, 309310 dialectos LISP, 380, 394-395. Véase también Common LISP; SCHEME diferencia de conjuntos, bases de datos relaciónales y, 426,428 difusión, 247 direcciones, 37,39 discriminantes, 59-60,84-86 diseño de lenguajes, 29 DML. Véase lenguaje de manipulación de datos (data manipulation language, DML) dobleces, 261 documentos HTML, Java y, 223 DOD. Véase Departamento de Defensa (Department of Defense, DOD) dominio de datos (D), 73, 75 DSL. Véase lenguajes de sistemas de datos (data system languages, DSL) EBCDIC. Véase código de intercambio de decimales codificados en binarios extendidos (Extended Binary Coded Decimal Interchange Code, EBCDIC) EBNF. Véase forma BackusNaur extendida (Extended Backus-Naur Form, EBNF) Editor WinScheme, 460 editor de doblez Origami, 261 editores, ejecución concurrente y, 104 EdScheme, 460 efecto de "yo-yo", 206 efectos colaterales abstracciones y, 95,98 ALGOL y, 120 C y, 159 cálculo lambda y, 454 en LISP y SCHEME, 382-388 funciones y, 364, 367

paradigmas funcionales y, 11 ejecución concurrente, 104,219 ejecución en paralelo, PROLOG y, 357 ejemplares de lenguajes, 205213. Véase también ejemplares ejemplares, 6 ,9 . Véase también ejemplares de lenguaje ejemplo de la cena de los filósofos, 239-243, 250-251, 258-259 ejemplos, 425-428, 442, 446 elegancia, 124, 364, 366 elevación (de excepciones), 91 else colgantes, 85 encadenamiento hacia adelante, 332-333 encadenamiento hacia atrás, 332-333 encapsulamiento objetos y, 166,169-172 unidad de proceso y, 237 Véase también encapsulamiento de datos encapsulamiento de datos, 75, 102 objetos y, 103 Véase también encapsulamiento enteros, 32-33 abstracciones y, 73-74,81 caracteres y, 34 LISP y, 371-373 PROLOG y, 339, 345 tipos enumeración y, 53 tipos unión y, 59 verificación de tipos y, 64 entidades, 421,436 abstractas, 336 de primera clase, 183-190 imprimibles, 435 representables, 436 entrada/salida, E/S (input/ output, l/O ) ALGOL y, 112 C y, 159 herencia múltiple y, 204 programación funcional y, 364 PROLOG y, 345 equijoin, 427,431, 434 equivalencia de declaración, 64 equivalencia de nombre, 63-64 equivalencia de tipo, 63 equivalencia estructural, 63-64

errores de sintaxis, intérpretes, 23 errores, lenguaje C y, 67 E/S. Véase entrada/salida (input/output, I/O) ESP. Véase PROLOG extendido autocontenido (Extended Self-Contained PROLOG, ESP) espacio de tupias, 262-266 especificación, 132-133 especificación algebraica, 76, 78-79,106 especificación de lenguaje, 120121

especificación de programa, 13 espera productiva, 239 esquema, 430-431,436-438 esquema de longitud variable con máximo fijo, 56 estado, 9 -1 0 ,1 0 3 ,1 6 5 ,1 6 6 autómatas finitos y, 293 estado de inicio, 293 estado final, 293 estado terminal/de terminación, 293,295 estancamiento cíclico, 267 estancamiento o punto muerto, 240, 243 ALGOL 68 y, 248 circular, 267 estándares, 26-27. Véase también transportabilidad estándares de lenguaje, abstracciones y, 74 estructura de palabras, caracteres y, 33 estructura lexicográfica (del lenguaje), 274 estructuras en ML, 411-412 en PROLOG, 339 estructuras de bloque anidadas, 7 ,9 ,1 0 9 Ada y, 133 ALGOL y, 116 Pascal y, 127 registros de activación y, 48 estructuras de control, 29, 73 estructuras de datos abstracciones y, 73, 83-93 C-Linda y, 266 LISP y, 375-376 PROLOG y, 341 etiquetas, 59-60,193 etiquetas dinámicas, 193

Sólo fines educativos - FreeLibros

índice evaluación perezosa, 399-400, 405-407 excepciones, 2 1 ,2 2,89-93,141142,413 modelo de reanudación, 89, 91 modelo de terminación, 89 excepciones definidas por el usuario, 92 exclusión central, principio de, 325 exclusión mutua, 239 exploración o rastreo, 23 exploradores (escáneres), 281, 295 expresión condicional, 377-378 expresión lambda, 375-376 expresión S (sexpr), 32,373-378, 380 macros como, 389 expresión simbólica, 32 expresiones cálculo lambda y, 455 excepciones y, 91 funciones y, 363 regular, 291-292 expresiones definidas por el usuario, 455 expresiones regulares, 291- 292 expresiones seguras, 429 extensibilidad, 26 extensión de tipo, 193 FA. Véase autómatas finitos (finite autómata, FA) FAC. Véase calculador de arreglo funcional (Functional Array Calculator, FAC) facilidades virtuales (de Object Pascal), 185-186 falla parcial, 268 FDM. Véase modelos de datos funcionales (functional data models, FDM) filosofía lógica y, 337 Turing y, 287 firmas, 412-413 fonología, 280 foobar, 344 forma Backus-Naur (BackusNaur Form, BNF), 16-18, 21, 307-308 ALGOL y, 118,123 ALGOL 60 y, 112,118,121

árbol de análisis sintáctico y, 309 gramáticas libres de contexto, 307 lenguaje de referencia y, 121 forma Backus-Naur extendida (Extended Backus-Naur Form, EBNF), 18-19, 307311 ML y, 413 forma normal de Backus. Véase forma Backus-Naur (BNF) forma normal de Chomsky (Chomsky Normal Form, CNF), 307,308-309 formas funcionales, 378-379 formas normales, 306-311,454, 455 formas, en LISP, 375-376 fórmulas, 447,452-453 FORTRAN, 8,14,15,40 ALGOL y, 111,112,115,118 analizador DFA para, 295 arreglos y, 53 Backus y, 408 bases de datos y, 421 gramáticas libres de contexto y, 304 identificadores y, 40 ligadura y, 42 LISP y, 369 palabras reservadas y, 41 parámetros y, 99 registros de activadón y, 48 verificadón de tipos y, 64 FORTRAN 90, tipos agregados y, 54 FORTRAN II, tipos agregados y, 53 FP, 408 Franz LISP, 393 FSA (autómatas de estado finito, finite-state autómata). Véase autómatas finitos (finite autómata, FA) FTP. Véase protocolo de transferencia de archivos (File Transfer Protocol, FTP) fuentes de software, 457-460 función de automodificación, 385-388 función de orden superior, 366, 412 función de primer orden, 367

Sólo fines educativos - FreeLibros

481

función miembro, 168 función preparada, 412 funcionadores, 340-345 ML y, 412-413 fundones, 363-364 abstracciones y, 95-96 cálculo lambda y, 451,454. Véase también cálculo lambda definidas, 363 paradigmas declarativos y,

10,11 Pascal y, 128 predefinidas, 41 semántica y, 20 tipos vs., 24-25 variables y, 31 funciones compatibles Turing, 454 funciones de primera dase, problemas funarg y, 400404 funciones definidas por el usuario, 371 funciones parcialmente aplicables, 411 funciones predefinidas, 4 1 ,5 7 generalidad, 25 generalizadón uniforme (Uniform Generalization), 448 geometría, 71-72 Gopher, Java y, 226 gráficas, 14-15 gramática restringida estructurada en uase, 282 gramática-vW, 22,122-123 gramática-W. Véase gramáticavW gramáticas, 273-283 ambiguas, 302-304 comprobabilidad y, 21 definidas, 274 estructurada en frases, véase gramáticas estructuradas en frases no restringidas, 288-290 para lenguajes naturales, 282, 311-315 sintaxis y, 275 gramáticas ambiguas, 302-304 gramáticas estructuradas en frases, 276,279-298 restringida, 282

482

índice

gramáticas libres de contexto (CFG), 281, 282, 298-311 formas normales y, 306-311 RTN y, 311-312 gramáticas no restringidas, 288-290 gramáticas recursivamente enumerables, 287-290 gramáticas regulares, 280-281, 290-298 CDW y, 289 gramática libre de contexto, 281 tablas de transición y, 294 gramáticas sensibles al contexto (context-sensitive grammars, CSG), 282-283, 311 gramática vW como una, 122 reconocedor, 283 gramáticas sobre X, 276-277 granularidad, 406-407 Grupo de Tarea de Bases de Datos (Data Base Task Group, DBTG), 423 grupo de trabajo de lenguaje de orden superior (HigherOrder Language Working Group, HOLWG), 130 hasA, 198 Haskell (lenguaje), 417 hechos, 331-332 negativos, 333-334 PROLOG y, 338, 339-340 representación negativa, 334335 Java y, 219,223 herencia Ada 95 y, 192 múltiple. Véase herencia múltiple Object Pascal y, 185-189 objetos y, 103 POO y, 166,172,196-217 Simula y, 174-175 herencia múltiple, 200-205,212213 heurística, 370 hipótesis, 322 HOLWG. Véase grupo de trabajo de lenguaje de orden superior (HigherOrder Language Working Group, HOLWG) Hope (lenguaje), 398

Hotjava, 217, 223, 224-225 HTTP. Véase protocolo de transferencia de hipertexto (HyperText Transfer Protocol, HTTP) IA. Véase inteligencia artificial IAL. Véase Lenguaje Algebraico Internacional (International Algebraic Language, IAL) IBM, 24,34 ALGOL y, 111 bases de datos relaciónales y, 428 FORTRAN y, 111,112 IMS de, 422 SQL y, 429 IBM 370,32 IBM 704, 364, 369, 373 IBM PROLOG, 353 IDE. Véase ambiente de desarrollo integrado (Integrated Development Environment, IDE) identidad de objeto, 438 identificadores, 18,40 Ada y, 252 ALGOL y, 120 campos y, 57 ligadura y, 41-42 tipos enumeración y, 53 tipos unión y, 59 IEEE. Véase Instituto de Ingenieros Eléctricos y Electrónicos (Institute of Electrical and Electronics Engineers, IEEE) implementación, 396-405 bases de datos y, 430 Common LISP y, 395 de clases heredadas, 192-194 monitores y, 244 PROLOG y, 349-355 implementación del lenguaje, 7 ,2 9 abstracciones y, 64,79-80 inanición, 239 incrustaciones en tiempo real, 16 independencia de datos, 75-76, 102. Véase también ortogonalidad independencia de la plataforma, Java e, 222, 226

indeterminismo, 352 índices arreglos e, 5 4 ,5 5 bases de datos e, 421-422, 432 verificación de tipos e, 64 individuos, como argumentos, 366 ingeniería de software, PROLOG e, 338 ingeniería del conocimiento, LISP y, 370 Institución de Estándares Británicos (British Standards Institution, BSI), 26 Instituto de Ingenieros Eléctricos y Electrónicos (Institute of Electrical and Electronics Engineers, IEEE), 2 6 ,3 4 SCHEME y, 370 Instituto Nacional Americano de Estándares (American National Standards Institute, ANSI), 26 instrucción goto, 21, 83 Ada y, 135 ALGOL y, 120 instrucciones anidadas, bloques y, 46 integridad de entidad, 434 integridad referencial, 432 inteligencia artificial (LA), 5 LISP y, 369-370 PROLOG y, 337,338,355, 356 SCHEME y, 394 intercalado, 236,250 interfaces de programación de aplicaciones (application programming interfaces, API), Java y, 222-223 interfaces gráficas de usuario (Graphical User Interfaces, GUI), Java e, 223 InterLISP, 394-395 Internet, 219,224, Véase también World Wide Web (WWW) intérpretes, 4,23-24, 29 Java y, 220 LISP y, 379 recursión y, 89,91 intersección, bases de datos relaciónales e, 427

Sólo fines educativos - FreeLibros

índice IPL. Véase Lenguaje de Procesamiento de Información (Information Processing Language, IPL) isA, 198,436 ISO. Véase Organización Internacional de Estándares (International Standards Organization, ISO) iteración, 84-88,388-389 Java, 34, 35, 217-228, 230 ajustadores de tipo, 222-223 animación y, 223 API en, 222-223 arreglos y, 55 cadenas de caracteres y, 57 fuentes de software y, 458 POO y, 198 sensibilidad a la caja tipográfica, 39 Java Development Kit (JDK), 222, 458 k-tupla, 425 Kernel APSE (KAPSE), 142 Kleene star, 292, 294 Laboratorio de Inteligencia Artificial del M IT (MIT Artificial Intelligence Laboratory), 394 Laboratorios Bell (Bell Labs), C y, 146-147 LAN. Véase red de área local (local area network, LAN) Lenguaje Algebraico Internacional (International Algebraic Language, IAL), 111 lenguaje anfitrión, 22,421, 422 lenguaje basado en clases, 77 Lenguaje Base de Sistemas de Información (Information System Base Language, ISBL), 428 lenguaje de consulta, 421, 425429 matemáticas y, 425 para bases de datos semánticas, 435-437 PROLOG como, 338, 339-340, 434

Lenguaje de Consulta Estructurado (Structured Query Language, SQL), 429 bases de datos y, 421,424, 429-434 fuentes de software y, 460 SEQUEL y, 428, 429 Lenguaje de Implementación de Red (Network Implementation Language, NIL), 245, 393 Lenguaje de Palabra de Código (Code Word Language, CWL), 289 Lenguaje de Procesamiento de Información (Information Processing Language, IPL), 369 Lenguaje de Programación Combinada (Combined Programming Language CPL), 145,146-148 lenguaje de definición de datos (data definition language, DDL), 11, 421,422,423 bases de datos y, 430,431 POO y, 437 lenguaje de manipulación de datos (data manipulation language, DML), 11, 24, 421, 422,423 bases de datos y, 430 POO y, 437 lenguaje de quinta generación, 14 PROLOG como, 337, 354-355 lenguaje de referencia, 120,121 lenguaje Eiffel, 202, 266 lenguaje ensamblador, 12,15, 147 abstracciones y, 83 Occam como, 246 traducción y, 23 lenguaje estructurado en bloques Ada como, 133 fuentes de software y, 458 máquina virtual y, 275 Pascal como, 46 lenguaje núcleo, 26 lenguaje orientado a procedimientos, 9 lenguajes actores, 172 lenguajes basados en objetos, fuentes de software y, 458

Sólo fines educativos - FreeLibros

483

lenguajes de alto nivel, 146,147 lenguajes de bajo nivel, 146, 147 lenguajes declarativos, 8,10-12, 317-329 comprobabilidad y, 21 lenguajes de cuarta generación, 14, 337 lenguajes de máquina, 14, 22 lenguajes de muy alto nivel, 13, 395 lenguajes de primera generación, 14 lenguajes de procedimiento, 14, 28-29 bases de datos y, 431 PROLOG vs., 357 RPC y, 247 vs. lenguajes funcionales, 364 lenguajes de programación, 1, 3, 29 abstracciones y, 70,71 comprobabilidad y, 20-21 confiabilidad y, 21-22 de alto nivel, 13, 83 de bajo nivel, 13-14, 22 de muy alto nivel, 13,395 matemáticas y, 4 para bases de datos, 421-439 para POO, 165-230 propósito de, 4 lenguajes de segunda generación, 14 lenguajes de sistemas de datos (data system languages, DSL), 421,422 lenguajes de tercera generación, 14 lenguajes formales, 271-315 comprobabilidad y, 21 definición, 274, 275-276 jerarquía de Chomsky de, 277-286 natural vs., 4 semántica y, 19 tipos de, 276,277-278 lenguajes funcionales, 15 características de, 365-369 colección de basura en, 404405. Véase también colector de basura implementación, 396-405 lenguajes imperativos, 107-270 ML y, 407 programación funcional y, 364

484

índice

tipos de datos y, 51 variables y, 31 lenguajes integrados, 16 lenguajes naturales comprobabilidad y, 21 gramáticas para, 282,311315 lenguajes formales vs., 4 LISP y, 370 PROLOG y, 338 semántica y, 20 lenguajes orientados a objetos,

10 abstracciones y, 5 ,1 0 3 lenguajes para publicación, 120 lenguajes. Véase lenguajes de programación liga punto a punto, 10,245 ligador, 23 ligadura, 31,41-42 alcance y, 44 cálculo de predicados y, 448 LISP y, 376,385,400-404 programación funcional y, 371 tiempo, 41 verificación de tipos y, 64 ligadura de dirección, 41-42,99 ligadura de nombre, 41-42 ligadura de tipo, 43,64. Véase también ligadura ligadura de valor, 42 ligadura dinámica, 4 1 ,4 2 Ada y, 175 bases de datos y, 437 ligadura de tipos y, 42 POO y, 166,175,214-217 ligadura estática, 4 1,175 ligas bases de datos y, 429 punto a punto, 10 registros de activación y, 4849 limitantes de integridad, bases de datos y, 433 límites dinámicos, 119 límites flexibles, 119 Linda, 10,263-267 lingüística, 280 LISP, 5 ,8 ,1 3 ,1 4 ,3 7 1 -3 9 3 ,4 1 8 abstracciones y, 74 alcance y, 47 apuntadores y, 38 cálculo lambda y, 451,454 como lenguaje funcional, 11, 364

estándares y, 27 funciones automodificables en, 385-388 funciones de primer orden y, 366 funciones integradas de, 376378,453 historia del, 369-370 listas y, 6 2 ,6 3 ML vs., 408 MYCIN y, 354 operadores y, 125 ortogonalidad y, 25 paso de procedimiento y, 124 problemas funarg en, 400-404 programación funcional y, 363,365 PROLOG y, 344, 354, 355 puro, 95,394 recursión y, 91 tipos de datos en, 32,371-373 traducción y, 22,29 univ en, 343 LISt Processing (Procesamiento por Lista). Véase LISP lista ligada, 127 lista objeto, LISP y, 371, 397 listas, 62 abstracciones y, 74 circulares, 211 excepciones y, 97 LISP y, 369, 371-375 M L y, 410 PROLOG y, 340 literales, 53 llamadas de procedimiento remoto (remóte procedure calis, RPC), 2 4 4 ,246,263 llamadas de procedimiento, 42 abstracciones y, 96-97 límite, 213-218 mensajes como, 168 monitores y, 243-244 registros de activación y, 48 remotas. Véase llamadas de procedimiento remotas (remóte procedure calis, RPC) localizadores uniformes de recursos (Uniform Resource Locators, URL), 218,226 lógica, 4-5 paradigmas declarativos y,

10,11 PROLOG y, 337

longitud de cadena de caracteres dinámica, 56 longitud de cadena de caracteres estática, 56 LPA PROLOG, 459-460 MacLISP, 394-395 macrocomputadoras (mainframes), 34 macros, 390-394 magnitud, 371-373 manejadores de contenido, en Java, 226 manejadores de excepción, 21, 22 ,2 9 Ada y, 141-142 ML y, 408,413 MAPSE. Véase APSE Mínimo (Mínimum APSE, MAPSE) máquina memoria virtual, 41 objetivo, 32 teórica, 2 0,275,349-352 Máquina de Turing (Turing Machine, TM), 5, 283-285, 288, 315 CLW y, 288 gramáticas no restringidas y, 288 máquina abstracta de Warren (Warren Abstract Machine, WAM), 353 máquina intermedio, 32 máquina virtual, 41,274-275 máquinas teóricas lenguajes formales y, 275 PROLOG y, 349-352 semántica y, 20 marcos, 47. Véase también registros de activación marcos de pila, 48 matemáticas, 4 abstracciones y, 7 2 ,7 6 cálculo de predicados y, 447 clases y, 179-180 lenguajes de consulta y, 425 LISP y, 369 lógica y, 319, 337 notación y, 25 paradigmas declarativos y,

10 programación funcional y, 364, 366 semántica y, 20-21 memoria apuntadores y, 37

Sólo fines educativos - FreeLibros

índice compartición. Véase memoria compartida traducción y, 22,23 virtual, 41 memoria compartida, 10, 234, 235 Ada y, 257 paso de mensajes y, 238 punto de reunión (rendezvous) y, 245 soluciones de sincronización para, 247, 262 memoria de acceso aleatorio (random access memory, RAM), 8 ,1 2 memoria de sólo lectura (readonly memory, ROM), 8 mensajes, 168 Ada y, 193 objetos y, 103 punto de reunión (rendezvous) y, 245 Smalltalk y, 193 mensajes de error, palabras reservadas y, 41 Meridian Ada Compíler, 457458 Meta Lenguaje (ML), 365,373, 398,407-417, 418 cálculo lambda y, 451 FP vs., 408 llamada por nombre y, 118 metalenguaje, 17, 21,365 BNF como, 421 especificación algebraica y, 78 metas, 332 metasímbolos, 17,291,308 métodos, 168,169-171 Ada y, 175 Java y, 220-221 objetos y, 103 procedimientos como, 173, 177 SCOOP y, 392-393 Simula y, 173 Micro-PROLOG, 339, 340, 459460 mixins, 391-394 ML. Véase Meta Lenguaje (ML) modelo de cascada (Waterfall), 142,143 modelo de reanudación, 91,92 modelo de red, 423-424,429 modelo de terminación, 91 modelo jerárquico, 422-423, 429

modelo relacional, 424-434 modelos abstractos, 76-78 modelos de datos funcionales (functional data models, FDM), 436-437 modelos de datos semánticos, 335-337 modelos de relación de entidad (entity relationship, ER), 335-337 modelos ER. Véase modelos de relación de entidad (entity relationship, ER) modos, ALGOL y, 123 Modula, 7 ,1 0 ,1 0 2 monitores en, 245 M odula-2,102 ALGOL y, 112,113,124-125 confiabilidad y, 21 encapsulamiento de datos y, 171 RPC y, 247 unidades de proceso en, 236 modularización ADT y, 101-102 Common LISP y, 395 módulo de carga, 23 módulo ejecutable, 23 módulos, 10 ADT y, 101-102 C y, 149-150 C-Linda y, 266 carga, 23 clases y, 180 ejecución concurrente de, 104 ejecutable, 23 encapsulamiento de datos y, 171 interfaz, 94 lista de exportación, 77, 102 lista de importación, 102 ML y, 412-413 monitores y, 244 PROLOG y, 354-355 refinamiento por pasos y, 7475 módulos de definición, 102 módulos de implementación, 102 módulos genéricos, PROLOG y, 353-354 modus ponens, 322,443 monitores, 240, 243-244 Ada y, 256 Concurrent Pascal y, 251-252

Sólo fines educativos - FreeLibros

485

Java y, 220 punto de reunión (rendezvous) y, 244 MULTICS. Véase Servicio MULTíplexado de Información y Computación (MULTiplexed Information and Computing Service, MULTICS) multiprocesamiento, 235,366 nombramiento directo, 246 nombres, 40 notación, 25 cálculo lambda y, 451 científica, 33 funciones y, 9 5 ,9 6 programación funcional y, 364, 366 números complejos LISP y, 371-373 ML y, 407-408 números de Bernoulli, 131 números de punto fijo, 3 3 ,3 4 números de punto flotante, 33, 34,135-136 PROLOG y, 344 subrangos y, 51 números/aritmética real de doble precisión, 34,68 números/aritmética reales, 3334 abstracciones y, 81 caracteres y, 34 LISP y, 371-373 PROLOG y, 345 verificación de tipos y, 64 O (OR) inclusivo, 442 Object Pascal, 167,173,206-210, 177 abstracciones y, 81 clases en, 183-190 fuentes de software y, 458 ligadura dinámica en, 215216 Objective-C, 198, 220 objetos, 9-10 abstracciones y, 73, 76, 79, 103 apuntadores y, 37 clases y, 181. Véase también clases definidos, 168 estado y, 165

486

índice

genéricos, Object Pascal y, 185 LISP y, 389-393, 397-398 ML y, 414 procesamiento paralelo y, 266 procesos como, 173 programación con, 166-180 Simula y, 172-175,173 tupias y, 262-266 objetos de primera clase funciones como, 364,367368,401 LISP y, 376 Occam, 10, 236 fuentes de software y, 459 paso de mensajes en, 246, 259-262 procesamiento paralelo y, 405 ocultamiento de información, 74-75, 80,102 Ada y, 175 bloques y, 110 ML y, 413 objetos y, 103 Pascal y, 183,188 POO y, 166 Véase también encapsulamiento de datos; encapsulamiento operaciones abstracciones y, 73, 76, 79 conjunto, 62 listas y, 63 objetos y, 103,165 registros y, 58 tipos de datos subrango y, 51 verificación de tipos y, 65 operador coma, C y, 157-158 operadores abstracciones y, 96-97 ALGOL y, 123-124 C y, 151-158 desplazamiento, 152 LISP y, 371-374 notación infija, 96 notación posfija, 96 notación prefija, 96 PROLOG y, 339-344 operadores definidos por el usuario, 97 optimización de compiladores, 23 ordenación lexicográfica, cadenas de caracteres y, 56

Organización Internacional de Estándares (International Standards Organization, ISO), 18, 429 organizaciones de estándares, 26-27 ortogonalidad, 23-24, 94 ALGOL y, 123 ML y, 413 SQL y, 430 OS. Véase sistema operativo (operating system, OS) palabra aceptada, 292 palabra clave, 39-41 palabras reservadas, 39-41, 252 palabras, lenguajes formales y, 275-276 paquete elaborado, 134 paquetes, 10,102 Ada y, 133-134,135,175 Common LISP y, 395 encapsulamiento de datos y, 171 Java y, 221, 223 LISP y, 390-394 tareas vs., 134 Véase también paquetes genéricos paquetes genéricos, 182-183 paquetes predefinidos, 132-133 par punteado, 373,377,396 paradigma abstracto, 6 paradigma basado en objetos, 6,9-1 0 ,1 2 5 paradigma en bloques estructurado, 7,8,109-161 paradigma de lenguaje para base de datos, 11 paradigma distribuido, 10 paradigmas, 1, 7-12 abstractos, 7 ,7 2 ejemplos de, 8 para procesamiento paralelo, 234-235 POO y, 165 paradigmas de lenguaje. Véase paradigmas paradigmas imperativos, 8 ,9 10

Paradox, 179-180, 369 paralelismo, 352, 361 granularidad de, 406-407 programación funcional y, 405-407

parámetros abstracciones y, 81, 94, 96-100 ALGOL y, 117,120,124 C y, 149 cálculo lambda y, 453-454, 455 formal vs. real, 41 formal, 96-97, 98 funciones y, 363, 364, 368 llamada por nombre, 117-118 llamada por valor, 117 nombre, 99,117,119 objetos y, 103 Pascal y, 117 PROLOG y, 357 reales, 96-97,98 referencia, 98-100 resultado de valor, 98-100 resultado, 98 valor, 98,119 var, 24, 41 PARC. Véase Xerox Palo Alto Research Center (PARC) semáforos en, 249-251 Pascal, 7 ,8 ,1 5 , 29,124-129 abstracciones y, 73-74, 80-81, 95 Ada y, 1 2 7 ,131,137 ALGOL y, 112,113,118,124, 125 apuntadores y, 36,38 archivos en, 421 arreglos y, 5 4 ,5 6 ,8 1 ,1 1 8 bases de datos y, 421 bloques y, 46 BNF y, 17,18, 21 C y, 149 cadenas de caracteres y, 56 comparado con lenguaje ensamblador, 13 conjuntos y, 60, 61 enteros y, 32 Estándar revisado ISO 1980, 18 estándares y, 27 extensibilidad y, 26 fuentes de software y, 457 funciones en, 368 generalidad y, 25 gramáticas libres de contexto, 305 HOLWG y, 130 identificadores y, 40 intérpretes y, 29 ligadura y, 41-42 modularización y, 102

Sólo fines educativos - FreeLibros

índice objetos en, 177 ortogonalidad y, 25 palabras clave en, 41 paso de procedimiento y, 123 PDA y, 301-302 pseudocódigo y, 17 registros y, 58 semántica de, 21 Simula y, 173 sintaxis de, 2.2, 84 tipos de datos y, 5 1,52 tipos unión y, 5 9 ,6 0 UNIX y, 158 Véase también Concurrent Pascal; Object Pascal; Sequential Pascal; Turbo Pascal; UCSD Pascal verificación de tipos y, 64,65, 66 Pascal-Ada, bloques y, 110 Pascal 74 Estándar, 65 Pascal 83 Estándar, 65 Pascal/R, 421 PascalS fuentes de software y, 459 Kit de implementación para, 270 paso de mensaje simétrico, 246 paso de mensaje sin búfer, 246 paso de mensajes, 169-171, 237 falla parcial de administración en, 267 Occam y, 258-262 procesamiento paralelo y, 245-247 tupias y, 263 paso de mensajes almacenados en búferes, 247 paso de mensajes de uno a muchos, 246 PC-SCHEME, 460 PC. Véase cálculo de predicados PDA. Véase autómata descendente (push-down automaton, PDA) P D P -11,146 persistencia, 421 pila, 37-38 Ada y, 140 alcance dinámico y, 46 LISP y, 397 pilas, 9,109 apuntadores y, 37 arreglos y, 55 máquina virtual y, 274 M L y, 411

Object Pascal y, 185,189-190 Turbo Pascal y, 184 Véase también pilas genéricas pilas genéricas, en C++, 190191 PL/I, 9 ,1 6 ALGOL y, 118 apuntadores y, 37 bases de datos y, 432 cadenas de caracteres y, 56 excepciones y, 91-92 gramáticas libres de contexto y, 304 identificadores y, 40 límites dinámicos y, 119 números reales y, 33,34 traducción y, 24 VDL y, 22 PM. Véase Principia Mathemaiica (PM) polimorfismo, 180-194 ML y, 408,409,411 objetos y, 103 POO y, 167 politipos, 412 POO. Véase programación orientada a objetos (objectoriented programming, OOP) precedencia de operador, 303, 305. Véase también operadores de precedencia predicado, 446-447 Principia Mathematica (PM), 322, 443-444,446 principio de exclusión central, 325 principio de subtipo, 200 principio del agujero de pichón, 250 problema de paro, 369 problemas funarg, 400-403 procedimientos abstracciones y, 76,81,94-101 alcance y, 44 ALGOL y, 115,123 bloques y, 109 como métodos, 173,178 como parámetros, 101 especificación algebraica y, 78 excepciones y, 90 ligadura y, 43 objetos y, 103 Pascal y, 126,178 PROLOG y, 357

Sólo fines educativos - FreeLibros

487

Simula y, 173 variables y, 31 procedimientos de intercambio, 81,82 procedimientos y funciones genéricos Ada y. Véase facilidad genérica de Ada Pascal y, 128 procesadores de texto, DFA y, 295 procesadores, procesos vs., 237 procesamiento de datos, 14 procesamiento paralelo, 233270 dos modelos para, 234 POO y, 267 PROLOG y, 352-353 programación funcional, 367, 405-407 soluciones de sicronización para, 246-262 tupias y objetos en, 262-267 procesos, 233,236 clases de, 173 como objetos, 173 Concurrent Pascal y, 251- 252 concurrente vs. paralelo, 233 múltiple, 236-237, 245 Pascal y, 236 procesadores vs., 235 punto de reunión (rendezvous) y, 244 sincronización de, 238-246 procesos concurrentes administración de falla parcial, en, 267 Pascal S y, 250 programación concurrente, 9, 126, 233 secuencial vs., 234 Véase también procesamiento paralelo procesos múltiples, 237-238, 245 procesos secuenciales de comunicación (communicating sequential processes, C.SP) BSP y, 247 Occam y, 246 punto de reunión (rendezvous) en, 244 producciones forma normal de Chomsky y, 307

488

índice

gramática libre de contexto y, 281 gramáticas sensibles al contexto y, 282 lenguajes formales y 275-276 PROLOG y 338 reglas de borrado, 282, 290 tablas de transición y 294 producto cartesiano, bases de datos relaciónales y 426427,429 programación "in the large", 13-14 lenguajes de muy alto nivel y, 13, 395 modularización y 102. Véase también modularización; módulos programación aplicativa, 363419 fuentes de software y 460 programación distribuida, fuentes de software y 458459 programación funcional, 363419 APL para, 407 cálculo lambda y 451, 454. Véase también cálculo lambda evaluación perezosa contra evaluación estricta en, 398399, 405-406 fuentes de software y, 460 ML para, 408-418. Véase también Meta Lenguaje (ML) paralelismo y, 405-407 programación lógica, 319-361 fuentes de software y, 459460 PROLOG y 333-334, 337-361 programación orientada a objetos, POO (ObjectOriented Programming, OOP), 165-230 Ada y 140 bases de datos y 437-438, 439 clases y polimorfismo en, 180-194 fuentes de software y 458 herencia y 166,171,196-217 Java y 217-228 LISP y 370, 389 periódicos y revistas en, 229

procesamiento paralelo y 266 Smalltalk y 194-196 programación sin procedimiento, PROLOG y, 338 programas en LBA, 283-286 en LISP, 369, 374-375 funciones y 364 PROLOG extendido autocontenido (Extended Self-Contained PROLOG, ESP), 356 PROLOG núcleo, 337-338, 355 PROLOG, 1 0 ,1 1 ,1 3 ,1 4 , 337-361 bases de datos relaciónales y 434 bases de datos y 439 cálculos lógicos y, 447-448 como lenguaje de consulta, 434 DEC-10 PROLOG, 338 encadenamiento hacia atrás y, 332-333 fortalezas y debilidades de, 356-357 fuentes de software y 459460 hechos negativos en, 333 listas y 62 operadores y 124 programación funcional y, 363 SCHEME y 365 semántica y 19 sintaxis Edinburgh, 338, 339, 460 traducción y 22, 29 propagación dinámica, 9 1 ,9 2 Protocolo de Control de Transmisión/Protocolo de Internet (Transmission Control Protocol/Internet Protocol, TCP/IP), 219 protocolo de transferencia de archivos (File Transfer Protocol, FTP), 218, 223, 226 protocolo de transferencia de hipertexto (HTTP), 218, 223,224 protocolos Java y 218, 225 objetos y, 103 prototipos, 33

Common LISP y, 395 ML y, 414 proyección, bases de datos relaciónales y, 426, 427, 429 proyecto rediflow, 407 prueba de teoremas ML y, 414 programación lógica y, 337 PROLOG y 338 prueba, modularización y, 102 pruebas, 322-329 búsqueda y, 329-337 cálculo lambda y, 452 cálculos lógicos y 444-446 programación funcional y, 364, 369 puertos, 245 punto binario, 33 punto radix, 33 QUOSET, 354 ramificación, 83-85 rastreo de marca, 404-405 RBS. Véase sistemas basados en reglas (rule-based systems, RBS) reales fijos, 135-136 recursión, 88-91 ALGOL y, 118 bloques y, 109 cálculo lambda y, 453, 454 funciones y, 364 izquierda, 348 ligadura y, 43 LISP y 370, 378-379, 382-383, 388 ML y, 411 programación funcional y, 369 PROLOG y, 340-342, 345-348 registros de activación y, 48 Wase también recursión de extremo recursión de extremo, 89, 90 PROLOG y, 345-348, 352 Véase también recursión red de área amplia (wide area network, WAN), 234 red de área local (local area network, LAN), 10, 234 red de transición aumentada (augmented transition network, ATN), 312-314 red, ejecución concurrente y, 104

Sólo fines educativos - FreeLibros

ín d ic e redes de transición recursiva (recursive transition networks, RTN), 311-313 reducción de gráficas en paralelo, 408 reducción de orden normal, 454 reducción gráfica en paralelo (Graph Reduction in Parallel, GRIP), 408 reducciones de orden aplicativo, 398,399 cálculo lambda y, 454 reducciones, cálculo lambda y, 452-453. referencias colgantes, 38 refinamiento por pasos,74-75 refutación completa, 326 regiones de datos, 239 registros, 57-61 activación. Véase registros de activación apuntadores y, 37 bases de datos y, 421,429 componentes, 57-58 implementación de, 68 ML y, 410 POO y, 177 tipos agregados y, 53 registros de activación, 43,474 9 ,6 8 excepciones y, 91 funciones y, 95 parámetros y, 100 recursión y, 90 registros discriminados, 137 registros variantes, 59-60 Ada y, 136 Pascal y, 65,126 regla decidible, 277,286-287 regla pitagórica, SCHEME y, 391 reglas cálculo lambda y, 451,452 PROLOG y, 338, 339-340 reglas de transformación, cálculo lambda y, 452 regularidad, 25,128 relaciones especificación algebraica y, 78-79 programación funcional y, 365-366 PROLOG y, 355, 365 relaciones, bases de datos y, 421

reloj de tiempo real (real-time dock, RTC), 349-352 remplazo uniforme, 322 rendezvous (punto de reunión), 10,244 Ada y, 252-257 Concurrent C y, 257-259 estancamiento cíclico y, 268 paso de mensajes y, 246 tubería y, 246 representaciones de hardware, 121

resolución correcta, 325 cálculo lambda y, 454-456 programación funcional y, 369 resoluciones, 322-330 búsqueda y, 329-336 PROLOG y, 340 restricciones, en PROLOG, 349350 retroceso o backtracking, 331332, 341, 343, 357 paralelismo y, 352 robótica, LISP y, 370 ROM. Véase memoria de sólo lectura (read-only memory, ROM) RPC. Véase llamadas de procedimiento remoto (remóte procedure calis, RPC) RTC. Véase reloj de tiempo real (real-time dock, RTC) RTN. Véase redes de transición recursiva (recursive transition networks, RTN) saltos, 83 SCOOPS, 389-394 SCHEME, 29, 365,418 abstracciones y, 74 alcance y, 47,400 cálculo lambda y, 451 efectos colaterales en, 382388 estándar IEEE para, 370 evaluación perezosa en, 398, 406 facilidad de ayuda (Help), 375-376 formas funcionales y, 379380 fuentes de software y, 460 función de automodificación en, 386

Sólo fines educativos - FreeLibros

4 89

inconsistencias de tipo en, 371, 373 ligaduras y, 401 lista de objetos en, 397-398 llamada por nombre y, 118 POO y, 390-394 problemas funarg en, 401-403 PROLOG y, 366 recursión y, 91, 382-383 vectores y cadenas de caracteres en, 389. Véase también LISP selección, bases de datos relaciónales y, 426, 427, 429 selectores, 371 semáforos, 240-243, 247-251 Ada y, 257 monitores y, 244 semántica, 4,19-20 ALGOL 60 y, 112 análisis, 23 análisis sintáctico y, 303, 305 ATN y, 312 cálculo lambda y, 451-454 comprobabilidad y, 20,21 definida, 275 funciones y, 364 lenguajes formales, 273, 275 ligadura y, 41 lingüística y, 279 ML y, 413-417 programación funcional y, 364, 368-369 sintaxis vs., 275 semántica de designación, 20, 21 ,2 9 semántica natural, 414 semánticas axiomáticas, 19-20, 28, 29 comprobabilidad y, 21 sensibilidad a la caja tipográfica, 40 separación, 443 SEQUEL, 428, 429. Véase también Lenguaje de Consulta Estructurado (Structured Query Language, SQL) Sequential Pascal (Pascal S), 249-251 Servicio MULTiplexado de Información y Computación (MULTiplexed Information and Computing Service, MULTICS), 146

490

ín dice

SE T L 2,5 ,1 3 arreglos y, 55 conjuntos y, 62 enteros y, 33 ligadura y, 43 sexpr. Véase expresión S símbolos autómata finito y, 295 cálculo lambda e inadecuado, 451-452 en LBA, 283-286 lenguajes formales y, 275-276 símbolos de inicio, 276 símbolos de terminal, 276 símbolos impropios, 451-452 símbolos no terminales, 277 simplicidad, 364 Simula, 75, 7 6,165,174 paso de procedimientos y, 123 POO y, 172-175 Simula-Smalltalk-C++/Java, bloques y, 110 sincronización, 238-273 Ada y, 257 Java y, 219 monitores y, 243 paralelismo y, 406-407 paso de mensaje y, 244-247 punto de reunión (rendezvous) y, 244 sintaxis, 4 ,1 6 abstracciones y, 74, 83,84 análisis sintáctico y, 304 cálculo lambda y, 451-454 comprobabilidad y, 21 confiabilidad y, 21 definida, 274 especificaciones algebraicas y, 78 lenguajes formales, 273, 274 lingüística y, 280 LISP y, 365, 374 ML y, 413-417 PDA y, 281 PROLOG y, 339-340 semántica vs., 275 Sistema de base de datos R, 429 sistema de administración de bases de datos (database management system, DBMS), 11, 29, 422. Véase también bases de datos sistema de producción, 276 sistema débilmente acoplado, 234, 264

sistema en tiempo de ejecución, 4 sistema fuertemente acoplado, 235 sistema GemStone, 437 sistema operativo (operating system, OS) C para escritura, 159 Java y, 219, 222, 224 procesamiento paralelo y, 236 sistema operativo V, RPC y, 247 sistemas basados en reglas (rule-based systems, RBS), 361 sistemas distribuidos, procesamiento paralelo y, 234, 235 sistemas en tiempo real, 269 sistemas expertos LISP y, 370 PROLOG y, 338, 355, 356 Smalltalk, 10, 29,193,194-197 abstracciones y, 81 Java y, 220 objetos y, 165 OPAL y, 438 Sm aIltalk-72,195 Sm alltalk-80,196,198 Smalltalk/V, 198 SNOBOL, 13 SNOBOL4 ligadura y, 42 cadenas de caracteres y, 56, 57 sobrecarga, 53, 64 Ada y, 182 métodos y, 103 operador, 64, 96, 437 sobrecarga de operador, 64,96, 438 sobrenombramiento, 97 solución de problemas, LISP y, 370 solución simple (PROLOG), 347 Spice LISP, 394 SQL. Véase Lenguaje de Consulta Estructurado (Structured Query Language, SQL) subcadenas de caracteres, 57 subclases, 179 subconjuntos, 26 subíndices, arreglos y, 54 submetas, 332, 346

subprogramas, 93-94,101 Ada y, 132-133 subrangos fijos, 51 superclase, 165, 219-222 suposición FIFO. Véase suposición primero en entrar/primero en salir (First-In-First-Out, FIFO) suposición primero en entrar/ primero en salir (First-InFirst-Out, FIFO), 244, 255 sustitución uniforme (Uniform Substitution), 443, 448 Synthese Languaje Library, 314 Tabla de Método Virtual (Virtual Method Table, VMT), 185,193, 206 tabla de dispersión (cálculo de direcciones) de objetos, 397-398 tabla de verdad, 442,443 tabla-v, 192-193 tablas de símbolos, 68 tablas de transición, 293-294 tablas virtuales, 214 tareas Ada y, 132,133, 237 estancamiento cíclico y, 268 paquetes vs., 135 unidades de procesos como, 237 tautología, 444 TCP/IP. Véase Protocolo de Control de Transmisión/ Protocolo de Internet (Transmission Control Protocol/Internet Protocol, TCP/IP) teoría, 322 de función, 5 de funciones, 451. Véase también cálculo lambda lógica, 443 Teoría de Aritmética de Enteros (Theory of Integer Arithmetic), 286 Teoría Cálculo Proposicional, 443 teoría de conjuntos, 5, 6 abstracciones y, 78 SETL y SETL2 y, 33 términos, en PROLOG, 339-340 tesis (cálculos lógicos), 443 thunk, 117 tiempo de carga, 41

Sólo fines educativos - FreeLibros

ín d ic e tiempo de compilación, 41 tiempo de ejecución, 41 análisis (APSE), 141 errores, confiabilidad y, 21 Java y, 220 ligadura y, 42 tiempo de vida, 42,48 tiempo real, excepciones y, 91 tipificación débil, 65-67 tipificación fuerte, 64-67 clases y, 179 Pascal y, 127-129 tipo de carácter predefinido, 52 tipo de datos Hollerith, 54 tipo entero universal, 136 tipo fijo universal, 136 tipo flotante universal, 136 tipo real universal, 136 tipos de datos, 29,31-68 abstractos. Véase tipos de datos abstractos (abstract data types, ADT) agregado, 32, 51,52-53 C y, 148-151 caracteres y, 34 encapsulamiento de datos y, 171 estructurado, 51-66 LISP y, 368,371-373 M L y, 408-412 primitivo, 31, 32-35,42 PROLOG y, 354-355 tipos de datos abstractos (abstract data type, ADT), 73-83 clases de, 102-103 modularización y, 101-102 monitores como, 243 objetos y, 166,188 procesos como, 237 programación funcional y, 371 PROLOG y, 354 tipos privados y, 133 tipos de datos agregados, 32, 51,52-53 Ada y, 137 verificación de tipos y, 64 tipos de datos de acceso. Véase apuntadores tipos de datos de caracteres, arreglos y, 54 tipos de datos de referencia. Véase apuntadores tipos de datos definidos por el usuario, 51-52

tipos de datos discretos, 51 tipos de datos enumerados, 52 arreglos y, 53 conjuntos y, 60 tipos de datos escalares, 134135,136 tipos de datos estructurados, 52-68 Ada y, 134-135 tipos de datos genéricos, 81-82 tipos de datos ordinales, 5 1 ,5 2 tipos de datos primitivos, 31, 32-35,42 tipos de datos privados, 133 limitados, 133 tipos de datos subrango, 52-53 arreglos y, 53,81 conjuntos y, 60 tipos etiquetados, 199-200 tipos funciones vs., 23-24 tipos unión, 58-60, 65 TM. Véase Máquina de Turing (Turing Machine, TM) tokens, 275 BNF y, 16,17 exploración o rastreo y, 22 expresiones regulares y, 291 gramática regular y, 281, 295 PDA y, 281 traducción y, 22 traducción rápida, 22-23 traductores, 22-23, 28-29 traductores generativos, 23 transacciones, 244,432-433 transponedores, 245,459 Equipo de Enseñanza (Education Kit), 459 transportabilidad, 26, 32 3DScheme, 460 tuberías, 245,249 tupias, 39 arreglos y, 55 bases de datos relaciónales y, 425-429 ML y, 409,410 objetos y, 262-266 programación funcional y, 366 Turbo C, fuentes de software y, 458 Turbo C++, fuentes de software, 458 Turbo Pascal, 86,102,126 clases en, 183-190

Sólo fines educativos - FreeLibros

491

fuentes de software y, 457, 458 POO y, 176-177 UCSD Pascal, 126,129 Unicode (código universal), 34 unidad, 102 unidades de paralelismo, 236 unidades de proceso, 236 unidades de programa, 133134 unificación, 328-329, 340 uniformidad, 25-26 unión (en base de datos relaciónales), 426, 428 unión de conjuntos, verificación de tipos y, 64 unión natural, 427 uniones discriminadas, 60, 65 uniones libres, 25,59-60 unív, 344 UNIX bases de datos semánticas y, 437 C y, 148,158, 248-249 Concurrent C y, 257 Java y, 224 MULTICS y, 147 PROLOG y, 355 semáforos y, 248-249 UNIX System Interprocess Communication Primitives, 267 URL. Véase localizadores uniformes de recursos (Uniform Resource Locators, URL) uso compartido de datos, 10, 104, 238. Véase también procesamiento paralelo; sincronización uso compartido de tiempo, 236 ejecución concurrente y, 104 monitores y, 244 utilidad, ejecución concurrente

y, 104 valor 1,151 valor de dominio, funciones y, 363 valor r, 151 valor rango, funciones y, 363 valor-simple, 363 valores cáculo lambda y, 455

492

ín d ic e

funciones y 363 LISP y, 376 objetos y, 165 valores booleanos, 34-35 C y, 151 tipos enumeración y, 52 tipos unión y, 60 variables, 31, 38-68 ALGOL y, 114 cálculo lambda y, 451-452, 455 control, 46 ligadura de/límite, 31,41-42, 43 PROLOG y, 340 propias, 114 registros de activación y, 4749 sensibilidad a la caja tipográfica y, 40 Véase también ligadura; variables limitadas visibilidad de, 4 4 ,6 8 ,1 3 3 134 variables de control, bloques y, 46 variables globales ligadura y, 43, 44 Pascal y, 127 programación funcional y, 368

variables inicializadas, ligadura de valor y, 43 variables libres, 43 alcance dinámico y, 46-47 bloques y, 44, 45 cálculo lambda y, 452 LISP y, 400, 401 variables limitadas, 43 abstracciones y, 97 arreglos y, 55 cálculo de predicados y, 448 cálculo lambda y, 451, 452 verificación de tipos y, 64. Véase también ligadura variables visibles, 43 ,6 8 Ada y, 133-134 VAX, 437 vectores, LISP y, 390 vehículo de prueba relacional de Peterlee (Peterlee Relational Test Vehicle), 428 Verdix Ada Development System, 457 verificación de error, tipo de datos subrango y, 52-53 verificación de tipo, 63-68 ALGOL y, 113 C y, 149 LISP y, 373

PROLOG y, 357 verificación de tipos dinámicos, 64 vista de almacenamiento (de bases de datos), 421-422 vista física (de bases de datos), 421-422,429 vistas conceptuales (de bases de datos), 421 vistas externas (de bases de datos), 421-422 visualización (arreglo), 49 VMS, 437 VMT. Véase Tabla de Método Virtual (Virtual Method Table, VMT) WAN. Véase red de área amplia (wide area network, WAN) Windows 95, Java y, 224 Windows NT, Java y, 224 World Wide Web (WWW), 217, 223 Xerox Corporation, 196 Xerox Palo Alto Research Center (PARC), 195 Zeta LISP, 394-395

Sólo fines educativos - FreeLibros

Lenguajes De Programación, 2da Edición - Doris Appleby-freelibros.pdf [1q7jdkzxyxqv] (2024)

References

Top Articles
Latest Posts
Article information

Author: Tyson Zemlak

Last Updated:

Views: 5867

Rating: 4.2 / 5 (63 voted)

Reviews: 94% of readers found this page helpful

Author information

Name: Tyson Zemlak

Birthday: 1992-03-17

Address: Apt. 662 96191 Quigley Dam, Kubview, MA 42013

Phone: +441678032891

Job: Community-Services Orchestrator

Hobby: Coffee roasting, Calligraphy, Metalworking, Fashion, Vehicle restoration, Shopping, Photography

Introduction: My name is Tyson Zemlak, I am a excited, light, sparkling, super, open, fair, magnificent person who loves writing and wants to share my knowledge and understanding with you.