Adding user-defined state to classes created with (proxy ...)

93 views
Skip to first unread message

Tom Faulhaber

unread,
Jan 2, 2009, 1:30:23 AM1/2/09
to Clojure
(This is sort of a follow-up to this thread from last July:
http://groups.google.com/group/clojure/browse_thread/thread/7f5cf3e78954b81d/aae7f082c51337c9?lnk=gst&q=proxy#aae7f082c51337c9.)

Recently, I've been building a version of java.io.Writer that knows
what the
current column is on the output so it can handle tabs, etc.

It would be pretty easy to do this with AOT, :genclass, etc. and just
have a ref
to the column number in my state, but life seems easier if you don't
have to use
AOT, so I considered doing it with a proxy.

However, proxies *only* support the methods provided by their super-
classes. They
do this so that the proxy class can be compiled once, making proxy
intances and
variation super-cheap.

I dug through the code in core_proxy.clj while trying to learn about
this and it
occurred to me that proxies could add state (similar to that created
in
:gen-class) with the following changes:

1) Add a __clojureProxyState field that points to a ref to a map, but
is initialized to
nil.
2) Add a getClojureProxyState() method to IProxy that returns the ref
in question, lazily
instantiating (ref {}) if necessary.

Of course, the ref to a map thing is optional. The state could be left
open as
in :gen-class.

This seems like it would add this capability without much problem. It
has a few
issues I can see:

* We're a functional language so more places to add state are against
the base
philosophy.
* The lazy instantiation could race (but that might be fixable).
* The new names increase the chance of name collison.

I'm interested to hear what folks think. If this was a capability that
Rich and
others agreed was a good addition, I'd be happy to build it and crank
out a
patch for it.

Tom Faulhaber

unread,
Jan 2, 2009, 1:32:50 AM1/2/09
to Clojure
Oops, sorry about the wrapping!

Chouser

unread,
Jan 3, 2009, 6:21:18 PM1/3/09
to clo...@googlegroups.com
On Fri, Jan 2, 2009 at 1:30 AM, Tom Faulhaber <tomfau...@gmail.com> wrote:
>
> (This is sort of a follow-up to this thread from last July:
> http://groups.google.com/group/clojure/browse_thread/thread/7f5cf3e78954b81d/aae7f082c51337c9?lnk=gst&q=proxy#aae7f082c51337c9.)
>
> Recently, I've been building a version of java.io.Writer
> that knows what the current column is on the output so it
> can handle tabs, etc.
>
> It would be pretty easy to do this with AOT, :genclass,
> etc. and just have a ref to the column number in my state,
> but life seems easier if you don't have to use AOT, so I
> considered doing it with a proxy.
>
> However, proxies *only* support the methods provided by
> their super- classes. They do this so that the proxy class
> can be compiled once, making proxy intances and variation
> super-cheap.

I much prefer using proxy over gen-class when at all
possible, and so far I've had a lot of success. For state
especially, closures are usually sufficient. Here's an
example of a proxy that maintains state in a local mutable
object. In this case it's a StringBuilder -- in other cases
a ref might be more appropriate:

(import '(java.io LineNumberReader FileReader PushbackReader))
(with-open [rdr (LineNumberReader. (FileReader. "test.clj"))]
(let [text (StringBuilder.)
pbr (proxy [PushbackReader] [rdr]
(read [] (let [i (proxy-super read)]
(.append text (char i))
i)))]
(read (PushbackReader. pbr))
(str text)))

This is based on code from clojure.contrib.repl-utils.
I know I mentioned this to you in IRC, but I thought I
should bring it up here for the benefit of others. In a lot
of cases this kind of usage is sufficient.

Your objection was, I believe, that you wanted to return the
proxy object from your function, but allow users of it to
access the state. As I suggested at the time, this can be
done by returning a Clojure collection (probably a map)
instead of the bare proxy object, and having the state live
in there. This has all the benefits of Clojure persistent,
as well as allowing for more than a single state field.
This might look something like:

(defn make-my-obj []
(let [my-state (ref 0)
obj (proxy [BaseClass] [] ...)]
{:my-state my-state, :obj obj}))

This would give you sufficient structure to add as much
state as you want. If you need ways to directly manipulate
that state, rather than going through the :obj, you could
add functions to the hash as well that by closing over the
same state could act as methods. Something like:

((:set-my-state my-obj) new-state)

Of course these could be wrapped in regular functions as
well get a more idiomatic api.

Finally, if this is just too clumsy, I would still prefer
gen-interface over gen-class. This would allow you to
declare all the state-manipulation and -access methods you'd
need. Then you could use 'proxy' and close over any state
objects you need, returning the base proxy object.

--Chouser

Tom Faulhaber

unread,
Jan 4, 2009, 1:28:12 AM1/4/09
to Clojure
Chris.

Yes, my current implementation is as we discussed (I use a vector
instead of a map, but that's a nit) and it works great as long as I
control the object and can pull out their component parts when I need
to. However, when I pass it other places that may not understand what
I have, it gets more problematic.

One of the things I would like to do is to allow users of the library
to be able to create one of these column-aware Writer objects and pass
it around like any other writer. This works fine (just by pulling out
the appropriate component and passing) but loses it's context if it
gets passed into your code later.

For example:

...
(let [[current-column my-writer] (column-writer *out*)]
(binding [*out* my-writer]
(joes-pprint '(a list of values))))
...


and, in some other library:

(defn joes-pprint [aseq]
(print "The seq: ")
(cl-format true "~&~{~a~^, ~}" aseq) ; my version of the CL format
function. true => *out*
(prn))

(Obviously this example is a little contrived. Interesting cases are
more likely to come up when multiple functions are doing the writing.)

Here, cl-format has no way to determine that *out* is bound to a
column-aware proxy on java.io.Writer, nor can it query that writer for
the current column. It's easy to make cl-format smart enough to take
either the Writer or the vector, but that would break compatibility
with other functions/methods that expect to get a writer.

Let me acknowledge two things here:
1) This use case may be obscure enough that I should just buckle down
and give (ns (:gen-class ...)) some love.
2) It occurred to me after writing my previous message, that it would
be pretty trivial to write a (proxy+ ...) extension outside the core.
If it found enough use, it could be pulled in (a la condp) as a non-
breaking change sometime in the future,

In another direction, your response gave me kind of a sick idea. It
would be pretty easy (I think) to create a "proxy-map" function:

(proxy-map [amap akey] ...)

proxy-map would create a new object that wrapped the map it was passed
and implemented IPersistantMap. It would also examine the the object
(presumably some Java object) at (akey amap) and proxy all the methods
it implements to it.

So if I had code like this:

(let [column-counter (ref 0)]
(proxy-map { :column column-counter
:writer (proxy [Writer] []
... (a bunch of message that close on column-
counter)}
:writer))

It would return an object that could be used as a Writer by any code
that wanted a Writer, but could also be examined by a piece of code
with (and (instance? IPersistantMap this-writer) (:column this-
writer)) which would return the ref to the column-count, if and only
if this was one of the above proxy-maps.

Implementing this would require some Java-fu, but what's interesting
is that core_proxy.clj seems to lay out a recipe that shows how to do
this and a whole bunch of things like it. Scary, awesome or pointless?
I'm not quite sure.

Following this argument a little further, you can think of clojure as
providing a powerful language for implementing systems that want to
generate JVM byte code directly.

On Jan 3, 3:21 pm, Chouser <chou...@gmail.com> wrote:
> On Fri, Jan 2, 2009 at 1:30 AM, Tom Faulhaber <tomfaulha...@gmail.com> wrote:
>
> > (This is sort of a follow-up to this thread from last July:
> >http://groups.google.com/group/clojure/browse_thread/thread/7f5cf3e78....)

Chouser

unread,
Jan 4, 2009, 11:39:44 AM1/4/09
to clo...@googlegroups.com
On Sun, Jan 4, 2009 at 1:28 AM, Tom Faulhaber <tomfau...@gmail.com> wrote:
>
> 1) This use case may be obscure enough that I should just buckle down
> and give (ns (:gen-class ...)) some love.

It may be, but let me again mention the option of gen-interface plus
proxy. Sometimes you need to produce a physical .class files of
concrete types (for servlet containers, for use with android, etc.).
But if that's not the case, you may prefer the simplicity of
gen-interface. Then you can implement your interface in as many ways
as needed using 'proxy'.

> 2) It occurred to me after writing my previous message, that it would
> be pretty trivial to write a (proxy+ ...) extension outside the core.
> If it found enough use, it could be pulled in (a la condp) as a non-
> breaking change sometime in the future,

Excellent point. gen-interface itself started in contrib.

> In another direction, your response gave me kind of a sick idea. It
> would be pretty easy (I think) to create a "proxy-map" function:
>
> (proxy-map [amap akey] ...)
>
> proxy-map would create a new object that wrapped the map it was passed
> and implemented IPersistantMap. It would also examine the the object
> (presumably some Java object) at (akey amap) and proxy all the methods
> it implements to it.

Yeah, that is kind of sick. :-D

--Chouser

Greg Harman

unread,
Jan 7, 2009, 3:18:39 PM1/7/09
to Clojure
Chouser,

Do you have an example of gen-interface + proxy working together? Take
a look at the following. Proxy works fine for a Java-provided
interface, but not for the generated one (ICompileTest.class is being
generated and is in the filesystem/classpath where expected.)

(ns compiletest)
(gen-interface :name ICompileTest)
(proxy [java.io.InputStream] []) ; Just to make sure proxy works by
itself (line 3)
(proxy [ICompileTest] []) ; Line 4

---

user=> (binding [*compile-path* *fusion-compile-path*] (compile
'compiletest))
java.lang.NullPointerException (compiletest.clj:4)

thanks,
Greg

Chouser

unread,
Jan 7, 2009, 3:46:30 PM1/7/09
to clo...@googlegroups.com
On Wed, Jan 7, 2009 at 3:18 PM, Greg Harman <gha...@gmail.com> wrote:
>
> Do you have an example of gen-interface + proxy working together?

You're calling my bluff, eh? Well, no I don't yet. I'm doing ugly
hacky things instead, to avoid the compile step. But since you've
thrown down the gauntlet, to mix some metaphors...

I think the problem with your example is trying to work with classes
or namespaces without any package names. This sometimes works a bit,
but it's not really supported. So I put your example code into a file
named "my_ns/compiletest.clj", and changed it to:

(ns my-ns.compiletest)
(gen-interface :name my_ns.ICompileTest)


(proxy [java.io.InputStream] []) ; Just to make sure proxy works by
itself (line 3)

(proxy [my_ns.ICompileTest] []) ; Line 4

Now compiling works fine for me:

user=> (compile 'my-ns.compiletest)
my-ns.compiletest

--Chouser

Greg Harman

unread,
Jan 7, 2009, 4:17:32 PM1/7/09
to Clojure
> You're calling my bluff, eh?  Well, no I don't yet.

Although I have been known to do some bluff-calling, in this case I
was actually hoping you had done it because I need this for a project
I'm working on. :-)

> I think the problem with your example is trying to work with classes
> or namespaces without any package names.  This sometimes works a bit,
> but it's not really supported.  So I put your example code into a file
> named "my_ns/compiletest.clj", and changed it to:
>
> (ns my-ns.compiletest)
> (gen-interface :name my_ns.ICompileTest)
> (proxy [java.io.InputStream] []) ; Just to make sure proxy works by
> itself (line 3)
> (proxy [my_ns.ICompileTest] []) ; Line 4
>
> Now compiling works fine for me:
>
> user=> (compile 'my-ns.compiletest)
> my-ns.compiletest

Yep, that works for me too. Thx.

-Greg
Reply all
Reply to author
Forward
0 new messages