5 El Lenguaje PLAY
Racket es a la vez un lenguaje y una plataforma para definir varios lenguajes y sus extensiones, permitiendo construir programas a partir de módulos definidos en múltiples lenguajes. Por ejemplo, este documento está escrito en el lenguaje Scribble, una especie de LaTeX moderno adecuado para documentar programas. El documento OOPLAI, que usaremos para estudiar OOP en el curso, también fue escrito en Scribble. Otros lenguajes que fueron definidos con Racket incluyen lenguajes pedagógicos, un Racket con tipos estáticos, Datalog para programación lógica, un lenguaje para definir aplicaciones web, un lenguaje para especificar lenguajes formalmente, y muchos más...
El curso de lenguajes no explora esta faceta de Racket, pero sí usaremos variantes de Racket. El curso usa principalmente el lenguaje PLAY, que está basado en el lenguaje PLAI. PLAI agrega esencialmente funciones convenientes para especificar tests y para definir y manejar estructuras de datos.
PLAY reutiliza los mecanismos para tests de PLAI, y simplifica el uso de pattern matching sobre las estructuras de datos, de forma que sea análogo al caso de matching sobre listas. Además posee una sintáxis más sucinta para definir valores, basándose también en el uso de pattern matching.
Para instalar el lenguaje PLAY debe utilizar la versión más reciente de DrRacket, e instalar el paquete play: File > Package Manager > especificar play en Package Source, y luego Install.
5.1 Definiendo Identificadores
PLAY introduce la forma def que permite introducir identificadores de forma simple o utilizando pattern matching. En el primer caso, def se utiliza de la misma forma que define. Por ejemplo:
(def x-max 100) (def y-max 100)
Cuando desarrollamos funciones que involucran varios cómputos intermedios, es una buena práctica introducir identificadores para referirse a esos valores, y así mejorar la legibilidad del código.
Por ejemplo, considere que representamos puntos como un par de coordenadas. Podemos calcular la distancia entre dos puntos como:
; distance :: (cons Int Int) (cons Int Int) -> Float ; Calcula la distancia entre dos puntos (define (distance p1 p2) (def x1 (car p1)) (def x2 (car p2)) (def y1 (cdr p1)) (def y2 (cdr p2)) (sqrt (+ (expt (- x1 x2) 2) (expt (- y1 y2) 2))))
> (distance (cons 0 0) (cons 1 1)) 1.4142135623730951
> (distance (cons 3 4) (cons 9 0)) 7.211102550927978
Aquí utilizamos def para nombrar las coordenadas de cada punto, de la misma forma que usaríamos define. Sin embargo, acceder a los valores de una estructura arbitraria puede ser bastante engorroso. Utilizando una definición basada en patrones se simplifica bastante el código:
; distance :: (cons Int Int) (cons Int Int) -> Float ; Calcula la distancia entre dos puntos (define (distance p1 p2) (def (cons x1 y1) p1) (def (cons x2 y2) p2) (sqrt (+ (expt (- x1 x2) 2) (expt (- y1 y2) 2))))
> (distance (cons 0 0) (cons 1 1)) 1.4142135623730951
> (distance (cons 3 4) (cons 9 0)) 7.211102550927978
def es un alias de match-define.
5.2 Tests
El lenguaje PLAY introduce test para verificar que una expresión evalúa al resultado esperado:
> (test (+ 1 2) 3) (good (+ 1 2) 3 3 "at line 10")
> (test (+ 1 2) 4) (bad (+ 1 2) 3 4 "at line 11")
Además, el lenguaje PLAY introduce test/exn para testear por errores:
(define (mysqrt x) (if (and (number? x) (positive? x)) (sqrt x) (error "mysqrt: should be called with positive number")))
> (test/exn (mysqrt -1) "positive") (good (mysqrt -1) "mysqrt: should be called with positive number" "positive" "at line 13")
El segundo argumento de test/exn tiene que ser un sub-string del mensaje de error esperado.
> (test/exn (/ 1 0) "by zero") (exception (/ 1 0) "/: division by zero" '<no-expected-value> "at line 15")
Por defecto PLAY muestra mensajes de éxito o fallo de los tests. Para mostrar mensajes sólo para los test fallidos, se debe evaluar:
(print-only-errors #t)
La evaluación de print-only-errors afecta todas las expresiones que le siguen, y en general basta con utilizarla al principio del archivo.
5.3 Estructuras de datos
Racket incluye una forma básica de definir nuevas estructuras de datos. El lenguaje PLAY provee una forma más elaborada y práctica, que usaremos en el curso. Se llama deftype. Con deftype, uno introduce un nombre para la estructura de datos, y luego especifica uno o más variantes de este tipo de dato. Por ejemplo:
deftype en conjunto con el uso de pattern matching (match y def) reemplazan la funcionalidad de define-type y type-case provistos por el lenguaje PLAI
(deftype BinTree (leaf value) (node value left right))
Note que cada variante (en este caso leaf y node) pueden tener distintas cantidades de atributos (incluso ninguno). Cada atributo se define simplemente con un nombre (por ej. value).
Cuando se usa deftype, se generan automaticamente varias funciones. Primero, se crean constructores, uno por variante, que permiten construir datos:
> (leaf 10) (leaf 10)
> (node 10 (leaf 3) (node 4 (leaf 1) (leaf 2))) (node 10 (leaf 3) (node 4 (leaf 1) (leaf 2)))
También se generan accesores, para inspeccionar datos existentes. Los accesores tienen el nombre variante-atributo, por ejemplo:
> (leaf-value (leaf 10)) 10
> (def y (node 1 (leaf 2) (leaf 3)))
> (node-right y) (leaf 3)
> (leaf-value (node-left y)) 2
> (BinTree? 10) #f
> (BinTree? (leaf 10)) #t
> (BinTree? '(10)) #f
> (BinTree? (node 10 (leaf 1) (leaf 2))) #t
> (leaf? 10) #f
> (leaf? (leaf 10)) #t
> (node? '(1 2 3)) #f
> (node? (node 3 (leaf 1) (leaf 2))) #t
> (node? (leaf 5)) #f
Para hacer un análisis por casos de una estructura definida usando deftype, simplemente utilizamos pattern matching sobre cada uno de sus constructores o variantes. Por ejemplo:
(define (contains? bt n) (match bt [(leaf v) (equal? v n)] [(node v l r) (or (equal? v n) (contains? l n) (contains? r n))]))
> (contains? (leaf 1) 2) #f
> (contains? (node 10 (leaf 3) (node 4 (leaf 1) (leaf 2))) 1) #t
El lenguaje Python soporta una una versión limitada de asignación basada en patrones, para el caso particular de tuplas y listas.
También podemos usar def para acceder a los componentes de la estructura:
> (def y (leaf 3))
> (def (node v l r) (node 1 (leaf 2) y))
> v 1
> l (leaf 2)
> r (leaf 3)
Note que a diferencia de la representación de árboles binarios usando listas del capítulo anterior, ahora no debemos preocuparnos del orden en que van los patrones correspondientes a los constructores de cada variante de la estructura.
La razón de fondo es que en una estructura de datos definida inductivamente usando deftype se cumple que:
Los constructores son funciones inyectivas. Por ejemplo (equal? (leaf a) (leaf b)) evalúa a #t solamente si (equal? a b) es #t.
Valores construidos con distintos constructores nunca son iguales.
No existe otra manera de construir valores de la estructura si no es con las variantes definidas.
Por lo tanto, si seguimos la receta de diseño y hacemos matching solamente sobre las variantes de una estructura, no tendremos que preocuparnos de matchings accidentales o inesperados.
La única precaución que debemos tener es recordar ser exhaustivos en el análisis de casos, para asegurarnos que las funciones sean completas para el tipo de datos considerado.
Ejercicios: Defina las funciones map-tree y sum-tree que operan sobre BinTrees.
5.4 Un último ejemplo
A modo de último ejemplo, considere un mini-lenguaje para describir expresiones aritméticas con números, adición y substracción:
| ‹expr› | ::= | ( num ‹number› ) |
|
| | | ( add ‹expr› ‹expr› ) |
|
| | | ( sub ‹expr› ‹expr› ) |
Note que la definición BNF nos da la gramática del lenguaje o, en forma equivalente, una descripción de la estructura de los árboles de sintáxis abstracta. Podemos describir el tipo de dato Expr directamente con deftype:
(deftype Expr (num n) (add left right) (sub left right))
(add (num 1) (sub (num 5) (num 2)))
(calc (add (num 1) (sub (num 5) (num 2))))
(define (calc expr) (match expr [(num n) ...] [(add l r) ... (calc l) ... (calc r) ...] [(sub l r) ... (calc l) ... (calc r) ...]))
; calc :: Expr -> number ; retorna el valor de la expresión dada (define (calc expr) (match expr [(num n) n] [(add l r) (+ (calc l) (calc r))] [(sub l r) (- (calc l) (calc r))])) (test (calc (add (num 1) (sub (num 5) (num 2)))) 4)