DEV Community

林子篆
林子篆

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

5 1

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

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more