├── .gitignore ├── .zprintrc ├── LICENSE ├── README.md ├── brew-formulae ├── README.md └── clj-zprint.rb ├── compiling-java ├── README.md ├── build.clj ├── deps.edn ├── java │ └── greatings │ │ └── Greater.java └── src │ └── compiling_java │ └── core.clj ├── connection-pooling └── hikari-cp-example │ ├── README.md │ ├── project.clj │ ├── resources │ └── logback.xml │ └── src │ └── hikari_cp_example │ └── core.clj ├── crawling └── pruning-html-with-clojure-walk │ ├── README.md │ ├── deps.edn │ └── src │ └── server │ └── core.clj ├── database-type-conversion └── jdbc-java-time-example │ ├── README.md │ ├── project.clj │ ├── resources │ └── data_readers.clj │ └── src │ └── jdbc_java_time_example │ └── core.clj ├── generating-files └── html-and-xml-example │ ├── 404.html │ ├── README.md │ ├── deps.edn │ ├── feed.xml │ └── src │ └── html_and_xml_example │ └── core.clj ├── git-hooks └── pre-commit ├── multimethods └── ensuring-multimethods-are-required │ ├── README.md │ ├── deps.edn │ └── src │ └── ensuring_multimethods_are_required │ ├── core.clj │ ├── printer.clj │ ├── shout.clj │ └── whisper.clj ├── rate-limiting └── do-once │ ├── README.md │ ├── deps.edn │ └── src │ └── do_once │ └── core.clj ├── rewriting-clojure-code └── rewrite-clj │ ├── README.md │ ├── deps.edn │ └── src │ └── rewrite_clj │ └── core.clj ├── sending-email └── sendgrid-example │ ├── README.md │ ├── project.clj │ └── src │ └── sendgrid_example │ └── core.clj ├── server-sent-events └── synchronous-handler-with-virtual-threads │ ├── .gitignore │ ├── README.md │ ├── deps.edn │ └── src │ └── app │ └── core.clj ├── sql └── in-any-all │ ├── README.md │ ├── deps.edn │ └── src │ └── in_any_all │ └── core.clj ├── sqlite └── application-defined-sql-functions │ ├── .gitignore │ ├── README.md │ ├── classes │ └── .gitignore │ ├── deps.edn │ └── src │ └── sqlite │ ├── db.clj │ └── db │ └── application_defined_functions.clj ├── virtual-threads ├── dynamic-var-perf │ ├── .gitignore │ ├── README.md │ ├── deps.edn │ └── src │ │ └── app │ │ └── core.clj ├── http-kit │ ├── README.md │ ├── deps.edn │ └── src │ │ └── server │ │ └── core.clj ├── jetty │ ├── README.md │ ├── deps.edn │ └── src │ │ └── server │ │ └── core.clj ├── managing-throughput │ ├── README.md │ ├── deps.edn │ └── src │ │ └── server │ │ └── core.clj ├── minimal-http-kit │ ├── deps.edn │ └── src │ │ └── app │ │ └── core.clj ├── minimal-http │ ├── deps.edn │ └── src │ │ └── app │ │ └── core.clj ├── minimal-jetty │ ├── deps.edn │ └── src │ │ └── app │ │ └── core.clj └── structured-concurrency │ ├── README.md │ ├── deps.edn │ └── src │ └── server │ └── core.clj └── zippers └── manipulating-html-and-xml-example ├── README.md ├── deps.edn ├── page.html └── src └── manipulating_html_and_xml_example └── core.clj /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/classes 3 | *.jar 4 | *.class 5 | **/.lein-* 6 | **/.nrepl-port 7 | **.cpcache 8 | /.cache/ 9 | -------------------------------------------------------------------------------- /.zprintrc: -------------------------------------------------------------------------------- 1 | {:style [:community :justified :respect-bl] 2 | :map {:comma? false}} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clj-cookbook 2 | 3 | A collection of useful Clojure recipes (the sort of things you do once in the lifetime of a project and then forget how to do). 4 | 5 | Meta level recipes will be documented here. 6 | 7 | ## Formatting pre-commit hook 8 | 9 | Pre-commit hook bash script can be found [here](https://github.com/andersmurphy/clj-cookbook/blob/master/git-hooks/pre-commit). 10 | 11 | Downloads and caches [zprint](https://github.com/kkinnear/zprint) and uses it to format staged code automatically on commit. 12 | 13 | Formatting rules can be configured [here](https://github.com/andersmurphy/clj-cookbook/blob/master/.zprintrc). 14 | 15 | Setup: 16 | 17 | ```sh 18 | git config core.hooksPath git-hooks 19 | chmod +x git-hooks/pre-commit 20 | ``` 21 | 22 | The accompanying blog post can be found [here](https://andersmurphy.com/2020/08/16/clojure-code-formatting-pre-commit-hook-with-zprint.html). 23 | -------------------------------------------------------------------------------- /brew-formulae/README.md: -------------------------------------------------------------------------------- 1 | # Brew Formulae 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2020/08/18/homebrew-write-your-own-brew-formula.html). 4 | 5 | ## To install clj-zprint 6 | 7 | Run the following command in the `brew-formulae` directory: 8 | 9 | `brew install --build-from-source clj-zprint.rb` 10 | 11 | ## To uninstall clj-zprint 12 | 13 | Run the following command: 14 | 15 | `brew install clj-zprint` 16 | -------------------------------------------------------------------------------- /brew-formulae/clj-zprint.rb: -------------------------------------------------------------------------------- 1 | class CljZprint < Formula 2 | desc "Formatter for Clojure" 3 | homepage "https://github.com/kkinnear/zprint" 4 | version "2020.08.09" 5 | 6 | if OS.linux? 7 | url "https://github.com/kkinnear/zprint/releases/download/1.0.0/zprintl-1.0.0" 8 | sha256 "b707f1188c175c2028c014f0ae88cb384283aa6d097bb31298d66852162581b1" 9 | else 10 | url "https://github.com/kkinnear/zprint/releases/download/1.0.0/zprintm-1.0.0" 11 | sha256 "b707f1188c175c2028c014f0ae88cb384283aa6d097bb31298d66852162581b1" 12 | end 13 | 14 | def install 15 | system "mv zprintm-1.0.0 clj-zprint" 16 | system "chmod 755 clj-zprint" 17 | bin.install "clj-zprint" 18 | end 19 | 20 | test do 21 | system "#{bin}/clj-zprint" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /compiling-java/README.md: -------------------------------------------------------------------------------- 1 | # Compiling java source with tools.build 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2021/12/12/clojure-compiling-java-source-with-tools-build.html). 4 | 5 | ## To compile the java source 6 | 7 | ``` 8 | clj -T:build jcompile 9 | ``` 10 | 11 | ## To run the repl 12 | 13 | ``` 14 | clj -M:dev 15 | ``` 16 | 17 | ## To build the uberjar 18 | 19 | ``` 20 | clj -T:build uber 21 | ``` 22 | 23 | ## Run uberjar 24 | 25 | ``` 26 | java -jar target/compiling-java-0.1.1.jar 27 | ``` 28 | -------------------------------------------------------------------------------- /compiling-java/build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require [clojure.tools.build.api :as b])) 3 | 4 | (def lib 'compiling-java) 5 | (def version "0.1.1") 6 | (def class-dir "target/classes") 7 | (def basis (b/create-basis {:project "deps.edn"})) 8 | (def uber-file (format "target/%s-%s.jar" (name lib) version)) 9 | 10 | (defn clean [_] 11 | (b/delete {:path "target"})) 12 | 13 | (defn jcompile [_] 14 | (clean nil) 15 | (b/javac {:src-dirs ["java"] 16 | :class-dir class-dir 17 | :basis basis 18 | :javac-opts ["-source" "8" "-target" "8"]})) 19 | 20 | (defn uber [_] 21 | (clean nil) 22 | (jcompile nil) 23 | (b/copy-dir {:src-dirs ["src" "resources"] 24 | :target-dir class-dir}) 25 | (b/compile-clj {:basis basis 26 | :src-dirs ["src"] 27 | :class-dir class-dir}) 28 | (b/uber {:class-dir class-dir 29 | :uber-file uber-file 30 | :basis basis 31 | :main 'compiling-java.core})) 32 | -------------------------------------------------------------------------------- /compiling-java/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.3"}} 3 | :aliases 4 | {:build {:deps {io.github.clojure/tools.build 5 | {:git/tag "v0.6.8" 6 | :git/sha "d79ae84"}} 7 | :ns-default build} 8 | :dev {;; So that we have access to java classes at the repl 9 | :paths ["src" "target/classes"]}}} 10 | -------------------------------------------------------------------------------- /compiling-java/java/greatings/Greater.java: -------------------------------------------------------------------------------- 1 | package greatings; 2 | 3 | public class Greater { 4 | public void great() { 5 | System.out.println("Hello, world!"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /compiling-java/src/compiling_java/core.clj: -------------------------------------------------------------------------------- 1 | (ns compiling-java.core 2 | (:gen-class) 3 | (:import [greatings Greater])) 4 | 5 | (defn -main [] 6 | (.great (Greater.))) 7 | -------------------------------------------------------------------------------- /connection-pooling/hikari-cp-example/README.md: -------------------------------------------------------------------------------- 1 | # Connection pooling with hikari-cp 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2019/07/14/clojure-connection-pooling-with-hikari-cp.html). 4 | 5 | For a more detailed explanation on what the pool size should be check out [this post](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing). 6 | -------------------------------------------------------------------------------- /connection-pooling/hikari-cp-example/project.clj: -------------------------------------------------------------------------------- 1 | (defproject hikari-cp-example "0.1.0-SNAPSHOT" 2 | :dependencies [[org.clojure/clojure "1.10.1"] 3 | [org.clojure/java.jdbc "0.7.7"] 4 | [org.postgresql/postgresql "42.2.3"] 5 | [hikari-cp "2.7.1"] 6 | [ch.qos.logback/logback-classic "1.2.3"]]) 7 | -------------------------------------------------------------------------------- /connection-pooling/hikari-cp-example/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-10contextName %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /connection-pooling/hikari-cp-example/src/hikari_cp_example/core.clj: -------------------------------------------------------------------------------- 1 | (ns hikari-cp-example.core 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [hikari-cp.core :as cp] 4 | [clojure.string :as str])) 5 | 6 | (defn db-info-from-url 7 | [db-url] 8 | (let [db-uri (java.net.URI. db-url) 9 | [username password] (str/split (or (.getUserInfo db-uri) ":") #":")] 10 | {:username (or username (System/getProperty "user.name")) 11 | :password (or password "") 12 | :port-number (.getPort db-uri) 13 | :database-name (str/replace-first (.getPath db-uri) "/" "") 14 | :server-name (.getHost db-uri)})) 15 | 16 | (def datasource-options 17 | (merge (db-info-from-url "postgresql://localhost:5432/databasename") 18 | {:auto-commit true 19 | :read-only false 20 | :adapter "postgresql" 21 | :connection-timeout 30000 22 | :validation-timeout 5000 23 | :idle-timeout 600000 24 | :max-lifetime 1800000 25 | :minimum-idle 5 26 | :maximum-pool-size 5 27 | :pool-name "db-pool" 28 | :register-mbeans false})) 29 | 30 | (defonce datasource (delay (cp/make-datasource datasource-options))) 31 | 32 | (def database-connection {:datasource @datasource}) 33 | 34 | (comment 35 | (clojure.java.jdbc/execute! 36 | database-connection 37 | "CREATE TABLE user_info (pid SERIAL PRIMARY KEY, name text)") 38 | 39 | (clojure.java.jdbc/insert! database-connection :user_info {:name "anders"})) 40 | -------------------------------------------------------------------------------- /crawling/pruning-html-with-clojure-walk/README.md: -------------------------------------------------------------------------------- 1 | # Pruning html with clojure walk 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2024/04/01/clojure-pruning-html-with-clojure-walk.html). 4 | -------------------------------------------------------------------------------- /crawling/pruning-html-with-clojure-walk/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0-alpha5"} 3 | org.clj-commons/hickory {:mvn/version "0.7.4"} 4 | metosin/malli {:mvn/version "0.14.0"} 5 | lambdaisland/regal {:mvn/version "0.0.143"}} 6 | :aliases {}} 7 | -------------------------------------------------------------------------------- /crawling/pruning-html-with-clojure-walk/src/server/core.clj: -------------------------------------------------------------------------------- 1 | (ns server.core 2 | (:require [clojure.walk :as walk] 3 | [malli.core :as m] 4 | [hickory.core :as hick] 5 | [lambdaisland.regal :refer [regex]])) 6 | 7 | (defn find-last-tag-match [tags hiccup] 8 | (let [branch? sequential? 9 | children (fn [children] (filter branch? children))] 10 | (->> (tree-seq branch? children hiccup) 11 | (filterv (fn [[t :as el]] (when (tags t) el))) 12 | peek))) 13 | 14 | (def blank-re 15 | ;; Malli uses re-find not re-matches so we need to specify start/end 16 | ;; or we will match on strings that contain whitespace and other content. 17 | ;; See: https://github.com/metosin/malli/issues/862 18 | [:cat :start [:* :whitespace] :end]) 19 | (def comment-re [:cat ""]) 20 | (def doc-type-re "") 21 | 22 | (def strings-to-remove 23 | (regex [:alt blank-re comment-re doc-type-re])) 24 | 25 | (defn tags-to-remove [tags] 26 | [:or 27 | [:cat [:fn tags] [:* :any]] 28 | [:cat [:not [:fn #{:br}]] :any] 29 | [:and :string [:re strings-to-remove]] 30 | [:not [:or :keyword :string [:vector :any] :map]]]) 31 | 32 | (def tags-to-unwrap 33 | [:or [:cat [:fn #{:div :span :article :main}] 34 | :any [:or [:* :any] :string]] 35 | [:cat [:vector :any]]]) 36 | 37 | (defn remove-tags [tags hiccup] 38 | (let [remove-tag? (m/validator (tags-to-remove tags)) 39 | unwrap-tag? (m/validator tags-to-unwrap)] 40 | (walk/postwalk 41 | #(cond (and (vector? %) (not (map-entry? %))) 42 | (let [el (into [] (remove remove-tag?) %)] 43 | (if (unwrap-tag? el) (peek el) el)) 44 | (map? %) (select-keys % [:href]) 45 | :else %) 46 | (vec hiccup)))) 47 | 48 | (comment 49 | (def html-data 50 | (slurp "https://clojure.org/reference/clojure_cli")) 51 | 52 | (->> html-data hick/parse hick/as-hiccup 53 | (find-last-tag-match #{:body :main}) 54 | (remove-tags #{:head :script :noscript :style :nav :meta 55 | :form :fieldset :object :embed :footer 56 | :link :aside :iframe :input :textarea 57 | :select :button :template}))) 58 | -------------------------------------------------------------------------------- /database-type-conversion/jdbc-java-time-example/README.md: -------------------------------------------------------------------------------- 1 | # Using java.time with clojure.java.jdbc 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2019/08/03/clojure-using-java-time-with-jdbc.html). 4 | -------------------------------------------------------------------------------- /database-type-conversion/jdbc-java-time-example/project.clj: -------------------------------------------------------------------------------- 1 | (defproject jdbc-java-time-example "0.1.0-SNAPSHOT" 2 | :dependencies [[org.clojure/clojure "1.10.1"] 3 | [org.clojure/java.jdbc "0.7.9"] 4 | [org.postgresql/postgresql "42.2.6"]]) 5 | -------------------------------------------------------------------------------- /database-type-conversion/jdbc-java-time-example/resources/data_readers.clj: -------------------------------------------------------------------------------- 1 | {time/inst jdbc-java-time-example.core/parse-time 2 | time/ld jdbc-java-time-example.core/parse-date} 3 | -------------------------------------------------------------------------------- /database-type-conversion/jdbc-java-time-example/src/jdbc_java_time_example/core.clj: -------------------------------------------------------------------------------- 1 | (ns jdbc-java-time-example.core 2 | (:require [clojure.java.jdbc :as jdbc]) 3 | (:import [java.sql Timestamp] 4 | [java.sql Date] 5 | [java.time.format DateTimeFormatter] 6 | [java.time LocalDate] 7 | [java.time Instant] 8 | [java.io FileWriter])) 9 | 10 | (extend-protocol jdbc/IResultSetReadColumn 11 | java.sql.Timestamp 12 | (result-set-read-column [v _ _] 13 | (.toInstant v)) 14 | java.sql.Date 15 | (result-set-read-column [v _ _] 16 | (.toLocalDate v))) 17 | 18 | (extend-protocol jdbc/ISQLValue 19 | java.time.Instant 20 | (sql-value [v] 21 | (Timestamp/from v)) 22 | java.time.LocalDate 23 | (sql-value [v] 24 | (Date/valueOf v))) 25 | 26 | (defn parse-date [string] 27 | (LocalDate/parse string)) 28 | 29 | (defn parse-time [string] 30 | (and string (-> (.parse (DateTimeFormatter/ISO_INSTANT) string) 31 | Instant/from))) 32 | 33 | (defmethod print-method java.time.Instant 34 | [inst out] 35 | (.write out (str "#time/inst \"" (.toString inst) "\"") )) 36 | 37 | (defmethod print-dup java.time.Instant 38 | [inst out] 39 | (.write out (str "#time/inst \"" (.toString inst) "\"") )) 40 | 41 | (defmethod print-method LocalDate 42 | [^LocalDate date ^FileWriter out] 43 | (.write out (str "#time/ld \"" (.toString date) "\""))) 44 | 45 | (defmethod print-dup LocalDate 46 | [^LocalDate date ^FileWriter out] 47 | (.write out (str "#time/ld \"" (.toString date) "\""))) 48 | 49 | (def database-connection "postgresql://localhost:5432/databasename") 50 | 51 | (comment 52 | (jdbc/execute! 53 | database-connection 54 | "CREATE TABLE event (pid SERIAL PRIMARY KEY, name text, 55 | created timestamp with time zone, 56 | log_date date )") 57 | 58 | (jdbc/insert! 59 | database-connection 60 | :event 61 | {:name "page_viewed" 62 | :created (Instant/now) 63 | :log_date (LocalDate/now)}) 64 | 65 | (jdbc/insert! 66 | database-connection 67 | :event 68 | {:name "page_viewed" 69 | :created #time/inst "2019-08-03T16:28:25.935Z" 70 | :log_date #time/ld "2019-08-03"}) 71 | 72 | (jdbc/query 73 | database-connection 74 | ["select * from event"])) 75 | -------------------------------------------------------------------------------- /generating-files/html-and-xml-example/404.html: -------------------------------------------------------------------------------- 1 |

404: Page not found

Sorry, we've misplaced that URL or it's 2 | pointing to something that doesn't exist.Head back home to try finding it again.

-------------------------------------------------------------------------------- /generating-files/html-and-xml-example/README.md: -------------------------------------------------------------------------------- 1 | # Generating HTML and XML 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2019/09/08/clojure-generating-html-and-xml.html). 4 | -------------------------------------------------------------------------------- /generating-files/html-and-xml-example/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {hiccup {:mvn/version "1.0.5"} 2 | org.clojure/data.xml {:mvn/version "0.0.8"}}} 3 | -------------------------------------------------------------------------------- /generating-files/html-and-xml-example/feed.xml: -------------------------------------------------------------------------------- 1 | Site TitleSite Descriptionhttps://andersmurphy.comFooFri, 6 Sep 2019 00:00:00 GMThttps://andersmurphy.com/foohttps://andersmurphy.com/fooBarSat, 7 Sep 2019 00:00:00 GMThttps://andersmurphy.com/barhttps://andersmurphy.com/barBazSun, 8 Sep 2019 00:00:00 GMThttps://andersmurphy.com/bazhttps://andersmurphy.com/baz -------------------------------------------------------------------------------- /generating-files/html-and-xml-example/src/html_and_xml_example/core.clj: -------------------------------------------------------------------------------- 1 | (ns html-and-xml-example.core 2 | (:require [hiccup.core :as html] 3 | [clojure.data.xml :as xml])) 4 | 5 | (def site-url "https://andersmurphy.com") 6 | 7 | (defn generate-404-html [] 8 | (html/html [:html 9 | [:body 10 | [:h1 {:class "post-title"} "404: Page not found"] 11 | [:p "Sorry, we've misplaced that URL or it's 12 | pointing to something that doesn't exist." 13 | [:a {:href site-url} "Head back home"] 14 | " to try finding it again."]]])) 15 | 16 | (defn write-404! [html] 17 | (let [path-name "404.html"] 18 | (spit path-name html))) 19 | 20 | (comment (-> (generate-404-html) 21 | write-404!)) 22 | 23 | (def site-title "Site Title") 24 | (def site-rss (str site-url "/feed.xml")) 25 | (def site-description "Site Description") 26 | 27 | (defn generate-rss-xml [posts] 28 | (xml/sexp-as-element 29 | [:rss 30 | {:version "2.0" 31 | :xmlns:atom "https://www.w3.org/2005/Atom" 32 | :xmlns:dc "https://purl.org/dc/elements/1.1/"} 33 | [:channel 34 | [:title site-title] 35 | [:description site-description] 36 | [:link site-url] 37 | [:atom:link 38 | {:href site-rss :rel "self" :type "application/rss+xml"}] 39 | (map (fn [{:keys [post-name date post-path-name]}] 40 | (let [post-url (str site-url "/" post-path-name)] 41 | [:item 42 | [:title post-name] 43 | [:pubDate date] 44 | [:link post-url] 45 | [:guid {:isPermaLink "true"} post-url]])) 46 | posts)]])) 47 | 48 | (def posts [{:post-name "Foo" 49 | :post-path-name "foo" 50 | :date "Fri, 6 Sep 2019 00:00:00 GMT"} 51 | {:post-name "Bar" 52 | :post-path-name "bar" 53 | :date "Sat, 7 Sep 2019 00:00:00 GMT"} 54 | {:post-name "Baz" 55 | :post-path-name "baz" 56 | :date "Sun, 8 Sep 2019 00:00:00 GMT"}]) 57 | 58 | (defn write-rss! [xml] 59 | (with-open [out-file (java.io.FileWriter. "feed.xml")] 60 | (xml/emit xml out-file))) 61 | 62 | (comment (-> (generate-rss-xml posts) 63 | write-rss!)) 64 | -------------------------------------------------------------------------------- /git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | mkdir -p .cache 4 | cd .cache 5 | 6 | if [ ! -f clj-zprint ]; then 7 | if [ "$(uname)" == "Darwin" ]; then 8 | curl -LJO "https://github.com/kkinnear/zprint/releases/download/1.0.0/zprintm-1.0.0" 9 | mv zprintm-1.0.0 clj-zprint 10 | chmod 755 clj-zprint 11 | elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then 12 | curl -LJO "https://github.com/kkinnear/zprint/releases/download/1.0.0/zprintl-1.0.0" 13 | mv zprintm-1.0.0 clj-zprint 14 | chmod 755 clj-zprint 15 | fi 16 | fi 17 | 18 | cd .. 19 | 20 | for file in $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(clj)$') 21 | do 22 | .cache/clj-zprint "{:search-config? true}" < "$file" > "$file.out" 23 | mv "$file.out" "$file" 24 | $(git add "$file") 25 | done 26 | -------------------------------------------------------------------------------- /multimethods/ensuring-multimethods-are-required/README.md: -------------------------------------------------------------------------------- 1 | # Ensuring multimethods are required 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2021/10/24/clojure-ensuring-multimethods-are-required.html). 4 | -------------------------------------------------------------------------------- /multimethods/ensuring-multimethods-are-required/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.11.0-alpha2"}}} 2 | -------------------------------------------------------------------------------- /multimethods/ensuring-multimethods-are-required/src/ensuring_multimethods_are_required/core.clj: -------------------------------------------------------------------------------- 1 | (ns ensuring-multimethods-are-required.core 2 | (:require 3 | [ensuring-multimethods-are-required.whisper] 4 | [ensuring-multimethods-are-required.shout] 5 | [ensuring-multimethods-are-required.printer :as p])) 6 | 7 | (let [loaded-methods (-> p/print methods keys set) 8 | expected-methods #{:default :shout :whisper}] 9 | (assert (= expected-methods loaded-methods) 10 | (str expected-methods " =/= " loaded-methods))) 11 | 12 | (run! p/print [{:text "Hello"} 13 | {:type :shout :text "Hello"} 14 | {:type :whisper :text "Hello"}]) 15 | -------------------------------------------------------------------------------- /multimethods/ensuring-multimethods-are-required/src/ensuring_multimethods_are_required/printer.clj: -------------------------------------------------------------------------------- 1 | (ns ensuring-multimethods-are-required.printer 2 | (:refer-clojure :exclude [print])) 3 | 4 | (defmulti print :type) 5 | 6 | (defmethod print :default [{:keys [text]}] (println text)) 7 | -------------------------------------------------------------------------------- /multimethods/ensuring-multimethods-are-required/src/ensuring_multimethods_are_required/shout.clj: -------------------------------------------------------------------------------- 1 | (ns ensuring-multimethods-are-required.shout 2 | (:require [ensuring-multimethods-are-required.printer :as p] 3 | [clojure.string :as str])) 4 | 5 | (defmethod p/print :shout [{:keys [text]}] 6 | (println (str/upper-case text))) 7 | -------------------------------------------------------------------------------- /multimethods/ensuring-multimethods-are-required/src/ensuring_multimethods_are_required/whisper.clj: -------------------------------------------------------------------------------- 1 | (ns ensuring-multimethods-are-required.whisper 2 | (:require [ensuring-multimethods-are-required.printer :as p] 3 | [clojure.string :as str])) 4 | 5 | (defmethod p/print :whisper [{:keys [text]}] 6 | (println (str/lower-case text))) 7 | -------------------------------------------------------------------------------- /rate-limiting/do-once/README.md: -------------------------------------------------------------------------------- 1 | # Persistent do once 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2020/02/08/clojure-persistent-rate-limiting.html). 4 | -------------------------------------------------------------------------------- /rate-limiting/do-once/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.10.1"} 2 | seancorfield/next.jdbc {:mvn/version "1.0.13"} 3 | org.postgresql/postgresql {:mvn/version "42.2.6"}}} 4 | -------------------------------------------------------------------------------- /rate-limiting/do-once/src/do_once/core.clj: -------------------------------------------------------------------------------- 1 | (ns do-once.core 2 | (:require [next.jdbc :as jdbc])) 3 | 4 | (def db {:dbtype "postgresql" :dbname "databasename"}) 5 | (def ds (jdbc/get-datasource db)) 6 | 7 | (defn done? [uuid name] 8 | (jdbc/execute-one! ds [" 9 | select * from do_once where uuid = ? and name = ?" 10 | uuid name])) 11 | 12 | (defn do! [uuid name] 13 | (jdbc/execute! ds [" 14 | insert into do_once (uuid, name) values (? , ?) on conflict (uuid, name) do nothing" 15 | uuid name])) 16 | 17 | (defmacro do-once! [uuid name & body] 18 | `(when-not (done? ~uuid ~name) 19 | (do! ~uuid ~name) 20 | ~@body)) 21 | 22 | (defmacro do-once-2! [& {:keys [uuid name action]}] 23 | `(when-not (done? ~uuid ~name) 24 | (do! ~uuid ~name) 25 | ~action)) 26 | 27 | (defn do-once-3! [{:keys [uuid name action]}] 28 | (when-not (done? uuid name) 29 | (do! uuid name) 30 | (action))) 31 | 32 | (comment 33 | (jdbc/execute! ds [" 34 | create table do_once ( 35 | pid serial primary key, 36 | uuid text not null, 37 | name text not null)"]) 38 | 39 | (jdbc/execute! ds [" 40 | create unique index do_once_unique ON do_once(uuid, name)"]) 41 | 42 | (do-once! "Nora" "email-2020-02-08" 43 | (println "email sent") 44 | (prn (+ 1 2 3 4))) 45 | 46 | (do-once-2! :uuid "Nora" 47 | :name "email-2020-02-09" 48 | :action (do (println "email sent") 49 | (prn (+ 1 2 3 4)))) 50 | 51 | (do-once-3! {:uuid "Nora" 52 | :name "email-2020-02-10" 53 | :action (fn [] 54 | (println "email sent") 55 | (prn (+ 1 2 3 4)))})) 56 | -------------------------------------------------------------------------------- /rewriting-clojure-code/rewrite-clj/README.md: -------------------------------------------------------------------------------- 1 | # Rewriting Clojure code with rewrite-clj 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/). 4 | -------------------------------------------------------------------------------- /rewriting-clojure-code/rewrite-clj/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.11.0-alpha2"} 2 | camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.2"} 3 | rewrite-clj/rewrite-clj {:mvn/version "1.0.682-alpha"}}} 4 | -------------------------------------------------------------------------------- /rewriting-clojure-code/rewrite-clj/src/rewrite_clj/core.clj: -------------------------------------------------------------------------------- 1 | (ns rewrite-clj.core 2 | (:require [rewrite-clj.zip :as z] 3 | [clojure.string :as str] 4 | [clojure.java.io :as io])) 5 | 6 | (defn kebab-case->camelCase 7 | [k] 8 | (let [words (str/split (name k) #"-")] 9 | (->> (map str/capitalize (rest words)) 10 | (apply str (first words)) 11 | keyword))) 12 | 13 | (defn skip-form [zloc] 14 | (or (z/right zloc) 15 | (if-not (z/up zloc) 16 | [(z/node zloc) :end] 17 | (recur (z/up zloc))))) 18 | 19 | (defn camel-case-snake-case-keys 20 | [{:keys [ignored-forms ignored-keys]} code] 21 | (let [ignored-forms (conj ignored-forms 22 | 'camel-case-snake-case-keys)] 23 | (loop [zloc (z/of-string code)] 24 | (if (z/end? zloc) 25 | (z/root-string zloc) 26 | (-> (let [sexpr (z/sexpr zloc)] 27 | (cond 28 | (and (or (list? sexpr) (vector? sexpr)) 29 | (or (get ignored-forms (first sexpr)) 30 | (get ignored-forms (second sexpr)))) 31 | (skip-form zloc), 32 | (and (keyword? sexpr) 33 | (not (namespace sexpr)) 34 | (not (get ignored-keys sexpr))) 35 | (-> (z/edit zloc kebab-case->camelCase) 36 | z/next), 37 | :else (z/next zloc))) 38 | recur))))) 39 | 40 | (defn get-clj-files 41 | [folder-path] 42 | (->> (io/file folder-path) 43 | file-seq 44 | (filter #(.isFile %)) 45 | (filter #(str/ends-with? (str %) ".clj")))) 46 | 47 | (comment 48 | (->> (get-clj-files "/Users/andersmurphy/projects/clj-cookbook") 49 | (run! (fn [file] 50 | (->> (slurp file) 51 | (camel-case-snake-case-keys 52 | {:ignored-keys 53 | #{:content-type 54 | :form-params} 55 | :ignored-forms 56 | #{'env}}) 57 | (spit file)))))) 58 | 59 | (comment 60 | (defn env [] 61 | {:foo-bar 23}) 62 | (env :foo-bar) 63 | (foo (env {:foo-bar 23}) 64 | (env {:foo-bar 23})) 65 | (env :foo-bar) 66 | (env :foo-var :bar-var) 67 | (-> :foo-car) 68 | (env :foo-var)) 69 | 70 | ;; Exclude files 71 | -------------------------------------------------------------------------------- /sending-email/sendgrid-example/README.md: -------------------------------------------------------------------------------- 1 | # Sending emails with SendGrid 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2019/01/06/clojure-sending-emails-with-sendgrid.html). 4 | -------------------------------------------------------------------------------- /sending-email/sendgrid-example/project.clj: -------------------------------------------------------------------------------- 1 | (defproject sendgrid-example "0.1.0-SNAPSHOT" 2 | :dependencies [[org.clojure/clojure "1.10.0"] 3 | [clj-http "3.9.1"] 4 | [cheshire "5.8.1"]]) 5 | -------------------------------------------------------------------------------- /sending-email/sendgrid-example/src/sendgrid_example/core.clj: -------------------------------------------------------------------------------- 1 | (ns sendgrid-example.core 2 | (:require [clj-http.client :as http] 3 | [clojure.string :as str])) 4 | 5 | (def data [{:name "Bob" :age 27 :favourite-food "bagels"} 6 | {:name "Sarah" :age 23 :favourite-food "apples"} 7 | {:name "John" :age 41 :favourite-food "pasta"}]) 8 | 9 | (defn escape-csv-value [value] 10 | (str "\"" value "\"")) 11 | 12 | (defn row->csv-row [row] 13 | (->> (map escape-csv-value row) 14 | (str/join ","))) 15 | 16 | (defn ms->csv-string [ms] 17 | (let [columns (keys (first ms)) 18 | headers (map name columns) 19 | rows (map #(map % columns) ms)] 20 | (->> (into [headers] rows) 21 | (map row->csv-row) 22 | (str/join "\n")))) 23 | 24 | (defn encode-string-to-base64 [string] 25 | (.encodeToString (java.util.Base64/getEncoder) (.getBytes string))) 26 | 27 | (defn send-email-with-csv [to-email csv-string] 28 | (http/post 29 | "https://api.sendgrid.com/v3/mail/send" 30 | {:headers {:authorization 31 | (str "Bearer " (System/getenv "SENGRID_API_KEY"))} 32 | :content-type :json 33 | :form-params 34 | {:personalizations [{:to [{:email to-email}] 35 | :subject "Hello, World!"}] 36 | :from {:email "from_address@exampl.com"} 37 | :content [{:type "text/plain" 38 | :value "Hello, World!"}] 39 | :attachments 40 | [{:filename "helloworld.csv" 41 | :content (encode-string-to-base64 csv-string)}]}})) 42 | 43 | (comment 44 | (->> data 45 | ms->csv-string 46 | (send-email-with-csv "john@example.com"))) 47 | -------------------------------------------------------------------------------- /server-sent-events/synchronous-handler-with-virtual-threads/.gitignore: -------------------------------------------------------------------------------- 1 | /db/ 2 | .* 3 | !.github 4 | !.gitignore 5 | .clj-kondo 6 | !.clj-kondo/config.edn 7 | !.clj-kondo/app/* 8 | !.env.edn -------------------------------------------------------------------------------- /server-sent-events/synchronous-handler-with-virtual-threads/README.md: -------------------------------------------------------------------------------- 1 | # Clojure: Synchronous server sent events with virtual threads and channels 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2024/10/07/clojure-synchronous-server-sent-events-with-virtual-threads-and-channels.html). 4 | -------------------------------------------------------------------------------- /server-sent-events/synchronous-handler-with-virtual-threads/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0"} 3 | ring/ring-core {:mvn/version "1.12.2"} 4 | org.clojure/core.async {:mvn/version "1.6.681"} 5 | org.slf4j/slf4j-nop {:mvn/version "2.0.16"} 6 | info.sunng/ring-jetty9-adapter {:mvn/version "0.35.0"}} 7 | :aliases {}} 8 | -------------------------------------------------------------------------------- /server-sent-events/synchronous-handler-with-virtual-threads/src/app/core.clj: -------------------------------------------------------------------------------- 1 | (ns app.core 2 | (:gen-class) 3 | (:require [ring.adapter.jetty9 :refer [run-jetty]] 4 | [clojure.java.io :as io] 5 | [clojure.core.async :as a] 6 | [ring.core.protocols :refer [StreamableResponseBody]] 7 | [clojure.core.async.impl.channels]) 8 | (:import (java.util.concurrent Executors) 9 | (org.eclipse.jetty.util.thread QueuedThreadPool) 10 | (org.eclipse.jetty.server Server) 11 | (java.io OutputStream) 12 | (java.util.concurrent Executors))) 13 | 14 | ;; Extend core.async channel with StreamableResponseBody 15 | (extend-type clojure.core.async.impl.channels.ManyToManyChannel 16 | StreamableResponseBody 17 | (write-body-to-stream [ch _response ^OutputStream output-stream] 18 | (with-open [out output-stream 19 | writer (io/writer out)] 20 | (try 21 | (loop [] 22 | (when-let [^String msg (a/!! [ch message] 37 | (let [v (a/>!! ch message)] 38 | (when-not v (swap! clients disj ch)) 39 | ;; Keeps the return semantics of >!! 40 | v)) 41 | 42 | (defn heartbeat>!! [ch msec] 43 | (Thread/startVirtualThread 44 | #(loop [] 45 | (Thread/sleep ^long msec) 46 | (when (send>!! ch "\n\n") 47 | (recur))))) 48 | 49 | (defn handler-sse [_] 50 | (let [ch (a/chan 10)] 51 | (swap! clients conj ch) 52 | (send>!! ch (format-event "Successfully connected")) 53 | ;; Every 10 seconds we send a heartbeat to check if output stream 54 | ;; is still open. 55 | (heartbeat>!! ch 10000) 56 | {:status 200 57 | :headers {"Content-Type" "text/event-stream;charset=UTF-8" 58 | "Cache-Control" "no-cache, no-store"} 59 | :body ch})) 60 | 61 | (defn broadcast-message-to-connected-clients! [message] 62 | (run! (fn [ch] (send>!! ch (format-event message))) @clients)) 63 | 64 | (def app 65 | (fn handler [{:keys [request-method uri] :as req}] 66 | (if (= [:get "/"] [request-method uri]) 67 | (handler-sse req) 68 | {:status 404}))) 69 | 70 | (defn start-server [& {:as opts}] 71 | (let [thread-pool (new QueuedThreadPool) 72 | _ (.setVirtualThreadsExecutor thread-pool 73 | (Executors/newVirtualThreadPerTaskExecutor))] 74 | (run-jetty #'app 75 | (merge 76 | {:port 8080 77 | :thread-pool thread-pool} 78 | opts)))) 79 | 80 | (defn -main [& _] 81 | (start-server)) 82 | 83 | (comment ;; local development only 84 | (def server 85 | (start-server :join? false)) 86 | ;; http://localhost:8080/ 87 | 88 | ;; stop server 89 | (Server/.stop server)) 90 | 91 | (comment 92 | ;; Open a terminal and connect 93 | ;; curl localhost:8080 -vv 94 | 95 | (broadcast-message-to-connected-clients! "Hello") 96 | 97 | @clients 98 | 99 | ) 100 | -------------------------------------------------------------------------------- /sql/in-any-all/README.md: -------------------------------------------------------------------------------- 1 | # Using any and all as alternatives to in 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2020/09/06/clojure-jdbc-using-any-and-all-as-an-alternative-to-in.html). 4 | -------------------------------------------------------------------------------- /sql/in-any-all/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.10.1"} 2 | seancorfield/next.jdbc {:mvn/version "1.0.13"} 3 | org.postgresql/postgresql {:mvn/version "42.2.6"}}} 4 | -------------------------------------------------------------------------------- /sql/in-any-all/src/in_any_all/core.clj: -------------------------------------------------------------------------------- 1 | (ns in-any-all.core 2 | (:require [next.jdbc :as jdbc] 3 | [next.jdbc.sql :as sql] 4 | [clojure.string :as str] 5 | [next.jdbc.prepare :as p]) 6 | (:import [java.sql PreparedStatement])) 7 | 8 | (def db {:dbtype "postgresql" :dbname "databasename"}) 9 | (def ds (jdbc/get-datasource db)) 10 | 11 | (extend-protocol p/SettableParameter 12 | clojure.lang.IPersistentVector 13 | (set-parameter [v ^PreparedStatement s i] 14 | (let [conn (.getConnection s) 15 | meta (.getParameterMetaData s) 16 | type-name (.getParameterTypeName meta i)] 17 | (if-let [elem-type (when (= (first type-name) \_) 18 | (apply str (rest type-name)))] 19 | (.setObject s i (.createArrayOf conn elem-type (to-array v))) 20 | (.setObject i s v))))) 21 | 22 | (comment 23 | (jdbc/execute! 24 | ds 25 | ["create table user_info (pid serial primary key, name text not null)"]) 26 | (jdbc/execute! ds ["create unique index user_info_unique ON user_info(name)"]) 27 | 28 | (sql/insert! ds :user_info {:name "Bob"}) 29 | (sql/insert! ds :user_info {:name "Jane"}) 30 | (sql/insert! ds :user_info {:name "Megan"}) 31 | (sql/insert! ds :user_info {:name "Alice"}) 32 | 33 | (sql/query ds ["select * from user_info where name in(?, ?)" "Bob" "Jane"]) 34 | (sql/query ds ["select * from user_info where name not in(?, ?)" "Bob" "Jane"]) 35 | (sql/query ds 36 | (let [names ["Bob" "Jane"]] 37 | (into [(str "select * from user_info where name in (" 38 | (str/join ", " (repeat (count names) "?")) 39 | ")")] 40 | names))) 41 | 42 | (sql/query ds 43 | ["select * from user_info where name = any(?)" 44 | (into-array String ["Bob" "Jane"])]) 45 | (sql/query ds 46 | ["select * from user_info where name != all(?)" 47 | (into-array String ["Bob" "Jane"])]) 48 | 49 | (sql/query ds ["select * from user_info where name = any(?)" ["Bob" "Jane"]]) 50 | (sql/query ds ["select * from user_info where name != all(?)" ["Bob" "Jane"]]) 51 | 52 | (sql/query ds ["select * from user_info where pid != all(?)" [1 2]]) 53 | (sql/query ds ["select * from user_info where pid = any(?)" [1 2]])) 54 | -------------------------------------------------------------------------------- /sqlite/application-defined-sql-functions/.gitignore: -------------------------------------------------------------------------------- 1 | .lsp 2 | .DS_Store 3 | .cpcache 4 | .clj-kondo/* 5 | .env.edn 6 | !.clj-kondo/config.edn 7 | db/database.* 8 | !classes -------------------------------------------------------------------------------- /sqlite/application-defined-sql-functions/README.md: -------------------------------------------------------------------------------- 1 | # SQLite application defined SQL functions with JDBC 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2023/07/16/clojure-sqlite-application-defined-sql-functions-with-jdbc.html). 4 | -------------------------------------------------------------------------------- /sqlite/application-defined-sql-functions/classes/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | 4 | # Except this file 5 | !.gitignore -------------------------------------------------------------------------------- /sqlite/application-defined-sql-functions/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "classes"] 2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"} 3 | com.github.seancorfield/next.jdbc {:mvn/version "1.3.874"} 4 | org.xerial/sqlite-jdbc {:mvn/version "3.42.0.0"}} 5 | :aliases {:dev 6 | {:main-opts 7 | [;; Ensures application defined functions are compiled 8 | ;; As they use gen-class to extend org.sqlite.Function 9 | "-e" "(compile 'sqlite.db.application-defined-functions)" 10 | "-r"]}}} 11 | -------------------------------------------------------------------------------- /sqlite/application-defined-sql-functions/src/sqlite/db.clj: -------------------------------------------------------------------------------- 1 | (ns sqlite.db 2 | (:require 3 | [next.jdbc :as jdbc]) 4 | (:import 5 | (org.sqlite Function))) 6 | 7 | (comment 8 | (compile 'sqlite.db.application-defined-functions) 9 | 10 | (let [my-datasource (jdbc/get-datasource 11 | {:jdbcUrl "jdbc:sqlite:db/database.db"})] 12 | (with-open [conn (jdbc/get-connection my-datasource)] 13 | (Function/create 14 | conn 15 | "hello_world" 16 | (sqlite.db.application-defined-functions.HelloWorld.)) 17 | (jdbc/execute! conn ["select hello_world()"]))) 18 | 19 | (let [my-datasource (jdbc/get-datasource 20 | {:jdbcUrl "jdbc:sqlite:db/database.db"})] 21 | (with-open [conn (jdbc/get-connection my-datasource)] 22 | (Function/create 23 | conn 24 | "regex_capture" 25 | (sqlite.db.application-defined-functions.RegexCapture.)) 26 | (jdbc/execute! conn 27 | ["select regex_capture(?, 'Hello, world!')" 28 | ", (world)!"]))) 29 | 30 | ) 31 | 32 | 33 | -------------------------------------------------------------------------------- /sqlite/application-defined-sql-functions/src/sqlite/db/application_defined_functions.clj: -------------------------------------------------------------------------------- 1 | (ns sqlite.db.application-defined-functions) 2 | 3 | (gen-class 4 | :name sqlite.db.application-defined-functions.HelloWorld 5 | :prefix "hello-world-" 6 | :extends org.sqlite.Function 7 | :exposes-methods {result superResult}) 8 | 9 | (defn hello-world-xFunc [this] 10 | (.superResult this "hello, world!")) 11 | 12 | (gen-class 13 | :name sqlite.db.application-defined-functions.RegexCapture 14 | :prefix "regex-capture-" 15 | :extends org.sqlite.Function 16 | :exposes-methods {result superResult 17 | value_text superValueText}) 18 | 19 | (defn regex-capture-xFunc [this] 20 | (.superResult this 21 | (let [result (re-find 22 | (re-pattern 23 | (.superValueText this 0)) 24 | (.superValueText this 1))] 25 | (if (vector? result) 26 | (second result) 27 | result)))) 28 | 29 | (comment 30 | (compile 'sqlite.db.application-defined-functions)) 31 | -------------------------------------------------------------------------------- /virtual-threads/dynamic-var-perf/.gitignore: -------------------------------------------------------------------------------- 1 | /db/ 2 | .* 3 | !.github 4 | !.gitignore 5 | .clj-kondo 6 | !.clj-kondo/config.edn 7 | !.clj-kondo/app/* 8 | !.env.edn -------------------------------------------------------------------------------- /virtual-threads/dynamic-var-perf/README.md: -------------------------------------------------------------------------------- 1 | # Virtual thread dynamic var performance 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2024/05/30/clojure-virtual-thread-dynamic-var-performance.html). 4 | -------------------------------------------------------------------------------- /virtual-threads/dynamic-var-perf/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0-alpha11"} 3 | criterium/criterium {:mvn/version "0.4.6"}} 4 | :aliases {}} 5 | -------------------------------------------------------------------------------- /virtual-threads/dynamic-var-perf/src/app/core.clj: -------------------------------------------------------------------------------- 1 | (ns app.core 2 | (:require [criterium.core :as crit]) 3 | (:refer-clojure :exclude [pmap]) 4 | (:import 5 | (java.lang ScopedValue) 6 | (java.util.concurrent 7 | StructuredTaskScope 8 | StructuredTaskScope$Subtask 9 | StructuredTaskScope$ShutdownOnFailure))) 10 | 11 | (defn pmap [f coll] 12 | (with-open [scope (StructuredTaskScope$ShutdownOnFailure/new)] 13 | (let [r (mapv (fn [x] 14 | (StructuredTaskScope/.fork scope 15 | (fn [] (f x)))) 16 | coll)] 17 | ;; join subtasks and propagate errors 18 | (.. scope join throwIfFailed) 19 | ;; fork returns a Subtask/Supplier not a future 20 | (mapv StructuredTaskScope$Subtask/.get r)))) 21 | 22 | (defmacro scoped-binding [bindings & body] 23 | (assert (vector? bindings) 24 | "a vector for its binding") 25 | (assert (even? (count bindings)) 26 | "an even number of forms in binding vector") 27 | `(.. ~@(->> (partition 2 bindings) 28 | (map (fn [[k v]] 29 | (assert (-> k resolve deref type (= ScopedValue)) 30 | (str k " is not a ScopedValue")) 31 | `(ScopedValue/where ~k ~v)))) 32 | (ScopedValue/get (delay ~@body)))) 33 | 34 | (comment 35 | (def ^:dynamic *context* nil) 36 | 37 | (def context-data 38 | {:increase 1 39 | :colors {:red 1 :blue 2 :green 3} 40 | :a "A bunch of context stuff" 41 | :b "B bunch of context stuff" 42 | :c "C bunch of context stuff"}) 43 | 44 | (crit/quick-bench 45 | (binding [*context* context-data] 46 | (pmap (bound-fn* 47 | (fn [x] 48 | (let [result (+ x (:increase *context*))] 49 | result))) 50 | (repeat 500000 1)))) 51 | 52 | ;; Evaluation count : 6 in 6 samples of 1 calls. 53 | ;; Execution time mean : 1.196520 sec 54 | ;; Execution time std-deviation : 41.818110 ms 55 | ;; Execution time lower quantile : 1.143932 sec ( 2.5%) 56 | ;; Execution time upper quantile : 1.243698 sec (97.5%) 57 | ;; Overhead used : 1.845282 ns 58 | 59 | (def scoped-context (ScopedValue/newInstance)) 60 | 61 | (crit/quick-bench 62 | (scoped-binding [scoped-context context-data] 63 | (pmap (fn [x] 64 | (let [result (+ x 65 | (:increase (ScopedValue/.get scoped-context)))] 66 | result)) 67 | (repeat 500000 1)))) 68 | 69 | ;; Evaluation count : 6 in 6 samples of 1 calls. 70 | ;; Execution time mean : 197.549698 ms 71 | ;; Execution time std-deviation : 24.230133 ms 72 | ;; Execution time lower quantile : 172.035517 ms ( 2.5%) 73 | ;; Execution time upper quantile : 220.525422 ms (97.5%) 74 | ;; Overhead used : 1.845282 ns 75 | 76 | (crit/quick-bench 77 | (let [context context-data] 78 | (pmap (fn [x] 79 | (let [result (+ x 80 | (:increase context))] 81 | result)) 82 | (repeat 500000 1)))) 83 | 84 | ;; Evaluation count : 6 in 6 samples of 1 calls. 85 | ;; Execution time mean : 189.313904 ms 86 | ;; Execution time std-deviation : 22.768815 ms 87 | ;; Execution time lower quantile : 165.292587 ms ( 2.5%) 88 | ;; Execution time upper quantile : 215.966794 ms (97.5%) 89 | ;; Overhead used : 1.845282 ns 90 | 91 | ) 92 | 93 | -------------------------------------------------------------------------------- /virtual-threads/http-kit/README.md: -------------------------------------------------------------------------------- 1 | # Virtual threads with ring and http-kit 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2023/09/15/clojure-virtual-threads-with-ring-and-http-kit.html). 4 | -------------------------------------------------------------------------------- /virtual-threads/http-kit/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps 3 | {org.clojure/clojure {:mvn/version "1.11.1"} 4 | metosin/muuntaja {:mvn/version "0.6.8"} 5 | metosin/reitit {:mvn/version "0.7.0-alpha5"} 6 | ring/ring-core {:mvn/version "1.10.0"} 7 | http-kit/http-kit {:mvn/version "2.7.0"} 8 | com.lambdaisland/hiccup {:mvn/version "0.0.4"}} 9 | :aliases {:dev {:jvm-opts ["-Duser.timezone=UTC" "--enable-preview"]}}} 10 | -------------------------------------------------------------------------------- /virtual-threads/http-kit/src/server/core.clj: -------------------------------------------------------------------------------- 1 | (ns server.core 2 | (:gen-class) 3 | (:require [muuntaja.core :as m] 4 | [org.httpkit.server :as hk-server] 5 | [reitit.ring :as ring] 6 | [reitit.coercion.spec] 7 | [reitit.ring.coercion :as rrc] 8 | [reitit.ring.middleware.muuntaja :as muuntaja] 9 | [reitit.ring.middleware.parameters :as parameters] 10 | [reitit.coercion.malli :as malli] 11 | [lambdaisland.hiccup :as h]) 12 | (:import (java.util.concurrent Executors))) 13 | 14 | (def app 15 | (ring/ring-handler 16 | (ring/router 17 | ["/hello" 18 | {:get {:handler (fn [_] 19 | {:headers {"Content-Type" "text/html"} 20 | :status 200 21 | :body (do 22 | ;; simulate work by sleeping 23 | ;; for 50 milliseconds 24 | (Thread/sleep 50) 25 | (h/render [:b "hello!"]))})}}] 26 | {:data {:coercion malli/coercion 27 | :muuntaja m/instance 28 | :middleware [parameters/parameters-middleware 29 | rrc/coerce-request-middleware 30 | muuntaja/format-response-middleware 31 | rrc/coerce-response-middleware]}}))) 32 | 33 | (comment 34 | 35 | (def server (hk-server/run-server #'app 36 | {:port 8080 37 | ;; Use virtual threads 38 | :worker-pool (Executors/newVirtualThreadPerTaskExecutor)})) 39 | 40 | (def server (hk-server/run-server #'app 41 | {:port 8080 42 | :thread 50})) 43 | ;; stop server 44 | (server)) 45 | 46 | ;; HTTP kit 47 | 48 | ;; No virtual threads 49 | ;; Running 10s test @ http://127.0.0.1:8080/hello 50 | ;; 12 threads and 120 connections 51 | ;; Requests/sec: 83074.61 52 | ;; Transfer/sec: 11.73MB 53 | 54 | ;; Virtual threads 55 | ;; Running 10s test @ http://127.0.0.1:8080/hello 56 | ;; 12 threads and 120 connections 57 | ;; Requests/sec: 85202.97 58 | ;; Transfer/sec: 12.03MB 59 | 60 | ;; Running 10s test @ http://127.0.0.1:8080/hello 61 | ;; 12 threads and 120 connections 62 | ;; Requests/sec: 928.14 63 | ;; Transfer/sec: 134.15KB 64 | 65 | ;; Virtual threads each handler has a 50ms thread sleep 66 | ;; Running 10s test @ http://127.0.0.1:8080/hello 67 | ;; 12 threads and 120 connections 68 | ;; Requests/sec: 2208.67 69 | ;; Transfer/sec: 319.22KB 70 | 71 | ;; 2.3x 72 | -------------------------------------------------------------------------------- /virtual-threads/jetty/README.md: -------------------------------------------------------------------------------- 1 | # Virtual threads with ring and jetty 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2023/09/16/clojure-virtual-threads-with-ring-and-jetty.html). 4 | -------------------------------------------------------------------------------- /virtual-threads/jetty/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps 3 | {org.clojure/clojure {:mvn/version "1.11.1"} 4 | metosin/muuntaja {:mvn/version "0.6.8"} 5 | metosin/reitit {:mvn/version "0.7.0-alpha5"} 6 | ring/ring-core {:mvn/version "1.11.0-alpha1"} 7 | ring/ring-jetty-adapter {:mvn/version "1.11.0-alpha1"} 8 | com.lambdaisland/hiccup {:mvn/version "0.0.4"}} 9 | :aliases {:dev {:jvm-opts ["-Duser.timezone=UTC" "--enable-preview"]}}} 10 | -------------------------------------------------------------------------------- /virtual-threads/jetty/src/server/core.clj: -------------------------------------------------------------------------------- 1 | (ns server.core 2 | (:gen-class) 3 | (:require [muuntaja.core :as m] 4 | [reitit.ring :as ring] 5 | [reitit.coercion.spec] 6 | [reitit.ring.coercion :as rrc] 7 | [reitit.ring.middleware.muuntaja :as muuntaja] 8 | [reitit.ring.middleware.parameters :as parameters] 9 | [ring.adapter.jetty :refer [run-jetty]] 10 | [reitit.coercion.malli :as malli] 11 | [lambdaisland.hiccup :as h]) 12 | (:import (java.util.concurrent Executors) 13 | (org.eclipse.jetty.util.thread QueuedThreadPool))) 14 | 15 | (def app 16 | (ring/ring-handler 17 | (ring/router 18 | ["/hello" 19 | {:get {:handler (fn [_] 20 | {:headers {"Content-Type" "text/html"} 21 | :status 200 22 | :body (do 23 | ;; simulate work by sleeping 24 | ;; for 50 milliseconds 25 | (Thread/sleep 50) 26 | (h/render [:b "hello!"]))})}}] 27 | {:data {:coercion malli/coercion 28 | :muuntaja m/instance 29 | :middleware [parameters/parameters-middleware 30 | rrc/coerce-request-middleware 31 | muuntaja/format-response-middleware 32 | rrc/coerce-response-middleware]}}))) 33 | 34 | (comment 35 | (def jetty-server (run-jetty app {:port 8080 :join? false})) 36 | 37 | (def jetty-server 38 | (let [thread-pool (new QueuedThreadPool) 39 | _ (.setVirtualThreadsExecutor thread-pool 40 | (Executors/newVirtualThreadPerTaskExecutor))] 41 | (run-jetty app {:port 8080 42 | :join? false 43 | :thread-pool thread-pool}))) 44 | 45 | (.stop jetty-server)) 46 | 47 | ;; Jetty 48 | 49 | ;; No virtual threads 50 | ;; Running 10s test @ http://127.0.0.1:8080/hello 51 | ;; 12 threads and 120 connections 52 | ;; Requests/sec: 79032.31 53 | ;; Transfer/sec: 8.74MB 54 | 55 | ;; Virtual threads 56 | ;; Running 10s test @ http://127.0.0.1:8080/hello 57 | ;; 12 threads and 120 connections 58 | ;; Requests/sec: 73505.95 59 | ;; Transfer/sec: 8.13MB 60 | 61 | ;; No virtual threads each handler has a 50ms thread sleep 62 | ;; Running 10s test @ http://127.0.0.1:8080/hello 63 | ;; 12 threads and 120 connections 64 | ;; Requests/sec: 842.92 65 | ;; Transfer/sec: 95.49KB 66 | 67 | ;; Virtual threads each handler has a 50ms thread sleep 68 | ;; Running 10s test @ http://127.0.0.1:8080/hello 69 | ;; 12 threads and 120 connections 70 | ;; Requests/sec: 2218.19 71 | ;; Transfer/sec: 251.28KB 72 | 73 | ;; 2.6x 74 | -------------------------------------------------------------------------------- /virtual-threads/managing-throughput/README.md: -------------------------------------------------------------------------------- 1 | # Managing throughput with virtual threads 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2024/05/06/clojure-managing-throughput-with-virtual-threads.html). 4 | -------------------------------------------------------------------------------- /virtual-threads/managing-throughput/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0-alpha11"}} 3 | :aliases {}} 4 | -------------------------------------------------------------------------------- /virtual-threads/managing-throughput/src/server/core.clj: -------------------------------------------------------------------------------- 1 | (ns server.core 2 | (:import 3 | (java.util.concurrent Executors Semaphore 4 | ExecutorCompletionService))) 5 | 6 | (defonce executor 7 | (Executors/newVirtualThreadPerTaskExecutor)) 8 | 9 | (defonce sem 10 | ;; 2rec/s 11 | (Semaphore/new 2 true)) 12 | 13 | (defn rate-limited-sem-release [sem] 14 | ;; block until available 15 | (Semaphore/.acquire sem) 16 | ;; Create another virtual thread that will release this semaphore 17 | ;; to refill the bucked when the time is up. 18 | (Thread/startVirtualThread 19 | #(do (Thread/sleep 1000) ;; wait 1 second 20 | (Semaphore/.release sem)))) 21 | 22 | (defn upmap 23 | ([f coll] 24 | (upmap nil f coll)) 25 | ([sem f coll] 26 | (let [cs (ExecutorCompletionService/new executor)] 27 | (Thread/startVirtualThread 28 | #(run! 29 | (fn [x] 30 | (when sem (rate-limited-sem-release sem)) 31 | (ExecutorCompletionService/.submit cs (fn [] (f x)))) coll)) 32 | (->> (repeatedly #(deref (ExecutorCompletionService/.take cs))) 33 | (take (count coll)))))) 34 | 35 | (comment 36 | (time (upmap inc [1 2 3 4 5 6])) 37 | 38 | (time 39 | (->> (upmap sem inc [1 2 3 4 5 6 8 9 10]) 40 | (run! prn)))) 41 | 42 | -------------------------------------------------------------------------------- /virtual-threads/minimal-http-kit/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0"} 3 | http-kit/http-kit {:mvn/version "2.8.0"}} 4 | :aliases {}} 5 | -------------------------------------------------------------------------------- /virtual-threads/minimal-http-kit/src/app/core.clj: -------------------------------------------------------------------------------- 1 | (ns app.core 2 | (:gen-class) 3 | (:require [org.httpkit.server :as hk-server])) 4 | 5 | (defn handler [req] 6 | {:status 200 7 | :headers {"Content-Type" "text/html"} 8 | :body "

hello

"}) 9 | 10 | (defn start-server [] 11 | (hk-server/run-server handler 12 | {:port 8080})) 13 | 14 | (comment 15 | (def server (start-server)) 16 | 17 | (server)) 18 | 19 | ;; oha -c 100 -z 10s -m GET http://localhost:8080 20 | 21 | -------------------------------------------------------------------------------- /virtual-threads/minimal-http/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0"}} 3 | :aliases {:dev {:jvm-opts 4 | ["-Djdk.tracePinnedThreads=short"]}}} 5 | -------------------------------------------------------------------------------- /virtual-threads/minimal-http/src/app/core.clj: -------------------------------------------------------------------------------- 1 | (ns app.core 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.string :as str]) 5 | (:import (java.net ServerSocket SocketException Socket) 6 | (java.io InputStream OutputStream BufferedReader))) 7 | 8 | (def responses 9 | {200 "HTTP/1.1 200 OK\r\n" 10 | 301 "HTTP/1.1 301 Moved Permanently\n" 11 | 404 "HTTP/1.1 404 Not Found\r\n"}) 12 | 13 | (defprotocol StreamableResponseBody 14 | (write-body-to-stream [body response output-stream])) 15 | 16 | (extend-protocol StreamableResponseBody 17 | byte/1 18 | (write-body-to-stream [body _ ^OutputStream output-stream] 19 | (.write output-stream ^bytes body) 20 | (.close output-stream)) 21 | String 22 | (write-body-to-stream [body _ ^OutputStream output-stream] 23 | (doto (io/writer output-stream) 24 | (.write body) 25 | (.close))) 26 | InputStream 27 | (write-body-to-stream [body _ ^OutputStream output-stream] 28 | (with-open [body body] 29 | (io/copy body output-stream)) 30 | (.close output-stream))) 31 | 32 | (defn parse-request [^BufferedReader r] 33 | (loop [line (.readLine r) 34 | request {}] 35 | (if (seq (str/trim line)) 36 | (if (str/starts-with? line "GET") 37 | (let [[request-method uri protocol] (str/split line #" ")] 38 | (recur (.readLine r) 39 | (assoc request 40 | :request-method request-method 41 | :uri uri 42 | :protocol protocol))) 43 | (let [[k v] (str/split line #":")] 44 | (recur (.readLine r) (assoc request :headers {k v})))) 45 | request))) 46 | 47 | (defn send-response [^OutputStream out response] 48 | (write-body-to-stream 49 | (str 50 | (get responses (:status response)) 51 | (apply str (for [[k v] (:headers response)] 52 | (str k " " v "\r\n"))) 53 | "\r\n" 54 | (when (:body response) 55 | (:body response))) 56 | response 57 | out)) 58 | 59 | (defn thread [f] 60 | (.start (Thread. ^Runnable f))) 61 | 62 | (defn run-adapter [handler options] 63 | (thread 64 | (fn [] 65 | (let [^ServerSocket server (ServerSocket. (:port options))] 66 | (try 67 | (while (not (.isClosed server)) 68 | (let [^Socket conn (.accept server)] 69 | (Thread/startVirtualThread 70 | #(try 71 | (let [^InputStream in (io/reader 72 | (.getInputStream conn)) 73 | ^OutputStream out (.getOutputStream conn)] 74 | (send-response out 75 | (handler (parse-request in)))) 76 | (catch SocketException _disconnect))))) 77 | (catch SocketException _disconnect) 78 | (finally (.close server))))))) 79 | 80 | (defn handler [req] 81 | {:status 200 82 | :headers {"Content-Type:" "text/html"} 83 | :body "

hello

"}) 84 | 85 | (defn start-server [] 86 | (run-adapter 87 | handler 88 | {:port 8080})) 89 | 90 | (comment 91 | (def server (start-server)) 92 | 93 | (server)) 94 | 95 | ;; minimal-jetty 96 | ;; Requests/sec: 59548.7883 97 | ;; minimal-http-kit 98 | ;; Requests/sec: 83047.3862 99 | ;; minimal-http 100 | ;; Requests/sec: 513.7097 101 | ;; Interestingly virtual threads are no more faster than real threads 102 | ;; wonder if SocketSever is the bottleneck here? 103 | 104 | -------------------------------------------------------------------------------- /virtual-threads/minimal-jetty/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0"} 3 | ring/ring-jetty-adapter {:mvn/version "1.12.2"}} 4 | :aliases {}} 5 | -------------------------------------------------------------------------------- /virtual-threads/minimal-jetty/src/app/core.clj: -------------------------------------------------------------------------------- 1 | (ns app.core 2 | (:gen-class) 3 | (:require [ring.adapter.jetty :refer [run-jetty]]) 4 | (:import (java.util.concurrent Executors) 5 | (org.eclipse.jetty.util.thread QueuedThreadPool))) 6 | 7 | (defn handler [req] 8 | {:status 200 9 | :headers {"Content-Type" "text/html"} 10 | :body "

hello

"}) 11 | 12 | (defn start-server [] 13 | (let [thread-pool (new QueuedThreadPool) 14 | _ (.setVirtualThreadsExecutor thread-pool 15 | (Executors/newVirtualThreadPerTaskExecutor))] 16 | (run-jetty handler 17 | {:port 8080 18 | :join? false 19 | :thread-pool thread-pool}))) 20 | 21 | (comment 22 | (def server (start-server)) 23 | 24 | (.stop server)) 25 | 26 | ;; oha -c 100 -z 10s -m GET http://localhost:8080 27 | -------------------------------------------------------------------------------- /virtual-threads/structured-concurrency/README.md: -------------------------------------------------------------------------------- 1 | # Structured Concurrency and Scoped Values 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2024/05/14/clojure-structured-concurrency-and-scoped-values.html). 4 | -------------------------------------------------------------------------------- /virtual-threads/structured-concurrency/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0-alpha11"}} 3 | :aliases 4 | {:dev {:jvm-opts ["--enable-preview"]}}} 5 | -------------------------------------------------------------------------------- /virtual-threads/structured-concurrency/src/server/core.clj: -------------------------------------------------------------------------------- 1 | (ns server.core 2 | (:refer-clojure :exclude [pmap]) 3 | (:import 4 | (java.lang ScopedValue) 5 | (java.util.function Supplier) 6 | (java.util.concurrent 7 | ExecutorService 8 | Executors 9 | Callable 10 | StructuredTaskScope 11 | StructuredTaskScope$Subtask 12 | StructuredTaskScope$ShutdownOnFailure 13 | StructuredTaskScope$ShutdownOnSuccess))) 14 | 15 | (comment ;; pmap without structured concurrency 16 | (defonce executor 17 | (Executors/newVirtualThreadPerTaskExecutor)) 18 | 19 | (defn pmap [f coll] 20 | (->> (mapv (fn [x] (ExecutorService/.submit executor 21 | ;; More than one matching method found: submit 22 | ;; So we need to type hint Callable 23 | ^Callable (fn [] (f x)))) 24 | coll) 25 | (mapv deref))) 26 | 27 | (pmap (fn [x] 28 | (let [result (inc x)] 29 | (Thread/sleep 50) ;; simulate some io 30 | (print (str "complete " result "\n")) 31 | result)) 32 | [1 2 3 4 5 6]) 33 | 34 | (pmap (fn [x] 35 | (let [result (inc x)] 36 | (Thread/sleep 50) ;; simulate some io 37 | (print (str "complete " result "\n")) 38 | result)) 39 | [1 2 "3" 4 5 6]) 40 | ) 41 | 42 | (comment ;; pmap with structured concurrency ShutdownOnFailure 43 | (defn pmap [f coll] 44 | (with-open [scope (StructuredTaskScope$ShutdownOnFailure/new)] 45 | (let [r (mapv (fn [x] 46 | (StructuredTaskScope/.fork scope 47 | (fn [] (f x)))) 48 | coll)] 49 | ;; join subtasks and propagate errors 50 | (.. scope join throwIfFailed) 51 | ;; fork returns a Subtask/Supplier not a future 52 | (mapv StructuredTaskScope$Subtask/.get r)))) 53 | 54 | (pmap (fn [x] 55 | (let [result (inc x)] 56 | (Thread/sleep 50) 57 | (print (str "complete " result "\n")) 58 | result)) 59 | [1 2 3 4 5 6]) 60 | 61 | (pmap (fn [x] 62 | (let [result (inc x)] 63 | (Thread/sleep 50) 64 | (print (str "complete " result "\n")) 65 | result)) 66 | [1 2 "3" 4 5 6]) 67 | ) 68 | 69 | (comment ;; alts with structured concurrency ShutdownOnSuccess 70 | (defn alts [f coll] 71 | (with-open [scope (StructuredTaskScope$ShutdownOnSuccess/new)] 72 | (run! (fn [x] 73 | (StructuredTaskScope/.fork scope (fn [] (f x)))) 74 | coll) 75 | ;; Throws if none of the subtasks completed successfully 76 | (.. scope join result))) 77 | 78 | (alts (fn [x] 79 | (let [result (inc x)] 80 | (Thread/sleep 100) 81 | (print (str "complete " result "\n")) 82 | result)) 83 | [1 2 3 4 5 6]) 84 | 85 | (alts (fn [x] 86 | (let [result (inc x)] 87 | (Thread/sleep 100) 88 | (print (str "complete " result "\n")) 89 | result)) 90 | [1 2 "3" 4 5 6]) 91 | ) 92 | 93 | (comment ;; binding conveyance with bound-fn* 94 | (def ^:dynamic *inc-amount* nil) 95 | 96 | (binding [*inc-amount* 3] 97 | (pmap (fn [x] 98 | (let [result (+ x *inc-amount*)] 99 | (Thread/sleep 50) 100 | (print (str "complete " result "\n")) 101 | result)) 102 | [1 2 3 4 5 6])) 103 | 104 | (binding [*inc-amount* 3] 105 | (pmap (bound-fn* 106 | (fn [x] 107 | (let [result (+ x *inc-amount*)] 108 | (Thread/sleep 50) 109 | (print (str "complete " result "\n")) 110 | result))) 111 | [1 2 3 4 5 6])) 112 | ) 113 | 114 | (comment ;; Scoped Values 115 | (def scoped-inc-amount (ScopedValue/newInstance)) 116 | 117 | ;; Single scoped value 118 | (ScopedValue/getWhere scoped-inc-amount 3 119 | (delay ;; https://clojure.atlassian.net/browse/CLJ-2792 120 | (pmap (fn [x] 121 | (let [result (+ x (ScopedValue/.get scoped-inc-amount))] 122 | (Thread/sleep 50) 123 | (print (str "complete " result "\n")) 124 | result)) 125 | [1 2 3 4 5 6]))) 126 | 127 | ;; pre CLJ-2792 128 | (ScopedValue/getWhere scoped-inc-amount 3 129 | (reify Supplier 130 | (get [_] 131 | (pmap (fn [x] 132 | (let [result (+ x (ScopedValue/.get scoped-inc-amount))] 133 | (Thread/sleep 50) 134 | (print (str "complete " result "\n")) 135 | result)) 136 | [1 2 3 4 5 6])))) 137 | 138 | (def scoped-dec-amount (ScopedValue/newInstance)) 139 | 140 | ;; Multiple scoped values 141 | (.. (ScopedValue/where scoped-inc-amount 3) 142 | (ScopedValue/where scoped-dec-amount -2) 143 | (ScopedValue/get 144 | (delay 145 | (pmap (fn [x] 146 | (let [result (+ x 147 | (ScopedValue/.get scoped-inc-amount) 148 | (ScopedValue/.get scoped-dec-amount))] 149 | (Thread/sleep 50) 150 | (print (str "complete " result "\n")) 151 | result)) 152 | [1 2 3 4 5 6])))) 153 | 154 | ;; Convenience macro 155 | (defmacro scoped-binding [bindings & body] 156 | (assert (vector? bindings) 157 | "a vector for its binding") 158 | (assert (even? (count bindings)) 159 | "an even number of forms in binding vector") 160 | `(.. ~@(->> (partition 2 bindings) 161 | (map (fn [[k v]] 162 | (assert (-> k resolve deref type (= ScopedValue)) 163 | (str k " is not a ScopedValue")) 164 | `(ScopedValue/where ~k ~v)))) 165 | (ScopedValue/get (delay ~@body)))) 166 | 167 | (scoped-binding [scoped-inc-amount 3 168 | scoped-dec-amount -2] 169 | (pmap (fn [x] 170 | (let [result (+ x 171 | (ScopedValue/.get scoped-inc-amount) 172 | (ScopedValue/.get scoped-dec-amount))] 173 | (Thread/sleep 50) 174 | (print (str "complete " result "\n")) 175 | result)) 176 | [1 2 3 4 5 6])) 177 | 178 | ) 179 | -------------------------------------------------------------------------------- /zippers/manipulating-html-and-xml-example/README.md: -------------------------------------------------------------------------------- 1 | # Manipulating HTML and XML 2 | 3 | The accompanying blog post can be found [here](https://andersmurphy.com/2019/11/17/clojure-manipulating-html-and-xml-with-zippers.html). 4 | -------------------------------------------------------------------------------- /zippers/manipulating-html-and-xml-example/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.10.1"} 2 | hiccup {:mvn/version "1.0.5"} 3 | hickory {:mvn/version "0.7.1"}}} 4 | -------------------------------------------------------------------------------- /zippers/manipulating-html-and-xml-example/page.html: -------------------------------------------------------------------------------- 1 | Anders Murphy

Clojure: sorting tuples

Sun, 27 Oct 2019 00:00:00 GMT

https://andersmurphy.com/2019/10/27/clojure-sorting-tuples.html

Clojure: generating HTML and XML

Sun, 8 Sep 2019 00:00:00 GMT

https://andersmurphy.com/2019/09/08/clojure-generating-html-and-xml.html

Clojure: using java.time with clojure.java.jdbc

Sat, 3 Aug 2019 00:00:00 GMT

https://andersmurphy.com/2019/08/03/clojure-using-java-time-with-jdbc.html

Clojure: connection pooling with hikari-cp

Sun, 14 Jul 2019 00:00:00 GMT

https://andersmurphy.com/2019/07/14/clojure-connection-pooling-with-hikari-cp.html

Clojure: emoji in strings

Tue, 2 Jul 2019 00:00:00 GMT

https://andersmurphy.com/2019/07/02/clojure-emoji-in-strings.html

Clojure: a debug macro for threading macros using tap>

Tue, 4 Jun 2019 00:00:00 GMT

https://andersmurphy.com/2019/06/04/clojure-a-debug-macro-for-threading-macros-using-tap.html

Clojure: intro to tap> and accessing private vars

Sat, 1 Jun 2019 00:00:00 GMT

https://andersmurphy.com/2019/06/01/clojure-intro-to-tap-and-accessing-private-vars.html

Clojure: sorting a sequence based on another sequence

Sat, 25 May 2019 00:00:00 GMT

https://andersmurphy.com/2019/05/25/clojure-sorting-a-sequence-based-on-another-sequence.html

Clojure: personalising text

Sat, 18 May 2019 00:00:00 GMT

https://andersmurphy.com/2019/05/18/clojure-personalising-text.html

Clojure: case conversion and boundaries

Sat, 4 May 2019 00:00:00 GMT

https://andersmurphy.com/2019/05/04/clojure-case-conversion-and-boundaries.html

Clojure: contains? and some

Fri, 5 Apr 2019 00:00:00 GMT

https://andersmurphy.com/2019/04/05/clojure-contains-and-some.html

Clojure: sorting

Sat, 9 Mar 2019 00:00:00 GMT

https://andersmurphy.com/2019/03/09/clojure-sorting.html

Lisp-1 vs Lisp-2

Fri, 8 Mar 2019 00:00:00 GMT

https://andersmurphy.com/2019/03/08/lisp-1-vs-lisp-2.html

Clojure: merging maps by key (join)

Sat, 16 Feb 2019 00:00:00 GMT

https://andersmurphy.com/2019/02/16/clojure-merging-maps-by-key.html

Clojure: string interpolation

Tue, 15 Jan 2019 00:00:00 GMT

https://andersmurphy.com/2019/01/15/clojure-string-interpolation.html

Clojure: sending emails with SendGrid

Sun, 6 Jan 2019 00:00:00 GMT

https://andersmurphy.com/2019/01/06/clojure-sending-emails-with-sendgrid.html

Clojure: validating phone numbers

Sat, 24 Nov 2018 00:00:00 GMT

https://andersmurphy.com/2018/11/24/clojure-validating-phone-numbers.html

Clojure: juxt and separate

Sun, 18 Nov 2018 00:00:00 GMT

https://andersmurphy.com/2018/11/18/clojure-juxt-and-separate.html

Clojure: map-values and map-keys

Sat, 10 Nov 2018 00:00:00 GMT

https://andersmurphy.com/2018/11/10/clojure-map-values-and-keys.html

Desert island code: compose and pipe

Thu, 4 Jan 2018 00:00:00 GMT

https://andersmurphy.com/2018/01/04/desert-island-code-compose-and-pipe.html

Desert island code: curry

Tue, 2 Jan 2018 00:00:00 GMT

https://andersmurphy.com/2018/01/02/desert-island-code-curry.html

Desert island code: reduce map and filter

Thu, 28 Dec 2017 00:00:00 GMT

https://andersmurphy.com/2017/12/28/desert-island-code-reduce-map-and-filter.html

Managing obfuscation with annotations

Sat, 8 Oct 2016 00:00:00 GMT

https://andersmurphy.com/2016/10/08/managing-obfuscation-with-annotations.html

Using Proguard instead of multidex

Thu, 19 May 2016 00:00:00 GMT

https://andersmurphy.com/2016/05/19/using-proguard-instead-of-multidex.html

Signing your app

Tue, 17 May 2016 00:00:00 GMT

https://andersmurphy.com/2016/05/17/signing-your-app.html

Introduction to Kotlin on Android

Tue, 6 Oct 2015 00:00:00 GMT

https://andersmurphy.com/2015/10/06/introduction-to-kotlin-on-android.html

Setting up Retrolambda

Wed, 16 Sep 2015 00:00:00 GMT

https://andersmurphy.com/2015/09/16/setting-up-retrolambda.html

Enabling multidex on Android

Thu, 10 Sep 2015 00:00:00 GMT

https://andersmurphy.com/2015/09/10/enabling-multidex-on-android.html

Binding Android views with Butter Knife

Wed, 2 Sep 2015 00:00:00 GMT

https://andersmurphy.com/2015/09/02/binding-android-views-with-butter-knife.html

Advantages of an Android free zone

Thu, 27 Aug 2015 00:00:00 GMT

https://andersmurphy.com/2015/08/27/advantages-of-an-android-free-zone.html
-------------------------------------------------------------------------------- /zippers/manipulating-html-and-xml-example/src/manipulating_html_and_xml_example/core.clj: -------------------------------------------------------------------------------- 1 | (ns manipulating-html-and-xml-example.core 2 | (:require [hiccup.core :as hiccup] 3 | [hickory.core :as hick] 4 | [clojure.xml :as xml] 5 | [hickory.zip :as hick-zip] 6 | [clojure.zip :as zip])) 7 | 8 | (def xml-feed (xml/parse "https://andersmurphy.com/feed.xml")) 9 | 10 | (defn xml-feed->hiccup [xml-feed] 11 | (->> (zip/xml-zip xml-feed) 12 | (iterate zip/next) 13 | (take-while (complement zip/end?)) 14 | (map zip/node) 15 | (filter (fn [node] (and (associative? node) 16 | (= (:tag node) :item)))) 17 | (map :content) 18 | (map (fn [[{[title] :content} 19 | {[date] :content} 20 | {[link] :content}]] 21 | [:div 22 | [:h1 title] 23 | [:p date] 24 | [:a {:href link} link]])))) 25 | 26 | (def html-page (slurp "https://andersmurphy.com/")) 27 | 28 | (defn zip-select-first [loc tag pred] 29 | (when-not (zip/end? loc) 30 | (if (some 31 | (every-pred associative? 32 | #(some-> % tag pred)) 33 | (zip/node loc)) 34 | loc 35 | (recur (zip/next loc) tag pred)))) 36 | 37 | (defn build-page [] 38 | (let [content (xml-feed->hiccup xml-feed)] 39 | (spit "page.html" 40 | (-> html-page 41 | hick/parse 42 | hick/as-hiccup 43 | hick-zip/hiccup-zip 44 | (zip-select-first :class #(= % "content container")) 45 | (zip/replace [:div {:class "content container"} content]) 46 | zip/root 47 | hiccup/html)))) 48 | --------------------------------------------------------------------------------