DEV Community

林子篆
林子篆

Posted on • Originally published at dannypsnl.github.io on

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)))
Enter fullscreen mode Exit fullscreen mode

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"))))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)))])
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)))]))
Enter fullscreen mode Exit fullscreen mode

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)))]))
Enter fullscreen mode Exit fullscreen mode

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* ...)))
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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")))
Enter fullscreen mode Exit fullscreen mode

but get

(lambda (req params)
  ((lambda (name age)
    (format "Hello, ~a. Your age is ~a."
            name
            age)) name age))
Enter fullscreen mode Exit fullscreen mode

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* ...)))
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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))))
Enter fullscreen mode Exit fullscreen mode

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))))
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

Top comments (0)