5
\$\begingroup\$

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 identityfunction. 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
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.