A Racket macro tutorial – get HTTP parameters easier
A few days ago, I post this
answer to respond to a
question about Racket's web framework. When researching on which
frameworks could be used. I found no frameworks make get values from
HTTP request easier. So I start to design a macro, which based on
routy and an assuming function
http-form/get
, as following shows:
(get "/user/:name" (lambda ((name route) (age form)) (format "Hello, ~a. Your age is ~a." name age)))
Let me explain this stuff. get
is a macro name, it's going to take a
string as route and a "lambda" as a request handler.
((name route) (age form))
means there has a parameter name
is taken
from route
and a parameter age
is taken from form
. And
(format "Hello, ~a. Your age is ~a." name age)
is the body of the
handler function.
Everything looks good! But we have no idea how to make it, not yet ;). So I'm going to show you how to build up this macro step by step, as a tutorial.
First, we have to ensure the target. I don't want to work with original Racket HTTP lib because I never try it, so I pick routy as a routing solution. A routy equivalent solution would look like:
(routy/get "/user/:name" (lambda (req params) (format "Hello, ~a. Your age is ~a." (request/param params 'name) (http-form/get req "age"))))
WARNING: There has no function named
http-form/get
, but let's assume we have such program to focus on the topic of the article: macro
Now we can notice that there was no name
, age
in lambda
now. But
have to get it by using request/param
and http-form/get
. But there
also has the same pattern, the route! To build up macro, we need the
following code at the top of the file macro.rkt
first:
#lang racket (require (for-syntax racket/base racket/syntax syntax/parse))
Then we get our first macro definition:
(define-syntax (get stx) (syntax-parse stx [(get route:str) #'(quote (routy/get route (lambda (req params) 'body)))])) (get "/user/:name") ; output: '(routy/get "/user/:name" (lambda (req params) 'body))
Let's take a look at each line, first, we have define-syntax
, which is
like define
but define a macro. It contains two parts, name and
syntax-parse
. The name part was (get stx)
, so the macro called
get
, with a syntax object stx
. The syntax-parse
part was:
(syntax-parse stx [(get route:str) #'(quote (routy/get route (lambda (req params) 'body)))])
The syntax-parse
part works on the syntax object, so it's arguments
are a syntax object and patterns! Yes, patterns! It's ok to have
multiple patterns like this:
(define-syntax (multiple-patterns? stx) (syntax-parse stx [(multiple-patterns? s:str) #'(quote ok-str)] [(multiple-patterns? s:id) #'(quote ok-id)])) (multiple-patterns? "") ; output: 'ok-str (multiple-patterns? a) ; output: 'ok-id
Now we want to add handler into get
, to reduce the complexity, we
introduce another feature: define-syntax-class
. The code would become:
(define-syntax (get stx) (define-syntax-class handler-lambda #:literals (lambda) (pattern (lambda (arg*:id ...) clause ...) #:with application #'((lambda (arg* ...) clause ...) arg* ...))) (syntax-parse stx [(get route:str handler:handler-lambda) #'(quote (routy/get route (lambda (req params) handler.application)))]))
First we compare syntax-parse
block, we add handler:handler-lambda
and handler.application
here:
(syntax-parse stx [(get route:str handler:handler-lambda) #'(quote (routy/get route (lambda (req params) handler.application)))]))
This is how we use a define-syntax-class
in a higher-level syntax.
handler:handler-lambda
just like route:str
, the only differences are
their pattern. route:str
always expected a string,
handler:handler-lambda
always expected a handler-lambda
. And notice
that handler:handler-lambda
would be the same as a:handler-lambda
,
just have to use a
to refer to that object. But better give it a
related name.
Then dig into define-syntax-class
:
(define-syntax-class handler-lambda #:literals (lambda) (pattern (lambda (arg*:id ...) clause* ...) #:with application #'((lambda (arg* ...) clause* ...) arg* ...)))
define-syntax-class
allows us add some stxclass-option
, for example:
#:literals (lambda)
marked lambda
is not a pattern variable, but a
literal pattern. The body of define-syntax-class
is a pattern, which
takes a pattern and some pattern-directive
. The most important
pattern-directive
was #:with
, which stores how to transform this
pattern, it takes a syntax-pattern
and an expr
, as you already saw,
this is usage: handler.application
.
The interesting part was ...
in the pattern, it means zero to many
patterns. A little tip makes such variables with a suffix *
like
arg*
and clause*
at here.
Now take a look at usage:
(get "/user/:name" (lambda (name age) (format "Hello, ~a. Your age is ~a." name age))) ; output: '(routy/get "/user/:name" (lambda (req params) ((lambda (name age) (format "Hello, ~a. Your age is ~a." name age)) name age)))
There are some issues leave now, since we have to distinguish route
and form
, current pattern of handler-lambda
is not enough. The
handler-lambda.application
also incomplete, we need
(lambda (req params) (format "Hello, ~a. Your age is ~a." (request/param params 'name) (http-form/get req "age")))
but get
(lambda (req params) ((lambda (name age) (format "Hello, ~a. Your age is ~a." name age)) name age))
right now.
To decompose the abstraction, we need another define-syntax-class
.
(define-syntax-class argument (pattern (arg:id (~literal route)) #:with get-it #'[arg (request/param params 'arg)]) (pattern (arg:id (~literal form)) #:with get-it #'[arg (http-form/get req (symbol->string 'arg))])) (define-syntax-class handler-lambda #:literals (lambda) (pattern (lambda (arg*:argument ...) clause* ...) #:with application #'(let (arg*.get-it ...) clause* ...)))
There are two changes, replace lambda
with let
in
handler-lambda.application=(it's more readable), and use =argument
syntax type instead of id
.
argument
has two patterns, arg:id (~literal route)
and
arg:id (~literal form)
to match (x route)
and (x form)
. Notice
that #:literals (x)
and (~literal x)
has the same ability, just pick
a fit one. symbol->string
converts an atom to a string, here is an
example:
(symbol->string 'x) ; output: "x"
Let's take a look at usage:
(get "/user/:name" (lambda ((name route) (age form)) (format "Hello, ~a. Your age is ~a." name age))) ; output: '(routy/get "/user/:name" (lambda (req params) (let ((name (request/param params 'name)) (age (http-form/get req (symbol->string 'age)))) (format "Hello, ~a. Your age is ~a." name age))))
Manually pretty output:
'(routy/get "/user/:name" (lambda (req params) (let ((name (request/param params 'name)) (age (http-form/get req (symbol->string 'age)))) (format "Hello, ~a. Your age is ~a." name age))))
1. Summary
With make up this tutorial, I learn a lot of macro tips in Racket that I don't know before. I hope you also enjoy this, also hope you can use everything you learn from here to create your helpful macro. Have a nice day.
2. End up, all code
#lang racket (require (for-syntax racket/base racket/syntax syntax/parse)) (define-syntax (get stx) (define-syntax-class argument (pattern (arg:id (~literal route)) #:with get-it #'[arg (request/param params 'arg)]) (pattern (arg:id (~literal form)) #:with get-it #'[arg (http-form/get req (symbol->string 'arg))])) (define-syntax-class handler-lambda #:literals (lambda) (pattern (lambda (arg*:argument ...) clause* ...) #:with application #'(let (arg*.get-it ...) clause* ...))) (syntax-parse stx [(get route:str handler:handler-lambda) #'(quote (routy/get route (lambda (req params) handler.application)))])) (get "/user/:name" (lambda ((name route) (age form)) (format "Hello, ~a. Your age is ~a." name age)))