Initial commit

This commit is contained in:
Jeremy Penner 2015-05-06 09:36:01 -04:00
commit 2ad806a8fc
8 changed files with 487 additions and 0 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.DS_Store

214
LICENSE Normal file
View file

@ -0,0 +1,214 @@
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial code and
documentation distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are
distributed by that particular Contributor. A Contribution 'originates' from
a Contributor if it was added to the Program by such Contributor itself or
anyone acting on such Contributor's behalf. Contributions do not include
additions to the Program which: (i) are separate modules of software
distributed in conjunction with the Program under their own license
agreement, and (ii) are not derivative works of the Program.
"Contributor" means any person or entity that distributes the Program.
"Licensed Patents" mean patent claims licensable by a Contributor which are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.
"Program" means the Contributions distributed in accordance with this
Agreement.
"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license to
reproduce, prepare derivative works of, publicly display, publicly perform,
distribute and sublicense the Contribution of such Contributor, if any, and
such derivative works, in source code and object code form.
b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under
Licensed Patents to make, use, sell, offer to sell, import and otherwise
transfer the Contribution of such Contributor, if any, in source code and
object code form. This patent license shall apply to the combination of the
Contribution and the Program if, at the time the Contribution is added by the
Contributor, such addition of the Contribution causes such combination to be
covered by the Licensed Patents. The patent license shall not apply to any
other combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor grants the licenses
to its Contributions set forth herein, no assurances are provided by any
Contributor that the Program does not infringe the patent or other
intellectual property rights of any other entity. Each Contributor disclaims
any liability to Recipient for claims brought by any other entity based on
infringement of intellectual property rights or otherwise. As a condition to
exercising the rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual property rights
needed, if any. For example, if a third party patent license is required to
allow Recipient to distribute the Program, it is Recipient's responsibility
to acquire that license before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright license
set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form under
its own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all warranties and
conditions, express and implied, including warranties or conditions of title
and non-infringement, and implied warranties or conditions of merchantability
and fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors all liability for
damages, including direct, indirect, special, incidental and consequential
damages, such as lost profits;
iii) states that any provisions which differ from this Agreement are offered
by that Contributor alone and not by any other party; and
iv) states that source code for the Program is available from such
Contributor, and informs licensees how to obtain it in a reasonable manner on
or through a medium customarily used for software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the Program.
Contributors may not remove or alter any copyright notices contained within
the Program.
Each Contributor must identify itself as the originator of its Contribution,
if any, in a manner that reasonably allows subsequent Recipients to identify
the originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities with
respect to end users, business partners and the like. While this license is
intended to facilitate the commercial use of the Program, the Contributor who
includes the Program in a commercial product offering should do so in a
manner which does not create potential liability for other Contributors.
Therefore, if a Contributor includes the Program in a commercial product
offering, such Contributor ("Commercial Contributor") hereby agrees to defend
and indemnify every other Contributor ("Indemnified Contributor") against any
losses, damages and costs (collectively "Losses") arising from claims,
lawsuits and other legal actions brought by a third party against the
Indemnified Contributor to the extent caused by the acts or omissions of such
Commercial Contributor in connection with its distribution of the Program in
a commercial product offering. The obligations in this section do not apply
to any claims or Losses relating to any actual or alleged intellectual
property infringement. In order to qualify, an Indemnified Contributor must:
a) promptly notify the Commercial Contributor in writing of such claim, and
b) allow the Commercial Contributor tocontrol, and cooperate with the
Commercial Contributor in, the defense and any related settlement
negotiations. The Indemnified Contributor may participate in any such claim
at its own expense.
For example, a Contributor might include the Program in a commercial product
offering, Product X. That Contributor is then a Commercial Contributor. If
that Commercial Contributor then makes performance claims, or offers
warranties related to Product X, those performance claims and warranties are
such Commercial Contributor's responsibility alone. Under this section, the
Commercial Contributor would have to defend claims against the other
Contributors related to those performance claims and warranties, and if a
court requires any other Contributor to pay any damages as a result, the
Commercial Contributor must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
appropriateness of using and distributing the Program and assumes all risks
associated with its exercise of rights under this Agreement , including but
not limited to the risks and costs of program errors, compliance with
applicable laws, damage to or loss of data, programs or equipment, and
unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of the
remainder of the terms of this Agreement, and without further action by the
parties hereto, such provision shall be reformed to the minimum extent
necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Program itself
(excluding combinations of the Program with other software or hardware)
infringes such Recipient's patent(s), then such Recipient's rights granted
under Section 2(b) shall terminate as of the date such litigation is filed.
All Recipient's rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and
does not cure such failure in a reasonable period of time after becoming
aware of such noncompliance. If all Recipient's rights under this Agreement
terminate, Recipient agrees to cease use and distribution of the Program as
soon as reasonably practicable. However, Recipient's obligations under this
Agreement and any licenses granted by Recipient relating to the Program shall
continue and survive.
Everyone is permitted to copy and distribute copies of this Agreement, but in
order to avoid inconsistency the Agreement is copyrighted and may only be
modified in the following manner. The Agreement Steward reserves the right to
publish new versions (including revisions) of this Agreement from time to
time. No one other than the Agreement Steward has the right to modify this
Agreement. The Eclipse Foundation is the initial Agreement Steward. The
Eclipse Foundation may assign the responsibility to serve as the Agreement
Steward to a suitable separate entity. Each new version of the Agreement will
be given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version of
the Agreement is published, Contributor may elect to distribute the Program
(including its Contributions) under the new version. Except as expressly
stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
licenses to the intellectual property of any Contributor under this
Agreement, whether expressly, by implication, estoppel or otherwise. All
rights in the Program not expressly granted under this Agreement are
reserved.
This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to this
Agreement will bring a legal action under this Agreement more than one year
after the cause of action arose. Each party waives its rights to a jury trial
in any resulting litigation.

48
README.md Normal file
View file

@ -0,0 +1,48 @@
# exav - EXecution As a Value
## Why?
The fundamental building block of Clojure is the immutable value. Values can be shared, values can be saved, values can be inspected, values can be transformed.
Programs often must ask for input from the external world. exav models this much like core.async; we write code which looks very much like a simple function which is producing a value, except that sprinkled throughout are operations which cause execution to be suspended until some external event takes place to supply us with a new value.
core.async is wonderful, but its fundamental building block is the channel, which is not a value. We have no way of looking inside the computation which is suspended. We have no way of rewinding it. We have no way of persisting it. core.async code builds up a black box, to be fed with data.
I want the ability to rewind time, hotload new code, then run time forward again. I want the ability to script complex interactions in a game that take place over arbitrary amounts of time, and for my entire game state to exist in a map that can be saved as EDN or Transit. exav gives me the tools to do this.
## Usage
exav currently has one basic building block: the `proc` macro. The `proc` macro works very much like the `go` macro in core.async, except that instead of returning a channel, it returns a function, and instead of blocking on `>!` or `<!`, it blocks on calls to `wait`. The function returned by proc takes zero, one, or two arguments.
(let [p (proc (str "Hello, " (wait :name)))
; calling a proc with no arguments starts the process
state (p)
; a proc will always return a map with a standard structure
; if the :waitfor key exists, the value is whatever was passed to (wait)
waitfor (:waitfor state) ; => :name
; calling a proc with two arguments continues the process
; you must pass the state returned by the proc, and the value to be returned by (wait)
state2 (p state "Bob")
; if the :result key exists, it will be the only thing in the map, and is equal to the
; final return value of the proc
result (:result state2) ; => "Hello, Bob"
; note that it is completely legal to hold onto and re-use old states!
result2 (:result (p state "Phil")) ; => "Hello, Phil"
; a proc can also be called with just one argument, which is the same as passing nil
; for the second argument
result3 (:result (p state)) ; => "Hello, "
])
`wait` always takes a single argument; a value that describes what the proc is waiting for.
## License
Copyright © 2014 Jeremy Penner
Distributed under the Eclipse Public License either version 1.0 or (at
your option) any later version.

3
doc/intro.md Normal file
View file

@ -0,0 +1,3 @@
# Introduction to exav
TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)

7
project.clj Normal file
View file

@ -0,0 +1,7 @@
(defproject exav "0.1.0-SNAPSHOT"
:description "EXecution As a Value, flexible tools for async"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.6.0"]
[org.clojure/core.async "0.1.346.0-17112a-alpha"]])

78
src/clj/exav/core.clj Normal file
View file

@ -0,0 +1,78 @@
(ns exav.core
(:require [clojure.core.async.impl.ioc-macros :as ioc]))
;; Goal:
;; Convert an arbitrary clojure form that contains "blocking" calls, to a function which
;; accepts a state and an argument (the result of the blocking call) and returns the next
;; state and a "blocking" value. This state can then be persisted, arbitrarily rolled back,
;; and re-run; it is a simple clojure value. No hidden local state is generated.
; "machine" is a mutable array of objects used by the core.async ioc-macros to implement
; the state machine. standard indices into the array are used to implement various fields
; (what state is running, the value of the previous expression, etc)
; We extend this with two extra fields - WAITFOR-IDX, which stores the value passed to (wait),
; and WAITFOR-FLAG-IDX, which is set to true when (wait) is called, so that we can tell the
; difference between a final return and (wait nil).
(def ^:const WAITFOR-IDX ioc/USER-START-IDX)
(def ^:const WAITFOR-FLAG-IDX (+ ioc/USER-START-IDX 1))
(def ^:const LOCALS-START-IDX (+ ioc/USER-START-IDX 2))
(defn wait [ev]
(assert nil "wait must be used in an proc block"))
(defn wait* [machine nextblock ev]
"The function called to implement our custom terminator (last statement in a 'block')
when the state machine is running. Returns nil to signal that the state machine should
suspend execution (the other option is :recur, which would immediately move on to the
next block)"
(ioc/aset-all! machine WAITFOR-IDX ev
WAITFOR-FLAG-IDX true
ioc/STATE-IDX nextblock)
nil)
(defn config-machine [machine state value]
"Takes apart a state map as returned from a proc, and sets up all the values in an empty
machine array to allow it to continue. Returns the state's thread bindings, basically just
to pass to run-machine, because they are the only thing we need to configure whether or not
we have a state."
(assert (and (integer? (:state state))
(vector? (:locals state)))
"state is not valid")
; copy locals
(doseq [[i local] (map vector (iterate inc LOCALS-START-IDX) (:locals state))]
(ioc/aset-object machine i local))
(ioc/aset-all! machine
; copy next state
ioc/STATE-IDX (:state state)
; copy argument
ioc/VALUE-IDX value)
(:bindings state))
(defn run-machine [machine bindings]
"Given a machine array and thread bindings, runs the machine until it finishes or blocks,
and returns a new state map with the results."
(let [bindings (or bindings (clojure.lang.Var/getThreadBindingFrame))]
(ioc/aset-all! machine ioc/BINDINGS-IDX bindings)
(ioc/run-state-machine machine)
(if (ioc/aget-object machine WAITFOR-FLAG-IDX)
; blocked
{:bindings bindings
:locals (vec (for [i (range LOCALS-START-IDX (.length machine))]
(ioc/aget-object machine i)))
:state (ioc/aget-object machine ioc/STATE-IDX)
:waitfor (ioc/aget-object machine WAITFOR-IDX)}
; complete
{:result (ioc/aget-object machine ioc/VALUE-IDX)})))
(defmacro proc [& body]
`(let [machine-fn# ~(ioc/state-machine `(do ~@body) 2 (keys &env) {`wait `wait*})]
(fn proc#
([] (run-machine (machine-fn#) nil))
([state#] (proc# state# nil))
([state# value#]
(let [machine# (machine-fn#)
bindings# (config-machine machine# state# value#)]
(run-machine machine# bindings#))))))

120
src/clj/exav/perfunct.clj Normal file
View file

@ -0,0 +1,120 @@
(ns exav.perfunct)
; Persistable functions:
; A function in clojure is a first-class value that can be passed around and composed.
; It can't, however, be sensibly persisted, except in ad-hoc ways. In general, there
; is not much of a use-case for persisting arbitrary functions. We don't really ever
; want to be given an arbitrary block of clojure code, compile it, and run it - that's
; a security disaster. It would often be nice, however, to have a first-class way of
; _referring_ to a particular set of whitelisted functions, and be able to write down
; and read those _references_.
; One could concievably just persist a clojure var, but that has its own set of problems.
; Chief among them is that you would still require a whitelist to ensure that you're not
; being tricked into calling "evil" functions in places that you didn't expect.
; Another significant problem is that you lose any particular ability to use or persist
; a closure.
; perfunct provides a simple solution to these problems. We introduce an object called
; a peref (for "persistable reference"), which contains a simple value used to refer to
; the function, can be directly called like a function (much like a var), and derefed (if
; it is referring to a non-function unpersistable thing, such as an atom or channel).
; We provide built-in hooks for pr-str and transit serialization, reader literals, and
; functions for extracting a peref's value as clojure data.
; In addition to persisting references to functions, we can use this flexible
; functionality to persist references to atoms, to core.async channels, to expensive
; objects that should be instantiated lazily, or even to resources meant to be loaded
; from disk.
; definvokable adapted from code by Meikel Brandmeyer
; https://groups.google.com/forum/#!topic/clojure/pl4HgR9L_lg
(def max-arities 20)
(defmacro definvokable
[type fields f & deftype-tail]
(let [args (repeatedly max-arities gensym)
arity (fn [n]
(let [args (take n args)]
`(invoke [this# ~@args] (~f this# ~@args))))
vararg `(invoke [this# ~@args more#]
(apply ~f this# ~@args more#))
apply-to `(applyTo [this# args#] (apply ~f this# args#))]
`(deftype ~type
~fields
clojure.lang.IFn
~@(map arity (range (inc max-arities)))
~vararg
~apply-to
~@deftype-tail)))
(defn invoke-peref [peref & args] (apply @peref args))
(definvokable Peref [lookup name params] invoke-peref
clojure.lang.IDeref
(deref [_] (apply (.lookup-fn lookup) name params)))
(defn invoke-peref-lookup [lookup name & args] (->Peref lookup name args))
(definvokable PerefLookup [id lookup-fn] invoke-peref-lookup)
(defn peref-tag [p]
(let [id (.id (.lookup p))]
(str (namespace id) "/" (name id))))
(defn peref-data [p]
(let [name (.name p)
params (.params p)]
(vec (cons name params))))
(defmethod print-method Peref [v ^java.io.Writer w]
"Print an edn literal for a Peref"
(let [tag (str "#" (peref-tag v) " ")]
(.write w tag)
(print-method (peref-data v) w)))
(defn cached [lookup-fn]
"Takes the result of a lookup-fn and memoizes it. Also provides a function,
accessed by looking up :clear-cache, which clears the cache completely if called
with no arguments, and clears the cache for a particular peref when called with
its arguments."
(let [cache (atom {})
clear-cache
(fn ([] (reset! cache {}))
([peref] (swap! cache #(dissoc % (peref-data peref)))))]
(fn [& args]
(if (= (first args) :clear-cache)
(apply clear-cache (rest args))
(if (contains? @cache args)
(get @cache args)
(let [v (apply lookup-fn (vec args))]
(swap! cache #(assoc % args v))
v))))))
(defn partial-map [fn-map]
"Takes a map of the form {name fn} and returns a lookup function which
takes a name and the arguments to pass to fn and returns the fn.
It is legal for the value to not be a function so long as the lookup
is not passed any extra arguments besides the name."
(fn [name & args]
(let [fn (get fn-map name)]
(if fn
(apply partial fn args)
nil))))
(defn compose-lookup [& lookup-fns]
"Returns a lookup-fn which calls each of the passed-in lookup-fns in turn,
returning the first non-nil result."
(fn [& args]
(loop [lookup-fns lookup-fns]
(if (seq lookup-fns)
(let [lookup-fn (first lookup-fns)
v (apply lookup-fn args)]
(if (nil? v)
(recur (rest lookup-fns))
v))))))
(defn lookup [id & lookup-fns]
"Creates a peref lookup function, identified by a namespaced symbol, which is
used to create peref objects. Simply call the result with the arguments to pass
to your lookup-fns, and it will return a peref object that will call your lookup-fns
when derefed."
(->PerefLookup id (apply compose-lookup lookup-fns)))

7
test/exav/core_test.clj Normal file
View file

@ -0,0 +1,7 @@
(ns exav.core-test
(:require [clojure.test :refer :all]
[exav.core :refer :all]))
(deftest a-test
(testing "FIXME, I fail."
(is (= 0 1))))