{"id":29166718,"url":"https://github.com/cloojure/tupelo","last_synced_at":"2026-03-07T23:01:18.511Z","repository":{"id":42788760,"uuid":"21364680","full_name":"cloojure/tupelo","owner":"cloojure","description":"Tupelo:  Clojure With A Spoonful of Honey","archived":false,"fork":false,"pushed_at":"2026-02-11T22:44:17.000Z","size":4862,"stargazers_count":520,"open_issues_count":7,"forks_count":15,"subscribers_count":12,"default_branch":"master","last_synced_at":"2026-03-07T04:05:15.018Z","etag":null,"topics":["clojure"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cloojure.png","metadata":{"files":{"readme":"README.adoc","changelog":"changelog.adoc","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2014-06-30T20:19:28.000Z","updated_at":"2026-03-05T06:57:46.000Z","dependencies_parsed_at":"2024-06-18T21:24:57.088Z","dependency_job_id":"3f9ce902-8f26-452a-bc9a-158634862241","html_url":"https://github.com/cloojure/tupelo","commit_stats":{"total_commits":2387,"total_committers":4,"mean_commits":596.75,"dds":"0.017595307917888547","last_synced_commit":"b5d08eb60f1df1ed7330af3c38daa8fce3b856de"},"previous_names":["cloojure/cooljure"],"tags_count":174,"template":false,"template_full_name":null,"purl":"pkg:github/cloojure/tupelo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloojure%2Ftupelo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloojure%2Ftupelo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloojure%2Ftupelo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloojure%2Ftupelo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cloojure","download_url":"https://codeload.github.com/cloojure/tupelo/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cloojure%2Ftupelo/sbom","scorecard":{"id":292112,"data":{"date":"2025-08-11","repo":{"name":"github.com/cloojure/tupelo","commit":"0db1189de681d32cffd1bf09f39b5840ba28fbe2"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.2,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.txt:0","Info: FSF or OSI recognized license: Eclipse Public License 1.0: LICENSE.txt:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: npmCommand not pinned by hash: misc/npm-install.bash:4","Warn: npmCommand not pinned by hash: misc/npm-install.bash:5","Warn: npmCommand not pinned by hash: misc/npm-install.bash:6","Warn: npmCommand not pinned by hash: npm-install.bash:2","Warn: npmCommand not pinned by hash: npm-install.bash:3","Warn: npmCommand not pinned by hash: npm-install.bash:4","Warn: npmCommand not pinned by hash: npm-install.bash:5","Info:   0 out of   7 npmCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-17T18:31:04.285Z","repository_id":42788760,"created_at":"2025-08-17T18:31:04.285Z","updated_at":"2025-08-17T18:31:04.285Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30236042,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T19:01:10.287Z","status":"ssl_error","status_checked_at":"2026-03-07T18:59:58.103Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure"],"created_at":"2025-07-01T09:01:26.550Z","updated_at":"2026-03-07T23:01:18.192Z","avatar_url":"https://github.com/cloojure.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n= Making Clojure Even Sweeter\n\n=== image:https://img.shields.io/clojars/v/tupelo.svg[link=\"http://clojars.org/tupelo\"] \u003c= Current Version (click for dep string)\n=== image:https://cljdoc.org/badge/tupelo/tupelo[link=\"https://cljdoc.org/d/tupelo/tupelo/CURRENT\"] \u003c= Full API docs (click for cljdoc.org)\n==== Old-style link:http://cloojure.github.io/doc/tupelo/[API Docs] on GitHub Pages (codox)\n\n== Tupelo Overview\n\nHave you ever wanted to do something simple but clojure.core doesn't support it? Or, maybe you are\nwishing for an enhanced version of a standard function.  If so, then Tupelo is for you!  Tupelo is\na library of helper and convenience functions to make working with Clojure simpler, easier, and more\nbulletproof.\n\n== Tupelo Organization\n\nThe functionality of the Tupelo library is divided into a number of\nnamespaces, each with a single area of focus. These are:\n\n==== Tupelo Core - A library of helper functions for core Clojure.\n\nPlease see the xref:tupelo-core-overview[tupelo.core] docs further below.\n\n==== Tupelo-Forest - A library for searching \u0026 manipulating tree-like data structures\n\nPlease see the link:docs/forest.adoc[tupelo.forest] docs for further information.\n\n==== Tupelo-Datomic - A library of helper functions for Datomic.\n\nThe *_tupelo-datomic_* library has been split out into an independent project.  Please\nsee https://github.com/cloojure/tupelo-datomic[the tupelo-datomic project] for details.\n\n==== Tupelo CSV - Functions for using CSV (Comma Separate Value) files\n\nThe standard link:http://github.com/davidsantiago/clojure-csv[clojure-csv library] has well-tested\nand useful functions for parsing CSV (Comma Separated Value) text data, but it does not offer all of\nthe convenience one may wish. Tupelo CSV emphasizes the idomatic Clojure usage of data, using\nsequences and maps. Please see the link:http://cloojure.github.io/doc/tupelo/tupelo.csv.html[tupelo.csv] docs.\n\n==== Tupelo Parse - Functions to ease text parsing\n\nPlease see the link:http://cloojure.github.io/doc/tupelo/tupelo.parse.html[tupelo.parse] docs.\n\n==== Tupelo String - Functions to ease string operations\n\nPlease see the link:http://cloojure.github.io/doc/tupelo/tupelo.string.html[tupelo.string] docs.\n\n==== Tupelo Schema - Type Definitions\n\nEnables type checking in Clojure code via link:https://github.com/plumatic/schema[Plumatic Schema].\nPlease see link:https://github.com/cloojure/tupelo/blob/master/src/tupelo/schema.clj[the source code] for\ndefinitions, and\nlink:https://github.com/cloojure/tupelo-datomic/blob/master/test/tst/tupelo_datomic/bond.clj[the\nJames Bond example code] for examples of the type-checking in action.\n\n==== Tupelo Types - A collection of functions for testing object types\n\nPlease see the link:http://cloojure.github.io/doc/tupelo/tupelo.types.html[tupelo.types] docs.\n\n==== Tupelo Misc - A grab bag of functions that don't fit anywhere else (yet!)\n\nPlease see the link:http://cloojure.github.io/doc/tupelo/tupelo.misc.html[tupelo.misc] docs.\n\n==== tupelo.base64 - Convert to/from base64 encoding.\n\nPlease see the link:http://cloojure.github.io/doc/tupelo/tupelo.base64.html[tupelo.base64] docs.\n\n==== tupelo.base64url - Convert to/from base64url encoding.\n\nPlease see the link:http://cloojure.github.io/doc/tupelo/tupelo.base64url.html[tupelo.base64url] docs.\n\n==== Tupelo Y64 - Convert to/from the URL-safe Y64 encoding (Yahoo YUI library).\n\nPlease see the link:http://cloojure.github.io/doc/tupelo/tupelo.y64.html[tupelo.y64] docs.\n\n\n[[tupelo-core-overview]]\n\n\n== Tupelo Core Overview\n\nHave you ever wanted to do something simple but `clojure.core` doesn't support it? Or, maybe\nyou are wishing for an enhanced version of a standard function. The goal of `tupelo.core` is to\nadd support for these convenience features, so that you have a simple way of using either\nthe enhanced version or the original version.\n\nThe goal in using `tupelo.core` is that you can just plop it into any namespace without\nhaving to worry about any conflicts with `clojure.core` functionality. So, both the core functions\nand the added/enhanced functions are both available for use at all times. As such, we \nnormally refer tupelo.core into our namespace as follows:\n\n[source,clojure]\n----\n(ns my.proj\n  (:use tupelo.core)\n  (:require\n    [clojure.string :as str]\n    ... ))\n----\n\n=== Expression Debugging\n\nHave you ever been debugging some code and had trouble printing out intermediate\nvalues?  For example:\n\n[source,clojure]\n----\n(-\u003e 1\n    (inc)       ; want to print value in pipeline after \"(inc)\" expression\n    (* 2))\n4\n----\nSuppose you want to display the value after the `(inc)` function. You can't just insert a\n`(println ...)` because the return value of `nil` will break the pipeline structure. Instead,\njust use `spy`:\n\n[source,clojure]\n----\n(-\u003e 1\n    (inc)\n    (spy)       ; print value at this location in pipeline\n    (* 2))\n; spy =\u003e 2      ; output from spy\n4               ; return value from the threading pipeline\n----\nThis tool is named `spy` since it can display values from inside any threading form without\naffecting the result of the expression.  In this case, `spy` printed the value `2` resulting from\nthe `(inc)` expression. Then, the value `2` continued to flow through the following expressions in\nthe pipeline so that the return value of the expression is unchanged.\n\nYou can add in a keyword message to label each `spy` output:\n[source,clojure]\n----\n(-\u003e 1\n    (inc)\n    (spy :after-inc)      ; add a custom keyword message\n    (* 2))\n; :after-inc =\u003e 2          ; spy output is labeled with keyword message\n4                         ; return value is unchanged\n----\nNote that `spy` works equally well inside either a \"thread-first\" or a \"thread-last\" form\n(e.g. using `\\-\u003e` or `\\-\u003e\u003e`), without requiring any changes.\n\n[source,clojure]\n----\n(-\u003e\u003e 1\n    (inc)\n    (spy :after-inc)      ; spy works equally with both  -\u003e  and  -\u003e\u003e  forms\n    (* 2))\n; :after-inc =\u003e 2\n4\n----\n\nHow does `spy` accomplish this trick? The answer is that the keyword message is assumed to be the\nlabel, since interesting debug values are more likely to be strings, numbers, or collections like\nvectors \u0026 maps (if both args are keywords, an exception is thrown; use some other technique for\ndebugging this use-case).  Thus, `spy` can detect whether it is in a thread-first or thread-last\nform, and then label the output correctly.  A side benefit is that keywords like `:after-inc` or\njust `:110` are easy to grep for in output log files.\n\nAs a bonus for debugging, the value is output using (pr-str ...) so that numbers and strings are\nunambiguous in the output:\n\n[source,clojure]\n----\n(-\u003e 30\n    (+ 4)\n    (spy :dbg)\n    (* 10))\n; :dbg =\u003e 34            ; integer result = 34\n340\n\n(-\u003e \"3\"\n    (str \"4\")\n    (spy :dbg)\n    (str \"0\"))\n; :dbg =\u003e \"34\"          ; string result = \"34\"\n\"340\"\n----\n\nSometimes you may prefer to print out the literal expression instead of a\nkeyword label. In this case, just use `spyx` (short for \"spy expression\") :\n[source,clojure]\n----\n(it-\u003e 1                 ; tupelo.core/it-\u003e \n      (spyx (inc it))\n      (* 2 it))\n; (inc it) =\u003e 2     ; the expression is used as the label\n4\n----\n\nIn other instances, you may wish to use `spyxx` to display the expression, its\ntype, and its value:\n[source,clojure]\n----\n(defn mystery-fn [] (into (sorted-map) {:b 2 :a 1}))\n(spyxx (mystery-fn))\n;  (mystery-fn) =\u003e  \u003c#clojure.lang.PersistentTreeMap {:a 1, :b 2}\u003e\"\n----\n\nNon-pure functions (i.e. those with side-effects) are safe to use with `spy`.\nAny expression supplied to spy will be evaluated only once.\n\nSometimes you may just want to save some repetition for a simple printout:\n[source,clojure]\n----\n(def answer 42)\n(spyx answer)\n; answer =\u003e 42\n----\n\nTo be precise, the function signatures for the `spy` family are:\n[source,clojure]\n----\n(spy \u003cexpr\u003e)             ; print value of \u003cexpr\u003e w/o custom message string\n(spy \u003cexpr\u003e :kw-label)   ; works with -\u003e \n(spy :kw-label \u003cexpr\u003e)   ; works with -\u003e\u003e  \n(spyx  \u003cexpr\u003e)           ; prints \u003cexpr\u003e and its value\n(spyxx \u003cexpr\u003e)           ; prints \u003cexpr\u003e, its type, and its value\n----\n\nIf you are debugging a series of nested function calls, it can often be handy to indent the `spy`\noutput to help in visualizing the call sequence. Using `with-spy-indent` will give you just what you\nwant:\n\n[source,clojure]\n----\n(doseq [x [:a :b]]\n  (spyx x)\n  (with-spy-indent\n    (doseq [y (range 3)]\n      (spyx y))))\nx =\u003e :a\n  y =\u003e 0\n  y =\u003e 1\n  y =\u003e 2\nx =\u003e :b\n  y =\u003e 0\n  y =\u003e 1\n  y =\u003e 2\n----\n\n=== Literate Threading Macro\n\nWe all love to use the threading macros `\\-\u003e` and `\\-\u003e\u003e` for certain tasks, but they only work if\nall of the forms should be threaded into the first or last argument.\n\nThe built-in threading macro `as\\-\u003e` can avoid this problem, but the order of the first expression\nand the placeholder symbol is arguably backwards from what most users would expect. Also, there is\noften no obvious name to use for the placeholder symbol.  Re-using a good idea from Groovy (also \ncopied by Kotlin), we simply use the symbol `it` as the placeholder symbol in each expression \nto represent the value of the previous result.\n\n[source,clojure]\n----\n(it-\u003e 1\n      (inc it)                                  ; thread-first or thread-last\n      (+ it 3)                                  ; thread-first\n      (/ 10 it)                                 ; thread-last\n      (str \"We need to order \" it \" items.\" )   ; middle of 3 arguments\n;=\u003e \"We need to order 2 items.\" )\n----\n\nHere is a more complicated example. Note that we can assign into a local `let` block from the `it`\nplaceholder value:\n[source,clojure]\n----\n(it-\u003e 3\n      (spy :initial it)\n      (let [x it]\n        (inc x))\n      (spy it :222)\n      (* it 2)\n      (spyx it))\n; :initial =\u003e 3\n; :222 =\u003e 4\n; it =\u003e 8\n8           ; return value\n----\n\nMore examples link:it-thread.adoc[can be found here].\n\nThe `it\\-\u003e` macro has a cousin `cond-it\\-\u003e` that allows you to thread the updated value through both the conditional and the action\nexpressions:\n\n[source,clojure]\n----\n\n(let [params {:a 1 :b 1 :c nil :d nil}]\n  (cond-it-\u003e params\n    (:a it)        (update it :b inc)\n    (= (:b it) 2)  (assoc it :c \"here\")\n    (:c it)        (assoc it :d \"again\")))\n\n;=\u003e {:a 1, :b 2, :c \"here\", :d \"again\"}\n----\n\n=== Map Value Lookup\n\nMaps are convenient, especially when keywords are used as functions to look up a value in\na map.  Unfortunately, attempting to look up a non-existent keyword in a map will return\n`nil`.  While sometimes convenient, this means that a simple typo in the keyword name will\nsilently return corrupted data (i.e. `nil`) instead of the desired value.\n\nInstead, use the function `grab` for keyword/map lookup:\n[source,clojure]\n----\n(grab k m)\n  \"A fail-fast version of keyword/map lookup.  When invoked as (grab :the-key the-map),\n   returns the value associated with :the-key as for (clojure.core/get the-map :the-key).\n   Throws an Exception if :the-key is not present in the-map.\"\n\n(def sidekicks {:batman \"robin\" :clark \"lois\"})\n(grab :batman sidekicks)\n;=\u003e \"robin\"\n\n(grab :spiderman sidekicks)\n;=\u003e IllegalArgumentException Key not present in map:\nmap : {:batman \"robin\", :clark \"lois\"}\nkeys: [:spiderman]\n----\nThe function `grab` should also be used in place of `clojure.core/get`. Simply reverse the order of arguments to\nmatch the \"keyword-first, map-second\" convention.\n\nFor looking up values in nested maps, the function `fetch-in` replaces `clojure.core/get-in`:\n[source,clojure]\n----\n(fetch-in m ks)\n  \"A fail-fast version of clojure.core/get-in. When invoked as (fetch-in the-map keys-vec),\n   returns the value associated with keys-vec as for (clojure.core/get-in the-map keys-vec).\n   Throws an Exception if the path keys-vec is not present in the-map.\"\n\n(def my-map {:a 1 :b {:c 3}})\n(fetch-in my-map [:b :c])\n3\n(fetch-in my-map [:b :z])\n;=\u003e IllegalArgumentException Key seq not present in map:\n;=\u003e   map : {:b {:c 3}, :a 1}\n;=\u003e   keys: [:b :z]\n----\n\n=== Map Dissociation\n\nClojure has functions `assoc` \u0026 `assoc-in`, `update` \u0026 `update-in`, and `dissoc`. However, there\nis no function `dissoc-in`.  The Tupelo function `dissoc-in` provides the desired functionality:\n\n[source,clojure]\n----\n(dissoc-in the-map keys-vec)\n  \"A sane version of dissoc-in that will not delete intermediate keys.\n   When invoked as (dissoc-in the-map [:k1 :k2 :k3... :kZ]), acts like\n   (clojure.core/update-in the-map [:k1 :k2 :k3...] dissoc :kZ). That is, only\n   the map entry containing the last key :kZ is removed, and all map entries\n   higher than kZ in the hierarchy are unaffected.\"\n----\n\nThe unit test shows the functions in action:\n\n[source,clojure]\n----\n(let [my-map {:a { :b { :c \"c\" }}} ]\n  (is (= (dissoc-in my-map []         ) my-map ))\n  (is (= (dissoc-in my-map [:a      ] ) {} ))\n  (is (= (dissoc-in my-map [:a :b   ] ) {:a {}} ))\n  (is (= (dissoc-in my-map [:a :b :c] ) {:a { :b {}}} ))\n  (is (= (dissoc-in my-map [:a :x :y] ) {:a { :b { :c \"c\" }\n                                             :x nil }} )))\n----\n\nNote that if non-existant keys are included in `keys-vec`, any missing map\nlayers will be constructed as necessary, which is consistant with the behavior\nof both `clojure.core/assoc-in` and `clojure.core/update-in` (note that `nil` is\nthe value of the final map entry, not the empty map `{}` as for the other examples).\n\nNote that only the map entry corresponding to the last key `kZ` is cleared. This\ndiffers from the `dissoc-in` function in the old clojure-contrib library which\nhad the unpredictable behavior of recursively (\u0026 silently) deleting all keys in\n`keys-vec` corresponding to empty maps.\n\n=== Gluing Together Like Collections\n\nThe `concat` function can sometimes have rather surprising results:\n[source,clojure]\n----\n(concat {:a 1} {:b 2} {:c 3} )\n;=\u003e   ( [:a 1] [:b 2] [:c 3] )\n----\n\nIn this example, the user probably meant to merge the 3 maps into one. Instead, the three\nmaps were mysteriously converted into length-2 vectors, which were then nested inside another\nsequence.\n\nThe `conj` function can also surprise the user:\n[source,clojure]\n----\n(conj [1 2] [3 4] )\n;=\u003e   [1 2  [3 4] ]\n----\n\nHere the user probably wanted to get `[1 2 3 4]` back, but instead got a nested\nvector by mistake.\n\nInstead of having to wonder if the items to be combined will be merged, nested, or\nconverted into another data type, we provide the `glue` function to *always*\ncombine like collections together into a result collection of the same type:\n\n[source,clojure]\n----\n; Glue together like collections:\n(is (= (glue [ 1 2] '(3 4) [ 5 6] )       [ 1 2 3 4 5 6 ]  ))   ; all sequential (vectors \u0026 lists)\n(is (= (glue {:a 1} {:b 2} {:c 3} )       {:a 1 :c 3 :b 2} ))   ; all maps\n(is (= (glue #{1 2} #{3 4} #{6 5} )      #{ 1 2 6 5 3 4 }  ))   ; all sets\n(is (= (glue \"I\" \" like \" \\a \" nap!\" )   \"I like a nap!\"   ))   ; all text (strings \u0026 chars)\n\n; If you want to convert to a sorted set or map, just put an empty one first:\n(is (= (glue (sorted-map) {:a 1} {:b 2} {:c 3})   {:a 1 :b 2 :c 3} ))\n(is (= (glue (sorted-set) #{1 2} #{3 4} #{6 5})  #{ 1 2 3 4 5 6  } ))\n----\n\nAn `Exception` will be thrown if the collections to be 'glued' are not all of\nthe same type. The allowable input types are:\n\n  - all sequential: any mix of lists \u0026 vectors (vector result)\n  - all maps (sorted or not)\n  - all sets (sorted or not)\n  - all text: any mix of strings \u0026 characters (string result)\n\n=== Adding Values to the Beginning or End of a Sequence\n\nClojure has the `cons`, `conj`, and `concat` functions, but it is not obvious how they should be\nused to add a new value to the beginning of a vector or list:\n\n[source,clojure]\n----\n; Add to the end\n\u003e (concat [1 2] 3)    ;=\u003e IllegalArgumentException\n\u003e (cons   [1 2] 3)    ;=\u003e IllegalArgumentException\n\u003e (conj   [1 2] 3)    ;=\u003e [1 2 3]\n\u003e (conj   [1 2] 3 4)  ;=\u003e [1 2 3 4]\n\u003e (conj  '(1 2) 3)    ;=\u003e (3 1 2)       ; oops\n\u003e (conj  '(1 2) 3 4)  ;=\u003e (4 3 1 2)     ; oops\n\n; Add to the beginning\n\u003e (conj     1  [2 3] ) ;=\u003e ClassCastException\n\u003e (concat   1  [2 3] ) ;=\u003e IllegalArgumentException\n\u003e (cons     1  [2 3] ) ;=\u003e (1 2 3)\n\u003e (cons   1 2  [3 4] ) ;=\u003e ArityException\n\u003e (cons     1 '(2 3) ) ;=\u003e (1 2 3)\n\u003e (cons   1 2 '(3 4) ) ;=\u003e ArityException \n----\n\nDo you know what `conj` does when you pass it `nil` instead of a sequence?  It silently replaces it\nwith an empty list:  `(conj nil 5)` =\u003e `(5)`  This can cause you to accumulate items in reverse\norder if you aren't aware of the default behavior:\n\n[source,clojure]\n----\n(-\u003e nil\n  (conj 1)\n  (conj 2)\n  (conj 3))\n;=\u003e (3 2 1)\n----\n\nThese failures are irritating and unproductive, and the error messages don't make it obvious what\nwent wrong.  Instead, use the simple `prepend` and `append` functions to add new elements to the\nbeginning or end of a sequence, respectively:\n\n[source,clojure]\n----\n(append [1 2] 3  )   ;=\u003e [1 2 3  ]\n(append [1 2] 3 4)   ;=\u003e [1 2 3 4]\n\n(prepend   3 [2 1])  ;=\u003e [  3 2 1]\n(prepend 4 3 [2 1])  ;=\u003e [4 3 2 1]\n----\n\nBoth `prepend` and `append` always return a vector result.\n\n=== Combining Scalars and Vectors \n\nSuppose we have a mixture of scalars \u0026 vectors (or lists) that we want to combine into a single\nvector. We want a function `???` to give us the following result:\n\n[source,clojure]\n----\n(???  1 2 3 [4 5 6] 7 8 9)  =\u003e  [1 2 3 4 5 6 7 8 9]\n----\n\nClojure doesn't have a function for this.  Instead we need to wrap all of the scalars into vectors\nand then use `glue` or `concat`:\n\n[source,clojure]\n----\n; can wrap individually or in groups\n(glue [1   2   3] [4 5 6] [7   8   9])  =\u003e  [1 2 3 4 5 6 7 8 9]   ; could also use concat\n(glue [1] [2] [3] [4 5 6] [7] [8] [9])  =\u003e  [1 2 3 4 5 6 7 8 9]   ; could also use concat\n----\n\nIt may be inconvenient to always wrap the scalar values into vectors just to combine them with an\noccasional vector value. Instead, it might be more convenient to ***unwrap*** the vector values,\nthen combine the result with other scalars. We can do that with the `\\-\u003evector` and `unwrap` functions:\n\n[source,clojure]\n----\n(-\u003evector 1 2 3 4 5 6 7 8 9)             =\u003e  [1 2 3 4 5 6 7 8 9]\n(-\u003evector 1 (unwrap [2 3 4 5 6 7 8]) 9)  =\u003e  [1 2 3 4 5 6 7 8 9]\n----\n\nIt will also work recursively for nested `unwrap` calls:\n\n[source,clojure]\n----\n(-\u003evector 1 (unwrap [2 3 (unwrap [4 5 6]) 7 8]) 9)  =\u003e  [1 2 3 4 5 6 7 8 9]\n----\n\n\n=== Removing Values from a Sequence\n\nSuppose you want to remove an element form a sequence. \nDid you know that Clojure has no equivalent to Java's `List.remove(int index)` function? Well, now it does:\n\n[source,clojure]\n----\n(s/defn drop-at :- ts/List\n  \"Removes an element from a collection at the specified index.\"\n  [coll     :- ts/List\n   index    :- s/Int]\n  ...)\n\n(is (= [  1 2] (drop-at (range 3) 0)))\n(is (= [0   2] (drop-at (range 3) 1)))\n(is (= [0 1  ] (drop-at (range 3) 2)))\n----\n\nUnlike the raw `take` and `drop` functions on which it is based, `drop-at` will throw an exception\nfor invalid values of `index`.\n\n=== Inserting Values into a Sequence\n\nSuppose you want to insert an element into a sequence. Tupelo has you covered here as well:\n\n[source,clojure]\n----\n(s/defn insert-at :- ts/List\n  \"Inserts an element into a collection at the specified index.\"\n  [coll     :- ts/List\n   index    :- s/Int\n   elem     :- s/Any]\n  ...)\n\n(is (= [9 0 1] (insert-at [0 1] 0 9)))\n(is (= [0 9 1] (insert-at [0 1] 1 9)))\n(is (= [0 1 9] (insert-at [0 1] 2 9)))\n----\n\nAs with `assoc`, you are allowed to insert the new element into the first empty slot after all\nexisting elements, but no further.  `insert-at` will throw an exception for invalid values of `index`.\n\n=== Replacing Values in a Sequence\n\nAnd, of course, you can also replace an element in a sequence:\n\n[source,clojure]\n----\n(s/defn replace-at :- ts/List\n  \"Replaces an element in a collection at the specified index.\"\n  [coll     :- ts/List\n   index    :- s/Int\n   elem     :- s/Any]\n   ...)\n\n(is (= [9 1 2] (replace-at (range 3) 0 9)))\n(is (= [0 9 2] (replace-at (range 3) 1 9)))\n(is (= [0 1 9] (replace-at (range 3) 2 9)))\n----\n\nAs with `drop-at`, `replace-at` will throw an exception for invalid values of `index`.\n\n=== Convenience in Testing Seq's\n\nClojure has an `empty?` function to indicate if a collection has zero elements or is `nil` (i.e. not\npresent).  However, clojure has no corresponding `not-empty?` function, and people have written into\nthe mailing wondering where it is.  Well, now it is available:\n\n[source,clojure]\n----\n(not-empty? coll)\n \"For any collection, returns true if coll contains any items;\n  otherwise returns false. Equivalent to (not (empty? coll)).\"\n----\nThe unit test shows it in action:\n\n[source,clojure]\n----\n(is (= (map not-empty? [\"1\"   [1]   '(1)  {:1 1}  #{1} ] )\n                       [true  true  true  true    true ]  ))\n(is (= (map not-empty? [\"\"     []      '()    {}     #{}    nil   ] )\n                       [false  false   false  false  false  false ] ))\n\n(is (= (keep-if not-empty?  [\"1\" [1] '(1) {:1 1} #{1} ] )\n                            [\"1\" [1] '(1) {:1 1} #{1} ] ))\n(is (= (drop-if not-empty?  [\"\"  []  '()  {}     #{}  nil] )\n                            [\"\"  []  '()  {}     #{}  nil] ))\n----\n\nJust to confuse things, Clojure does have the similarly named functions `empty` and `not-empty`.\nBe sure to avoid these two functions for predicate tests.\n\nA similar, but more complicated, situation exists in the case of `not-any?`.  \nClojure has the `not-any?` function to indicate if a predicate is false for all items\nin a collection. However, there has never been a corresponding `any?` function such that\n\n[source,clojure]\n----\n  (= (not-any?  pred coll) \n     (not (any? pred coll)))\n----\nfor any predicate and collection. The situation has become more confusion as of Clojure\n  1.9.0-alpha10 since a completely unrelated function `any?` has been added in support of\n  `clojure.spec`.  The new `any?` function is defined as:\n\n[source,clojure]\n----\n(defn any?\n  \"Returns true given any argument.\"\n  [x] true)\n----\nSo the new `any?` function is a semantic mismatch to the `not-any?` function and \nis completely unrelated to testing a collection using a predicate.\n\nThe Tupelo library attempts to resolve this confusing situation by providing both positive and\nnegative versions of the collection test with a name which does not conflict with either\n`any?` or `not-any?` in `clojure.core`:\n\n[source,clojure]\n----\n(has-some? pred coll)\n  \"For any predicate pred \u0026 collection coll, returns true if (pred x) is logical true for at least one x in\n   coll; otherwise returns false.  Like clojure.core/some, but returns only true or false.\"\n\n(has-none? pred coll)\n  \"For any predicate pred \u0026 collection coll, returns false if (pred x) is logical true for at least one x in\n   coll; otherwise returns true.  Equivalent to clojure.core/not-any?, and is the inverse of has-some?.\"\n----\n\nThe unit test shows these functions in action:\n\n[source,clojure]\n----\n(is (= true   (has-some? odd? [1 2 3] ) ))\n(is (= false  (has-some? odd? [2 4 6] ) ))\n(is (= false  (has-some? odd? []      ) ))\n\n(is (= false  (has-none? odd? [1 2 3] ) ))\n(is (= true   (has-none? odd? [2 4 6] ) ))\n(is (= true   (has-none? odd? []      ) ))\n----\n\n=== Searching for entries in Collections, Maps, and Sets\n\nSometimes we want an easy way to find out if an item is n a collection.  The Tupelo library supplies\nthree convenient functions for this purpose: `contains-elem?`, `contains-key?`, and `contains-val?`.  \n\nThe most generic function is `contains-elem?`, which is intended for vectors or any other clojure `seq`:\n\n[source,clojure]\n----\n(testing \"vecs\"\n  (let [coll (range 3)]\n    (isnt (contains-elem? coll -1))\n    (is   (contains-elem? coll  0))\n    (is   (contains-elem? coll  1))\n    (is   (contains-elem? coll  2))\n    (isnt (contains-elem? coll  3))\n    (isnt (contains-elem? coll  nil)))\n\n  (let [coll [ 1 :two \"three\" \\4]]\n    (isnt (contains-elem? coll  :no-way))\n    (isnt (contains-elem? coll  nil))\n    (is   (contains-elem? coll  1))\n    (is   (contains-elem? coll  :two))\n    (is   (contains-elem? coll  \"three\"))\n    (is   (contains-elem? coll  \\4)))\n\n  (let [coll [:yes nil 3]]\n    (isnt (contains-elem? coll  :no-way))\n    (is   (contains-elem? coll  :yes))\n    (is   (contains-elem? coll  nil))))\n----\n\nHere we see that for an integer range or a mixed vector, `contains-elem?` works as expected for both\nexisting and non-existant elements in the collection.  For maps, we can also search for any\nkey-value pair (expressed as a len-2 vector):\n\n[source,clojure]\n----\n(testing \"maps\"\n   (let [coll {1 :two \"three\" \\4}]\n     (isnt (contains-elem? coll nil ))\n     (isnt (contains-elem? coll [1 :no-way] ))\n     (is   (contains-elem? coll [1 :two]))\n     (is   (contains-elem? coll [\"three\" \\4])))\n   (let [coll {1 nil \"three\" \\4}]\n     (isnt (contains-elem? coll [nil 1] ))\n     (is   (contains-elem? coll [1 nil] )))\n   (let [coll {nil 2 \"three\" \\4}]\n     (isnt (contains-elem? coll [1 nil] ))\n     (is   (contains-elem? coll [nil 2] ))))\n----\n\nIt is also straightforward to search a set:\n\n[source,clojure]\n----\n(testing \"sets\"\n  (let [coll #{1 :two \"three\" \\4}]\n    (isnt (contains-elem? coll  :no-way))\n    (is   (contains-elem? coll  1))\n    (is   (contains-elem? coll  :two))\n    (is   (contains-elem? coll  \"three\"))\n    (is   (contains-elem? coll  \\4)))\n\n  (let [coll #{:yes nil}]\n    (isnt (contains-elem? coll  :no-way))\n    (is   (contains-elem? coll  :yes))\n    (is   (contains-elem? coll  nil)))))\n----\n\nFor maps \u0026 sets, it is simpler (\u0026 more efficient) to use `contains-key?` to find a map entry or a\nset element:\n\n[source,clojure]\n----\n(deftest t-contains-key?\n  (is   (contains-key?  {:a 1 :b 2} :a))\n  (is   (contains-key?  {:a 1 :b 2} :b))\n  (isnt (contains-key?  {:a 1 :b 2} :x))\n  (isnt (contains-key?  {:a 1 :b 2} :c))\n  (isnt (contains-key?  {:a 1 :b 2}  1))\n  (isnt (contains-key?  {:a 1 :b 2}  2))\n\n  (is   (contains-key?  {:a 1 nil   2} nil))\n  (isnt (contains-key?  {:a 1 :b  nil} nil))\n  (isnt (contains-key?  {:a 1 :b    2} nil))\n\n  (is   (contains-key? #{:a 1 :b 2} :a))\n  (is   (contains-key? #{:a 1 :b 2} :b))\n  (is   (contains-key? #{:a 1 :b 2}  1))\n  (is   (contains-key? #{:a 1 :b 2}  2))\n  (isnt (contains-key? #{:a 1 :b 2} :x))\n  (isnt (contains-key? #{:a 1 :b 2} :c))\n\n  (is   (contains-key? #{:a 5 nil   \"hello\"} nil))\n  (isnt (contains-key? #{:a 5 :doh! \"hello\"} nil))\n\n  (throws? (contains-key? [:a 1 :b 2] :a))\n  (throws? (contains-key? [:a 1 :b 2]  1)))\n----\n\nAnd, for maps, you can also search for values with `contains-val?`:\n\n[source,clojure]\n----\n(deftest t-contains-val?\n  (is   (contains-val? {:a 1 :b 2} 1))\n  (is   (contains-val? {:a 1 :b 2} 2))\n  (isnt (contains-val? {:a 1 :b 2} 0))\n  (isnt (contains-val? {:a 1 :b 2} 3))\n  (isnt (contains-val? {:a 1 :b 2} :a))\n  (isnt (contains-val? {:a 1 :b 2} :b))\n\n  (is   (contains-val? {:a 1 :b nil} nil))\n  (isnt (contains-val? {:a 1 nil  2} nil))\n  (isnt (contains-val? {:a 1 :b   2} nil))\n\n  (throws? (contains-val?  [:a 1 :b 2] 1))\n  (throws? (contains-val? #{:a 1 :b 2} 1)))\n----\n\nAs seen in the test, each of these functions works correctly when for searching for `nil` values.\n\n=== Focus on Vectors\n\nClojure's seq abstraction (and lazy seq's) is very useful, but sometimes you just want everything to\nstay in a nice, eager, random-access vector.  Here is an eager (non-lazy) version of `for` which\nalways returns results in a vector:\n\n[source,clojure]\n----\n(is= (forv [x (range 4)] (* x x))\n       [0 1 4 9] )\n----\n\n=== Simplified Lazy Sequence Generation\n\nClojure training materials seem to vary somewhat in the recommended form for the generation of a lazy sequence. This\nis further complicated by the legacy function `lazy-cat` which can easily cause an out-of-memory error\n(link:https://stuartsierra.com/2015/04/26/clojure-donts-concat[please see this post]).\nA simpler form is possible using `tupelo.core/lazy-cons` macro.  An example\nof this form in use is:\n\n[source,clojure]\n----\n(defn lazy-countdown [n]\n  (when (\u003c= 0 n)\n    (lazy-cons n (lazy-countdown (dec n)))))\n\n(deftest t-all\n  (is= (lazy-countdown  5) [5 4 3 2 1 0] )\n  (is= (lazy-countdown  1) [1 0] )\n  (is= (lazy-countdown  0) [0] )\n  (is= (lazy-countdown -1) nil ))\n----\n\nThe new macro `lazy-cons` accepts the output value as the first arg, and a recursive function call\nas the second arg. The recursive call will have delayed-execution and will not be invoked until it is required.\nThe `(when \u003ccondition\u003e)` form returns `nil` to signal the termination of the lazy sequence.\n\n*_Implementation note:_*\n\nThe canonical structure of `when` and `lazy-cons` shown above is not required, but is probably the simplest of multiple\npossible choices. The new form of `(lazy-cons val (recursive-call...))` is nothing but a simplification\nof the original `clojure.core` form `(lazy-seq (cons val (recursive-call...)))` which reduces typing and \nthe possibility of errors.\n\nPlease note that `tupelo.core/lazy-cons` bears no relation to the historical `lazy-cons` which was\nbriefly considered for `clojure.core` circa 2008.\n\n=== Generator Functions for Lazy Sequences (a la Python)\n\nOne of the nice features of Python is the ability to use Generator Functions. These allow a function to \"yield\"\na result from anywhere in the code, which is placed in a lazy output buffer for consumption by the calling function.\nThe generator function is \"paused\" until the output value is consumed, then resumes execution where it left off\nwith all local state preserved.  This ability is especially handy when you have nested loops or other structures\nthat make it inconvenient to return a result as the last expression in a function.\n\n[source,clojure]\n----\n(defn concat-gen    ; concat a list of collections\n  [\u0026 collections]\n  (lazy-gen\n    (doseq [curr-coll collections]\n      (doseq [item curr-coll]\n        (yield item)))))\n\n(defn concat-gen-pair\n  [\u0026 collections]\n  (lazy-gen\n    (doseq [curr-coll collections]\n      (doseq [item curr-coll]\n        (yield-all [item item])))))\n\n(def c1 [1 2 3])\n(def c2 [4 5 6])\n(def c3 [7 8 9])\n\n(is= [1 2 3 4 5 6 7 8 9]                            (concat-gen       c1 c2 c3))\n(is= [1 1  2 2  3 3  4 4  5 5  6 6  7 7  8 8  9 9]  (concat-gen-pair  c1 c2 c3))\n----\n\n`lazy-gen` uses a `core.async` channel to buffer output, with a default buffer size of 32 (controlled by\nthe dynamic var `*lazy-gen-buffer-size*`). Result values passed to `yield` generate a lazy sequence that is the\nresult of the (lazy-gen ...) macro. The closely-related function `yield-all` inserts the elements of a collection\nonto the output stream instead of just a single value.  Besides `doseq`, `lazy-gen` is also very handy for\ngenerating a lazy seq within a `loop`-`recur` expression.\n\n=== Validating Intermediate Results\n\nWithin a processing chain, it is often desirable to verify that an intermediate value is\nwithin an expected range or of an expected type. The built-in `assert` function cannot be\nused for this purpose since it returns `nil`, and the Plumatic Schema `validate` can only\nperform a limited amount of type testing.  The `(validate ...)` function performs\narbitrary validation, throwing an exception if a non-truthy result is returned:\n\n[source,clojure]\n----\n(validate tstfn tstval)\n \"Used to validate intermediate results. Returns tstval if the result of\n  (tstfn tstval) is truthy.  Otherwise, throws IllegalStateException.\"\n\n(is (= 3    (validate pos?        3    )))\n(is (= 3.14 (validate number?     3.14 )))\n(is (= 3.14 (validate #(\u003c 3 % 4)  3.14 )))\n----\n\nA closely related function is `verify`.  It is like validate but accepts an expression instead of a\npredicate/value pair. Upon success, the expression value is returned; otherwise an exception is thrown:\n\n\n[source,clojure]\n----\n(throws? (verify (= 1 2)))\n(is= 333 (verify (* 3 111))))\n----\n\n=== Convenient Wild-Card Matches\n\nSometimes in testing, we want to verify that a key-value pair is present in a map, but we\ndon't know or care what the value is.  For example, Datomic returns maps containing the key\n`:db/id`, but the associated value is unpredictable. Tupelo provides the `(matches? ...)`\nexpression to make these tests a snap:\n\n[source,clojure]\n----\n(matches? pattern \u0026 values)\n\n(matches? { :a 1 :b _       }\n          { :a 1 :b 99      }\n          { :a 1 :b [1 2 3] }\n          { :a 1 :b nil     } )   ;=\u003e true\n(matches? [1 _ 3] [1 2 3] )       ;=\u003e true\n----\nNote that a wildcard can match either a primitive or a composite value. It works for both maps\nand vectors. The only restriction is that the wildcard symbol `_` (underscore) cannot be used as\na key in the pattern-map (it can be used anywhere in a vector-pattern).\"\n\n=== Fast \u0026 Simple Wild-Card Matches\n\nSometimes using `core.match` is overkill. For some patterns \u0026 values it can run very slowly or even\ncreate a stack overflow exception.  For most cases, all you really need is a simple wildcard match.\n\nThe `wild-match?` function returns `true` if a pattern is matched by one or more values.  The special\nkeyword `:*` (colon-star) in the pattern serves as a wildcard value.  Note that a wildcard can match\neither a primitive or a composite value: Usage:\n\n[source,clojure]\n----\n(wild-match? pattern \u0026 values)\n----\n\nSamples:\n[source,clojure]\n----\n(wild-match?  {:a :* :b 2} \n              {:a 1  :b 2})         ;=\u003e true\n\n(wild-match?  [1 :* 3]\n              [1 2  3]\n              [1 9  3] ))           ;=\u003e true\n\n(wild-match?  {:a :*       :b 2} \n              {:a [1 2 3]  :b 2})   ;=\u003e true\n----\n\n\n=== Map Entries (Key-Value pairs)\n\nSometimes you want to extract the keys \u0026 values from a map for manipulation or extension\nbefore building up another map (especially useful for manipulating default function args).\nHere is very handy function for that:\n\n[source,clojure]\n----\n(keyvals m)\n \"For any map m, returns the keys \u0026 values of m as a vector,\n  suitable for reconstructing via (apply hash-map (keyvals m)).\"\n\n(keyvals {:a 1 :b 2})\n;=\u003e [:b 2 :a 1]\n(apply hash-map (keyvals {:a 1 :b 2}))\n;=\u003e {:b 2, :a 1}\n----\n\n=== Default Value in Case of Exception\n\nSometimes you know an operation may result in an Exception, and you would like to have the\nException converted into a default value.  That is when you need:\n\n[source,clojure]\n----\n(with-exception-default default-val \u0026 body)\n \"Evaluates body \u0026 returns its result.  In the event of an exception the\n  specified default value is returned instead of the exception.\"\n\n(with-exception-default 0\n  (Long/parseLong \"12xy3\"))\n;=\u003e 0\n----\n\n\nThis feature is put to good use in link:http://cloojure.github.io/doc/tupelo/tupelo.parse.html[tupelo.parse],\nwhere you will find functions that work like this:\n\n[source,clojure]\n----\n(parse-long \"123\")                  ; throws if parse error\n;=\u003e 123\n(parse-long \"1xy23\" :default 666)   ; returns default val if parse error\n;=\u003e 666\n----\n\n=== Floating Point Number Comparison\n\nEveryone knows that you shouldn't compare floating-point numbers (e.g. float,\ndouble, etc) for equality since roundoff errors can prevent a precise match\nbetween logically equivalent results.  However, it has always been awkward to\nregenerate \"approx-equals\" code by hand every time new project requires it.\nHere we have a simple function that compares two floating-point values (cast to\ndouble) for relative equality by specifying either the number of significant\ndigits that must match or the maximum error tolerance allowed:\n\n[source,clojure]\n----\n(rel= val1 val2 \u0026 opts)\n \"Returns true if 2 double-precision numbers are relatively equal, else false.\n  Relative equality is specified as either (1) the N most significant digits are\n  equal, or (2) the absolute difference is less than a tolerance value.  Input\n  values are coerced to double before comparison.\"\n----\n\nAn extract from the unit tests illustrates the use of `rel=`\n\n[source,clojure]\n----\n(is      (rel=   123450000   123456789 :digits 4 ))       ; .12345 * 10^9\n(is (not (rel=   123450000   123456789 :digits 6 )))\n(is      (rel= 0.123450000 0.123456789 :digits 4 ))       ; .12345 * 1\n(is (not (rel= 0.123450000 0.123456789 :digits 6 )))\n\n(is      (rel= 1 1.001 :tol 0.01 ))                       ; :tol value is absolute error\n(is (not (rel= 1 1.001 :tol 0.0001 )))\n----\n\nNote that, for the :digits variant, _'equality'_ is truly relative, since only the N most significant\ndigits of each value must match.\n\n=== String Operations\n\nBe sure to see the dedicated functions \nlink:http://cloojure.github.io/doc/tupelo/tupelo.string.html[in the tupelo.string namespace!]\n\nSuppose you have a bunch of nested results and you just want to convert everything into a single\nstring. In that case, `strcat` is for you:\n\n[source,clojure]\n----\n(is (= (strcat \"I \" [ \\h \\a nil \\v [\\e \\space (byte-array [97])\n                      [ nil 32 \"complicated\" (Math/pow 2 5) '( \"str\" nil \"ing\") ]]] )\n       \"I have a complicated string\" ))\n----\nNote that any `nil` values map to the empty string as with `clojure.core/str`.\n\nSometimes, you may wish to clip a string to a maximum length for ease of display. In that case, use `clip-str`:\n\n[source,clojure]\n----\n(is (= \"abc\"             (clip-str  3 \"abcdefg\")))\n(is (= \"{:a 1, :\"        (clip-str  8 (sorted-map :a 1 :b 2) )))\n(is (= \"{:a 1, :b 2}\"    (clip-str 99 (sorted-map :a 1 :b 2) )))\n----\n\nNotice that clip-str will accept any argument type (map, sequence, etc), and convert it into a\nstring for you. Also, it will work correctly even if the clip-length is an upper bound; shorter\nstrings are returned unchanged.\n\n=== Keeping \u0026 Dropping Elements of a Sequence\n\nWhen processing sequences of data, we often need to extract a sequence of desired data, or,\nconversely, remove all of the undesired elements.\nHave you ever been left wondering which of these two forms is correct?\n\n[source,clojure]\n----\n(let [result (filter even? (range 10)) ]\n  (assert (or (= result [ 1 3 5 7 9 ] )     ; is it \"remove bad\" (falsey)\n              (= result [ 0 2 4 6 8 ] ))))  ; or    \"keep good\"  (truthy) ???\n----\n\nI normally think of filters as removing bad things.  Air filters remove dust.  Coffee filters keep\ncoffee grounds out of my cup. A noise filter in my stereo removes contaminating frequencies from my\nmusic. However, `filter` in Clojure is written in reverse, so that it *_keeps_* items identified by\nthe predicate. Wouldn't be nicer (and much less ambiguous) if you could just write the following?\n\n[source,clojure]\n----\n(is (= [0 2 4 6 8]  (keep-if even? (range 10))\n                    (drop-if odd?  (range 10))))\n----\n\nIt seems to me that `keep-if` and `drop-if` are much more natural names and remove ambiguity from\nthe code.  Of course, these are just thin wrappers around the built-in `clojure.core`\nfunctions, but they are much less ambiguous. I think they make the code easier to read and the\nintent more obvious.\n\n=== Keeping \u0026 Dropping Elements from a Map or Set\n\nThe two functions `keep-if` and `drop-if` can be used equally well in order to retain or remove\nelements from a clojure map or set. The semantics for sets look the same as for a sequence (vector\nor list). The predicate can be any 1-arg function:\n\n[source,clojure]\n----\n(keep-if even? #{1 2 3 4 5} )\n;=\u003e #{4 2}\n(drop-if even? #{1 2 3 4 5} )\n;=\u003e #{1 3 5}\n----\n\nNotice that the functions recognized the input collection as a set, and returned a set as the\nresult.  Very convenient.\n\nFor maps, each element is a MapEntry, which contains both a key and value. `keep-if` and `drop-if`\nunderstand maps, and will destructure each MapEntry. Thus, the predicate function can be any 2-arg\nfunction:\n\n[source,clojure]\n----\n(def mm {10  0,   20 0\n         11  1,   21 1\n         12  2,   22 2\n         13  3,   23 3} )\n\n(is (= (keep-if   (fn [k v] (odd?  v))  mm)\n       (drop-if   (fn [k v] (even? v))  mm)\n        {11  1,   21 1\n         13  3,   23 3} ))\n\n(is (= (keep-if  (fn [k v] (\u003c k 19))  mm)\n       (drop-if  (fn [k v] (\u003e k 19))  mm)\n        {10  0\n         11  1\n         12  2\n         13  3} ))\n----\n\nAs with sets, the functions recognized that a map was supplied, accepted a 2-arg predicate function, and\nreturned back a map to the user.\n\nBoth `keep-if` and `drop-if` will throw an Exception if the predicate function supplied has the\nwrong arity, or if the supplied collection is not one of either the sequential (vector or list),\nmap, or set data types.\n\n\n=== Extracting *_Only_* Values\n\nThe pervasive use of seq's in Clojure means that scalar values often appear wrapped in a vector or\nsome other sequence type.  As a result, one often sees code like `(first some-var)` and it is not\nalways clear that the code is simply \"unwrapping\" a scalar value, since there could well be\nremaining values in the sequence. Indeed, for a length-1 sequence it would be equally valid\nto use `(last some-var)` since first=last if there is only one item in the list.\n\nTo clarify that we are simply _unwrapping_ a single value from\nthe sequence, we may use the function `only`:\n\n[source,clojure]\n----\n(only seq-arg)\n \"Ensures that a sequence is of length=1, and returns the only value present.\n  Throws an exception if the length of the sequence is not one.  Note that,\n  for a length-1 sequence S, (first S), (last S) and (only S) are equivalent.\"\n----\n\n=== Getting Past Second Base\n\nClojure has the functions `first`, `second`, and requires the use of `nth` for any subsequent\nposition.  Sometimes it is handy to have a quick way to grab the 3rd item from a sequential\ncollection. Tupelo provides the `third` function to fill this void:\n\n[source,clojure]\n----\n(is= nil (third [       ]))\n(is= nil (third [1      ]))\n(is= nil (third [1 2    ]))\n(is= 3   (third [1 2 3  ]))\n(is= 3   (third [1 2 3 4]))\n----\n\n=== The Truth Is Not Ambiguous\n\nClojure marries the worlds of Java and Lisp. Unfortunately, these two worlds have different ideas of\ntruth, so Clojure accepts both `false` and `nil` as _false_. Sometimes, however, you want to coerce\nlogical values into literal _true_ or _false_ values, so we provide a simple way to do that:\n\n[source,clojure]\n----\n(truthy? arg)\n \"Returns true if arg is logical true (neither nil nor false);\n  otherwise returns false.\"\n\n(falsey? arg)\n \"Returns true if arg is logical false (either nil or false);\n  otherwise returns false. Equivalent to (not (truthy? arg)).\"\n----\n\nSince `truthy?` and `falsey?` are functions (instead of special forms or\nmacros), we can use them as an argument to `filter` or any other place that a\nhigher-order-function is required:\n\n[source,clojure]\n----\n(def data [true :a 'my-symbol 1 \"hello\" \\x false nil])\n(filter truthy? data)\n;=\u003e [true :a my-symbol 1 \"hello\" \\x]\n(filter falsey? data)\n;=\u003e [false nil]\n\n(is (every? truthy? [true :a 'my-symbol 1 \"hello\" \\x] ))\n(is (every? falsey? [false nil] ))\n\n(let [count-if (comp count keep-if) ]\n  (let [num-true    (count-if truthy? data)   ; \u003c= better than (count-if boolean data)\n        num-false   (count-if falsey? data) ] ; \u003c= better than (count-if not     data)\n    (is (and  (= 6 num-true)\n              (= 2 num-false) )))))\n----\n\n=== Keeping It Simple with `not-nil?`\n\nClojure has the build-in function `some` to return the first _truthy value_ from a _sequence_\nargument. It also has the poorly named function `some?` which returns the _value_ `true` if a\n_scalar_ argument satisfies `(not (nil? arg))`. It is easy to confuse `some` and `some?`, not only\nin their return type but also in the argument they accept (sequence or scalar).  In keeping with the\nstyle for other basic test functions, we provide the function `not-nil?` as the opposite of `nil?`.\n\nThe unit tests show how `not-nil?` leads to a more natural code syntax:\n\n[source,clojure]\n----\n(let [data [true :a 'my-symbol 1 \"hello\" \\x false nil] ]\n  (let [notties   (keep-if not-nil? data)\n        nillies   (drop-if not-nil? data) ]\n    (is (and  (= notties [true :a 'my-symbol 1 \"hello\" \\x false] )\n              (= nillies [nil] )))\n    (is (every?   not-nil? notties))        ; the 'not' can be used\n    (is (not-any?     nil? notties)))       ;   in either first or 2nd positon\n\n  (let [count-if (comp count keep-if) ]\n    (let [num-valid-1     (count-if some?    data)    ; awkward phrasing, doesn't feel natural\n          num-valid-2     (count-if not-nil? data)    ; matches intent much better\n          num-nil         (count-if nil?     data) ]  ; intent is plain\n      (is (and (= 7 num-valid-1 num-valid-2 )\n               (= 1 num-nil))))))\n----\n\n=== Identifying Sequences  \n\n*Update 2016-6-13: Now included in clojure.core 1.9.0-alpha5!*\n\nIn some situations, a function may need to verify that an argument is _seqable_, that is, will a\ncall to `(seq some-arg)` succeed?  If so, `some-arg` may be interpreted as a sequence of values.\nClojure doesn't have a built-in function for this (please note that `seqable?` is different from\n`seq?`), but we can copy an solution from the old `clojure.contrib.core/seqable`:\n\n[source,clojure]\n----\n(is (seqable?   \"abc\"))\n(is (seqable?   {1 2 3 4} ))\n(is (seqable?  #{1 2 3} ))\n(is (seqable?  '(1 2 3) ))\n(is (seqable?   [1 2 3] ))\n(is (seqable?   (byte-array [1 2] )))\n\n(is (not (seqable?  1 )))\n(is (not (seqable? \\a )))\n----\n\n== Change Log\n\nPlease see the link:changelog.adoc[the ChangeLog for details] docs.\n\n== Other useful libraries\n\nThere are several other libraries that provide useful value-added functionality to clojure.core:\n\n  - link:https://github.com/weavejester/medley[Medley]\n  - link:https://github.com/plumatic/plumbing[Plumatic Plumbing]\n  - link:https://github.com/marick/suchwow[Such Wow] \n  - link:http://www.clojure-toolbox.com/[The Clojure Toolbox] - For a comprehehsive list of Clojure libraries\n\n== Requirements\n\n - Clojure 1.8.0\n - Java 1.8\n\n== License\n\nCopyright © 2015-2017  Alan Thompson\n\nDistributed under the link:https://www.eclipse.org/legal/epl-v10.html[Eclipse Public License], the same as Clojure.\n\n== Development Environment\n\nDeveloped using link:https://www.jetbrains.com/idea/[*IntelliJ IDEA*] \nwith the link:https://cursive-ide.com/[*Cursive* Clojure plugin].\n\nimage:images/intellij-idea-logo-400.png[IntelliJ,200,200]\n\nimage:images/cursive-logo-300.png[Cursive]\n\nYourKit supports open source projects with its full-featured Java Profiler.\nYourKit, LLC is the creator of\nlink:https://www.yourkit.com/java/profiler/[YourKit Java Profiler]\nand link:https://www.yourkit.com/.net/profiler/[YourKit .NET Profiler],\ninnovative and intelligent tools for profiling Java and .NET applications.\n\nimage:https://www.yourkit.com/images/yklogo.png[YourKit,400,400]\n\n== ToDo List (#todo)\n\n  types\n  schema (\u0026 schema-datomic)\n  re-work csv\n  kill y64?\n  Update all NS docstrings\n  zipcode distance testing\n  lein plugin\n  make CLJS compatible\n  more docs for other namespaces\n  add more test.check\n  add spy-let, spy-defn, spy-validate, etc\n  blog posts\n\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcloojure%2Ftupelo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcloojure%2Ftupelo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcloojure%2Ftupelo/lists"}