apuntes scheme

31

Click here to load reader

Upload: jaimejuliao

Post on 23-Jun-2015

1.909 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Apuntes Scheme

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

Page 2: Apuntes Scheme

1

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

5)) 3*(4

+5)

Page 3: Apuntes Scheme

2

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 ) ) ( 2 1 ) [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]

Page 4: Apuntes Scheme

3

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 ) ) ) ( ( a b ) c ( d e ) )

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

#t (symbol? -4.6) →

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

#f (symbol? 'doce) →

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

#t (procedure? +) → #t

(procedure? 'cons) →#f

(procedure? 100) →#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]

Page 5: Apuntes Scheme

4

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

Page 6: Apuntes Scheme

5

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

Page 7: Apuntes Scheme

6

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)

Page 8: Apuntes Scheme

7

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

Page 9: Apuntes Scheme

8

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

n1...

31

211 ++++

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

Page 10: Apuntes Scheme

9

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

Un número racional ba

está formado por 2 enteros: a su numerador y b su denominador que debe ser

distinto de 0.

Page 11: Apuntes Scheme

10

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:

(crea-rac 3 4) ⇒ 43

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

La suma de dos fracciones ba

y dc

es otra fracción cuyo numerador es a*d + b*c y su denominador

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

Page 12: Apuntes Scheme

11

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

Otra función interesante es la igualdad entre racionales, nos basamos en que dcbabc

ba .. =↔= ,

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

Page 13: Apuntes Scheme

12

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

Page 14: Apuntes Scheme

13

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.

Page 15: Apuntes Scheme

14

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:

1. ∑=

=b

an

nbaf ),(

2. ∑=

=b

an

nbaf 3),(

3. ∑= +−−

=b

an sananbaf

)34)(34(1),(

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

Page 16: Apuntes Scheme

15

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

Page 17: Apuntes Scheme

16

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

Page 18: Apuntes Scheme

17

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: 22 )21()21( YYXX −+− , la expresión body aparece en 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.

Page 19: Apuntes Scheme

18

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.

Page 20: Apuntes Scheme

19

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

Page 21: Apuntes Scheme

20

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.

Page 22: Apuntes Scheme

21

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

Page 23: Apuntes Scheme

22

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

Generadorde señal

Filtro /Modulador

Filtro /Modulador

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

Page 24: Apuntes Scheme

23

(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:hojas del árbol

FILTRO:odd?

MAP:square

ACUMULA:+, 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

Page 25: Apuntes Scheme

24

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

Page 26: Apuntes Scheme

25

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

Page 27: Apuntes Scheme

26

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

Page 28: Apuntes Scheme

27

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

• constantes numéricas 3, 4, 6

• variables X, Y, Z

Operadores • suma

(+ sum1 sum2) • producto

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

Page 29: Apuntes Scheme

28

sum?(x) ::= si ¬atom?(x) entonces eq?(car(x),`+) sino falso fsi

product?(x) ::= si ¬atom?(x) entonces eq?(car(x),'*) sino falso fsi

Page 30: Apuntes Scheme

29

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

Page 31: Apuntes Scheme

30

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