Macros
(No, no las macros C).
Las macros de raqueta le permiten realizar una metaprogramación en tiempo de compilación, escribir código que escribe código (o código que escribe código que escribe código, hasta el infinito). La capacidad de manipular las características del lenguaje que no son de primera clase le permite eliminar cada pieza de repetitivo que no puede con las funciones. Dicho de otra manera, mientras que las funciones se resumen sobre los valores, las macros se resumen sobre la sintaxis. Puede crear nuevas construcciones de lenguaje y llevar el lenguaje de programación al nivel de abstracción de su problema.
Por ejemplo, aquí hay una macro Racket simple para “factorizar” la memorización:
(define-syntax-rule (define/memoized (fx ...) e ...)
(define f
(let ([m (make-hash)])
(λ (x ...) (hash-ref! m (list x ...) (λ () e ...))))))
Aquí hay una definición normal de Fibonacci en Racket:
(define (fib n)
(if (< n 2) n
(+ (fib (- n 1)) (fib (- n 2)))))
Para memorizarlo, ahora solo cambiamos
define
a
define/memoized
:
(define/memoized (fib n)
(if (< n 2) n
(+ (fib (- n 1)) (fib (- n 2)))))
(tiempo (fib 1000))
; resultado:
; tiempo de CPU: 0 tiempo real: 0 tiempo gc: 0
; 434665576869374564356885276750406258025646605173717804024817290895365554179490
; 518904038798400792551692959225930803226347752096896232398733224711616429964409
; 06533187938298969649928516003704476137795166849228875
También puede usarlo para funciones con cualquier número de argumentos:
(define/memoized (ackermann mn)
(match* (mn)
[(0 _) (+ n 1)]
[(_ 0) (ackermann (- m 1) 1)]
[(_ _) (ackermann (- m 1) (ackermann m (- n 1)))]))
(tiempo (ack 4 1))
; resultado:
; tiempo de CPU: 204 tiempo real: 203 gc tiempo: 40
; 65533
Las macros de raquetas son higiénicas: aunque la macro anterior introduce una nueva variable
m
, no interferirá de ninguna manera con el usuario macro incluso si el usuario tiene una variable diferente también llamada
m
.
Con las macros, cualquiera que sea la característica que desee tener en su idioma, no necesita esperar a que el implementador del lenguaje agregue: es solo otra macro que puede agregar usted mismo (y compartir con otros como biblioteca si lo desea). De hecho, las macros Racket son mucho más potentes que una simple transformación de código como el ejemplo anterior: pueden realizar cálculos arbitrarios, incluidos los efectos secundarios.
Racket en sí es un lenguaje pequeño. Todas las características del lenguaje aparentemente fundamentales, como la coincidencia de patrones, el sistema de objetos o los módulos de primera clase son solo macros. Los dialectos de Racket como Typed Racket, Lazy Racket y Scribble, todos se expanden de forma macro a Vanilla Racket, y un programa puede estar compuesto de diferentes módulos escritos en diferentes idiomas (o su propio DSL), lo que considere más apropiado.
Puede encontrar más información sobre Racket aquí (tienen excelente documentación y materiales de aprendizaje).
Tipos dependientes
La mayoría de los sistemas de tipos lo ayudan a descartar ciertas clases de errores. En un lenguaje de tipo dependiente, puede ir más allá y codificar invariantes de programa arbitrarios en los tipos. Una vez que el programa se compila, sabes que es correcto (hasta las especificaciones que imponen los tipos), no porque no hayas visto el caso de prueba que falla, sino porque el compilador ha demostrado estáticamente que tu programa no puede salir mal.
Un ejemplo clásico es un vector cuyo tipo informa sobre su longitud: (el ejemplo está en Agda)
data List (A : Set) : ℕ → Set where
[] : List A 0
_∷_ : ∀ {n} → A → List A n → List A (1 + n)
Ahora puede indicar más sobre funciones como map
o append
, como “el map
conserva la longitud” o “la lista de resultados de append
es tan larga como la longitud total de sus argumentos”.
map : ∀ {AB n} → (A → B) → List A n → List B n
map f [] = []
map f (x ∷ xs) = fx ∷ map f xs
_ ++ _: ∀ {A mn} → Lista A m → Lista A n → Lista A (m + n)
[] ++ ys = ys
(x ∷ xs) ++ ys = x ∷ (xs ++ ys)
Además de la corrección, más información significa que el compilador tiene más oportunidades para optimizar su programa. Por ejemplo, ML dependiente utiliza una forma limitada de tipos dependientes para optimizar las comprobaciones vinculadas para operaciones de matriz. Puede hacer cosas más complicadas, como definir un nuevo lenguaje de programación cuya semántica sea dada por un intérprete, luego escribir un compilador garantizado para emitir código de bajo nivel que esté de acuerdo con esta semántica de alto nivel.
Idris es un lenguaje de tipo dependiente destinado a la programación práctica.
(en lugar de probar el teorema). Su tutorial es un buen lugar para comenzar.