Tema 4: Procedimientos y estructuras recursivas Contenidos 2. Listas estructuradas 2.1. Definición y ejemplos 2.2. Pseudo árboles con niveles 2.3. Funciones recursivas sobre listas estructuradas 2.4. Ejercicios 3. Datos funcionales y barreras de abstracción 3.1. Datos funcionales 3.2. Barrera de abstracción 3.3. Números racionales 2. Listas estructuradas Hemos visto temas anteriores que las listas en Scheme se implementan como un estructura de datos recursiva, formadas a partir de parejas y de una lista vacía. Una vez conocida su implementación, vamos a volver a estudiar las listas desde un nivel de abstracción alto, usando las funciones car y cdr para obtener el primer elemento y el resto de la lista y la función cons para añadir un nuevo elemento a su cabeza. En la mayoría de funciones y ejemplos que hemos visto hasta ahora las listas están formadas por datos y el recorrido por la lista es un recorrido lineal, una iteración por sus elementos. En este apartado vamos a ampliar este concepto y estudiar cómo trabajar con listas que contienen otras listas. 2.1. Definición y ejemplos Las listas en Scheme pueden tener cualquier tipo de elementos, incluido otras listas. Llamaremos lista estructurada a una lista que contiene otras sublistas. Lo contrario de lista estructurada es una lista plana, una lista formada por elementos que no son listas. Llamaremos hojas a los elementos de una lista que no son sublistas. Por ejemplo, la lista estructurada: '(a b (c d e) (f (g h))) es una lista estructurada con 4 elementos: El elemento 'a , una hoja El elemento 'b , otra hoja La lista plana '(c d e) La lista estructurada '(f (g h)) Una lista formada por parejas la consideraremos una lista plana, ya que no contiene ninguna sublista. Por ejemplo, la lista ’((a . 3) (b . 5) (c . 12)) es una lista plana de tres elementos (hojas) que son parejas. 2.1.1. Definiciones en Scheme Vamos a escribir las definiciones anteriores en código de Scheme. Un dato es una hoja si no es una lista: (define (hoja? dato) (not (list? dato))) Una definición recursiva de lista plana: Una lista es plana si y solo si el primer elemento es una hoja y el resto es plana. Y el caso base: Una lista vacía es plana. En Scheme: (define (plana? lista) (or (null? lista) (and (hoja? (car lista)) (plana? (cdr lista))))) Una lista es estructurada cuando alguno de sus elementos es otra lista: (define (estructurada? lista) (if (null? lista) #f (or (list? (car lista)) (estructurada? (cdr lista))))) Ejemplos: (plana? '(a b c d e f)) #t (plana? '((a . 1) (b . 2) (c . 3))) #t (plana? '(a (b c) d)) #f (plana? '(a () b)) #f (estructurada? '(1 2 3 4)) #f (estructurada? '((a . 1) (b . 2) (c . 3))) (estructurada? '(a () b)) #t (estructurada? '(a (b c) d)) #t #f Realmente bastaría con haber hecho una de las dos definiciones y escribir la otra como la negación de la primera: (define (estructurada? lista) (not (plana? lista))) O bien: (define (plana? lista) (not (estructurada? lista))) 2.1.2. Ejemplos de listas estructuradas Las listas estructuradas son muy útiles para representar información jerárquica en donde queremos representar elementos que contienen otros elementos. Por ejemplo, las expresiones de Scheme son listas estructuradas: '(= 4 (+ 2 2)) '(if (= x y) (* x y) (+ (/ x y) 45)) '(define (factorial x) (if (= x 0) 1 (* x (factorial (- x 1))))) El análisis sintáctico de una oración puede generar una lista estructurada de símbolos, en donde se agrupan los distintos elementos de la oración: '((Juan) (compró) (la entrada (de los Miserables)) (el viernes por la tarde) ) Una página HTML, con sus distintos elementos, unos dentro de otros, también se puede representar con una lista estructurada: '((<h1> Mi lista de la compra </h1>) (<ul> (<li> naranjas </li>) (<li> tomates </li>) (<li> huevos </li>) </ul>)) 2.2. Pseudo árboles con niveles Las listas estructuradas definen una estructura de niveles, donde la lista inicial representa el primer nivel, y cada sublista representa un nivel inferior. Los datos de las listas representan las hojas. Por ejemplo, la representación en forma de niveles de la lista '((1 2 3) 4 5) es la siguiente: La estructura no es un árbol propiamente dicho, porque todos los datos están en las hojas. Otro ejemplo. ¿Cuál sería la representación en niveles de la siguiente lista estructurada?: '(let ((x 12) (y 5)) (+ x y))) 2.2.1 Altura de una lista estructurada La altura de una lista estructurada viene dada por su número de niveles: una lista plana tiene una altura de 1, la lista anterior tiene una altura de 2. Veamos una definición más precisa. altura(lista vacía) = 0 altura(hoja) = 0 altura(lista) = max (1 + altura (primer-elemento (lista)), altura (resto (lista)) En Scheme: (define (altura lista) (cond ((null? lista) 0) ((hoja? lista) 0) (else (max (+ 1 (altura (car lista))) (altura (cdr lista)))))) Por ejemplo: (altura '(1 (2 3) 4)) 2 (altura '(1 (2 (3)) 3)) 3 Hay que hacer notar que en la función anterior que el parámetro lista va a aceptar tanto hojas como listas. 2.2.2. Implementación de altura con map Es posible definir la función utilizando las función de orden superior map para aplicar la propia función que estamos definiendo a los elementos de la lista: (define (altura lista) (cond ((null? lista) 0) ((hoja? lista) 0) (else (+ 1 (apply max (map altura lista)))))) 2.3. Funciones recursivas sobre listas estructuradas Vamos a diseñar distintas funciones recursivas que trabajan con la estructura jerárquica de las listas estructuradas. (num-hojas lista) : cuenta las hojas de una lista estructurada (pertenece? lista) : busca una hoja en una lista estructurada (cuadrado-lista lista) : eleva todas las hojas al cuadrado (suponemos que la lista estructurada contiene números) (map-lista f lista) : similar a map, aplica una función a todas las hojas de la lista estructurada y devuelve el resultado (otra lista estructurada) 2.3.1. num-hojas Cuenta el número de hojas de una lista estructurada. Definición matemática: num-hojas(lista vacía) = 0 num-hojas(hoja) = 1 num-hojas(lista) = num-hojas (primer-elemento (lista)) + num-hojas (resto (lista)) Implementación en Scheme: (define (num-hojas lista) (cond ((null? lista) 0) ((hoja? lista) 1) (else (+ (num-hojas (car lista)) (num-hojas (cdr lista)))))) Por ejemplo: (num-hojas '(1 2 (3 4 (5) 6) (7))) Con map y apply : 7 (define (num-hojas (cond ((null? lista) ((hoja? lista) (else (apply + lista) 0) 1) (map num-hojas lista))))) 2.3.2. pertenece? Comprueba si el dato x aparece en la lista estructurada. (define (pertenece? x lista) (cond ((null? lista) #f) ((hoja? lista) (equal? x lista)) (else (or (pertenece? x (car lista)) (pertenece? x (cdr lista)))))) Ejemplos: (pertenece? 'a '(b c (d (a)))) #t (pertenece? 'a '(b c (d e (f)) g)) #f 2.3.3. cuadrado-lista Devuelve una lista estructurada con la misma estructura y sus números elevados al cuadrado. (define (cuadrado-lista lista) (cond ((null? lista) '()) ((hoja? lista) (* lista lista)) (else (cons (cuadrado-lista (car lista)) (cuadrado-lista (cdr lista)))))) Por ejemplo: (cuadrado-lista '(2 3 (4 (5)))) (4 9 (16 (25)) 2.3.4. map-lista Devuelve una lista estructurada igual que la original con el resultado de aplicar a cada uno de sus hojas la función f (define (map-lista f lista) (cond ((null? lista) '()) ((hoja? lista) (f lista)) (else (cons (map-lista f (car lista)) (map-lista f (cdr lista)))))) Por ejemplo: (map-lista (lambda (x) (* x x)) '(2 3 (4 (5)))) (4 9 (16 (25)) 2.4. Ejercicios Planteamos a continuación algunos ejercicios para que practiquéis las funciones recursivas sobre las listas estructuradas. tres-elementos? Escribe la función (tres-elementos? lista) que tome una lista estructurada como argumento. Devolverá #t si cada subsista que aparece y cada uno de sus elementos tienen 3 elementos. Suponemos que nunca se llamará a la función con una lista vacía. Ejemplos: (tres-elementos? (tres-elementos? (tres-elementos? (tres-elementos? '(1 2 3)) #t '((1 2 3) 2 3)) #t '((1 2 3) (4 5 6))) '(1 2 (3 3))) #f #f diff-listas Escribe la función (diff-listas l1 l2) que tome como argumentos dos listas estructuradas con la misma estructura, pero con diferentes elementos, y devuelva una lista de parejas que contenga los elementos que son diferentes. Ejemplos: (dif-listas '(a (b ((c)) d e) f) '(1 (b ((2)) 3 4) f)) ((a . 1) (c . 2) (d . 3) (e . 4)) (diff-listas '() '()) () (diff-listas '((a b) c) '((a b) c)) () transformar Define un procedimiento llamado (transformar plantilla lista) que reciba dos listas como argumento: la lista plantilla será una lista estructurada y estará compuesta por números enteros positivos con una estructura jerárquica, como (2 (3 1) 0 (4)) . La segunda lista será una lista plana con tantos elementos como indica el mayor número de plantilla más uno (en el caso anterior serían 5 elementos). El procedimiento deberá devolver una lista estructurada donde los elementos de la segunda lista se sitúen (en situación y en estructura) según indique la plantilla. Ejemplos: (transformar '((0 1) 4 (2 3)) '(hola que tal estas hoy)) ((hola que) hoy (tal estas)) (transformar '(1 4 3 2 5 (0)) '(vamos todos a aprobar este examen)) (todos este aprobar a examen (vamos)) nivel-hoja Escribe la función (nivel-hoja dato lista) que recorra la lista buscando el dato y devuelva el nivel en que se encuentra. Suponemos que el dato está en la lista y no está repetido. Ejemplos: (nivel-hoja 2 '(1 2 (3))) 1 (nivel-hoja 2 '(1 (2) 3)) 2 (nivel-hoja 2 '(1 3 4 ((2))) 4 hojas-nivel Define la función (hojas-nivel lista) que reciba una lista estructurada y devuelva una lista de parejas con las hojas y la profundidad en que se encuentra cada hoja. Ejemplos: (hojas-nivel '(2 3 4 (5) ((6)))) ((2.1) (3.1) (4.1) (5.2) (6.3)) (hojas-nivel '(2 (5 (8 9) 10 1))) ((2.1) (5.2) (8.3) (9.3) (10.2) (1.2)) acumula Define la función (acumula lista nivel) que reciba una lista estructurada compuesta por números y un número de nivel. Devolverá la suma de todos los nodos hoja que tengan una profundidad mayor o igual que el nivel indicado. Ejemplos: (acumula '(2 3 4 (5) ((6))) 2) (acumula '(2 3 4 (5) ((6))) 3) (acumula '(2 (5 (8 9) 10 1)) 3) 11 6 17 3. Datos funcionales y barreras de abstracción 3.1. Datos funcionales Hablamos de datos funcionales para referirnos a valores inmutables de primera clase (que podemos asignar a variables). Ejemplos en Scheme: int, boolean, char, string, pareja, … Un dato funcional se inicializa con un estado, pero no se puede modificar una vez creado. Sí que podemos construir nuevos datos a partir de datos ya existentes. Pero la característica fundamental es su inmutabilidad. En las operaciones en las que intervienen datos funcionales siempre se devuelve un dato nuevo recién creado; los objetos “matemáticos” son así. Por ejemplo, si sumamos dos números racionales el resultado es un número nuevo; los números que intervienen en la operación no se modifican. Los datos funcionales son muy útiles y se recomienda su utilización en la mayoría de lenguajes. En los lenguajes orientados a objetos como Java o Scala este tipo de datos reciben el nombre de objetos valor - ver p. 73 Effective Java Programming, Joshua Bloch). Los lenguajes de programación funcional no proporcionan mecanismos como los de clases o interfaces para definir nuevos datos. Debemos crearlos de forma ad-hoc definiendo un conjunto de funciones que construyen y trabajan con los datos y teniendo la disciplina de sólo utilizar esas para operar con los datos. Es lo que llamamos barrera de abstracción del dato. Al definir nuevas operaciones que trabajen con un dato no debemos “saltar” esa barrera de abstracción. 3.1.1. Abstracción de datos Cita de Abelson y Sussman en la que explica el concepto de abstracción de datos: When defining procedural abstraction we make an abstraction that separates the way the procedure would be used from the details of how the procedure is implemented in terms of more primitive procedures. The analogous notion for compound data is called data abstraction. Data abstraction is a methodology that enables us to isolate how a compound data object is used from the details of how it is constructed from more primitive data objects. The basic idea of data abstraction is to structure the programs that are to use compound data objects so that they operate on “abstract data”. That is, our programs should use data in such a way as to make no assumptions about the data that are not strictly necessary for performing the task at hand. At the same time, a “concrete” data representation is defined independent of the programs that use the data. The interface between these two parts of our system will be a set of procedures, called selectors and constructors, that implement the abstract data in terms of the concrete representation. 3.2. Barrera de abstracción Definimos como barrera de abstracción (o interfaz) de un dato funcional al conjunto de funciones que nos permiten trabajar con él y que separan su uso de su implementación. ;;-----------------------------------------;; ;; ;; ;; USO DE LA ABSTRACCION ;; ;; ;; ;; --------INTERFAZ: funciones ------------;; ;; ;; ;; IMPLEMENTACION ;; ;; ;; ;; ----------------------------------------;; La funciones que están por encima de la barrera de abstracción deben usar las funciones propuestas por la barrera. De esta forma se esconde la implementación y se independiza las funciones que usan la abstracción de su implementación. 3.3. Ejemplo de barrera de abstracción para construir datos: números racionales Veamos un ejemplo de dato funcional. Supongamos que queremos construir el tipo de dato número racional, un número formado por un numerador y un denominador, con las operaciones típicas de suma, resta, multiplicación y división. Vamos a definir una nomenclatura estándar para nombrar a las funciones de la barrera de abstracción. Utilizaremos el sufijo ‘rat’ (del inglés ‘rational’) para todas las funciones que trabajan con el nuevo tipo de datos. La barrera de abstracción está formada por constructores, selectores y operadores: Constructores: Funciones que devuelven un nuevo dato creado a partir de sus elementos Selectores: Funciones que devuelven algún elemento a partir del dato Operadores: Funciones que definen operaciones sobre los datos. Algunas devuelven nuevos datos resultantes de la operación o devuelven otros tipos En programación funcional no existen mutadores. Una vez construido un dato no se puede modificar (eso sí, se puede construir un nuevo dato a partir de los ya existentes). 3.3.1. Diseño de la barrera de abstracción números racionales En el caso de los números racionales, vamos a definir las siguientes funciones: Constructor (o constructores): make-rac Selectores y operadores: num-rac , denom-rac , suma-rac , sub-rac , div-rac Constructores (make-rac num denom) : Toma dos números enteros num y denom y devuelve un nuevo número racional cuyo numerador es num y denominador es denom . Selectores (num-rac rac) : Toma un número racional y devuelve su numerador. (denom-rac rac) : Toma un número racional y devuelve su denominador. Operadores (suma-rac r1 r2) : Toma dos números racionales r1 y r2 y devuelve su suma. (resta-rac r1 r2) : Toma dos números racionales r1 y r2 y devuelve su resta. (mult-rac r1 r2) : Toma dos números racionales r1 y r2 y devuelve su multiplicación. (div-rac r1 r2) : Toma dos números racionales r1 y r2 y devuelve su división. 3.3.2. Implementación de los datos racionales (define (make-rac numer denom) (cons numer denom)) (define (num-rac rac) (car rac)) (define (denom-rac rac) (cdr rac)) (define (add-rac x y) (make-rac (+ (* (num-rac x) (denom-rac y)) (* (num-rac y) (denom-rac x))) (* (denom-rac x) (denom-rac y)))) (define (sub-rac x y) (make-rac (- (* (num-rac x) (denom-rac y)) (* (num-rac y) (denom-rac x))) (* (denom-rac x) (denom-rac y)))) (define (mul-rac x y) (make-rac (* (num-rac x) (num-rac y)) (* (denom-rac x) (denom-rac y)))) (define (div-rac x y) (make-rac (* (num-rac x) (denom-rac y)) (* (denom-rac x) (num-rac y)))) (define (equal-rac? x y) (= (* (num-rac x) (denom-rac y)) (* (denom-rac x) (num-rac y)))) (define (rac->string rac) (string-append (number->string (num-rac rac)) "/" (number->string (denom-rac rac)))) Una vez definida la barrera de abstracción, hemos creado una nueva abstracción que amplía el vocabulario y las capacidades de nuestros programas. Ahora podemos usar números racionales de la misma forma que usamos números enteros o reales: (define r1 (make-rac 1 3)) ; r1 = 1/3 (num-rac r1) 1 (denom-rac r1) 3 (define r2 (make-rac 2 3)) (define r3 (add-rac r1 r2)) (rac->string r2) "2/3" (rac->string r3) "9/9" 3.3. Modificación de la implementación Una de las ventajas de definir una barrera de abstracción es que es posible cambiar la implementación de las funciones sin afectar a las funciones que las usan. Si esas funciones han respetado la definición de la barrera y no han accedido directamente a la implementación van a seguir funcionando sin problemas. Veamos un ejemplo. Vamos a modificar la implementación del número racional para añadir a la estructura el símbolo racional . Este símbolo nos va a permitir añadir un predicado que compruebe si un dato es del tipo racional. Tenemos que modificar el constructor make-racional y los selectores num-rac y denom-rac : (define (make-rac numer denom) (cons 'racional (cons numer denom))) (define (num-rac rac) (cadr rac)) (define (denom-rac rac) (cddr rac)) El resto de funciones no hay que tocarlas, siguen funcionando correctamente, porque hacen uso de estas funciones de la barrera de abstracción. Ahora ya podemos definir el predicado racional? que comprueba si un número es racional: (define (racional? x) (and (pair? x) (equal? (car x) 'racional))) Ejemplos: (define r1 (make-rac 1 3)) ; r1 = 1/3 (racional? r1) #t (racional? 12) #f (num-rac r1) 1 (denom-rac r1) 3 (define r2 (make-rac 2 3)) (define r3 (suma-rac r1 r2)) (rac->string r2) "2/3" (rac->string r3) "9/9" Lenguajes y Paradigmas de Programación, curso 2013–14 © Departamento Ciencia de la Computación e Inteligencia Artificial, Universidad de Alicante Domingo Gallardo, Cristina Pomares