├── pylon.gif ├── .gitignore ├── project.clj ├── src └── pylon │ ├── classes.cljs │ └── macros.clj └── README.md /pylon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bodil/pylon/HEAD/pylon.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | *.jar 7 | *.class 8 | .lein-deps-sum 9 | .lein-failures 10 | .lein-plugins 11 | .repl 12 | /js 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.bodil/pylon "0.3.0" 2 | :description "A Javascript class system in 100% Clojurescript" 3 | :url "https://github.com/bodil/pylon" 4 | :license {:name "Apache License, version 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0.html"} 6 | :plugins [[lein-cljsbuild "0.2.10"] 7 | [org.bodil/lein-noderepl "0.1.4"]] 8 | :cljsbuild {:builds 9 | [{:source-path "src" 10 | :compiler 11 | {:output-to "js/main.js" 12 | :output-dir "js" 13 | :optimizations :simple 14 | :target :nodejs 15 | :jar true}}]} 16 | :main "js/main.js") 17 | -------------------------------------------------------------------------------- /src/pylon/classes.cljs: -------------------------------------------------------------------------------- 1 | (ns pylon.classes) 2 | 3 | (defn- method-fn-name 4 | [method-name] 5 | (str "__pylon$method$" method-name)) 6 | 7 | (defn- pylon-prop? [prop] 8 | (= "__pylon$" (subs prop 0 8))) 9 | 10 | (defn- pylon-parent-proto [p] 11 | (when-let [parent (aget p "__pylon$superclass")] 12 | (when-let [proto (.-prototype parent)] 13 | (when (.hasOwnProperty proto "__pylon$classname") 14 | proto)))) 15 | 16 | (defn- find-props [p] 17 | (let [parent (pylon-parent-proto p) 18 | props (remove pylon-prop? (.getOwnPropertyNames js/Object p))] 19 | (if parent 20 | (concat props (find-props parent)) 21 | props))) 22 | 23 | (defn create-ctor [] 24 | (fn ctor [& args] 25 | (this-as 26 | this 27 | (let [p (.getPrototypeOf js/Object this) 28 | superclass (aget p "__pylon$superclass")] 29 | (doseq [bind (apply hash-set (find-props p))] 30 | (let [func (aget this bind)] 31 | (when (fn? func) 32 | (aset this bind (goog/bind func this)))))) 33 | (when-let [constructor (.-constructor this)] 34 | (.apply constructor this (into-array args))) 35 | this))) 36 | 37 | (defn invoke-super [superclass method context args] 38 | (let [proto (.-prototype superclass) 39 | foreign? (nil? (aget proto "__pylon$classname")) 40 | method-name (if foreign? method (method-fn-name method)) 41 | args (if foreign? args (cons context args)) 42 | super-method (aget proto method-name) 43 | super-fn (if (and (= method "constructor") (not super-method)) 44 | superclass super-method) 45 | args (into-array args)] 46 | (.apply super-fn context args))) 47 | 48 | (defn method-wrapper [funcname] 49 | (fn [& args] 50 | (this-as this (apply (aget this funcname) (cons this args))))) 51 | -------------------------------------------------------------------------------- /src/pylon/macros.clj: -------------------------------------------------------------------------------- 1 | (ns pylon.macros 2 | (:require [clojure.walk :as w])) 3 | 4 | (defn- method-fn-name 5 | [method-name] 6 | (str "__pylon$method$" method-name)) 7 | 8 | (defn- transform-body [body] 9 | (w/postwalk 10 | (fn [form] 11 | (if (and (seq? form) 12 | (= 2 (count form)) 13 | (= 'clojure.core/deref (first form)) 14 | (symbol? (second form)) 15 | (= "." (subs (name (second form)) 0 1))) 16 | (list (symbol (str ".-" (subs (name (second form)) 1))) (symbol "this")) 17 | form)) 18 | body)) 19 | 20 | (defn- method-def 21 | [ctor method-name sig body] 22 | (let [sig-with-this (apply vector 'this sig) 23 | body (transform-body body)] 24 | `(fn ~(symbol method-name) ~sig-with-this 25 | (let [~'__pylon_method_name ~method-name 26 | ~'__pylon_prototype (.-prototype ~ctor)] 27 | ~@body)))) 28 | 29 | (defn- method-from-spec [spec] 30 | (if (= 'defn (first spec)) (method-from-spec (rest spec)) 31 | (let [name (name (first spec))] 32 | {:name name 33 | :fn-name (method-fn-name name) 34 | :sig (second spec) 35 | :body (drop 2 spec)}))) 36 | 37 | (defn- parse-args [args] 38 | (loop [args args opts {}] 39 | (cond 40 | (keyword? (first args)) 41 | (recur (drop 2 args) (assoc opts (first args) (second args))) 42 | :else [opts args]))) 43 | 44 | (defmacro defclass 45 | [class-name & args] 46 | (let [[{:keys [extends mixin]} specs] (parse-args args) 47 | methods (map method-from-spec specs) 48 | ctor (gensym "ctor") 49 | class-string (str (ns-name *ns*) "." class-name)] 50 | `(let [~ctor (pylon.classes/create-ctor)] 51 | 52 | ;; Build the constructor 53 | (def ~class-name ~ctor) 54 | 55 | ;; Extend with superclass prototype 56 | ~(when extends 57 | `(do 58 | (goog/inherits ~class-name ~extends) 59 | (aset (.-prototype ~class-name) "__pylon$superclass" ~extends))) 60 | 61 | ;; Extend with mixins 62 | ~@(for [m mixin] 63 | `(let [proto# (or (.-prototype ~m) ~m)] 64 | (goog/mixin (.-prototype ~class-name) proto#))) 65 | 66 | (aset (.-prototype ~ctor) "__pylon$classname" ~class-string) 67 | 68 | ;; Define methods 69 | ~@(for [{:keys [name fn-name sig body]} methods 70 | :let [dashname (symbol (str "-" name))]] 71 | `(let [func# ~(method-def class-name name sig body)] 72 | ;; Apply the method to the prototype 73 | (aset (.-prototype ~class-name) ~fn-name func#) 74 | (set! (.. ~class-name -prototype ~dashname) 75 | (pylon.classes/method-wrapper ~fn-name)) 76 | ;; Export the method name 77 | (goog/exportProperty (.-prototype ~class-name) 78 | ~name (.. ~class-name -prototype ~dashname)))) 79 | 80 | ;; Export the class 81 | (goog/exportSymbol ~class-string ~class-name)))) 82 | 83 | (defmacro super [& args] 84 | `(let [super# (aget ~'__pylon_prototype "__pylon$superclass")] 85 | (pylon.classes/invoke-super super# ~'__pylon_method_name ~'this ~args))) 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pylon 2 | 3 | A Javascript class system in 100% Clojurescript. 4 | 5 | ## Rationale 6 | 7 | Class based OO is not cool. In fact, it's quite the opposite of the 8 | kind of code you should be writing as a Clojurescript developer. 9 | However, you sometimes find yourself having to deal with Javascript's 10 | ecosystem—certain of its more popular libraries in particular—out of 11 | necessity. Clojurescript's core interop facilities are fine for 12 | dealing with dumb JS objects, but when you have to get into more 13 | advanced OO—creating classes and subclasses—the primitives are no 14 | longer adequate. 15 | 16 | Pylon is intended as an interop facility. It is most emphatically not 17 | intended as a tool for turning Clojurescript into an OO language. 18 | Don't build your applications on it; be smart and use functional 19 | programming instead. Pylon is only for when you really need to deal 20 | with legacy Javascript code. 21 | 22 | ## Installation 23 | 24 | To use Pylon in your project, put the following in the `:dependencies` 25 | vector of your `project.clj` file: 26 | 27 | ```clojure 28 | [org.bodil/pylon "0.3.0"] 29 | ``` 30 | 31 | ## Defining Classes 32 | 33 | Use the `defclass` macro to build Javascript style classes using Pylon. 34 | 35 | ```clojure 36 | (ns pylon.test 37 | (:require [pylon.classes]) 38 | (:use-macros [pylon.macros :only [defclass]])) 39 | 40 | (defclass Hello 41 | (defn constructor [name] 42 | (set! @.name name)) 43 | (defn hello [] 44 | (console/log (str "Hello " @.name "!")))) 45 | 46 | (.hello (Hello. "Kitty")) 47 | ;; => "Hello Kitty!" 48 | ``` 49 | 50 | Note that all methods have a `this` symbol available to them, just 51 | like in Javascript. Unlike in Javascript, it will always be bound to 52 | the actual object instance, even when passing an instance method as a 53 | callback. 54 | 55 | Notice the shorthand for referencing object properties: `@.name` 56 | anywhere inside a `defclass` is synonymous to `(.-name this)`. Thus, 57 | to read a property `foo` on the current object, simply use `@.foo`, 58 | and to set the property, use `(set! @.foo value)`. If `foo` is a 59 | method, you can invoke it directly using `(@.foo)`, or with arguments, 60 | `(@.foo "bar" "gazonk")`. (Note that this only works reliably on Pylon 61 | methods, as Clojurescript will clobber `this` when a function is 62 | called in this way.) 63 | 64 | ## Inheritance 65 | 66 | Pylon allows you to define inheritance using the `:extends` keyword, 67 | and call superclass methods using the `super` macro. 68 | 69 | ```clojure 70 | (ns pylon.test 71 | (:require [pylon.classes]) 72 | (:use-macros [pylon.macros :only [defclass super]])) 73 | 74 | (defclass Hello 75 | (defn constructor [name] 76 | (set! @.name name)) 77 | (defn hello [] 78 | (console/log (str "Hello " @.name "!")))) 79 | 80 | (.hello (Hello. "everypony")) 81 | ;; => "Hello everypony!" 82 | 83 | (defclass HelloSailor :extends Hello 84 | (defn constructor [] 85 | (super "sailor"))) 86 | 87 | (.hello (HelloSailor.)) 88 | ;; => "Hello sailor!" 89 | ``` 90 | 91 | ## Mixins 92 | 93 | If you need multiple inheritance, you can use the `:mixin` keyword to 94 | extend your prototype further. Note that we've left the world of 95 | prototypal inheritance behind when we do this: properties are copied 96 | from the mixin objects into your object prototype; it does not 97 | actually add more parent prototypes, which would be impossible. 98 | 99 | # License 100 | 101 | Copyright 2012 Bodil Stokke 102 | 103 | Licensed under the Apache License, Version 2.0 (the "License"); you 104 | may not use this file except in compliance with the License. You may 105 | obtain a copy of the License at 106 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). 107 | 108 | Unless required by applicable law or agreed to in writing, software 109 | distributed under the License is distributed on an "AS IS" BASIS, 110 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 111 | implied. See the License for the specific language governing 112 | permissions and limitations under the License. 113 | --------------------------------------------------------------------------------