Automatic forward-declaration in clojure
Table of Contents
- 1 Auto-declare in clojure
- 1.1 The reason why clojure resolves symbols at compile time
- 1.2 a with-auto-declare macro
- 1.3 example of the use of with-auto-declare
1 Automatic forward-declaration in clojure
There is one think that distracts me everytime I write a lot clojure code: the lack of forward-declarations.
Actually,clojure supports forward declarations,but you have to use the declare macro and I hate to stop working
on my function to add a declare on top of it. When I return to the function, I have often forgot what I wanted
to write …
I found a few discussions on this topic here, here and here.
This is what Rich Hickey answered to the question of why clojure resolves symbols at compile time
(the reason why you have to declare all the vars you have not yet defined).
1.1 The reason why clojure resolves symbols at compile time
3. Rich Hickey View profile More options Nov 11 2008, 4:16 pm On Nov 11, 12:24 am, Paul Barry <pauljbar...@gmail.com> wrote: - Show quoted text - I can't speak for Scheme, but CL resolves symbols earlier than does Clojure - at read time. This causes a number of problems and a lot of complexity [1]. In Clojure, I had to solve the Lisp-1 vs defmacro problem, and did so by separating symbols from vars. That means that symbols are simple when read, and only get resolved when compiled. Were compilation to auto-create vars for never before seen symbols, many of the problems of packages would remain, so I decided that vars and other name resolutions needed to be created intentionally, via def/import/refer. This leads to many fewer errors, and avoids the problems identified in [1]. There is now a declare macro that makes it simple and obvious to intentionally claim names for later use: (declare a b c) ; ...def a b c in any order This expands into (def a) (def b) (def c). Note that no initial values are supplied, so they are unbound, and you will get a runtime error if you use them before subsequently defining them, unlike your (def a nil) above, which I don't recommend. So, you don't need to define things in any specific order - use declare. RichSo the resolution of vars at compile time is good for clojure's namespaces, which are very well thought out in comparison to other lisps like common lisp. Also, it helps to avoid typos. When you spelled the name of a function wrong and clojure would allow forward-declarations, it would assume that you are referring to a var that isn't defined yet and would forward-declare that for you. The error would come up at runtime and (for me, at least) something as triviall as a spelling error is hard to see.
If you look from that position, declare is actually a feature of clojure. But nevertheless I tend to write my clojure-code in a top-down fashion, just like I think about the code. I start with highest level of abstraction at the top of my file and work down to the lower levels of abstraction. This makes the code easier to write and to read for me. Instead of writing new code above my old code or going away from my function and add a declare and go back to my function, everytime I want to have a forward declaration, I wanted to be able to specify symbols to be forward-declared without a need to leave the function and forgetting what I wanted to implement…
1.2 a with-auto-declare macro
So here is my solution: A with-auto-declare macro.
With-auto-declare takes a bunch of code, searches for symbols marked to be forward-declared and adds a declare
with these symbols on top of the code and gives the code with the auto-declared symbols replaced by the normal
symbols. I choose to mark a symbol to be forward-declared by writing a D!- in front of it (please let me
know if you have a better alternative).
Here's the code:
(ns auto-declare.core) (defn auto-declare-symbol? "Symbols which start with D!- and can not be resolved are subject to be auto-declared" [symb] (and (.startsWith (str symb) "D!-") (not (resolve symb)))) (defn auto-declare-symbol-to-normal-symbol [auto-declare-symbol] (-> auto-declare-symbol str (.substring 3) symbol)) (refer 'clojure.walk) (defmacro with-auto-declare [symb & code] (let [auto-defs# (->> code flatten (filter auto-declare-symbol?) distinct) normal-symbols# (map auto-declare-symbol-to-normal-symbol auto-defs#) replacement-map# (-> {} (into (map vector auto-defs# normal-symbols#)))] `(do (declare ~@normal-symbols#) ~@(clojure.walk/postwalk-replace replacement-map# code))))Flatten extracts all symbols of the code, and distinct removes the duplicates from the auto-declare symbols. I use the function clojure.walk/postwalk-replace to replace all occurences of an auto-declare symbol with the corresponding normal-symbol. Therefor I need an replacement map, with is easily filled with into. The expression (map vector coll1 coll2) just zips the collections together. The 2-element vectors are interpreted as key-value pairs from into.
1.3 example of the use of with-auto-declare
As an example: Here is a piece of code from my blogpost: Porting PAIP to clojure - chapter 2:
Edit: Ok I admit that the D!- looks somewhat ugly. I used it to prevent that it shadows an already defined symbol accidently. But, because I test if the symbol can be resolve to decide if it is an auto-declare symbol, this isn't neccesary. So I changed the with-auto-declare macro that it takes the prefix as first argument.
(declare sentence noun-phrase verb-phrase article noun verb one-of) (defn sentence [] (concat (noun-phrase) (verb-phrase))) (defn noun-phrase [] (concat (article) (noun))) (defn verb-phrase [] (concat (verb) (noun-phrase))) (defn article [] (one-of ["the" "a"])) (defn noun [] (one-of ["man" "ball" "woman" "table"])) (defn verb [] (one-of ["hit" "took" "saw" "liked"]))
=> #'auto-declare.core/verbI had to declare every single function to get the code compiling… Now with my macro, the code becomes:
(with-auto-declare (defn sentence [] (concat (D!-noun-phrase) (D!-verb-phrase))) (defn noun-phrase [] (concat (D!-article) (D!-noun))) (defn verb-phrase [] (concat (D!-verb) (D!-noun-phrase))) (defn article [] (D!-one-of ["the" "a"])) (defn noun [] (D!-one-of ["man" "ball" "woman" "table"])) (defn verb [] (D!-one-of ["hit" "took" "saw" "liked"])))
=> #'auto-declare.core/verbIt works! With this macro, you have both: the convienience of automatic-forward declarations when wanted, and you don't have the risk that typos are interpreted as forward-declarations. As always I am thankfull of comments/advice etc.
Edit: Ok I admit that the D!- looks somewhat ugly. I used it to prevent that it shadows an already defined symbol accidently. But, because I test if the symbol can be resolve to decide if it is an auto-declare symbol, this isn't neccesary. So I changed the with-auto-declare macro that it takes the prefix as first argument.
(ns auto-declare.core) (defn auto-declare-symbol? "Symbols which start with D!- and can not be resolved are subject to be auto-declared" [prefix symb] (and (.startsWith (str symb) (str prefix)) (not (resolve symb)))) (defn auto-declare-symbol-to-normal-symbol [prefix auto-declare-symbol] (-> auto-declare-symbol str (.substring (.length (str prefix))) symbol)) (refer 'clojure.walk) (defmacro with-auto-declare [symb & code] (let [auto-defs# (->> code flatten (filter #(auto-declare-symbol? symb %)) distinct) normal-symbols# (map #(auto-declare-symbol-to-normal-symbol symb %1) auto-defs#) replacement-map# (-> {} (into (map vector auto-defs# normal-symbols#)))] `(do (declare ~@normal-symbols#) ~@(clojure.walk/postwalk-replace replacement-map# code))))
=> #'auto-declare.core/with-auto-declareThe example: Now with a _ as prefix:
(with-auto-declare _ (defn sentence [] (concat (_noun-phrase) (_verb-phrase))) (defn noun-phrase [] (concat (_article) (_noun))) (defn verb-phrase [] (concat (_verb) (_noun-phrase))) (defn article [] (_one-of ["the" "a"])) (defn noun [] (_one-of ["man" "ball" "woman" "table"])) (defn verb [] (_one-of ["hit" "took" "saw" "liked"])))
=> #'auto-declare.core/verb
the D!- makes the code unreadable
AntwortenLöschenwhat would be a better sign ?
Löschenhow about metadata?
Löschencould you post how it looks like, I edited my post. with-auto-declare
Löschennow takes the prefix as first argument