Tema 4: Procedimientos y estructuras recursivas

Anuncio
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
Descargar