data hiding

51 views
Skip to first unread message

Chouser

unread,
Aug 18, 2008, 3:15:05 PM8/18/08
to clo...@googlegroups.com
Let's say I have a library of functions that operate on a some data,
and I really *really* don't want client code to be able to see the
data directly. But I still want "normal" API patterns for access via
functions. For example, a guessing game:

(clojure/in-ns 'guesslib)
(clojure/refer 'clojure)

(def #^{:private true} thekey (gensym))

(defn new-target [mx]
(let [t (Math/floor (* mx (Math/random)))]
(fn [k] (when (= k thekey) t))))

(defn guess [tf g]
(let [t (tf thekey)]
(cond (< g t) :too-low
(> g t) :too-high
:else :you-win)))

Now in the user namespace, you could have this interaction:

user=> (refer 'guesslib)
nil
user=> (def tf (new-target 10))
#'user/tf
user=> tf
guesslib.new_target__2134$fn__2136@17cff66
user=> (tf guesslib/thekey)
java.lang.IllegalStateException: var: guesslib/thekey is not public

I've got a guess target, but I can't see it directly. However, I can
still interact with it:

user=> (guess tf 5)
:too-high
user=> (guess tf 2)
:too-low
user=> (guess tf 4)
:you-win

Nice, right? Except there's private and then there's private:

user=> (def stolen (ref nil))
#'user/stolen
user=> (clojure/in-ns 'guesslib) ; now you see where I'm going
#<Namespace: guesslib>
guess=> (dosync (ref-set user/stolen thekey))
G__2133
guess=> (in-ns 'user)
#<Namespace: user>
user=> (guess tf (tf @stolen))
:you-win

I win every time. :-/ So, my question is -- what's the right way to
do this? Or is my goal itself the problem?

Another way to go about it is to put all the functions that need
access to the target value in its scope (currently this is just
"guess", but this is just an example of a larger problem). In order
to preserve a normal calling API, I'd have to do something like:

(clojure/in-ns 'guesslib)
(clojure/refer 'clojure)

(defn new-target [mx]
(let [t (Math/floor (* mx (Math/random)))]
{'guess (fn [g]
(cond (< g t) :too-low
(> g t) :too-high
:else :you-win))}))

(defn guess [fmap & args]
(apply ('guess fmap) args))

...so the toplevel functions just delegate to the closures in the map.
I suppose one could make a macro to reduce the repetitiveness of
these top-level functions, but it still seems rather awkward.

What am I missing?
--Chouser

Stuart Sierra

unread,
Aug 18, 2008, 4:26:25 PM8/18/08
to Clojure
You've basically described OOP -- an object that responds to messages
without exposing its internal state. You could do it by closing over
a reference:

(clojure/in-ns 'guesslib)

(let [stash (ref {})]

(defn new-target [mx]
(let [handle (gensym)
t (Math/floor (* mx (Math/random)))]
(dosync (alter stash assoc handle t))
handle))

(defn guess [handle g]
(let [t (@stash handle)]
(cond (< g t) :too-low
(> g t) :too-high
:else :you-win))))


user=> (def tf (new-target 10))
#'user/tf
user=> (guess tf 5)
:too-low
user=> (guess tf 8)
:too-low
user=> (guess tf 9)
:you-win
user=> tf
G__2540


-Stuart

Chouser

unread,
Aug 20, 2008, 11:07:19 PM8/20/08
to clo...@googlegroups.com
On Mon, Aug 18, 2008 at 4:26 PM, Stuart Sierra
<the.stua...@gmail.com> wrote:
>
> You've basically described OOP -- an object that responds to messages
> without exposing its internal state.

I realize hiding data with encapsulation is a feature of OOP. I guess
my point is that many other OOP features are currently provided by
Clojure, although often in non-traditional ways, but that I wasn't
sure about the best way to accomplish this specific feature.

> You could do it by closing over a reference:

In my previous example I did close over the data I needed, but as I
said it seems awkward to have to define the functions in one place
(the map in new-target) and then sorta proxy each one with a top-level
method.

> (let [stash (ref {})]
>
> (defn new-target [mx]

[...clip...]

This is indeed a solution to the proxying awkwardness, but I don't
think using defn inside a let is an acceptable solution in Clojure.

--Chouser

Stuart Sierra

unread,
Aug 21, 2008, 3:37:44 PM8/21/08
to Clojure
On Aug 20, 11:07 pm, Chouser <chou...@gmail.com> wrote:
> > (let [stash (ref {})]
>
> > (defn new-target [mx]
>
> [...clip...]
>
> This is indeed a solution to the proxying awkwardness, but I don't
> think using defn inside a let is an acceptable solution in Clojure.

Hi Chris,
Just curious -- why don't you like defn inside let? I think Scheme
specifies that internal definitions are "local":
http://www.cs.cmu.edu/afs/cs/project/ai-repository/ai/html/r4rs/r4rs_7.html#SEC44
but Clojure does not. I can't think of any other way you could hide a
symbol.
-Stuart

Chouser

unread,
Aug 21, 2008, 4:08:15 PM8/21/08
to clo...@googlegroups.com
On Thu, Aug 21, 2008 at 3:37 PM, Stuart Sierra
<the.stua...@gmail.com> wrote:
>
> Just curious -- why don't you like defn inside let?

It's generally recommended in Clojure to do def or defn only at the top level.

Perhaps this case is a little different because we're not re-def'ing
an existing symbol, but I don't think it's necessary.

> I can't think of any other way you could hide a symbol.

I showed a mechanism in my first post on the subject. Instead of
doing def's in the let, you set up a map of the closures you want, and
return that. Then you can have top-level defn's that turn around and
run the closures that have access to the private locals they closed
over.

If that's the best we can do, then that's fine and I'll see if there's
some macroness that can tidy it up a bit.

--Chouser

Stephen C. Gilardi

unread,
Aug 21, 2008, 4:21:11 PM8/21/08
to clo...@googlegroups.com

On Aug 21, 2008, at 4:08 PM, Chouser wrote:

> It's generally recommended in Clojure to do def or defn only at the
> top level.

Lambda to the rescue...

(def new-target
(let [stash (ref {})]
(fn [mx]
...)))

or as a concrete example:

(def another
(let [stash (ref 1)]
(fn [] (dosync (commute stash inc)) @stash)))

or as recently discussed:

(def another
(let [stash (ref 1)]
(fn [] (dosync (alter stash inc)))))

--Steve

Stephen C. Gilardi

unread,
Aug 21, 2008, 4:31:41 PM8/21/08
to clo...@googlegroups.com

On Aug 21, 2008, at 4:21 PM, Stephen C. Gilardi wrote:

>
>
> On Aug 21, 2008, at 4:08 PM, Chouser wrote:
>
>> It's generally recommended in Clojure to do def or defn only at the
>> top level.
>
> Lambda to the rescue...
>
> (def new-target
> (let [stash (ref {})]
> (fn [mx]
> ...)))

Which you already had... ' sorry for the noise.

:-)

--Steve


Rich Hickey

unread,
Aug 25, 2008, 4:53:52 PM8/25/08
to Clojure


On Aug 18, 3:15 pm, Chouser <chou...@gmail.com> wrote:
Not much. Clojure's namespaces are to protect against accident, not
malice. And closures, as you showed, provide for truly inaccessible
data.

Rich
Reply all
Reply to author
Forward
0 new messages