Racket: Domain-Specific Mini-Languages

Written by Dominik Joe Pantůček on 9 ledna, 2020.

In one of our projects, we needed to write an HTTP application server in Racket. In this application server we needed to route HTTP requests to various methods of various modules. Read on how we slightly modified a simple syntax helper to ease this task for us.

The aforementioned project consists of a React.js[1] front-end application and Racket[2] back-end application server with REST[3] API[4] over HTTP[5]. For data serialization between the front-end and back-end parts, JSON[6] was chosen. Implementing an HTTP server in Racket is pretty straightforward using the standard web-server library[7]. A simple HTTP server typically looks like:

(require web-server/servlet-env)

(serve-servlet dispatcher)

Of course, the (serve-servlet)[8] accepts arguments like port, paths to handle and many others. But basically all boils down to writing a dispatcher that dispatches functions to given servlets. Each servlet is than a procedure that accepts (request?) structure and must return a (response?) structure. But how to map between the HTTP requests and these procedures? Turns out that Racket web-server contains neat syntax for this where you can specify paths, optional method and a function to call and when you define all your servlets, it creates a dispatcher function that can be used with (serve-servlet) directly. It goes like this:

(define dispatcher
  (dispatch-case
    (("my" "path") #:method post my-path-servlet)
    (("another" "one") another-servlet)
    (("param-test" (number-arg)) test-number)
    (else my-404-servlet)))

As you might expect, POST[9] request to „/my/path“ will be handled by (my-path-servlet) and implicit GET[9] request to „/another/one“ will be handled by (another-servlet). Another cool feature is the possibility of parsing arguments within the URL path like we see in the third example where GET request to „/param-test/666“ will call (test-number 666) without anything done on the programmer side.

But what if we wanted all paths to have a prefix like „/api“? And what it we wanted to explicitly see the methods there? We can define our own mini-language on top of the (dispatch-case) syntax. The solution may seem a bit cryptic at first glance, but here it is anyway:

(define-for-syntax (dispatch-api-case-helper stx)
  (syntax-parse stx
    (((method:id (path+args:expr ...) proc:id) rest ...)
     #`((("api" path+args ...) #:method method proc)
        #,@(dispatch-api-case-helper #'(rest ...))))
    (((else proc:id))
     #'((else proc)))
    (()
     #'())))

(define-syntax (dispatch-api-case stx)
  (syntax-parse stx
    ((_ rest ...)
     #`(dispatch-case
        #,@(dispatch-api-case-helper #'(rest ...))))))

Here a new syntax (dispatch-api-case) is defined. It transforms its contents according to a tail-recursive rule which is basically a generic Racket procedure defined at syntax stage[10]. Let’s rewrite our example before to be under a „/api“ prefix and with explicit methods:

(define dispatcher
  (dispatch-api-case
    (post ("my" "path") my-path-servlet)
    (get ("another" "one") another-servlet)
    (get ("param-test" (number-arg)) test-number)
    (else my-404-servlet)))

Our little domain-specific mini-language – if we do not mind calling such simple syntax transformer a language – just transforms this syntax to the generic (dispatch-case). And of course that is then transformed into more primitive forms. But our transformation yields pretty simple result:

(define dispatcher
  (dispatch-case
    (("api" "my" "path") #:method post my-path-servlet)
    (("api" "another" "one") #:method get another-servlet)
    (("api" "param-test" (number-arg)) #:method get test-number)
    (else my-404-servlet)))

And – as always – Rackets saves us from doing the repetitive task of writing the prefix by hand and remembering which HTTP method it is. Our (dispatch-api-case) is actually also our API documentation. It clearly reads the method, path, arguments and what it calls. And best of all – not only it saves the time of the programmer, but also it ensures you cannot introduce typos in the path prefix or overlook which HTTP method is used.

 

Hope you liked this little venture into the world of web applications and see you again next week!


References

1. https://reactjs.org/

2. https://racket-lang.org/

3. Wikipedia contributors. (2020, January 5). Representational state transfer. In Wikipedia, The Free Encyclopedia. Retrieved 14:06, January 9, 2020, from https://en.wikipedia.org/w/index.php?title=Representational_state_transfer&oldid=934194719

4. Wikipedia contributors. (2020, January 6). Application programming interface. In Wikipedia, The Free Encyclopedia. Retrieved 14:06, January 9, 2020, from https://en.wikipedia.org/w/index.php?title=Application_programming_interface&oldid=934435591

5. Wikipedia contributors. (2020, January 3). Hypertext Transfer Protocol. In Wikipedia, The Free Encyclopedia. Retrieved 14:07, January 9, 2020, from https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol&oldid=933935986

6. Wikipedia contributors. (2020, January 5). JSON. In Wikipedia, The Free Encyclopedia. Retrieved 14:07, January 9, 2020, from https://en.wikipedia.org/w/index.php?title=JSON&oldid=934272952

7. https://docs.racket-lang.org/web-server/index.html

8. https://docs.racket-lang.org/web-server/run.html?q=serve%2Fservlet#%28def._%28%28lib._web-server%2Fservlet-env..rkt%29._serve%2Fservlet%29%29

9. RFC7230: Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing, available online at https://tools.ietf.org/html/rfc7230#section-3.1.1

10. https://docs.racket-lang.org/reference/syntax-model.html?q=define-syntax#(tech._transformer)