Donnerstag, 31. Mai 2012

auto-declare in clojure

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. 
Rich 
So 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:
(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/verb
I 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/verb
It 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-declare
The 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
Date: 2012-06-01T12:55+0200
Author: Maik Schünemann
Org version 7.8.09 with Emacs version 23
Validate XHTML 1.0

Kommentare:

  1. the D!- makes the code unreadable

    AntwortenLöschen
    Antworten
    1. how about metadata?

      Löschen
    2. could you post how it looks like, I edited my post. with-auto-declare
      now takes the prefix as first argument

      Löschen