Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Meta-syntactic programming with Racket: string interpolation (klibert.pl)
67 points by klibertp on May 1, 2017 | hide | past | favorite | 9 comments


This is cool; I've never seen an example of extending Racket's readtable.

I always imagined implementing string interpolation by overriding the implicit #%datum macro and checking for literal strings. Here's a quick (probably non-idiomatic) example for comparison.

  #lang racket
  
  (require
    (rename-in racket (#%datum core-datum))
    (for-syntax syntax/parse))
  
  (define-syntax (#%datum stx)
    (syntax-parse stx
      [(_ . x:str) (interpolate #'x)]
      [(_ . x)     #'(core-datum . x)]))
  
  (define-for-syntax (interpolate stx)
    (define re            #rx"@{[^}]+}")
    (define (trim match)  (substring match 2 (- (string-length match) 1)))
    (define (to-stx val)  (datum->syntax stx val))
    (define (datum val)   (to-stx (cons #'core-datum val)))
    (define (source text) (to-stx (read (open-input-string text))))
    (let* ([text     (syntax->datum stx)]
           [matches  (regexp-match* re text)]
           [template (datum (regexp-replace re text "~a"))]
           [values   (map (compose source trim) matches)])
      (if (null? matches)
          template
          (to-stx `(,#'format ,template ,@values)))))
  
  42 ;=> 42
  #t ;=> #t
  "1 time" ;=> "1 time"
  
  (define foo 2)
  "@{foo} times" ;=> "2 times"
  "@{(+ foo 1)} times" ;=> "3 times"
  
  (define (scoped foo)
    (define (format . args) "boom!")
    "@{foo} times")
  (scoped 10) ;=> "10 times"


Great solution! Thanks for sharing.

Your last example is especially interesting, as it shows the problem with the reader extensions I forgot to mention in the post: the introduced identifiers have local scope. In other words, both redefining `string-append` and not having it in local scope when calling it are problems.

In your solution the `#'format` expression binds the `format` function to whatever is called `format` in the current module (but in the next phase, which is why you require `#%datum` normally and not `for-syntax`).

I regret not putting the hygiene considerations into the post, I planned it but, honestly, forgot about it :( I'll add it to the post, along with your solution (is that alright?)


I think just tweaking your parse method to use #'string-append and #'~a would be enough to preserve hygiene for your introduced identifiers. datum->syntax will happily work with mixed combinations of syntax-objects and data.

And yes, feel free to do anything with that example and share further. Not enough people have had exposure to Racket's implicit expansion macros. (Is that even what they're called?)


> I think just tweaking your parse method to use #'string-append and #'~a

Yes, but then I'd need to explain what the syntax objects really are instead of calling them "some meta-data on top of a list" and moving on :)


You could have used the scribble reader (the @-expression syntax, not to be confused with the full documentation system). But you'd also then find that there is a more robust way to get what you implement with a simple definition:

    #lang at-exp racket
    (require racket/date)
    (define s string-append)
    (displayln @s{Current date&time: @(date->string (current-date) #t)})


Yes! This is why I chose to use #@ instead of @, to avoid conflict with at-exp reader. As noted below by moron4hire, the @-exp reader is the better solution for this problem. I concentrated on providing an example of how to extend the reader, not on making an example of a useful or needed extension. I should have probably written this on top of the post...

Anyway, thanks for reading!


A nice overview of how to modify the reader, but in my opinion the simple function call is more readable than the macro: (string-append "An error occured at " file ":" line ".")

I don't have to know/infer/lookup/guess how the read table has been modified, and I find the @ syntax a little distracting.


> I find the @ syntax a little distracting.

I guess that's a matter of personal preference. I can imagine arguing for both: the syntactic sugar reduces the number of characters needed and makes dealing with whitespace in the string easier (no " " in between vars). The function version reduces the need to visually parse the whole string every time to learn what forms are inside of it and makes the amount of things you need to keep in your head at one time lower.

Personally, I have nothing against the plain `(string-append ...)` and I probably wouldn't use the extension from the post. I just wanted to show one way to write such an extension. There are very few examples of modifying Racket's reader or readtable because it's unnecessary to get most syntax extensions to work - the arithmetization's post is a great example of this. This is why I gave up (early in the process of writing this post) on finding an example actually useful or needed. I came up with something that is widely understood and missing from normal Racket and used that instead :)


I thought that part was clear, that it was pedagogical example more than a useful one.

But to anyone who still doesn't get it and might be thinking the fact that Racket lacking such a feature is evidence that it's a less well-developed language than others, the reason Racket doesn't have string interpolation is because it's not really needed. If you're going to do large amounts of string templating, the "Racket Way" is to use Scribble: https://docs.racket-lang.org/scribble/index.html

Scribble is basically Literate Racket. And it's able to generate a very wide variety of document formats, far more than I've ever seen any documentation or string templating tool do.

There is also quasi-quoting and un-quoting, for doing a very similar thing with Symbols: https://docs.racket-lang.org/reference/quasiquote.html




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: