Is it possible to decompose a Clojure function?
Asked Answered
A

4

6

While I may incorrectly interpret the concept of homoiconicity, I've understood it as 'code being data'.

So, I can write code like this:

(def subject "world")
(def helo '(str "Hello " subject))

At this point, helo is only data, but can be executed as code like this:

(eval helo)

which returns "Hello world".

I can also continue to treat helo as data:

(first helo)
(count helo)

which returns respectively str and 3.

So far so good. However, as soon as I wrap the code in a function, I seem to lose the ability to treat code as data:

(defn helofn [subject]
  (str "Hello " subject))

How do I decompose helofn? It seems that I can't treat it as data; if I do this:

(count helofn)

I get an exception:

java.lang.UnsupportedOperationException: count not supported on this type: user$helofn

Is there another way to decompose helofn, or am I just expecting too much from homoiconicity?

Author answered 23/5, 2013 at 15:34 Comment(0)
P
6

defn is just a macro:

(macroexpand '(defn helofn [subject]
  (str "Hello " subject)))

(def helofn (clojure.core/fn ([subject] (str "Hello " subject))))

If you define helofn the way you defined helo, you'll be able to treat it as data:

(def helofn '(fn [subject]
  (str "Hello " subject)))

Now you can eval and call this function:

((eval helofn) "world")

and to treat it as a data:

(count helofn)

But, when you use defn macro you associates helofn variable with compiled function and not with it's code.

It's not just functions. Let's say you defined hello with the following code:

(def helo (str "Hello " subject))

Now hello is associated with "Hello world" string and not with (str "Hello " subject) code. So, now there is no way to get the code this string was built with.

N.B. If you want to treat clojure code as data you should look into its macros. Any code passed to a macro is treated as data and any data returned by a macro is treated as code.

Psychosurgery answered 23/5, 2013 at 16:19 Comment(0)
W
9

The helofn definition is data, but you're letting it be evaluated (just as you explicitly evaluated the helo list). If you treated the definition in the same way as helo, then it will remain data, and amenable to whatever transformations you want to apply:

(def helofndata '(defn helofn [subject]
                   (str "Hello " subject))

=> (second helofndata)
helofn
=> (eval helofndata)
#'user/helofn
Weinberger answered 23/5, 2013 at 16:20 Comment(0)
P
6

defn is just a macro:

(macroexpand '(defn helofn [subject]
  (str "Hello " subject)))

(def helofn (clojure.core/fn ([subject] (str "Hello " subject))))

If you define helofn the way you defined helo, you'll be able to treat it as data:

(def helofn '(fn [subject]
  (str "Hello " subject)))

Now you can eval and call this function:

((eval helofn) "world")

and to treat it as a data:

(count helofn)

But, when you use defn macro you associates helofn variable with compiled function and not with it's code.

It's not just functions. Let's say you defined hello with the following code:

(def helo (str "Hello " subject))

Now hello is associated with "Hello world" string and not with (str "Hello " subject) code. So, now there is no way to get the code this string was built with.

N.B. If you want to treat clojure code as data you should look into its macros. Any code passed to a macro is treated as data and any data returned by a macro is treated as code.

Psychosurgery answered 23/5, 2013 at 16:19 Comment(0)
B
4

Homoiconicity is a very powerful concept and I don't think you are expecting too much from it.

defn is actually a macro that uses the def special form to define a function, so:

(defn sq [x]
  (* x x))

Is actually equivalent to:

(def sq (fn ([x] (* x x))))

So defn here is receiving the args sq [x] (* x x), then builds the list (def sq (fn ([x] (* x x)))), returns it as the result of the macro and is then eval'ed. This is all done through the manipulation of lists, maps, vectors, symbols, etc., by the defn macro.

The fact that in Clojure you can't get the original list of symbols from which you defined a function, has to do with the fact that in Clojure all code is compiled. This is why evaluating (fn [x] 1) in the REPL returns something like #<user$eval809$fn__810 user$eval809$fn__810@10287d> . But still, as mentioned in a previous answer, the code that is evaluated is data.

Maybe I'm going too far with this, but if you wanted to have for each function you define, the data from which it was created, you could add it to its metadata by creating your own custom macro.

Here's a naive implementation for such a macro:

(defmacro defn* [x & body ]
  (let [form `'~&form
        x    (vary-meta x assoc :form form)]
    `(defn ~x ~@body)))
;=> #'user/defn*

(defn* sq [x]
  (* x x))
;=> #'user/sq

(:form (meta #'sq))
;=> (defn* sq [x] (* x x))

&form is an implicit argument (together with &env) that contains the whole (unevaluated) form with which the macro was called (i.e. the data that is evaluated by the compiler).

Hope this helps and it doesn't bring more confusion.

Bevan answered 23/5, 2013 at 17:28 Comment(0)
N
1

It looks like no based on

get a clojure function's code

and

Can you get the "code as data" of a loaded function in Clojure?

Basically you can get the source from a function defined in a .clj file but there's no reliable way to retrieve the data structures that built a function from the function alone.

EDIT: Also I think you are expecting too much from homoiconicity. The code itself is data yes but it's fairly standard to not be able to retrieve the original source code based on the artifact emitted by that code. Like when I have 2 I have no way of knowing that it was produced by (+ 1 1) or (- 4 2) in the same way a function is a piece of data created by calling fn over some other data structures that get interpreted as code.

Nichols answered 23/5, 2013 at 16:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.