[Cs254f11] Macro confusion
Omri Bernstein
ob08 at hampshire.edu
Tue Oct 11 15:50:51 EDT 2011
As a presage to this email, I spend a lot of time explaining, and my
question doesn't really come until the very end. Also, by the time I got to
the end of writing , I managed to accidentally solve my original question,
and now I'm curious whether anybody can tell me why what I did solved the
problem.
I've been trying to work on getting the arities for all the clojure
functions, and what types of arguments each arity for each function will
accept. I'm on the second part now, and I plan on going about it by trying
all the possible combinations of argument types, and returning the types of
the ones that don't error. So I decided to make a general-purpose macro for
returning a list of all the possible combinations of applying a function f
to a series of collections--using each value from the first collection as
the first argument to f, and each from the second collection as the second
argument to f, etc. If you understand how the "for" loop comprehension works
(if not, maybe the examples
here<http://clojuredocs.org/clojure_core/clojure.core/for>would help),
this is an example of how the concept would work, using + as
the function, [1 2 3] as the first collection, and [10 20] as the second
collection:
(for [elem1 [1 2 3] elem2 [10 20]]
(+ elem1 elem2))
>> (11 21 12 22 13 23)
This could be generalized to:
(defn combins
[f c1 c2]
(for [elem1 c1 elem2 c2]
(f elem1 elem2))
(combins + [1 2 3] [10 20])
>> (11 21 12 22 13 23)
The problem, which may be apparent after looking at it, is that something
like the "combins" function above is not well suited to a long series of
collections (frankly, for what I plan on using it for, this is not a
necessary feature--but still). In order to do a combination of + with [0
128], [0 64], [0 32], [0 16], [0 8], [0 4], [0 2], and [0 1] (which should
return a list of all the positive integers that are less than 256), I would
have to create a function that looked something like:
(defn combins2
[f c1 c2 c3 c4 c5 c6 c7 c8]
(for [elem1 c1 elem2 c2 elem3 c3 elem4 c4 elem5 c5 elem6 c6 elem7 c7 elem8
c8]
(f elem1 elem2 elem3 elem4 elem5 elem6 elem7 elem8))
(combins + [0 128] [0 64] [0 32] [0 16] [0 8] [0 4] [0 2] [0 1])
>> (1 2 3 4 5 6 7 8 9 10 ... 250 251 252 253 254 255)
I figured that creating a macro would be a more elegant way to handle a
theoretically limitless series of collections. A quick introduction to
macros: macros are functions that generate code as output. When writing a
macro, you use symbols like `, ~, and ~@. The ` symbol (called the backquote
symbol) is used a lot like the ' symbol (but they are not the same thing),
to denote something that should be returned as is, not evaluated. The ~
symbol (called the unquote symbol) is used to denote something that should
be evaluated and then that return value should be inserted as is. The ~@
symbol (called the unqoute splice symbol, or something) is pretty cool, it
returns the elements inside of what it evaluates to. Both of the unquote
symbols have to be used inside a backquoted block of code. Here's an example
of how it works:
(def example-collection [1 2 3])
~example-collection
>> #<CompilerException java.lang.IllegalStateException: Var
clojure.core/unquote is unbound. (NO_SOURCE_FILE:0)>
~@example-collection
>> #<CompilerException java.lang.IllegalStateException: Var
clojure.core/unquote-splicing is unbound. (NO_SOURCE_FILE:0)>
`(list 0 example-collection 4 5 6)
>> (clojure.core/list 0 user.core/example-collection 4 5 6)
`(list 0 ~example-collection 4 5 6)
>> (clojure.core/list 0 [1 2 3] 4 5 6)
`(list 0 ~@example-collection 4 5 6)
>> (clojure.core/list 0 1 2 3 4 5 6)
Note the difference between the above examples versus using ' instead of `,
which just returns exactly what comes after it.
'(list 0 example-collection 4 5 6)
>> (list 0 example-collection 4 5 6)
'(list 0 ~example-collection 4 5 6)
>> (list 0 ~example-collection 4 5 6)
'(list 0 ~@example-collection 4 5 6)
>> (list 0 ~@example-collection 4 5 6)
In my case I want to generate a "for" with a variable number of bindings.
Essentially, instead of passing a "hard-coded" vector of variables and
bindings to the "for" list comprehension, I'm passing a vector that has been
derived. After a lot (a lot) of attempts, I managed to come up with
something elegant and concise, using an infinite lazy list of "anonymous"
symbols (in clojure, I believe it is idiomatic to use an underscore to
denote a variable that is being defined whose name is not important). For
people unfamiliar with macros--I'm kind of jumping right into the deep-end
here, apologies:
(def infinite-symbol-list
(map #(symbol (format "_%d" %)) (iterate inc 1)))
(take 4 infinite-symbol-list)
>> (_1 _2 _3 _4)
(defmacro combins
[f & colls]
`(for ~(vec (interleave infinite-symbol-list colls))
(~f ~@(take (count colls) infinite-symbol-list))))
(combins + [0 128] [0 64] [0 32] [0 16] [0 8] [0 4] [0 2] [0 1])
>> (1 2 3 4 5 6 7 8 9 10 ... 250 251 252 253 254 255)
I'll try to quickly explain the macro I wrote. When you have "& var-name" at
the end of an argument list for a function, it makes a list out of the
remaining arguments and stores that in "var-name". For example:
(defn example-fn
[a & more]
(list a more))
(example-fn 1 2 3 4 5 6 7 8)
>> (1 (2 3 4 5 6 7 8))
The "interleave" function takes two collections, and then returns a list of
(first coll1), (first coll2), (second coll1), (second coll2), etc. until one
of the collections runs out of elements. Here, I am interleaving the
infinite symbol list (_1 _2 _3 ...) with the "colls"--which should be a
collection of the series of collections passed to combins. Let's represent
colls as (c1 c2 c3 ... cN) where cN is the last collection in the series.
Interleave these will return (_1 c1 _2 c2 _3 c3... _N cN). Calling "vec" on
this will return [_1 c1 _2 c2 _3 c3 ... _N cN]. Since ~ will return the
result of evaluating what is after it, `(for ~(vec (interleave
infinite-symbol-list colls)) ...) will return (clojure.core/for [_1 c1 _2 c2
_3 c3 ... _N cN] ...) which is exactly what I want. After this, I want to
call f using _1 as the first argument, _2 as the second argument, etc. I
have to do ~f to ensure that I am using "the function inside f" (e.g. "+")
as opposed to "f itself" (which does not exist). Then, because
infinite-symbol-list is infinite (shocker, I know) we have to make sure to
take only what we need of it. Doing (take (count colls)
infinite-symbol-list) will return (_1 _2 _3 ... _N), where N is the number
of collections in the series (i.e. "(count colls)"). Since ~@ will return
the elements inside of the return value of the stuff after it, ~@ should
return _1 _2 _3 ... _N. So, in the example that f is +, (~f ~@(take (count
colls) infinite-symbol-list)) will return (+ _1 _2 _3 ... _N). Great. Still
using + as the example f, the macro as a whole will return and then
evaluate:
(clojure.core/for [_1 c1 _2 c2 _3 c3 ... _N cN]
(+ _1 _2 _3 ... _N))
I hope all of that made sense. So...my question. Originally, I told you that
I was planning on testing each arity of each function, using all the
possible combinations of argument types, to discover what argument types
each arity would accept. If I were just trying to test a single list of
argument types on a single function, I could do this:
(def arg-types
{\t :character
[1 2 3 4] :collection
+ :function
42 :number
#"ab" :regex
#{1 2 3 4} :set
"Text is in here." :string})
(defn try-args
[f & elems]
(try (do (apply f elems) (cons f (map test-types elems)))
(catch Exception e)))
(try-args + \t)
>> nil
(try-args + 42)
>> (#<core$_PLUS_ clojure.core$_PLUS_ at 507726> :number)
(try-args + 42 \t)
>> nil
(try-args + 42 42)
>> (#<core$_PLUS_ clojure.core$_PLUS_ at 507726> :number :number)
All that #<core$_PLUS...> nonsense is equivalent to +. So it makes sense to
me that I could do the following to test what argument types a 1-arity call
to + or - accepts:
(combins try-args [+ -] (keys arg-types))
>> #<ClassCastException java.lang.ClassCastException: java.lang.Character
cannot be cast to clojure.lang.IFn>
But I can't, and it looks like it's trying to evaluate the result of (keys
(arg-types)), and I don't understand why, but if I cast the result into a
vector that should fix the problem:
(combins try-args [+ -] (vec (keys arg-types))
>> (nil nil nil nil nil nil nil nil nil nil nil nil nil nil)
So it "works" now, but it's not returning what it "should" be. Here's the
equivalent of what I expect the macro to return and evaluate:
(for [_1 [+ -] _2 (vec (keys arg-types))]
(try-args _1 _2))
>> (nil nil nil (#<core$_PLUS_ clojure.core$_PLUS_ at 507726> :number) nil nil
nil nil nil nil (#<core$_ clojure.core$_ at ac9cbe> :number) nil nil nil)
Wow, just right now, at this point in writing this email, I just fixed the
problem. If instead of:
(defmacro combins
[f & colls]
`(for ~(vec (interleave infinite-symbol-list colls))
(~f ~@(take (count colls) infinite-symbol-list))))
I change colls to `~colls inside the interleave:
(defmacro combins
[f & colls]
`(for ~(vec (interleave infinite-symbol-list `~colls))
(~f ~@(take (count colls) infinite-symbol-list))))
(combins try-args [+ -] (keys arg-types))
>> (nil nil nil (#<core$_PLUS_ clojure.core$_PLUS_ at 507726> :number) nil nil
nil nil nil nil (#<core$_ clojure.core$_ at ac9cbe> :number) nil nil nil)
It works! Can anybody explain why that works? I just now tried this thinking
it might solve the problem where it seemed to be evaluating the terms in
colls. I figured that back-quoting the return value might stop the return
value from further evaluation. But I have no idea why this solves the
problem of it returning nils where it should not have been--I still don't
even know why it was doing that incorrectly in the first place.
-Omri
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.hampshire.edu/pipermail/cs254f11/attachments/20111011/0b97e72f/attachment-0001.htm>
More information about the Cs254f11
mailing list