├── .gitignore ├── .travis.yml ├── README.md ├── project.clj ├── src ├── poppea.clj └── poppea │ └── core.clj └── test └── poppea └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: lein2 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # poppea 2 | 3 | A couple of macros that do useful, general things. 4 | 5 | [![Build Status](https://travis-ci.org/JulianBirch/poppea.svg?branch=master)](https://travis-ci.org/JulianBirch/poppea) 6 | 7 | ## Usage 8 | 9 | [![Leiningen version](http://clojars.org/net.colourcoding/poppea/latest-version.svg)](http://clojars.org/net.colourcoding/poppea) 10 | 11 | Namespace: 12 | 13 | ```clj 14 | (:require 15 | [poppea :refer :all]) 16 | ``` 17 | 18 | ## document-partial 19 | 20 | `document-partial` does the same thing as `partial`, except that 21 | rather than returning a function, it returns a record that does 22 | the same thing and (importantly) is serializable. 23 | 24 | ```clj 25 | (defn f [a b c d] (+ a b c d)) 26 | 27 | (document-partial f 1 2) 28 | => #poppea.DocumentedPartial{:poppea/function #'user/f, :b 2, :a 1} 29 | 30 | ((document-partial f 1 2) 3 4) 31 | => 10 32 | ``` 33 | 34 | It's a proper data structure, so you can do things like this: 35 | 36 | ```clj 37 | ((assoc (document-partial f 1 2) :a 5) 3 4) 38 | => 14 39 | ``` 40 | 41 | Note that this provides a fairly elegant way of serializing 42 | anonymous functions: rewrite to a var function and then use 43 | document-partial. 44 | 45 | `document-partial` is a macro, so can't be used in all of the places partial can be. 46 | * The function has to be a symbol referring to a var. This is nearly always how `partial` is used anyway. 47 | * It's not recursive, so `(document-partial comp identity)` won't perform `document-partial` on identity. Which is actually a good thing in practice. 48 | * Nesting `document-partial` within an anonymous function works, but is confusing. I don't recommend doing it, especially considering that it should be considered an alternative to anonymous functions. 49 | 50 | ### document-partial-% 51 | 52 | If you need to insert arguments into the middle of functions, you can call document-partial-%. 53 | 54 | ```clj 55 | ((document-partial-% / % 2) 6) 56 | ;;; 6 57 | 58 | ((document-partial-% list %2 :x) 1 2 3 4) 59 | ;;; (2 :x 3 4) 60 | ``` 61 | 62 | Note that you can't put % inside a subexpression or document-partial-% inside a lambda function and expect nice things to happen. 63 | 64 | ### Easter eggs 65 | 66 | `defrecord-get` is the same as `defrecord` but implements acts as 67 | a function the same way a `hash-map` does. `defrecord-fn` is a 68 | helper function for records that implement `IFn`. 69 | 70 | ## Currying 71 | 72 | Poppea implements very basic ML-style currying in three closely related macros. defn-curried, defn-curried- and fn-curried 73 | 74 | ```clj 75 | (defn-curried wrap-handler [handler request] 76 | (assoc (handler request) :x 3)) 77 | ``` 78 | 79 | is the same as 80 | 81 | ```clj 82 | (defn wrap-handler 83 | ([handler request] 84 | (assoc (handler request) :x 3)) 85 | ([handler] 86 | (fn [request] (assoc (handler request) :x 3)))) 87 | ``` 88 | 89 | It should be pointed out that there are a couple of practical issues of which one should be aware when using defn-curried. 90 | 91 | * Since it's not returning a var, reloading on the repl won't change a curried function. 92 | * Be careful using currying with -> and ->>. The argument order may not be what you expect. Clojure doesn't have ML's rich operators. 93 | 94 | This is a valid alternative to over-long anonymous functions, and 95 | in my opinion vastly preferable to functions that just return 96 | other functions. 97 | 98 | ### Performance 99 | 100 | `defn-curried` is marginally faster than `partial`. `document-partial` is significantly slower. It's very rare that this is material in either direction. 101 | 102 | ### Road Map 103 | 104 | I'd like to implement a version that does eager evaluation of the code that only depends on the curried arguments, but it's much more complex than what has currently been implemented. 105 | 106 | It'd also be nice to have a generator macro, which rewrites imperative style code as a reducer. Turning a reducer into a lazy sequence shoudn't be problematic. 107 | 108 | ## quick-map 109 | 110 | The other macro is coffee-map, which is inspired by Coffeescript's quick map syntax. 111 | 112 | ```clj 113 | (let [x 3 114 | y 5] 115 | (coffee-map x y "a" 78)) 116 | ``` 117 | 118 | is the same as 119 | 120 | ```clj 121 | (hash-map :x 3 :y 5 "a" 78) 122 | ``` 123 | 124 | Basically, this is structuring, the complement of Clojures {:keys [x y]} destructuring. 125 | 126 | ## Why is it called Poppea? 127 | 128 | [Listen](http://www.youtube.com/watch?v=ijDi-2RADX0) 129 | 130 | ## License 131 | 132 | Copyright © 2013 Julian Birch 133 | 134 | Distributed under the Eclipse Public License, the same as Clojure. 135 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject net.colourcoding/poppea "0.2.1" 2 | :min-lein-version "2.0.0" 3 | :description "Useful macros for Clojure" 4 | :url "http://github.com/JulianBirch/poppea" 5 | :license {:name "Eclipse Public License" 6 | :url "http://www.eclipse.org/legal/epl-v10.html"} 7 | :dependencies [[org.clojure/clojure "1.5.0"] 8 | [org.clojure/tools.reader "1.0.0"]] 9 | :global-vars { *warn-on-reflection* true }) 10 | -------------------------------------------------------------------------------- /src/poppea.clj: -------------------------------------------------------------------------------- 1 | (ns poppea 2 | (:require [clojure.string :as s] 3 | [clojure.tools.reader.edn :as edn])) 4 | 5 | (defn- curry 6 | [[params1 params2] body] 7 | (cons (vec params1) 8 | (if (empty? params2) 9 | body 10 | (list (apply list 'fn (vec params2) body))))) 11 | 12 | (defn- do-curried [symbol to-fn params] 13 | (let [result (split-with (complement vector?) params) 14 | [[name doc meta] [args & body]] result 15 | [doc meta] (if (string? doc) [doc meta] [nil doc]) 16 | body (if meta (cons meta body) body) 17 | arity-for-n #(-> % inc (split-at args) (to-fn body)) 18 | arities (->> 19 | (range 0 (count args)) 20 | (map arity-for-n) 21 | reverse) 22 | before (keep identity [symbol name doc])] 23 | (concat before arities))) 24 | 25 | (defmacro defn-curried 26 | "Builds a multiple arity function similar that returns closures 27 | for the missing parameters, similar to ML's behaviour." 28 | [& params] 29 | (do-curried 'defn curry params)) 30 | 31 | (defmacro defn-curried- 32 | "Builds a multiple arity function similar that returns closures 33 | for the missing parameters, similar to ML's behaviour." 34 | [& params] 35 | (do-curried 'defn- curry params)) 36 | 37 | (defmacro fn-curried 38 | "Builds a multiple arity function similar that returns closures 39 | for the missing parameters, similar to ML's behaviour." 40 | [& params] 41 | (do-curried 'fn curry params)) 42 | 43 | (defn- coffee-map-fn [keys] 44 | (if (empty? keys) 45 | [] 46 | (let [k (first keys)] 47 | (if (symbol? k) 48 | (->> keys next coffee-map-fn (cons k) (cons (keyword k))) 49 | (->> keys next next coffee-map-fn 50 | (cons (second keys)) 51 | (cons k)))))) 52 | 53 | (defmacro coffee-map [& keys] 54 | (cons 'hash-map (coffee-map-fn `~keys))) 55 | 56 | (defn extract-symbols [index lhs] 57 | (cond (symbol? lhs) lhs 58 | (vector? lhs) index 59 | (map? lhs) (or (:as lhs) index) 60 | :else index)) 61 | 62 | (defn- binding-symbols [binding-clause] 63 | (doall 64 | (map #(if (symbol? %) (keyword (name %)) %) 65 | (map-indexed extract-symbols binding-clause)))) 66 | 67 | (defn binding-symbols-for-var [function] 68 | (->> function meta :arglists 69 | (sort-by (comp - count)) 70 | first binding-symbols)) 71 | 72 | (defn parameter-capture? [param] 73 | (and (symbol? param) 74 | (re-find #"^%" (name param)))) 75 | 76 | (defn bound-index [param] 77 | (if (parameter-capture? param) 78 | (let [s (.substring (name param) 1) 79 | a (edn/read-string s)] 80 | (cond (number? a) (dec a) 81 | (= (count s) 0) 0)))) 82 | 83 | (defn-curried include-% [params bound index] 84 | (if index 85 | (nth params index) 86 | bound)) 87 | 88 | (defn bound-params [this] 89 | (->> (::function this) 90 | binding-symbols-for-var 91 | (map #(get this % ::missing)) 92 | (remove #(= % ::missing)))) 93 | 94 | (defn partial-invoke-% [this & params] 95 | (let [bound-params (bound-params this) 96 | indexes (apply vector (map bound-index bound-params)) 97 | non-nil-indexes (remove nil? indexes) 98 | c (if (empty? non-nil-indexes) 99 | 0 100 | (inc (apply max non-nil-indexes)))] 101 | (apply (::function this) 102 | (concat (map (include-% params) bound-params indexes) 103 | (drop c params))))) 104 | 105 | (defn partial-invoke [this & params] 106 | (apply (::function this) 107 | (concat (bound-params this) params))) 108 | 109 | ;;; Don't use this, it's just used to implement defrecord-get 110 | (defn record-lookup [this & path] (get-in this path nil)) 111 | 112 | (defmacro defrecord-fn [function-symbol & definition] 113 | (let [invoke (symbol "invoke") 114 | applyTo (symbol "applyTo")] 115 | (concat ['defrecord] definition 116 | `(clojure.lang.IFn 117 | (~invoke [this#] 118 | (~function-symbol this#)) 119 | (~invoke [this# a#] 120 | (~function-symbol this# a#)) 121 | (~invoke [this# a# b#] 122 | (~function-symbol this# a# b#)) 123 | (~invoke [this# a# b# c#] 124 | (~function-symbol this# a# b# c#)) 125 | (~invoke [this# a# b# c# d#] 126 | (~function-symbol this# a# b# c# d#)) 127 | (~invoke [this# a# b# c# d# e#] 128 | (~function-symbol this# a# b# c# d# e#)) 129 | (~applyTo [this# args#] 130 | (clojure.lang.AFn/applyToHelper this# args#)))))) 131 | 132 | (defrecord-fn partial-invoke-% 133 | DocumentedPartialArg []) 134 | (defrecord-fn partial-invoke 135 | DocumentedPartial []) 136 | 137 | (defn document-partial-map [symbol process params] 138 | (assert (not (nil? (resolve `~symbol))) 139 | (str "Could not resolve " symbol)) 140 | (let [function (resolve `~symbol)] 141 | `(hash-map 142 | ::function ~function 143 | ~@(interleave (binding-symbols-for-var function) 144 | (map process params))))) 145 | 146 | (defn capture-% [s] 147 | (if (parameter-capture? s) `'~s s)) 148 | 149 | (defmacro document-partial-% [symbol & params] 150 | `(map->DocumentedPartialArg 151 | ~(document-partial-map symbol capture-% params))) 152 | 153 | (defmacro document-partial [symbol & params] 154 | `(map->DocumentedPartial 155 | ~(document-partial-map symbol identity params))) 156 | 157 | (defmacro defrecord-get [& definition] 158 | `(defrecord-fn record-lookup ~@definition)) 159 | 160 | (defn at [yield] 161 | (fn ([] (println "Arity 0") (yield)) 162 | ([x] (println "Arity 1") (yield x)) 163 | ([r x] (println "Arity 2") 164 | (def p0 yield) 165 | (def p1 r) 166 | (def p2 x) 167 | (yield r x)))) 168 | -------------------------------------------------------------------------------- /src/poppea/core.clj: -------------------------------------------------------------------------------- 1 | (ns poppea.core) 2 | 3 | (defn foo 4 | "I don't do a whole lot." 5 | [x] 6 | (println x "Hello, World!")) 7 | -------------------------------------------------------------------------------- /test/poppea/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns poppea.core-test 2 | (:use clojure.test 3 | poppea 4 | [clojure.string :as s])) 5 | 6 | (def a 4) 7 | (def b 5) 8 | 9 | (deftest quick-map-test 10 | (is (= {:a 4 :b 5 :c 3} 11 | (coffee-map a b :c 3)))) 12 | 13 | (defn-curried add [x y] (+ x y)) 14 | 15 | (deftest currying 16 | (is (= 7 17 | ((add 3) 4)))) 18 | 19 | (defn f [a b] (a b)) 20 | 21 | (deftest document-partials 22 | (is (= count (:a (let [c count] (document-partial f c)))) 23 | "Binds correctly to local bindings.") 24 | (is (document-partial string?) 25 | "Should handle partials with no parameters.") 26 | (is (= 3 ((document-partial count) [:a :b :c]))) 27 | (is (= 5 ((document-partial + 2 1) 2))) 28 | (is (= 3 ((document-partial-% count) [:a :b :c]))) 29 | (is (= 5 ((document-partial-% + 2 1) 2))) 30 | (is ((document-partial-% < % 4) 3)) 31 | (is (= [2 7 3 4] 32 | ((document-partial-% list %2 7) 1 2 3 4))) 33 | (is (not ((document-partial-% < % 4) 4)))) 34 | 35 | (deftest symbol-resolution 36 | (is (document-partial s/blank?) 37 | "Should handle aliases.")) 38 | --------------------------------------------------------------------------------