Introducción
Una de las cosas que me gustan de arc es que tiene un poquito de sintaxis, que ayuda a acortar la longitud de los programas. En particular, con ~ se indica la versión negada de una función.
Por ejemplo:
(equal? 1 1) ; ==> #t (~equal? 1 1) ; ==> #f (equal? 1 2) ; ==> #f (~equal? 1 2) ; ==> #t
Creando una readtable para ~
Empecemos por crear la readtable, que es la parte que tiene el código más específico de este ejemplo. Vamos a poner todos estos archivos en un nuevo directorio llamado neg.
Archivo: neg/main.rkt (versión 1)
#lang racket/base (provide (rename-out (neg-read read) (neg-read-syntax read-syntax))) {define (neg-read in) (parameterize ([current-readtable (make-neg-readtable)]) (read in))} {define (neg-read-syntax src in) (parameterize ([current-readtable (make-neg-readtable)]) (read-syntax src in))} {define (make-neg-readtable) (make-readtable (current-readtable) #\~ 'non-terminating-macro neg-parse)} {define neg-parse (case-lambda [(ch port) ; for read (let ([next (read/recursive port #f)]) `(negate ,next))] [(ch port src line col pos) ; for read-syntax (let ([next (read-syntax/recursive src port #f)]) (datum->syntax #f `(negate ,next) (let-values ([(l c p) (port-next-location port)]) (list src line col pos (and pos (- p pos))))))])}
Podemos probar esto usando #reader. Lo malo de usar este ejemplo con #reader es que sólo lee una expresión, así que tenemos que encerrar todo lo que queremos leer en un begin. Además como la función negate no está en racket/base, hay que recordar importarla desde racket/function.
Archivo: neg/test1.rkt
#lang racket/base (require (only-in racket/function negate)) #reader "main.rkt" [begin (equal? 1 1) ; ==> #t (~equal? 1 1) ; ==> #f (equal? 1 2) ; ==> #f (~equal? 1 2) ; ==> #t ({lambda (x) (equal? x 0)} 0) ; ==> #t (~{lambda (x) (equal? x 0)} 0) ; ==> #f ({lambda (x) (equal? x 0)} 1) ; ==> #f (~{lambda (x) (equal? x 0)} 1) ; ==> #t ]
El nombre de ~equal?
El problema de la versión 1 es que la función creada por ~equal? no tiene un lindo nombre.
Archivo: neg/test2.rkt
#lang racket/base (require racket/function) #reader "main.rkt" [begin (displayln equal?) ; ==> #(displayln ~equal?) ; ==> #<...ket function.rkt:36:11=""> :( equal? ; ==> # ~equal? ; ==> #<...ket function.rkt:36:11=""> :( ]</...ket> </...ket>
Se podría arreglar usando procedure-rename, pero en vez de eso vamos crear la función directamente con lambda y ponerle el nombre usando la propiedad 'inferred-name. Esta versión tiene sus ventajas y desventajas porque arregla los nombres, pero no funciona con argumentos con keywords y no está optimizada para funciones con 1 ó 2 argumentos. Hacer una versión que arregle todos los detalles es posible, pero está en contra de la idea de que ~ produzca sólo una transformación simple.
Archivo: neg/main.rkt (versión 2)
#lang racket/base (provide neg-read neg-read-syntax make-neg-readtable (rename-out [neg-read read] [neg-read-syntax read-syntax])) {define (neg-read in) (parameterize ([current-readtable (make-neg-readtable)]) (read in))} {define (neg-read-syntax src in) (parameterize ([current-readtable (make-neg-readtable)]) (read-syntax src in))} {define (make-neg-readtable) (make-readtable (current-readtable) #\~ 'non-terminating-macro neg-parse)} {define neg-parse (case-lambda [(ch port) (let ([next (read/recursive port #f)]) `(#%expression {lambda args (not (apply ,next args))}))] [(ch port src line col pos) (let ([next (read-syntax/recursive src port #f)]) (let ([stx (datum->syntax #f `(#%expression {lambda args (not (apply ,next args))}) (let-values ([(l c p) (port-next-location port)]) (list src line col pos (and pos (- p pos)))))]) (if (identifier? next) (syntax-property stx 'inferred-name (string->symbol (string-append (list->string (list ch)) (symbol->string (syntax-e next))))) stx)))])}
¿Por que ~equal? no es equal? a ~equal? ?
Otro detalle es que en cada ejecución ~equal? crea una nueva función, así que son todas distintas. Me gustaría que fueran iguales porque al mirar (equal? ~equal? ~equal?) dan ganas de que sea #t, pero hasta ahora las soluciones que se me ocurrieron son largas y destruyen el optimizador de Racket.
Archivo: neg/test3.rkt
#lang racket/base (provide neg-read neg-read-syntax make-neg-readtable (rename-out [neg-read read] [neg-read-syntax read-syntax])) {define (neg-read in) (parameterize ([current-readtable (make-neg-readtable)]) (read in))} {define (neg-read-syntax src in) (parameterize ([current-readtable (make-neg-readtable)]) (read-syntax src in))} {define (make-neg-readtable) (make-readtable (current-readtable) #\~ 'non-terminating-macro neg-parse)} {define neg-parse (case-lambda [(ch port) (let ([next (read/recursive port #f)]) `(#%expression {lambda args (not (apply ,next args))}))] [(ch port src line col pos) (let ([next (read-syntax/recursive src port #f)]) (let ([stx (datum->syntax #f `(#%expression {lambda args (not (apply ,next args))}) (let-values ([(l c p) (port-next-location port)]) (list src line col pos (and pos (- p pos)))))]) (if (identifier? next) (syntax-property stx 'inferred-name (string->symbol (string-append (list->string (list ch)) (symbol->string (syntax-e next))))) stx)))])}
Extendiendo un lenguaje con neg/neg-exp
Para poder usar esta extensión de una manera sencilla, vamos a crear un lenguaje neg/neg-exp que agregue ~ a un lenguaje cualquiera.Para ello es necesario como primer paso crear la estructura de directorios correcta y registrarlo como una colección. Al directorio principal donde están estos archivos lo llamamos (por ejemplo) neg. Para registrar la colección hay que ir al directorio que contiene al directorio neg y ejecutar el comando raco link neg. En este directorio tiene que estar el archivo neg/main.rkt y además hay que crear los subdirectorios necesarios para tener el archivo neg/neg-exp/lang/reader.rkt.
Archivo: neg/neg-exp/lang/reader.rkt
#lang racket/base (require syntax/module-reader (only-in neg make-neg-readtable)) (provide (rename-out [neg-read read] [neg-read-syntax read-syntax] [neg-get-info get-info])) (require (only-in racket/function negate)) (provide negate) {define (wrap-reader p) (lambda args (parameterize ([current-readtable (make-neg-readtable)]) (apply p args)))} {define-values (neg-read neg-read-syntax neg-get-info) (make-meta-reader 'neg-exp "language path" {lambda (str) (let ([s (string->symbol (string-append (bytes->string/latin-1 str) "/lang/reader"))]) (and (module-path? s) s))} wrap-reader wrap-reader {lambda (proc) {lambda (key defval) (case key #;[(color-lexer) (dynamic-require 'syntax-color/scheme-lexer 'scheme-lexer)] [else (if proc (proc key defval) defval)])}})}
Ahora podemos usar neg-exp con un lenguaje cualquiera, por ejemplo racket o racket/base. Lo bueno es que ya no hace falta encerrar todo el programa en un solo begin. Además, al haber registrado a neg como una colección, se puede llamar a esta extensión desde cualquier archivo, sin importan la posición relativa de los directorios en que están localizados.
Archivo: neg/test4.rkt
#lang neg/neg-exp racket/base (require (only-in racket/function negate)) (equal? 1 1) ; ==> #t (~equal? 1 1) ; ==> #f (equal? 1 2) ; ==> #f (~equal? 1 2) ; ==> #t ({lambda (x) (equal? x 0)} 0) ; ==> #t (~{lambda (x) (equal? x 0)} 0) ; ==> #f ({lambda (x) (equal? x 0)} 1) ; ==> #f (~{lambda (x) (equal? x 0)} 1) ; ==> #t
Lenguajes neg/racket y neg/racket/base
Los dos lenguajes que más probablemente uno quiere usar extendidos con ~ son racket y racket/base, por lo que vale la pena crear las combinaciones como lenguajes independientes. Veamos como hacer esto para neg/racket/base. Para eso hay que crear la estructura de subdirectorios correcta y dos archivos.Primero creamos el archivo que define el lenguaje. Este lenguaje es igual que racket/base, pero le agregamos negate por si hace falta. Además vamos a usar las funciones read y read-syntax definidas en neg y no las originales de racket/base.
Archivo: neg/racket/base.rkt
#lang racket/base (require "../main.rkt") (require (only-in racket/function negate)) (provide (all-from-out racket/base) read read-syntax negate)
Archivo: neg/racket/base/lang/reader.rkt
#lang s-exp syntax/module-reader neg/racket/base #:read read #:read-syntax read-syntax #:language-info '#(racket/language-info get-info #f) (require neg)
Archivo: neg/test5.rkt
#lang neg/racket/base (equal? 1 1) ; ==> #t (~equal? 1 1) ; ==> #f (equal? 1 2) ; ==> #f (~equal? 1 2) ; ==> #t ({lambda (x) (equal? x 0)} 0) ; ==> #t (~{lambda (x) (equal? x 0)} 0) ; ==> #f ({lambda (x) (equal? x 0)} 1) ; ==> #f (~{lambda (x) (equal? x 0)} 1) ; ==> #t
El mismo proceso se puede usar para crear neg/racket, haciendo los cambios obvios.