pax_global_header 0000666 0000000 0000000 00000000064 13141156451 0014513 g ustar 00root root 0000000 0000000 52 comment=16b3df3f8683c02d9f58035ca8856070bfa76082
prismatic-plumbing-clojure-0.5.4/ 0000755 0000000 0000000 00000000000 13141156451 0016766 5 ustar 00root root 0000000 0000000 prismatic-plumbing-clojure-0.5.4/.gitignore 0000644 0000000 0000000 00000000145 13141156451 0020756 0 ustar 00root root 0000000 0000000 .lein-failures
*~
pom.xml
pom.xml.asc
target/**
.*.swp
.nrepl-port
.repl
out
.lein-repl-history
/doc/ prismatic-plumbing-clojure-0.5.4/CHANGELOG.md 0000644 0000000 0000000 00000011616 13141156451 0020604 0 ustar 00root root 0000000 0000000 ## 0.5.4
* Allow redefining keys in an inner scope, and clarify the semantics.
* Nicer error messages for `safe-get`, `safe-select-keys`, `merge-disjoint`.
## 0.5.3
* **Deprecate** `keywordize-map` in favor of `clojure.walk/keywordize-keys`
* Fix dependent optional bindings (e.g. (fnk [a {b a}])) broken in 0.5.1
* Fnks remember their name, and named fnks can be used without a key in `graph/graph` forms (with an implicit key generated from `(keyword (name f))`).
## 0.5.2
* Fix broken cycle check in Clojurescript topological sort.
## 0.5.1
* (Experimental) include default values as metadata on fnk schemas.
## 0.5.0
* **BREAKING**: Bump to Schema 1.0.1, breaking compatibility with pre-1.0.0 Schema.
## 0.4.4
* Bump to latest Schema version, which should fix AOT compilation when used with Clojure 1.7-RC1 and later.
## 0.4.3
* Actually fix *update* warnings under Clojure 1.7 (commit missed the 0.4.2 release).
## 0.4.2
* Letk now supports simple symbol bindings as well as map destructuring bindings.
* Fix *update* warnings under Clojure 1.7.
## 0.4.1
* Fix concurrency issue recently introduced in distinct-by in Clojure (sequence had to be realized in creator thread due to transient restrictions)
## 0.4.0
* **Breaking** Bump dependencies, potemkin no longer included transitively through schema.
## 0.3.7
* Add support for destructuring namespaced keywords, i.e.
`(= 1 (letk [[a/b] {:a/b 1}] b))` and `(= 1 ((fnk [a/b] b) {:a/b 1}))`
* Fix warnings about `*clojurescript-version*` when compiling ClojureScript
## 0.3.6
* **BREAKING**: Define `update` only if `clojure.core/update` does not exist (ie. legacy clojure(script) versions)
## 0.3.5
* Fix bug in `safe-get` in ClojureScript due to missing `:include-macros true` in plumbing.core
## 0.3.4
* Add `plumbing.map/keyword-map`, `plumbing.core/if-letk`, `plumbing.core/when-letk`
* Bump schema version to 0.3.1, fixing cljs warnings from that project, and move schema.macros calls over to schema.core.
* Minimum required schema version is now 0.3.0
## 0.3.3
* Properly generate cross-platform assertions, fixing ClojureScript errors that tried to throw Java errors.
## 0.3.2
* Fix cljs compilation issue appearing in some circumstances (No such namespace: js)
## 0.3.1
* Fix cljs issue where plumbing.fnk.schema was missing from dependency tree
## 0.3.0
* **BREAKING**: `?>` and `?>>` require a body expression in parens, and take an arbitrary number of body expressions.
* Add ClojureScript support via cljx
* Add plumbing.graph-async namespace to define asynchronous graphs using core.async channels. A core.async dependency has *not* been added to project.clj and must be supplied by user if this namespace is used.
* Add `update` and `mapply` to plumbing.core
## 0.2.2
* Don't depend on a specific Clojure version, and add support for Clojure 1.6.x
## 0.2.1
* Fix for issues with AOT compilation after introducing schema
## 0.2.0
* Replace fnk/graph's internal schema format with `prismatic/schema`. This is a breaking change if (and only if) you've explicitly written old-style fnk/graph schemas like `{:x true :y false}`, or code for manipulating such schemas.
* Drop support for Clojure 1.4.x
## 0.1.1
* Fix bug when aliasing optional values with arg names, i.e. `(let [a 1] ((fnk [{a a}] a) {}))`
* Implement well-defined semantics for optional values that reference other symbols bound within a (let/(de)fnk) form, matching Clojure: symbols are bound in the order given, so that an optional value can reference a symbol bound within the same destructuring form iff that symbol appears earlier in the form.
* Add update-in-when, grouped-map, conk-when, cons-when, rsort-by, as->> to plumbing.core
## 0.1.0
* Minor bugfixes and improved tests
* Perf improvements for `map-keys` and `map-vals` (thanks [bendlas](https://github.com/bendlas)!)
* Pulled out [lazymap](https://bitbucket.org/kotarak/lazymap) as a dependency. `plumbing.lazymap` is no more -- it's now included indirectly as `lazymap.core`. Thanks to Meikel Brandmeyer for a great library, and working with us to extend it to accommodate Graph's use case.
* Lazily compiled graphs are now lazy about checking for required inputs, so a lazily compiled graph fn can be called without inputs not needed for computing the subset of outputs that will be extracted.
* Explicit output-schema metadata on a fnk is taken as gold, rather than being merged with explicit data by analyzing the fnk body, and must be explicit rather than a spec.
* Moved `comp-partial` from pfnk to graph, and added `instance` for fnks/graphs
* Automatic efficient positional forms for fnks that take no rest args.
* A new `eager-compile` that can produce graphs that are almost as fast as hand-coded replacements, by avoiding maps internally where possible using positional fns, and using Records when maps are necessary. The old `eager-compile` is still available as `interpreted-eager-compile`.
## 0.0.1
* Initial release
prismatic-plumbing-clojure-0.5.4/README.md 0000644 0000000 0000000 00000022551 13141156451 0020252 0 ustar 00root root 0000000 0000000 # Plumbing and Graph: the Clojure utility belt
This first release includes our '[Graph](http://plumatic.github.io/prismatics-graph-at-strange-loop)' library, our `plumbing.core` library of very commonly used functions (the only namespace we `:use` across our codebase), and a few other supporting namespaces.
*New in 0.3.0: support for ClojureScript*
*New in 0.2.0: support for schema.core/defn-style schemas on fnks and Graphs. See `(doc fnk)` for details.*
Leiningen dependency (Clojars):
[](http://clojars.org/prismatic/plumbing)
[Latest API docs](http://plumatic.github.io/plumbing).
**This is an alpha release. We are using it internally in production, but the API and organizational structure are subject to change. Comments and suggestions are much appreciated.**
Check back often, because we'll keep adding more useful namespaces and functions as we work through cleaning up and open-sourcing our stack of Clojure libraries.
## Graph: the Functional Swiss-Army Knife
Graph is a simple and *declarative* way to specify a structured computation, which is easy to analyze, change, compose, and monitor. Here's a simple example of an ordinary function definition, and its Graph equivalent:
```clojure
(require '[plumbing.core :refer (sum)])
(defn stats
"Take a map {:xs xs} and return a map of simple statistics on xs"
[{:keys [xs] :as m}]
(assert (contains? m :xs))
(let [n (count xs)
m (/ (sum identity xs) n)
m2 (/ (sum #(* % %) xs) n)
v (- m2 (* m m))]
{:n n ; count
:m m ; mean
:m2 m2 ; mean-square
:v v ; variance
}))
(require '[plumbing.core :refer (fnk sum)])
(def stats-graph
"A graph specifying the same computation as 'stats'"
{:n (fnk [xs] (count xs))
:m (fnk [xs n] (/ (sum identity xs) n))
:m2 (fnk [xs n] (/ (sum #(* % %) xs) n))
:v (fnk [m m2] (- m2 (* m m)))})
```
A Graph is just a map from keywords to keyword functions ([learn more](#fnk)). In this case, `stats-graph` represents the steps in taking a sequence of numbers (`xs`) and producing univariate statistics on those numbers (i.e., the mean `m` and the variance `v`). The names of arguments to each `fnk` can refer to other steps that must happen before the step executes. For instance, in the above, to execute `:v`, you must first execute the `:m` and `:m2` steps (mean and mean-square respectively).
We can "compile" this Graph to produce a single function (equivalent to `stats`), which also checks that the map represents a valid Graph:
```clojure
(require '[plumbing.graph :as graph] '[schema.core :as s])
(def stats-eager (graph/compile stats-graph))
(= {:n 4
:m 3
:m2 (/ 25 2)
:v (/ 7 2)}
(into {} (stats-eager {:xs [1 2 3 6]})))
;; Missing :xs key exception
(thrown? Throwable (stats-eager {:ys [1 2 3]}))
```
Moreover, as of the 0.1.0 release, `stats-eager` is *fast* -- only about 30% slower than the hand-coded `stats` if `xs` has a single element, and within 5% of `stats` if `xs` has ten elements.
Unlike the opaque `stats` fn, however, we can modify and extend `stats-graph` using ordinary operations on maps:
```clojure
(def extended-stats
(graph/compile
(assoc stats-graph
:sd (fnk [^double v] (Math/sqrt v)))))
(= {:n 4
:m 3
:m2 (/ 25 2)
:v (/ 7 2)
:sd (Math/sqrt 3.5)}
(into {} (extended-stats {:xs [1 2 3 6]})))
```
A Graph encodes the structure of a computation, but not how it happens, allowing for many execution strategies. For example, we can compile a Graph lazily so that step values are computed as needed. Or, we can parallel-compile the Graph so that independent step functions are run in separate threads:
```clojure
(def lazy-stats (graph/lazy-compile stats-graph))
(def output (lazy-stats {:xs [1 2 3 6]}))
;; Nothing has actually been computed yet
(= (/ 25 2) (:m2 output))
;; Now :n, :m, and :m2 have been computed, but :v is still behind a delay
(def par-stats (graph/par-compile stats-graph))
(def output (par-stats {:xs [1 2 3 6]}))
;; Nodes are being computed in futures, with :m and :m2 going in parallel after :n
(= (/ 7 2) (:v output))
```
We can also ask a Graph for information about its inputs and outputs (automatically computed from its definition):
```clojure
(require '[plumbing.fnk.pfnk :as pfnk])
;; stats-graph takes a map with one required key, :xs
(= {:xs s/Any}
(pfnk/input-schema stats-graph))
;; stats-graph outputs a map with four keys, :n, :m, :m2, and :v
(= {:n s/Any :m s/Any :m2 s/Any :v s/Any}
(pfnk/output-schema stats-graph))
```
If schemas are provided on the inputs and outputs of the node functions, these propagate through into the Graph schema as expected.
We can also have higher-order functions on Graphs to wrap the behavior on each step. For instance, we can automatically profile each sub-function in 'stats' to see how long it takes to execute:
```clojure
(def profiled-stats (graph/compile (graph/profiled ::profile-data stats-graph)))
;;; times in milliseconds for each step:
(= {:n 1.001, :m 0.728, :m2 0.996, :v 0.069}
@(::profile-data (profiled-stats {:xs (range 10000)})))
```
… and so on. For more examples and details about Graph, check out the [graph examples test](https://github.com/plumatic/plumbing/blob/master/test/plumbing/graph_examples_test.cljx).
## Bring on (de)fnk
Many of the functions we write (in Graph and elsewhere) take a single (nested) map argument with keyword keys and have expectations about which keys must be present and which are optional. We developed a new style of binding ([read more here](https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk)) to make this a lot easier and to check that input data has the right 'shape'. We call these 'keyword functions' (defined by `defnk`) and here's what one looks like:
```clojure
(use 'plumbing.core)
(defnk simple-fnk [a b c]
(+ a b c))
(= 6 (simple-fnk {:a 1 :b 2 :c 3}))
;; Below throws: Key :c not found in (:a :b)
(thrown? Throwable (simple-fnk {:a 1 :b 2}))
```
You can declare a key as optional and provide a default:
```clojure
(defnk simple-opt-fnk [a b {c 1}]
(+ a b c))
(= 4 (simple-opt-fnk {:a 1 :b 2}))
```
You can do nested map bindings:
```clojure
(defnk simple-nested-fnk [a [:b b1] c]
(+ a b1 c))
(= 6 (simple-nested-fnk {:a 1 :b {:b1 2} :c 3}))
;; Below throws: Expected a map at key-path [:b], got type class java.lang.Long
(thrown? Throwable (simple-nested-fnk {:a 1 :b 1 :c 3}))
```
Of course, you can bind multiple variables from an inner map and do multiple levels of nesting:
```clojure
(defnk simple-nested-fnk2 [a [:b b1 [:c {d 3}]]]
(+ a b1 d))
(= 4 (simple-nested-fnk2 {:a 1 :b {:b1 2 :c {:d 1}}}))
(= 5 (simple-nested-fnk2 {:a 1 :b {:b1 1 :c {}}}))
```
You can also use this binding style in a `let` statement using `letk`
or within an anonymous function by using `fnk`.
## More good stuff
There are a bunch of functions in `plumbing.core` that we can't live without. Here are a few of our favorites.
When we build maps, we often use `for-map`, which works like `for` but for maps:
```clojure
(use 'plumbing.core)
(= (for-map [i (range 3)
j (range 3)
:let [s (+ i j)]
:when (< s 3)]
[i j]
s)
{[0 0] 0, [0 1] 1, [0 2] 2, [1 0] 1, [1 1] 2, [2 0] 2})
```
`safe-get` is like `get` but throws when the key doesn't exist:
```clojure
;; IllegalArgumentException Key :c not found in {:a 1, :b 2}
(thrown? Exception (safe-get {:a 1 :b 2} :c))
```
Another frequently used map function is `map-vals`:
```clojure
;; return k -> (f v) for [k, v] in map
(= (map-vals inc {:a 0 :b 0})
{:a 1 :b 1})
```
Ever wanted to conditionally do steps in a `->>` or `->`? Now you can with our
'penguin' operators. Here's a few examples:
```clojure
(use 'plumbing.core)
(= (let [add-b? false]
(-> {:a 1}
(merge {:c 2})
(?> add-b? (assoc :b 2))))
{:a 1 :c 2})
(= (let [inc-all? true]
(->> (range 10)
(filter even?)
(?>> inc-all? (map inc))))
[1 3 5 7 9])
```
Check out [`plumbing.core`](https://github.com/plumatic/plumbing/blob/master/src/plumbing/core.cljx) for many other useful functions.
## ClojureScript
As of 0.3.0, plumbing is available in ClojureScript! The vast majority of the
library supports ClojureScript, with the only exceptions that are JVM-specific
optimizations.
Here's an example usage of `for-map`:
```clojure
(ns plumbing.readme
(:require [plumbing.core :refer-macros [for-map]]))
(defn js-obj->map
"Recursively converts a JavaScript object into a map with keyword keys"
[obj]
(for-map [k (js-keys obj)
:let [v (aget obj k)]]
(keyword k) (if (object? v) (js-obj->map v) v)))
(is (= {:a 1 :b {:x "x" :y "y"}}
(js-obj->map
(js-obj "a" 1
"b" (js-obj "x" "x"
"y" "y")))))
;; Note: this is a contrived example; you would normally use `cljs.core/clj->js`
```
## Community
Plumbing now has a [mailing list](https://groups.google.com/forum/#!forum/prismatic-plumbing). Please feel free to join and ask questions or discuss how you're using Plumbing and Graph.
## Supported Clojure versions
Plumbing is currently supported on Clojure 1.5.x and 1.6.x.
## License
Distributed under the Eclipse Public License, the same as Clojure.
prismatic-plumbing-clojure-0.5.4/bin/ 0000755 0000000 0000000 00000000000 13141156451 0017536 5 ustar 00root root 0000000 0000000 prismatic-plumbing-clojure-0.5.4/bin/push_docs.sh 0000755 0000000 0000000 00000000702 13141156451 0022063 0 ustar 00root root 0000000 0000000 #!/bin/bash
set -e
# Script to generate docs and push to github pages.
# https://github.com/weavejester/codox/wiki/Deploying-to-GitHub-Pages
cd `dirname $0`
git fetch --tags
latestTag=$(git describe --tags `git rev-list --tags --max-count=1`)
git checkout $latestTag
lein doc
cd ../doc
git checkout gh-pages # To be sure you're on the right branch
git add .
git commit -am "new documentation push."
git push -u origin gh-pages
cd ..
git checkout - prismatic-plumbing-clojure-0.5.4/bin/release.sh 0000755 0000000 0000000 00000000201 13141156451 0021506 0 ustar 00root root 0000000 0000000 #!/bin/bash
set -e
# Script to push a release with lein-release and then push docs.
cd `dirname $0`
lein release
./push_docs.sh
prismatic-plumbing-clojure-0.5.4/bin/setup_codox.sh 0000755 0000000 0000000 00000000502 13141156451 0022426 0 ustar 00root root 0000000 0000000 #!/bin/bash
set -e
# One-time script to setup codox deploy to github pages.
# https://github.com/weavejester/codox/wiki/Deploying-to-GitHub-Pages
cd `dirname $0`
cd ..
rm -rf doc && mkdir doc
git clone git@github.com:plumatic/plumbing.git doc
cd doc
git symbolic-ref HEAD refs/heads/gh-pages
rm .git/index
git clean -fdx prismatic-plumbing-clojure-0.5.4/project.clj 0000644 0000000 0000000 00000007052 13141156451 0021132 0 ustar 00root root 0000000 0000000 (defproject prismatic/plumbing "0.5.4"
:description "Prismatic's Clojure utility belt."
:url "https://github.com/plumatic/plumbing"
:license {:name "Eclipse Public License - v 1.0"
:url "http://www.eclipse.org/legal/epl-v10.html"
:distribution :repo}
:dependencies [[prismatic/schema "1.0.1"]
[de.kotka/lazymap "3.1.0" :exclusions [org.clojure/clojure]]]
:profiles {:dev {:dependencies [[org.clojure/clojure "1.6.0"]
[org.clojure/clojurescript "0.0-2665"]
[org.clojure/core.async "0.1.346.0-17112a-alpha"]]
:plugins [[com.keminglabs/cljx "0.6.0" :exclusions [org.clojure/clojure]]
[codox "0.8.8"]
[lein-cljsbuild "1.0.5"]
[com.cemerick/clojurescript.test "0.3.1"]]
:cljx {:builds [{:source-paths ["src"]
:output-path "target/generated/src/clj"
:rules :clj}
{:source-paths ["src"]
:output-path "target/generated/src/cljs"
:rules :cljs}
{:source-paths ["test"]
:output-path "target/generated/test/clj"
:rules :clj}
{:source-paths ["test"]
:output-path "target/generated/test/cljs"
:rules :cljs}]}}
:1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]}
:1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]}
:1.8 {:dependencies [[org.clojure/clojure "1.8.0-RC1"]]}}
:jar-exclusions [#"\.cljx"]
:aliases {"all" ["with-profile" "dev:dev,1.5:dev,1.7;dev,1.8"]
"deploy" ["do" "clean," "cljx" "once," "deploy" "clojars"]
"test" ["do" "clean," "cljx" "once," "test," "with-profile" "dev" "cljsbuild" "test"]}
:lein-release {:deploy-via :shell
:shell ["lein" "deploy"]}
:auto-clean false
:source-paths ["target/generated/src/clj" "src"]
:resource-paths ["target/generated/src/cljs"]
:test-paths ["target/generated/test/clj" "test"]
:cljsbuild {:test-commands {"unit" ["phantomjs" :runner
"this.literal_js_was_evaluated=true"
"target/unit-test.js"]}
:builds
{:dev {:source-paths ["src"
"target/generated/src/clj"
"target/generated/src/cljs"]
:compiler {:output-to "target/main.js"
:optimizations :whitespace
:pretty-print true}}
:test {:source-paths ["src"
"target/generated/src/clj"
"target/generated/src/cljs"
"target/generated/test/clj"
"target/generated/test/cljs"]
:compiler {:output-to "target/unit-test.js"
:optimizations :whitespace
:pretty-print true}}}}
:codox {:src-uri-mapping {#"target/generated/src/clj" #(str "src/" % "x")}
:src-dir-uri "http://github.com/plumatic/plumbing/blob/master/"
:src-linenum-anchor-prefix "L"}
:jvm-opts ^:replace [])
prismatic-plumbing-clojure-0.5.4/src/ 0000755 0000000 0000000 00000000000 13141156451 0017555 5 ustar 00root root 0000000 0000000 prismatic-plumbing-clojure-0.5.4/src/plumbing/ 0000755 0000000 0000000 00000000000 13141156451 0021372 5 ustar 00root root 0000000 0000000 prismatic-plumbing-clojure-0.5.4/src/plumbing/core.clj 0000644 0000000 0000000 00000036407 13141156451 0023026 0 ustar 00root root 0000000 0000000 (ns plumbing.core
"Utility belt for Clojure in the wild"
(:refer-clojure :exclude [update])
(:require
[schema.utils :as schema-utils]
[schema.macros :as schema-macros]
[plumbing.fnk.schema :as schema :include-macros true]
[plumbing.fnk.impl :as fnk-impl]))
(set! *warn-on-reflection* true)
(def ^:private +none+
"A sentinel value representing missing portions of the input data."
::missing)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Maps
(defmacro for-map
"Like 'for' for building maps. Same bindings except the body should have a
key-expression and value-expression. If a key is repeated, the last
value (according to \"for\" semantics) will be retained.
(= (for-map [i (range 2) j (range 2)] [i j] (even? (+ i j)))
{[0 0] true, [0 1] false, [1 0] false, [1 1] true})
An optional symbol can be passed as a first argument, which will be
bound to the transient map containing the entries produced so far."
([seq-exprs key-expr val-expr]
`(for-map ~(gensym "m") ~seq-exprs ~key-expr ~val-expr))
([m-sym seq-exprs key-expr val-expr]
`(let [m-atom# (atom (transient {}))]
(doseq ~seq-exprs
(let [~m-sym @m-atom#]
(reset! m-atom# (assoc! ~m-sym ~key-expr ~val-expr))))
(persistent! @m-atom#))))
(defmacro -unless-update
"Execute and yield body only if Clojure version preceeds introduction
of 'update' into core namespace."
[body]
`(schema-macros/if-cljs
~body
~(when (pos? (compare
[1 7 0]
(mapv #(get *clojure-version* %)
[:major :minor :incremental])))
body)))
(-unless-update
(defn update
"Updates the value in map m at k with the function f.
Like update-in, but for updating a single top-level key.
Any additional args will be passed to f after the value.
WARNING As of Clojure 1.7 this function exists in clojure.core and
will not be exported by this namespace."
([m k f] (assoc m k (f (get m k))))
([m k f x1] (assoc m k (f (get m k) x1)))
([m k f x1 x2] (assoc m k (f (get m k) x1 x2)))
([m k f x1 x2 & xs] (assoc m k (apply f (get m k) x1 x2 xs)))))
(defn map-vals
"Build map k -> (f v) for [k v] in map, preserving the initial type"
[f m]
(cond
(sorted? m)
(reduce-kv (fn [out-m k v] (assoc out-m k (f v))) (sorted-map) m)
(map? m)
(persistent! (reduce-kv (fn [out-m k v] (assoc! out-m k (f v))) (transient {}) m))
:else
(for-map [[k v] m] k (f v))))
(defn map-keys
"Build map (f k) -> v for [k v] in map m"
[f m]
(if (map? m)
(persistent! (reduce-kv (fn [out-m k v] (assoc! out-m (f k) v)) (transient {}) m))
(for-map [[k v] m] (f k) v)))
(defn map-from-keys
"Build map k -> (f k) for keys in ks"
[f ks]
(for-map [k ks] k (f k)))
(defn map-from-vals
"Build map (f v) -> v for vals in vs"
[f vs]
(for-map [v vs] (f v) v))
(defn dissoc-in
"Dissociate this keyseq from m, removing any empty maps created as a result
(including at the top-level)."
[m [k & ks]]
(when m
(if-let [res (and ks (dissoc-in (get m k) ks))]
(assoc m k res)
(let [res (dissoc m k)]
(when-not (empty? res)
res)))))
(defn ^:deprecated keywordize-map
"DEPRECATED. prefer clojure.walk/keywordize-keys.
Recursively convert maps in m (including itself)
to have keyword keys instead of string"
[x]
(cond
(map? x)
(for-map [[k v] x]
(if (string? k) (keyword k) k) (keywordize-map v))
(seq? x)
(map keywordize-map x)
(vector? x)
(mapv keywordize-map x)
:else
x))
(defmacro lazy-get
"Like get but lazy about default"
[m k d]
`(if-let [pair# (find ~m ~k)]
(val pair#)
~d))
(defn safe-get
"Like get but throw an exception if not found"
[m k]
(lazy-get
m k
(schema/assert-iae false "Key %s not found in %s" k
(binding [*print-length* 200]
(print-str (mapv key m))))))
(defn safe-get-in
"Like get-in but throws exception if not found"
[m ks]
(if (seq ks)
(recur (safe-get m (first ks)) (next ks))
m))
(defn assoc-when
"Like assoc but only assocs when value is truthy"
[m & kvs]
(assert (even? (count kvs)))
(into (or m {})
(for [[k v] (partition 2 kvs)
:when v]
[k v])))
(defn update-in-when
"Like update-in but returns m unchanged if key-seq is not present."
[m key-seq f & args]
(let [found (get-in m key-seq +none+)]
(if-not (identical? +none+ found)
(assoc-in m key-seq (apply f found args))
m)))
(defn grouped-map
"Like group-by, but accepts a map-fn that is applied to values before
collected."
[key-fn map-fn coll]
(persistent!
(reduce
(fn [ret x]
(let [k (key-fn x)]
(assoc! ret k (conj (get ret k []) (map-fn x)))))
(transient {}) coll)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Seqs
(defn aconcat
"Like (apply concat s) but lazier (and shorter) "
[s]
(lazy-cat (first s) (when-let [n (next s)] (aconcat n))))
(defn unchunk
"Takes a seqable and returns a lazy sequence that
is maximally lazy and doesn't realize elements due to either
chunking or apply.
Useful when you don't want chunking, for instance,
(first awesome-website? (map slurp +a-bunch-of-urls+))
may slurp up to 31 unneed webpages, wherease
(first awesome-website? (map slurp (unchunk +a-bunch-of-urls+)))
is guaranteed to stop slurping after the first awesome website.
Taken from http://stackoverflow.com/questions/3407876/how-do-i-avoid-clojures-chunking-behavior-for-lazy-seqs-that-i-want-to-short-ci"
[s]
(when (seq s)
(cons (first s)
(lazy-seq (unchunk (rest s))))))
(defn sum
"Return sum of (f x) for each x in xs"
([f xs] (reduce + (map f xs)))
([xs] (reduce + xs)))
(defn singleton
"returns (first xs) when xs has only 1 element"
[xs]
(when-let [xs (seq xs)]
(when-not (next xs)
(first xs))))
(defn indexed
"Returns [idx x] for x in seqable s"
[s]
(map-indexed vector s))
(defn positions
"Returns indices idx of sequence s where (f (nth s idx))"
[f s]
(keep-indexed (fn [i x] (when (f x) i)) s))
(defn frequencies-fast
"Like clojure.core/frequencies, but faster.
Uses Java's equal/hash, so may produce incorrect results if
given values that are = but not .equal"
[xs]
(let [res (java.util.HashMap.)]
(doseq [x xs]
(.put res x (unchecked-inc (int (or (.get res x) 0)))))
(into {} res)))
(defn distinct-fast
"Like clojure.core/distinct, but faster.
Uses Java's equal/hash, so may produce incorrect results if
given values that are = but not .equal"
[xs]
(let [s (java.util.HashSet.)]
(filter #(when-not (.contains s %) (.add s %) true) xs)))
(defn distinct-by
"Returns elements of xs which return unique
values according to f. If multiple elements of xs return the same
value under f, the first is returned"
[f xs]
(let [s (atom #{})]
(for [x xs
:let [id (f x)]
:when (not (contains? @s id))]
(do (swap! s conj id)
x))))
(defn distinct-id
"Like distinct but uses reference rather than value identity, very clojurey"
[xs]
(let [s (java.util.IdentityHashMap.)]
(doseq [x xs]
(.put s x true))
(iterator-seq (.iterator (.keySet s)))))
(defn interleave-all
"Analogy: partition:partition-all :: interleave:interleave-all"
[& colls]
(lazy-seq
((fn helper [seqs]
(when (seq seqs)
(concat (map first seqs)
(lazy-seq (helper (keep next seqs))))))
(keep seq colls))))
(defn count-when
"Returns # of elements of xs where pred holds"
[pred xs]
(count (filter pred xs)))
(defn conj-when
"Like conj but ignores non-truthy values"
([coll x] (if x (conj coll x) coll))
([coll x & xs]
(if xs
(recur (conj-when coll x)
(first xs)
(next xs))
(conj-when coll x))))
(defn cons-when
"Like cons but does nothing if x is non-truthy."
[x s]
(if x (cons x s) s))
(def rsort-by
"Like sort-by, but prefers higher values rather than lower ones."
(comp reverse sort-by))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Control flow
(defmacro ?>>
"Conditional double-arrow operation (->> nums (?>> inc-all? (map inc)))"
[do-it? & args]
`(if ~do-it?
(->> ~(last args) ~@(butlast args))
~(last args)))
(defmacro ?>
"Conditional single-arrow operation (-> m (?> add-kv? (assoc :k :v)))"
[arg do-it? & rest]
`(if ~do-it?
(-> ~arg ~@rest)
~arg))
(defmacro fn->
"Equivalent to `(fn [x] (-> x ~@body))"
[& body]
`(fn [x#] (-> x# ~@body)))
(defmacro fn->>
"Equivalent to `(fn [x] (->> x ~@body))"
[& body]
`(fn [x#] (->> x# ~@body)))
(defmacro <-
"Converts a ->> to a ->
(->> (range 10) (map inc) (<- (doto prn)) (reduce +))
Jason W01fe is happy to give a talk anywhere any time on
the calculus of arrow macros"
[& body]
`(-> ~(last body) ~@(butlast body)))
(defmacro as->>
"Like as->, but can be used in double arrow."
[name & forms-and-expr]
`(as-> ~(last forms-and-expr) ~name ~@(butlast forms-and-expr)))
(defmacro memoized-fn
"Like fn, but memoized (including recursive calls).
The clojure.core memoize correctly caches recursive calls when you do a top-level def
of your memoized function, but if you want an anonymous fibonacci function, you must use
memoized-fn rather than memoize to cache the recursive calls."
[name args & body]
`(let [a# (atom {})]
(fn ~name ~args
(let [m# @a#
args# ~args]
(if-let [[_# v#] (find m# args#)]
v#
(let [v# (do ~@body)]
(swap! a# assoc args# v#)
v#))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Miscellaneous
(defn swap-pair!
"Like swap! but returns a pair [old-val new-val]"
([a f]
(loop []
(let [old-val @a
new-val (f old-val)]
(if (compare-and-set! a old-val new-val)
[old-val new-val]
(recur)))))
([a f & args]
(swap-pair! a #(apply f % args))))
(defn get-and-set!
"Like reset! but returns old-val"
[a new-val]
(first (swap-pair! a (constantly new-val))))
(defn millis ^long []
(System/currentTimeMillis)
)
(defn mapply
"Like apply, but applies a map to a function with positional map
arguments. Can take optional initial args just like apply."
([f m] (apply f (apply concat m)))
([f arg & args] (apply f arg (concat (butlast args) (apply concat (last args))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; fnk
(defmacro letk
"Keyword let. Accepts an interleaved sequence of binding forms and map forms like:
(letk [[a {b 2} [:f g h] c d {e 4} :as m & more] a-map ...] & body)
a, c, d, and f are required keywords, and letk will barf if not in a-map.
b and e are optional, and will be bound to default values if not present.
g and h are required keys in the map found under :f.
m will be bound to the entire map (a-map).
more will be bound to all the unbound keys (ie (dissoc a-map :a :b :c :d :e)).
:as and & are both optional, but must be at the end in the specified order if present.
The same symbol cannot be bound multiple times within the same destructing level.
Optional values can reference symbols bound earlier within the same binding, i.e.,
(= [2 2] (let [a 1] (letk [[a {b a}] {:a 2}] [a b]))) but
(= [2 1] (let [a 1] (letk [[{b a} a] {:a 2}] [a b])))
If present, :as and :& symbols are bound before other symbols within the binding.
Namespaced keys are supported by specifying fully-qualified key in binding form. The bound
symbol uses the _name_ portion of the namespaced key, i.e,
(= 1 (letk [[a/b] {:a/b 1}] b)).
Map destructuring bindings can be mixed with ordinary symbol bindings."
[bindings & body]
(schema/assert-iae (vector? bindings) "Letk binding must be a vector")
(schema/assert-iae (even? (count bindings)) "Letk binding must have even number of elements")
(reduce
(fn [cur-body-form [bind-form value-form]]
(if (symbol? bind-form)
`(let [~bind-form ~value-form] ~cur-body-form)
(let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form
&env
(fnk-impl/ensure-schema-metadata &env bind-form)
[]
cur-body-form)]
`(let [~map-sym ~value-form] ~body-form))))
`(do ~@body)
(reverse (partition 2 bindings))))
(defmacro if-letk
"bindings => binding-form test
If test is true, evaluates then with binding-form bound to the value of
test, if not, yields else"
([bindings then]
`(if-letk ~bindings ~then nil))
([bindings then else]
(assert (vector? bindings) "if-letk requires a vector for its binding")
(assert (= 2 (count bindings)) "if-letk requires exactly 2 forms in binding vector")
(let [form (bindings 0) tst (bindings 1)]
`(let [temp# ~tst]
(if temp#
(letk [~form temp#]
~then)
~else)))))
(defmacro when-letk
"bindings => binding-form test
When test is true, evaluates body with binding-form bound to the value of test"
[bindings & body]
`(if-letk ~bindings (do ~@body)))
(defmacro fnk
"Keyword fn, using letk. Generates a prismatic/schema schematized fn that
accepts a single explicit map i.e., (f {:foo :bar}).
Explicit top-level map structure will be recorded in output spec, or
to capture implicit structure use an explicit prismatic/schema hint on the
function name.
Individual inputs can also be schematized by putting :- schemas after the
binding symbol. Schemas can also be used on & more symbols to describe
additional map inputs, or on entire [] bindings to override the automatically
generated schema for the contents (caveat emptor).
By default, input schemas allow for arbitrary additional mappings
({s/Keyword s/Any}) unless explicit binding or & more schemas are provided."
[& args]
(let [[name? more-args] (if (symbol? (first args))
(schema-macros/extract-arrow-schematized-element &env args)
[nil args])
[bind body] (schema-macros/extract-arrow-schematized-element &env more-args)]
(fnk-impl/fnk-form &env name? bind body &form)))
(defmacro defnk
"Analogy: fn:fnk :: defn::defnk"
[& defnk-args]
(let [[name args] (schema-macros/extract-arrow-schematized-element &env defnk-args)
take-if (fn [p s] (if (p (first s)) [(first s) (next s)] [nil s]))
[docstring? args] (take-if string? args)
[attr-map? args] (take-if map? args)
[bind body] (schema-macros/extract-arrow-schematized-element &env args)]
(schema/assert-iae (symbol? name) "Name for defnk is not a symbol: %s" name)
(let [f (fnk-impl/fnk-form &env name bind body &form)]
`(def ~(with-meta name (merge (meta name) (assoc-when (or attr-map? {}) :doc docstring?)))
~f))))
(set! *warn-on-reflection* false)
;;;;;;;;;;;; This file autogenerated from src/plumbing/core.cljx
prismatic-plumbing-clojure-0.5.4/src/plumbing/core.cljx 0000644 0000000 0000000 00000036305 13141156451 0023213 0 ustar 00root root 0000000 0000000 (ns plumbing.core
"Utility belt for Clojure in the wild"
(:refer-clojure :exclude [update])
#+cljs
(:require-macros
[plumbing.core :refer [for-map lazy-get -unless-update]]
[schema.macros :as schema-macros])
(:require
[schema.utils :as schema-utils]
#+clj [schema.macros :as schema-macros]
[plumbing.fnk.schema :as schema :include-macros true]
#+clj [plumbing.fnk.impl :as fnk-impl]))
#+clj (set! *warn-on-reflection* true)
(def ^:private +none+
"A sentinel value representing missing portions of the input data."
::missing)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Maps
(defmacro for-map
"Like 'for' for building maps. Same bindings except the body should have a
key-expression and value-expression. If a key is repeated, the last
value (according to \"for\" semantics) will be retained.
(= (for-map [i (range 2) j (range 2)] [i j] (even? (+ i j)))
{[0 0] true, [0 1] false, [1 0] false, [1 1] true})
An optional symbol can be passed as a first argument, which will be
bound to the transient map containing the entries produced so far."
([seq-exprs key-expr val-expr]
`(for-map ~(gensym "m") ~seq-exprs ~key-expr ~val-expr))
([m-sym seq-exprs key-expr val-expr]
`(let [m-atom# (atom (transient {}))]
(doseq ~seq-exprs
(let [~m-sym @m-atom#]
(reset! m-atom# (assoc! ~m-sym ~key-expr ~val-expr))))
(persistent! @m-atom#))))
(defmacro -unless-update
"Execute and yield body only if Clojure version preceeds introduction
of 'update' into core namespace."
[body]
`(schema-macros/if-cljs
~body
~(when (pos? (compare
[1 7 0]
(mapv #(get *clojure-version* %)
[:major :minor :incremental])))
body)))
(-unless-update
(defn update
"Updates the value in map m at k with the function f.
Like update-in, but for updating a single top-level key.
Any additional args will be passed to f after the value.
WARNING As of Clojure 1.7 this function exists in clojure.core and
will not be exported by this namespace."
([m k f] (assoc m k (f (get m k))))
([m k f x1] (assoc m k (f (get m k) x1)))
([m k f x1 x2] (assoc m k (f (get m k) x1 x2)))
([m k f x1 x2 & xs] (assoc m k (apply f (get m k) x1 x2 xs)))))
(defn map-vals
"Build map k -> (f v) for [k v] in map, preserving the initial type"
[f m]
(cond
(sorted? m)
(reduce-kv (fn [out-m k v] (assoc out-m k (f v))) (sorted-map) m)
(map? m)
(persistent! (reduce-kv (fn [out-m k v] (assoc! out-m k (f v))) (transient {}) m))
:else
(for-map [[k v] m] k (f v))))
(defn map-keys
"Build map (f k) -> v for [k v] in map m"
[f m]
(if (map? m)
(persistent! (reduce-kv (fn [out-m k v] (assoc! out-m (f k) v)) (transient {}) m))
(for-map [[k v] m] (f k) v)))
(defn map-from-keys
"Build map k -> (f k) for keys in ks"
[f ks]
(for-map [k ks] k (f k)))
(defn map-from-vals
"Build map (f v) -> v for vals in vs"
[f vs]
(for-map [v vs] (f v) v))
(defn dissoc-in
"Dissociate this keyseq from m, removing any empty maps created as a result
(including at the top-level)."
[m [k & ks]]
(when m
(if-let [res (and ks (dissoc-in (get m k) ks))]
(assoc m k res)
(let [res (dissoc m k)]
(when-not (empty? res)
res)))))
(defn ^:deprecated keywordize-map
"DEPRECATED. prefer clojure.walk/keywordize-keys.
Recursively convert maps in m (including itself)
to have keyword keys instead of string"
[x]
(cond
(map? x)
(for-map [[k v] x]
(if (string? k) (keyword k) k) (keywordize-map v))
(seq? x)
(map keywordize-map x)
(vector? x)
(mapv keywordize-map x)
:else
x))
(defmacro lazy-get
"Like get but lazy about default"
[m k d]
`(if-let [pair# (find ~m ~k)]
(val pair#)
~d))
(defn safe-get
"Like get but throw an exception if not found"
[m k]
(lazy-get
m k
(schema/assert-iae false "Key %s not found in %s" k
(binding [*print-length* 200]
(print-str (mapv key m))))))
(defn safe-get-in
"Like get-in but throws exception if not found"
[m ks]
(if (seq ks)
(recur (safe-get m (first ks)) (next ks))
m))
(defn assoc-when
"Like assoc but only assocs when value is truthy"
[m & kvs]
(assert (even? (count kvs)))
(into (or m {})
(for [[k v] (partition 2 kvs)
:when v]
[k v])))
(defn update-in-when
"Like update-in but returns m unchanged if key-seq is not present."
[m key-seq f & args]
(let [found (get-in m key-seq +none+)]
(if-not (identical? +none+ found)
(assoc-in m key-seq (apply f found args))
m)))
(defn grouped-map
"Like group-by, but accepts a map-fn that is applied to values before
collected."
[key-fn map-fn coll]
(persistent!
(reduce
(fn [ret x]
(let [k (key-fn x)]
(assoc! ret k (conj (get ret k []) (map-fn x)))))
(transient {}) coll)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Seqs
(defn aconcat
"Like (apply concat s) but lazier (and shorter) "
[s]
(lazy-cat (first s) (when-let [n (next s)] (aconcat n))))
(defn unchunk
"Takes a seqable and returns a lazy sequence that
is maximally lazy and doesn't realize elements due to either
chunking or apply.
Useful when you don't want chunking, for instance,
(first awesome-website? (map slurp +a-bunch-of-urls+))
may slurp up to 31 unneed webpages, wherease
(first awesome-website? (map slurp (unchunk +a-bunch-of-urls+)))
is guaranteed to stop slurping after the first awesome website.
Taken from http://stackoverflow.com/questions/3407876/how-do-i-avoid-clojures-chunking-behavior-for-lazy-seqs-that-i-want-to-short-ci"
[s]
(when (seq s)
(cons (first s)
(lazy-seq (unchunk (rest s))))))
(defn sum
"Return sum of (f x) for each x in xs"
([f xs] (reduce + (map f xs)))
([xs] (reduce + xs)))
(defn singleton
"returns (first xs) when xs has only 1 element"
[xs]
(when-let [xs (seq xs)]
(when-not (next xs)
(first xs))))
(defn indexed
"Returns [idx x] for x in seqable s"
[s]
(map-indexed vector s))
(defn positions
"Returns indices idx of sequence s where (f (nth s idx))"
[f s]
(keep-indexed (fn [i x] (when (f x) i)) s))
#+clj
(defn frequencies-fast
"Like clojure.core/frequencies, but faster.
Uses Java's equal/hash, so may produce incorrect results if
given values that are = but not .equal"
[xs]
(let [res (java.util.HashMap.)]
(doseq [x xs]
(.put res x (unchecked-inc (int (or (.get res x) 0)))))
(into {} res)))
#+clj
(defn distinct-fast
"Like clojure.core/distinct, but faster.
Uses Java's equal/hash, so may produce incorrect results if
given values that are = but not .equal"
[xs]
(let [s (java.util.HashSet.)]
(filter #(when-not (.contains s %) (.add s %) true) xs)))
(defn distinct-by
"Returns elements of xs which return unique
values according to f. If multiple elements of xs return the same
value under f, the first is returned"
[f xs]
(let [s (atom #{})]
(for [x xs
:let [id (f x)]
:when (not (contains? @s id))]
(do (swap! s conj id)
x))))
#+clj
(defn distinct-id
"Like distinct but uses reference rather than value identity, very clojurey"
[xs]
(let [s (java.util.IdentityHashMap.)]
(doseq [x xs]
(.put s x true))
(iterator-seq (.iterator (.keySet s)))))
(defn interleave-all
"Analogy: partition:partition-all :: interleave:interleave-all"
[& colls]
(lazy-seq
((fn helper [seqs]
(when (seq seqs)
(concat (map first seqs)
(lazy-seq (helper (keep next seqs))))))
(keep seq colls))))
(defn count-when
"Returns # of elements of xs where pred holds"
[pred xs]
(count (filter pred xs)))
(defn conj-when
"Like conj but ignores non-truthy values"
([coll x] (if x (conj coll x) coll))
([coll x & xs]
(if xs
(recur (conj-when coll x)
(first xs)
(next xs))
(conj-when coll x))))
(defn cons-when
"Like cons but does nothing if x is non-truthy."
[x s]
(if x (cons x s) s))
(def rsort-by
"Like sort-by, but prefers higher values rather than lower ones."
(comp reverse sort-by))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Control flow
(defmacro ?>>
"Conditional double-arrow operation (->> nums (?>> inc-all? (map inc)))"
[do-it? & args]
`(if ~do-it?
(->> ~(last args) ~@(butlast args))
~(last args)))
(defmacro ?>
"Conditional single-arrow operation (-> m (?> add-kv? (assoc :k :v)))"
[arg do-it? & rest]
`(if ~do-it?
(-> ~arg ~@rest)
~arg))
(defmacro fn->
"Equivalent to `(fn [x] (-> x ~@body))"
[& body]
`(fn [x#] (-> x# ~@body)))
(defmacro fn->>
"Equivalent to `(fn [x] (->> x ~@body))"
[& body]
`(fn [x#] (->> x# ~@body)))
(defmacro <-
"Converts a ->> to a ->
(->> (range 10) (map inc) (<- (doto prn)) (reduce +))
Jason W01fe is happy to give a talk anywhere any time on
the calculus of arrow macros"
[& body]
`(-> ~(last body) ~@(butlast body)))
(defmacro as->>
"Like as->, but can be used in double arrow."
[name & forms-and-expr]
`(as-> ~(last forms-and-expr) ~name ~@(butlast forms-and-expr)))
(defmacro memoized-fn
"Like fn, but memoized (including recursive calls).
The clojure.core memoize correctly caches recursive calls when you do a top-level def
of your memoized function, but if you want an anonymous fibonacci function, you must use
memoized-fn rather than memoize to cache the recursive calls."
[name args & body]
`(let [a# (atom {})]
(fn ~name ~args
(let [m# @a#
args# ~args]
(if-let [[_# v#] (find m# args#)]
v#
(let [v# (do ~@body)]
(swap! a# assoc args# v#)
v#))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Miscellaneous
(defn swap-pair!
"Like swap! but returns a pair [old-val new-val]"
([a f]
(loop []
(let [old-val @a
new-val (f old-val)]
(if (compare-and-set! a old-val new-val)
[old-val new-val]
(recur)))))
([a f & args]
(swap-pair! a #(apply f % args))))
(defn get-and-set!
"Like reset! but returns old-val"
[a new-val]
(first (swap-pair! a (constantly new-val))))
(defn millis ^long []
#+clj (System/currentTimeMillis)
#+cljs (.getTime (js/Date.)))
(defn mapply
"Like apply, but applies a map to a function with positional map
arguments. Can take optional initial args just like apply."
([f m] (apply f (apply concat m)))
([f arg & args] (apply f arg (concat (butlast args) (apply concat (last args))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; fnk
(defmacro letk
"Keyword let. Accepts an interleaved sequence of binding forms and map forms like:
(letk [[a {b 2} [:f g h] c d {e 4} :as m & more] a-map ...] & body)
a, c, d, and f are required keywords, and letk will barf if not in a-map.
b and e are optional, and will be bound to default values if not present.
g and h are required keys in the map found under :f.
m will be bound to the entire map (a-map).
more will be bound to all the unbound keys (ie (dissoc a-map :a :b :c :d :e)).
:as and & are both optional, but must be at the end in the specified order if present.
The same symbol cannot be bound multiple times within the same destructing level.
Optional values can reference symbols bound earlier within the same binding, i.e.,
(= [2 2] (let [a 1] (letk [[a {b a}] {:a 2}] [a b]))) but
(= [2 1] (let [a 1] (letk [[{b a} a] {:a 2}] [a b])))
If present, :as and :& symbols are bound before other symbols within the binding.
Namespaced keys are supported by specifying fully-qualified key in binding form. The bound
symbol uses the _name_ portion of the namespaced key, i.e,
(= 1 (letk [[a/b] {:a/b 1}] b)).
Map destructuring bindings can be mixed with ordinary symbol bindings."
[bindings & body]
(schema/assert-iae (vector? bindings) "Letk binding must be a vector")
(schema/assert-iae (even? (count bindings)) "Letk binding must have even number of elements")
(reduce
(fn [cur-body-form [bind-form value-form]]
(if (symbol? bind-form)
`(let [~bind-form ~value-form] ~cur-body-form)
(let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form
&env
(fnk-impl/ensure-schema-metadata &env bind-form)
[]
cur-body-form)]
`(let [~map-sym ~value-form] ~body-form))))
`(do ~@body)
(reverse (partition 2 bindings))))
(defmacro if-letk
"bindings => binding-form test
If test is true, evaluates then with binding-form bound to the value of
test, if not, yields else"
([bindings then]
`(if-letk ~bindings ~then nil))
([bindings then else]
(assert (vector? bindings) "if-letk requires a vector for its binding")
(assert (= 2 (count bindings)) "if-letk requires exactly 2 forms in binding vector")
(let [form (bindings 0) tst (bindings 1)]
`(let [temp# ~tst]
(if temp#
(letk [~form temp#]
~then)
~else)))))
(defmacro when-letk
"bindings => binding-form test
When test is true, evaluates body with binding-form bound to the value of test"
[bindings & body]
`(if-letk ~bindings (do ~@body)))
(defmacro fnk
"Keyword fn, using letk. Generates a prismatic/schema schematized fn that
accepts a single explicit map i.e., (f {:foo :bar}).
Explicit top-level map structure will be recorded in output spec, or
to capture implicit structure use an explicit prismatic/schema hint on the
function name.
Individual inputs can also be schematized by putting :- schemas after the
binding symbol. Schemas can also be used on & more symbols to describe
additional map inputs, or on entire [] bindings to override the automatically
generated schema for the contents (caveat emptor).
By default, input schemas allow for arbitrary additional mappings
({s/Keyword s/Any}) unless explicit binding or & more schemas are provided."
[& args]
(let [[name? more-args] (if (symbol? (first args))
(schema-macros/extract-arrow-schematized-element &env args)
[nil args])
[bind body] (schema-macros/extract-arrow-schematized-element &env more-args)]
(fnk-impl/fnk-form &env name? bind body &form)))
(defmacro defnk
"Analogy: fn:fnk :: defn::defnk"
[& defnk-args]
(let [[name args] (schema-macros/extract-arrow-schematized-element &env defnk-args)
take-if (fn [p s] (if (p (first s)) [(first s) (next s)] [nil s]))
[docstring? args] (take-if string? args)
[attr-map? args] (take-if map? args)
[bind body] (schema-macros/extract-arrow-schematized-element &env args)]
(schema/assert-iae (symbol? name) "Name for defnk is not a symbol: %s" name)
(let [f (fnk-impl/fnk-form &env name bind body &form)]
`(def ~(with-meta name (merge (meta name) (assoc-when (or attr-map? {}) :doc docstring?)))
~f))))
#+clj (set! *warn-on-reflection* false)
prismatic-plumbing-clojure-0.5.4/src/plumbing/fnk/ 0000755 0000000 0000000 00000000000 13141156451 0022150 5 ustar 00root root 0000000 0000000 prismatic-plumbing-clojure-0.5.4/src/plumbing/fnk/README.md 0000644 0000000 0000000 00000022351 13141156451 0023432 0 ustar 00root root 0000000 0000000 ## Motivation
As part of our first open source release, we're contemplating introducing `fnk` and `defnk` macros with different destructuring syntax than the rest of Clojure.
Below, we've collected some background on the rational behind introducing `fnk`, together with a proposed syntax and several alternatives. Any and all input on these ideas would be much appreciated.
For more documentation and examples of graph and fnk, we encourage checking out `test/plumbing/fnk/fnk_examples_test.clj` and `test/plumbing/graph_examples_test.clj`.
### Background
We're very excited to begin sharing the Clojure infrastructure that powers Prismatic. Our goals for 2013 include releasing open-source libraries for storage, machine learning, deployment, production services, and more for the Clojure community to (hopefully) use, contribute to, and build upon.
Our first release is plumbing.[Graph], a library for declaratively specifying the composition structure of complex functions, along with other portions of our low-level "plumbing" library that support it.
```clojure
{:n (fnk [xs] (count xs))
:m (fnk [xs n] (/ (sum xs) n))
:m2 (fnk [xs n] (/ (sum #(* % %) xs) n))
:v (fnk [m m2] (- m2 (* m m)))}
```
This example shows a simple Graph that expresses the computation of univariate statistics of a sequence of input numbers `xs` in four steps. Dependencies between steps are expressed by argument and keyword names (e.g., the variance `v` is computed from the mean `m` and mean-square `m2`). The details of Graph are not vital for this discussion (see the [blog post](Graph) if you're interested), except for the following two high-level constraints on the implementation of `fnk`:
1. To express dependency structure simply (without repeating ourselves), we must be be able to interrogate a `fnk` to ask for the *names* of its arguments.
2. The inputs, outputs, and intermediate values of a Graph are (nested) maps with keyword keys. Thus, the arguments to a `fnk` are equivalent to keyword destructuring.
We cannot simply implement `(fnk [xs n] …)` with `(fn [{:keys [xs n]}])` for two reasons. First, arglist metadata is not supported by Clojure's current function-defining macros (`defn` puts it on the var, but neither `defn` nor `fn` puts it on the fn itself). Second, while Clojure does offer excellent destructuring support (including for maps) out of the box, it turns out to be somewhat verbose for the cases commonly encountered in Graph.
Thus, we are exploring the definition of a new family keyword functions (`fnk` and `defnk`) that use a new destructuring syntax focused around (nested) maps with keyword keys, and also provide explicit metadata about a function's input and output *schemata*.
Our `fnk` experiment has been running internally for more than a year now, and we've found `fnk` to be quite useful for not only for defining Graphs, but also for many other situations involving maps with keyword keys. Across our current codebase, about 5% of function definitions use a variant of `fnk` over `fn`.
## Fnk syntax
### (Why not) Clojure's destructuring syntax?
While Clojure's built-in destructuring is generally great, it leaves some things to be desired when we're only concerned with destructuring nested maps with keyword keys, and want to make heavy use of extra features like required keys or default values:
* If we're only interested in top-level map inputs, we'd prefer to be able to say just `(fnk [a b c])` over `(fn [{:keys [a b c]}])` or `(fn [{a :a b :b c :c}])`.
* To require keys, I have to say (`fn [{:keys [a b c] :as m}] (assert (every? (partial contains? m) [:a :b :c]))) …)`. This means I have to mention every argument twice.
* Similarly, for default values, `(fn [{:keys [a] :or {a 2}}])` requires repeating argument names.
* For nested map bindings, I must either repeat myself or mix :keys with direct map destructuring: `(fnk [{{b :b} :a}])` or `(fnk [{{:keys [b]} :a}])`
This, while one option for Graph would be to just add arglist metadata to Clojure's `fn`, we have instead explored alternative syntax possibilities.
### Our fnk syntax proposal
Our primary design goal was to make keyword map destructuring, including nested and optional bindings, as straightforward and clear as possible. Other forms of destructuring (i.e., for sequences) will not be supported. We will introduce the syntax by example:
* Functions take a single map argument, and bare symbols in the top-level binding represent required keys of this map:
```clojure
(defnk foo [x y]
(+ x y))
(= (foo {:x 1 :y 2}) 3)
(thrown? Exception (foo {:x 1})) ;; y is required
```
* Optional keys with defaults are given as maps:
```clojure
(defnk foo [x y {z 10}]
(+ x y z))
(= (foo {:x 1 :y 2)) 13)
(= (foo {:x 1 :y 2 :z 3)) 6)
```
* Nested bindings are introduced with a vector (to match top-level bindings), but begin with the keyword to bind from:
```clojure
(defnk foo [x [:sub c {d 10}]] (+ x c d))
(= (foo {:x 1 :sub {:c 2}}) 13)
```
* `:as` and `&` are allowed in terminal binding positions, with same meaning as ordinary Clojure destructuring:
```clojure
(defnk foo [x & y :as z] [x y z])
(= (foo {:x 10 :y 20}) [10 {:y 20} {:x 10 :y 20}])
````
Advantages:
* Common case of flat required keys with no defaults is as simple as can be
* Nested bindings are as minimal as possible
* Notation is internally consistent: `[]` always indicates map binding, `{}` optional args
* Key name repetition is eliminated for required keys and default values.
Known disadvantages:
* Different from existing Clojure destructuring
* No sequence binding
* Disparity between outer binding and nested bindings (which begin with keyword)
* Renaming a key is a bit verbose -- `[:a :as b]`
### Alternatives
Let's take a simple example that includes most features of the above proposal, and compare with several alternative possibilities:
```clojure
(defnk foo [x {y 1} [:z :as zalt] [:sub c]] ;; above proposal
[x y zalt c])
(= (foo {:x 5 :z 10 :sub {:c 20}}) [5 1 10 20])
```
**Potential alternative 1:** exising Clojure syntax (see above).
```clojure
(defn foo [{x :x y :y zalt :z {c :c} :sub :or {y 1} :as m}] ;; existing syntax
(assert (and (contains? m :x) (contains? (:sub m) :c)))
[x y zalt c])
```
* Advantages: already exists, known to everyone, consistent
* Disadvantages: verbose if you only care about map bindings, especially if you want required keys, default values, or nested bindings, all of which we use quite frequently.
* Neutral: for Graph, we also have to modify `fn` (or create our own version) to record metadata about arglists and extract required or optional keys.
**Potential alternative 2:** an earlier version of fnk used `[]` for map bindings, and within a binding, `{}` to introduce sub-bindings and renamings, and `[]` for default values.
```clojure
(defnk-2 foo [x [y 1] {zalt :z [c] :sub}] ;; alternative 2
[x y zalt c])
(= (foo {:x 5 :z 10 :sub {:c 20}}) [5 1 10 20])
```
* Advantage: Nested binding completely uniform with top-level (due to extra level of syntax)
* Disadvantage: Each nested binding requires two levels of syntax
* Disadvantage: [] used for two things (map binding and default values)
**Potential alternative 3:** like primary proposal, but use `#{}` literals for map binding (rather than `[]`) because map bindings are unordered (and to differentiate from existing syntax). It's not clear how to best support nested binding keys, `:as:` and `&` in this unordered setting, but something like this might work:
```clojure
(defnk-3 foo #{x {y 1} #{:z ^:as zalt} #{:sub c}} ;; alternative 3
[x y zalt c])
```
* Advantage: Set literal for binding conveys un-ordered nature of bindings
* Advangage: Set literal also avoids any possibility for confusion with existing destructuring/`defn`
* Disadvantage: `#{}` is not so pretty
* Disadvantage: No obvious clean way to support `:as` and `&`, or enforce that the keyword comes first in nested binding, unless we change these to use metadata or add additional syntax.
## Addendum: underlying metadata representation for `fnk`
For the purposes of Graph, a `fnk` is just a fn of a single map argument, which also responds to protocol fn `(io-schemata f)` that returns a pair of an input schema and an output schema.
An input schema is a nested map where keys are keywords and leaves are true or false, to indicate optional or required keys. (Ultimately, it might be useful to put more sophisticated type information at the leaves). Similarly, an output schema is a nested map where all the leaves are true (representing guaranteed elements of the return value).
For example, `(satisfies-schema? {:x true :y false :z true} {:x 2 :z 1})`.
Because Graph only depends on these protocol and schema definitions, you can use it without our `fnk` by definining schema metadata directly, or designing your own syntax. Of course, we'd still like to get `fnk` right in our release, which is why we really need your input.
[Graph]: http://plumatic.github.io/prismatics-graph-at-strange-loop
prismatic-plumbing-clojure-0.5.4/src/plumbing/fnk/impl.clj 0000644 0000000 0000000 00000045270 13141156451 0023613 0 ustar 00root root 0000000 0000000 (ns plumbing.fnk.impl
"Core utilities for parsing our 'fnk'-style binding syntax.
Documented and tested through the actual 'letk','fnk', and 'defnk'
macros in plumbing.core.
The core entry points into this namespace are 'letk*' and 'fnk*',
which parse the new binding syntax and generate fnk bodies,
respectively.
For efficiency, two different methods of generating fnk bodies are
used. If the fnk takes a fixed set of arguments (i.e., no & or
:as), then a 'positional' version of the fnk that is called like an
ordinary Clojure fn (e.g., (f a b) rather than (f {:a a :b b}) is
generated as an implementation detail, and stored in metadata of
the actual keyword fnk (which is just a thin wrapper around the
positional version). If '& or :as are used, no such positional
function is generated.
The advantage of these 'positional' functions is that they can be
accessed using 'efficient-call-forms' or 'positional-fn' to call
the fnk without incurring the overhead of producing and then
destructuring a top-level map. See plumbing.graph.positional for
an example use."
(:require
[clojure.set :as set]
[schema.core :as s]
[schema.macros :as schema-macros]
[plumbing.fnk.schema :as schema]
[plumbing.fnk.pfnk :as pfnk]))
;; TODO: maybe ^:strict metadata to turn off accepting additional keys?
;;; Helpers
(defn name-sym
"Returns symbol of x's name.
Converts a keyword/string to symbol, or removes namespace (if any) of symbol"
[x]
(with-meta (symbol (name x)) (meta x)))
(defn qualified-sym
"Returns qualified symbol of x, an instance of Named"
[x]
(symbol (namespace x) (name x)))
;;; Parsing new fnk binding style
(declare letk-input-schema-and-body-form)
(defn- any-schema? [s]
(= `s/Any s))
(defn- assert-unschematized [x]
(let [schema (schema-macros/extract-schema-form x)]
(schema/assert-iae (any-schema? schema) "Schema metadata not allowed on %s :- %s" x schema)))
(defn ensure-schema-metadata [env x]
(schema-macros/normalized-metadata env x nil))
(defn schema-override [sym schema]
(vary-meta sym assoc :schema schema))
(defn- process-schematized-map
"Take an optional binding map like {a 2} or {a :- Number 2} and convert the schema
information to canonical metadata, if present."
[env binding]
(case (count binding)
1 (let [[sym v] (first binding)]
{(ensure-schema-metadata env sym) v})
2 (let [[[[sym _]] [[schema v]]] ((juxt filter remove) #(= (val %) :-) binding)]
(schema/assert-iae (and (symbol? sym) schema)
"Bad schematized binding %s: should look like {a :- Number 2}" binding)
{(schema-macros/normalized-metadata env sym schema) v})))
;; TODO: unify this with positional version.
(defn letk-arg-bind-sym-and-body-form
"Given a single element of a single letk binding form and a current body form, return
a map {:schema-entry :body-form} where schema-entry is a tuple
[bound-key schema external-schema?], and body-form wraps body with destructuring
for this binding as necessary."
[env map-sym binding key-path body-form]
(cond (symbol? binding)
{:schema-entry [(keyword binding) (schema-macros/extract-schema-form binding)]
:body-form `(let [~(name-sym binding) (schema/safe-get ~map-sym ~(keyword binding) ~key-path)]
~body-form)}
(map? binding)
(let [schema-fixed-binding (process-schematized-map env binding)
[bound-sym opt-val-expr] (first schema-fixed-binding)
bound-key (keyword bound-sym)]
(assert-unschematized binding)
(schema/assert-iae (= 1 (count schema-fixed-binding))
"optional binding has more than 1 entry: %s" schema-fixed-binding)
{:schema-entry [`(with-meta (s/optional-key ~bound-key) {:default '~opt-val-expr}) (schema-macros/extract-schema-form bound-sym)]
:body-form `(let [~(name-sym bound-sym) (get ~map-sym ~bound-key ~opt-val-expr)]
~body-form)})
(vector? binding)
(let [[bound-key & more] binding
{inner-input-schema :input-schema
inner-external-input-schema :external-input-schema
inner-map-sym :map-sym
inner-body-form :body-form} (letk-input-schema-and-body-form
env
(with-meta (vec more) (meta binding))
(conj key-path bound-key)
body-form)]
(schema/assert-iae
(keyword? bound-key)
"First element to nested binding not a keyword: %s" bound-key)
{:schema-entry [bound-key inner-input-schema inner-external-input-schema]
:body-form `(let [~inner-map-sym (schema/safe-get ~map-sym ~bound-key ~key-path)]
~inner-body-form)})
:else (throw (IllegalArgumentException. (format "bad binding: %s" binding)))))
(defn- extract-special-args
"Extract trailing & sym and :as sym, possibly with schema metadata. Returns
[more-bindings special-args-map] where special-args-map is a map from each
special symbol found to the symbol that was found."
[env special-arg-signifier-set binding-form]
{:pre [(set? special-arg-signifier-set)]}
(let [[more-bindings special-bindings] (split-with (complement special-arg-signifier-set) binding-form)]
(loop [special-args-map {}
special-arg-set special-arg-signifier-set
[arg-signifier & other-bindings :as special-bindings] special-bindings]
(if-not (seq special-bindings)
[more-bindings special-args-map]
(do
(schema/assert-iae (special-arg-set arg-signifier) "Got illegal special arg: " arg-signifier)
(let [[sym remaining-bindings] (schema-macros/extract-arrow-schematized-element env other-bindings)]
(schema/assert-iae (symbol? sym) "Argument to %s not a symbol: %s" arg-signifier binding-form)
(recur (assoc special-args-map arg-signifier sym)
(disj special-arg-set arg-signifier)
remaining-bindings)))))))
(defn letk-input-schema-and-body-form
"Given a single letk binding form, value form, key path, and body
form, return a map {:input-schema :external-input-schema :map-sym :body-form}
where input-schema is the schema imposed by binding-form, external-input-schema
is like input-schema but includes user overrides for binding vectors,
map-sym is the symbol which it expects the bound value to be bound to,
and body-form wraps body in the bindings from binding-form from map-sym."
[env binding-form key-path body-form]
(schema/assert-iae (vector? binding-form) "Binding form is not vector: %s" binding-form)
(let [binding-schema (schema-macros/extract-schema-form binding-form)
[bindings {more-sym '& as-sym :as}] (extract-special-args env #{'& :as} binding-form)
as-sym (or as-sym (ensure-schema-metadata env (gensym "map")))
[input-schema-elts
external-input-schema-elts
bound-body-form] (reduce
(fn [[input-schema-elts external-input-schema-elts cur-body] binding]
(let [{:keys [schema-entry body-form]}
(letk-arg-bind-sym-and-body-form
env as-sym binding key-path cur-body)
[bound-key input-schema external-input-schema] schema-entry]
[(conj input-schema-elts [bound-key input-schema])
(conj external-input-schema-elts
[bound-key (or external-input-schema input-schema)])
body-form]))
[[] [] body-form]
(reverse
(schema-macros/process-arrow-schematized-args
env bindings)))
explicit-schema-keys (keep (comp first schema/unwrap-schema-form-key first)
input-schema-elts)
final-body-form (if more-sym
`(let [~more-sym (dissoc ~as-sym ~@explicit-schema-keys)]
~bound-body-form)
bound-body-form)
make-input-schema (fn [elts]
(if-not (or more-sym (seq elts) (empty? key-path))
`s/Any ;; allow [:a :as :b] inner bindings without requiring :a be a map
(merge
(into {} elts)
(let [more-schema (if more-sym
(schema-macros/extract-schema-form more-sym)
`s/Any)]
(if (any-schema? more-schema)
{`s/Keyword `s/Any}
(do (schema/assert-iae (map? more-schema)
"& %s schema must be a map" more-sym)
more-schema))))))]
(when as-sym (assert-unschematized as-sym))
(schema/assert-iae (not (some #{'&} (map first input-schema-elts))) "Cannot bind to &")
(schema/assert-distinct (concat (map name-sym explicit-schema-keys)
(remove nil? [more-sym as-sym])))
{:input-schema (make-input-schema input-schema-elts)
:external-input-schema (if-not (any-schema? binding-schema)
binding-schema
(make-input-schema external-input-schema-elts))
:map-sym as-sym
:body-form final-body-form}))
;;; Positional fnks
(def +none+
"A sentinel value used to indicate a non-provided optional value in a positional form."
::none)
(defn positional-arg-bind-sym-and-body
"Given a single element of a fnk binding form and a current body form, return
a pair [[k bind-sym] new-body-form] where bind-sym is a suitable symbol to bind
to k in the fnk arglist (including tag metadata if applicable) and new-body-form
is wrapped with destructuring for this binding as necessary."
[env binding body-form]
(cond (symbol? binding)
(let [bind-sym (gensym (name binding))]
[[(keyword binding) bind-sym]
`(let [~(name-sym binding) ~bind-sym] ~body-form)])
(map? binding)
(let [[bs ov] (first (process-schematized-map env binding))
bind-sym (gensym (name bs))]
[[(keyword bs) bind-sym]
`(let [~(name-sym bs) (if (identical? +none+ ~bind-sym) ~ov ~bind-sym)]
~body-form)])
(vector? binding)
(let [[k & more] binding
{:keys [map-sym body-form]} (letk-input-schema-and-body-form
env (ensure-schema-metadata env (vec more)) [k]
body-form)]
[[k
(with-meta map-sym
(if (= (last (butlast binding)) :as) (meta (last binding)) {}))]
body-form])
:else (throw (IllegalArgumentException. (format "bad binding: %s" binding)))))
(defn positional-arg-bind-syms-and-body
"Given a fnk binding form and body form, return a pair
[bind-sym-map new-body-form] where bind-sym-map is a map from keyword args
to binding symbols and and new-body-form wraps body to do any extra processing
of nested or optional bindings above and beyond the bindings achieved by
bind-sym-vector."
[env bind body-form]
(reduce
(fn [[cur-bind cur-body] binding]
(let [[bind-sym new-body] (positional-arg-bind-sym-and-body env binding cur-body)]
[(conj cur-bind bind-sym) new-body]))
[{} body-form]
(reverse (schema-macros/process-arrow-schematized-args env bind))))
(defn positional-info
"If fnk has a positional function implementation, return the pair
[positional-fn positional-arg-ks] such that if positional-arg-ks is [:a :b :c],
calling (positional-fn a b c) is equivalent to calling (fnk {:a a :b b :c c}),
but faster. Optional values to fnk can be simulated by passing +none+ as the
value, i.e., (positional-fn +none+ b +none) is like (fnk {:b b})."
[fnk]
(get (meta fnk) ::positional-info))
(defn efficient-call-forms
"Get [f arg-forms] that can be used to call a fnk most efficiently, using the
positional version if available, or otherwise the raw fnk. arg-form-map
is a map from keywords representing arguments to fnk to *forms* that evaluate
to the corresponding arguments.
The basic idea is that (eval (cons f arg-forms)) would yield code for an
efficient call to fnk. However, this form is not returned directly, because
in most cases the literal function f cannot be directly evaluated due to
a quirk in Clojure -- e.g., try (eval `(~(let [x 1] (fn [y] (+ y x))) 2)).
For examples of how this is used, see 'positional-fn' below, or the positional
compilation in plumbing.graph.positional."
[fnk arg-form-map]
(if-let [[positional-f positional-args] (positional-info fnk)]
(do (schema/assert-iae (set/superset? (set (keys arg-form-map))
(set positional-args))
"Trying to call fn that takes args %s with args %s"
positional-args arg-form-map)
[positional-f (map arg-form-map positional-args)])
[fnk [`(into {} (remove #(identical? +none+ (second %)) ~arg-form-map))]]))
(defn positional-fn
"Given argument order in arg-ks, produce an ordinary fn that can be called
with arguments in this order. arg-ks must include all required keys of fnk.
Example: (= ((positional-fn a-fnk [:b :a]) [1 2]) (a-fnk {:a 2 :b 1}))
Can only be applied to fnks with a positional form, and should yield
a function that is significantly faster than calling fnk directly by
avoiding the construction and destructuring of the outer map. Uses 'eval',
so while the produced function is fast, the actual production of the
positional-fn is generally relatively slow."
[fnk arg-ks]
(schema/assert-iae (apply distinct? ::dummy arg-ks)
"Invalid positional args %s contain duplicates" arg-ks)
(schema/assert-iae (positional-info fnk)
"Called positional-fn on a fnk without a positional form")
(let [input-schema (pfnk/input-schema fnk)
[missing-req missing-opt] (schema/split-schema-keys
(apply dissoc (schema/explicit-schema-key-map input-schema)
(set arg-ks)))
extra-args (remove (partial schema/possibly-contains? input-schema) arg-ks)
arg-syms (mapv name-sym arg-ks)
[pos-fn pos-args] (efficient-call-forms
fnk
(merge (zipmap arg-ks arg-syms)
(zipmap missing-opt (repeat +none+))))]
(schema/assert-iae (and (empty? missing-req) (empty? extra-args))
"Invalid positional args %s missing %s, with extra %s"
arg-ks missing-req extra-args)
((eval `(fn [f#] (fn ~arg-syms (f# ~@pos-args))))
pos-fn)))
(defn positional-fnk-form
"Takes an optional name, input schema, seq of ordered [key optional?] pairs,
an arg-sym-map from these keywords to symbols, and and a positional fn body
that can reference these symbols.
Produces a form generating a IFn/PFnk that can be called as a keyword function,
and has metadata containing the positional function for efficient compilation
as described in 'efficient-call-forms' and 'positional-fn' above, with
argument order the same as in input-schema by default. Example:
(def f (eval (i/positional-fnk-form 'foo {:x s/Any (s/optional-key :y) s/Any}
[`(+ ~'x (if (= ~'y i/+none+) 5 ~'y))])))
(= [6 3] [(f {:x 1}) (f {:x 1 :y 2})])
(= [6 3] [((i/positional-fn f [:x]) 1) ((i/positional-fn f [:y :x]) 2 1)])."
[fn-name external-input-schema ordered-ks->opt arg-sym-map body form]
(let [[req-ks opt-ks] (schema/split-schema-keys (into {} ordered-ks->opt))
explicit-schema-keys (mapv first ordered-ks->opt)
pos-args (mapv #(do (schema-macros/assert! (contains? arg-sym-map %))
(arg-sym-map %))
explicit-schema-keys)]
`(let [pos-fn# (fn ~(symbol (str fn-name "-positional"))
~pos-args
~@body)]
(vary-meta
(s/fn
~fn-name
[m# :- ~external-input-schema]
(plumbing.core/letk [~(into (mapv qualified-sym req-ks)
(mapv (fn [k] {(qualified-sym k) +none+}) opt-ks))
m#]
(pos-fn# ~@(mapv name-sym explicit-schema-keys))))
merge
(assoc ~(meta form) ::positional-info [pos-fn# ~explicit-schema-keys])))))
;;; Generating fnk bodies
(defn fnk-form
"Take an optional name, binding form, and body for a fnk, and make an
IFn/PFnk for these arguments.
For efficiency, two different methods of generating fnk bodies are
used. If the fnk takes a fixed set of arguments (i.e., no & or
:as), then a 'positional' version of the fnk that is called like an
ordinary Clojure fn (e.g., (f a b) rather than (f {:a a :b b}) is
generated as an implementation detail, and stored in metadata of
the actual keyword fnk (which is just a thin wrapper around the
positional version). If '& or :as are used, no such positional
function is generated."
[env name? bind body form]
(let [{:keys [map-sym body-form input-schema external-input-schema]}
(letk-input-schema-and-body-form env bind [] `(do ~@body))
explicit-output-schema (if name? (schema-macros/extract-schema-form name?) `s/Any)
output-schema (if (any-schema? explicit-output-schema)
(schema/guess-expr-output-schema (last body))
explicit-output-schema)
fn-name (vary-meta (or name? (gensym "fnk")) assoc :schema output-schema)]
((fn [fn-form]
`(vary-meta ~fn-form assoc :name '~name?))
(if (and (not (schema-macros/cljs-env? env))
(not-any? #{'& :as} bind)) ;; If we can make a positional fnk form, do it.
(let [[bind-sym-map bound-body] (positional-arg-bind-syms-and-body env bind `(do ~@body))]
(positional-fnk-form
fn-name
external-input-schema
(vec (schema/explicit-schema-key-map input-schema))
bind-sym-map
[bound-body]
form))
(with-meta `(s/fn ~fn-name
[~(schema-override map-sym external-input-schema)]
(schema/assert-iae (map? ~map-sym) "fnk called on non-map: %s" ~map-sym)
~body-form)
(meta form))))))
prismatic-plumbing-clojure-0.5.4/src/plumbing/fnk/pfnk.clj 0000644 0000000 0000000 00000004140 13141156451 0023577 0 ustar 00root root 0000000 0000000 (ns plumbing.fnk.pfnk
"Core protocol and helpers for schema.core to extract and attach
input and output schemas to fnks. This protocol says nothing about
how fnks are created, so users are free to create PFnks directly
using fn->fnk, or using custom binding syntax (of which 'fnk' et al
are one possible example)."
(:require
[schema.core :as s :include-macros true]
[plumbing.fnk.schema :as schema :include-macros true]))
(set! *warn-on-reflection* true)
(defprotocol PFnk
"Protocol for keyword functions and their specifications, e.g., fnks and graphs."
(io-schemata [this]
"Return a pair of [input-schema output-schema], as specified in plumbing.fnk.schema."))
(defn input [^schema.core.FnSchema s]
(let [[[is :as args] :as schemas] (.-input-schemas s)]
(schema/assert-iae (= 1 (count schemas)) "Fnks have a single arity, not %s" (count schemas))
(schema/assert-iae (= 1 (count args)) "Fnks take a single argument, not %s" (count args))
(schema/assert-iae (instance? schema.core.One is) "Fnks take a single argument, not variadic")
(let [s (.-schema ^schema.core.One is)]
(schema/assert-iae (map? s) "Fnks take a map argument, not %s" (type s))
s)))
(defn output [^schema.core.FnSchema s]
(.-output-schema s))
(extend-type clojure.lang.Fn
PFnk
(io-schemata [this]
(assert (fn? this))
((juxt input output) (s/fn-schema this))))
(defn input-schema [pfnk]
(first (io-schemata pfnk)))
(defn output-schema [pfnk]
(second (io-schemata pfnk)))
(defn input-schema-keys [f]
(-> f input-schema schema/explicit-schema-key-map keys))
(defn fn->fnk
"Make a keyword function into a PFnk, by associating input and output schema metadata."
([f io] (fn->fnk f nil io))
([f name [input-schema output-schema :as io]]
(vary-meta (s/schematize-fn f (s/=> output-schema input-schema)) assoc :name name)))
(defn fnk-name
"Get the name of a fnk, if named"
[f]
(:name (meta f)))
(set! *warn-on-reflection* false)
;;;;;;;;;;;; This file autogenerated from src/plumbing/fnk/pfnk.cljx
prismatic-plumbing-clojure-0.5.4/src/plumbing/fnk/pfnk.cljx 0000644 0000000 0000000 00000004032 13141156451 0023767 0 ustar 00root root 0000000 0000000 (ns plumbing.fnk.pfnk
"Core protocol and helpers for schema.core to extract and attach
input and output schemas to fnks. This protocol says nothing about
how fnks are created, so users are free to create PFnks directly
using fn->fnk, or using custom binding syntax (of which 'fnk' et al
are one possible example)."
(:require
[schema.core :as s :include-macros true]
[plumbing.fnk.schema :as schema :include-macros true]))
#+clj (set! *warn-on-reflection* true)
(defprotocol PFnk
"Protocol for keyword functions and their specifications, e.g., fnks and graphs."
(io-schemata [this]
"Return a pair of [input-schema output-schema], as specified in plumbing.fnk.schema."))
(defn input [^schema.core.FnSchema s]
(let [[[is :as args] :as schemas] (.-input-schemas s)]
(schema/assert-iae (= 1 (count schemas)) "Fnks have a single arity, not %s" (count schemas))
(schema/assert-iae (= 1 (count args)) "Fnks take a single argument, not %s" (count args))
(schema/assert-iae (instance? schema.core.One is) "Fnks take a single argument, not variadic")
(let [s (.-schema ^schema.core.One is)]
(schema/assert-iae (map? s) "Fnks take a map argument, not %s" (type s))
s)))
(defn output [^schema.core.FnSchema s]
(.-output-schema s))
(extend-type #+clj clojure.lang.Fn #+cljs object
PFnk
(io-schemata [this]
(assert (fn? this))
((juxt input output) (s/fn-schema this))))
(defn input-schema [pfnk]
(first (io-schemata pfnk)))
(defn output-schema [pfnk]
(second (io-schemata pfnk)))
(defn input-schema-keys [f]
(-> f input-schema schema/explicit-schema-key-map keys))
(defn fn->fnk
"Make a keyword function into a PFnk, by associating input and output schema metadata."
([f io] (fn->fnk f nil io))
([f name [input-schema output-schema :as io]]
(vary-meta (s/schematize-fn f (s/=> output-schema input-schema)) assoc :name name)))
(defn fnk-name
"Get the name of a fnk, if named"
[f]
(:name (meta f)))
#+clj (set! *warn-on-reflection* false)
prismatic-plumbing-clojure-0.5.4/src/plumbing/fnk/schema.clj 0000644 0000000 0000000 00000022474 13141156451 0024113 0 ustar 00root root 0000000 0000000 (ns plumbing.fnk.schema
"A very simple type system for a subset of schemas consisting of nested
maps with optional or required keyword keys; used by fnk and kin.
Since schemas are turing-complete and not really designed for type inference,
(and for simplicity) we err on the side of completeness (allowing all legal programs)
at the cost of soundness.
These operations also bake in some logic specific to reasoning about Graphs,
namely that all input keys to a node must be explicitly mentioned as optional or
required, or provided via `instance`, and will thus deliberately drop extra key
schemas on inputs as appropriate. Output schemas may not have optional keys."
(:require
[schema.core :as s :include-macros true]
[schema.utils :as schema-utils]
[schema.macros :as schema-macros])
)
(def Schema (s/protocol s/Schema))
(def InputSchema {(s/cond-pre (s/eq s/Keyword) schema.core.OptionalKey s/Keyword) Schema})
(def OutputSchema Schema)
(def IOSchemata [(s/one InputSchema 'input) (s/one OutputSchema 'output)])
(def GraphInputSchema {(s/cond-pre schema.core.OptionalKey s/Keyword) Schema})
(def MapOutputSchema {s/Keyword Schema})
(def GraphIOSchemata [(s/one GraphInputSchema 'input) (s/one MapOutputSchema 'output)])
;;; Helpers
(defmacro assert-iae
"Like assert, but throws a RuntimeException in Clojure (not an AssertionError),
and also takes args to format."
[form & format-args]
`(schema-macros/assert! ~form ~@format-args))
(defn assert-distinct
"Like (assert (distinct? things)) but with a more helpful error message."
[things]
(let [repeated-things (->> things
frequencies
(filter #(> (val %) 1))
seq)]
(assert-iae (empty? repeated-things) "Got repeated items (expected distinct): %s" repeated-things)))
(defn safe-get
"Like (get m k), but throws if k is not present in m."
[m k key-path]
(assert-iae (map? m)
"Expected a map at key-path %s, got type %s" key-path (schema-utils/type-of m))
(let [[_ v :as p] (find m k)]
(when-not p (throw (ex-info ^String (schema-utils/format* "Key %s not found in %s" k (keys m))
{:error :missing-key
:key k
:map m})))
v))
(defn non-map-union [s1 s2]
(cond (= s1 s2) s1
(= s1 s/Any) s2
(= s2 s/Any) s1
:else s1)) ;; Punt, just take the first
(defn non-map-diff
"Return a difference of schmas s1 and s2, where one is not a map.
Punt for now, assuming s2 always satisfies s1."
[s1 s2]
nil)
(defn map-schema? [m]
(instance? clojure.lang.APersistentMap m)
)
;;; Input schemata
(s/defn unwrap-schema-form-key :- (s/maybe (s/pair s/Keyword "k" s/Bool "optional?"))
"Given a possibly-unevaluated schema map key form, unpack an explicit keyword
and optional? flag, or return nil for a non-explicit key"
[k]
(cond (s/specific-key? k)
[(s/explicit-schema-key k) (s/required-key? k)]
;; Deal with `(s/optional-key k) form from impl
(and (sequential? k) (not (vector? k)) (= (count k) 2)
(= (first k) 'schema.core/optional-key))
[(second k) false]
;; Deal with `(with-meta ...) form from impl
(and (sequential? k) (not (vector? k)) (= (first k) `with-meta))
(unwrap-schema-form-key (second k))))
(s/defn explicit-schema-key-map :- {s/Keyword s/Bool}
"Given a possibly-unevaluated map schema, return a map from bare keyword to true
(for required) or false (for optional)"
[s]
(->> s
keys
(keep unwrap-schema-form-key)
(into {})))
(s/defn split-schema-keys :- [(s/one [s/Keyword] 'required) (s/one [s/Keyword] 'optional)]
"Given output of explicit-schema-key-map, split into seq [req opt]."
[s :- {s/Keyword s/Bool}]
(->> s
((juxt filter remove) val)
(mapv (partial mapv key))))
(defn- merge-on-with
"Like merge-with, but also projects keys to a smaller space and merges them similar to the
values."
[key-project key-combine val-combine & maps]
(->> (apply concat maps)
(reduce
(fn [m [k v]]
(let [pk (key-project k)]
(if-let [[ok ov] (get m pk)]
(assoc m pk [(key-combine ok k) (val-combine ov v)])
(assoc m pk [k v]))))
{})
vals
(into {})))
(s/defn union-input-schemata :- InputSchema
"Returns a minimal input schema schema that entails satisfaction of both s1 and s2"
[i1 :- InputSchema i2 :- InputSchema]
(merge-on-with
#(if (s/specific-key? %) (s/explicit-schema-key %) :extra)
(fn [k1 k2]
(cond (s/required-key? k1) k1
(s/required-key? k2) k2
(s/optional-key? k1) (do (assert (= k1 k2)) k1)
(= k1 k2) k1
:else (assert-iae false "Only one extra schema allowed")))
(fn [s1 s2]
(if (and (map-schema? s1) (map-schema? s2))
(union-input-schemata s1 s2)
(non-map-union s1 s2)))
i1 i2))
(s/defn required-toplevel-keys :- [s/Keyword]
"Which top-level keys are required (i.e., non-false) by this input schema."
[input-schema :- InputSchema]
(keep
(fn [k]
(when (s/required-key? k)
(s/explicit-schema-key k)))
(keys input-schema)))
;;; Output schemata
(defn guess-expr-output-schema
"Guess an output schema for an expr. Currently just looks for literal map structure and
all keyword keys."
[expr]
(if (and (map? expr) (every? keyword? (keys expr)))
(into {} (for [[k v] expr] [k (guess-expr-output-schema v)]))
'schema.core/Any))
;;; Combining inputs and outputs.
(defn schema-diff ;; don't validate since it returns better errors.
"Subtract output-schema from input-schema, returning nil if it's possible that an object
satisfying the output-schema satisfies the input-schema, or otherwise a description
of the part(s) of input-schema not met by output-schema. Strict about the map structure
of output-schema matching input-schema, but loose about everything else (only looks at
required keys of output-schema."
[input-schema output-schema] ;; not schematized since it returns more helpful errors
(cond (not (map-schema? input-schema))
(non-map-diff input-schema output-schema)
(not (map-schema? output-schema))
(schema-macros/validation-error input-schema output-schema (list 'map? (s/explain output-schema)))
:else
(->> (for [[k v] input-schema
:when (s/specific-key? k)
:let [required? (s/required-key? k)
raw-k (s/explicit-schema-key k)
present? (contains? output-schema raw-k)]
:when (or required? present?)
:let [fail (if-not present?
'missing-required-key
(schema-diff v (get output-schema raw-k)))]
:when fail]
[k fail])
(into {})
not-empty)))
(defn assert-satisfies-schema [input-schema output-schema]
(let [fails (schema-diff input-schema output-schema)]
(when fails (throw (ex-info (str fails) {:error :does-not-satisfy-schema
:failures fails})))))
(s/defn ^:always-validate compose-schemata
"Given pairs of input and output schemata for fnks f1 and f2,
return a pair of input and output schemata for #(f2 (merge % (f1 %))).
f1's output schema must not contain any optional keys."
[[i2 o2] :- IOSchemata
[i1 o1] :- [(s/one InputSchema 'input) (s/one MapOutputSchema 'output)]]
(assert-satisfies-schema (select-keys i2 (keys o1)) o1)
[(union-input-schemata (apply dissoc i2 (concat (keys o1) (map s/optional-key (keys o1)))) i1)
o2])
(defn schema-key [m k]
(cond (contains? m k)
k
(contains? m (s/optional-key k))
(s/optional-key k)
:else nil))
(defn possibly-contains? [m k]
(boolean (schema-key m k)))
(s/defn split-schema
"Return a pair [ks-part non-ks-part], with any extra schema removed."
[s :- InputSchema ks :- [s/Keyword]]
(let [ks (set ks)]
(for [in? [true false]]
(into {} (for [[k v] s
:when (and (s/specific-key? k)
(= in? (contains? ks (s/explicit-schema-key k))))]
[k v])))))
(s/defn sequence-schemata :- GraphIOSchemata
"Given pairs of input and output schemata for fnks f1 and f2, and a keyword k,
return a pair of input and output schemata for #(let [v1 (f1 %)] (assoc v1 k (f2 (merge-disjoint % v1))))"
[[i1 o1] :- GraphIOSchemata
[k [i2 o2]] :- [(s/one s/Keyword "key") (s/one IOSchemata "inner-schemas")]]
(assert-iae (not (possibly-contains? i1 k)) "Duplicate key output (possibly due to a misordered graph) %s for input %s from input %s" k (s/explain i2) (s/explain i1))
(assert-iae (not (possibly-contains? o1 k)) "Node outputs a duplicate key %s given inputs %s" k (s/explain i1))
(let [[used unused] (split-schema i2 (keys o1))]
(assert-satisfies-schema used o1)
[(union-input-schemata unused i1)
(assoc o1 k o2)]))
;;;;;;;;;;;; This file autogenerated from src/plumbing/fnk/schema.cljx
prismatic-plumbing-clojure-0.5.4/src/plumbing/fnk/schema.cljx 0000644 0000000 0000000 00000022364 13141156451 0024301 0 ustar 00root root 0000000 0000000 (ns plumbing.fnk.schema
"A very simple type system for a subset of schemas consisting of nested
maps with optional or required keyword keys; used by fnk and kin.
Since schemas are turing-complete and not really designed for type inference,
(and for simplicity) we err on the side of completeness (allowing all legal programs)
at the cost of soundness.
These operations also bake in some logic specific to reasoning about Graphs,
namely that all input keys to a node must be explicitly mentioned as optional or
required, or provided via `instance`, and will thus deliberately drop extra key
schemas on inputs as appropriate. Output schemas may not have optional keys."
(:require
[schema.core :as s :include-macros true]
[schema.utils :as schema-utils]
#+clj [schema.macros :as schema-macros])
#+cljs
(:require-macros
#+cljs [schema.macros :as schema-macros]
[plumbing.fnk.schema :refer [assert-iae]]))
(def Schema (s/protocol s/Schema))
(def InputSchema {(s/cond-pre (s/eq s/Keyword) schema.core.OptionalKey s/Keyword) Schema})
(def OutputSchema Schema)
(def IOSchemata [(s/one InputSchema 'input) (s/one OutputSchema 'output)])
(def GraphInputSchema {(s/cond-pre schema.core.OptionalKey s/Keyword) Schema})
(def MapOutputSchema {s/Keyword Schema})
(def GraphIOSchemata [(s/one GraphInputSchema 'input) (s/one MapOutputSchema 'output)])
;;; Helpers
(defmacro assert-iae
"Like assert, but throws a RuntimeException in Clojure (not an AssertionError),
and also takes args to format."
[form & format-args]
`(schema-macros/assert! ~form ~@format-args))
(defn assert-distinct
"Like (assert (distinct? things)) but with a more helpful error message."
[things]
(let [repeated-things (->> things
frequencies
(filter #(> (val %) 1))
seq)]
(assert-iae (empty? repeated-things) "Got repeated items (expected distinct): %s" repeated-things)))
(defn safe-get
"Like (get m k), but throws if k is not present in m."
[m k key-path]
(assert-iae (map? m)
"Expected a map at key-path %s, got type %s" key-path (schema-utils/type-of m))
(let [[_ v :as p] (find m k)]
(when-not p (throw (ex-info ^String (schema-utils/format* "Key %s not found in %s" k (keys m))
{:error :missing-key
:key k
:map m})))
v))
(defn non-map-union [s1 s2]
(cond (= s1 s2) s1
(= s1 s/Any) s2
(= s2 s/Any) s1
:else s1)) ;; Punt, just take the first
(defn non-map-diff
"Return a difference of schmas s1 and s2, where one is not a map.
Punt for now, assuming s2 always satisfies s1."
[s1 s2]
nil)
(defn map-schema? [m]
#+clj (instance? clojure.lang.APersistentMap m)
#+cljs (or (instance? cljs.core.PersistentArrayMap m)
(instance? cljs.core.PersistentHashMap m)))
;;; Input schemata
(s/defn unwrap-schema-form-key :- (s/maybe (s/pair s/Keyword "k" s/Bool "optional?"))
"Given a possibly-unevaluated schema map key form, unpack an explicit keyword
and optional? flag, or return nil for a non-explicit key"
[k]
(cond (s/specific-key? k)
[(s/explicit-schema-key k) (s/required-key? k)]
;; Deal with `(s/optional-key k) form from impl
(and (sequential? k) (not (vector? k)) (= (count k) 2)
(= (first k) 'schema.core/optional-key))
[(second k) false]
;; Deal with `(with-meta ...) form from impl
(and (sequential? k) (not (vector? k)) (= (first k) `with-meta))
(unwrap-schema-form-key (second k))))
(s/defn explicit-schema-key-map :- {s/Keyword s/Bool}
"Given a possibly-unevaluated map schema, return a map from bare keyword to true
(for required) or false (for optional)"
[s]
(->> s
keys
(keep unwrap-schema-form-key)
(into {})))
(s/defn split-schema-keys :- [(s/one [s/Keyword] 'required) (s/one [s/Keyword] 'optional)]
"Given output of explicit-schema-key-map, split into seq [req opt]."
[s :- {s/Keyword s/Bool}]
(->> s
((juxt filter remove) val)
(mapv (partial mapv key))))
(defn- merge-on-with
"Like merge-with, but also projects keys to a smaller space and merges them similar to the
values."
[key-project key-combine val-combine & maps]
(->> (apply concat maps)
(reduce
(fn [m [k v]]
(let [pk (key-project k)]
(if-let [[ok ov] (get m pk)]
(assoc m pk [(key-combine ok k) (val-combine ov v)])
(assoc m pk [k v]))))
{})
vals
(into {})))
(s/defn union-input-schemata :- InputSchema
"Returns a minimal input schema schema that entails satisfaction of both s1 and s2"
[i1 :- InputSchema i2 :- InputSchema]
(merge-on-with
#(if (s/specific-key? %) (s/explicit-schema-key %) :extra)
(fn [k1 k2]
(cond (s/required-key? k1) k1
(s/required-key? k2) k2
(s/optional-key? k1) (do (assert (= k1 k2)) k1)
(= k1 k2) k1
:else (assert-iae false "Only one extra schema allowed")))
(fn [s1 s2]
(if (and (map-schema? s1) (map-schema? s2))
(union-input-schemata s1 s2)
(non-map-union s1 s2)))
i1 i2))
(s/defn required-toplevel-keys :- [s/Keyword]
"Which top-level keys are required (i.e., non-false) by this input schema."
[input-schema :- InputSchema]
(keep
(fn [k]
(when (s/required-key? k)
(s/explicit-schema-key k)))
(keys input-schema)))
;;; Output schemata
(defn guess-expr-output-schema
"Guess an output schema for an expr. Currently just looks for literal map structure and
all keyword keys."
[expr]
(if (and (map? expr) (every? keyword? (keys expr)))
(into {} (for [[k v] expr] [k (guess-expr-output-schema v)]))
'schema.core/Any))
;;; Combining inputs and outputs.
(defn schema-diff ;; don't validate since it returns better errors.
"Subtract output-schema from input-schema, returning nil if it's possible that an object
satisfying the output-schema satisfies the input-schema, or otherwise a description
of the part(s) of input-schema not met by output-schema. Strict about the map structure
of output-schema matching input-schema, but loose about everything else (only looks at
required keys of output-schema."
[input-schema output-schema] ;; not schematized since it returns more helpful errors
(cond (not (map-schema? input-schema))
(non-map-diff input-schema output-schema)
(not (map-schema? output-schema))
(schema-macros/validation-error input-schema output-schema (list 'map? (s/explain output-schema)))
:else
(->> (for [[k v] input-schema
:when (s/specific-key? k)
:let [required? (s/required-key? k)
raw-k (s/explicit-schema-key k)
present? (contains? output-schema raw-k)]
:when (or required? present?)
:let [fail (if-not present?
'missing-required-key
(schema-diff v (get output-schema raw-k)))]
:when fail]
[k fail])
(into {})
not-empty)))
(defn assert-satisfies-schema [input-schema output-schema]
(let [fails (schema-diff input-schema output-schema)]
(when fails (throw (ex-info (str fails) {:error :does-not-satisfy-schema
:failures fails})))))
(s/defn ^:always-validate compose-schemata
"Given pairs of input and output schemata for fnks f1 and f2,
return a pair of input and output schemata for #(f2 (merge % (f1 %))).
f1's output schema must not contain any optional keys."
[[i2 o2] :- IOSchemata
[i1 o1] :- [(s/one InputSchema 'input) (s/one MapOutputSchema 'output)]]
(assert-satisfies-schema (select-keys i2 (keys o1)) o1)
[(union-input-schemata (apply dissoc i2 (concat (keys o1) (map s/optional-key (keys o1)))) i1)
o2])
(defn schema-key [m k]
(cond (contains? m k)
k
(contains? m (s/optional-key k))
(s/optional-key k)
:else nil))
(defn possibly-contains? [m k]
(boolean (schema-key m k)))
(s/defn split-schema
"Return a pair [ks-part non-ks-part], with any extra schema removed."
[s :- InputSchema ks :- [s/Keyword]]
(let [ks (set ks)]
(for [in? [true false]]
(into {} (for [[k v] s
:when (and (s/specific-key? k)
(= in? (contains? ks (s/explicit-schema-key k))))]
[k v])))))
(s/defn sequence-schemata :- GraphIOSchemata
"Given pairs of input and output schemata for fnks f1 and f2, and a keyword k,
return a pair of input and output schemata for #(let [v1 (f1 %)] (assoc v1 k (f2 (merge-disjoint % v1))))"
[[i1 o1] :- GraphIOSchemata
[k [i2 o2]] :- [(s/one s/Keyword "key") (s/one IOSchemata "inner-schemas")]]
(assert-iae (not (possibly-contains? i1 k)) "Duplicate key output (possibly due to a misordered graph) %s for input %s from input %s" k (s/explain i2) (s/explain i1))
(assert-iae (not (possibly-contains? o1 k)) "Node outputs a duplicate key %s given inputs %s" k (s/explain i1))
(let [[used unused] (split-schema i2 (keys o1))]
(assert-satisfies-schema used o1)
[(union-input-schemata unused i1)
(assoc o1 k o2)]))
prismatic-plumbing-clojure-0.5.4/src/plumbing/graph.clj 0000644 0000000 0000000 00000032427 13141156451 0023175 0 ustar 00root root 0000000 0000000 (ns plumbing.graph
"A Graph is a simple, declarative way to define a composition of functions that is
easy to define, modify, execute, test, and monitor.
This blog post provides a high-level overview of Graph and its benefits:
http://plumatic.github.io/prismatics-graph-at-strange-loop
Concretely, a Graph specification is just a Clojure (nested) map with keyword keys
and keyword functions at the leaves.
A Graph is defined recursively as either:
1. a keyword function (i.e., fn satisfying PFnk), or
2. a Clojure map from keywords to (sub)graphs.
A Graph is a declarative specification of a single keyword function that
produces a map output, where each value in the output is produced by executing
the corresponding keyword function in the Graph. The inputs to the keyword
function are given by the outputs of other nodes in the graph with matching
keywords (mimicking lexical scope in the case of nested maps), or failing that,
from keywords in the input map.
For more details and examples of Graphs, see test/plumbing/graph_examples_test.cljx."
(:refer-clojure :exclude [compile])
(:require
[lazymap.core :as lazymap]
[schema.core :as s]
[schema.macros :as schema-macros]
[plumbing.fnk.schema :as schema :include-macros true]
[plumbing.fnk.pfnk :as pfnk]
[plumbing.fnk.impl :as fnk-impl]
[plumbing.graph.positional :as graph-positional]
[plumbing.core :as plumbing :include-macros true]
[plumbing.map :as map])
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Constructing graphs
(defn working-array-map
"array-map in cljs no longer preserves ordering, replicate the old functionality."
[& args]
(schema-macros/if-cljs
(.fromArray cljs.core/PersistentArrayMap (apply array args) true true)
(apply array-map args)))
(defn ->graph
"Convert a graph specification into a canonical well-formed 'graph', which
is an array-map with nodes in a correct topological order that will respond
to 'io-schemata' with a specification of the graph inputs and outputs.
The graph specification can be a Clojure map, in which case the topological
order will be computed (an error will be thrown for cyclic specifications),
or a sequence of key-value pairs that are already in a valid topological order
(an error will be thrown if the order is not valid). Values in the input
sequence are also converted to canonical graphs via recursive calls to ->graph."
[graph-nodes]
(if (or (fn? graph-nodes) (= graph-nodes (::self (meta graph-nodes))))
graph-nodes
(let [canonical-nodes (plumbing/map-vals ->graph graph-nodes)
graph (->> (if-not (map? graph-nodes)
(map first graph-nodes)
(->> canonical-nodes
(plumbing/map-vals pfnk/input-schema-keys)
map/topological-sort
reverse))
(mapcat #(find canonical-nodes %))
(apply working-array-map))]
(assert (every? keyword? (keys graph)))
(with-meta graph
{::io-schemata (update-in (reduce schema/sequence-schemata
[{} {}]
(for [[k node] graph]
[k (pfnk/io-schemata node)]))
[0] assoc s/Keyword s/Any)
::self graph}))))
;; Any Clojure map can be treated as a graph directly, without calling ->graph
(defn io-schemata* [g]
(plumbing/safe-get (meta (->graph g)) ::io-schemata))
(extend-protocol pfnk/PFnk
clojure.lang.IPersistentMap
(io-schemata [g] (io-schemata* g))
(io-schemata [g] (io-schemata* g)))
(defn- split-nodes [s]
(loop [in s out []]
(if-let [[f & r] (seq in)]
(cond (keyword? f) ;; key then value
(recur (next r) (conj out [f (first r)]))
(fn? f)
(do (schema/assert-iae (pfnk/fnk-name f) "Inline fnks must have a name (to be used as a key)")
(recur r (conj out [(keyword (pfnk/fnk-name f)) f])))
:else ;; inline graph
(recur r (into out f)))
out)))
(defn graph
"An ordered constructor for graphs, which enforces that the Graph is provided
in a valid topological ordering. This is a sanity check, and also enforces
defining graphs in a readable way. Most explicit graphs should be created
with this constructor.
(graph
:x-plus-1 (fnk [x] (inc x))
:2-x-plus-2 (fnk [x-plus-1] (* 2 x-plus-1)))
in addition, an 'inline' graph can be provided in place of a key-value
sequence, which will be merged into the graph at this position.
a named fnk can also be provided in place of a key-value pair,
where the fnk's name (as a keyword) is the implicit key."
[& nodes]
(let [partitioned (split-nodes nodes)]
(schema/assert-distinct (map first partitioned))
(->graph partitioned)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Compiling and running graphs
(defn eager-compile
"Compile graph specification g to a corresponding fnk that is optimized for
speed. Wherever possible, fnks are called positionally, to reduce the
overhead of creating and destructuring maps, and the return value is a
record, which is much faster to create and access than a map. Compilation
is relatively slow, however, due to internal calls to 'eval'."
[g]
(if (fn? g)
g
(let [g (for [[k sub-g] (->graph g)]
[k (eager-compile sub-g)])]
(graph-positional/positional-flat-compile (->graph g)))))
(defn positional-eager-compile
"Like eager-compile, but produce a non-keyword function that can be called
with args in the order provided by arg-ks, avoiding the overhead of creating
and destructuring a top-level map. This can yield a substantially faster
fn for Graphs with very computationally inexpensive node fnks."
[g arg-ks]
(fnk-impl/positional-fn (eager-compile g) arg-ks))
(defn simple-flat-compile
"Helper method for simple (non-nested) graph compilations that convert a graph
specification to a fnk that returns a Clojure map of the graph node values.
(make-map m) converts an initial Clojure map m to the return type of the fnk,
and (assoc-f m k f) associates the value given by (f) under key k to map m."
[g check-input? make-map assoc-f]
(let [g (->graph g)
req-ks (schema/required-toplevel-keys (pfnk/input-schema g))]
(pfnk/fn->fnk
(fn [m]
(when check-input?
(let [missing-keys (seq (remove #(contains? m %) req-ks))]
(schema/assert-iae (empty? missing-keys)
"Missing top-level keys in graph input: %s"
(set missing-keys))))
(apply
dissoc
(reduce
(fn [inner [k node-f]]
(schema/assert-iae (not (contains? inner k))
"Inner graph key %s duplicated" k)
(assoc-f inner k node-f))
(make-map m)
g)
(keys m)))
(pfnk/io-schemata g))))
(defn simple-hierarchical-compile
"Hierarchical extension of simple-nonhierarchical-compile."
[g check-input? make-map assoc-f]
(if (fn? g)
g
(simple-flat-compile
(for [[k sub-g] (->graph g)]
[k (simple-hierarchical-compile sub-g check-input? make-map assoc-f)])
check-input? make-map assoc-f)))
(defn restricted-call
"Call fnk f on the subset of keys its input schema explicitly asks for"
[f in-m]
(f (select-keys in-m (pfnk/input-schema-keys f))))
(defn interpreted-eager-compile
"Compile graph specification g to a corresponding fnk that returns an
ordinary Clojure map of the node result fns on a given input. The
compilation is much faster than 'eager-compile', but the compiled fn
will typically be much slower."
[g]
(simple-hierarchical-compile
g
true
(fn [m] m)
(fn [m k f] (assoc m k (restricted-call f m)))))
(defn lazy-compile
"Compile graph specification g to a corresponding fnk that returns a
lazymap of the node result fns on a given input. This fnk returns
the lazymap immediately, and node values are computed and cached as needed
as values are extracted from the lazymap. Besides this lazy behavior,
the lazymap can be used interchangeably with an ordinary Clojure map.
Required inputs to the graph are checked lazily, so you can omit input
keys not required by unneeded output keys."
[g]
(simple-hierarchical-compile
g
false
(fn [m] (reduce-kv assoc (lazymap/lazy-hash-map) m)) ;; into is extremely slow on lazymaps.
(fn [m k f] (lazymap/delay-assoc m k (delay (restricted-call f m))))))
;; TODO: move out.
(defn par-compile
"Experimental. Launches one future per node at startup; we probably woudln't
use this in production, and will release more sophisticated parallel
compilations later.
Compile graph specification g to a corresponding fnk that returns a
lazymap of the node result fns on a given input. This fnk returns
the lazymap immediately, and node values are computed and cached in parallel
starting immediately (and attempts to extract values from the lazymap will
block until each value is computed). Besides this lazy behavior,
the lazymap can be used interchangeably with an ordinary Clojure map."
[g]
(simple-hierarchical-compile
g
true
(fn [m] (into (lazymap/lazy-hash-map) m))
(fn [m k f] (lazymap/delay-assoc m k (future (restricted-call f m))))))
(defn compile
"Compile graph specification g to a corresponding fnk using the a default
compile strategy for host.
Clojure: eager-compile
ClojureScript: interpreted-eager-compile"
[g]
(eager-compile g)
)
(defn run
"Eagerly run a graph on an input by compiling and then executing on this input."
[g input]
((interpreted-eager-compile g) input))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Higher-order functions on graphs
(defn check-comp-partial!
"Check that instance-fn is a valid fn to comp-partial with graph g."
[g instance-fn]
(let [is (pfnk/input-schema g)
os (pfnk/output-schema instance-fn)]
(schema/assert-iae (map? os) "instance-fn must have output metadata")
(let [extra-ks (remove #(schema/possibly-contains? is %) (keys os))]
(schema/assert-iae (empty? extra-ks) "instance-fn provides unused keys: %s" (vec extra-ks)))
(doseq [[k s] os]
(schema/assert-satisfies-schema (or (get is k) (get is (s/optional-key k))) s))))
(defn comp-partial-fn
"Return a new pfnk representing the composition #(f (merge % (other %)))"
[f other]
(pfnk/fn->fnk
(fn [m] (f (merge m (other m))))
(schema/compose-schemata (pfnk/io-schemata f) (pfnk/io-schemata other))))
(defn comp-partial
"Experimental.
An extension of pfnk/comp-partial that supplies new parameters to a subgraph,
useful in composing hierarchical graphs.
g is a graph, and instance-fn is a fnk that takes arguments from the surrounding
context and produces new parameters that are fed into g. Works by comp-partialing
all leafs that expects any parameter produced by instance-fn with instance-fn,
so beware of expensive instance-fns, or those that expect caching of some sort
(i.e., attempt to generate shared state).
Throws an error if any parameter supplied by instance-fn is not used by at least
one node in g."
[g instance-fn]
(if (fn? g)
(comp-partial-fn g instance-fn)
(let [os (pfnk/output-schema instance-fn)]
(check-comp-partial! g instance-fn)
(->graph
(map/map-leaves
(fn [node-fn]
(if (some os (pfnk/input-schema-keys node-fn))
(comp-partial-fn node-fn instance-fn)
node-fn))
g)))))
(defmacro instance
"Experimental.
Convenience macro for comp-partial, used to supply inline parameters to a
subgraph (or fnk).
Example:
(= {:x 21}
(run (instance {:x (fnk [a] (inc a))} [z] {:a (* z 2)})
{:z 10}))"
([g m] `(instance ~g [] ~m))
([g bind m]
`(comp-partial ~g (plumbing/fnk ~bind ~m))))
(defn profiled
"Modify graph spec g, producing a new graph spec with a new top-level key
'profile-key'. After each node value is computed, the number of milliseconds
taken to compute its value will be stored under an atom at 'profile-key'."
[profile-key g]
(assert (and (keyword? profile-key) (not (get g profile-key))))
(->graph
(assoc (map/map-leaves-and-path
(fn [ks f]
(pfnk/fn->fnk
(fn [m]
(let [pm (plumbing/safe-get m profile-key)
start (System/nanoTime)
res (f (dissoc m profile-key))]
(swap! pm assoc-in ks
(/ (- (System/nanoTime) start) 1000000.0)
)
res))
[(assoc (pfnk/input-schema f)
profile-key s/Any)
(pfnk/output-schema f)]))
(->graph g))
profile-key (plumbing/fnk [] (atom {})))))
;;;;;;;;;;;; This file autogenerated from src/plumbing/graph.cljx
prismatic-plumbing-clojure-0.5.4/src/plumbing/graph.cljx 0000644 0000000 0000000 00000032324 13141156451 0023361 0 ustar 00root root 0000000 0000000 (ns plumbing.graph
"A Graph is a simple, declarative way to define a composition of functions that is
easy to define, modify, execute, test, and monitor.
This blog post provides a high-level overview of Graph and its benefits:
http://plumatic.github.io/prismatics-graph-at-strange-loop
Concretely, a Graph specification is just a Clojure (nested) map with keyword keys
and keyword functions at the leaves.
A Graph is defined recursively as either:
1. a keyword function (i.e., fn satisfying PFnk), or
2. a Clojure map from keywords to (sub)graphs.
A Graph is a declarative specification of a single keyword function that
produces a map output, where each value in the output is produced by executing
the corresponding keyword function in the Graph. The inputs to the keyword
function are given by the outputs of other nodes in the graph with matching
keywords (mimicking lexical scope in the case of nested maps), or failing that,
from keywords in the input map.
For more details and examples of Graphs, see test/plumbing/graph_examples_test.cljx."
(:refer-clojure :exclude [compile])
(:require
#+clj [lazymap.core :as lazymap]
[schema.core :as s]
#+clj [schema.macros :as schema-macros]
[plumbing.fnk.schema :as schema :include-macros true]
[plumbing.fnk.pfnk :as pfnk]
#+clj [plumbing.fnk.impl :as fnk-impl]
#+clj [plumbing.graph.positional :as graph-positional]
[plumbing.core :as plumbing :include-macros true]
[plumbing.map :as map])
#+cljs (:require-macros [schema.macros :as schema-macros]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Constructing graphs
(defn working-array-map
"array-map in cljs no longer preserves ordering, replicate the old functionality."
[& args]
(schema-macros/if-cljs
(.fromArray cljs.core/PersistentArrayMap (apply array args) true true)
(apply array-map args)))
(defn ->graph
"Convert a graph specification into a canonical well-formed 'graph', which
is an array-map with nodes in a correct topological order that will respond
to 'io-schemata' with a specification of the graph inputs and outputs.
The graph specification can be a Clojure map, in which case the topological
order will be computed (an error will be thrown for cyclic specifications),
or a sequence of key-value pairs that are already in a valid topological order
(an error will be thrown if the order is not valid). Values in the input
sequence are also converted to canonical graphs via recursive calls to ->graph."
[graph-nodes]
(if (or (fn? graph-nodes) (= graph-nodes (::self (meta graph-nodes))))
graph-nodes
(let [canonical-nodes (plumbing/map-vals ->graph graph-nodes)
graph (->> (if-not (map? graph-nodes)
(map first graph-nodes)
(->> canonical-nodes
(plumbing/map-vals pfnk/input-schema-keys)
map/topological-sort
reverse))
(mapcat #(find canonical-nodes %))
(apply working-array-map))]
(assert (every? keyword? (keys graph)))
(with-meta graph
{::io-schemata (update-in (reduce schema/sequence-schemata
[{} {}]
(for [[k node] graph]
[k (pfnk/io-schemata node)]))
[0] assoc s/Keyword s/Any)
::self graph}))))
;; Any Clojure map can be treated as a graph directly, without calling ->graph
(defn io-schemata* [g]
(plumbing/safe-get (meta (->graph g)) ::io-schemata))
(extend-protocol pfnk/PFnk
#+clj clojure.lang.IPersistentMap
#+cljs cljs.core.PersistentArrayMap
(io-schemata [g] (io-schemata* g))
#+cljs cljs.core.PersistentHashMap
(io-schemata [g] (io-schemata* g)))
(defn- split-nodes [s]
(loop [in s out []]
(if-let [[f & r] (seq in)]
(cond (keyword? f) ;; key then value
(recur (next r) (conj out [f (first r)]))
(fn? f)
(do (schema/assert-iae (pfnk/fnk-name f) "Inline fnks must have a name (to be used as a key)")
(recur r (conj out [(keyword (pfnk/fnk-name f)) f])))
:else ;; inline graph
(recur r (into out f)))
out)))
(defn graph
"An ordered constructor for graphs, which enforces that the Graph is provided
in a valid topological ordering. This is a sanity check, and also enforces
defining graphs in a readable way. Most explicit graphs should be created
with this constructor.
(graph
:x-plus-1 (fnk [x] (inc x))
:2-x-plus-2 (fnk [x-plus-1] (* 2 x-plus-1)))
in addition, an 'inline' graph can be provided in place of a key-value
sequence, which will be merged into the graph at this position.
a named fnk can also be provided in place of a key-value pair,
where the fnk's name (as a keyword) is the implicit key."
[& nodes]
(let [partitioned (split-nodes nodes)]
(schema/assert-distinct (map first partitioned))
(->graph partitioned)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Compiling and running graphs
#+clj
(defn eager-compile
"Compile graph specification g to a corresponding fnk that is optimized for
speed. Wherever possible, fnks are called positionally, to reduce the
overhead of creating and destructuring maps, and the return value is a
record, which is much faster to create and access than a map. Compilation
is relatively slow, however, due to internal calls to 'eval'."
[g]
(if (fn? g)
g
(let [g (for [[k sub-g] (->graph g)]
[k (eager-compile sub-g)])]
(graph-positional/positional-flat-compile (->graph g)))))
#+clj
(defn positional-eager-compile
"Like eager-compile, but produce a non-keyword function that can be called
with args in the order provided by arg-ks, avoiding the overhead of creating
and destructuring a top-level map. This can yield a substantially faster
fn for Graphs with very computationally inexpensive node fnks."
[g arg-ks]
(fnk-impl/positional-fn (eager-compile g) arg-ks))
(defn simple-flat-compile
"Helper method for simple (non-nested) graph compilations that convert a graph
specification to a fnk that returns a Clojure map of the graph node values.
(make-map m) converts an initial Clojure map m to the return type of the fnk,
and (assoc-f m k f) associates the value given by (f) under key k to map m."
[g check-input? make-map assoc-f]
(let [g (->graph g)
req-ks (schema/required-toplevel-keys (pfnk/input-schema g))]
(pfnk/fn->fnk
(fn [m]
(when check-input?
(let [missing-keys (seq (remove #(contains? m %) req-ks))]
(schema/assert-iae (empty? missing-keys)
"Missing top-level keys in graph input: %s"
(set missing-keys))))
(apply
dissoc
(reduce
(fn [inner [k node-f]]
(schema/assert-iae (not (contains? inner k))
"Inner graph key %s duplicated" k)
(assoc-f inner k node-f))
(make-map m)
g)
(keys m)))
(pfnk/io-schemata g))))
(defn simple-hierarchical-compile
"Hierarchical extension of simple-nonhierarchical-compile."
[g check-input? make-map assoc-f]
(if (fn? g)
g
(simple-flat-compile
(for [[k sub-g] (->graph g)]
[k (simple-hierarchical-compile sub-g check-input? make-map assoc-f)])
check-input? make-map assoc-f)))
(defn restricted-call
"Call fnk f on the subset of keys its input schema explicitly asks for"
[f in-m]
(f (select-keys in-m (pfnk/input-schema-keys f))))
(defn interpreted-eager-compile
"Compile graph specification g to a corresponding fnk that returns an
ordinary Clojure map of the node result fns on a given input. The
compilation is much faster than 'eager-compile', but the compiled fn
will typically be much slower."
[g]
(simple-hierarchical-compile
g
true
(fn [m] m)
(fn [m k f] (assoc m k (restricted-call f m)))))
#+clj
(defn lazy-compile
"Compile graph specification g to a corresponding fnk that returns a
lazymap of the node result fns on a given input. This fnk returns
the lazymap immediately, and node values are computed and cached as needed
as values are extracted from the lazymap. Besides this lazy behavior,
the lazymap can be used interchangeably with an ordinary Clojure map.
Required inputs to the graph are checked lazily, so you can omit input
keys not required by unneeded output keys."
[g]
(simple-hierarchical-compile
g
false
(fn [m] (reduce-kv assoc (lazymap/lazy-hash-map) m)) ;; into is extremely slow on lazymaps.
(fn [m k f] (lazymap/delay-assoc m k (delay (restricted-call f m))))))
#+clj ;; TODO: move out.
(defn par-compile
"Experimental. Launches one future per node at startup; we probably woudln't
use this in production, and will release more sophisticated parallel
compilations later.
Compile graph specification g to a corresponding fnk that returns a
lazymap of the node result fns on a given input. This fnk returns
the lazymap immediately, and node values are computed and cached in parallel
starting immediately (and attempts to extract values from the lazymap will
block until each value is computed). Besides this lazy behavior,
the lazymap can be used interchangeably with an ordinary Clojure map."
[g]
(simple-hierarchical-compile
g
true
(fn [m] (into (lazymap/lazy-hash-map) m))
(fn [m k f] (lazymap/delay-assoc m k (future (restricted-call f m))))))
(defn compile
"Compile graph specification g to a corresponding fnk using the a default
compile strategy for host.
Clojure: eager-compile
ClojureScript: interpreted-eager-compile"
[g]
#+clj (eager-compile g)
#+cljs (interpreted-eager-compile g))
(defn run
"Eagerly run a graph on an input by compiling and then executing on this input."
[g input]
((interpreted-eager-compile g) input))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Higher-order functions on graphs
(defn check-comp-partial!
"Check that instance-fn is a valid fn to comp-partial with graph g."
[g instance-fn]
(let [is (pfnk/input-schema g)
os (pfnk/output-schema instance-fn)]
(schema/assert-iae (map? os) "instance-fn must have output metadata")
(let [extra-ks (remove #(schema/possibly-contains? is %) (keys os))]
(schema/assert-iae (empty? extra-ks) "instance-fn provides unused keys: %s" (vec extra-ks)))
(doseq [[k s] os]
(schema/assert-satisfies-schema (or (get is k) (get is (s/optional-key k))) s))))
(defn comp-partial-fn
"Return a new pfnk representing the composition #(f (merge % (other %)))"
[f other]
(pfnk/fn->fnk
(fn [m] (f (merge m (other m))))
(schema/compose-schemata (pfnk/io-schemata f) (pfnk/io-schemata other))))
(defn comp-partial
"Experimental.
An extension of pfnk/comp-partial that supplies new parameters to a subgraph,
useful in composing hierarchical graphs.
g is a graph, and instance-fn is a fnk that takes arguments from the surrounding
context and produces new parameters that are fed into g. Works by comp-partialing
all leafs that expects any parameter produced by instance-fn with instance-fn,
so beware of expensive instance-fns, or those that expect caching of some sort
(i.e., attempt to generate shared state).
Throws an error if any parameter supplied by instance-fn is not used by at least
one node in g."
[g instance-fn]
(if (fn? g)
(comp-partial-fn g instance-fn)
(let [os (pfnk/output-schema instance-fn)]
(check-comp-partial! g instance-fn)
(->graph
(map/map-leaves
(fn [node-fn]
(if (some os (pfnk/input-schema-keys node-fn))
(comp-partial-fn node-fn instance-fn)
node-fn))
g)))))
(defmacro instance
"Experimental.
Convenience macro for comp-partial, used to supply inline parameters to a
subgraph (or fnk).
Example:
(= {:x 21}
(run (instance {:x (fnk [a] (inc a))} [z] {:a (* z 2)})
{:z 10}))"
([g m] `(instance ~g [] ~m))
([g bind m]
`(comp-partial ~g (plumbing/fnk ~bind ~m))))
(defn profiled
"Modify graph spec g, producing a new graph spec with a new top-level key
'profile-key'. After each node value is computed, the number of milliseconds
taken to compute its value will be stored under an atom at 'profile-key'."
[profile-key g]
(assert (and (keyword? profile-key) (not (get g profile-key))))
(->graph
(assoc (map/map-leaves-and-path
(fn [ks f]
(pfnk/fn->fnk
(fn [m]
(let [pm (plumbing/safe-get m profile-key)
start #+clj (System/nanoTime) #+cljs (plumbing/millis)
res (f (dissoc m profile-key))]
(swap! pm assoc-in ks
#+clj (/ (- (System/nanoTime) start) 1000000.0)
#+cljs (- (plumbing/millis) start))
res))
[(assoc (pfnk/input-schema f)
profile-key s/Any)
(pfnk/output-schema f)]))
(->graph g))
profile-key (plumbing/fnk [] (atom {})))))
prismatic-plumbing-clojure-0.5.4/src/plumbing/graph/ 0000755 0000000 0000000 00000000000 13141156451 0022473 5 ustar 00root root 0000000 0000000 prismatic-plumbing-clojure-0.5.4/src/plumbing/graph/positional.clj 0000644 0000000 0000000 00000007254 13141156451 0025356 0 ustar 00root root 0000000 0000000 (ns plumbing.graph.positional
"A compilation method for graphs that avoids maps for speed."
(:use plumbing.core)
(:require
[schema.core :as s]
[plumbing.fnk.schema :as schema]
[plumbing.fnk.pfnk :as pfnk]
[plumbing.fnk.impl :as fnk-impl])
(:import
clojure.lang.IFn))
(defn def-graph-record
"Define a record for the output of a graph. It is usable as a function to be
as close to a map as possible. Return the typename."
([g] (def-graph-record g (gensym "graph-record")))
([g record-type-name]
;; NOTE: This eval is needed because we want to define a record based on
;; information (a graph) that's only available at runtime.
(eval `(defrecord ~record-type-name ~(->> g
pfnk/output-schema
keys
(mapv (comp symbol name)))
IFn
(invoke [this# k#]
(get this# k#))
(invoke [this# k# not-found#]
(get this# k# not-found#))
(applyTo [this# args#]
(apply get this# args#))))
record-type-name))
(defn graph-let-bindings
"Compute the bindings for functions and intermediates needed to form the body
of a positional graph, E.g.
[`[[f-3 ~some-function]] `[[intermediate-3 (f-3 intermediate-1 intermediate-2)]]]"
[g g-value-syms]
(->> g
(map (fn [[kw f]]
(let [f-sym (-> kw name (str "-fn") gensym)
arg-forms (map-from-keys g-value-syms (pfnk/input-schema-keys f))
[f arg-forms] (fnk-impl/efficient-call-forms f arg-forms)]
[[f-sym f] [(g-value-syms kw) (cons f-sym arg-forms)]])))
(apply map vector)))
(defn eval-bound
"Evaluate a form with some symbols bound to some values."
[form bindings]
((eval `(fn [~(mapv first bindings)] ~form))
(map second bindings)))
(defn graph-form
"Construct [body-form bindings-needed-for-eval] for a positional graph."
[g arg-keywords]
(let [value-syms (->> g
pfnk/io-schemata
(mapcat schema/explicit-schema-key-map)
(map key)
(map-from-keys (comp gensym name)))
[needed-bindings value-bindings] (graph-let-bindings g value-syms)
record-type (def-graph-record g)]
[`(fn
positional-graph# ;; Name it just for kicks.
~(mapv value-syms arg-keywords)
(let ~(vec (apply concat value-bindings))
(new ~record-type ~@(->> g pfnk/output-schema keys (mapv value-syms)))))
needed-bindings]))
(defn positional-flat-compile
"Positional compile for a flat (non-nested) graph."
[g]
(let [arg-ks (->> g pfnk/input-schema-keys)
[positional-fn-form eval-bindings] (graph-form g arg-ks)
input-schema (pfnk/input-schema g)
pos-fn-sym (gensym "pos")
input-schema-sym (gensym "input-schema")
output-schema-sym (gensym "output-schema")]
(vary-meta ;; workaround evaluation quirks
(eval-bound
`(let [~pos-fn-sym ~positional-fn-form]
~(fnk-impl/positional-fnk-form
(fnk-impl/schema-override 'graph-positional output-schema-sym)
input-schema-sym
(vec (schema/explicit-schema-key-map input-schema))
(into {} (for [k (keys (schema/explicit-schema-key-map input-schema))] [k (symbol (name k))]))
(list `(~pos-fn-sym ~@(mapv (comp symbol name) arg-ks)))
nil))
(into eval-bindings
[[input-schema-sym input-schema]
[output-schema-sym (pfnk/output-schema g)]]))
assoc :schema (let [[is os] (pfnk/io-schemata g)] (s/=> os is)))))
prismatic-plumbing-clojure-0.5.4/src/plumbing/graph_async.clj 0000644 0000000 0000000 00000006335 13141156451 0024371 0 ustar 00root root 0000000 0000000 (ns plumbing.graph-async
(:require
[clojure.core.async :as async :refer [go !]]
[clojure.core.async.impl.protocols :as async-protocols]
[plumbing.fnk.pfnk :as pfnk]
[plumbing.fnk.schema :as schema :include-macros true]
[plumbing.core :as plumbing :include-macros true]
[plumbing.graph :as graph :include-macros true]))
(defn asyncify
"Take a fnk f and return an async version by wrapping non-channel
return values in a channel"
[f]
(pfnk/fn->fnk
(fn [m]
(let [v (f m)]
(if (satisfies? async-protocols/ReadPort v)
v
(go v))))
(pfnk/io-schemata f)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Public
(defn async-compile
"Experimental.
Compile a hierarchical graph with (some) async fnks into an channel that
contains the computed graph once completed.
Each fnk can perform async operations by returning a channel that contains
its node value once completed.
Each node function will be evaluated as its dependencies have been fully
computed."
[g]
(if (fn? g)
(asyncify g)
(let [g (graph/->graph (plumbing/map-vals async-compile g))
req-ks (schema/required-toplevel-keys (pfnk/input-schema g))
edges (concat
(for [[k v] g
parent-k (filter g (pfnk/input-schema-keys v))]
[parent-k k])
(for [k (keys g)]
[k ::done]))
child-map (->> edges
(group-by first)
(plumbing/map-vals #(set (map second %))))
parent-map (->> edges
(group-by second)
(plumbing/map-vals #(set (map first %))))]
(pfnk/fn->fnk
(fn [m]
(let [missing-keys (seq (remove #(contains? m %) req-ks))]
(schema/assert-iae (empty? missing-keys)
"Missing top-level keys in graph input: %s"
(set missing-keys)))
(let [result (async/chan)
remaining-parents (atom parent-map)
results (atom m)
run-node (fn run-node [k]
(go
(if (= ::done k)
(>! result (select-keys @results (keys g)))
(let [f (g k)
r (