Background
I am authoring pulley.objective
, an Open Source library that provides an object system inspired by Python's object model. I have some features implemented, and would appreciate feedback before I continue.
Ultimate goals include:
- Support for calculated properties
- Allow methods to be attached to an object and invoked
- A method differs from a function in that a method has access to the object on which the method was invoked
- Objects are (mostly) compatible with Clojure's persistent map objects.
- Inheritance, with Python-style MRO and access to
super
The first two are currently implemented, and the third (map compatibility) is partially implemented. The rest (inheritance, etc.) will be implemented in the future.
Code
The code here is from revision 4ff7e05d1ed3dcd9c3d2642bd13f506809259a3b. Although I'm only posting the code here, feel free to comment on anything in the repository, such as documentation.
All the code is contained in src/com/positronic_solutions/pulley/objective.clj
.
Preliminaries
Here's the namespace declaration and a few preliminary declarations:
(ns com.positronic-solutions.pulley.objective)
(declare object-
property-get-)
PersistentObject
The PersistentObject
type is responsible for implementing objects (I've elided most of the invoke
variants here, since they're practically the same):
(deftype PersistentObject [attrs]
clojure.lang.Associative
(assoc [self k v]
(object- (assoc attrs k v)))
(cons [self entry]
(let [[k v] entry]
(assoc self k v)))
(containsKey [self k]
(contains? attrs k))
(empty [self]
(new PersistentObject (empty attrs)))
(entryAt [self k]
(. attrs (entryAt k)))
(equiv [self other]
(. attrs (equiv other)))
(seq [self]
(seq attrs))
(valAt [self k]
(get attrs k))
(valAt [self k default]
(get attrs k default))
clojure.lang.Counted
(count [self]
(count attrs))
clojure.lang.IFn
(invoke [self k]
(if (contains? self k)
(-> (get self k)
(property-get- self))
(throw (new IllegalArgumentException (str "Attribute not found: " k)))))
(invoke [self k arg]
((self k) arg))
(invoke [self k arg1 arg2]
((self k) arg1 arg2))
(invoke [self k arg1 arg2 arg3]
((self k) arg1 arg2 arg3))
.
.
.
(invoke [self k arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14 arg15 arg16 arg17 arg18 arg19]
((self k) arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14 arg15 arg16 arg17 arg18 arg19))
(invoke [self k arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14 arg15 arg16 arg17 arg18 arg19 args]
(apply (self k) arg1 arg2 arg3 arg4 arg5 arg6 arg7 arg8 arg9 arg10 arg11 arg12 arg13 arg14 arg15 arg16 arg17 arg18 arg19 args))
(applyTo [self args]
(clojure.lang.AFn/applyToHelper self args)))
The object-
function provides a private "constructor" for PersistentObject
objects:
(defn- object- [attrs]
(new PersistentObject attrs))
map->object
provides a public constructor:
(defn map->object [attrs]
(object- attrs))
Properties
The IProperty
protocol is used to support properties. Only getting properties is supported in this version.
(defprotocol ^:private IProperty
(property-get- [self instance]))
By default, attributes can be seen as "properties" where the "getter" is the identity
function. So we extend IProperty
to Object
as:
(extend-protocol IProperty
Object
(property-get- [self instance]
self))
This implementation will be used when a non-property attribute is accessed.
The property
function is used to implement a property:
(defn property
([& {getter :get}]
(reify IProperty
(property-get- [self instance]
(getter instance)))))
Methods
Methods are implemented using properties. The method*
function wraps a function f
as a property
which "binds" f
to the instance the property is on. As with Python methods, f
is expected to take an explicit self
argument.
(defn method*
([f]
(property :get (fn [instance]
(fn [& args]
(apply f instance args))))))
The method
macro is provides some syntactic sugar for defining methods:
(defmacro method
([& forms]
`(method* (fn ~@forms))))
Example Use
Here's a simple example of creating and using an object. Additional examples can be found in the project's Readme:
First, import the library:
(require '[com.positronic-solutions.pulley.objective :as objective])
Now, create an object:
(def foo (objective/map->object {;; "Normal" attribute
::x 5
;; Property that increments ::x
::inc (objective/property :get (fn [self]
(update self ::x inc)))
;; Example method
;; Computes sum of xs + self's ::x
::sum (objective/method [self & xs]
(reduce + (self ::x) xs))}))
Now we can use foo
as follows:
(foo ::x) ;; => 5
((foo ::inc) ::x) ;; => 6
(foo ::inc ::x) ;; => 6
(foo ::inc ::inc ::x) ;; => 7
(foo ::sum 1 2 3) ;; => 11
(foo ::inc ::sum 1 2 3) ;; => 12
(foo ::inc ::inc ::sum 1 2 3) ;; => 13