Download Cómo diseñar programas
Document related concepts
Transcript
PARTE III Más en procesamiento de datos arbitrariamente grandes SECCIÓN 14 Más definiciones auto-referenciales de datos Muchas clases de datos pueden requerir definiciones más complejas que las definiciones autoreferenciales de listas y números. La definiciones de datos de ambos consisten en dos cláusulas; ambos tienen una sola auto-referencia. Sin embargo, muchas clases de datos interesantes requieren definiciones más complejas. De hecho, al respecto no hay límte a las variaciones. Es necesario aprender a formular definiciones de datos a voluntad, el punto de partida de éstas son las descripciones informales de la información. Una vez que se tienen, se puede seguir una receta de diseño ligeramente modificada para definiciones de datos auto-referenciales. 14.1 Estructuras en estructuras Los investigadores médicos se apoyan en árboles familiares (genealógicos) para investigar males hereditarios. Por ejemplo, buscar en un árbol genealógico un cierto color de ojos. Dicha tarea puede auxiliarse del cómputo, de ahí que sea natural representar árboles familiares y funciones para realizar dicho procesamiento. Una forma de mantener un árbol familiar es añadir un nodo al árbol cada vez que nace un niño. Desde este nodo se pueden dibujar conexiones a los nodos del padre y de la madre, lo cual muestra cómo se relaciona la gente en el árbol. Para los individuos cuyos padres se desconocen, no se dibuja ninguna conexión, resultando lo que se denomina un árbol familiar de ancestros, pues para cualquier nodo en el árbol, se puede encontrar los ancestros de la persona si seguimos las flechas, pero no los descendientes. Al mismo tiempo que se registra el árbol familiar, se pueden registrar ciertas piezas de información: la fecha de nacimiento, el peso al nacer, el color de los ojos, o el color del pelo, entre otros. En cuanto a dibujar un árbol de ancestros familiares véase la figura 35. Adam es el hijo de Bettina y Carl, tiene ojos amarillos, y nació en 1950. De forma similar Gustav es el hijo de Eva y Fred, tiene ojos cafés, y nació en 1988. Para representar un niño en el árbol familiar se necesita 191 combinar diversas piezas de información: el padre, la madre, el nombre, la fecha de nacimiento, y el color de los ojos. Lo que sugiere definir una nueva estructura: (define-struct hijo (padre madre nombre fecha ojos)) Carl (1926) Eyes: green Bettina (1926) Eyes: green Adam (1950) Eyes: yellow Dave (1955) Eyes: black Eva (1965) Eyes: blue Fred (1966) Eyes: pink Gustav (1988) Eyes: brown Figura 35. Un ejemplo de árbol familiar de ancestros Los cinco campos de estructuras hijo registan la información necesaria, la cual sugiere la siguiente definición de datos: Una estructura hijo: (make-hijo p m n f o) Donde p y m son estructuras hijo; n y o son símbolos, y f es número Si bien esta definición de datos es simple, no sirve. La definición se refiere a sí misma, pero debido a que no tiene alguna cláusula, no existe forma de crear una estructura hijo. Si trataramos de crear una estructura hijo, tendríamos que escribir (make-hijo (make-hijo (make-hijo (make-hijo ... ))) ... ... ... ...) 192 sin parar. Por esa razón se requiere que toda definición de datos auto-referencial consista en varias cláusulas (por ahora) y que cuando menos una de ellas no se refiera a la definición de datos. Si se pospone la definición de datos por un momento y se estudia en su lugar cómo emplear estructuras hijo para representar árboles familiares. Supóngase que se requiere añadir un hijo a un árbol familiar existente, que además se tienen ya representaciones de los padres. Por ejemplo, para Adam se podría crear la siguiente estructura: (make-hijo Carl Bettina 'Adam 1950 'yellow) Suponiendo que Carl y Bettina representan a los padres de Adam. El problema es que no siempre se conocen los padres de una persona. En la familia representada de la figura 35, no se conocen los padres de Bettina. Por lo que a pesar de que no se conozcan el padre o la madre de una persona, se debe emplear algún valor Scheme para los dos campos correspondientes de la estructura hijo. Se podrían emplear todo tipo de valores para indicar la carencia de información (5, false, o ‘ninguno); aquí se emplea empty. Por ejemplo, para construir una estructura hijo para Bettina, se hace lo siguiente: (make-hijo empty empty 'Bettina 1926 'green) Por supuesto, si sólo uno de los padres falta, se llena el campo correspondiente con empty. El análisis sugiere que un nodo hijo tiene la siguiente definición de datos: Un nodo hijo es (make-hijo p m n f o) donde 1. p y m son cualquiera de a. empty o b. nodos hijo; 2. n y o son símbolos; 3. f es un número. Esta definición es especial en dos aspectos. Primero, es una definición de datos auto-referencial que involucra estructuras. Segundo, la definición de datos menciona dos opciones para el primer y el segundo componente. Lo que viola las convenciones relacionadas con la forma de las definiciones de datos. Se puede evitar ese problema al optar por definir la colección de nodos en un árbol familiar. Un nodo-árbol-familiar, naf, es cualquiera de 1. empty, o 193 2. (make-hijo padre madre nombre fecha ojos) donde p y m son nafs, n y o son símbolos, y f es un número. Esta nueva definición satisface las convenciones. Consiste de dos cláusulas. Una de las cláusulas es auto-referencial, la otra no. En contraste con las definiciones de datos previas relacionadas con estructuras, la definición de naf no es una explicación directa de qué clase de datos se muestran en cada campo. En su lugar, se tiene una multicláusulas y auto-referencial. Considerándose que está es la primera de tales definiciones, al traducir cuidadosamente el ejemplo de la figura 35, se garantiza que la nueva clase de datos puede representar la información de interés. La información para Carl es fácil de traducir a un naf: (make-hijo empty empty 'Carl 1926 'green) Bettina y Fred se representan con nodos similares. A su vez, el nodo para Adam se crea con (make-hijo (make-hijo empty empty 'Carl 1926 'green) (make-hijo empty empty 'Bettina 1926 'green) 'Adam 1950 'yellow) Como muestran los ejemplos, una transliteración simple nodo-por-nodo de la figura 35 requiere numerosas repeticiones de datos. Por ejemplo, si se construye la estructura hijo para Dave como la de Adam, se obtiene (make-hijo (make-hijo empty empty 'Carl 1926 'green) (make-hijo empty empty 'Bettina 1926 'green) 'Dave 1955 'black) De ahí que sea una buena idea introducir una definición de variable por nodo y emplearla después. Para facilitar las cosas, se emplea Carl para representar la estructura hijo que describe Carl, etc. La transliteración completa del árbol familiar en Scheme se encuentra en la figura 36. Las definiciones de estructura en la figura 36 corresponden naturalemente a una imagen de cajas anidadas profundamente. Cada caja contiene cinco compartimentos. Las primeras dos contienen a su vez cajas, las cuales contienen cajas en sus dos primeros compartimentos, etc. De ahí que si se dibujaran las definiciones de estructura para el árbol familiar empleando cajas anidadas, 194 rápidamente quedaríamos abrumados por los detalles del dibujo. Es más, el dibujo copiaría ciertas porciones del árbol de la misma forma que nuestro intento de emplear make-hijo sin definiciones de variable. Por dichas razones, es mejor imaginar las estructuras como cajas y flechas, como originalmente se dibujaron en la figura 35. En general, un programador debe ir y venir flexiblemente entre dichas ilustraciones gráficas. Para extraer valores de estructuras, la imagen de cajas-en-cajas opera mejor; para descubrir cómo operar grandes colecciones de estructuras interconectadas, la imagen cajas-y-flechas opera mejor. ;; Generación mayor: (define Carl (make-hijo empty empty 'Carl 1926 'green)) (define Bettina (make-hijo empty empty 'Bettina 1926 'green)) ;; Generación intermedia: (define Adam (make-hijo Carl Bettina 'Adam 1950 'yellow)) (define Dave (make-hijo Carl Bettina 'Dave 1955 'black)) (define Eva (make-hijo Carl Bettina 'Eva 1965 'blue)) (define Fred (make-hijo empty empty 'Fred 1966 'pink)) ;; Generación más joven: (define Gustav (make-hijo Fred Eva 'Gustav 1988 'brown)) Figura 36. Una representación Scheme del árbol familiar de muestra Una vez que se dispone de una comprensión clara de la representación de árboles familiares, se puede iniciar el diseño de funciones que consumen nafs. Considérese inicialmente una función genérica de este tipo: ;; fun-para-naf : naf -> ??? (define (fun-para-naf un-árbol) ...) Después de todo, se debe poder construir el formato sin tener que considerar el propósito de la función. Puesto que la definición de datos de nafs contiene dos cláusulas, el formato debe consistir de una cond-expresión con dos cláusulas. La primera opera con empty , la segunda con estructuras hijo: ; fun-para-naf : naf -> ??? (define (fun-para-naf un-árbol) (cond [(empty? un-árbol) ...] [else ; (hijo? naf) ...])) 195 Es más, para la primera cláusula, la entrada es atómica de ahí que no se requiera nada adicional. Para la segunda cláusula, sin embargo, la entrada contiene cinco piezas de información: dos nodos de árbol familiar adicionales, el nombre de la persona, la fecha de nacimiento, y el color de ojos: ; fun-para-naf : naf -> ??? (define (fun-para-naf un-árbol) (cond [(empty? un-árbol) ...] [else ... (fun-para-naf (hijo-padre un-árbol)) ... ... (fun-para-naf (hijo-madre un-árbol)) ... ... (hijo-nombre un-árbol) ... ... (hijo-fecha un-árbol) ... ... (hijo-ojos un-árbol) ...])) Aplicándose fun-para-naf a los campos del padre y la madre debido a auto-referencias en la segunda cláusula de la definición de datos. Si volvemos a un ejemplo concreto de función ancestro-ojo-azul?, la función que determina si alguien en un árbol familiar tiene ojos azules: ;; ancestro-ojo-azul? : naf -> booleano ;; determinar si un-árbol contiene una estructura hijo con ´azul en el campo ojos (define (ancestro-ojo-azul? un-árbol) ...) Siguiendo la receta, se desarrollan algunos ejemplos. Considérese el nodo de árbol familiar para Carl. No tiene ojos azules, y debido a que no tiene ningún ancestro conocido en el árbol familiar, el árbol familiar representado por dicho nodo no contiene una persona con ojos azules. De ahí que (ancestro-ojo-azul? Carl) devuelve false. En cambio, el árbol familiar representado por Gustav contiene un nodo para Eva quien tiene ojos azules. Por lo que (ancestro-ojo-azul? Gustav) devuelve true. 196 El formato de la función es como el de fun-para-naf, excepto que emplea el nombre ancestro-ojoazul? Como siempre, se emplea el formato para orientar el diseño de la función. Primero se supone que se presenta (empty? un-árbol), en cuyo caso, el árbol familiar es empty, y nadie tiene ojos azules. De ahí que la respuesta deba ser false. La segunda cláusula del formato contiene varias expresiones que requieren interpretación: 1. (ancestro-ojo-azul? (hijo-padre un-árbol)), la cual determina si alguien en el naf del padre tiene ojos azules. 2. (ancestro-ojo-azul? (hijo-madre un-árbol)), la cual determina si alguien en el naf de la madre tiene ojos azules. 3. (hijo-nombre un-árbol), extrae el nombre del hijo. 4. (hijo-fecha un-árbol), extrae la fecha de nacimiento del hijo. 5. (hijo-ojos un-árbol), obtiene el color de los ojos del hijo. Depende ahora de nosotros emplear dichos valores apropiadamente. Es evidente que si la estructura hijo contiene ‘blue en el campo ojos, la función responde true. De otra forma, la función produce true si hay alguna persona en el árbol del padre o de la madre. El resto son datos que no nos sirven. Ese análisis sugiere formular una expresión condicional y que su primera condición es (symbol=? (hijo-ojos un-árbol) 'azul) Las dos recursiones son dos condiciones adicionales. Si alguna produce true, la función produce true. La else-cláusula genera false. En resumen, la respuesta en la segunda cláusula es la expresión: (cond [(symbol=? (hijo-ojos un-árbol) 'azul) true] [(ancestro-ojo-azul? (hijo-padre un-árbol)) true] [(ancestro-ojo-azul? (hijo-madre un-árbol)) true] [else false]) La primera definición en la figura 37 reúne todo. La segunda definición muestra como formular dicha cond-expresión como una equivalente or-expresión, probando cada condición una después de la otra, hasta que una de ellas sea true o que todas se hayan evaluado a false. La función ancestro-ojo-azul? es raro que emplee la recursión como condiciones en condexpresiones. Para comprender cómo opera esto, evaluemos una aplicación manual de ancestroojo-azul? a Carl: 197 (ancestro-ojo-azul? Carl) = (ancestro-ojo-azul? (make-hijo empty empty ‘Carl 1926 ‘green)) = (cond [(empty (make-hijo empty empty ‘Carl 1926 ‘green)) false] [else (cond [(symbol=? (hijo-ojos (make-hijo empty empty ‘Carl 1926 ‘green)) ‘blue) true] [(ancestro-ojo-azul? (hijo-padre (make-hijo empty empty ‘Carl 1926 ‘green))) true] [(ancestro-ojo-azul? (hijo-madre (make-hijo empty empty ‘Carl 1926 ‘green))) true] [else false])]) = (cond [(symbol=? ‘green ‘blue) true] [(ancestro-ojo-azul? empty) true] [(ancestro-ojo-azul? empty) true] [else false]) = (cond [false true] [false true] [false true] [else false]) = false La evaluación confirma que ancestro-ojo-azul? opera adecuadamente para Carl, y también muestra cómo opera la función. Ejercicio 14.1.1 La segunda definición de ancestro-ojo-azul? emplea una or-expresión en lugar de un condicional anidado, como puede verse en la figura 37. Emplear una evaluación manual que muestre que dicha definición produce la misma salida para las entradas empty y Carl. 198 ;; ancestro-ojo-azul? : naf -> booleano ;; determinar si un-árbol contiene una estructura hijo con >azul en el campo ojos ;; versión 1: empleando una cond-expresión anidada (define (ancestro-ojo-azul? un-árbol) (cond [(empty? un-árbol) false] [else (cond [(symbol=? (hijo-ojos un-árbol) 'blue) true] [(ancestro-ojo-azul? (hijo-padre un-árbol)) true] [(ancestro-ojo-azul? (hijo-madre un-árbol)) true] [else false])])) ;; ancestro-ojo-azul? : naf -> booleano ;; determinar si un-árbol contiene una estructura hijo con >azul en el campo ojos ;; versión 2: empleando una or-expresión (define (ancestro-ojo-azul? un-árbol) (cond [(empty? un-árbol) false] [else (or (symbol=? (hijo-ojos un-árbol) ‘blue) (or (ancestro-ojo-azul? (hijo-padre un-árbol)) (ancestro-ojo-azul? (hijo-madre un-árbol))))])) Figura 37. Dos funciones para encontrar un ancesto ojo-azul Ejercicio 14.1.2 Confirmar manualmente que genera false: (ancestro-ojo-azul? empty) Con base en lo anterior evaluar (ancestro-ojo-azul? Gustav) a mano, y con DrScheme. Para la evaluación manual, saltarse aquellos pasos de la evaluación que tienen que ver con extraer, comparar y condiciones relacionadas con empty?. También reusar ecuaciones establecidas donde sea posible, especialmente la anterior. Ejercicio 14.1.3 Desarrollar contar-personas, consume un naf y genera el número de personas en el correspondiente árbol familiar. Ejercicio 14.1.4 Desarrollar edad-promedio, consume un naf y el año actual, y genera la edad promedio de todos los individuos en un árbol familiar. Ejercicio 14.1.5 Desarrollar la función color-ojos, consume un naf y genera una lista de todos los colores de ojos en el árbol familiar. Un color de ojos puede ocurrir más de una vez en la lista. Sugerencia: Emplear la operación Scheme append, la cual consume dos listas y produce la concatenación de ambas. 199 Por ejemplo: (append (list ´a ´b ´c) (list ´d ´e)) = (list ´a ´b ´c ´d ´e) Se discute el desarrollo de funciones como append en la sección 17. Ejercicio 14.1.6 Supóngase que se requiere ancestro-propio-ojo-azul?, parecido a ancestro-ojoazul?, pero únicamente responde true cuando propiamente un ancestro tiene ojos azules. El contrato para esta nueva función es el mismo que para la anterior: ;; ancestro-propio-ojo-azul? : naf -> booleano ;; determinar si un-árbol tiene un ancestro ojo-azul (define (ancestro-propio-ojo-azul? un-árbol) …) Los resultados difieren ligeramente. Para apreciar la diferencia, se necesita analizar Eva, quien tiene ojo-azul, pero no tiene un ancestro ojo-azul. De ahí que (ancestro-propio-ojo-azul? Eva) es false. Después de todo Eva no es un ancestro propio de ella misma. Supóngase alguien considera la declaración de propósito y genera esta solución: (define (ancestro-propio-ojo-azul? naf) (cond [(empty? naf) false] [else (or (ancestro-propio-ojo-azul? (hijo-padre naf)) (ancestro-propio-ojo-azul? (hijo-madre naf)))])) ¿Cuál sería el resultado de (ancestro-propio-ojo-azul? A) para cualquier naf A? Corrija la expresión propuesta. 14.2 Ejercicio ampliado: Árboles de búsqueda binaria Se trabaja más con árboles que con árboles familiares. En particular con los bien conocidos denominados árboles de búsqueda binaria, que se emplean en muchas aplicaciones orientadas a almacenar y recuperar información. 200 En particular, árboles binarios que manejan información acerca de personas. En este contexto, un árbol de búsqueda binaria es parecido a un árbol familiar que en lugar de estructuras hijo contiene nodos: (define-struct nodo (nss nombre izquierdo derecho)) De ahí que si decide registrar el número del seguro social, el nombre y otros dos árboles; éstos como si fueran campos parentales de árboles familiares, aunque la relación entre un nodo y su campo izquierdo y derecho no se base en relaciones familiares. Si nss, número del seguro social, nombre es un símbolo e izquierdo y derecho son como los campos de los árboles familiares en un af, que en lugar de estructuras hijo contiene nodos. La correspondiente definición de datos es parecida a la de los árboles familiares: Un árbol binario, AB, es cualquiera de 1. false, o 2. (make-nodo ns pn izq der) donde ns es un número, pn un símbolo, e izq y der son ABs. La opción de false para indicar carencia de información es arbitraria. Se pudo haber empleado empty nuevamente, pero false es igualmente bueno y es más frecuente; Ejemplos de árboles binarios: (make-nodo 15 'd false (make-nodo 24 'i false false)) (make-nodo 15 'd (make-nodo 87 'h false false) false) La figura 38 muestra cómo se debe pensar acerca de tales árboles. Los que se construyen hacia abajo, o sea, con la raíz arriba y las ramas abajo. Cada círculo corresponde a un nodo, etiquetados con su campo ns de una estructura nodo correspondiente. Los árboles omiten false. 201 Ejercicio 14.2.1 Dibuje los dos primeros árboles mencionados, desarrolle contiene-ab?, una función que consume un número y un AB y determina si el número ocurre en el árbol. Ejercicio 14.2.2 Desarrolle busca-ab, consume un número n y un AB, si el árbol contiene una estructura de nodo cuyo campo ns es n, la función genera el valor de pn en ese nodo, en cualquier otro caso genera false. Sugerencia: Emplear contiene-ab. O emplear boolean? para encontrar si busca-ab fue empleada exitosamente en un subárbol. Se discutirá esta segunda técnica, denominada backtracking, en el intermezzo al final de esta parte. Árbol A Árbol B Figura 38. Un árbol de búsqueda binario y un árbol binario Ambos árboles en la figura 38 son árboles binarios pero difieren de forma significativa. Si se leen los números en los dos árboles de izquierda a derecha se obtienen las secuencias: Árbol A Árbol B 10 87 15 15 24 24 29 29 63 63 77 33 89 89 95 95 99 99 La secuencia del árbol A está ordenada de forma ascendente, la de B no. Un árbol binario que tiene una secuencia ordenada de información es un ÁRBOL DE BÚSQUEDA BINARIA. Todo árbol de búsqueda binaria es un árbol binario, pero no todo árbol binario es uno de búsqueda binaria. Se dice que la clase de árboles de búsqueda binaria es una SUBCLASE PROPIA de la de árboles binarios. En concreto, se formula una condición –o invariante de datos– que distingue a los árboles de búsqueda binaria de los árboles binarios: El invariante ABB Un árbol de búsqueda binaria, ABB, es un AB 1. false es siempre un ABB, y 202 2. (make-nodo ns pn izq der) es un ABB si 1. izq y der son ABBs, 2. los nss en izq son menores que ns, y 3. los nss en der son mayores que ns. Las condiciones segunda y tercera son diferentes de cualquier definición de datos vista hasta ahora. Ubican un obstáculo adicional y desacostumbrado en la construcción de ABBs. Ahora se requiere inspeccionar todos los números en dichos árboles y garantizar que son menores (o mayores) que ns. Ejercicio 14.2.3 Desarrolle la función enorden, consume un AB y genera una lista de los nss en el árbol, la lista contiene los números de izquierda a derecha. Sugerencia: Emplear la operación Scheme append, la cual concatena listas: (append (list 1 2 3) (list 4) (list 5 6 7)) ;; se evalúa a (list 1 2 3 4 5 6 7) ¿enorden qué genera en un ABB? Buscar en nodo específico en un ABB es más fácil que en un AB. Para descubrir si un AB contiene un nodo con un campo nss específico, una función puede haber buscado en cada nodo del árbol. En contraste, inspeccionar un árbol de búsqueda binaria requiere menores inspecciones. Supóngase que el ABB dado es (make-nodo 66 'a I D) Si se busca 66, se ha encontrado. Si 63, dado el nodo anterior, únicamente se centraría la búsqueda en I debido a que todos los nodos con nnss menores que 66 están en I. De forma parecida, si se busca 99, se ignora I y hay que centrarse en D debido a que todos los nodos con nnss mayores que 66 están en D. Ejercicio 14.2.4 Desarrolle busca-ABB, consume un número n y un ABB. Si el árbol contiene una estructura de nodo cuyo campo ns es n, la función genera el valor del campo pn en dicho nodo, en caso contrario false. La organización de la función aprovecha el Invariante ABB de modo que la función realiza el menor número de comparaciones posible. Compare buscar en árboles de búsqueda binaria con buscar en listas ordenadas. Construir un árbol binario es fácil; construir un árbol de búsqueda binaria es complicado, además de generador de errores. Para crear un AB se combinan dos ABs, un número nss y un nombre con 203 make-nodo. El resultado es, por definición, un AB. Para crear un ABB, este procedimiento falla debido a que el resultado típicamente no es un ABB. Por ejemplo, si un árbol contiene 3 y 5, y el otro contiene 2 y 6, no hay forma de reunirlos en un solo árbol de búsqueda binaria. Se puede superar este problema (cuando menos) de dos formas. Primero, dada una lista de números y símbolos, se puede determinar manualmente cómo serían los de los correspondientes ABB y emplear make-nodo para construirlo. Segundo, se puede escribir una función que construya un ABB de la lista, un nodo después de otro. Ejercicio 14.2.5 Desarrollar la función crear-abb. Consume un ABB B, un número N, y un símbolo S. Produce un ABB que es como B y que en lugar de un subárbol false contiene la estructura nodo (make-nodo N S false false) Probar la función con (crear-abb false 66 ‘a); lo que debe crear un solo nodo. Luego demuestre que lo siguiente se cumple: (crear-abb (crear-abb false 66 'a) 53 'b) = (make-nodo 66 'a (make-nodo 53 'b false false) false) Finalmente, crear el árbol A de la figura 38 empleando crear-abb. Ejercicio 14.2.6 Desarrollar la función crear-abb-de-lista. Consume una lista de números y nombres; produce un ABB al aplicar de forma repetida crear-abb. La definición de datos para una lista de números y nombres es como sigue. Una lista-de-número/nombre es cualquiera de 1. empty o 2. (cons (list nns nom) ldnn) donde nns es un número, nom un símbolo, y ldnn es una lista-de-número/nombre. Considérense los siguientes ejemplos: 204 (define muestra ‘(99 o) ‘(77 l) ‘(24 i) ‘(10 h) ‘(95 g) ‘(15 d) ‘(89 c) ‘(29 b) ‘ 63 a))) (define muestra (list (list 99 'o) (list 77 'l) (list 24 'i) (list 10 'h) (list 95 'g) (list 15 'd) (list 89 'c) (list 29 'b) (list 63 'a))) Son equivalentes, sin embargo, el de la izquierda es definido con la abreviación quote, el de la derecha empleando list. El árbol de la izquierda en la figura 38 es el resultado de emplear crearabb-de-lista en dicha lista. 14.3 Listas en listas La World Wide Web, o simplemente “la Web”, se ha convertido en la parte más interesante de Internet, una red global de computadoras. De forma burda, se puede afirmar que la web es una colección de páginas web. Cada una de éstas es una secuencia de palabras, gráficas, películas y mensajes de audio, entre otras cosas; lo más importante es que, las páginas web contienen ligas a otras páginas web. Un navegador web permite a la gente ver páginas web, presenta una página web como una secuencia de palabras, imágenes, etc. Algunas palabras en una página pueden ir subrayadas. Dando click a palabras subrayadas conduce a una nueva página web. Los más modernos navegadores también proporcionan un generador de páginas web. Estas herramientas ayudan a la gente a crear colecciones de páginas web. Un generador, entre otras cosas, puede buscar palabras o reemplazar una palabra por otra. O sea, páginas web son cosas que se debe poder representar en una computadora, y existen muchas funciones que procesan páginas web. 205 Una simplificación es considerar a una página web compuesta por palabras y otras páginas web anidadas. Una forma de comprender una página es como una secuencia de palabras y páginas web. Esta descripción informal sugiere una representación natural de páginas web como una lista de símbolos, la cual representa palabras, y páginas web, las cuales representan páginas web anidadas. Después de todo, se ha enfatizado previamente que una lista puede contener diferentes clases de cosas. Sin embargo, cuando se detalla esta idea como una definición de datos, se obtiene algo raro. Una página-web, PW, es o 1. empty, 2. (cons s pw) donde s es un símbolo y pw es una página web; o 3. (cons pwe pw) donde ambas pwe y pw son páginas web. Esta definición de datos difiere de otras de listas de símbolos en que tienen tres cláusulas en lugar de dos y que tiene tres auto-referencias en lugar de una. De todas estas auto-referencias, la primera al inicio de una lista construida es la más rara. Se denominan dichas páginas web como inmediatamente empotradas. Debido a lo raro de la definición de datos, se construyen algunos ejemplos de páginas web antes de continuar. Una página web sencilla es: '(The TeachScheme! Project aims to improve the problem-solving and organization skills of high school students. It provides software and lecture notes as well as exercises and solutions for teachers.) contiene sólo palabras, una página compleja es: '(The TeachScheme Web Page Here you can find: (LectureNotes for Teachers) (Guidance for (DrScheme: a Scheme programming environment)) (Exercise Sets) (Solutions for Exercises) For further information: write to scheme@cs) Las páginas inmediatamente empotradas inician con paréntesis y los símbolos 'LectureNotes, 'Guidance, 'Exercises, y 'Solutions. La segunda página web empotrada contiene otra página empotrada, la cual inicia con ´DrScheme. Se dice que esta página está completamente empotrada con respecto a la página entera. 206 Si se desarrolla la función tamaño, que consume una página web y genera el número de palabras que contiene, así como el de las páginas incrustadas. tamaño : pw -> número contar el número de símbolos que ocurre en una-pw (define (tamaño una-pw) ... ) Las dos páginas web de arriba sugieren dos buenos ejemplos, pero son demasiado complejas. A continuación se mencionan tres ejemplos, uno por subclase de datos: (= (tamaño empty) 0) (= (tamaño (cons ´Uno empty))} 1) (= (tamaño (cons (cons ´Uno empty) empty)) 1) Los primeros dos son obvios. El último requiere una pequeña explicación. Es una página web que contiene una página incrustada inmediatamente, y nada más. La página web incrustada es una del segundo ejemplo, y contiene uno y solo un símbolo del tercer ejemplo. Para desarrollar el formato para tamaño, avancemos cuidadosamente a través de la receta de diseño. La forma de la definición de datos sugiere que se requieren tres cond-cláusulas: una para la página empty, otra para la que inicie con un símbolo, y otra para una página que inicia con una página incrustada. Mientras la primera condición es la prueba familiar para empty, la segunda y la tercera requieren una inspección más minuciosa debido a que ambas cláusulas en la definición de datos emplean cons, y un simple cons? no distinguirá las dos formas de datos. Si la página no es empty, es construida, y la característica distintiva es el primer ítem en la lista. En otras palabras, la segunda condición debe emplear un predicado que pruebe el primer ítem en una-pw: ;; tamaño : PW -> número ;; contar el número de símbolos que ocurren en una-pw (define (tamaño una-pw) ... ) (cond [(empty? una-pw) ...] [(symbol? (first una-pw)) ... (first una-pw) ... (tamaño (rest una-pw)) ...] [else ... (tamaño (first una-pw)) ... (tamaño (rest una-pw)) ...])) 207 El resto del formato es como siempre. La segunda y tercera cláusula cond contienen expresiones selector para el primer ítem y el resto de la lista. Debido a que (rest una-pw) es siempre una página web y debido a que (first un-pw) es uno en el tercer caso, se agrega una llamada recursiva a tamaño para dichas expresiones selector. Empleando los ejemplos y el formato, estamos listos para diseñar tamaño: véase figura 39. Las diferencias entre la definición y el formato son mínimas, lo cual muestra nuevamente cómo mucho de una función puede diseñarse pensando sistemáticamente acerca de las definiciones de datos de sus entradas. ;; tamaño : PW -> número ;; contar el número de símbolos que ocurren en una-pw (define (tamaño una-pw) ... ) (cond [(empty? una-pw) 0] [(symbol? (first una-pw)) (+ 1 (tamaño (rest una-pw)))] [else (+ tamaño (first una-pw)) (tamaño (rest una-pw)))])) Figura 39. Definición de tamaño para páginas web Ejercicio 14.3.1 Explicar brevemente cómo definir tamaño al emplear su formato y los ejemplos. Probarla empleando los ejemplos previos. Ejercicio 14.3.2 Desarrolle la función ocurre1, que consume una pw y un símbolo, generando el número de veces que el símbolo ocurre en la pw, ignorando las pw anidadas. Desarrolle ocurre2, donde no ignora la ocurrencia del símbolo en las páginas anidadas. Desarrolle la función ocurre2. Es como ocurre1, pero cuenta todas las ocurrencias de un símbolo, incluyendo las páginas incrustadas. Ejercicio 14.3.3 Desarrolle la función reemplaza, consume dos símbolos, nuevo y viejo, así como una-pw, generando una página idéntica a una-pw con todas las ocurrencias de viejo reemplazadas por nuevo. Ejercicio 14.3.4 No son agradables árboles web profundos, pues requieren muchos cambios de página para llegar a la información útil, de ahí la necesidad de medir la profundidad de una página. Una profundidad 0 de una página implica que ésta maneja únicamente símbolos; una página con otra página web inmediatamente incorporada, tiene la profundidad de ésta más 1; si una página tiene varias páginas inmediatamente incorporadas, su profundidad es la profundidad máxima de 208 las páginas incrustadas más uno. Desarrolle profundidad, la cual consume una pw y calcula su profundidad. 14.4 Ejercicio ampliado: Evaluar Scheme DrScheme es un programa que consiste en varias partes. Una función verifica si las definiciones y expresiones escritas son expresiones gramaticales Scheme, otra evalúa dichas expresiones. Es factible desarrollar versiones simples de tales funciones. Lo primero que se requiere desarrollar es una representación para los programas Scheme. En otras palabras, se debe representar una expresión Scheme como una pieza de datos Scheme. Supóngase que para empezar se desea representar números, variables, sumas y multiplicaciones; los números pueden representar números y los símbolos, variables; sumas y multiplicaciones, sin embargo, requieren una clase de datos compuestos, ya que constan de un operador y dos subexpresiones. Una forma directa de representar sumas y multiplicaciones es emplear dos estructuras: una para sumas y otra para multiplicaciones. Dichas definiciones de estructuras son: (define-struct sum (izquierda derecha) (define-struct mul (izquierda derecha) Analizar algunos ejemplos: Dichos ejemplos cubren todos los casos: números, variables, expresiones simples, y expresiones anidadas. Expresión Scheme 3 X (* 3 10) representación de una expresión Scheme 3 'x (make-mul 3 10) (+ (* 3 3) (* 4 4)) (make-sum (make-mul 3 3) (make-mul 4 4)) (+ (* x x) (* y y)) (make-sum (make-mul 'x 'x) (make-mul 'y 'y)) (* 1/2 (* 3 3)) (make-mul 1/2 (make-mul 3 3)) Ejercicio 14.4.1 Desarrollar definiciones de datos para la representación de expresiones Scheme. Luego traducir las siguientes expresiones en representaciones: 209 1. (+ 10 -10) 2. (+ (* 20 3) 33) 3. (* 3.14 (* r r)) 4. (+ (* 9/5 c) 32) 5. (+ (* 3.14 (* o o)) (* 3.14 (* i i))) Un evaluador Scheme es una función que consume una representación de una expresión Scheme y produce su valor. Por ejemplo, la expresión 3 tiene el valor 3, (+ 3 5) tiene el valor 8, (+ (* 3 3) (* 4 4)) tiene el valor 25, etc. Puesto que por ahora se ignoran las definiciones, una expresión que contiene una variable, por ejemplo, (+ 3 x), no tiene un valor; después de todo, no se sabe qué representa la variable. O sea, nuestro evaluador Scheme debe ser aplicado únicamente a expresiones que no contienen variables. Se dice que son expresiones numéricas. Ejercicio 14.4.2 Desarrollar numérica?, que consume la representación de una expresión Scheme y determina si es numérica. Ejercicio 14.4.3 Proporcionar una definición de datos para expresiones numéricas. Desarrollar la función evaluar-expresión. La función consume (la representación de) una expresión Scheme numérica y calcula su valor. Cuando se haya probado la función, modificarla para que consuma todo tipos de expresiones Scheme; la versión revisada marca un error cuando encuentra una variable. Ejercicio 14.4.4 Cuando alguien evalúa una aplicación (f a) sustituye a por los parámetros de f en el cuerpo de f. De forma más general, cuando se evalúan expresiones con variables, se sustituyen variables con valores. Desarrollar la función subst. Consume (la representación de) una variable (V), un número (N), y (la representación de ) una expresión Scheme. Produce una expresión estructuralmente equivalente en la cual todas las ocurrencias de V son sustituidas por N. 210 SECCIÓN 15 Definiciones de datos mutuamente referidas En la sección precedente, se desarrollaron representaciones de árboles familiares, páginas web, y expresiones Scheme. Desarrollar funciones para dichas definiciones de datos se apoyó en una y la misma receta de diseño. Si se desea desarrollar representaciones más realistas de páginas web o expresiones Scheme, o se desea estudiar árboles familiares de descendientes en lugar de ancestros, se debe aprender a describir clases de datos que estén interrelacionados. O sea, se deben formular varias definiciones de datos a la vez, donde las definiciones de datos no sólo se refieren a sí mismas, sino a otras definiciones de datos. 15.1 Listas de estructuras, listas en estructuras Cuando se construye un árbol familiar de forma retroactiva, a menudo se inicia desde la perspectiva de un hijo procediéndose a partir del mismo, con los padres, abuelos, etc. En la medida en que se construye el árbol, se escribe quién es hijo de quién, más que quiénes son sus padres. Se escribe un árbol familiar de descendientes. Dibujar un árbol de descendientes es parecido a dibujar uno de ancestros, excepto en que las flechas apuntan en dirección contraria. La figura 40 representa la misma familia de la figura 35, pero dibujado desde la perspectiva de los descendientes. Representar estos nuevos tipos de árboles familiares y sus nodos en computadora requiere una clase diferente de datos que las de árboles familiares de ancestros. Esta vez un nodo debe incluir información acerca de los hijos en lugar de los dos padres. La definición de estructura es: (define-struct padre (hijos nombre fecha ojos)) Los tres últimos campos en una estructura padre contienen la misma información básica que la correspondiente estructura hijo, pero los contenidos del primero plantean un aspecto interesante. Puesto que un padre puede tener un número arbitrario de hijos, el campo hijos debe contener un número indeterminado de nodos, cada uno representando un hijo. 211 Carl (1926) Eyes: green Bettina (1926) Eyes: green Adam (1950) Eyes: yellow Dave (1955) Eyes: black Eva (1965) Eyes: blue Fred (1966) Eyes: pink Gustav (1988) Eyes: brown Figura 40. Un árbol familiar de descendientes La opción natural es insistir en que el campo hijos siempre representa una lista de estructuras padre. La lista representa los hijos; si una persona no tiene hijos, la lista es empty. Lo que sugiere la siguiente definición de datos: Un padre es una estructura: (make-padre ldh n f o) donde ldh, lista de hijos; n y o son símbolos, y f es un número. Sin embargo, esta definición de datos viola el criterio referente a definiciones. En particular, menciona el nombre de una colección no definida aún: lista de hijos. Puesto que es imposible definir la clase de padres sin conocer qué es una lista de hijos, habría que empeza por ésta. Una lista de hijos es cualquiera de: 1. empty o 2. (cons p ldh) donde p es un padre y ldh es una lista de hijos. Esta segunda definición parece estándar, sin embargo, es afectada con el mismo problema que la de padres. La clase desconocida a la que se refiere es la de padres, la cual no se puede definir sin la definición de la lista de hijos, etc. 212 La conclusión es que las definiciones de datos se refieran una a otra y únicamente son significativas si se introducen juntas. Un padre es una estructura (make-padre ldh n f o) donde ldh, lista de hijos; n y o es símbolo, y f es un número. Una lista-de-hijos es cualquiera de 1. empty o 2. (cons p ldh) donde p es un padre y ldh es una lista de hijos. Cuando dos, o más, definiciones de datos se refieren cada una a la otra, se dice que son MUTUAMENTE RECURSIVAS o MUTUAMENTE REFERENCIALES. Ahora se puede traducir el árbol familiar de la figura 40 al lenguaje de datos de Scheme. Por supuesto, antes de crear una estructura padre se deben primero definir todos los nodos que representan hijos. Y, como en la sección 14.1, la mejor manera de hacerlo es nombrar la estructura padre antes de reusarla en la lista de hijos. Por ejemplo: (define Gustav (make-padre empty 'Gustav 1988 'brown)) (make-padre (list Gustav) 'Fred 1966 'pink) Para crear una estructura padre para Fred, se requiere primero definir una para Gustav de modo de formar (list Gustav), la lista de hijos de Fred. La figura 41 contiene la representación Scheme completa del árbol de descendientes. Para evitar repeticiones, se incluyen definiciones para listas de hijos. Compare las definiciones con la figura 36, la cual representa la misma familia como árbol de ancestros. Analizando el desarrollo de descendiente-ojo-azul?, la compañera natural de ancestro-ojo-azul? Consume una estructura padre y determina si ésta o cualquiera de sus descendientes tiene ojos azules. ;; descendiente-ojo-azul? : padre -> booleano ;; determinar si un-padre o cualquiera de sus descendientes (hijos, nietos, etc) tiene ‘blue ;; en el campo ojos (define (descendiente-ojo-azul? un-padre) …) 213 A continuación tres ejemplos simples, formulados como pruebas: (boolean=? (descendiente-ojo-azul? Gustav) false) (boolean=? (descendiente-ojo-azul? Eva) true) (boolean=? (descendiente-ojo-azul? Bettina) true) ;; Generación más joven (define Gustav (make-padre empty 'Gustav 1988 'brown)) (define Fred&Eva (list Gustav)) ;; Generación intermedia: (define Adam (make-padre empty 'Adam 1950 'yellow)) (define Dave (make-padre empty 'Dave 1955 'black)) (define Eva (make-padre Fred&Eva 'Eva 1965 'blue)) (define Fred (make-padre Fred&Eva 'Fred 1966 'pink)) (define Carl&Bettina (list Adam Dave Eva)) ;; Generación mayor (define Carl (make-padre Carl&Bettina 'Carl 1926 'green)) (define Bettina (make-padre Carl&Bettina 'Bettina 1926 'green)) Figura 41. Representación Scheme de un árbol familiar de descendientes Un examen de la figura 40 explica las respuestas en cada caso. Conforme a nuestras reglas, el formato para descendiente-ojo-azul? es simple. Puesto que su entrada es una clase sencilla de estructuras, el formato contiene únicamente expresiones selector para los campos en la estructura: (define (descendiente-ojo-azul? un-padre) …(padre-hijos un-padre) … … (padre-nombre un-padre) … … (padre-fecha un-padre) … … (padre-ojos un-padre) … ) La definición de estructura para padre especifica cuatro campos, de ahí las cuatro expresiones. 214 Las expresiones en el formato nos recuerdan que el color de ojos del padre está disponible y puede ser verificado. De ahí que se añada una cond-expresión que compare (padre-ojos un-padre) a ‘azul: (define (descendiente-ojo-azul? un-padre) (cond [(symbol=? (padre-ojos un-padre) ‘blue) true] [else …(padre-hijos un-padre) … … (padre-nombre un-padre) … … (padre-fecha un-padre) … ] ) ) La respuesta es true si el condicional se cumple. La cláusula else contiene las expresiones remanentes. Los campos nombre y fecha no tienen nada que ver con el color de ojos de una persona, por lo que se ignoran. Lo que nos deja con (padre-hijos un-padre) una expresión que extrae la lista de hijos de la estructura padre. Si el color de ojos de alguna estructura padre no es ‘blue, es evidente que se debe buscar en la lista de hijos un descendiente ojo-azul. Siguiendo nuestra guía para funciones complejas, se agrega la función a nuestra lista de deseos y se continúa. La función que se quiere en la lista de deseos consume una lista de hijos y verifica si cualquiera de ellos o sus nietos tienen ojos azules. A continuación el contrato, encabezado y declaración de propósito: ;; hijos-ojo-azul? : lista-de-hijos -> booleano ;; determinar si cualquiera de las estructuras en uldh tiene ojo-azul ;; o tiene algún descendiente ojo-azul (define (hijos-ojo-azul? uldh) …) Empleando hijos-ojo-azul? se puede completar la definición de descendiente-ojo-azul? (define (descendiente-ojo-azul? un-padre) (cond [(symbol=? (padre-ojos un-padre) ‘blue) true] [else (hijos-ojo-azul? (padre-hijo un-padre))])) O sea, si un-padre no tiene ojos azules, se busca en la lista de sus hijos. Antes de poder probar descendiente-ojo-azul?, se debe definir la función de la lista de deseos. Para elaborar ejemplos y pruebas para hijos-ojo-azul?, se emplean las definiciones de la lista-dehijos de la figura 41. 215 (not (hijos-ojo-azul? (list Gustav))) (hijos-ojo-azul? (list Adam Dave Eva)) Gustav no tiene ojos azules y no tiene registrados descendientes. Por lo que, hijos-ojo-azul? produce false para (list Gustav). En cambio, Eva tiene ojos azules, por lo que hijos-ojo-azul? produce true para la segunda lista de hijos. Puesto que la entrada para hijos-ojo-azul? es una lista, el formato es el patrón estándar: (define (hijos-ojo-azul? uldh) (cond [(empty? uldh) …] [else … (first uldh) … … (hijos-ojo-azul? (rest uldh)) …])) Ahora se consideran los dos casos. Si la entrada de hijos-ojo-azul? es empty, la respuesta es false. De otra forma se tienen dos expresiones: 1. (first uldh), la cual extrae el primer ítem, una estructura padre, de la lista; y 2. (hijos-ojo-azul? (rest uldh)), la cual determina si cualquiera de las estructuras en uldh es ojoazul o tiene algún descendiente ojo-azul. Afortunadamente se cuenta con una función que determina si una estructura padre o cualquiera de sus descendientes tiene ojos azules: descendiente-ojo-azul?. Esto sugiere que se verifique si (descendiente-ojo-azul? (first uldh)) se cumple y, en caso afirmativo, puede producir true. Si no, la segunda expresión determina si se tiene más suerte con el resto de la lista. La figura 42 contiene las definiciones completas para ambas funciones: descendiente-ojo-azul? e hijos-ojo-azul? A diferencia de otros grupos de funciones, estas funciones se refieren una a la otra. Son Mutuamente Recursivas. No sorprende que las referencias mutuas en las definiciones se correspondan con las referencias mutuas en las definiciones de datos. La figura contiene también un par de definiciones opcionales que emplean or en lugar de cond-expresiones anidadas. Ejercicio 15.1.1 Evaluar manualmente (descendiente-ojo-azul? Eva). Luego evaluar (descendiente-ojo-azul? Bettina) 216 Ejercicio 15.1.2 Desarrollar qué-tan-lejos. Determina qué tan lejos está un descendiente con ojos azules, si existe, de un determinado padre. Si el padre tiene ojos azules, la distancia es 0; si no tiene ojos azules, pero alguno de los hijos sí, la distancia es 1, etc. Si ningún descendiente de un padre determinado tiene ojos azules, la función devuelve false cuando se aplica al correspondiente árbol familiar. ; descendiente-ojo-azul? : padre -> boolean ;; determinar si un-padre o cualquiera de sus descendientes (hijos, nietos, ;; etc) tiene ‘blue en el campo ojos (define (descendiente-ojo-azul? un-padre) (cond [(symbol=? (padre-ojos un-padre) ‘blue) true] [else (hijos-ojo-azul? (padre-hijos un-padre))])) ;; hijos-ojo-azul? : lista-de-hijos -> boolean ;; determinar si cualquiera de las estructuras en uldh tiene ojo-azul ;; o tiene algún descendiente ojo-azul (define (hijos-ojo-azul? uldh) (cond [(empty? uldh) false] [else (cond [(descendiente-ojo-azul? (first uldh) true] [else (hijos-ojo-azul? (rest uldh))])])) ; descendiente-ojo-azul? : padre -> boolean ;; determinar si un-padre o cualquiere de sus descendientes (hijos, nietos, ;; etc) tiene ‘blue en el campo ojos (define (descendiente-ojo-azul? un-padre) (or (symbol=? (padre-ojos un-padre) ‘blue) (hijos-ojo-azul? (padre-hijo un-padre)))) ;; hijos-ojo-azul? : lista-de-hijos -> boolean ;; determinar si cualquiera de las estructuras en uldh tiene ojo-azul ;; o tiene algún descendiente ojo-azul (define (hijos-ojo-azul? uldh) (cond [(empty? uldh) false] [else (or (descendiente-ojo-azul? (first uldh)) (hijos-ojo-azul? (rest uldh)))])) Figura 42. Dos programas para encontrar un descendiente ojo-azul 217 Ejercicio 15.1.3 Desarrollar la función cuenta-descendientes, la cual consume un padre y produce el número de descendientes, incluyendo al padre. Desarrollar la función cuenta-descendientes-propios, la cual consume un padre y produce el número de los que son propiamente descendientes, o sea, los nodos en el árbol familiar excluyendo al padre (o madre). Ejercicio 15.1.4 Desarrollar la función color-ojos, la cual consume un padre y produce una lista de todos los colores de ojos en el árbol. Un color de ojo puede ocurrir más de una vez en la lista. Sugerencia: Emplear la operación Scheme append, la cual consume dos listas y produce su concatenación. 15.2 Diseño de funciones para definiciones mutuamente referidas La receta para el diseño de funciones con base en definiciones de datos mutuamente referidas generaliza la de datos auto-referenciales. De hecho, ofrece únicamente dos recomendaciones adicionales. Primero, se deben crear simultáneamente varios formatos, uno para cada definición. Segundo, se deben anotar los formatos con auto-referencias y REFERENCIAS CRUZADAS, o sea, referencias entre diferentes formatos. A continuación una explicación más detallada de las diferencias. El análisis y diseño de datos Si el problema menciona un número de diferentes clases de información (de tamaño arbitrario), se requiere un grupo de definiciones de datos que sean auto-referenciales y que se refieran cada una a la otra. En dichos grupos, se identifican las auto-referencias y las referencias cruzadas entre dos definiciones de datos. En el ejemplo previo, se requieren dos definiciones interrelacionadas: Una estructura padre (make-padre ldh n f o) donde ldh es una lista de hijos, n y o son símbolos, y f es un número, Una lista de hijos es cualquiera de 1. empty o 2. (cons p ldh) donde p es un padre y ldh es una lista de hijos 218 La primera tiene que ver con padres y el otro con listas de hijos. La primera (incondicionalmente) define un padre en términos de símbolos, números y una lista de hijos, o sea, contiene una referencia cruzada a la segunda condición. Ésta es una definición condicional; su primera cláusula es simple, su segunda cláusula referencia tanto la definición para padres como para lista-de-hijos. Contrato, propósito, encabezado Para procesar clases de datos interrelacionadas, se requieren tantas funciones como clases de definiciones. De ahí que, se deban formular tantos contratos, declaraciones de propósito, y encabezados en paralelo como definiciones de datos. Formatos Los formatos se crean en paralelo, siguiendo las recomendaciones relacionadas con datos compuestos, datos mezclados, y datos auto-referenciales. Finalmente, se deben determinar para cada expresión selector en cada formato si corresponde con una referencia cruzada de alguna definición. Si es el caso, anotarse en la misma forma que se anotan referencias cruzadas. A continuación los formatos para el ejemplo (define (fun-padre un-padre) …(padre-nombre un-padre) …(padre-fecha un-padre) …(padre-ojos un-padre) …(fun-hijos (padre-hijos un-padre) (define (fun-hijos uldh) (cond [(empty? uldh) …] [else …(fun-padre (first uldh)…(fun-hijos (rest uldh)) …])) El formato fun-padre no tiene condiciones debido a que la definición de datos para padre no contiene ninguna cláusula. Contiene una referencia cruzada al segundo formato; para procesar el campo children de la estructura padre. Conforme las mismas reglas, funchildren es condicional. La segunda cond-cláusula contiene una auto-referencia, para el rest de la lista, y una referencia cruzada para el first ítem de la lista, la cual es una estructura padre. 219 Una comparación de las definiciones de datos y los formatos muestra qué tan parecidas son. Para enfatizar la similaridad entre auto-referencias y referencias cruzadas, las definiciones de datos y formatos han sido marcados con flechas. Es fácil ver cómo las flechas correspondientes tienen el mismo origen y destino en los dos dibujos. El cuerpo Conforme se procede para crear las definiciones finales, se inicia con un formato o una cond-cláusula que no contiene auto-referencias al formato ni referencias cruzadas a otros formatos. Los resultados son típicamente fáciles de formular para dichos formatos o condcláusulas. En el resto de este paso se procede como antes. Cuando se tiene que operar con otras cláusulas o funciones, se recuerda qué calcula cada expresión en el formato, suponiendo que todas las funciones operan según se ha especificado en los contratos. Entonces se decide cómo combinar dichas piezas de datos en una respuesta final. Conforme se realiza esto, hay que tener presentes las orientaciones relacionadas con la composición de funciones complejas (secciones 7.3 y 12). La figura 43 sintetiza la receta de diseño ampliada. FASE Análisis y diseño de datos Formato Cuerpo META ACTIVIDAD Formular un Desarrollar un grupo de definiciones de datos mutuamente recursivas grupo de •cuando menos una definición o una opción en una definición debe definiciones referirse a datos básicos •identificar explícitamente todas las referencias de datos entre definiciones de datos relacionadas Formular un Desarrollar simultáneamente tantos formatos como definiciones de datos grupo de existen •desarrollar cada formato según corresponda con las reglas para esquemas de definiciones de datos compuestos y/o mezclados •marcar formatos con función recursiones y aplicaciones cruzadas de modo que coincidan con las referencias cruzadas en las definiciones de datos Definir un Formular una expresión Scheme para cada formato, y para cada condgrupo de cláusula en un formato •explicar qué calcula cada expresión en cada funciones formato •usar funciones auxiliares donde sea necesario Figura 43. Pasos esenciales en el diseño de grupos de funciones para grupos de definiciones de datos; para otros casos véase figuras 4, 12 y 18 15.3 Ejercicio ampliado: Más en páginas Web Con definiciones de datos mutuamente referenciales se pueden representar páginas web en forma más precisa que en la sección 14.3. A continuación la definición de estructura básica: 220 (define-struct pw (encabezado cuerpo)) Los dos campos contienen dos piezas esenciales de datos en una página web: un encabezado y un cuerpo. La definición de datos especifíca que un cuerpo es una lista de palabras y páginas web. Una Página Web, PW, es una estructura (make-pw e p) donde e es un símbolo y p es un documento (web). Un documento (Web) es cualquiera de 1. empty, 2. (cons s p) donde s es un símbolo y p es un documento, o 3. (cons w p) donde w es una página Web y p es un documento. Ejercicio 15.3.1 Desarrollar la función tamaño, que consume una página web y produce el número de símbolos (palabras) que contiene. Ejercicio 15.3.2 Desarrollar la función pw-a-archivo. La función consume una página web y produce una lista de símbolos. La lista contiene todas las palabras en un cuerpo y todos los encabezados de páginas web empotradas. Los cuerpos de las páginas web inmediatamente empotradas se ignoran. Ejercicio 15.3.3 Desarrollar la función ocurre. Consume un símbolo y una página web y determina si el primero ocurre en cualquier lugar de la segunda, incluyendo las páginas web empotradas. Ejercicio 15.3.4 Desarrollar el programa encuentra. Consume una página web y un símbolo. Produce false, si el símbolo no ocurre en el cuerpo de la página o de sus páginas empotradas. Si el símbolo ocurre cuando menos una vez, produce una lista de los encabezados encontrados en el camino al símbolo. Sugerencia: Defina una auxiliar como encuentra que produzca únicamente true cuando una página web contenga la palabra encontrada. Emplearla para definir encuentra. Otra opción, emplear boolean? para determinar si una recursión natural de encuentra produce una lista o un booleano. Calcule entonces el resultado otra vez. Se discutirá esta segunda técnica, denominada backtracking, en el intermezzo al final de esta parte. 221 222