Documentos de Académico
Documentos de Profesional
Documentos de Cultura
INTERPRETACIÓN y NÚMEROS
Los números en Scheme se consideran una categoría aparte. Así para evaluar un número basta con
introducir a la entrada: (el número entre corchetes es el prompt de Scheme)
[1] 7
7
[2]
Esto se debe a que el intérprete de Scheme sigue un ciclo entrada-evaluación-salida que evalúa cada
entrada del usuario. De forma que cada entrada puede considerarse un programa.
DEFINICIONES Y LITERALES
A la hora de evaluar una cadena "siete" es necesario asignarle una definición previa.
Las definiciones se realizan en Scheme a través de la expresión define:
[2] 'siete
siete
[3]
EXPRESIONES ARITMÉTICAS
Las operaciones aritméticas se consideran un tipo más de función y se expresan en notación prefija:
(+ 1 2) 1+2
(* 3 (+ 4 3*(4
5)) +5)
1
así desde Scheme:
[3] (* 3 (+ 4 5))
27
[4]
LISTAS
En Scheme una lista se denota por una colección de elementos encerrados entre paréntesis y separados
por espacios en blanco. en concreto la lista vacía se representa por ( ).
Scheme proporciona la función cons como constructora de listas, permitiendo añadir un elemento de
cada vez. Su sintaxis es la siguiente:
Anidamiento
La última lista presenta a su vez la lista ( 2 1 ) anidada. Los elementos de una lista son aquellos que no
aparecen anidados dentro de otra, así:
( ( a b ( c d ) ) e ( f g ) h )
tiene 4 elementos : ( a b ( c d ) ), e, ( f g ) y h.
Composición
La generación de una lista completa se puede conseguir a través de cons junto con la composición
funcional.
Así:
[8] ( 1 2 )
Error símbolo 1 no definido
[9]
El apóstrofe indica que todos los elementos dentro de la lista se deben interpretar como literales.
2
NOTA: En el ejemplo anterior tres no necesita apóstrofe al pertenecer a la lista que a su vez está bajo
el efecto del apóstrofe externo.
Otro ejemplo:
• ( cXXXXr lista ) ,, donde cada "X" puede ser : a (car), d (cdr) ó no aparecer.
Ejemplo:
(cadadr '( 1 ( ( 2 3 ) 4 ) ) ) ≡ (car (cdr (car (cdr '(1((2 3)4))
)))
devolvería 4
• Aparte de: atom?, null?, list? y pair?, Scheme distingue dos tipos de átomos:
Ejemplos:
Ejemplos:
(boolean? #t) → #t
(boolean? (number? 'a)) → #t
(boolean? (car '(1 2))) → #f
• El único tipo de dato que nos queda por identificar son las funciones, en Scheme procedures,
(procedure? n) devuelve #t si n es una función.
DEFINICIÓN DE FUNCIONES
En Scheme se pueden asociar valores a nombres mediante la función define. Así, por ejemplo,
escribiendo:
3
A partir de ahí podemos referir ese valor como pi.
[12] pi
3.141592
[13]
La evaluación de la función define también nos permite asociar un nombre de función con la
correspondiente definición en el entorno. Tras la evaluación el intérprete responde escribiendo el nombre
de la función:
Sentencias condicionales
En Scheme la expresión condicional SI-ENTONCES-SINO se realiza mediante la forma especial if.
Al evaluarse una expresión if, el intérprete primero evalúa el <predicado> de la función. Si se evalúa a
CIERTO, el intérprete evalúa y devuelve el valor de <consecuente>, en caso contrario el intérprete evalúa
y devuelve el valor de <alternativa>.
Ejemplos:
4
<predicado-k> se evalúa a CIERTO, entonces evalúa y retorna el valor de dicha cláusula, i.e.,
<expresion-k>.
Si todos los predicados de las cláusulas se evaluan a FALSO la estructura cond no retorna ningún
valor.
El consecuente de una cláusula puede ser vacío, en cuyo caso, si su predicado es el primero que se
hace cierto el valor devuelto por cond es el resultado de la evaluación de dicho predicado.
También se puede indicar, a través del símbolo else cual es el valor a retornar por defecto. Para ello
debemos añadir una cláusula final al cond de la forma: (else <expresion>), devolviéndose el resultado de
evaluar <expresion>.
OPERADORES LÓGICOS
Los predicados de las formas o estructuras if y cond pueden ser cualquier expresión que se evalúe a
CIERTO o a FALSO como, por ejemplo, expresiones de comparación (<, >, =, <>, >=, <=), o las
funciones (null?, atom?, list?, pair?, symbol?, number?) o predicados más complejos utilizando las
funciones lógicas habituales ( and, or y not ).
Debemos recordar que en Scheme FALSO se identifica mediante el átomo nil (o la lista vacía) y que
cualquier otra S-expresión indica el valor de verdad CIERTO.
Ejemplo de programa:
Definir la función countAtoms(s) para contar el número de átomos de una expresión simbólica s.
• Base: Si s es un átomo distinto de nil, el número de átomos de s es 1. Si s es nil (lista vacía) el
número de átomos de s es 0.
En Scheme:
countAtoms
[19] (countAtoms '(a (b (c d)) (e) f))
6
[20]
5
II.2 Abstracción de datos y números
Una de las características esenciales de la Programación Funcional (PF) es la capacidad de manejar
tipos abstractos de datos.
Para ello nos vamos a centrar en el cálculo con números. Vamos a ver como la abstracción de datos
nos va a permitir combatir el problema de la inexactitud en la representación y en las operaciones con
números .
Números
Hasta ahora hemos visto como Scheme identificaba dos tipos de átomos: los símbolos y los números.
Sin embargo dentro de los números Scheme nos ofrece la siguiente pila de subtipos donde cada nivel es
un subconjunto del inmediatamente superior:
Number (Número)
Complex (Complejo - zi )
Real (Real - xi )
Por ejemplo, el átomo 3 es un entero. Por tanto, también es un racional (3/1), un real (3.0) y un
complejo (3+0i), y obviamente es el número representado por 3. Para identificar estos tipos Scheme nos
ofrece los siguientes predicados:
(number? obj)
(complex? obj)
(real? obj)
(rational? obj)
(integer obj)
Representación y precisión.
Cada número (objeto) en Scheme puede tener más de una representación externa, así en el ejemplo
anterior, 3, 3.0, 3/1 y 3+0i son todas representaciones externas del mismo número entero, el tres. Las
operaciones sobre números se realizan sobre objetos abstractos, tratando, en la medida de lo posible,
abstraerse de su representación.
<numerador>/<denominador>
<parte_real>+<parte_imaginaria>i
Aunque nosotros trabajemos con datos inicialmente exactos, hay operaciones que necesariamente
generan números inexactos. Esta imprecisión se arrastrará en todas las operaciones donde se utilicen
dichos valores. Este hecho debe poder detectarse para identificar que números son exactos y cuales no.
Para ello, todo número en Scheme es exacto o inexacto, esta información es mantenida automáticamente
por Scheme, y para consultarla nos ofrece dos predicados:
(exact? z)
(inexact? z)
6
El criterio que sigue Scheme para identificar la exactitud es el siguiente: dada una constante numérica
se considera inexacta si contiene un punto decimal, presenta un exponente, o uno de sus dígitos es
desconocido, si no es inexacto entonces es exacto.
Es posible obtener la versión exacta de un número inexacto por aproximación, y de manera similar
podemos obtener la versión inexacta más cercana a un número exacto, esta labor la realizan las siguientes
funciones:
(exact->inexact z)
(inexact->exact z)
Por ejemplo:
Propiedades
Disponemos de un conjunto de predicados que nos permiten extraer unas serie de propiedades de un
número, propiedades referidas a su signo, paridad o si es nulo:
(positive? x)
(negative? x)
(odd? n)
(even? n)
(zero? z)
Operaciones
Predefinidas en Scheme encontramos múltiples operaciones sobre números, entre ellas tenemos las
siguientes:
(= z1 ...) ⇒ igualdad
(< x1 ...) ⇒ monótono creciente
(> x1 ...) ⇒ monótono decreciente
(<= x1 ...) ⇒ monótono no decreciente
(>= x1 ...) ⇒ monótono no creciente
7
(log z) ⇒ logaritmo en base 10
(sin z) ⇒ seno en radianes
(cos z) ⇒ coseno en radianes
(tan z) ⇒ tangente en radianes
(asin z) ⇒ arcoseno en radianes
(acos z) ⇒ arcocoseno en radianes
(atan z) ⇒ arcotangente en radianes
Las operaciones con ... en sus argumentos se ejecutan sobre un número arbitrario de argumentos,
como mínimo los especificados, ejecutando las operaciones con asociatividad a izquierdas.
Entrada/Salida
La entrada y salida de números se consigue por medio de dos funciones que nos permiten recoger una
cadena de texto (string) y convertirla en un número, y viceversa:
(string->number z)
(string->number z radix)
(number->string z)
(number->string z radix)
Estas funciones llevan incluidas como segundo argumento la base a utilizar (2, 8, 10 o 16), que por
defecto será 10. Si no es posible realizar la conversión por no representar un número válido, la función
devuelve #f.
La función suma-armonicos calcula la suma de los primeros n términos de una serie de armónicos, es
decir:
1 1 1
1+ + + ... +
2 3 n
La base en este caso se refiere al valor n=0 en el que la suma es nula.
suma-armonicos(n) ::=
si n=0
entonces 0
sino 1/n + suma-armonicos(n-1)
finsi
(define(suma-armonicos n)
(if (zero? n) 0 (+ (/1 n) (suma-armonicos (- n 1)))))
8
Vamos a definir ahora la función indice que dada una lista y un índice n, nos devuelve el enésimo
elemento, empezando en cero.
Ejemplos de indice(lista,n):
indice( (a b c d e f), 3) → d
indice( (a b c), 3) → Error: indice fuera de rango
Definición de indice:
indice(lista,n)::= si list?(lista)
entonces si length(lista)>n
entonces _indice(lista,n)
sino error
finsi
sino error
finsi
_indice(lista,n)::= si n=0
entonces car(lista)
sino _indice(cdr(lista),n-1)
finsi
Nota: La función error interrumpe el proceso de evaluación, muestra sus argumentos y devuelve el
prompt al usuario.
Sin embargo es posible realizar operaciones aritméticas con números racionales y obtener resultados
exactos. Con este objetivo nos aprovecharemos de la abstracción de datos. Para ejemplificar la potencia
de la abstracción a la hora de mantener la exactitud en la representación y manipulación de los números,
vamos a tomar los números racionales, tipo de dato que el estándar de Scheme ya incorpora. Por ejemplo:
(/ 1 3) ⇒ 1/3
(+ 1/3 2/6) ⇒ 2/3
(/ 2/3) ⇒ 3/2
NUMEROS RACIONALES
a
Un número racional está formado por 2 enteros: a su numerador y b su denominador que debe ser
b
distinto de 0.
9
Por el momento no nos preocuparemos de la representación de un número racional.
SELECTORES
Nos basaremos en la idea de que un racional está compuesto por un numerador y un denominador y a
los que tenemos acceso a través de 2 funciones:
Así si rac es un racional, su numerador será (rnum rac) y su denominador (rden rac). Son las
funciones selectoras para números racionales al igual que car y cdr aplicadas a listas.
CONSTRUCTOR
De la misma forma que con cons en listas, necesitamos un constructor para los racionales, éste será
crea-rac que construye un racional a partir de dos enteros, siendo el denominador no nulo. De esta forma,
un racional rac verifica:
Por ejemplo:
3
(crea-rac 3 4) ⇒
4
A partir de los selectores y el constructor podemos empezar a construir nuestras funciones aritméticas
independientemente de la representación final.
La primera función va a ser rzero? que nos indicará si un racional es nulo o no (su denominador no
importa, su numerador ha de ser cero).
OPERACIONES ARITMETICAS
Ahora combinaremos dos racionales para definir las operaciones aritméticas básicas (+ - * /)
obteniendo resultados exactos.
SUMA
a c
La suma de dos fracciones y es otra fracción cuyo numerador es a*d + b*c y su denominador
b d
es b*d. Así si x e y son dos racionales definimos r+,
(define (r+ X Y)
(crea-rac (+ (* (rnum X) (rden Y))
(* (rnum Y) (rden X)))
(* (rden X) (rden Y))))
PRODUCTO
De la misma forma el producto lo definimos como, denominador b*d y numerador a*c. La función r*
será:
(define (r* X Y)
(crea-rac (* (rnum X) (rnum Y))
(* (rden X) (rden Y))))
RESTA/DIFERENCIA
10
De forma similar la diferencia será: r-
(define (r- X Y)
(crea-rac (- (* (rnum X) (rden Y))
(* (rnum Y) (rden X)))
(* (rden X) (rden Y))))
INVERSA
Para definir el cociente, definimos previamente el inverso de un racional no nulo como rinver:
(define (rinver X)
(if (rzero? X)
(error "rinver no puede invertir" X)
(crea-rac (rden X) (rnum X))))
COCIENTE
A partir de ella definimos el cociente como el producto de X por la inversa de Y, así r/ es:
(define (r/ X Y)
(r* X (rinver Y)))
IGUALDAD
a c
Otra función interesante es la igualdad entre racionales, nos basamos en que = ↔ a.b = c.d ,
b b
así r= :
(define (r= X Y)
(= (* (rnum X) (rden Y)) (* (rnum Y) (rden X))))
SIGNO
De igual forma podemos definir rpositive? que devuelve cierto en caso de que numerador y
denominador tengan el mismo signo. Nos apoyamos en positive? y negative? de Scheme.
MAYOR Y MENOR
El predicado r> y r< se basan en que la diferencia entre 2 racionales (r-) sea positiva o no (rpositive?)
basta en r> intercambiar X e Y para obtener r< :
11
Como vemos existe una gran semejanza entre estas funciones. Cuando ocurre esto en programación
funcional, se opta por una función común donde el operador a aplicar se le pasa como argumento, es
decir, una Función de Orden Superior.
Con este objetivo definimos una función valor-extremo que recibe tres argumentos:
(valor-extremo Operador X Y) ⇒ X o Y
valor-extremo::A×A×(A×A→Booleano)→A
(define (rmax X Y)
(valor-extremo r> X Y))
(define (rmin X Y)
(valor-extremo r< X Y))
Como vemos la versatilidad de la PF nos permite trabaja con objetos de distinto tipo con sólo
especificar el operador adecuado.
POTENCIA DE LA ABSTRACCIÓN
A partir de estas funciones es posible definir programas complejos que trabajen con aritmética exacta
sobre racionales. De esta forma, siempre que proporcionemos, el constructor y los selectores podremos
generar un paquete de funciones sobre racionales. Es más, ya que hasta ahora hemos manejado los datos
como abstractos, esto nos permitirá cambiar su representación interna con sólo modificar los selectores y
el constructor: crea-rac, rnum y rden.
REPRESENTACIÓN
Únicamente nos resta dar una representación concreta a los datos. Una posibilidad sería una lista
(a b) siendo a el numerador y b el denominador, donde b debe ser distinto de cero.
(define (crea-rac a b)
(if (zero? b) (error "denominador 0 en crea-rac")
(cons a (list b))))
Podríamos cambiar la representación a un par (a.b) y sólo cambiaríamos estas 3 funciones y el resto
permanecería inalterado.
Finalmente si queremos una representación única, podemos trabajar con la siguiente representación
canónica:
(define (crea-rac a b)
(if (zero? b) (error "denominador 0 en crea-rac")
(list (quotient a (mcd a b)) (quotient b (mcd a b)))))
12
II.3 Funciones de orden superior y Evaluación Parcial
En este apartado vamos a ver dos de las características que permiten considerar a las funciones en PF
como "ciudadanos de primera clase". Lo que se refleja en el hecho de que las funciones puedan ser
tratadas como cualquier otro objeto del lenguaje. Pudiendo actuar como argumentos y como resultado.
Tratamos ahora de escribir una función que recibe un argumento y devuelve una lista con dicho
argumento mediante una expresión lambda.
Para evitar tener que reescribir la función cada vez que la deseamos aplicar, le damos un nombre:
(define haz-lista-de-uno
(lambda (item) (cons item '( ))))
Ejemplo:
> (make-suma 4)
(lambda(x) (+ x 4))
> ((make-suma 4) 7)
11
>
Las funciones lambda son definiciones funcionales anónimas, que no serán de especial utilidad para la
definición de funciones locales a una función (como veremos más adelante) y como argumento o
resultado de otra función.
13
Estas expresiones disponen de una segunda sintaxis que nos permite trabajar con un número arbitrario
de argumentos:
En este caso la expresión lambda recibirá un número arbitrario de argumentos, en el momento de ser
invocada la función todos sus argumentos se introducen en una lista que es referenciada como
<Lista-Args> en el cuerpo de la función <expresion>.
En esta ocasión la expresión lambda recibe al menos n argumentos ligados a los <Argi>, y el resto de
argumentos se reciben agrupados en una lista como <Argn+1>
Definición: Una función se dice de orden superior si alguno de sus argumentos es una función o si
devuelve una función o una estructura conteniendo una función.
Las funciones de orden superior nos permiten un mayor nivel de abstracción, y su utilización puede
justificarse mediante los ejemplos siguientes:
Ejemplos:
b
1. f ( a, b) = ∑ n
n=a
b
2. f ( a, b) = ∑ n 3
n=a
b
1
3. f ( a, b) = ∑
n = a ( 4n − 3a )( 4n − 3a + s )
Su definición es:
14
Las tres funciones son muy similares, pudiéndose definir una función que es la abstracción de todas
ellas:
term(X) ::= X
next(X) ::= X+1
f(a,b) ::= sum(term, a, next, b)
Emplearemos las expresiones lambda para definir con una sóla función f(a,b):
(define (f a b)
(sum (lambda (X) X) a (lambda (X) (+ X 1)) b))
APPLY
Supongamos que queremos calcular el máximo de dos números en una lista ls, no podemos llamar a
(max ls) ya que ls no es del tipo correcto ya que cada argumento debe ser un número, así:
Solucion A: Construir un programa recursivo que calcule max de una lista ls.
Solución B: Scheme nos ofrece la función apply, que permite aplicar una función de k argumentos a
una lista de k elementos. El resultado es el mismo de pasar los elemento de la lista como los k
argumentos. Su sintaxis es la siguiente:
Donde <funcion> debe tener tantos argumentos como elementos tenga <Lista-de-elems>.
Devolviendo el resultado de evaluar:
(<funcion> <elementos-de-la-lista-de-elementos>)
Ejemplo:
MAP
La función map requiere como primer argumento el nombre de una función y a continuación tantas
listas como argumentos necesita esta última. El valor retornado es la lista de resultados de aplicar la
función dada a los elementos correspondientes en las listas.
Todas las listas han de tener la misma longitud.
La sintaxis general:
15
Ejemplos:
FOR-EACH
Similar a la función anterior, en el sentido de establecer un proceso secuencial (el indicado por la
función) sobre los elementos correspondientes de las listas, pero en este caso no retorna resultado alguno.
sería :
16
Definiciones Locales - Let y Letrec
El lenguaje Scheme es interpretado y permite la definición de los elementos del lenguaje a distintos
niveles.
General Environment
User Environment
Function
Environment
El ámbito de las definiciones tiene tres niveles anidados. Entorno General, en él vienen las
definiciones del propio intérprete incorporando las funciones y constantes predefinidas en el lenguaje.
Entorno de Usuario, constituido por el Entorno General y las funciones definidas por el usuario. Entorno
de Funcion, dentro de una función es posible establecer dos tipos de definiciones internas: argumentos y
definiciones locales.
Este apartado presenta las formas especiales Let y Letrec que permiten establecer definiciones locales
en la definición de la propia función y que permiten simplificar la lectura y definición de la propia
función.
LET
Nos permite asociar una definición a un conjunto de símbolos, limitando su ámbito a dicha expresión.
Su sintaxis es la siguiente:
Cada una de estas definiciones sólo tienen una vigencia en la expresión body que constituye el cuerpo
de la función.
Ejemplo:
Suma de dos definiciones internas (a y b) en una expresión:
[32] (let ( (a 2) (b 3)) (+ a b) )
5
[33]
negrita.
(define (distancia X Y)
(let ( (dif2 (lambda (X1 X2)
(expt (- X1 X2) 2))))
(sqrt (+ (dif2 (car X) (car Y) )
(dif2 (cadr X) (cadr Y))))))
LETREC
Es similar a la anterior salvo que en este caso se permite utilizar las propias definiciones de forma
recursiva, los id's pueden aparecer en las expresiones val's.
17
Ejemplo: Definición de la función factorial.
(define (fact n)
(letrec ((aux (lambda (n) (if (zero? n) 1
(aux (- n 1)))))
(aux n))))
> (fact 5)
120
>
Currying en Scheme
Se define el "currying" como la capacidad de definir una función f que actúa sobre n argumentos de
tipo A1 A2…An de forma distinta a la clásica: f:A1xA2x…xAn → B
Sino como una función que va consumiendo entre 1 y n argumentos. Para ello se define como:
f:A1→A2→A3→…→An→ B
La función se aplica al primer argumento y devuelve una función que tomará un nuevo argumento y
devolverá otra función. En caso de que haya n argumentos al final disponemos de un valor constante; si
hay menos nos devuelve una función que puede ser aplicada al resto de argumentos restantes.
Este tipo de funciones se denominan "funciones parametrizables". De esta forma conseguimos simular
una función de n argumentos a través de una función de orden superior de sólo un argumento y que
retorna una función.
El empleo de este concepto en Scheme se alcanza a través del empleo de funciones lambda, siendo
éste el objeto devuelto con cada nuevo argumento. Veamos su utilización en un sencillo ejemplo:
Fácilmente podemos definir una fucnión que le sume cualquier número en lugar de 5. Para ello
emplearemos la ventaja de que una función pueda devolver como valor de retorno otra función. Así
definimos una función curried+ que tiene como único argumento m y devuelve una función lambda, i.e.
sin nombre, que tiene un sólo argumento n y retorna la suma de m y n:
(define (curried+ m)
(lambda(n)
(+ m n)))
((curried+ 5) 7) → 12
En este caso curried+ es donde se ha aplicado el concepto de currying pasando de una función con 2
argumentos x e y, a reescribirla como una función con un argumento x y retorna otra función de un
argumento, lo que se denomina currying o (según autores) currificación.
18
Ejemplo con Member: Supongamos que queremos aplicar la función member? sobre varias listas
consultando por el mismo elemento. Con este objetivo definimos la función member-c que recibe un
argumento item y devuelve una función helper con un argumento que debe ser una lista:
Ejemplo con map: La función map tiene al menos 2 parámetros: una función fuc y una lista ls.
Su definición:
Esto puede ser reescrito a través de la función apply-to-all que toma un argumento fuc y devuelve una
función con un argumento ls que es una lista. Podemos definirla como:
Ejemplo:
(define map
(lambda (proc ls)
((apply-to-all proc) ls)))
19
Ejemplo de currying: función Swapper
Se va a definir una función que recibe una lista Ls y dos elementos que deben ser intercambiados en
ella x e y. Su definición es la siguiente:
(define swapper
(lambda (x y Ls)
(cond
((null? Ls) '( ))
((equal? (car Ls) x)(cons y (swapper x y (cdr Ls))))
((equal? (car Ls) y)(cons x (swapper x y (cdr Ls))))
(else (cons (car ls) (swapper x y (cdr ls)))))))
Currificación de swapper, swapper-m recibe dos argumentos x e y y retorna una función lambda con
un argumento Ls de tipo lista.
(define swapper-m
(lambda (x y)
(letrec
((helper
(lambda(Ls)
(cond
(null? Ls) '( ))
((equal? (car Ls) x) (cons y (helper (cdr Ls))))
((equal? (car Ls) y) (cons y (helper (cdr Ls)))))
(else (cons (car Ls) (helper (cdr Ls))))))))
helper)))
Por ejemplo para crear una función que intercambie 0 por 1's en una lista bastaría con:
Conclusiones
• Hemos introducido el concepto de currying.
• Permite redefinir una función con n = m+k parámetros como una función de m parámetros que
retorna una función de k parámetros.
• De forma general, podemos redefinir una función de n argumentos como n funciones de 1
argumento.
20
II.4 Computación con listas infinitas: streams
En este apartado veremos como resolver problemas en Scheme sobre listas de longitud infinita. Para
ello se introducirá el concepto de stream como una secuencia infinita de elementos, y los operadores
necesarios para su programación, lo que implicará modificar también la estrategia de evaluación de
Scheme
Estrategia Impaciente o Ansiosa (Eager): En este caso la expresión a reducir es siempre la más
interna. Así para evaluar la llamada a una función hay que evaluar primeramente sus argumentos.
Esta estrategia es la que siguen los lenguajes imperativos como C, Pascal o Fortran, y algunos
Funcionales como Scheme o Lisp.
Estrategia Perezosa (Lazy): A la hora de reducir una expresión se selecciona siempre la más externa.
Lo que permite evaluar una función sin tener necesariamente que evaluar sus argumentos. Esta
estrategia es la utilizada en lenguajes funcionales puros como Haskell. La ventaja de esta estrategia
es que permite trabajar con estructuras infinitas, evita reducir argumentos (expresiones) que no son
necesarios para la evaluación. Todo ello confluye en el hecho de que por lo general necesita un
número igual o menor de pasos que la evaluación ansiosa.
Veamos en que medida afecta al número de pasos necesarios para realizar la reducción de una
expresión. En primer lugar vamos a ver dos posibles secuencias de reducción, utilizando las estrategias
anteriores, al evaluar el cuadrado de una suma. Una posible secuencia con evaluación impaciente es
(cuadrado (+ 3 4))
= ; definición de +
(cuadrado 7)
= ; definición de cuadrado
(* 7 7)
= ; definición de *
49
(cuadrado (+ 3 4))
= ; definición de cuadrado
(* (+ 3 4) (+ 3 4))
= ; definición de +
(* 7 (+ 3 4))
= ; definición de +
(* 7 7)
= ; definición de *
49
He aquí un segundo ejemplo sobre una función (fst X Y) que devuelve el valor de su primer argumento.
La primera secuencia con evaluación impaciente es
21
(fst (cuadrado 4) (cuadrado 2))
= ; definición de cuadrado
(fst (* 4 4) (cuadrado 2))
= ; definición de *
(fst 16 (cuadrado 2))
= ; definición de cuadrado
(fst 16 (* 2 2))
= ; definición de *
(fst 16 4)
= ; definción de fst
16
Como vemos en los ejemplos, independientemente del orden, si ambos acaban, obtienen el mismo
resultado. En el primer ejemplo la estrategia impaciente necesita un menor número de pasos. Lo que
consigue al poder reutilizar el valor de un argumento ya reducido, que en el caso de la estrategia perezosa
tiene que evaluarse cada vez que se utiliza. Por el contrario, en el segundo ejemplo es la estrategia
perezosa la que necesita un menor número de pasos, ya que evita la reducción del segundo argumento que
no llega a ser necesario en la evaluación. La evaluación perezosa, frente a la impaciente, presenta dos
propiedades deseables: (i) termina siempre que cualquier otro orden de reducción termine, y (ii) no
requiere más (sino posiblemente menos) pasos que la evaluación impaciente.
Veremos como modificar la estrategia de evaluación de Scheme, de forma que las estructuras de longitud
infinita puedan ser procesadas por evaluación perezosa.
Streams
Un stream es una secuencia posiblemente infinita de elementos. Existe una gran similitud con las listas de
Scheme en su estructura, pero verdaderamente difieren en como se evalúan lo que permite que un stream
pueda tener longitud infinita. Como consecuencia, la resolución de problemas sobre streams tiene un
enfoque propio, muy semejante al procesamiento de señales electrónicas, tal y como podemos ver en la
siguiente figura:
Como se puede ver la resolución consiste en la definición de una secuencia de operadores que se
aplican secuencialmente a la señal de entrada en cada instante de tiempo. En Scheme la analogía sería la
aplicación secuencial de una serie de funciones a cada elemento del stream, que ahora puede ser
procesado sin esperar por el resto de elementos.
La filosofía de trabajo con streams consistirá en identificar la secuencia de operaciones op1 op2 ... opn
que debemos aplicar sobre los elementos de la secuencia para obtener el resultado. Para ello
implementamos cada operador como una función sobre streams, y su aplicación se logra por medio de la
composición funcional:
De forma similar a las listas, para implementar estas operaciones necesitamos definir los operadores que
nos permiten construir y examinar streams:
22
(head s) ⇒ cabeza del stream s
(tail s) ⇒ cola del stream s
(cons-stream a b) ⇒ construye un stream con a y b, verificando que:
(cons-stream (head s) (tail s)) ⇒ s
Ejemplo:
Pretendemos definir una función que calcule la suma de los cuadrados de las hojas impares de un árbol
binario de números. Una posible implementación en Scheme sería la siguiente función:
;; CONCATENACION de streams
(define (filter-odd s)
(cond ((empty-stream? s) the-empty-stream)
((odd? (head s))
(cons-stream (head s) (filter-odd (tail s))))
(else (filter-odd (tail s)))))
23
(define (map-square s)
(if (empty-stream? s)
the-empty-stream
(cons-stream (square (head s))
(map-square (tail s)))))
(define (accumulate-+ s)
(if (empty-stream? s)
0
(+ (head s) (accumulate-+ (tail s)))))
Finalmente podemos componer estas funciones para definir de la función original en términos de streams:
24
(enumera-hojas arbol)))))
Implementación de Streams
Para definir la implementación de los operadores sobre streams vamos a fijar la verdadera potencia de los
streams sobre un ejemplo concreto.
Consideremos el problema de localizar el segundo número primo entre 104 y 106, una posible solución
sería la siguiente:
Este enfoque con listas sería muy ineficiente (ya que tendría que generar en primer lugar una listas con
todos los números del intervalo, para a continuación filtrar todos los números que no son primos para
quedarse al final con el segundo elemento de la lista resultante). Sin embargo con streams esta definición
es eficiente. La justificación es que en un stream no tienen porque estar definidos de forma explícita todos
sus elementos, sino que se pospone la definición de los elementos que no sean necesarios.
Este hecho se refleja en la implementación de los operadores básicos sobre streams que se basan en las
dos siguientes funciones de Scheme que modifican el orden de evaluación de las expresiones:
(define (empty-stream? s)
(null? s))
El constructor cons-stream es una estructura especial de Scheme (una macro en el caso de DrScheme)
ya que si la definiéramos como una función su invocación evaluaría el argumento <b> antes de tratar de
posponer su evaluación con la función delay, con lo que no conseguiríamos el efecto deseado.
Así devuelve un par donde la cabeza está definida y el resto (la cola) es una promesa cuya evaluación se
pospone hasta que se necesiten sus valores.
25
Para localizar los dos primeros primos (10007 y 10009) en el intervalo sólo es necesario generar los
primeros 10 elementos del intervalo si utilizamos streams.
Streams Infinitos
Siempre podemos crear una función que integre todos los pasos a la vez, y así en el ejemplo anterior que
vaya comprobando si es primo cada número y si es el que buscamos lo devuelva. Pero los streams
presentan una ventaja adicional y es que son capaces de definir y trabajar con secuencias infinitas. Así por
ejemplo podríamos definir los números Naturales por medio de la siguiente función
(define (enteros-desde n)
(cons-stream n (enteros-desde (+ n 1)))
El objeto naturales es una secuencia infinita de elementos, con esta implementación un stream
infinito que ahora podemos manipular con las funciones head y tail. Así podemos ahora definir los
números primos como:
De nuevo si necesitamos en un programa los primeros números primos, basta con utilizar esta última
invocación. Al representarlos como un stream no sólo conseguimos que se calculen únicamente los
números primos que se necesiten, sino que además si se vuelven a necesitar no habrá que recalcularlos
(recordemos gracias a que tail se implementa sobre la función force).
En Scheme podemos lograr un efecto similar con los streams. Vamos a definir la secuencia infinita de los
números de Fibonacci como:
(define (fibgen a b)
(cons-stream a (fibgen b (+ a b))))
Fibs es un par cuyo primer elemento es 0 y su cdr es una promesa que se evalúa a (fibgen 1 1),
que una vez evaluada producirá un par cuyo car será 1 y su cdr una promesa que se evaluará a
(fibgen 1 2) y así sucesivamente.
Vamos aprovecharnos de la evaluación pospuesta para definir streams de forma implícita. Por ejemplo,
podemos definir el stream unos definido por una secuencia infinita de unos como:
Como vemos, el stream es un par donde el car es un 1 y el cdr es una promesa que se evalúa de nuevo
al stream unos, comenzando con otro 1 y su cola de nuevo es unos, y así sucesivamente.
Ahora vamos a definir la funcion add-streams que nos genera el stream suma de otros dos streams
de longitud variable:
26
Ahora podemos definir el conjunto de los enteros como:
Esta definición es correcta ya que los enteros son un par donde el primer elemento es el 1 y el resto se
obtiene de sumar unos y el propio enteros. En la suma se utiliza siempre un elemento de enteros
ya calculado en el paso anterior con lo que se accede directamente a él sin tener siquiera que ser vuelto a
calcular.
Derivación simbólica
Escribir un programa que permita aplicar las reglas de derivación para la suma y el producto de una
expresión que se proporcionará en forma prefija. Deberá indicarse, además, quién es la variable con
respecto a la que se deriva.
Operandos Operadores
• constantes numéricas • suma
3, 4, 6 (+ sum1 sum2)
• variables • producto
X, Y, Z (* mul1 mul2)
same-variable?(v1,v2) ::=
variable?(v1) ∧ variable?(v2) ∧ eq?(v1,v2)
• Constructores
• Selectores
• Test
27
sum?(x) ::= si ¬atom?(x)
entonces eq?(car(x),`+)
sino falso
fsi
product?(x) ::= si ¬atom?(x)
entonces eq?(car(x),'*)
sino falso
fsi
28
3) Función de derivación de expresiones simbólicas
deriv(exp,var):=
si constant?(exp)
entonces 0
sino
si variable?(exp)
entonces
si same-variable?(exp,var)
entonces 1
sino 0
fsi
sino
si sum?(exp)
entonces
make-sum( deriv(addend(exp), var),
deriv(augend(exp), var))
sino
si product?(exp)
entonces
make-sum(
make-product( multiplier(exp),deriv(multiplicand(exp), var))
make-product( deriv(multiplier(exp), var), multiplicand(exp)))
sino
error
fsi
fsi
fsi
fsi
Ejemplo:
Dada la expresión: x + 3 *(x + y + 2), la pasamos a notación prefija para poder derivarla.
deriv( '(+ X (* 3 (+ X (+ Y 2)))), 'X)
⇒
(+ 1 (+ (* 3 (+ 1 (+ 0 0)) (* 0 (+ X (+ Y 2))))))
Vamos a tratar de simplificar las expresiones resultantes, para ello modificaremos los constructores de la
suma y el producto para que analicen sus componentes antes de construir una nueva expresión. Si
conocemos la regla de evaluación de la nueva expresión se aplica directamente y el resultado es lo que
devuelve el constructor. Veámoslo con el constructor de la suma.
make-sum2(a1,a2)::=
si number?(a1) ∧ number?(a2)
entonces a1 + a2
sino
si number?(a1) ∧ cero?(a1)
entonces a2
sino
si number?(a2) ∧ cero?(a2)
entonces a1
sino list('+,a1,a2)
fsi
fsi
fsi
29
Ejemplo:
(deriv '(+ x 3) 'x) ⇒ 1
Matching
Un ejemplo típico de programación simbólica es el matching, el cual permite determinar si una lista se
corresponde o no con un patrón dado. El patrón puede contener, además de símbolos concretos,
metacaracteres . Los metacaracteres más habituales son * y ?. El primero se corresponde con cualquier
secuencia de s-expresiones (incluída la secuencia vacía) en la lista, mientras que el segundo debe
corresponderse exactamente con una s-expresión en la lista.
Ejemplos:
Las funciones de matching pueden ser más complejas, incorporando otro tipo de patrones, como, por
ejemplo, funciones. En este caso se podrían preguntar cuestiones del tipo: si una s-expresión de la lista es
un número, o un símbolo.
(else #f)))
Ejemplos:
30