{.inject.} in Nim

Posted on Apr 24, 2024

While working on my HTTP server framework/library for Nim (Silk), I got to a situation where I wanted a simpler syntax for adding handler functions. Similar to the -> and => lambdas from std/sugar, but even simpler. So I wrote a template. Here’s what was needed for simple route handlers before:

import asyncdispatch
import silk

var serv = newServer(
  ServerConfig(host: "0.0.0.0", port: Port(8080)),
)

# This is way too verbose.
serv.GET("/", proc(ctx: Context) {.async.} = ctx.sendString("Hello, world!"))

serv.start()

Notice how long the anonymous proc is for the route. This makes the line way too long, and is unnecessary boilerplate, especially if you’re going to be writing multiple of these types of routes, and especially for such a simple function (just sending a string response). Nim has really good metaprogramming capabilities, thankfully, and we don’t even need a macro to solve this. Just a good ‘ol template.

I want the handler function to always have an implied ctx variable that represents the connection (request/response) context, but I don’t want the user to have to declare that the ctx variable exists every time they write a handler function, like they would with an anonymous function / lambda.

So I made this template:

template `~>`*(expr: untyped): untyped =
  proc(ctx: Context) {.async.} = expr

Which is used like so:

import asyncdispatch
import silk

var serv = newServer(
  ServerConfig(host: "0.0.0.0", port: Port(8080)),
)

# Much more sleek.
serv.GET("/", ~> ctx.sendString("Hello, world!"))

serv.start()

And I figured this would work great. So I compile and run, and:

Error: undeclared identifier: 'ctx'

Darn. I guess Nim requires identifiers to be declared before the template/macro is completed, by default. I wasn’t sure where to look for the answer, so I asked some helpful folks over on Nim’s official Discord server. I quickly got a response, and this is apparently what I needed:

template `~>`*(expr: untyped): untyped =
  proc(ctx{.inject.}: Context) {.async.} = expr

This correctly compiles with the previous example. So if you’re wanting to pass a symbol into a template in Nim, but that symbol hasn’t been declared in your code previously (but will be given meaning within the template itself), then you need to use the {.inject.} pragma.