Está en la página 1de 31

PROGRAMACIÓN FUNCIONAL: SCHEME

II PROGRAMACIÓN EN SCHEME .................................................................................................1


II.1 SINTAXIS BÁSICA ..........................................................................................................................1
SÍMBOLOS: ÁTOMOS Y NÚMEROS.............................................................................................1
INTERPRETACIÓN y NÚMEROS........................................................................................................... 1
DEFINICIONES Y LITERALES............................................................................................................... 1
EXPRESIONES ARITMÉTICAS .............................................................................................................. 1
LISTAS ...................................................................................................................................................... 2
Anidamiento ............................................................................................................................................... 2
Composición............................................................................................................................................... 2
Evaluación de listas (funciones como listas) .............................................................................................. 2
Funciones adicionales de Scheme para estructuras..................................................................................... 3
DEFINICIÓN DE FUNCIONES.....................................................................................................3
Sentencias condicionales ............................................................................................................................ 4
Forma condicional genérica: COND .......................................................................................................... 4
OPERADORES LÓGICOS........................................................................................................................ 5
Ejemplo de programa: ............................................................................................................................ 5
II.2 ABSTRACCIÓN DE DATOS Y NÚMEROS ...........................................................................................6
Números..........................................................................................................................................6
Representación y precisión. ........................................................................................................................ 6
Propiedades................................................................................................................................................. 7
Operaciones ................................................................................................................................................ 7
Entrada/Salida............................................................................................................................................. 8
Definición recursiva de funciones aritméticas................................................................................8
Aritmética exacta por abstracción de datos....................................................................................9
NUMEROS RACIONALES .............................................................................................................9
II.3 FUNCIONES DE ORDEN SUPERIOR Y EVALUACIÓN PARCIAL.........................................................13
Definición generalizada de funciones en Scheme - Funciones LAMBDA ....................................13
Funciones de orden superior ........................................................................................................14
Funciones de orden superior predefinidas ................................................................................................ 15
Definiciones Locales - Let y Letrec...............................................................................................17
Currying en Scheme......................................................................................................................18
Conclusiones............................................................................................................................................. 20
II.4 COMPUTACIÓN CON LISTAS INFINITAS: STREAMS ........................................................................21
Estrategias de evaluación: Perezosa vs Impaciente................................................................................... 21
Streams .........................................................................................................................................22
Funciones de orden superior sobre streams .............................................................................................. 24
Implementación de Streams...................................................................................................................... 25
Streams Infinitos....................................................................................................................................... 26
Streams Infinitos definidos implícitamente .............................................................................................. 26
II.5 EJEMPLOS DE PROGRAMACIÓN SIMBÓLICA: DERIVACIÓN SIMBÓLICA Y MATCHING.....................27
Derivación simbólica ................................................................................................................................ 27
Matching................................................................................................................................................... 30
II PROGRAMACIÓN EN Scheme
En este apartado vamos a ver como se pueden escribir programas funcionales en Scheme empleando
los conceptos de programación funcional.

II.1 Sintaxis básica


En primer lugar veremos como se representan los datos (las S-expresiones) para luego pasar a la
definición de funciones.

SÍMBOLOS: ÁTOMOS Y NÚMEROS


En Scheme los átomos se denominan símbolos.
• Estos se forman por caracteres distintos de: ( ) []{}; , " ' ` # \
• Además + - . no pueden aparecer al principio de un símbolo.

Ejemplos de símbolos válidos: abcd r cdr p2q4 errores? uno-dos *ahora&

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:

(define identificador valor) (NO LO EMPLEAREMOS CON ESTE USO)

Pero esta es una característica de programación procedural y no la emplearemos. Sin embargo si lo


que queremos escribir la cadena como un símbolo constante (es decir que no se evalúe) debemos emplear
la función quote:
(quote siete) ó 'siete

Así podremos introducir:

[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:

(cons primerelem ListaResto)


Ejemplos:

[4] (cons 1 '( ) )


(1)
[5] (cons 2 '( 1 ) )
(21)
[6] (cons 'tres '( 2 1 ) )
( tres 2 1 )
[7] (cons '( 2 1) '( tres 2 1) )
( ( 2 1) tres 2 1)
[8]

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.

Ejemplo: Para generar ( 2 1 ) podemos hacer (cons 2 (cons 1 '( ) ) )

Evaluación de listas (funciones como listas)


Al introducir y usar la lista vacía es necesario emplear '( ) esto se debe a que Scheme al encontrarse
con una lista interpreta siempre el primer elemento como una función.

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.

[9] '( ( 2 1 ) tres 2 1 )


( ( 2 1 ) tres 2 1)
[10]

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:

[10] (cons '( a b ) '( c ( d e ) ) )


((ab)c(de))

Funciones adicionales de Scheme para estructuras

• ( 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:

Numéricos: (number? n) devuelve #t si n es un número.


Simbólicos: (symbol? n) devuelve #t si n es un símbolo.

Ejemplos:

(number? -4.6) → (symbol? -4.6) →


#t #f
(number? '3) → #t (symbol? '3) → #f
(number? 'doce) → (symbol? 'doce) →
#f #t
(number? #t) → #f (symbol? #t) → #f

• (boolean? n) devuelve #t si n es un valor lógico.

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.

(procedure? cons) → (procedure? +) → #t


#t
(procedure? 'cons) → (procedure? 100) →
#f #f

DEFINICIÓN DE FUNCIONES
En Scheme se pueden asociar valores a nombres mediante la función define. Así, por ejemplo,
escribiendo:

[11] (define pi 3.141592)


pi
[12]

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:

[13] (define (square X) (* X X))


square
[14] (square 2)
4
[15] (square (+ 3 1))
16
[16]

La forma general de la definición de una función es la siguiente:

(define (<nombre> <parametros formales>) <cuerpo>)

Donde <nombre> es un símbolo asociado con la definición de la función en el retorno. Los


<parametros formales> son los nombres utilizados dentro del cuerpo de la función para referirse a los
correspondientes argumentos de la misma. El <cuerpo> es una expresión que produce el valor de la
aplicación de la función cuando los parámetros formales son reemplazados por los argumentos actuales
en la evaluación de dicha función.

Sentencias condicionales
En Scheme la expresión condicional SI-ENTONCES-SINO se realiza mediante la forma especial if.

(if <predicado> <consecuente> <alternativa>)

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:

[16] (define (maxAoB a b) (if (> a b) 'a 'b) )


maxAoB
[17] (maxAoB 3 2)
a
[18]

Forma condicional genérica: COND


Existe también una forma condicional genérica equivalente a múltiples expresiones condicionales
SI-ENTONCES-SINO anidadas, como una estructura "case", ésta es la forma especial cond.

(cond (<predicado1> <expresion1>)


(<predicado2> <expresion2>)
..................................................
(<predicadoN> <expresionN>))

Los argumentos de la construcción cond son pares de expresiones de la forma


(<predicado> <expresion>) llamadas cláusulas. Cuando el intérprete evalúa la estructura primero evalúa
los predicados de las cláusulas en el orden dado hasta encontrar una cláusula en la que el predicado

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.

• Recurrencia: Si s no es un átomo, se suponen conocidos los valores de countAtoms(car(s)) y


countAtomos(cdr(s)). Por tanto el número de átomos en s será la suma de ellos. De foma abstracta:

countAtoms (s) ::= si null?(s)


entonces 0
sino
si atom?(s)
entonces 1
sino countAtoms(car(s)) + countAtoms(cdr(s))
fsi
fsi

En Scheme:

[18] (define (countAtoms s)


(cond ((null? s) 0)
((atom? s) 1)
(else (+ (countAtoms (car s)) (countAtoms(cdr s))) )))

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 )

Rational (Racional - qi)

Integer (Entero - ni)

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.

Para representar números racionales e imaginarios, Scheme utiliza la siguiente sintaxis:

<numerador>/<denominador>

<parte_real>+<parte_imaginaria>i

Ejemplos: 5/4, -4/3, 3+4i ó 4i

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:

(exact->inexact 1/3) ⇒ 0.3333333333333333


(inexact->exact 0.333333333333) ⇒ 6004799503154657/18014398509481984

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 ...) ⇒ suma de una serie de números


(- z1 z2 ...) ⇒ resta de una serie de números
(- z) ⇒ negación
(* z1 ...) ⇒ producto de una serie de números
(/ z1 ...) ⇒ división de una serie de números
(/ z) ⇒ inverso

(= z1 ...) ⇒ igualdad
(< x1 ...) ⇒ monótono creciente
(> x1 ...) ⇒ monótono decreciente
(<= x1 ...) ⇒ monótono no decreciente
(>= x1 ...) ⇒ monótono no creciente

(max x1 ...) ⇒ máximo de una serie de números


(min x1 ...) ⇒ mínimo de una serie de números

(abs x) ⇒ valor absoluto

(floor x) ⇒ mayor entero menor o igual que x


(ceiling x) ⇒ menor entero mayor o igual que
(truncate x) ⇒ parte entera
(round x) ⇒ redondeo

(gcd n1 ...) ⇒ máximo común divisor


(lcm n1 ...) ⇒ mínimo común múltiplo

(exp z) ⇒ función exponencial

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

(expt z1 z2) ⇒ potencia


(sqrt z) ⇒ raíz cuadrada

(quotient n1 n2) ⇒ n1 div n2


(remainder n1 n2) ⇒ resto de div, signo del numerador
(modulo n1 n2) ⇒ resto de div, signo del denominador

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.

Definición recursiva de funciones aritméticas


A la hora de aplicar la recursión sobre números fijaremos la base y la recurrencia de forma similar a
como hacemos con las listas. Veamos un ejemplo.

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.

Base: si n=0 entonces suma-armonicos(0)=0


Recurrencia: conocemos suma-armonicos(n-1),, entonces
suma-armonicos(n) = (1/n) + suma-armonicos(n-1)

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:

Base: indice(lista,0) es car(lista)


Recurrencia: conocemos indice(cdr(lista), n-1) e indice(lista, n)=indice(cdr(lista),n-1)

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

Ahora podemos implementarla en Scheme como:

(define (indice lista n)


(if (list? lista) (_indice lista n)
(error "argumento lista no es una lista")))

Nota: La función error interrumpe el proceso de evaluación, muestra sus argumentos y devuelve el
prompt al usuario.

(define (_indice lista n)


(cond ((null? lista) (error "indice fuera de rango"))
((zero? n) (car lista))
(else (_indice (cdr lista) (- n 1)))))

Aritmética exacta por abstracción de datos


A la hora de trabajar con operaciones aritméticas nos encontramos con resultados inexactos, el caso
más simple lo encontramos al evaluar un cociente de enteros. En este caso el resultado no tiene porque ser
exacto y así al dividir (1 / 3) obtenemos el número inexacto 0.333333... que es distinto de 1/3.

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.

Abstrayéndonos de su representación vamos a definir la funciones que servirán de interfaz para el


acceso y construcción de racionales.

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:

(rnum rac) ⇒ numerador


(rden rac) ⇒ denominador

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:

(crea-rac (rnum rac) (rden rac)) ⇒ rac

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).

(define (rzero? rac)


(zero? (rnum rac)))

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.

(define (rpositive? rac)


(OR (AND (positive? (rnum rac)) (positive? (rden rac)))
(AND (negative? (rnum rac)) (negative? (rden rac)))))

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< :

(define (r> X Y) (rpositive? (r- X Y)))


(define (r< X Y) (r> Y X))

De igual forma pueden construirse muchas otras operaciones.

Funciones como argumento: MAXIMO Y MINIMO


Para acabar vamos a definir la funcion rmax que recibe 2 racionales y devuelve el 1º en caso de que
sea mayor que el segundo, en otro caso devuelve el 2º.
Para su definición nos basamos en como se define básica de max para los reales:

(define (max X Y) (if (> X Y) X Y))

Para rmax basta cambiar el operador ">":

(define (rmax X Y) (if (r> X Y) X Y))

Para rmin cambiamos de nuevo el operador:

(define (rmin X Y) (if (r< X Y) X Y))

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

lo que formalmente se expresaría por:

valor-extremo::A×A×(A×A→Booleano)→A

Donde Operador es la función a aplicar a los argumentos X e Y. Esta función devuelve X si el


resultado de evaluar (oper X Y) es #t e Y en caso contrario. Así la definición de valor-extremo es:

(define (valor-extremo oper X Y)


(if (oper X Y) X Y))

De esta forma rmax y rmin se definirán como:

(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 (rnum rac) (car rac))


(define (rden rac) (cadr rac))

(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.

rnum → (car rac)


rden → (cdr rac)
crea-rac → (cons a b)

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.

Definición generalizada de funciones en Scheme - Funciones LAMBDA


Scheme proporciona un método más elegante que el ya visto para la definición de funciones. Este se
basa en el Cálculo-λ introducido por el lógico Alonzo Church (1932-33). En realidad los lenguajes
funcionales son una versión "suavizada" de este cálculo. Veámoslo con un ejemplo:

(cons 19 '( )) ⇒ (19)

Tratamos ahora de escribir una función que recibe un argumento y devuelve una lista con dicho
argumento mediante una expresión lambda.

(lambda (item) (cons item '( )))

Si ahora aplicamos la expresión a 19:

((lambda (item) (cons item '( ))) 19) → (19)

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 '( ))))

A partir de ahí puede ser aplicada como otra función más:

(haz-lista-de-uno 19) ⇒ (19)

La forma general de una expresión-λ es la siguiente:

(lambda (par1 …) <expresion> ) ⇒ función λ

y la definición con nombre sería:

(define <nom-funcion> <expr-lambda>) ⇒ función <nom-funcion>

Ejemplo:

> (define (make-suma num)


(lambda (X) (+ X num)))
make-suma

> (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:

(lambda <Lista-Args> <expresion>)

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>.

(lambda (<Arg1> ... <Argn> . <Argn+1>) <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>

Funciones de orden superior


A partir de la definición de este tipo de función se expondrán varios ejemplos de su uso y las
funciones de orden superior (FOS) predefinidas en Scheme.

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:

Definir 3 programas funcionales que retornen los valores de:

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:

f(a,b) ::= si a>b


entonces 0
sino a+f(a+1,b)
fsi

g(a,b) ::= si a>b


entonces 0
sino a*a*a+g(a+1,b)
fsi

h(a,b) ::= si a>b


entonces 0
sino 1/(a*(a+2)) + h(a+4,b)
fsi

14
Las tres funciones son muy similares, pudiéndose definir una función que es la abstracción de todas
ellas:

sum(term, a, next, b) ::= si a>b


entonces 0
sino term(a)+sum(term, next(a), next, b)
fsi

En cuyo caso la definición de la función f(a,b) se realizaría de la forma siguiente:

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))

Funciones de orden superior predefinidas

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í:

>(max '(2 4))


error
>

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:

(apply <funcion> <lista-de-elementos>)

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:

> (apply max '(2 4)) ; equivalente a (max 2 4)


4
> (apply + '(4 11 23)) ; equivalente a (+ 4 11 23)
38
>

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:

(map <funcion> <list1> <list2> ...)

15
Ejemplos:

> (map car '((a b) (c d) (e f)))


(a c e)
> (map + '(1 2) '(4 5 6))
(5 7)
>

Sumar 2 a los elementos de una lista


> (map (lambda(x) (+ z 2)) '(1 2 3 4))
(3 4 5 6)
>

Comprobar que el átomo a pertenece a las listas elementos de una lista


> (map (lambda(x) (member? 'a x)) '((a b c) (b c d) (c d a)))
(#t #f #t)
>

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.

>(for-each display '((a b) (c d) (e f)))


(a b) (c d) (e f)
>

Ejemplo de uso de funciones de orden superior:

Traspuesta de una matriz:

(tras '((1 2) (3 4) (5 6))) ⇒ ((1 3 5) (2 4 6))

sería :

(define (tras Mat)


(apply map List Mat))

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:

(let ( (id1 val1) (id2 val2) … (idn valn) ) body)

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]

Definición de la distancia euclídea: ( X 1 − X 2) + (Y 1 − Y 2) , la expresión body aparece en


2 2

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))))))

[33] (distancia '(2 3) '(3 6))


2
[34]

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:

La función + toma 2 argumentos (números) y devuelve su suma. Podemos definir un procedimiento


(función) add5 que suma 5 a su argumento:

(define add5 (lambda (n) (+ 5 n)))


o
(define (add5 n) (+ 5 n))

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)))

Así (curried+ 5) devuelve una función definida como:

(lambda (n) (+ 5 n)) ; m queda ligado a 5

Para sumar ahora 5 y 7, llamaríamos a:

((curried+ 5) 7) → 12

Es más, ahora podemos definir add5 como:

(define add5 (curried+ 5))

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:

(define (member-c item)


(letrec ((helper
(lambda (ls)
(if (null? ls)
#f
(or (equal? (car ls) item)
(helper (cdr ls)))))))
helper))

Ahora podemos definir un member? en función de member-c

(define (member? item lista)


((member-c item) lista))

Ejemplo con map: La función map tiene al menos 2 parámetros: una función fuc y una lista ls.

(map add1 '(1 2 3 4)) → (2 3 4 5)

Su definición:

(define (map fuc ls)


(if (null? Ls)
'( )
(cons (fuc (car ls)) (map fuc (cdr ls)))))

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:

(define (apply-to-all fuc)


(letrec
((helper (lambda(ls)
(if (null? Ls)
'( )
(cons (fuc (car ls)) (helper (cdr ls)))))))
helper)))

Ejemplo:

> ((apply-to-all add1) '(1 2 3 4))


(2 3 4 5)
>

A partir de ella definimos map como:

(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)))))))

[36] (swapper 0 1 '(0 1 2 0 1 2))


(1 0 2 1 0 2)
[37]

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:

[37] ((swapper-m 0 1) '(0 1 2 0 1 2))


(1 0 2 1 0 2)
[38]

Podemos además darle nombre int01:


[38] (define int01 (swapper-m 0 1))
int01
[39]

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

Estrategias de evaluación: Perezosa vs Impaciente


Una computadora para evaluar una expresión aplica un proceso de reducción, o simplificación, hasta
alcanzar una expresión no reducible que es el resultado de la evaluación. Esta expresión final decimos que
está en forma canónica o en forma normal.
Cada lenguaje de programación presenta una determinada estrategia de reducción de expresiones, que
establece el orden en que deben ser reducidas las expresiones para alcanzar la forma normal. En cada
paso se aplica una regla predefinida o de usuario que reescribe o reduce expresión seleccionada de
acuerdo con la estrategia de reducción. En cualquier caso el resultado de la evaluación debe ser el mismo.

Vamos a ver dos de las estrategias principales de evaluación:

ƒ 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

Otra posible secuencia de reducción con evaluación perezosa es

(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

Utilizando la estrategia perezosa la secuencia es

(fst (cuadrado 4) (cuadrado 2))


= ; definición de fst
(cuadrado 4)
= ; definición de cuadrado
(* 4 4)
= ; definición de *
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:

Generador Filtro / Filtro / Modulador


de señal Modulador ... Modulador de salida Salida

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:

(opn . . . (op2 (op1 Stream-Entrada) ) . . .) ⇒ Salida

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

the-empty-stream ⇒ se define como el stream vacío

(empty-stream? s) ⇒ cierto si s es el stream nulo

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:

;; suma de los cuadrados de las hojas impares de un arbol binario


;; arbol: ( <arbol-izq> <arbol-der>)
;; arbol(degenerado): <Num-Hoja>

(define (suma-cuadrados-impares arbol)


(if (nodo-hoja? arbol)
(if (odd? arbol)
(square arbol)
0)
(+ (suma-cuadrados-impares (rama-izq arbol))
(suma-cuadrados-impares (rama-der arbol)))))

Para implementarla sobre streams identificamos la secuencia de operaciones como

ENUMERA: FILTRO: MAP: ACUMULA:


hojas del árbol odd? square +, 0

La definición de estas funciones sobre streams es la siguiente:

(define (enumera-hojas arbol)


(if (nodo-hoja? arbol)
(cons-stream arbol the-empty-stream)
(append-streams (enumera-hojas (rama-izq arbol))
(enumera-hojas (rama-der arbol)))))

;; CONCATENACION de streams

(define (append-streams s1 s2)


(if (empty-stream? s1)
s2
(cons-stream (head s1)
(append-streams (tail s1) s2))))

;; FILTRA IMPARES de un stream

(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)))))

;;; MAP square a lo elemento de un stream

23
(define (map-square s)
(if (empty-stream? s)
the-empty-stream
(cons-stream (square (head s))
(map-square (tail s)))))

;;; ACUMULA la suma de los elementos del stream

(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:

(define (suma-cuadrados-impares arbol)


(accumulate-+
(map-square
(filter-odds
(enumera-hojas arbol)))))

Funciones de orden superior sobre streams


Las funciones sobre streams, como accumulate-+, map-square o filter-odds pueden generalizarse a
funciones de orden superior que nos faciliten la resolución de problemas similares sobre streams:

;;; elimina de s los elementos X que no cumplen (oper X)


(define (filter-s oper s)
(cond ((empty-stream? s) the-empty-stream)
((oper (head s))
(cons-stream (head s) (filter-s oper (tail s))))
(else (filter-s oper (tail s)))))

;;; MAP oper sobre los elementos de un stream

(define (map-s oper s)


(if (empty-stream? s)
the-empty-stream
(cons-stream (oper(head s))
(map-s oper (tail s)))))

;;; ACUMULA la suma de los elementos del stream


;;; similar a foldl sobre listas

(define (accumulate-s oper val-ini s)


(if (empty-stream? s)
val-ini
(oper (head s) (accumulate-s oper val-ini (tail s)))))

Así el ejemplo anterior se reduce a la siguiente definición:

(define (suma-cuadrados-impares arbol)


(accumulate-s + 0
(map-s square
(filter-s odd?

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:

(head (tail (filter-s prime? (enumerate-interval 10000 1000000))))

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:

(delay <expr>) ⇒ pospone la evaluación de <expr>


el resultado de eval se denomina una promesa

(force <expr>) ⇒ fuerza la evaluación de la promesa <expr>


una vez evaluada una promesa, se registra su valor y no se recalcula

De acuerdo con estas dos funciones, podemos ahora definir ahora:

(define (head s) (car s))

(define (tail s) (force (cdr s)))

(define (cons-stream <a> <b>)


(cons <a> (delay <b>))

(define the-empty-stream '())

(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.

Volviendo al problema anterior, la función enumerate-interval quedaría definida como :

(define (enumerate-interval n1 n2)


(if (> n2 n1)
the-empty-stream
(cons-stream n1 (enumerate-interval (+ n1 1) n2))))

Con lo que la evaluación de la expresión

(enumerate-interval 10000 1000000) ⇒ (10000 . <promesa>)

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)))

(define naturales (enteros-desde 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:

(filter-s prime? (tail naturales))

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).

Streams Infinitos definidos implícitamente


De nuevo podemos basarnos en la analogía con el procesamiento de señales eléctricas. En ellas, tal y
como muestra la siguiente figura, podemos tener una salida que no sólo depende de la entrada actual sino
también de la salida inmediatamente anterior por medio de una retroalimentación (o feedback).

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))))

(define fibs (fibgen 0 1))

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:

(define unos (cons-stream 1 unos))

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:

(define (add-streams s1 s2)


(cond ((empty-stream? s1) s2)
((empty-stream? s2) s1)
(else (cons-stream (+ (head s1) (head s2))
(add-streams (tail s1) (tail s2))))))

26
Ahora podemos definir el conjunto de los enteros como:

(define enteros (cons-stream 1 (add-streams unos enteros)))

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.

II.5 Ejemplos de programación simbólica: derivación simbólica y


matching.
En este apartado veremos dos ejemplos donde se explota la capacidad para la computación simbólica del
lenguaje Scheme.

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.

1) Identificación de los tipos básicos de datos que vamos a manejar:

Operandos Operadores
• constantes numéricas • suma
3, 4, 6 (+ sum1 sum2)
• variables • producto
X, Y, Z (* mul1 mul2)

constant?(x) ::= number?(x)

variable?(x) ::= symbol?(x)

same-variable?(v1,v2) ::=
variable?(v1) ∧ variable?(v2) ∧ eq?(v1,v2)

2) Selectores, constructores y funciones de test de las expresiones (sumas y productos)


empleados.

• Constructores

make-sum(a1,a2) ::= list('+,a1,a2)

make-product(a1,a2) ::= list('*,a1,a2)

• Selectores

addend(s) ::= cadr(s)

augend(s) ::= caddr(s)

multiplier(p) ::= cadr(p)

multiplicand(p) ::= caddr(p)

• 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.

4) Versión mejorada de make-sum


Vamos a tratar de realizar la simplificación de la suma a la vez que se construye. Los casos
analizados son la suma de constantes y la suma con el elemento neutro.

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

(deriv '(+ x x) 'x) ⇒ 2

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:

Patrón Lista Matching


(a b * c ? d) (a b (c d) e h c f d) si
(a b * c) (a b c) si
(a b ? c) (a b c) no

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.

(define (nulo? patron)


(cond ((null? patron))
(else (and (eq? (car patron) '*)
(nulo? (cdr patron))))))

(define (match patron lista)


(cond ((null? patron) (null? lista)) ;; fin de ambas

((null? lista) (nulo? patron)) ;; fin de lista, ¿y patron?

((eq? (car patron) '?) ;; metacaracter ?


(match (cdr patron) (cdr lista)))

((eq? (car patron) '*) ;; metacaracter *


(or (match (cdr patron) (cdr lista))
(match patron (cdr lista))))

((eq? (car patron) (car lista)) ;; coinciden elementos


(match (cdr patron) (cdr lista)))

(else #f)))

Normalmente, además de saber si se produce o no matching, es interesante saber cómo se produce el


mismo. En este sentido una variante de la función match, bastante habitual, consiste en incorporar en el
patrón pares de valores metacaracter-variable, de modo que de producirse el matching la función retorna
una lista en la que cada variable le corresponde la parte de la lista con la que se produjo dicho matching.

Ejemplos:

Patrón Lista Matching


(a b (* x) c (? y) d) (a b (c d) e h c f d) ((x ((c d) e h )) (y f))
(a b (* x) c) (a b c) (x ())
(a b (? x) c) (a b c) no

30