PageRenderTime 56ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/src/trammel/core.clj

https://github.com/tavisrudd/trammel
Clojure | 367 lines | 295 code | 56 blank | 16 comment | 23 complexity | 60850d82b6dde30f990e543f50b3738d MD5 | raw file
  1. ;; trammel.clj -- Contracts programming library for Clojure
  2. ;; by Michael Fogus - <http://fogus.me/fun/trammel>
  3. ;; May 26, 2010
  4. ; Copyright (c) Michael Fogus, 2010. All rights reserved. The use
  5. ; and distribution terms for this software are covered by the Eclipse
  6. ; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
  7. ; which can be found in the file COPYING the root of this
  8. ; distribution. By using this software in any fashion, you are
  9. ; agreeing to be bound by the terms of this license. You must not
  10. ; remove this notice, or any other, from this software.
  11. (ns trammel.core
  12. "The core contracts programming functions and macros for Trammel."
  13. (:use [trammel.funcify :only (funcify)]
  14. [trammel.factors]
  15. [trammel.utils])
  16. (:require [fogus.thneed.utils :as fogus]))
  17. ;; HOF support
  18. (defrecord HOC [args argspec ctx])
  19. (defmacro _ [args & argspec]
  20. `(HOC. '~args (vec '~argspec) nil))
  21. (comment
  22. (_ [n] even? number? => number?)
  23. )
  24. ;; # base functions and macros
  25. (defn- build-pre-post-map
  26. "Takes a vector of the form `[pre ... => post ...]` and infers the expectations described
  27. therein. The map that comes out will look like Clojure's default pre- anlein d post-conditions
  28. map. If the argument is already a map then it's assumed that the default pre/post map is used and
  29. as a result is used directly without manipulation.
  30. "
  31. [cnstr]
  32. (if (vector? cnstr)
  33. (let [[L M R] (partition-by #{'=>} cnstr)]
  34. {:pre (when (not= L '(=>)) L)
  35. :post (if (= L '(=>)) M R)})
  36. cnstr))
  37. (defn tag-hocs [args cnstr]
  38. nil)
  39. (defn- build-constraints-map
  40. "Takes the corresponding arglist and a vector of the contract expectations, the latter of which looks
  41. like any of the following:
  42. [(= 0 _)] or [number?] ;; only the pre-
  43. [number? => number?] ;; a pre- and post-
  44. [=> number?] ;; only a post-
  45. [foo bar => baz] ;; 2 pre- and 1 post-
  46. It then takes this form and builds a pre- and post-condition map of the form:
  47. {:pre [(foo x) (bar x)]
  48. :post [(baz %)]}
  49. "
  50. [args cnstr]
  51. (let [hocs (tag-hocs args cnstr)]
  52. [args
  53. (->> (build-pre-post-map cnstr)
  54. (fogus/manip-map (partial funcify '[%]) [:post])
  55. (fogus/manip-map (partial funcify args) [:pre]))]))
  56. (comment
  57. (let [hoc (_ [n] even? number? => number?)]
  58. (build-contract 'hof (build-constraints-map (:args hoc) (:argspec hoc))))
  59. (macroexpand '(contract my-map "mymap" [fun sq] [(_ [n] number? => number?) (seq sq) => seq]))
  60. (contract my-map "mymap" [fun sq] [(_ [n] number? => number?) (seq sq) => seq])
  61. )
  62. (defn- build-contract
  63. "Expects a seq representing an arity-based expectation of the form:
  64. [[x] {:pre [(foo x)] :post [(bar %)]}]
  65. It then uses this data to build another list reprsenting a specific arity body
  66. for a higher-order function with attached pre- and post-conditions that directly
  67. calls the function passed in:
  68. ([f x] {:pre [(foo x)] :post [(bar %)]} (f x))
  69. However, the picture is slightly more compilcated than that because Clojure does
  70. not have disparate pre-/post-conditions. Therefore, it's on me to provide a
  71. slightly more crystaline picture of the condition failure when it occurs. As a
  72. result the body of the contract is interwoven with `try`/`catch` blocks to catch
  73. and examine the contents of `AssertionErrors` and based on context rethrow them
  74. with more information. At the moment this information only takes the form of a
  75. richer assertion message.
  76. "
  77. [message cnstr]
  78. (let [[args pre-post-map] cnstr]
  79. `(~(into '[f] args)
  80. (let [ret# (try
  81. ((fn []
  82. ~(select-keys pre-post-map [:pre])
  83. ~(list* 'f (mapcat (fn [item]
  84. (cond (symbol? item) [item]
  85. (map? item) [(:as item)]
  86. :else [item]))
  87. args))))
  88. (catch AssertionError pre#
  89. (throw (AssertionError. (str "Pre-condition failure: " ~message \newline (.getMessage pre#))))))]
  90. (try
  91. ((fn []
  92. ~(select-keys pre-post-map [:post])
  93. ret#))
  94. (catch AssertionError post#
  95. (throw (AssertionError. (str "Post-condition failure: " ~message \newline (.getMessage post#))))))))))
  96. (defmacro contract
  97. "The base contract form returning a higher-order function that can then be partially
  98. applied to an existing function to 'apply' a contract. Take for example a simple
  99. contract that describes an expectation for a function that simply takes one or two
  100. numbers and returns the double:
  101. (def doubler-contract
  102. (contract doubler
  103. Ensures that when given a number,
  104. the result is doubled.
  105. [x] [number? => (= (* 2 x) %)]
  106. [x y] [(every? number? [x y])
  107. =>
  108. (= (* 2 (+ x y)) %)]))
  109. You can then partially apply this contract with an existing function:
  110. (def doubler
  111. (partial doubler-contract
  112. #(* 2 %)))
  113. (def bad-doubler
  114. (partial doubler-contract
  115. #(* 3 %)))
  116. And then running these functions will be checked against the contract at runtime:
  117. (doubler 2)
  118. ;=> 4
  119. (bad-doubler 2)
  120. ; java.lang.AssertionError:
  121. ; Assert failed: (= (* 2 x) %)
  122. Similar results would occur for the 2-arity versions of `doubler` and `bad-doubler`.
  123. While it's fine to use `partial` directly, it's better to use the `with-constraints` function
  124. found in this same library.
  125. If you're so inclined, you can inspect the terms of the contract via its metadata, keyed on
  126. the keyword `:constraints`.
  127. "
  128. [n docstring & constraints]
  129. (if (not (string? docstring))
  130. (throw (IllegalArgumentException. "Sorry, but contracts require docstrings"))
  131. (let [raw-cnstr (partition 2 constraints)
  132. arity-cnstr (for [[a c] raw-cnstr]
  133. (build-constraints-map a c))
  134. fn-arities (for [b arity-cnstr]
  135. (build-contract docstring b))
  136. body (list* 'fn n fn-arities)]
  137. `(with-meta
  138. ~body
  139. {:constraints (into {} '~arity-cnstr)
  140. :docstring ~docstring}))))
  141. (defn with-constraints
  142. "A contract combinator.
  143. Takes a target function and a number of contracts and returns a function with the contracts
  144. applied to the original. This is the preferred way to apply a contract previously created
  145. using `contract` as the use of `partial` may not work as implementation details change.
  146. "
  147. ([f] f)
  148. ([f c] (partial c f))
  149. ([f c & more]
  150. (apply with-constraints (with-constraints f c) more)))
  151. (defmacro defcontract
  152. "Convenience macro for defining a named contract. Equivalent to `(def fc (contract ...))`"
  153. [name docstring & forms]
  154. `(def ~name
  155. (contract ~(symbol (str name "-impl")) ~docstring ~@forms)))
  156. (defmacro defconstrainedfn
  157. "Defines a function using the `contract` vector appearing after the arguments.
  158. (defconstrainedfn sqr
  159. [n] [number? (not= 0 n) => pos? number?]
  160. (* n n))
  161. Like the `contract` macro, multiple arity functions can be defined where each argument vector
  162. is immediately followed by the relevent arity expectations. This macro will also detect
  163. if a map is in that constraints position and use that instead under the assumption that
  164. Clojure's `:pre`/`:post` map is used instead.
  165. "
  166. [name & body]
  167. (let [mdata (if (string? (first body))
  168. {:doc (first body)}
  169. {})
  170. body (if (:doc mdata)
  171. (next body)
  172. body)
  173. body (if (vector? (first body))
  174. (list body)
  175. body)
  176. body (for [[args cnstr & bd] body]
  177. (list* args
  178. (if (vector? cnstr)
  179. (second (build-constraints-map args cnstr))
  180. cnstr)
  181. bd))]
  182. `(defn ~name
  183. ~(str (:doc mdata))
  184. ~@body)))
  185. ; clojure/core_deftype.clj
  186. (defn- build-positional-factory
  187. "Used to build a positional factory for a given type/record. Because of the
  188. limitation of 20 arguments to Clojure functions, this factory needs to be
  189. constructed to deal with more arguments. It does this by building a straight
  190. forward type/record ctor call in the <=20 case, and a call to the same
  191. ctor pulling the extra args out of the & overage parameter. Finally, the
  192. arity is constrained to the number of expected fields and an ArityException
  193. will be thrown at runtime if the actual arg count does not match."
  194. [nom classname fields invariants chk]
  195. (let [fn-name (symbol (str '-> nom))
  196. [field-args over] (split-at 20 fields)
  197. field-count (count fields)
  198. arg-count (count field-args)
  199. over-count (count over)]
  200. `(defconstrainedfn ~fn-name
  201. [~@field-args ~@(if (seq over) '[& overage] [])]
  202. ~invariants
  203. (with-meta
  204. ~(if (seq over)
  205. `(if (= (count ~'overage) ~over-count)
  206. (new ~nom
  207. ~@field-args
  208. ~@(for [i (range 0 (count over))]
  209. (list `nth 'overage i)))
  210. (throw (clojure.lang.ArityException. (+ ~arg-count (count ~'overage)) (name '~fn-name))))
  211. `(new ~nom ~@field-args))
  212. {:contract ~chk}))))
  213. (defmacro defconstrainedrecord
  214. [name slots inv-description invariants & etc]
  215. (let [fields (vec slots)
  216. ns-part (namespace-munge *ns*)
  217. classname (symbol (str ns-part "." name))
  218. ctor-name (symbol (str name \.))
  219. positional-factory-name (symbol (str "->" name))
  220. map-arrow-factory-name (symbol (str "map->" name))
  221. chk `(contract ~(symbol (str "chk-" name))
  222. ~inv-description
  223. [{:keys ~fields :as m#}]
  224. ~invariants)]
  225. `(do
  226. (let [t# (defrecord ~name ~fields ~@etc)]
  227. (defn ~(symbol (str name \?)) [r#]
  228. (= t# (type r#))))
  229. ~(build-positional-factory name classname fields invariants chk)
  230. (defconstrainedfn ~map-arrow-factory-name
  231. ([{:keys ~fields :as m#}]
  232. ~invariants
  233. (with-meta
  234. (merge (new ~name ~@(for [e fields] nil)) m#)
  235. {:contract ~chk})))
  236. ~name)))
  237. (defn- apply-contract
  238. [f]
  239. (if (:hooked (meta f))
  240. f
  241. (with-meta
  242. (fn [m & args]
  243. (if-let [contract (-> m meta :contract)]
  244. ((partial contract identity) (apply f m args))
  245. (apply f m args)))
  246. {:hooked true})))
  247. (when *assert*
  248. (alter-var-root (var assoc) apply-contract)
  249. (alter-var-root (var dissoc) apply-contract)
  250. (alter-var-root (var merge) apply-contract)
  251. (alter-var-root (var merge-with) (fn [f] (let [mw (apply-contract f)] (fn [f & maps] (apply mw f maps)))))
  252. (alter-var-root (var into) apply-contract)
  253. (alter-var-root (var conj) apply-contract)
  254. (alter-var-root (var assoc-in) apply-contract)
  255. (alter-var-root (var update-in) apply-contract))
  256. (defmacro defconstrainedtype
  257. [name slots inv-description invariants & etc]
  258. (check-args! name slots inv-description invariants)
  259. (let [fields (vec slots)
  260. ctor-name (symbol (str name \.))
  261. factory-name (symbol (str "->" name))]
  262. `(do
  263. (let [t# (deftype ~name ~fields ~@etc)]
  264. (defn ~(symbol (str name \?)) [r#]
  265. (= t# (type r#))))
  266. (let [chk# (contract ~(symbol (str "chk-" name))
  267. ~inv-description
  268. [{:keys ~fields :as m#}] ~invariants)]
  269. (defconstrainedfn ~factory-name
  270. (~fields ~invariants
  271. (~ctor-name ~@fields))))
  272. ~name)))
  273. (defmacro defconstrainedvar
  274. [name init inv-description invariants]
  275. `(do
  276. (def ~name ~init)
  277. (set-validator! (var ~name) (partial (contract ~(symbol (str "chk-" name))
  278. ~inv-description
  279. [~name]
  280. ~invariants)
  281. (fn [x#] true)))))
  282. (defmacro constrained-atom
  283. [init inv-description invariants]
  284. `(do
  285. (let [r# (atom ~init)]
  286. (set-validator! r# (partial (contract ~(symbol (str "chk-atom" ))
  287. ~inv-description
  288. [the-atom#]
  289. ~invariants)
  290. (fn [x#] true)))
  291. r#)))
  292. (defmacro constrained-ref
  293. [init inv-description invariants]
  294. `(do
  295. (let [r# (ref ~init)]
  296. (set-validator! r# (partial (contract ~(symbol (str "chk-ref" ))
  297. ~inv-description
  298. [the-ref#]
  299. ~invariants)
  300. (fn [x#] true)))
  301. r#)))
  302. (defmacro constrained-agent
  303. [init inv-description invariants]
  304. `(do
  305. (let [r# (agent ~init)]
  306. (set-validator! r# (partial (contract ~(symbol (str "chk-agent" ))
  307. ~inv-description
  308. [the-agent#]
  309. ~invariants)
  310. (fn [x#] true)))
  311. r#)))