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))))
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.
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)))
Top comments (0)