Search code examples
clojurecompojuredestructuring

Destructuring forms and Compojure?


I'd thought I'd post this as I got it to work through guesswork without a real understanding of what's going on and I thought it might be helpful if someone explained it.

I understand how to get at an element of the :params map in a Compojure handler:

(GET "/something" [some_arg] "this is the response body")

or

(GET "/something" {{some_arg "some_arg"} :params} "this is the response body")

although I don't completely understand what the {some_arg "some_arg"} part is doing :(

I also wanted to access the :remote-addr part of the request as well as some_arg. And I ended up with

(GET "/something" {{some_arg "some_arg"} :params ip :remote-addr}
    (do-something-with some_arg ip))

So, I get that the unquoted strings some_arg and ip are the names of variables to which I want the values bound but the map above isn't a valid Clojure map. How does it work?

I also get that this is evaluated against the Ring request map (which is somehow supplied by the defroutes macro) but the expression above isn't a function or macro definition so how can it 'exist' as a valid expression in my code? Is there some sort of suspension of the normal rules for macro arguments? I've been unable to find a definition of the syntax of destructuring forms comprehensible to this non-Lisp'er.


Solution

  • The map is a valid destructuring map. In any place where you bind names, you can use destructuring. You could do the same thing in a let, like this:

    user=> (let [{{some-arg "some_arg"} :params ip :remote-addr} {:remote-addr "127.0.0.1" :params {"some_arg" "some_value"}}] [ip some-arg])
    ["127.0.0.1" "some_value"]
    

    I wrote a post about map destructuring in the context of named arguments, but it applies here. You might find this useful: Clojure - named arguments

    There are a lot of blog posts demonstrating destructuring, including this one. I'm not sure which one would be a canonical place to learn from.

    I don't pretend to know what exactly compojure does with that map under the hood, but I presume it throws it in a let or something similar as I demonstrated above. GET is a macro, so it doesn't have to evaluate the map you pass it, which is why you wouldn't get an error unless it evaluated it.

    user=> (defmacro blah [m])
    #'user/blah
    user=> (blah {a "b" c "d"})
    nil
    user=> (defn blah [m])
    #'user/blah
    user=> (blah {a "b" c "d"})
    java.lang.Exception: Unable to resolve symbol: a in this context (NO_SOURCE_FILE:9)
    

    Under the hood, magic happens to that map and it gets passed to a function called destructuring that does the destructuring magic.

    There isn't really anything special going on here other than normal macro/special form foo and delayed evaluation.