2
votes

I've been working on converting a static site generator that I wrote in Python to Racket. This is primarily a learning exercise to get to know Racket better. I've now got the Racket version working, but there is one thing that I like much better about the Python version: the pathlib.Path object. The generator has a lot of path handling, which looks like this in the Python code:

render_template(root / "templates" / "index.jinja")

Whereas the Racket code looks more like this:

(render-template (build-path (root) "templates" "index.jinja"))

I find the ability to read paths much cleaner in the Python code, so I'd like to modify the Racket reader to be able to support something like the following:

(render-template (root) / "templates" / "index.jinja")

Playing around with readers, I figured out how to make this kind of expression work:

(render-template  / foo / templates / index.jinja /)

I'd be OK with the syntax, but I can't figure out how to evaluate the path elements as Racket expressions and not just strings. Even if I did figure that out, I still have the issue that naive string handling would cause problems with something like:

(render-template / (root) / (string-join 
                             (list "templates" "subdir") "/") 
                 / "index.jinja" /)

So, any advice/input on what I can do here? :)

1

1 Answers

3
votes

If I understand correctly, originally you had something like this:

(define (root)
  "/")

(define (render-template path)
  (displayln path))

(render-template (build-path (root) "templates" "index.jinja"))
;; => /templates/index.jinja

What I suggest is simply changing render-template so that it may be called with multiple path "parts" -- and it handles calling build-path for you:

(define (render-template . path-parts)
  (define path (apply build-path path-parts))
  (displayln path))

Now you can call it like this:

(render-template (root) "templates" "index.jinja")
;; => /templates/index.jinja

Incidentally, calling it the original way still works, because build-path will act as identity in this case:

(render-template (build-path (root) "templates" "index.jinja"))
;; => /templates/index.jinja

I think this is the most "Rackety" way. One nice thing about s-expressions is you don't have to type "separators" like , or /. Spaces suffice. And I think the more you read and write Racket code, the more you'll feel this way.

Granted, perhaps the most Rackety thing of all is the ability to make your own little (or big) languages. If you wanted a "DSL" to write files consisting primarily of paths, that might be one thing. But in this case I'm not sure I see the big win.


If anything, maybe you want just a macro that makes / act as whitespace in this context. i.e. To make / mean the "nothing" that it ultimately needs to be, in the expanded code.

For example:

#lang racket/base

(require (for-syntax racket/base syntax/parse))

(define-syntax (render-template stx)
  (define-splicing-syntax-class pp
    (pattern (~seq part (~optional (~literal /)))))
  (syntax-parse stx
    [(_ p:pp ...) #'(do-render-template p.part ...)]))

(define (do-render-template . path-parts)
  (define path (apply build-path path-parts))
  (displayln path))

(render-template (root) / "templates" / "index.jinja")
;; => /templates/index.jinja
(render-template (root) "templates" "index.jinja")
;; => /templates/index.jinja

Note that the / are completely optional here. They get treated as whitespace.

Also note that the real work is done in the function, renamed now to do-render-template. The macro is just a wrapper. Generally it's best for macros to do as little work as necessary, and for things that can be functions to be functions.

But again, personally I wouldn't bother with the macro, I'd go with the approach I suggested above.


Update: As a p.s., if I understand correctly, Python's PathLib.Path is defining / as an operator? Well, Racket doesn't really have "operators". It has functions. And the math functions like / accept any number of arguments. So instead of 10 / 5 / 2 we write (/ 10 5 2). Which, really, brings us full-circle back to build-path: A function that takes any number of path parts.

I suppose you could effectively rename build-path to /:

(require (rename-in (except-in racket /)
                    [build-path /]))
(/ (root) "templates" "index.jinja")

But this isn't really operator overloading, because these are plain functions not methods. And... I wouldn't do it. :)