├── .gitignore ├── README.md ├── deps.edn ├── src ├── data_readers.cljc └── perc │ ├── core.cljc │ └── core.cljs └── test └── perc └── simple_test.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache/ 2 | .DS_Store 3 | .main.js 4 | package-lock.json 5 | /out/ 6 | /node_modules/ 7 | cljs-test-runner-out 8 | .calva 9 | .clj-kondo 10 | .lsp 11 | .nrepl-port -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `#%` _(`perc`)_ 2 | ============================= 3 | 4 | `perc` is a low-`char`b, keto-friendly function syntax sweetener for helping humans not say what does not need to be said. 5 | 6 | # What is it? 7 | 8 | Syntactically, `perc`s are very similar to Clojure's anonymous function syntax `#(Point. %1 %2 %3)` 9 | 10 | However, in addition to these indexical references, `perc`s allow associative references by keywords, like so: 11 | 12 | ```clojure 13 | #%(Point. %:x %:y %:z) 14 | ``` 15 | 16 | Sure, you've gotta add this one character after the hash - `%` - but the extra sweetness you get out of the other end is totally worth the squeeze: `%:the-name-you-already-gave-it` 17 | 18 | # Getting Started 19 | 20 | Place the following in the `:deps` map of your `deps.edn` file: 21 | 22 | ```clojure 23 | ... 24 | johnmn3/perc {:git/url "https://github.com/johnmn3/perc" 25 | :sha "4b8689986af25c7adb5935006cae15d190b307ce"} 26 | ... 27 | ``` 28 | 29 | If you want to test things out _right now_, from the comfort of your own `~/home`, go ahead and drop this in your bash pipe and smoke it: 30 | 31 | ```clojure 32 | clj -Sdeps '{:deps {johnmn3/perc {:git/url "https://github.com/johnmn3/perc" :sha "4b8689986af25c7adb5935006cae15d190b307ce"}}}' -m cljs.main -c perc.core -re node -r 33 | ``` 34 | 35 | Then you should be able to test things out right away: 36 | 37 | ```clojure 38 | Cloning: https://github.com/johnmn3/perc 39 | Checking out: https://github.com/johnmn3/perc at 1c7e1d63aae9b2e59087ffc7774f6520b34e4c26 40 | ClojureScript 1.10.520 41 | cljs.user=> (#%(println "hi" %:x) {:x 1}) 42 | hi 1 43 | nil 44 | ``` 45 | 46 | In ClojureScript, once a project is launched, you don't need to require anything because tagged literals work globally. In Clojure, you'll have to require `perc.core` in one of your project's namespaces. 47 | 48 | # Overview 49 | 50 | So, starting off from the basics, you know how Clojure has anonymous functions: 51 | 52 | ```clojure 53 | ((fn [{:keys [x y z]}] [(inc x) (inc y) (inc z)]) 54 | {:x 1 :y 2 :z 3}) ; => [2 3 4] 55 | ``` 56 | 57 | And you also know how Clojure gives you a sugared syntax for that: 58 | 59 | ```clojure 60 | (#(do [(inc (:x %)) (inc (:y %)) (inc (:z %))]) 61 | {:x 1 :y 2 :z 3}) ; => [2 3 4] 62 | ``` 63 | 64 | Well, with `perc`s, you can instead refer to `(:x %)` as just `%:x`. We refer to these compound references with `%` as [path expressions](#basic-path-expressions), which act like a `get-in` into the passed in parameters, but they mostly still act how you'd expect them to with regard to the behavior of Clojure's existing sugared anonymous function syntax, like with `%`, `%1`, `%2`, `%&`, etc. So you can mostly use them as a drop in replacement. 65 | 66 | So with [path expressions](#basic-path-expressions) you can do [named parameters](#named-anonymous-parameters) (`%:x`, `%1:x`), [namespaced named params](#namespaced-anonymous-parameters) (`%:*/x`, `%:*s/valid?`), [keyword args](#keyword-arguments) (`%&:logging?`), or even a nested path like `%:x:y:z` (which becomes something like `(get-in % [:x :y :z])`) among others which we'll go over in the [details](#details) section below. 67 | 68 | To use these [path expressions](#basic-path-expressions), all you have to do is, right before your function form, add a percent symbol (`%`) to the right of what would be the anoymous function's hash symbol. Like: 69 | 70 | ```clojure 71 | (#%(do [(inc %:x) (inc %:y) (inc %:z)]) 72 | {:x 1 :y 2 :z 3}) ; => [2 3 4] 73 | ``` 74 | 75 | Notice that we're returning a vector here - as such, we can instead lean on [return literals](#return-literals) to elide the evaluation of the list expression altogether, like so: 76 | 77 | ```clojure 78 | (#%[(inc %:x) (inc %:y) (inc %:z)] 79 | {:x 1 :y 2 :z 3}) ; => [2 3 4] 80 | ``` 81 | 82 | And if we're dealing with nested values, we can drill into the data at any depth using the concatenatability of [path expressions](#advanced-path-expressions): 83 | 84 | ```clojure 85 | (#%[(inc %:x:val) (inc %:y:val) (inc %:z:val)] 86 | {:x {:val 1} :y {:val 2} :z {:val 3}}) ; => [2 3 4] 87 | ``` 88 | 89 | When we're [mapping or reducing](#mapping-reducing) over a collection of maps, it can be quite eloquent: 90 | 91 | ```clojure 92 | (->> [{:a 5 :b {:c 6}} 93 | {:a 7 :b {:c 8}} 94 | {:a 9 :b {:c 10}}] 95 | 96 | (mapv #%{:a (inc %:a) 97 | :b {:c (dec %:b:c)}}) 98 | 99 | (reduce #%{:a (+ %1:a %2:a) 100 | :b {:c (+ %1:b:c %2:b:c)}})) ; => {:a 24, :b {:c 21}} 101 | ``` 102 | 103 | Take a moment to reflect on the above expression's effectiveness in conveying only that which needs to be said and nothing more. 104 | 105 | # Details 106 | 107 | - [What is it?](#what-is-it?) 108 | - [Getting Started](#getting-started) 109 | - [Overview](#overview) 110 | - [Details](#details) 111 | - [Basic Path Expressions](#basic-path-expressions) 112 | - [Named Anonymous Parameters](#named-anonymous-parameters) 113 | - [Namespaced Anonymous Parameters](#namespaced-anonymous-parameters) 114 | - [Return Literals](#return-literals) 115 | - [Advanced Path Expressions](#advanced-path-expressions) 116 | - [Mapping and Reducing](#mapping-and-reducing) 117 | - [Thread Fns](#thread-fns) 118 | - [Keyword Arguments](#keyword-arguments) 119 | - [Nesting](#nesting) 120 | - [`%%` & `%%%`](#`%%`-&-`%%%`) 121 | - [How](#how) 122 | - [Why Not](#why-not) 123 | - [Roadmap](#roadmap) 124 | 125 | ## Basic Path Expressions 126 | 127 | The sections below go over each of `perc`'s features, showing a table of what expressions will result in what output code and providing some cannonical example code for each variation. The actual output code will look slightly different, but the `get-in` examples give you an idea of what is happening. 128 | 129 | ## Named anonymous parameters 130 | 131 | Again, all you have to do is prepend your function body with `#%` - like you would normally use just `#` for with anonymous functions. Then you can use `%` and `%1` like usual, but you can also do `%:foo` or `%1:bar`. 132 | 133 | ```clojure 134 | (defn Point [x y z] 135 | (str [x y z])) ; => #'cljs.user/Point 136 | 137 | (#%(Point %:x %:y %:z) 138 | {:x 1 139 | :y 2 140 | :z 3}) ; => "[1 2 3]" 141 | ``` 142 | 143 | Note that if no index is given (like `%`) then the first param `%1` is implied. 144 | 145 | We now might ask, why did we decide to keep Clojure's 1-based indexing for anonymymous parameters? 146 | 147 | Answer: `perc`s are a read-time code notation designed for humans, not programmatic runtime generation. Because they're tag literals, they disappear after read time. They're literally not meant to be interpreted at runtime like other runtime data (just like Clojure's existing anonymous fn syntax), so it's not as if you'll be adding indexes to `perc`s expressions programmatically anyway. If you disagree with this decision and preferred we moved to 0-based indexes feel free to file an issue here and we can debate the merits. 148 | 149 | |Path Expression|=>|Expression| 150 | |---:|---:|:---| 151 | |`%`| => | `(get-in % [0])`| 152 | |`%:x`| => | `(get-in % [0 :x])`| 153 | |`%:foo`| => | `(get-in % [0 :foo])`| 154 | 155 | ```clojure 156 | (#%(println :% % 157 | :%:x %:x 158 | :%:foo %:foo) 159 | {:x 1 160 | :foo 2}) 161 | ; :% {:x 1, :foo 2} :%:x 1 :%:foo 2 162 | ``` 163 | 164 | ## Namespaced Anonymous Parameters 165 | 166 | Namespaced keywords do not work directly in `perc` for the JVM Clojure - consecutive colons (`::`) are not allowed within tokens. As a workaround, we place a `*` after the first colon of a keyword to indicate that the token should be converted to a namespaced keyword. 167 | 168 | ```clojure 169 | (#%{:x %:*/x 170 | :y %:*s/y 171 | :z %:foreign/z} 172 | {::x 1 173 | ::s/y 2 174 | :foreign/z 3}) 175 | ; {:x 1, :y 2, :z 3} 176 | ``` 177 | 178 | |Path Expression|=>|Expression| 179 | |---:|---:|:---| 180 | |`%:*/x`| => | `(get-in % [0 ::x])`| 181 | |`%:*/foo`| => | `(get-in % [0 ::foo])`| 182 | |`%:*foo/bar`| => | `(get-in % [0 ::foo/bar])`| 183 | 184 | ```clojure 185 | (#%(println :%:*/x %:*/x 186 | :%:*/foo %:*/foo 187 | :%:*foo/bar %:*foo/bar) 188 | {::x 1 189 | ::foo 2 190 | ::foo/bar 3}) 191 | ; :%:*/x 1 :%:*/foo 2 :%:*foo/bar 3 192 | ``` 193 | 194 | ## Multiple parameters 195 | 196 | Pulling named values out of multiple different parameters works as you'd expect. 197 | 198 | ```clojure 199 | #%(response %1:ctx %2:status %2:body) 200 | ``` 201 | 202 | Here we grab `:ctx` from the first parameter, `:status` from the second and `:body` from the second. 203 | 204 | This also makes it easier to deal with ambiguous keys coming in from multiple map sources. For example, suppose we want a function that takes three point maps and provides them to a `Triangle` constructor: 205 | 206 | ```clojure 207 | #%(Triangle. %1:x %1:y %1:z, 208 | %2:x %2:y %2:z, 209 | %3:x %3:y %3:z) 210 | ``` 211 | 212 | Doing that with the regular old syntax, we would clobber coordinates if we tried to destructure using `:keys [x y z]`. And the alternative would be rather verbose and unnecessarily confusing: 213 | 214 | ```clojure 215 | #(let [{x1 :x y1 :y z1 :z} %1 216 | {x2 :x y2 :y z2 :z} %2 217 | {x3 :x y3 :y z3 :z} %3] 218 | (Triangle. x1 y1 z1, 219 | x2 y2 z2, 220 | x3 y3 z3)) 221 | ``` 222 | 223 | |Path Expression|=>|Expression| 224 | |---:|---:|:---| 225 | |`%1`| => | `(get-in % [0])`| 226 | |`%2`| => | `(get-in % [1])`| 227 | |`%1:x`| => | `(get-in % [0 :x])`| 228 | |`%:x`| => | `(get-in % [0 :x])`| 229 | |`%2:*/x`| => | `(get-in % [1 ::x])`| 230 | |`%2:foo`| => | `(get-in % [1 :foo])`| 231 | |`%3:*foo/bar`| => | `(get-in % [2 ::foo/bar])`| 232 | 233 | ```clojure 234 | (#%(println :%1 %1 235 | :%2 %2 236 | :%1:x %1:x 237 | :%:x %:x 238 | :%2:*/x %2:*/x 239 | :%2:foo %2:foo 240 | :%3:*foo/bar %3:*foo/bar) 241 | {:x 1} 242 | {::x 2 243 | :foo 3} 244 | {::foo/bar 3}) 245 | ; :%1 {:x 1} :%2 {:perc.core/x 2, :foo 3} :%1:x 1 :%:x 1 :%2:*/x 2 :%2:foo 3 :%3:*foo/bar 3 246 | ``` 247 | 248 | ## Return literals 249 | 250 | You don't have to follow the `#%` tag with a set of parenthesis - you can use any collection or elide one altogether. _Return literal_ vectors and maps can be used for quick updates in place, like: 251 | 252 | ```clojure 253 | (#%{::a (inc %1) ::b (inc %2)} 4 5) 254 | ; #:cljs.user{:a 5, :b 6} 255 | ``` 256 | 257 | Or 258 | 259 | ```clojure 260 | (#%[(inc %:x) (inc %:y)] {:x 4 :y 5}) 261 | ; [5 6] 262 | ``` 263 | 264 | Or just for wrapping stuff 265 | 266 | ```clojure 267 | (#%{:a %1 :b %2} 4 5) 268 | ; {:a 4, :b 5} 269 | ``` 270 | 271 | This makes for a short and quick way to restructure data as it flows through deeply nested transformations. 272 | 273 | To return a namespaced map literal, you must put a space between the `#%` reader tag and the map: 274 | 275 | ```clojure 276 | (#% #:Point{:x %:*/x :y %:*s/y :z %:foreign/z} 277 | {::x 1 ::s/y 2 :foreign/z 3}) 278 | ; #:Point{:x 1, :y 2, :z 3} 279 | ``` 280 | 281 | Otherwise Clojure would have concatenated them into a `#%#:Point` token above. 282 | 283 | ## Advanced Path Expressions 284 | 285 | As discussed in the [overview](#overview), you can concatenate anonymous parameters together to create arbitrarily long _path expressions,_ which are like a cross between a `get-in` and the `->` thread operator. 286 | 287 | ### Path Expression Table 288 | |Path Expression|=>|Expression| 289 | |---:|---:|:---| 290 | |`%:x:y`| => | `(get-in % [0 :x :y])`| 291 | |`%2:x:y/z`| => | `(get-in % [1 :x :y/z])`| 292 | |`%1:x:*y/z`| => | `(get-in % [0 :x ::y/z])`| 293 | |`%2:x:*/z`| => | `(get-in % [1 :x ::z])`| 294 | |`%2:x:a%1:*/z`| => | `(get-in % [1 :x :a 0 ::z])`| 295 | |`%2:x:a%1:*/z%3`| => | `(get-in % [1 :x :a 0 ::z 2])`| 296 | |`%:x:a%1:*/z%3`| => | `(get-in % [0 :x :a 0 ::z 2])`| 297 | 298 | ```clojure 299 | (#%(println :%:x:y %:x:y 300 | :%2:x:y/z %2:x:y/z 301 | :%1:x:*y/z %1:x:*y/z 302 | :%2:x:*/z %2:x:*/z 303 | :%:x:a%1:*/z %:x:a%1:*/z 304 | :%2:x:a%1:*/z%1 %2:x:a%1:*/z%1 305 | :%:x:a%1:*/z%3 %:x:a%1:*/z%3) 306 | {:x {:y 2 :y/z 3 ::y/z 4 ::z 5 :a [{::z [6 7 8 9]}]}} 307 | {:x {:y 9 :y/z 8 ::y/z 7 ::z 6 :a [{::z [5 4 3 2]}]}}) 308 | ; :%:x:y 2 :%2:x:y/z 8 :%1:x:*y/z 4 :%2:x:*/z 6 :%:x:a%1:*/z [6 7 8 9] :%2:x:a%1:*/z%1 5 :%:x:a%1:*/z%3 8 309 | ``` 310 | 311 | ## Mapping and Reducing 312 | 313 | Mapping and reducing are things we do a lot in Clojure. With `perc`s we can more easily slice and dice named values in deeply nested transformations. Here's a more involved example: 314 | 315 | ```clojure 316 | (->> [{:a {:z {:x 5}} 317 | :b {:c {:p {:q 6}}}} 318 | {:a {:z {:x 7}} 319 | :b {:c {:p {:q 8}}}} 320 | {:a {:z {:x 9}} 321 | :b {:c {:p {:q 10}}}}] 322 | 323 | (mapv #%{:a {:z {:x (inc %:a:z:x)}} 324 | :b {:c {:p {:q (dec %:b:c:p:q)}}}}) 325 | 326 | (reduce #%{:a {:z {:x (+ %1:a:z:x %2:a:z:x)}} 327 | :b {:c {:p {:q (+ %1:b:c:p:q %2:b:c:p:q)}}}})) 328 | ``` 329 | 330 | So far, we've been operating over collections of maps. What if we're operating over a collection of vectors here? Top level index references like `%1` aren't automatically indexing into the first param like a top level keyword reference like `%:a` (otherwise we wouldn't be able to access params other than the first). In order to imply top level, indexical access into the first param, we can use the `#%1` 1-arity version of `perc`. 331 | 332 | ### 1-arity Fns 333 | The `#%1` reader tag is for when you're mapping over collections of vectors and you know `map` will only be passing one parameter to the fn. Top level indexes within the fn body will then index into the first param (rather than the fn's whole list of params): 334 | 335 | ```clojure 336 | (->> [[{:x 1 :y 2 :z 3} {:x 2 :y 3 :z 4} {:x 3 :y 4 :z 5}] 337 | [{:x 1 :y 2 :z 3} {:x 2 :y 3 :z 4} {:x 3 :y 4 :z 5}]] 338 | 339 | (mapv #%1[(assoc %3 :x (inc %3:x) :z (dec %3:z)) 340 | (assoc %2 :x (inc %2:x) :z (dec %2:z)) 341 | (assoc %1 :x (inc %1:x) :z (dec %1:z))]) 342 | 343 | (mapv #%1[%1:x %1:y %1:z 344 | %2:x %2:y %2:z 345 | %3:x %3:y %3:z])) ; => [[4 4 4 3 3 3 2 2 2] [4 4 4 3 3 3 2 2 2]] 346 | ``` 347 | 348 | If we only used `#%` there, then in the fn body we would have had to reference the first fn param each time: 349 | 350 | ```clojure 351 | ... 352 | (mapv #%[(assoc %1%3 :x (inc %1%3:x) :z (dec %1%3:z)) 353 | ... 354 | (mapv #%[%1%1:x %1%1:y %1%1:z 355 | ... 356 | ``` 357 | 358 | If we were passing extra arguments to the `mapv`s, then those first indexes might be useful, but usually we're not passing extra arguments. 359 | 360 | Let's show a more involved example where we transform from vectors to maps to vectors: 361 | 362 | ```clojure 363 | (defn rand-point-3d [] 364 | {:tag :point-3d 365 | :x (rand-int 100) 366 | :y (rand-int 100) 367 | :z (rand-int 100)}) 368 | 369 | (defn rand-triangle-3d [] 370 | [(rand-point-3d) (rand-point-3d) (rand-point-3d)]) 371 | 372 | (->> (repeatedly rand-triangle-3d) 373 | (take 2) 374 | ;; mess with the maps and attach them to keys 375 | (mapv #%1{:a (assoc %3 :x %3:x%inc :z %3:z%dec) ; <- notice the easter egg? teehee :D 376 | :b (assoc %2 :x %2:x%inc :z %2:z%dec) 377 | :c (assoc %1 :x %1:x%inc :z %1:z%dec)}) 378 | ;; return them as vectors in some other sort order 379 | (mapv #%[:Triangle 380 | %:b:x %:b:y %:b:z 381 | %:a:x %:a:y %:a:z 382 | %:c:x %:c:y %:c:z])) 383 | ;; [[:Triangle 85 53 41 36 72 16 97 26 10] [:Triangle 100 15 15 99 99 24 50 97 5]] 384 | ``` 385 | 386 | ## Thread Fns 387 | 388 | _Thread functions_ allow you to create a function that is more easily threaded by a `->` operator. 389 | 390 | Thread functions implement the `#%>` reader tag which has the same semantics as `#%1` but wraps it in a pair of parentheses so that the anonymous fn can be threaded through appropriately: 391 | 392 | ```clojure 393 | (-> {:z/x {:y [1 'b :c 8 {::s/a {:num 9}}]}} 394 | 395 | #%>[%:z/x:y%5:*s/a:num]) 396 | ; => [9] 397 | ``` 398 | 399 | It's great for quickly transforming maps within a thread context: 400 | 401 | ```clojure 402 | (-> {::x 1 ::s/y 2 :foreign/z 3} 403 | 404 | #%> #:Point{:x %:*/x :y %:*s/y :z %:foreign/z}) 405 | ; => #:Point{:x 1, :y 2, :z 3} 406 | ``` 407 | 408 | Or for condensing navigation paths until a common branch between two values in a deeply nested structure: 409 | 410 | ```clojure 411 | (-> {:z/x {:y [1 'b :c 8 {::s/a {:num 9}}]}} 412 | 413 | #%> %:z/x:y 414 | 415 | #%>(+ %1 %5:*s/a:num)) 416 | ; => 10 417 | ``` 418 | 419 | ## Keyword arguments 420 | 421 | [Keyword arguments](https://clojure.org/guides/destructuring#_keyword_arguments) provide a convenient way to give arguments to a function without having to worry about the order of those arguments. The official docs show this as an example: 422 | 423 | ```clojure 424 | (defn configure [val & {:keys [debug verbose] 425 | :or {debug false, verbose false}}] 426 | (println "val =" val " debug =" debug " verbose =" verbose)) 427 | ``` 428 | 429 | Allowing us to do: 430 | 431 | ```clojure 432 | (configure 12 :verbose true :debug true) 433 | ``` 434 | 435 | Similarly, using `perc`'s vararg syntax allows us to easily stick keyword arguments in places anonymously: 436 | 437 | ```clojure 438 | (def app 439 | {:app-name :foo-server 440 | :routes ["*" 200] 441 | :configure #%(do (when %&:debug 442 | (when %&:verbose (println (str "Welcome to " %1:app-name%name "!"))) 443 | (println :app-state %1) 444 | (println :config %2) 445 | (println :optional-arguments-supplied %&)) 446 | #_...stuff 447 | (assoc %1 :config (merge %2 %&)))}) 448 | 449 | (def config {:accept-connections true}) 450 | 451 | (-> app 452 | #_... 453 | #%>(%:configure % config :verbose true :debug true) 454 | #_...) 455 | ;; Welcome to foo-server! 456 | ;; :app-state {:app-name :foo-server, :routes [* 200], :configure #function[perc.core/fn--7923]} 457 | ;; :config {:accept-connections true} 458 | ;; :optional-arguments-supplied {:verbose true, :debug true} 459 | ;=> {:app-name :foo-server, 460 | ;; :routes ["*" 200], 461 | ;; :configure #function[perc.core/fn--7923], 462 | ;; :config {:accept-connections true, :verbose true, :debug true}} 463 | ``` 464 | 465 | # Nesting 466 | 467 | Like with traditional, sugared anonymous functions, you can't nest `perc`s of a given type, like: 468 | 469 | ```clojure 470 | #%(do #%()) ; => Syntax error reading source at (REPL:xxx:xx). 471 | ; No nesting for reader tag #% 472 | ``` 473 | 474 | ## `%%` & `%%%` 475 | 476 | However, there are also the tagged literals `#%%` and `#%%%` for explicitly nesting deeper levels. They each transform `%%` and `%%%` symbols, respectively, within their enclosing forms. 477 | 478 | Suppose we had some data: 479 | 480 | ```clojure 481 | {::demo/events [e1 e2 e3] 482 | :acme/event-handler (fn ... 483 | ::time-out-callback (fn ... 484 | ... } 485 | ``` 486 | 487 | Using old-school syntax, we might do something like this to apply the event handler to the events: 488 | 489 | ```clojure 490 | (fn [{events ::demo/events 491 | event-handler :acme/event-handler] 492 | (mapv #(event-handler (:event/data %)) 493 | events) 494 | ``` 495 | 496 | Using the new-school syntax, we don't have to give as many things new names: 497 | 498 | ```clojure 499 | #%(mapv 500 | #%%(%:acme/event-handler %%:event/data) 501 | %:*demo/events) 502 | ``` 503 | 504 | Being able to reference multiple levels of depth with `%`, `%%` and `%%%` allows us to maintain syntactic concision without having to take the classical `(fn [])` escape hatch as often. 505 | 506 | # How 507 | 508 | `perc`s employ [tagged literals](https://clojure.org/reference/reader#tagged_literals). They essentially work like macros but take only one parameter (the token to the right of them) and have no parenthesis around themselves and their parameter. At read time, the pair of reader tag and its parameter are replaced by the return value of the tag's transformation function. Because we only need to instrument a single form with our syntax sugar, tagged literals work out pretty good for this use-case. 509 | 510 | # Why Not? 511 | 512 | One downside is that, as with destructuring by :keys, you cannot reference by keys that are not actually keywords. So it's most useful for maps where keys are actually keywords and not strings or other types of objects. Those values can still be referenced by index, as with the existing sugared anonymous function syntax. 513 | 514 | Clojure(Script)'s anonymous function syntax sugar is actually built into the language's reader. Because a `perc`'s anonymous function expand to a regular anonymous function, the resulting code will likely be a little larger. 515 | 516 | Technically, the tokens within the anaphoric macros are not "valid" Clojure symbols. 517 | 518 | `perc`s allow Clojure to use ClojureScript's more permissive anonymous function arity handling. As such, they do not assert Clojure's more strict arity checking. 519 | 520 | # Roadmap 521 | 522 | One thing I'd like to explore next is some kind of relational query syntax, leaning on something like [EQL](https://github.com/edn-query-language/eql), [Meander](https://github.com/noprompt/meander), [Odin](https://github.com/halgari/odin) or [juxt/pull](https://github.com/juxt/pull) under the hood. 523 | 524 | It would be nice to have a syntax that can express both the query and the projection in a single form, but I can't figure out where to squeeze declarations for joins cleanly. 525 | 526 | ```clojure 527 | #%?{:size ?size%:house:size|:door:size ; <- joins whole query on :size but returns value 528 | :number-of-windows (count %2:house-templates%?size:windows) 529 | :about-the-door %:door:description} ; => {:size :large 530 | ; :number-of-windows 20 531 | ; :about-the-door "Big'ol door"} 532 | ``` 533 | 534 | But if we didn't want to actually return the size, we'd have to declare it outside the form body: 535 | 536 | ```clojure 537 | #%?(do ?size%:house:size|:door:size 538 | {:number-of-windows (count %2:house-templates%?size:windows) 539 | :about-the-door %:door:description} ; => {:number-of-windows 20 540 | ; :about-the-door "Big'ol door"} 541 | ``` 542 | 543 | Let me know if you think of anything. 544 | 545 | From a performance perspective, I'd also like to bring in something like [structural/with-slots](https://github.com/joinr/structural#structuralcorewith-slots) to allow type hinting and auto optimization. 546 | 547 | Per structural's readme: 548 | 549 | It’s basically a poor man’s optimizing compiler for the use-case of unpacking type-hinted structures for efficient reads 550 | 551 | So it'd be nice to output code that is even more efficient than what you'd normally write by hand and allow for type hints in really tight loops. 552 | 553 | Additionally, I'd like to explore tools like [streamer](https://github.com/divs1210/streamer), where we unwind and rewind code into `->>` forms, scan for contiguous blocks of transducing candidates and then use `streamer` to rewrite those into transducing blocks, thereby preserving laziness when implied but reducing down to a transducer when eager consumption is detected - auto-transducified code. With those two additions, you might even get a non-trivial _performance_ `perc`. 554 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "test"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.1"} 3 | org.clojure/clojurescript {:mvn/version "1.10.520"}} 4 | :aliases {:test {:extra-deps {olical/cljs-test-runner {:mvn/version "3.7.0"}} 5 | :main-opts ["-m" "cljs-test-runner.main"]}}} 6 | -------------------------------------------------------------------------------- /src/data_readers.cljc: -------------------------------------------------------------------------------- 1 | {% perc.core/% 2 | %1 perc.core/%1 3 | %> perc.core/%> 4 | 5 | %% perc.core/%% 6 | %%1 perc.core/%%1 7 | %%> perc.core/%%> 8 | 9 | %%% perc.core/%%% 10 | %%%1 perc.core/%%%1 11 | %%%> perc.core/%%%>} 12 | -------------------------------------------------------------------------------- /src/perc/core.cljc: -------------------------------------------------------------------------------- 1 | (ns perc.core 2 | (:require [clojure.string :as string] 3 | #?(:cljs [cljs.reader :refer [read-string]]) 4 | [clojure.walk :as w])) 5 | 6 | (defn element-starts-with-symbol? [sym el] 7 | (and (= (str sym) 8 | (->> el str (take-while #{(first (str sym))}) (apply str))) 9 | (= (str sym) 10 | (->> el str (take (count sym)) (apply str))))) 11 | 12 | (defn extract-& [s] 13 | (if (string/starts-with? s "&") 14 | [(apply str (rest s)) 15 | "&"] 16 | [s nil])) 17 | 18 | (def nums 19 | (->> "0123456789" 20 | (into #{}))) 21 | 22 | (defn extract-numbers [s] 23 | (let [res (if (-> s first nums) 24 | [(->> s (drop-while nums) (apply str)) 25 | (->> s (take-while nums) (apply str))] 26 | [s nil])] 27 | res)) 28 | 29 | (defn extract-star [s] 30 | (if (string/starts-with? s "*") 31 | [(apply str (rest s)) 32 | "*"] 33 | [s nil])) 34 | 35 | (defn smells-like-keyword [s] 36 | (some-> s (string/starts-with? ":"))) 37 | 38 | (defn read-key [s] 39 | (let [s1 (if-not (and (re-find #"/" s) 40 | (-> s first str (= "*"))) 41 | (str ":" s) 42 | (if (->> s (take 2) (apply str) (= "*/")) 43 | (->> s (drop 2) (apply str "::")) 44 | (->> s rest (apply str "::"))))] 45 | (if (and (re-find #"/" s1) (string/starts-with? s1 "::")) 46 | (let [[n k] (string/split s1 #"/") 47 | n (->> n (drop 2) (apply str)) 48 | k2 (->> k (drop 2) (apply str)) 49 | n2 (str (get (ns-aliases *ns*) (symbol n)))] 50 | (if k 51 | (keyword n2 k) 52 | (keyword n))) 53 | (if (string/starts-with? s1 "::") 54 | (keyword (str *ns*) (->> s1 (drop 2) (apply str))) 55 | (keyword (->> s1 rest (apply str))))))) 56 | 57 | (defn read-keys [s] 58 | (let [ks (-> s (string/split #":") rest)] 59 | (when (seq ks) 60 | (if (= 1 (count ks)) 61 | [(read-key (first ks))] 62 | (mapv read-key ks))))) 63 | 64 | (defn explode [s] 65 | (let [[s vararg?] (extract-& s) 66 | [s index] (extract-numbers s) 67 | index (some-> index read-string dec) 68 | new-keys (when (smells-like-keyword s) 69 | (read-keys s)) 70 | new-sym (when-not (seq new-keys) 71 | (when (and s (not (= s ""))) 72 | (symbol s))) 73 | thread (into [] 74 | (filter #(not (nil? %)) 75 | (if new-sym 76 | [index new-sym] 77 | (into [index] new-keys))))] 78 | {:vararg? vararg? 79 | :index index 80 | :new-keys new-keys 81 | :thread thread})) 82 | 83 | (defn one-token [{:keys [root-local sym el] :as state}] 84 | (let [result (symbol (str "perclocal" sym)) 85 | ex (if el (explode el) {:thread [root-local]})] 86 | (select-keys ex [:index :vararg? :thread :new-keys]))) 87 | 88 | (defn mk-sym-state [{:keys [sym el] :as state}] 89 | (let [drop-el (dec (count sym)) 90 | el (->> el str (drop drop-el) (apply str)) 91 | tokens (-> el 92 | str 93 | (string/split #"%") 94 | rest) 95 | nt (->> tokens 96 | (mapv (fn [token] 97 | (if (string/starts-with? token "*:") 98 | (apply str ":" (rest token)) 99 | token)))) 100 | one-tkn? (= 1 (count tokens)) 101 | token-thread (->> nt 102 | (mapv #(one-token (assoc state :el % :token %))) 103 | (mapv :thread) 104 | (apply concat) 105 | vec) 106 | first-token (assoc 107 | (one-token (assoc state 108 | :el (first nt) 109 | :token (first nt))) 110 | :token-thread token-thread)] 111 | first-token)) 112 | 113 | (defn vararg-index [sym expr] 114 | (let [nums (->> expr 115 | (tree-seq coll? seq) 116 | (filter #(string/starts-with? (str %) (str sym))) 117 | (map #(re-find #"^%\d" (str %))) 118 | (filter some?) 119 | (map rest) 120 | (map #(apply str %)) 121 | (map read-string))] 122 | (if (not (empty? nums)) 123 | (apply max nums) 124 | 0))) 125 | 126 | (defn mk-sym [{:as perc-state :keys [root-local threaded? expr sym]}] 127 | (let [{:keys [vararg? 128 | index 129 | token-thread] :as state} 130 | (mk-sym-state perc-state) 131 | vindex (vararg-index sym expr) 132 | obj (if threaded? 133 | `(first (vec ~root-local)) 134 | (if-not vararg? 135 | `(vec ~root-local) 136 | `(apply hash-map (drop ~vindex ~root-local)))) 137 | thread (if (or threaded? index vararg?) 138 | token-thread 139 | (into [0] 140 | token-thread)) 141 | get-thread (->> thread 142 | (mapv (fn [tkn] 143 | (if (int? tkn) 144 | (list 'nth tkn) 145 | tkn)))) 146 | res `(-> ~obj ~@get-thread)] 147 | res)) 148 | 149 | (defn handle [{:keys [sym threaded? root-local] :as state} el] 150 | (if (= el root-local) 151 | (throw (Exception. (str "No nesting for reader tag #" sym))) 152 | (if (element-starts-with-symbol? sym el) 153 | (mk-sym (assoc state :el el)) 154 | el))) 155 | 156 | (defn mk-perc [sym threaded? expr] 157 | (let [root-local (symbol (str "perclocal" sym)) 158 | res `(fn [& ~root-local] 159 | ~(w/postwalk 160 | (partial handle 161 | {:threaded? threaded? 162 | :root-local root-local 163 | :expr expr 164 | :sym (str sym)}) 165 | expr))] 166 | res)) 167 | 168 | (defn perc 169 | [sym threaded? expr] 170 | (mk-perc sym threaded? expr)) 171 | 172 | (defn % [expr & [threaded?]] 173 | (let [res (->> expr 174 | (perc '% threaded?))] 175 | res)) 176 | 177 | (defn %1 [expr] 178 | (% expr true)) 179 | 180 | (defn %% [expr & [threaded?]] 181 | (->> expr 182 | (perc '%% threaded?))) 183 | 184 | (defn %%1 [expr] 185 | (%% expr true)) 186 | 187 | (defn %%% [expr & [threaded?]] 188 | (->> expr 189 | (perc '%%% threaded?))) 190 | 191 | (defn %%%1 [expr] 192 | (%%% expr true)) 193 | 194 | (defn %> [expr] 195 | (let [perc-expr (% expr true)] 196 | `(~perc-expr))) 197 | 198 | (defn %%> [expr] 199 | (let [perc-expr (%% expr true)] 200 | `(~perc-expr))) 201 | 202 | (defn %%%> [expr] 203 | (let [perc-expr (%%% expr true)] 204 | `(~perc-expr))) 205 | 206 | (defn -main [& args] 207 | (println :hi args)) 208 | -------------------------------------------------------------------------------- /src/perc/core.cljs: -------------------------------------------------------------------------------- 1 | (ns perc.core 2 | (:require-macros [perc.core])) 3 | -------------------------------------------------------------------------------- /test/perc/simple_test.cljs: -------------------------------------------------------------------------------- 1 | (ns perc.simple-test 2 | (:require [cljs.test :as t] 3 | [clojure.string :as s] 4 | [perc.core :as perc])) 5 | 6 | (t/deftest perc-normal 7 | (t/testing "old school %" 8 | (t/is (= 1 (#%(inc %) 0)))) 9 | (t/testing "permissive arity" 10 | (t/is (= 1 (#%(inc %) 0 nil)))) 11 | (t/testing "by arity index" 12 | (t/is (= 1 (#%(inc %1) 0))) 13 | (t/is (= 1 (#%(inc %1) 0 nil))) 14 | (t/is (= 1 (#%(inc %2) nil 0))) 15 | (t/is (= 1 (#%(inc %3) nil nil 0))) 16 | (t/is (= 1 (#%(inc %3) nil nil 0 nil)))) 17 | (t/testing "by large arity index" 18 | (t/is (= 1 (#%(inc %10) nil nil nil nil nil nil nil nil nil 0))))) 19 | 20 | (t/deftest perc-keys 21 | (t/testing "by key %:x" 22 | (t/is (= 1 (#%(inc %:x:y) {:x {:y 0}}))) 23 | (t/is (= 1 (#%(inc %:x) {:x 0} nil))) 24 | 25 | (t/is (= 1 (#%(inc %1:x) {:x 0}))) 26 | (t/is (= 1 (#%(inc %1:x) {:x 0} nil))) 27 | (t/is (= 1 (#%(inc %2:x) nil {:x 0}))) 28 | (t/is (= 1 (#%(inc %3:x) nil nil {:x 0}))) 29 | (t/is (= 1 (#%(inc %3:x) nil nil {:x 0} nil))) 30 | 31 | (t/is (= 1 (#%(inc %10:x) nil nil nil nil nil nil nil nil nil {:x 0}))))) 32 | 33 | (t/deftest perc-namespaced-keys 34 | (t/testing "by namespaced key %::x" 35 | (t/is (= 1 (#%(inc %:*/x) {::x 0}))) 36 | (t/is (= 1 (#%(inc %:*/x) {::x 0} nil))) 37 | 38 | (t/is (= 1 (#%(inc %1:*/x) {::x 0}))) 39 | (t/is (= 1 (#%(inc %1:*/x) {::x 0} nil))) 40 | (t/is (= 1 (#%(inc %2:perc/x) nil {:perc/x 0}))) 41 | ;; %3:*/t/x 42 | ;; %1:*/s/y 43 | ;; (t/is (= 1 (#%(inc %3::t/x) nil nil {::t/x 0}))) 44 | (t/is (= 1 (#%(inc %3:*s/x) nil nil {::s/x 0}))) 45 | (t/is (= 1 (#%(inc %3:perc/x) nil nil {:perc/x 0} nil))) 46 | 47 | (t/is (= 1 (#%(inc %10:*/x) nil nil nil nil nil nil nil nil nil {::x 0}))))) 48 | 49 | (t/deftest double-perc 50 | (t/testing "double perc %%::x" 51 | (t/is (= 1 (#%%(inc %%) 0))) 52 | (t/is (= 2 (#%%(inc %%) 1 nil))) 53 | 54 | (t/is (= 3 (#%%(inc %%1) 2))) 55 | (t/is (= 4 (#%%(inc %%1) 3 nil))) 56 | (t/is (= 1 (#%%(inc %%2) nil 0))) 57 | (t/is (= 1 (#%%(inc %%3) nil nil 0))) 58 | (t/is (= 1 (#%%(inc %%3) nil nil 0 nil))) 59 | 60 | (t/is (= 1 (#%%(inc %%10) nil nil nil nil nil nil nil nil nil 0))))) 61 | 62 | (t/deftest double-perc-keys 63 | (t/testing "double-perc map keys %:x" 64 | (t/is (= 1 (#%%(inc %%:x) {:x 0}))) 65 | (t/is (= 1 (#%%(inc %%:x) {:x 0} nil))) 66 | 67 | (t/is (= 1 (#%%(inc %%1:x) {:x 0}))) 68 | (t/is (= 1 (#%%(inc %%1:x) {:x 0} nil))) 69 | (t/is (= 1 (#%%(inc %%2:x) nil {:x 0}))) 70 | (t/is (= 1 (#%%(inc %%3:x) nil nil {:x 0}))) 71 | (t/is (= 1 (#%%(inc %%3:x) nil nil {:x 0} nil))) 72 | 73 | (t/is (= 1 (#%%(inc %%10:x) nil nil nil nil nil nil nil nil nil {:x 0}))))) 74 | 75 | (t/deftest double-perc-namespaced-keys 76 | (t/testing "double-perc namespaced keys %::x" 77 | (t/is (= 1 (#%%(inc %%:*/x) {::x 0}))) 78 | (t/is (= 1 (#%%(inc %%:*/x) {::x 0} nil))) 79 | 80 | (t/is (= 1 (#%%(inc %%1:*/x) {::x 0}))) 81 | (t/is (= 1 (#%%(inc %%1:*/x) {::x 0} nil))) 82 | (t/is (= 1 (#%%(inc %%2:perc/x) nil {:perc/x 0}))) 83 | (t/is (= 1 (#%%(inc %%3:*t/x) nil nil {::t/x 0}))) 84 | (t/is (= 1 (#%%(inc %%3:perc/x) nil nil {:perc/x 0} nil))) 85 | 86 | (t/is (= 1 (#%%(inc %%10:*/x) nil nil nil nil nil nil nil nil nil {::x 0}))))) 87 | 88 | 89 | (t/deftest triple-perc 90 | (t/testing "triple perc %%%::x" 91 | (t/is (= 1 (#%%%(inc %%%) 0))) 92 | (t/is (= 1 (#%%%(inc %%%) 0 nil))) 93 | 94 | (t/is (= 1 (#%%%(inc %%%1) 0))) 95 | (t/is (= 1 (#%%%(inc %%%1) 0 nil))) 96 | (t/is (= 1 (#%%%(inc %%%2) nil 0))) 97 | (t/is (= 1 (#%%%(inc %%%3) nil nil 0))) 98 | (t/is (= 1 (#%%%(inc %%%3) nil nil 0 nil))) 99 | 100 | (t/is (= 1 (#%%%(inc %%%10) nil nil nil nil nil nil nil nil nil 0))))) 101 | 102 | (t/deftest triple-perc-keys 103 | (t/testing "triple-perc map keys %%%:x" 104 | (t/is (= 1 (#%%%(inc %%%:x) {:x 0}))) 105 | (t/is (= 1 (#%%%(inc %%%:x) {:x 0} nil))) 106 | 107 | (t/is (= 1 (#%%%(inc %%%1:x) {:x 0}))) 108 | (t/is (= 1 (#%%%(inc %%%1:x) {:x 0} nil))) 109 | (t/is (= 1 (#%%%(inc %%%2:x) nil {:x 0}))) 110 | (t/is (= 1 (#%%%(inc %%%3:x) nil nil {:x 0}))) 111 | (t/is (= 1 (#%%%(inc %%%3:x) nil nil {:x 0} nil))) 112 | 113 | (t/is (= 1 (#%%%(inc %%%10:x) nil nil nil nil nil nil nil nil nil {:x 0}))))) 114 | 115 | (t/deftest triple-perc-namespaced-keys 116 | (t/testing "triple-perc namespaced keys %%%::x" 117 | (t/is (= 1 (#%%%(inc %%%:*/x) {::x 0}))) 118 | (t/is (= 1 (#%%%(inc %%%:*/x) {::x 0} nil))) 119 | 120 | (t/is (= 1 (#%%%(inc %%%1:*/x) {::x 0}))) 121 | (t/is (= 1 (#%%%(inc %%%1:*/x) {::x 0} nil))) 122 | (t/is (= 1 (#%%%(inc %%%2:perc/x) nil {:perc/x 0}))) 123 | (t/is (= 1 (#%%%(inc %%%3:*t/x) nil nil {::t/x 0}))) 124 | (t/is (= 1 (#%%%(inc %%%3:perc/x) nil nil {:perc/x 0} nil))) 125 | 126 | (t/is (= 1 (#%%%(inc %%%10:*/x) nil nil nil nil nil nil nil nil nil {::x 0}))))) 127 | 128 | --------------------------------------------------------------------------------