On this page:
5.1 Definiendo Identificadores
5.2 Tests
5.3 Estructuras de datos
5.4 Un último ejemplo

5 El Lenguaje PLAY

Éric Tanter

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.

Es importante notar que text/exn solo detecta errores reportados explícitamente por nuestro código usando error, como en el ejemplo anterior. No detecta errores lanzados implicitamente por el runtime del lenguaje:
> (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

Finalmente, se generan predicados de tipo, para poder clasificar datos respecto al tipo general y sus variantes:
> (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:

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

Un procesamiento simple de esta estructura es evaluar una expresión para obtener su valor númerico. Por ejemplo, podríamos representar la expresión 1 + (5 - 2) con la siguiente estructura:
(add (num 1)
     (sub (num 5) (num 2)))
y usar calc para obtener su valor:
(calc (add (num 1)
           (sub (num 5) (num 2))))

La definición de calc es directa, siguiendo la receta que estudiamos para procesar estructuras de datos (ver Procesar estructuras recursivas). Primero, derivamos el patrón de calc basándonos directamente en la definición del tipo de dato Expr:
(define (calc expr)
  (match expr
    [(num n) ...]
    [(add l r) ... (calc l) ... (calc r) ...]
    [(sub l r) ... (calc l) ... (calc r) ...]))
Lo cual es muy simple de completar:
; 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)
Note como deftype y match calzan perfectamente con esta metodología.