diff --git a/00_frontmatter/acknowledgements.asciidoc b/00_frontmatter/acknowledgements.asciidoc index 17dee713..c0831b80 100644 --- a/00_frontmatter/acknowledgements.asciidoc +++ b/00_frontmatter/acknowledgements.asciidoc @@ -25,6 +25,7 @@ it together. Those contributors are: * Daemian Mack, https://github.com/daemianmack[daemianmack] on GitHub * Dan Allen, https://github.com/mojavelinux[mojavelinux] on GitHub * Daniel Gregoire, https://github.com/semperos[semperos] on GitHub +* David McNeil, https://github.com/david-mcneil[david-mcneil] on GitHub * Dmitri Sotnikov, https://github.com/yogthos[yogthos] on GitHub * Edmund Jackson, https://github.com/ejackson[ejackson] on GitHub * Eric Normand, https://github.com/ericnormand[ericnormand] on GitHub diff --git a/00_frontmatter/conventions.asciidoc b/00_frontmatter/conventions.asciidoc index 2870188a..8397c0da 100644 --- a/00_frontmatter/conventions.asciidoc +++ b/00_frontmatter/conventions.asciidoc @@ -69,7 +69,7 @@ retain the traditional REPL style (with `user=>`). What follows is an example of each, a REPL-only sample and its simplified version. ._REPL-only_: -[source,shell-session] +[source,text] ---- user=> (+ 1 2) 3 diff --git a/00_frontmatter/frontmatter.asciidoc b/00_frontmatter/frontmatter.asciidoc index af487508..868f8835 100644 --- a/00_frontmatter/frontmatter.asciidoc +++ b/00_frontmatter/frontmatter.asciidoc @@ -158,7 +158,7 @@ asynchronous coordination with +core.async+. * <>, deals with all the ways in which your program can interact with the local computer upon which it is running. This -includes reading fromand writing to standard input and output streams, +includes reading from and writing to standard input and output streams, creating and manipulating files, serializing and deserializing files, etc. diff --git a/01_primitive-data/1-02_managing-whitespace.asciidoc b/01_primitive-data/1-02_managing-whitespace.asciidoc index 8883a6ca..4ce37057 100644 --- a/01_primitive-data/1-02_managing-whitespace.asciidoc +++ b/01_primitive-data/1-02_managing-whitespace.asciidoc @@ -36,8 +36,8 @@ pass:[clojure.string/replace "ROT13: Whyvhf Pnrfne" ;; Or, to reconstitute a file from lines (if they already have newlines...) -(def lines ["#! /bin/bash\n", "du -a ./ | sort -n -r\n"]) +(def lines ["#! /bin/bash\n" + "du -a ./ | sort -n -r\n"]) (apply str lines) ;; -> "#! /bin/bash\ndu -a ./ | sort -n -r\n" ---- @@ -80,7 +81,8 @@ of your +rows+ collection: ---- ;; Constructing a CSV from a header string and vector of rows (def header "first_name,last_name,employee_number\n") -(def rows ["luke,vanderhart,1","ryan,neufeld,2"]) +(def rows ["luke,vanderhart,1" + "ryan,neufeld,2"]) (apply str header (interpose "\n" rows)) ;; -> "first_name,last_name,employee_number\nluke,vanderhart,1\nryan,neufeld,2" diff --git a/01_primitive-data/1-05_integer-to-character-conversions.asciidoc b/01_primitive-data/1-05_integer-to-character-conversions.asciidoc index 7b2c695e..7bda27e5 100644 --- a/01_primitive-data/1-05_integer-to-character-conversions.asciidoc +++ b/01_primitive-data/1-05_integer-to-character-conversions.asciidoc @@ -45,9 +45,10 @@ code point specified by the integer: (char 945) ;; -> \α -(reduce #(str %1 (char %2)) - "" - [115 101 99 114 101 116 32 109 101 115 115 97 103 101 115]) + +(def codes [115 101 99 114 101 116 32 109 101 115 115 97 103 101 115]) +;; -> #'user/codes +(apply str (map char codes)) ;; -> "secret messages" ---- diff --git a/01_primitive-data/1-07_regexp-matching.asciidoc b/01_primitive-data/1-07_regexp-matching.asciidoc index 3bd4ac4e..d415174a 100644 --- a/01_primitive-data/1-07_regexp-matching.asciidoc +++ b/01_primitive-data/1-07_regexp-matching.asciidoc @@ -38,11 +38,11 @@ only if the _entire_ string matches the pattern: [source,clojure] ---- -;; In find, #"\w+" is any contiguous word characters +;; In re-find, #"\w+" is any contiguous word characters (re-find #"\w+" "my-param") ;; -> "my" -;; But in matches, #"\w+" means "all word characters" +;; But in re-matches, #"\w+" means "all word characters" (re-matches #"\w+" "my-param") ;; -> nil diff --git a/01_primitive-data/1-11_inflecting-strings.asciidoc b/01_primitive-data/1-11_inflecting-strings.asciidoc index 2c9425e8..e1f8388c 100644 --- a/01_primitive-data/1-11_inflecting-strings.asciidoc +++ b/01_primitive-data/1-11_inflecting-strings.asciidoc @@ -89,7 +89,7 @@ The library also has support for inflections like +camelize+,(((camel case)))((( [source,clojure] ---- ;; Convert "snake_case" to "CamelCase" -(inf/camelize "my_object") +(inf/camel-case "my_object") ;; -> "MyObject" ;; Clean strings for usage as URL parameters diff --git a/01_primitive-data/1-16_rounding.asciidoc b/01_primitive-data/1-16_rounding.asciidoc index a76c9d1d..3f9ce10e 100644 --- a/01_primitive-data/1-16_rounding.asciidoc +++ b/01_primitive-data/1-16_rounding.asciidoc @@ -81,7 +81,7 @@ mode and any number of expressions, executing those expressions in a +BigDecimal+ context tuned to that precision. So what does precision look like? Well, it's a little strange. The most basic precision is simply a positive integer "scale" value. This value specifies the -number of decimal places to work with. More complex precisions involve a +number of significant figures to work with. More complex precisions involve a +:rounding+ value, specified as a key/value pair like +:rounding FLOOR+ (this _is_ a macro of course, so why not?). When not specified, the default rounding mode is +HALF_UP+, but any of the values +CEILING+, +FLOOR+, +HALF_UP+, diff --git a/01_primitive-data/1-20_simple-statistics.asciidoc b/01_primitive-data/1-20_simple-statistics.asciidoc index 486964a4..6f8f8d97 100644 --- a/01_primitive-data/1-20_simple-statistics.asciidoc +++ b/01_primitive-data/1-20_simple-statistics.asciidoc @@ -67,11 +67,11 @@ retrieve the discrete list of modes: ---- (defn mode [coll] (let [freqs (frequencies coll) - occurrences (group-by second freqs) + occurrences (group-by val freqs) modes (last (sort occurrences)) modes (->> modes - second - (map first))] + val + (map key))] modes)) (mode [:alan :bob :alan :greg]) @@ -124,23 +124,23 @@ Here is a breakdown of how +mode+ works: ---- (defn mode [coll] (let [freqs (frequencies coll) ; <1> - occurrences (group-by second freqs) ; <2> + occurrences (group-by val freqs) ; <2> modes (last (sort occurrences)) ; <3> modes (->> modes ; <4> - second - (map first))] + val + (map key))] modes)) ---- <1> +frequencies+ returns a map that tallies the number of times each value in +coll+ occurs. This would be something like +{:a 1 :b 2}+. -<2> +group-by+ with +second+ inverts the +freqs+ map, turning keys +<2> +group-by+ with +val+ inverts the +freqs+ map, turning keys into values and merging duplicates into groups. This would turn +{:a 1 :b 1}+ into `{1 [[:a 1] [:b 1]]}`. <3> The list of occurrences is now sortable. The last pair in the sorted list will be the modes, or most frequently occurring values. <4> The final step is processing the raw mode pairs into discrete - values. Taking +second+ turns pass:[[2 [[:alan 2\]\]\]] into pass:[[[:alan 2\]\]], and +(map first)+ turns that into +(:alan)+. + values. Taking +val+ turns pass:[[2 [[:alan 2\]\]\]] into pass:[[[:alan 2\]\]], and +(map key)+ turns that into +(:alan)+. The standard deviation measures how much, on average, the individual values in a population deviate from the mean: the higher the standard deviation is, the diff --git a/01_primitive-data/1-23_currency.asciidoc b/01_primitive-data/1-23_currency.asciidoc index 9e98d8f1..27664611 100644 --- a/01_primitive-data/1-23_currency.asciidoc +++ b/01_primitive-data/1-23_currency.asciidoc @@ -75,7 +75,7 @@ beyond arithmetic, including rounding and currency conversion: ;; -> # ---- -The +round+ function takes four arguments. The first three are an amount of currency, a(((numeric types, rounding/truncating)))(((rounding up/down))) +The +round+ function takes three arguments: an amount of currency, a(((numeric types, rounding/truncating)))(((rounding up/down))) scale factor, and a rounding mode. The scaling factor is a somewhat(((scaling factors))) peculiar argument. It might be familiar to you if you've ever done scaling with +BigDecimal+, which shares identical factors. A scale of diff --git a/01_primitive-data/1-33_relative-times.asciidoc b/01_primitive-data/1-33_relative-times.asciidoc index a18c9b20..8732b113 100644 --- a/01_primitive-data/1-33_relative-times.asciidoc +++ b/01_primitive-data/1-33_relative-times.asciidoc @@ -49,7 +49,7 @@ over +plus+ and +minus+: [source,clojure] ---- -;; 1.day.from_now +;; 1.year.from_now (t/plus (t/now) (t/years 1)) ;; -> # diff --git a/02_composite-data/2-09_get-item-at-index.asciidoc b/02_composite-data/2-09_get-item-at-index.asciidoc index 62a8bec9..9e0dcfd8 100644 --- a/02_composite-data/2-09_get-item-at-index.asciidoc +++ b/02_composite-data/2-09_get-item-at-index.asciidoc @@ -71,7 +71,7 @@ not found: [source,clojure] ---- (get [:a :b :c] 5) -;; -> :nil +;; -> nil (get [:a :b :c] 5 :not-found) ;; -> :not-found diff --git a/02_composite-data/2-13_testing-set-membership.asciidoc b/02_composite-data/2-13_testing-set-membership.asciidoc index 1d6213b0..8170b2a1 100644 --- a/02_composite-data/2-13_testing-set-membership.asciidoc +++ b/02_composite-data/2-13_testing-set-membership.asciidoc @@ -15,10 +15,11 @@ is a member of the set: [source,clojure] ---- -(contains? #{:red :white :green} :blue) +(def my-set #{:red :white :green}) +(contains? my-set :blue) ;; -> false -(contains? #{:red :white :green} :green) +(contains? my-set :green) ;; -> true ---- @@ -28,10 +29,22 @@ value itself if it is a member, or +nil+ if it is not: [source,clojure] ---- -(get #{:red :white :green} :blue) +(get my-set :blue) ;; -> nil -(get #{:red :white :green} :green) +(get my-set :green) +;; -> :green +---- + +If desired, you can also pass a third argument to be used as the default return value instead +of +nil+ if a set doesn't contain the value: + +[source,clojure] +---- +(get my-set :blue :no-such-luck) +;; -> :no-such-luck + +(get my-set :green :no-such-luck) ;; -> :green ---- @@ -41,8 +54,6 @@ member, and +nil+ otherwise: [source,clojure] ---- -(def my-set #{:red :white :green}) - (my-set :blue) ;; -> nil @@ -55,13 +66,32 @@ do with maps. Thus, the following is equivalent to having used +get+: [source,clojure] ---- -(:blue #{:red :white :green}) +(:blue my-set) ;; -> nil -(:green #{:red :white :green}) +(:green my-set) ;; -> :green ---- +When using a set as a function, one cannot specify a second argument +as a default value: + +[source,clojure] +---- +(my-set :blue :no-such-luck) +ArityException Wrong number of args (2) passed to: PersistentHashSet clojure.lang.AFn.throwArity (AFn.java:429) +---- + +However, you can add a default value when using a keyword as a function with a set as the first argument: + +[source,clojure] +---- +(:blue my-set :no-such-luck) +;; -> :no-such-luck + +(:green my-set :no-such-luck) +;; -> :green +---- ==== Discussion @@ -85,7 +115,7 @@ itself is both easy and idiomatic: ---- This snippet first creates an infinite lazy sequence consisting of -random numbers between 1 and 10, using +repeatedly+ to call +random integers between 0 (inclusive) and 10 (exclusive), using +repeatedly+ to call +rand-int+ (wrapped in an anonymous function) over and over. Then it feeds this sequence through a filter, with a set of the numbers 1–3 as the filter predicate. diff --git a/02_composite-data/2-20_as-sequences.asciidoc b/02_composite-data/2-20_as-sequences.asciidoc index 65965d3c..831a0679 100644 --- a/02_composite-data/2-20_as-sequences.asciidoc +++ b/02_composite-data/2-20_as-sequences.asciidoc @@ -80,6 +80,20 @@ their sort order in the map. For example: ;; -> ([:a 1] [:b 2] [:c 3] [:d 4]) ---- +It's also possible to retrieve the map entry for a particular key. +Use the +find+ function, which is similar to +get+ except two facts: +first, it returns map entries; second, it does'nt accept an optional +default return value: + +[source,clojure] +---- +(find {:name "Kvothe" :class "Bard"} :name) +;; -> [:name "Kvothe"] + +(find {:name "Kvothe" :class "Bard"} :race) +;; -> nil +---- + There is another interesting fact about the entry values in this sequence. They are printed as vectors, and they _are_ vectors insofar as they implement the full vector interface. However, their concrete @@ -92,7 +106,7 @@ be used to retrieve the key and value of an entry: [source,clojure] ---- -(def entry (first {:a 1 :b 2})) +(def entry (find {:a 1 :b 2} :a)) (class entry) ;; -> clojure.lang.MapEntry @@ -101,7 +115,7 @@ be used to retrieve the key and value of an entry: ;; -> :a (val entry) -;; -> :1 +;; -> 1 ---- These functions should be preferred to using the +first+ and +second+ diff --git a/02_composite-data/2-23_combining-maps.asciidoc b/02_composite-data/2-23_combining-maps.asciidoc index f4f3ff26..a9a5e5ae 100644 --- a/02_composite-data/2-23_combining-maps.asciidoc +++ b/02_composite-data/2-23_combining-maps.asciidoc @@ -77,17 +77,22 @@ concise solutions to common problems. For example, by merging with ---- It is also possible to merge nested maps by creating a recursive merge -function: +function. [source,clojure] ---- -(defn deep-merge - [& maps] - (apply merge-with deep-merge maps)) - -(deep-merge {:foo {:bar {:baz 1}}} - {:foo {:bar {:qux 42}}}) -;; -> {:foo {:bar {:qux 42, :baz 1}}} +;; Copied verbatim from the defunct clojure-contrib (http://bit.ly/deep-merge-with) +(defn deep-merge-with [f & maps] + (apply + (fn m [& maps] + (if (every? map? maps) + (apply merge-with m maps) + (apply f maps))) + maps)) + +(deep-merge-with + {:foo {:bar {:baz 1}}} + {:foo {:bar {:baz 6 :qux 42}}}) +;; -> {:foo {:bar {:qux 42, :baz 7}}} ---- As you saw in the previous examples, +merge-with+ is a versatile tool: we diff --git a/02_composite-data/2-26_determining-if-a-collection-holds-one-of-several-values.asciidoc b/02_composite-data/2-26_determining-if-a-collection-holds-one-of-several-values.asciidoc index 9b4fd16e..68defa4a 100644 --- a/02_composite-data/2-26_determining-if-a-collection-holds-one-of-several-values.asciidoc +++ b/02_composite-data/2-26_determining-if-a-collection-holds-one-of-several-values.asciidoc @@ -42,9 +42,9 @@ you're using to test a collection with. Consider the following: ---- Because the +some+ function returns the _value_ returned from the -predicate function, not just +true+ or +false+, using it with sets +predicate function when it's logically true, not just +true+ or +false+, using it with sets that contain +nil+ or +false+ probably isn't what you want--it will -return +nil+ or +false+ if the item actually _is_ in the set. The simplest solution is to test for +nil+ or +false+ separately, +return +nil+ if the item actually _is_ in the set. The simplest solution is to test for +nil+ or +false+ separately, using the +nil?+ or +false?+ predicate functions built into Clojure: [source,clojure] diff --git a/03_general-computing/3-01_bare-bones.asciidoc b/03_general-computing/3-01_bare-bones.asciidoc index 77f53924..7cfff325 100644 --- a/03_general-computing/3-01_bare-bones.asciidoc +++ b/03_general-computing/3-01_bare-bones.asciidoc @@ -37,7 +37,7 @@ Let's dissect the +java+ invocation here a bit. First, we set the Java classpath ---- A full explanation of the classpath is beyond the scope -of this recipe, but suffice it to say thatit is a list of places where Java +of this recipe, but suffice it to say that it is a list of places where Java should look to load classes. A full discussion of classpaths on the(((Java, classpaths))) JVM can be found at http://bit.ly/docs-classpaths. In the final part of the invocation, we specify the class that Java should load and execute the +main+ method: diff --git a/03_general-computing/3-02_interactive-docs.asciidoc b/03_general-computing/3-02_interactive-docs.asciidoc index a0332b1c..3d50f3eb 100644 --- a/03_general-computing/3-02_interactive-docs.asciidoc +++ b/03_general-computing/3-02_interactive-docs.asciidoc @@ -11,7 +11,7 @@ From a REPL, you want to read documentation for a function.((("REPL (read-eval-p Print the documentation for a function at the REPL with the +doc+ macro: -[source,shell-session] +[source,text] ---- user=> (doc conj) ------------------------- @@ -24,7 +24,7 @@ clojure.core/conj Print the source code for a function at the REPL with the +source+ macro: -[source,shell-session] +[source,text] ---- user=> (source reverse) (defn reverse @@ -37,7 +37,7 @@ user=> (source reverse) Find functions with documentation matching a given regular expression using +find-doc+: -[source,shell-session] +[source,text] ---- user=> (find-doc #"defmacro") ------------------------- @@ -68,7 +68,7 @@ You can peek under the hood at almost everything in Clojure at any time. The next example may be a bit mind-expanding if you're not used to this level of introspection at runtime: -[source,shell-session] +[source,text] ---- user=> (source source) (defmacro source @@ -92,7 +92,7 @@ available. You can get around this by namespacing the macros (+clojure.repl/doc+ instead of +doc+,) or, for extended use, by pass:[use]-ing the namespace: -[source,shell-session] +[source,text] ---- user=> (ns foo) foo=> (doc +) diff --git a/03_general-computing/3-03_exploring-namespaces.asciidoc b/03_general-computing/3-03_exploring-namespaces.asciidoc index 4fe078d7..25d9bada 100644 --- a/03_general-computing/3-03_exploring-namespaces.asciidoc +++ b/03_general-computing/3-03_exploring-namespaces.asciidoc @@ -11,7 +11,7 @@ You want to know what namespaces are loaded and what public vars are available i Use +loaded-libs+ to obtain the set of currently loaded namespaces. For example, from a REPL: -[source,shell-session] +[source,text] ---- user=> (pprint (loaded-libs)) #{clojure.core.protocols clojure.instant clojure.java.browse @@ -21,7 +21,7 @@ user=> (pprint (loaded-libs)) Use +dir+ from a REPL to print the public vars in a namespace: -[source,shell-session] +[source,text] ---- user=> (dir clojure.instant) parse-timestamp diff --git a/03_general-computing/3-05_main/3-05_main.asciidoc b/03_general-computing/3-05_main/3-05_main.asciidoc index e5a97e88..1a3d4fe7 100644 --- a/03_general-computing/3-05_main/3-05_main.asciidoc +++ b/03_general-computing/3-05_main/3-05_main.asciidoc @@ -117,7 +117,7 @@ provides a +-main+ function, like so: (println (foo.util/greet name)))) ---- -When you invoke Clojure with +foo.core+ as the "main" namespace, it +When you invoke Clojure with +foo+ as the "main" namespace, it calls the +-main+ function with the provided command-line arguments: [source,shell-session] diff --git a/03_general-computing/3-06_running-programs-from-the-command-line.asciidoc b/03_general-computing/3-06_running-programs-from-the-command-line.asciidoc index c355488e..75551346 100644 --- a/03_general-computing/3-06_running-programs-from-the-command-line.asciidoc +++ b/03_general-computing/3-06_running-programs-from-the-command-line.asciidoc @@ -62,7 +62,7 @@ whatever namespace you have specified as +:main+ in your project's _project.clj_ file. For example, setting +:main my-cli.core+ will invoke +my-cli.core/-main+. Alternatively, you may omit implementing +-main+ and provide +:main+ with the fully qualified name of a -function (e.g., +my.cli.core/alt-main+); this function will be invoked +function (e.g., +my-cli.core/alt-main+); this function will be invoked instead of +-main+. While the printed arguments in the preceding solution _look_ like Clojure @@ -82,7 +82,7 @@ no requirement that this function be prefixed with a +-+ (indicating it is a Java method); it simply must accept a variable number of arguments (as +-main+ normally does).(((functions, command line invocation of))) -For example, you can add a function +add-main+ to +my.cli/core+: +For example, you can add a function +add-main+ to +my-cli.core+: [source,clojure] ---- diff --git a/03_general-computing/3-07_parse-command-line-arguments.asciidoc b/03_general-computing/3-07_parse-command-line-arguments.asciidoc index 4fda8c65..f66d949d 100644 --- a/03_general-computing/3-07_parse-command-line-arguments.asciidoc +++ b/03_general-computing/3-07_parse-command-line-arguments.asciidoc @@ -6,14 +6,14 @@ by Ryan Neufeld; originally submitted by Nicolas Bessi ==== Problem You want to write command-line tools in Clojure that can parse input -arguments.(((command lines, parsing input arguments)))(((parsing, input arguments)))((("development ecosystem", "command line parsing")))(((tools.cli library)))((("Clojure", "clojure.tools.cli/cli"))) +arguments.(((command lines, parsing input arguments)))(((parsing, input arguments)))((("development ecosystem", "command line parsing")))(((tools.cli library)))((("Clojure", "clojure.tools.cli/parse-opts"))) ==== Solution Use the https://github.com/clojure/tools.cli[+tools.cli+] library. -Before starting, add `[org.clojure/tools.cli "0.2.4"]` to your project's +Before starting, add `[org.clojure/tools.cli "0.3.1"]` to your project's dependencies, or start a REPL using +lein-try+: [source,shell-session] @@ -21,46 +21,43 @@ dependencies, or start a REPL using +lein-try+: $ lein try org.clojure/tools.cli ---- -Use the +clojure.tools.cli/cli+ function in your project's +-main+ +Use the +clojure.tools.cli/parse-opts+ function in your project's +-main+ function entry point to parse command-line arguments:footnote:[Since +tools.cli+ is so cool, this example can run entirely at the REPL.] [source,clojure] ---- -(require '[clojure.tools.cli :refer [cli]]) +(require '[clojure.tools.cli :refer [parse-opts]]) (defn -main [& args] - (let [[opts args banner] (cli args - ["-h" "--help" "Print this help" - :default false :flag true])] - (when (:help opts) - (println banner)))) + (let [{:keys [options arguments summary errors]} (parse-opts args + [["-h" "--help" "Print this help" :default false]])] + (when (:help options) + (println summary)))) ;; Simulate entry into -main at the command line (-main "-h") ;; *out* -;; Usage: -;; -;; Switches Default Desc -;; -------- ------- ---- -;; -h, --no-help, --help false Print this help +;; -h, --help Print this help ---- ==== Discussion Clojure's +tools.cli+ is a simple library, with only one function, -+cli+, and a slim data-oriented API for specifying how arguments ++parse-opts+, and a slim data-oriented API for specifying how arguments should be parsed. Handily enough, there isn't much special about this function: an arguments vector and specifications go in, and a map of parsed -options, variadic arguments, and a help banner come out. It's really the +options, variadic arguments, a help summary, and error messages come out. It's really the epitome of good, composable functional programming. -To configure how options are parsed, pass any number of spec vectors +To configure how options are parsed, pass a sequence of spec vectors after the +args+ list. To specify a +:port+ parameter, for example, -you would provide the spec `["-p" "--port"]`. The +"-p"+ isn't +you would provide the spec `["-p" "--port PORT"]`. The +"-p"+ isn't strictly necessary, but it is customary to provide a single-letter -shortcut for command-line options (especially long ones). In the -returned +opts+ map, the text of the last option name will be interned +shortcut for command-line options (especially long ones). The +PORT+ is +added to indicate the option requires an argument, of which +PORT+ is a +short description. In the +returned +options+ map, the text of the last option name will be interned to a keyword (less the +--+). For example, +"--port"+ would become +:port+, and +"--super-long-option"+ would become +:super-long-option+. @@ -83,7 +80,7 @@ optional string following the final argument name: [source,clojure] ---- -["-p" "--port" "The incoming port the application will listen on."] +["-p" "--port PORT" "The incoming port the application will listen on."] ---- Everything after the argument name and description will be interpreted @@ -91,98 +88,120 @@ as options in key/value pairs. +tools.cli+ provides the following options: +:default+:: The default value returned in the absence of user input. - Without specifying, the default of +:default+ is +nil+. + Without specifying, the resulting option map will not contain an entry + for this option unless set by user. + ++:default-desc+:: An optional description of the default value. This should + be used when the string representation of the default value is too ugly + to be printed on the command line. -+:flag+:: If truthy (not +false+ or +nil+), indicates an argument - behaves like a flag or switch. This argument will _not_ take any - value as its input. - +:parse-fn+:: The function used to parse an argument's value. This can be used to turn string values into integers, floats, or other data types. - + +:assoc-fn+:: The function used to combine multiple values for a single argument. ++:validate-fn+:: A function that receives the parsed option value and returns + a falsy value when the value is invalid. + ++:validate-msg+:: An optional message that will be added to the +:errors+ + vector on validation failure. + ++:validate+:: A vector of +[validate-fn validate-msg]+. You can either set + +:validate-fn+ and +:validate-msg+ seperately or bundle them together this + way. + ++:id+, +:short-opt+, +:long-opt+, +:required+, +:desc+:: These options will + overwrite the values specified or indicated by the positional arguments, e.g. + +:port+, +"-p"+, +"--port"+, +PORT+, +"The incoming port the application + will listen on."+, respectively. + Here's a complete example: [source,clojure] ---- -(def app-specs [["-n" "--count" :default 5 +(defn assoc-max + "Associate a numeric value into a map. If a value already exists for the + desired key, keep the max of the new & old values." + [m k v] + (if (contains? m k) + (update-in m [k] max v) + (assoc m k v))) + +(def app-specs [["-n" "--count COUNT" :default 5 + :default-desc "FIVE" :parse-fn #(Integer. %) - :assoc-fn max] - ["-v" "--verbose" :flag true - :default true]]) + :assoc-fn assoc-max + :validate [#(< % 100) "Reached the maximum."]] + ["-v" nil :long-opt "--verbose" + :default false]]) -(first (apply cli ["-n" "2" "-n" "50"] app-specs)) -;; -> {:count 50, :verbose true} +(select-keys (parse-opts ["-n" "2" "-n" "50"] app-specs) [:options :errors]) +;; -> {:errors nil, :options {:verbose false, :count 50}} -(first (apply cli ["--no-verbose"] app-specs)) -;; -> {:count 5, :verbose false} ----- +(select-keys (parse-opts ["-n" "2" "-n" "200"] app-specs) [:options :errors]) +;; -> {:errors ["Failed to validate \"-n 200\": Reached the maximum."], :options {:verbose false, :count 5}} -When writing flag options, a useful shortcut is to omit the +:flag+ -option and add a "`[no-]`" prefix to the argument's name. +cli+ will -interpret this argument spec as including +:flag true+ without you having -to specify it as such: +(select-keys (parse-opts ["--verbose"] app-specs) [:options :errors]) +;; -> {:errors nil, :options {:verbose true, :count 5}} -[source,clojure] ----- -["-v" "--[no-]verbose" :default true] +(println (:summary (parse-opts ["--verbose"] app-specs))) +;; *out* +;; -n, --count COUNT FIVE +;; -v, --verbose ---- One thing the +tools.cli+ library _doesn't_ provide is a hook into the application container's launch life cycle. It is your responsibility to -add a +cli+ call to your +-main+ function and know when to print the -help banner. A general pattern for use is to capture the results of -+cli+ in a +let+ block and determine if help needs to be printed. This -is also useful for ensuring the validity of arguments (especially since -there is no +:required+ option): +add a +parse-opts+ call to your +-main+ function and know when to print the +help summary. A general pattern for use is to capture the results of ++parse-opts+ in a +let+ block and determine if help needs to be printed. This +is also useful for ensuring the validity of arguments: [source,clojure] ---- (def required-opts #{:port}) (defn missing-required? - "Returns true if opts is missing any of the required-opts" + "Returns true if opts is missing any of the required-opts" [opts] (not-every? opts required-opts)) (defn -main [& args] - (let [[opts args banner] (cli args - ["-h" "--help" "Print this help" - :default false :flag true] - ["-p" "--port" :parse-fn #(Integer. %)])] - (when (or (:help opts) - (missing-required? opts)) - (println banner)))) + (let [{:keys [options arguments summary errors]} (parse-opts args + [["-h" "--help" "Print this help" :default false] + ["-p" "--port PORT" :parse-fn #(Integer. %)]])] + (when (or (:help options) + (missing-required? options)) + (println summary)))) ---- As with many applications, you may want to accept a variable number of arguments; for example, a list of filenames. In most cases, you don't need to do anything special to capture these arguments--just supply them after any other options. These variadic -arguments will be returned as the second item in ++cli++'s returned vector: +arguments will be returned as the value of key +:auguments+ in ++parse-opts++'s returned map: [source,clojure] ---- -(second (apply cli ["-n" "5" "foo.txt" "bar.txt"] app-specs)) +(:arguments (parse-opts ["-n" "5" "foo.txt" "bar.txt"] app-specs)) ;; -> ["foo.txt" "bar.txt"] ---- If your variadic arguments look like flags, however, you'll need(((variadic arguments)))((("arguments, variadic"))) -another trick. Use +--+ as an argument to indicate to +cli+ that +another trick. Use +--+ as an argument to indicate to +parse-opts+ that everything that follows is a variadic argument. This is useful if you're invoking another program with the options originally passed to your program: [source,clojure] ---- -(second (apply cli ["-n" "5" "--port" "80"] app-specs)) -;; -> Exception '--port' is not a valid argument ... +(select-keys (parse-opts ["-n" "5" "--port" "80"] app-specs) [:arguments :errors]) +;; -> {:errors ["Unknown option: \"--port\""], :arguments ["80"]} -(second (apply cli ["-n" "5" "--" "--port" "80"] app-specs)) -;; -> ["--port" "80"] +(select-keys (parse-opts ["-n" "5" "--" "--port" "80"] app-specs) [:arguments :errors]) +;; -> {:errors nil, :arguments ["--port" "80"]} ---- Once you've finished toying with your application's option parsing at @@ -194,7 +213,7 @@ pass on to subsequent programs, so too must you use +--+ to indicate to [source,shell-session] ---- # If app-specs were rigged up to a project... -$ lein run -- -n 5 --no-verbose +$ lein run -- -n 5 --verbose ---- ==== See Also diff --git a/03_general-computing/3-09_polymorphism-with-protocols.asciidoc b/03_general-computing/3-09_polymorphism-with-protocols.asciidoc index c6b7e546..8994ee99 100644 --- a/03_general-computing/3-09_polymorphism-with-protocols.asciidoc +++ b/03_general-computing/3-09_polymorphism-with-protocols.asciidoc @@ -267,7 +267,7 @@ protocol's methods: (perimeter (->Square 1)) ;; -> 4 -;; Calculate the area of a parallelogram without defining a record +;; Calculate the area of a rectangle without defining a record (area (let [b 2 h 3] diff --git a/03_general-computing/3-11_core-async.asciidoc b/03_general-computing/3-11_core-async.asciidoc index 55e7de2d..14437e9b 100644 --- a/03_general-computing/3-11_core-async.asciidoc +++ b/03_general-computing/3-11_core-async.asciidoc @@ -207,7 +207,7 @@ Fewer changes are required to make +producer+ asynchronous: (doseq [msg (messages) out channels] ; <1> ( - (>! out item)))) ; <3> + (>! out msg)))) ; <3> ---- <1> For each message and channel... diff --git a/03_general-computing/3-13_core-logic.asciidoc b/03_general-computing/3-13_core-logic.asciidoc index d591addd..dc69ca41 100644 --- a/03_general-computing/3-13_core-logic.asciidoc +++ b/03_general-computing/3-13_core-logic.asciidoc @@ -267,21 +267,21 @@ solution: ---- (cl/run 1 [director-name] (cl/fresh [studio film-coll film cast director] - (cl/membero [studio :name "Newmarket Films"] graph) - (cl/membero [studio :type :FilmStudio] graph) - (cl/membero [studio :filmsCollection film-coll] graph) + (cl/membero [studio :name "Newmarket Films"] movie-graph) + (cl/membero [studio :type :FilmStudio] movie-graph) + (cl/membero [studio :filmsCollection film-coll] movie-graph) - (cl/membero [film-coll :type :FilmCollection] graph) - (cl/membero [film-coll :film film] graph) + (cl/membero [film-coll :type :FilmCollection] movie-graph) + (cl/membero [film-coll :film film] movie-graph) - (cl/membero [film :type :Film] graph) - (cl/membero [film :cast cast] graph) + (cl/membero [film :type :Film] movie-graph) + (cl/membero [film :cast cast] movie-graph) - (cl/membero [cast :type :FilmCast] graph) - (cl/membero [cast :director director] graph) + (cl/membero [cast :type :FilmCast] movie-graph) + (cl/membero [cast :director director] movie-graph) - (cl/membero [director :type :Person] graph) - (cl/membero [director :name director-name] graph))) + (cl/membero [director :type :Person] movie-graph) + (cl/membero [director :name director-name] movie-graph))) ;; -> ("Christopher Nolan") ---- diff --git a/04_local-io/4-03_exec-system-command.asciidoc b/04_local-io/4-03_exec-system-command.asciidoc index 1077fc99..97573c94 100644 --- a/04_local-io/4-03_exec-system-command.asciidoc +++ b/04_local-io/4-03_exec-system-command.asciidoc @@ -161,6 +161,6 @@ sequence of promises returned by +sh-pipe+):(((functions, realized?))) ==== See Also -* If you don't need piping or +clj-common-execs+ advanced features, +* If you don't need piping or +clj-commons-exec+'s advanced features, consider using http://bit.ly/clj-java-shell-api[+clojure.java.shell+] diff --git a/04_local-io/4-05_copy-file.asciidoc b/04_local-io/4-05_copy-file.asciidoc index 71d74da3..2e39d06c 100644 --- a/04_local-io/4-05_copy-file.asciidoc +++ b/04_local-io/4-05_copy-file.asciidoc @@ -63,7 +63,7 @@ will catch any exceptions and optionally overwrite: (or (:overwrite options) (= false (.exists destination)))) (try - (= nil (clojure.java.io/copy source destination)) ; <3> + (nil? (clojure.java.io/copy source destination)) ; <3> (catch Exception e (str "exception: " (.getMessage e)))) false))) @@ -84,7 +84,7 @@ The +safe-copy+ function takes the source and destination file paths to copy fro destination file exists, and if so, if it should be overwritten. If all is OK, it will then perform the +copy+ inside a +try-catch+ body. -<3> Note the equality check against +nil+ for when the file is copied. +<3> Note the test for +nil?+ when the file is copied. If you add this, you will always get a Boolean value from the function. This makes the function more convenient to use, since you can then conditionally check whether the operation succeed or not. diff --git a/04_local-io/4-06_delete-file.asciidoc b/04_local-io/4-06_delete-file.asciidoc index c02ec8ec..cf40ac88 100644 --- a/04_local-io/4-06_delete-file.asciidoc +++ b/04_local-io/4-06_delete-file.asciidoc @@ -94,8 +94,8 @@ must first delete all files in the given directory: The +delete-directory+ function will get a +file-seq+ with the contents of the given path. It will then filter to only get the files of that directory. The next step is to delete all the files, and then -finish up by deleting the directory itself. Note the call to +doall+. -If you do not call +doall+, the deletion of the files would be lazy and +finish up by deleting the directory itself. Note the call to +doseq+. +If you do not call +doseq+, the deletion of the files would be lazy and then the files would still exist when the call to delete the actual directory was made, so that call would fail.(((functions, delete-directory))) diff --git a/04_local-io/4-09_read-write-files.asciidoc b/04_local-io/4-09_read-write-files.asciidoc index ae7a8914..91559c85 100644 --- a/04_local-io/4-09_read-write-files.asciidoc +++ b/04_local-io/4-09_read-write-files.asciidoc @@ -13,7 +13,7 @@ Write a string to a file with the built-in +spit+ function: [source,clojure] ---- -(spit "stuff.txt" "my stuff") +(spit "stuff.txt" "all my stuff") ---- Read the contents of a file with the built-in +slurp+ function: @@ -72,7 +72,7 @@ newline, including the last one: (defn spitn "Append to file with newline" [path text] - (spit path (str text "\n") :append true) + (spit path (str text "\n") :append true)) ---- When used with strings, +spit+ and +slurp+ deal with the entire diff --git a/04_local-io/4-13_parallelizing-file-processing-using-iota.asciidoc b/04_local-io/4-13_parallelizing-file-processing-using-iota.asciidoc index 7a6fb38b..db20bea5 100644 --- a/04_local-io/4-13_parallelizing-file-processing-using-iota.asciidoc +++ b/04_local-io/4-13_parallelizing-file-processing-using-iota.asciidoc @@ -24,7 +24,7 @@ To count the words in a very large file, for example: [source,clojure] ---- -(require '[iota :as io] +(require '[iota :as iota] '[clojure.core.reducers :as r] '[clojure.string :as str]) @@ -33,7 +33,7 @@ To count the words in a very large file, for example: (defn count-map "Returns a map of words to occurence count in the given string" [s] - (reduce (fn [m w] (update-in m [w] (fnil (partial inc) 0))) + (reduce (fn [m w] (update-in m [w] (fnil inc 0))) {} (str/split s #" "))) diff --git a/04_local-io/4-16_edn-record.asciidoc b/04_local-io/4-16_edn-record.asciidoc index 2aa9ebcc..6dfe576e 100644 --- a/04_local-io/4-16_edn-record.asciidoc +++ b/04_local-io/4-16_edn-record.asciidoc @@ -83,7 +83,7 @@ records is close to, but not quite the tagged format edn expects. Where Clojure prints +"#user.SimpleRecord{:a 42}"+ for a +SimpleRecord+, what is really needed for edn is a tag-style string -like +""#user/SimpleRecord {:a 42}"+. The +like +"#user/SimpleRecord {:a 42}"+. The +miner.tagged/pr-tagged-record-on+ function understands how to write records in this format (to a +java.io.Writer+). By extending Clojure's +print-method+ multimethod with this function, you ensure Clojure diff --git a/04_local-io/4-17_unknown-reader-literals.asciidoc b/04_local-io/4-17_unknown-reader-literals.asciidoc index 7b80f2b6..7ffa2094 100644 --- a/04_local-io/4-17_unknown-reader-literals.asciidoc +++ b/04_local-io/4-17_unknown-reader-literals.asciidoc @@ -100,6 +100,12 @@ format: ;; -> #my.example/unknown 42 ---- +Under the hood, +pr+ delegates to the multimethod +print-method+ to +write a string representation of a value. By providing our own +implementation of +print-method+ for +TaggedValue+, the +pr+ family of +functions are able to serialize values in such a manner that they are +recoverable later (as illustrated above). + .clojure.core/read **** @@ -131,7 +137,12 @@ for reading data, it's generally better to use the edn variants.footnote:[The Cl * https://github.com/edn-format/edn[edn: extensible data notation] on GitHub * <>, and <> +* The entry for + https://clojuredocs.org/clojure.edn/read[+clojure.edn/read+ on + ClojureDocs] for a discussion about how the +clojure.core+ and + +clojure.edn+ reader procedures differ with regard to + +data_readers.clj+. ++++ -++++ \ No newline at end of file +++++ diff --git a/04_local-io/4-18_read-property-file.asciidoc b/04_local-io/4-18_read-property-file.asciidoc index 4758ae9d..621d019d 100644 --- a/04_local-io/4-18_read-property-file.asciidoc +++ b/04_local-io/4-18_read-property-file.asciidoc @@ -83,7 +83,7 @@ capability to parse values into numbers or Booleans, and providing default values. By default, ++propertea++'s +read-properties+ function treats all -property values as strings. Consider the following property file with +property values as strings. Consider a file +other.properties+ with an integer and Boolean key: ---- diff --git a/04_local-io/4-19_handle-binary-files.asciidoc b/04_local-io/4-19_handle-binary-files.asciidoc index c695872b..d46889fb 100644 --- a/04_local-io/4-19_handle-binary-files.asciidoc +++ b/04_local-io/4-19_handle-binary-files.asciidoc @@ -62,7 +62,7 @@ using an intermediate `ByteBuffer`: (defn prepare-string [strdata] (let [strlen (count strdata) version 66 - buflen (+ 1 4 (count strdata)) + buflen (+ 1 4 strlen) bb (ByteBuffer/allocate buflen) buf (byte-array buflen)] (doto bb diff --git a/04_local-io/4-20_read-write-csv.asciidoc b/04_local-io/4-20_read-write-csv.asciidoc index ebaeaacd..cce84b47 100644 --- a/04_local-io/4-20_read-write-csv.asciidoc +++ b/04_local-io/4-20_read-write-csv.asciidoc @@ -8,16 +8,25 @@ You need to read or write CSV data.((("I/O (input/output) streams", "CSV data")) ==== Solution -Use +clojure.data.csv/read-csv+ to lazily read CSV data from a +String+ or +java.io.Reader+: +Use +clojure.data.csv/read-csv+ to lazily read CSV data from a +String+ or +java.io.Reader+. + +To follow along with this recipe, add `[org.clojure/data.csv "0.1.2"]` to your project’s dependencies, or start a REPL with +lein-try+: + +[source,shell-session] +---- +$ lein try org.clojure/data.csv +---- [source,clojure] ---- -(clojure.data.csv/read-csv "this,is\na,test" ) +(require '[clojure.data.csv :as csv]) + +(csv/read-csv "this,is\na,test") ;; -> (["this" "is"] ["a" "test"]) (with-open [in-file (clojure.java.io/reader "in-file.csv")] (doall - (clojure.data.csv/read-csv in-file))) + (csv/read-csv in-file))) ;; -> (["this" "is"] ["a" "test"]) ---- @@ -25,7 +34,7 @@ Use +clojure.data.csv/write-csv+ to write CSV data to a +java.io.Writer+: [source,clojure] ---- (with-open [out-file (clojure.java.io/writer "out.csv")] - (clojure.data.csv/write-csv out-file [["this" "is"] ["a" "test"]])) + (csv/write-csv out-file [["this" "is"] ["a" "test"]])) ;; -> nil ---- @@ -33,7 +42,7 @@ Use +clojure.data.csv/write-csv+ to write CSV data to a +java.io.Writer+: The +clojure.data.csv+ library makes it easy to work with CSV. You need to remember that +read-csv+ is lazy; if you want to force it to read data immediately, you'll need to wrap the call to +read-csv+ in +doall+.(((csv library))) -When reading, you can change the separator and quote delimiters, which default to +\+ and +\"+, respectively. You must specify the delimiters using chars, not strings, though: +When reading, you can change the separator and quote delimiters, which default to +\,+ and +\"+, respectively. You must specify the delimiters using chars, not strings, though: [source,clojure] ---- @@ -46,7 +55,7 @@ When writing, as with +read-csv+, you can configure the separator, quote, and ne [source,clojure] ---- (with-open [out-file (clojure.java.io/writer "out.csv")] - (clojure.data.csv/write-csv out-file [["this" "is"] ["a" "test"]] + (csv/write-csv out-file [["this" "is"] ["a" "test"]] :separator \$ :quote \-)) ;; -> nil ---- diff --git a/04_local-io/4-22_read-write-xml.asciidoc b/04_local-io/4-22_read-write-xml.asciidoc index e0900fab..258a80e0 100644 --- a/04_local-io/4-22_read-write-xml.asciidoc +++ b/04_local-io/4-22_read-write-xml.asciidoc @@ -24,7 +24,7 @@ use +clojure.xml/parse+: [source,clojure] ---- -(require '[clojure.xml :as xml]) +(require 'clojure.xml) (clojure.xml/parse (clojure.java.io/file "simple.xml")) ;; -> {:tag :simple, :attrs nil, :content [ ;; {:tag :item, :attrs {:id "1"}, :content ["First"]} @@ -36,7 +36,7 @@ function from the +clojure.core+ namespace: [source,clojure] ---- -(xml/xml-seq (clojure.xml/parse (clojure.java.io/file "simple.xml"))) +(xml-seq (clojure.xml/parse (clojure.java.io/file "simple.xml"))) ---- +xml-seq+ returns a tree sequence of nodes; that is, a sequence of diff --git a/04_local-io/4-23_read-write-json.asciidoc b/04_local-io/4-23_read-write-json.asciidoc index b49cf98f..d97284f7 100644 --- a/04_local-io/4-23_read-write-json.asciidoc +++ b/04_local-io/4-23_read-write-json.asciidoc @@ -11,7 +11,17 @@ You need to read or write JSON data.((("I/O (input/output) streams", "JSON data" ==== Solution Use the +clojure.data.json/read-str+ function to read a string of JSON -as Clojure data: +as Clojure data. + +To follow along with this recipe, add `[org.clojure/data.json "0.2.6"]` to your project’s dependencies, or start a REPL with +lein-try+: + +[source,shell-session] +---- +$ lein try org.clojure/data.json +---- + +First things first, require `clojure.data.json`, then you're ready to start +parsing some JSON! [source,clojure] ---- @@ -47,7 +57,7 @@ same parameters and options as their string brethren: (json/read reader)) ;; -> [{"foo" "bar"}] ---- - + By virtue of JavaScript's simpler types, JSON notation has a much lower fidelity than Clojure data. As such, you may find you want to tweak the way keys or values are interpreted. diff --git a/04_local-io/4-24_pdf/4-24_pdf.asciidoc b/04_local-io/4-24_pdf/4-24_pdf.asciidoc index 8fdcdf38..16a2645f 100644 --- a/04_local-io/4-24_pdf/4-24_pdf.asciidoc +++ b/04_local-io/4-24_pdf/4-24_pdf.asciidoc @@ -113,10 +113,10 @@ Under the hood, collections of content are automatically expanded: [source, clojure] ---- ;; This *collection* of paragraphs... -(pdf [{} [[:paragraph "foo"] [:paragraph "bar"]]] "document.pdf") +(pdf/pdf [{} [[:paragraph "foo"] [:paragraph "bar"]]] "document.pdf") ;; is equivalent to these *individual* paragraphs -(pdf [{} [:paragraph "foo"] [:paragraph "bar"]] "document.pdf") +(pdf/pdf [{} [:paragraph "foo"] [:paragraph "bar"]] "document.pdf") ---- Apart from plain strings, each content element is represented as a diff --git a/05_network-io/5-02_async-http-requests.asciidoc b/05_network-io/5-02_async-http-requests.asciidoc index 757bb0a4..b505fa10 100644 --- a/05_network-io/5-02_async-http-requests.asciidoc +++ b/05_network-io/5-02_async-http-requests.asciidoc @@ -72,8 +72,8 @@ request completion: [source,clojure] ---- (http/get "http://example.com" - {:timeout 1000 ;; ms - :query-params {:search "value"}} + {:timeout 1000 ;; ms + :query-params {:search "value"}} (fn [{:keys [status headers body error]}] (if error (binding [*out* *err*] diff --git a/05_network-io/5-06_queueing-with-rabbitmq.asciidoc b/05_network-io/5-06_queueing-with-rabbitmq.asciidoc index 13a616d2..5fe1812f 100644 --- a/05_network-io/5-06_queueing-with-rabbitmq.asciidoc +++ b/05_network-io/5-06_queueing-with-rabbitmq.asciidoc @@ -78,6 +78,8 @@ channel, direct exchange, routing key (your queue name), and a message: [source,clojure] ---- +(require '[langohr.basic :as lb]) + (lb/publish ch "" resize-queue "hello.jpg") ---- @@ -258,7 +260,7 @@ messages from them: * Pull, using +langohr.basic/get+ * Push, using +langohr.consumers/subscribe+ -In the Push API, you make a synchronous invocation of the +get+ function +In the Pull API, you make a synchronous invocation of the +get+ function to retrieve a single message from a queue. The return value of +get+ is a tuple of metadata map and a body. The body payload, as returned, is an array of bytes--for plain-text messages you can use the string @@ -276,7 +278,7 @@ invoke the +String+ constructor with an encoding option of +"UTF-8"+: When no messages are present on a queue, +get+ will return +nil+. -In the Pull API, you subscribe to a queue using +In the Push API, you subscribe to a queue using +langohr.consumers/subscribe+, providing a message handler function that will be invoked for each message the queue receives. This function will be invoked with three arguments: a channel, metadata, and the body diff --git a/05_network-io/5-07_communicating-with-mqtt.asciidoc b/05_network-io/5-07_communicating-with-mqtt.asciidoc index 20be349f..457b8076 100644 --- a/05_network-io/5-07_communicating-with-mqtt.asciidoc +++ b/05_network-io/5-07_communicating-with-mqtt.asciidoc @@ -37,10 +37,11 @@ listens to a topic and prints messages it receives: (defn connect-and-subscribe [broker-addr topics subscriberid] (let [qos-levels (vec (repeat (count topics) 2)) ;; All at qos 2 + topics-and-qos (zipmap topics qos-levels) conn-sub (mh/connect broker-addr subscriberid)] (if (mh/connected? conn-sub) (do - (mh/subscribe conn-sub topics message-handler {:qos qos-levels}) + (mh/subscribe conn-sub topics-and-qos message-handler) conn-sub)))) ;; Return conn-sub for later mh/disconnect... (def subscriberid (mh/generate-id)) @@ -157,7 +158,7 @@ A multilevel wildcard and needs to appear at the end of the string. For example, these subscriptions are possible: +SNControl/#+:: -Any device under +SNControl/Florida+ (e.g., +SNControl/Florida/device1/sensor1+ and +SNControl/Florida/device1/sensor2+) and +SNControl/California/device1+ will match. +Any device under +SNControl+ (e.g., +SNControl/Florida/device1/sensor1+, +SNControl/Florida/device1/sensor2+ and +SNControl/California/device1+) will match. `SNControl/+/device1`:: Any +device1+ in states under domain +SNControl+ will match(e.g., +SNControl/Florida/device1+ and +SNControl/California/device1+). @@ -172,8 +173,8 @@ arriving from the broker. In the +connect-and-subscribe+ method, the the broker address and client ID (generated using +generate-id+, or some other unique ID). Then it checks that the connection has been established using the +connected?+ method. The +subscribe+ method is -invoked with the connection, a vector of topics to subscribe to, a message -handler, and a +:qos+ option. The subscriber then waits for some time +invoked with the connection, a map of topics to subscribe to with their respective qos and a message +handler. The subscriber then waits for some time and disconnects using the +disconnect+ method. The +connect-and-publish+ method calls the method +connect+, which @@ -202,4 +203,4 @@ some actions based on an alarm that the code has received). Planet Solutions with MQTT and IBM WebSphere MQ Telemetry_] (IBM Redbooks), by Valerie Lampkin _et al._, for a more detailed explanation of MQTT * The http://bit.ly/inno-begins-at-home[TED talk] by Andy - Stanford-Clark, one of the inventors of MQTT--a humorous and informative session on how MQTT can be used \ No newline at end of file + Stanford-Clark, one of the inventors of MQTT--a humorous and informative session on how MQTT can be used diff --git a/05_network-io/5-10_tcp-server.asciidoc b/05_network-io/5-10_tcp-server.asciidoc index bf2a9697..79863528 100644 --- a/05_network-io/5-10_tcp-server.asciidoc +++ b/05_network-io/5-10_tcp-server.asciidoc @@ -46,11 +46,11 @@ of these accept a +java.net.Socket+ as an argument and will return a +java.io.Reader+ or +java.io.Writer+ built from the socket's input and output streams. -+server+ handles actually creating an instance of +ServerSocket+ on a +The +serve+ function handles actually creating an instance of +ServerSocket+ on a particular port. It also takes a handler function, which will be used to process the incoming request and determine a response message. -After creating an instance of +ServerSocket+, +server+ immediately +After creating an instance of +ServerSocket+, +serve+ immediately calls its +accept+ method, which blocks until a TCP connection is established. When a client connects, it returns the session as an instance of +java.net.Socket+. @@ -64,9 +64,9 @@ also calls the +flush+ method on the writer to ensure that all the data is actually sent back to the client, instead of being buffered in the +Writer+ instance. -After sending the response, the +serve+ method returns. Because it +After sending the response, the +serve+ function returns. Because it used the +with-open+ macro when creating the server socket and the TCP -session socket, it will invoke the +close+ method on each before +session socket, it will invoke the +close+ function on each before returning, which disconnects the client and ends the session. To try it out, invoke the +serve+ function in the REPL. For a simple @@ -125,13 +125,15 @@ typically you'd like to be able to serve multiple incoming connections. Fortunately, this is relatively straightforward to do given the -concurrency tools that Clojure provides. Modifying the +serve+ function to work as a persistent server requires three changes: +concurrency tools that Clojure provides. Modifying the +serve+ function to +work as a persistent server requires three changes: - Run the server on a separate thread so it doesn't block the REPL. - Don't close the server socket after handling the first request. - After handling a request, loop back to immediately handle another. -Also, because the server will be running on a non-REPL thread, it would be good to provide a mechanism for terminating the server other +Also, because the server will be running on a non-REPL thread, it +would be good to provide a mechanism for terminating the server other than killing the whole JVM. The modified code looks like this: @@ -150,7 +152,8 @@ The modified code looks like this: running)) ---- -pass:[]The key feature of this code is that it launches the server socket +pass:[]The key feature of this code is that +it launches the server socket asynchronously inside a future and calls the +accept+ method inside of a loop. It also creates an atom called +running+ and returns it, checking it each time it loops. To stop the server, reset the atom to @@ -200,4 +203,4 @@ HTTP requests or JMS queues) actually work. - The http://bit.ly/clj-java-io-api[API documentation] for the +clojure.java.io+ namespace - <> -- Wikipedia on http://bit.ly/wiki-tcp[the TCP protocol] \ No newline at end of file +- Wikipedia on http://bit.ly/wiki-tcp[the TCP protocol] diff --git a/06_databases/6-01_connecting-to-an-SQL-database.asciidoc b/06_databases/6-01_connecting-to-an-SQL-database.asciidoc index 30c56de1..99fba92f 100644 --- a/06_databases/6-01_connecting-to-an-SQL-database.asciidoc +++ b/06_databases/6-01_connecting-to-an-SQL-database.asciidoc @@ -126,7 +126,7 @@ and plain strings. For example, a complete URI string may be provided under the ---- ;; As a spec string (def db-spec - "jdbc:postgresql://bilbo:secret@localhost:5432/cookbook_experiment") + "jdbc:postgresql://bilbo:secret@localhost:5432/cookbook_experiments") ;; As a connection URI map... ;; with a username and password... @@ -162,7 +162,7 @@ of everyday database access needs. ==== See Also * See <>, to learn about - pooling connections to an SQL database with +c3p0+ and + pooling connections to an SQL database with +BoneCP+ and +clojure.java.jdbc+. * See <>, to learn about using +clojure.java.jdbc+ to interact with an SQL database. diff --git a/06_databases/6-03_manipulating-an-SQL-database.asciidoc b/06_databases/6-03_manipulating-an-SQL-database.asciidoc index c0e0be87..44bb197b 100644 --- a/06_databases/6-03_manipulating-an-SQL-database.asciidoc +++ b/06_databases/6-03_manipulating-an-SQL-database.asciidoc @@ -104,7 +104,7 @@ with the result: ---- If you no longer need a particular table, invoke -+clojure.java.dbc/jdb-do-commands+ with the appropriate DDL statements ++clojure.java.jdbc/db-do-commands+ with the appropriate DDL statements generated by +java-jdbc.ddl/drop-table+: [source,clojure] @@ -231,7 +231,7 @@ If an exception is thrown, the transaction is rolled back: Transactions can be explicitly set to roll back with the +clojure.java.jdbc/db-set-rollback-only!+ function. This setting can be unset with the +clojure.java.jdbc/db-unset-rollback-only!+ -function and tested with the +clojure.java.jdbc/is-rollback-only+ +function and tested with the +clojure.java.jdbc/db-is-rollback-only+ function: [source,clojure] diff --git a/06_databases/6-04_korma.asciidoc b/06_databases/6-04_korma.asciidoc index e13cae38..b7d81d79 100644 --- a/06_databases/6-04_korma.asciidoc +++ b/06_databases/6-04_korma.asciidoc @@ -49,7 +49,8 @@ acceptable to +:refer :all+ its contents into model namespaces: [source,clojure] ---- -(require '[korma.db :refer :all]) +(require '[korma.db :refer :all] + '[korma.core :refer :all]) (defdb db (postgres {:db "learn_korma"})) @@ -88,7 +89,7 @@ must match the names of the columns in the database: [source,clojure] ---- (insert posts - (values nil {:title "First post" :content "blah blah blah"})) + (values {:title "First post" :content "blah blah blah"})) ---- To retrieve values from the database, query using +select+. Successful diff --git a/06_databases/6-05_text-search.asciidoc b/06_databases/6-05_text-search.asciidoc index 381e9f4b..0d01b751 100644 --- a/06_databases/6-05_text-search.asciidoc +++ b/06_databases/6-05_text-search.asciidoc @@ -93,7 +93,7 @@ generation: ;; -> org.apache.lucene.analysis.util.CharArraySet (def stop-words - (doto (CharArray. clucy.core/*version* 3 true) + (doto (CharArraySet. clucy.core/*version* 3 true) (.add "do") (.add "not") (.add "index"))) diff --git a/06_databases/6-06_indexing-with-elasticsearch.asciidoc b/06_databases/6-06_indexing-with-elasticsearch.asciidoc index 815a113b..037a2de7 100644 --- a/06_databases/6-06_indexing-with-elasticsearch.asciidoc +++ b/06_databases/6-06_indexing-with-elasticsearch.asciidoc @@ -69,7 +69,7 @@ To create an index, use the +clojurewerkz.elastisch.rest.index/create+ function: (esi/create "test1") ;; Create an index with custom settings -(esi/create "test2" :settings {"number_of_shards" 1})) +(esi/create "test2" :settings {"number_of_shards" 1}) ---- A full explanation of the available indexing settings is outside the @@ -101,7 +101,7 @@ an index is created using the +:mapping+ option: :term_vector "with_positions_offsets"}}}}) -(esi/create "test3" :mappings mapping-types))) +(esi/create "test3" :mappings mapping-types) ---- ===== Indexing documents @@ -130,7 +130,7 @@ cause a document ID to be generated automatically: (esi/create "test4" :mappings mapping-types) -(def doc {:username "happyjoe" +(def doc1 {:username "happyjoe" :first-name "Joe" :last-name "Smith" :age 30 @@ -139,8 +139,8 @@ cause a document ID to be generated automatically: :biography "N/A"}) -(esd/create "test4" "person" doc) -;; => {:ok true, :_index people, :_type person, +(esd/create "test4" "person" doc1) +;; => {:created true, :_index "test4", :_type "person", ;; :_id "2vr8sP-LTRWhSKOxyWOi_Q", :_version 1} ---- @@ -148,7 +148,7 @@ cause a document ID to be generated automatically: [source,clojure] ---- -(esr/put "test4" "person" "happyjoe" doc) +(esd/put "test4" "person" "happyjoe" doc1) ---- ==== Discussion diff --git a/06_databases/6-10_connect-to-datomic.asciidoc b/06_databases/6-10_connect-to-datomic.asciidoc index 4fba4838..6d564659 100644 --- a/06_databases/6-10_connect-to-datomic.asciidoc +++ b/06_databases/6-10_connect-to-datomic.asciidoc @@ -19,7 +19,7 @@ $ lein try com.datomic/datomic-free ---- To create and connect to an in-memory database, use -+database.api/create-database+ and +datomic.api/connect+: ++datomic.api/create-database+ and +datomic.api/connect+: [source,clojure] ---- diff --git a/06_databases/6-11_schema.asciidoc b/06_databases/6-11_schema.asciidoc index 01c2103e..421627cf 100644 --- a/06_databases/6-11_schema.asciidoc +++ b/06_databases/6-11_schema.asciidoc @@ -16,7 +16,7 @@ the two in some way.(((Datomic database, schema definition)))(((schema definitio Datomic schemas are defined in terms of _attributes_. It's probably easiest to jump straight to an example.(((attributes))) -To follow along with this recipe, complete the steps in the solution in <>. After doing this, you +To follow along with this recipe, first complete the recipes for <>. After doing this, you should have an in-memory database and connection, +conn+, to work with. Consider the attributes a user might have: @@ -32,13 +32,15 @@ name, and role, as well as insertions of the three static roles: [source,clojure] ---- +(require '[datomic.api :as d]) + (def user-schema [{:db/doc "User email address" :db/ident :user/email :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/unique :db.unique/identity - :db/id #db/id[:db.part/db] + :db/id (d/tempid :db.part/db) :db.install/_attribute :db.part/db} {:db/doc "User name" @@ -46,19 +48,19 @@ name, and role, as well as insertions of the three static roles: :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/index true - :db/id #db/id[:db.part/db] + :db/id (d/tempid :db.part/db) :db.install/_attribute :db.part/db} {:db/doc "User roles" :db/ident :user/roles :db/valueType :db.type/ref :db/cardinality :db.cardinality/many - :db/id #db/id[:db.part/db] + :db/id (d/tempid :db.part/db) :db.install/_attribute :db.part/db} - [:db/add #db/id[:db.part/user] :db/ident :user.roles/guest] - [:db/add #db/id[:db.part/user] :db/ident :user.roles/author] - [:db/add #db/id[:db.part/user] :db/ident :user.roles/editor]]) + [:db/add (d/tempid :db.part/user) :db/ident :user.roles/guest] + [:db/add (d/tempid :db.part/user) :db/ident :user.roles/author] + [:db/add (d/tempid :db.part/user) :db/ident :user.roles/editor]]) ---- We define a group as having: @@ -81,7 +83,7 @@ Define the group as follows: :db/valueType :db.type/uuid :db/cardinality :db.cardinality/one :db/unique :db.unique/value - :db/id #db/id[:db.part/db] + :db/id (d/tempid :db.part/db) :db.install/_attribute :db.part/db} {:db/doc "Group name" @@ -89,14 +91,14 @@ Define the group as follows: :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/index true - :db/id #db/id[:db.part/db] + :db/id (d/tempid :db.part/db) :db.install/_attribute :db.part/db} {:db/doc "Group users" :db/ident :group/users :db/valueType :db.type/ref :db/cardinality :db.cardinality/many - :db/id #db/id[:db.part/db] + :db/id (d/tempid :db.part/db) :db.install/_attribute :db.part/db}]) ---- @@ -105,8 +107,6 @@ connection: [source,clojure] ---- -(require '[datomic.api :as d]) - @(d/transact (d/connect "datomic:mem://sample-database") (concat user-schema group-schema)) ;; -> {:db-before datomic.db.Db@25b48c7b, @@ -157,10 +157,10 @@ can be related to--this means that entities can relate to themselves! You also use +:db/valueType :db.type/ref+ and lone +:db/ident+ values to model enumerations, such as the user roles that you defined. These -enumerations are not actually schemas; they are normal entities with a +enumerations are not actually attributes; they are normal entities with a single attribute, +:db/ident+. An entity's +:db/ident+ value serves as a shorthand for that entity; you may use this value in lieu of the -entity's +:db/id+ value in transactions and queries. +entity ID (the +:db/id+ value) in transactions and queries. Attributes with +:db/valueType :db.type/ref+ and +:db/unique+ values are implicitly indexed as though you had added +:db/index true+ to diff --git a/06_databases/6-12_transact-basics.asciidoc b/06_databases/6-12_transact-basics.asciidoc index d540de3e..e5c23874 100644 --- a/06_databases/6-12_transact-basics.asciidoc +++ b/06_databases/6-12_transact-basics.asciidoc @@ -11,8 +11,8 @@ You need to add data to your Datomic database.(((Datomic database, adding data t Use a Datomic connection to transact data. -To follow along with this recipe, complete the steps in the solutions to <>, and -<>. +To follow along with this recipe, first complete the recipes for +<>, and <>. After doing this, you will have a connection, +conn+, and a schema installed against which you can @@ -27,7 +27,7 @@ insert data: :user/name "Martin Fowler" :user/roles [:user.roles/author :user.roles/editor]}]) -@(d/transact conn tx-data) +(def tx-result @(d/transact conn tx-data)) (q '[:find ?name :where [?e :user/name ?name]] @@ -107,10 +107,10 @@ with all futures, when you dereference it, it will block until the transaction completes. Either way, dereferencing the future returns a map, with four keys: -+:db-before+:: ++:db-before+:: The value of the database just before the transaction was committed -+:db-after+:: ++:db-after+:: The value of the database just after the transaction was committed +:tx-data+:: diff --git a/06_databases/6-13_transact-retract-data.asciidoc b/06_databases/6-13_transact-retract-data.asciidoc index 1050303b..9489545b 100644 --- a/06_databases/6-13_transact-retract-data.asciidoc +++ b/06_databases/6-13_transact-retract-data.asciidoc @@ -12,22 +12,21 @@ You need to remove data from your Datomic database.(((Datomic database, removing To remove a value for an attribute, you should use the +:db/retract+ operation in transactions. -To follow along with this recipe, complete the steps in -the solutions to <>, and -<>. After doing this, you will have a -connection, +conn+, and a schema installed against which you can -insert data. +To follow along with this recipe, first complete the recipes for <>, and +<>. To start things off, add a user, Barney Rubble, and verify that he has an email address: [source,clojure] ---- +(require '[datomic.api :as d]) + (def new-id (d/tempid :db.part/user)) (def tx-result @(d/transact conn [{:db/id new-id :user/name "Barney Rubble" - :user/email "barney@example.com"}])) + :user/email "barney@rubble.me"}])) (def after-tx-db (:db-after tx-result)) @@ -51,7 +50,7 @@ To retract Barney's email, transact a transaction with the [source,clojure] ---- (def retract-tx-result @(d/transact conn [[:db/retract barney-id - :user/email "barney@example.com"]])) + :user/email "barney@rubble.me"]])) (def after-retract-db (:db-after retract-tx-result)) @@ -66,7 +65,7 @@ To retract entire entities, use the +:db.fn/retractEntity+ built-in transactor f [source,clojure] ---- -(def retract-entity-tx-result +(def retract-entity-tx-result @(d/transact conn [[:db.fn/retractEntity barney-id]])) (def after-retract-entity-db (:db-after retract-entity-tx-result)) @@ -115,4 +114,4 @@ data permanently. ==== See Also -* http://bit.ly/datomic-excision[The Datomic blog post] covering the excision feature \ No newline at end of file +* http://bit.ly/datomic-excision[The Datomic blog post] covering the excision feature diff --git a/06_databases/6-14_dry-run-transactions.asciidoc b/06_databases/6-14_dry-run-transactions.asciidoc index 5e9f8f29..75ae88e9 100644 --- a/06_databases/6-14_dry-run-transactions.asciidoc +++ b/06_databases/6-14_dry-run-transactions.asciidoc @@ -14,10 +14,8 @@ Build your transaction as usual, but instead of calling +d/transact+ or +d/transact-async+, use +d/with+ to produce an in-memory database that includes the changes your transaction provides. -To follow along with this recipe, complete the steps in the solutions to <>, and -<>. After doing this, you will have a -connection, +conn+, and a schema installed against which you can -insert data. +To follow along with this recipe, first complete the recipes for <>, and +<>. First, add some data to the database about Fred Flintstone. As of about 4000 BCE, Fred didn't have an email, but we at least know his @@ -80,7 +78,7 @@ that of Fred's email in the in-memory database: [db name] (-> (d/q '[:find ?email :in $ ?name - :where + :where [?entity :user/name ?name] [?entity :user/email ?email]] db diff --git a/06_databases/6-15_traversing-indices.asciidoc b/06_databases/6-15_traversing-indices.asciidoc index 1469c36e..9443210a 100644 --- a/06_databases/6-15_traversing-indices.asciidoc +++ b/06_databases/6-15_traversing-indices.asciidoc @@ -11,11 +11,8 @@ You want to execute simple Datomic queries with high performance.(((Datomic data Use the +datomic.api/datoms+ function to directly access the core Datomic indexes in your database. -To follow along with this recipe, complete the steps in -the solutions to <>, and -<>. After doing this, you will have a -connection, +conn+, and a schema installed against which you can -insert data. +To follow along with this recipe, first complete the recipes for <>, and +<>. For example, to quickly find the entities that have the provided attribute and value set, invoke +datomic.api/datoms+, specifying the +:avet+ index @@ -28,7 +25,7 @@ value: (d/transact conn [{:db/id (d/tempid :db.part/user) :user/name "Barney Rubble" - :user/email "barney@example.com"}]) + :user/email "barney@rubble.me"}]) (defn entities-with-attr-val "Return entities with a given attribute and value." @@ -40,7 +37,7 @@ value: (def barney (first (entities-with-attr-val (d/db conn) :user/email - "barney@example.com"))) + "barney@rubble.me"))) (:user/email barney) ;; -> "barney@example.com" @@ -144,7 +141,7 @@ other hand, will return only entities with that specific attribute and value pair. What is returned by +datoms+ is a stream of +Datum+ objects. Each -datum responds to +:a+, +:e+, +t+, +:v+, and +:added+ as functions.(((range="endofrange", startref="ix_DBdt"))) +datum responds to +:a+, +:e+, +:t+, +:v+, and +:added+ as functions.(((range="endofrange", startref="ix_DBdt"))) ==== See Also diff --git a/07_webapps/7-04_forms.asciidoc b/07_webapps/7-04_forms.asciidoc index bc0e54cd..fbbfcf94 100644 --- a/07_webapps/7-04_forms.asciidoc +++ b/07_webapps/7-04_forms.asciidoc @@ -42,7 +42,7 @@ To follow along with this recipe, clone the https://github.com/clojure-cookbook/ "Show a form requesting the user's name, or greet them if they submitted the form" [req] - (let [name (get-in req [:params "name"])] + (let [name (get-in req [:form-params "name"])] (if name (show-name name) (show-form)))) @@ -78,4 +78,4 @@ Note that the form keys are passed in as strings, not keywords. ==== See Also * <> -* Ring's http://bit.ly/ring-parameters[parameters documentation] \ No newline at end of file +* Ring's http://bit.ly/ring-parameters[parameters documentation] diff --git a/07_webapps/7-10_basic.asciidoc b/07_webapps/7-10_basic.asciidoc index 690f0e7c..82f99248 100644 --- a/07_webapps/7-10_basic.asciidoc +++ b/07_webapps/7-10_basic.asciidoc @@ -12,7 +12,7 @@ Compojure at a higher level of abstraction, by defining resources.(((web applica Use https://github.com/clojure-liberator/liberator[Liberator] to create HTTP-compliant, RESTful web apps. -To follow along with this recipe, create a new project using the command *+lein new liberator-test+*. +To follow along with this recipe, create a new project using the command *+lein new app liberator-test+*. Inside your _project.clj_, add the following dependencies to your +:dependencies+ key: diff --git a/07_webapps/7-11_enlive.asciidoc b/07_webapps/7-11_enlive.asciidoc index 7ef03dda..b4fdfad7 100644 --- a/07_webapps/7-11_enlive.asciidoc +++ b/07_webapps/7-11_enlive.asciidoc @@ -21,7 +21,7 @@ To follow along with this recipe, start a REPL using +lein-try+: [source,shell-session] ---- $ lein try enlive ----- +---- To begin, create a file _post.html_ to serve as an Enlive template: @@ -79,7 +79,7 @@ single string:

Why Clojure Rocks

By Luke VanderHart

-
Functional programming!
+
Functional programming!
---- @@ -205,7 +205,7 @@ would be parsed into the Clojure data: ({:tag :div, :attrs {:id "foo"}, :content - ({:tag :span, :attrs {:class "bar"}, :content ("Hello!")})})})} + ({:tag :span, :attrs {:id "bar"}, :content ("Hello!")})})})} ---- This is more verbose, but it is easier to manipulate from Clojure. You @@ -238,8 +238,9 @@ value given as its argument.(((transform function))) The return value is not itself a string, but a sequence of strings, each one a small fragment of HTML code. This allows the underlying data structure to be transformed to a string representation -lazily. For simplicity, our example just reduces the string -concatenation function +str+ across the results, but this is actually +lazily. For simplicity, our example uses the string +concatenation function +str+ to +reduce+ the +result of +all-posts-page+ , but this is actually not optimally performant. To build a string most efficiently, use the Java +StringBuilder+ class, which uses mutable state to build up a +String+ object with the best possible performance. Alternatively, @@ -300,8 +301,8 @@ braces). The range selector contains two other selectors and inclusively matches all the nodes between the two matched nodes, in document order. The starting node is in key position in the map literal and the ending node is in value position, so the selector -+{[:.foo] [:.bar]}+ will match all nodes between nodes with an ID of -"foo" and an ID of "bar". ++{[:#foo] [:#bar]}+ will match all nodes between nodes with a CSS ID of +"foo" and a CSS ID of "bar". The example in the solution uses a range selector in the +defsnippet+ form to select all the nodes that are part of the same logical blog @@ -366,7 +367,7 @@ recent +n+ comic titles in the XKCD archives: (comic-titles 5) ;; -> ("Oort Cloud" "Git Commit" "New Study" - "Telescope Names" "Job Interview") +;; "Telescope Names" "Job Interview") ---- ===== When to use Enlive diff --git a/07_webapps/7-12_templating-with-selmer.asciidoc b/07_webapps/7-12_templating-with-selmer.asciidoc index 75a0adc4..07e1a57a 100644 --- a/07_webapps/7-12_templating-with-selmer.asciidoc +++ b/07_webapps/7-12_templating-with-selmer.asciidoc @@ -51,7 +51,7 @@ The template can then be rendered by calling the +selmer.parser/render-file+ fun [source, clojure] ---- -(require '[selmer.parser :refer [render-file]]) +(require '[selmer.parser :refer [render-file render]]) (println (render-file "base.html" @@ -204,7 +204,7 @@ and display it on the page, we could write the following filter:footnote:[You'll [source, clojure] ---- (require '[markdown.core :refer [md-to-html-string]] - '[selmer.filters/add-filter!]) + '[selmer.filters :refer [add-filter!]]) (add-filter! :markdown md-to-html-string) ---- diff --git a/07_webapps/7-14_markdown.asciidoc b/07_webapps/7-14_markdown.asciidoc index bc1ace83..160d7a03 100644 --- a/07_webapps/7-14_markdown.asciidoc +++ b/07_webapps/7-14_markdown.asciidoc @@ -22,7 +22,8 @@ Use +markdown.core/md-to-html+ to read a Markdown document and generate a string [source,clojure] ---- -(require '[markdown.core :as md]) +(require '[markdown.core :as md] + '[clojure.java.io :refer [input-stream output-stream]) (md/md-to-html "input.md" "output.html") @@ -47,7 +48,7 @@ with Markdown content to its HTML representation: ==== Discussion Markdown is a popular lightweight markup language that's easy to read and write and -can be converted to structurally valid HTML.(((parsing, parser differences))) +can be converted to structurally valid HTML.(((parsing, parser differences))) Since Markdown leaves many aspects of rendering the HTML open to interpretation, it's not guaranteed that different parsers will produce the same representation. This can be a problem if you render a Markdown preview on the client using one @@ -74,13 +75,13 @@ will be decorated with a class compatible with the http://alexgorbatchev.com/Syn +markdown-clj+ supports all the standard Markdown tags, with the exception of reference-style links (because the parser uses a single pass to generate the document). The +markdown.core/md-to-html+ processes the input line by line, and the entirety of the content -does not need to be stored in memory when processing. On the other hand, both the +md-to-html-string+ and +md-to-html+ -functions load the entire contents into memory. +does not need to be stored in memory when processing. On the other hand, the +md-to-html-string+ +function keeps the entire input and output in memory. The parser accepts additional formatting options. These include +:heading-anchors+, +:code-style+, +:custom-transformers+, and +:replacement-transformers+. -When the +:heading-anchors+ keyis set to +true+, an anchor will be generated for each heading tag: +When the +:heading-anchors+ key is set to +true+, an anchor will be generated for each heading tag: [source,clojure] ---- @@ -90,7 +91,7 @@ When the +:heading-anchors+ keyis set to +true+, an anchor will be generated for [source,html] ----

- + foo bar BAz

---- @@ -133,7 +134,7 @@ Finally, we can provide a custom set of transformers to replace the built-in one [source,clojure] ---- -(markdown/md-to-html-string "#foo" :replacement-transformers [capitalize]) +(md/md-to-html-string "#foo" :replacement-transformers [capitalize]) ---- ==== See Also diff --git a/08_deployment-and-distribution/8-06_primitive-arrays.asciidoc b/08_deployment-and-distribution/8-06_primitive-arrays.asciidoc index 9b03bc1e..505233b7 100644 --- a/08_deployment-and-distribution/8-06_primitive-arrays.asciidoc +++ b/08_deployment-and-distribution/8-06_primitive-arrays.asciidoc @@ -34,8 +34,8 @@ arrays. +amap+ uses a parallel binding syntax similar to +doseq+: (defn map-sqrt [xs] (hiphip.double/amap [x xs] (Math/sqrt x))) -(seq (map-sqrt (double-array (range 1000)))) -;; -> (2.0 3.0 4.0) +(seq (map-sqrt (double-array (range 5)))) +;; -> (0.0 1.0 1.4142135623730951 1.7320508075688772 2.0) (defn pointwise-product "Produce a new double array with the product of corresponding elements of @@ -95,7 +95,7 @@ ordinary Clojure, and this is generally what you should try first: [source,clojure] ---- (defn dot-product [ws xs] - (reduce + (map * ws xs)) + (reduce + (map * ws xs))) ---- Once you identify a bottleneck in your mathematical operations, @@ -106,10 +106,10 @@ using +asum+, primarily because +map+ produces sequences of an intermediate sequence, all arithmetic operations on boxed numbers are significantly slower than on their primitive counterparts. -++hiphip++'s +amap+, +afill!+, +reduce+, and +asum+ macros (among others) +++hiphip++'s +amap+, +afill!+, +areduce+, and +asum+ macros (among others) are available for +int+, +long+, +float+, and +double+ types. If you wanted to use +reduce+ over an array of floats, for example, you would -use +hiphip.float/reduce+. These macros define the appropriate +use +hiphip.float/areduce+. These macros define the appropriate type hints and optimizations per type. Clojure also comes with diff --git a/09_distributed-computation/9-00_introduction.asciidoc b/09_distributed-computation/9-00_introduction.asciidoc index a1339df2..56de43e1 100644 --- a/09_distributed-computation/9-00_introduction.asciidoc +++ b/09_distributed-computation/9-00_introduction.asciidoc @@ -14,7 +14,7 @@ we'll be covering http://cascalog.org/[Cascalog], a data-processing library built on top of http://hadoop.apache.org/[Hadoop], which is an open source MapReduce implementation.((("Amazon’s Elastic MapReduce (EMR)", see="Elastic MapReduce (EMR)")))((("MapReduce", see="Elastic MapReduce (EMR)")))((("cloud computing", see="distributed computation")))((("Elastic MapReduce (EMR)", "basics of")))(((distributed computation, Elastic MapReduce))) -We'll also briefly cover http://storm-project.net/[Storm], a +We'll also briefly cover http://storm.apache.org/[Storm], a real-time stream-processing library in use at several tech giants such as Twitter, Groupon, and Yahoo!. diff --git a/09_distributed-computation/9-01_stream-processing.asciidoc b/09_distributed-computation/9-01_stream-processing.asciidoc index 454ffaf7..b0fca70e 100644 --- a/09_distributed-computation/9-01_stream-processing.asciidoc +++ b/09_distributed-computation/9-01_stream-processing.asciidoc @@ -22,13 +22,13 @@ they should provide high-level abstractions that help you organize and grow the complexity of your stream-processing logic to accommodate new features and a complex world.(((userbases)))(((realtime computation systems)))(((activity stream processing))) -Clojure offers just such a tool in http://storm-project.net/[Storm], a +Clojure offers just such a tool in http://storm.apache.org/[Storm], a distributed real-time computation system that aims to be for real-time computation what Hadoop is for batch computation. In this section, you'll build a simple activity stream processing system that can be easily extended to solve real-world problems.(((Storm, project creation/setup))) -First, create a new http://storm.incubator.apache.org/[Storm project] using its Leiningen template: +First, create a new http://storm.apache.org/[Storm project] using its Leiningen template: [source,shell-session] ---- @@ -79,15 +79,15 @@ data rather than a random data generator): (emit-spout! collector [(rand-nth events)]))))) ---- -Next, open _src/feeds/bolts/clj_. Add a bolt that accepts a user and +Next, open _src/feeds/bolts.clj_. Add a bolt that accepts a user and an event and produces a tuple of +(user, event)+ for each user in the system. A bolt consumes a stream, does some processing, and emits a new stream: [source,clojure] ---- -(defbolt active-user-bolt ["user" "event"] [{event "event" :as tuple} collector] - (doseq [user [:jim :rob :karen :kaitlyn :emma :travis]] +(defbolt active-user-bolt ["user" "event"] [{event "event" :as tuple} collector] + (doseq [user [:jim :rob :karen :kaitlyn :emma :travis]] (emit-bolt! collector [user event])) (ack! collector tuple)) ---- @@ -544,7 +544,7 @@ code and happy programmers. ==== See Also -* http://storm-project.net/[Storm's website] +* http://storm.apache.org/[Storm's website] * The Storm http://bit.ly/storm-template[project template] * https://github.com/nathanmarz/storm-deploy[+storm-deploy+], a tool for easy Storm deployment * https://github.com/utahstreetlabs/risingtide[Rising Tide], the feed diff --git a/10_testing/10-06_tracing.asciidoc b/10_testing/10-06_tracing.asciidoc index 2fefeaaa..16b7979b 100644 --- a/10_testing/10-06_tracing.asciidoc +++ b/10_testing/10-06_tracing.asciidoc @@ -15,8 +15,8 @@ code as it runs. Before starting, add `[org.clojure/tools.trace "0.7.6"]` to your project's dependencies under the +:development+ profile (in the vector -at the `[:profiles :dev :dependencies]` path instead of the -`[:dependencies]` path). Alternatively, start a REPL using +lein-try+: +at the `:profiles {:dev {:dependencies [...]}}` path instead of the +`:dependencies [...]` path). Alternatively, start a REPL using +lein-try+: [source,shell-session] ---- @@ -142,10 +142,11 @@ functions and vars in a namespace. Even things defined _after_ [source,clojure] ---- (def my-inc inc) -(defn my-dec [n] (dec n)) (t/trace-ns 'user) +(defn my-dec [n] (dec n)) + (my-inc (my-dec 0)) ;; -> 0 ;; TRACE t1217: (user/my-dec 0) @@ -170,4 +171,4 @@ functions and vars in a namespace. Even things defined _after_ ++++ -++++ \ No newline at end of file +++++ diff --git a/10_testing/10-08_verify-java-interop.asciidoc b/10_testing/10-08_verify-java-interop.asciidoc index 201ad7fa..b554616c 100644 --- a/10_testing/10-08_verify-java-interop.asciidoc +++ b/10_testing/10-08_verify-java-interop.asciidoc @@ -79,7 +79,7 @@ Add a type hint to call the http://bit.ly/javadoc-file-constructor[`public File( (File. s)) ---- -Checking again, +core.type+ is satisfied: +Checking again, +core.typed+ is satisfied: [source,shell-session] ---- @@ -215,4 +215,4 @@ desirable in larger code bases. * https://github.com/clojure/core.typed[+core.typed+ Home] on GitHub * The http://bit.ly/core-typed-doc[+core.typed+ API reference]—particularly the documentation for +non-nil-return+ and +nilable-param+ * <>, and <>, for further - examples of how to use +core.typed+ \ No newline at end of file + examples of how to use +core.typed+ diff --git a/10_testing/10-09_hof-typed.asciidoc b/10_testing/10-09_hof-typed.asciidoc index 31d3377c..586dcea3 100644 --- a/10_testing/10-09_hof-typed.asciidoc +++ b/10_testing/10-09_hof-typed.asciidoc @@ -45,7 +45,7 @@ function with type annotations attached: (fn> [m :- Any] (when (map? m) (and (every? ks? (keys m)) - (every? ks? (vals m)))))) + (every? vs? (vals m)))))) ---- Each argument to +hash-of?+ has type `[Any -> Any]`: a single argument @@ -98,7 +98,7 @@ at runtime. Why not type-check our higher-order functions as well? ++core.typed++'s type-checking abilities aren't limited to only data types--it can also type-check functions as types themselves.(((anonymous functions)))(((functions, anonymous))) -By writing returning anonymous functions created with the +By returning anonymous functions created with the +clojure.core.typed/fn>+ form instead of +fn+, it is possible to annotate function objects with ++core.typed++'s rich type-checking system. When defining functions with +fn>+, annotate types to its arguments diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87859cc7..d4a6c5c0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,7 @@ # Contributing to Clojure Cookbook ---- - -**NOTE: As of 2014-01-10 we are *not* soliciting any new recipe submissions *or corrections*. We are now working with O'Reilly to prepare the book for final publication. (This includes copyediting, quality assurance, indexing, etc.)** - -We will begin accepting whole recipes again once the book is published and we can begin working on the online resource complimentary to the print book. - ---- - Our goal with *Clojure Cookbook* is to build the best possible cookbook, chock full of excellent Clojure recipes. To do this we need the help of the whole community. From typos and factual corrections, to ideas for recipes, to whole recipes themselves, your experience is what we need to build the best *Clojure Cookbook*. -This document describes how you can contribute and what you'll receive if you do. - ## Valuable Contributions Nearly any knowledge or bit of information you could contribute to the cookbook is valuable, but here are some concrete ideas: @@ -23,20 +13,6 @@ Nearly any knowledge or bit of information you could contribute to the cookbook *Major contributions such as a whole recipe require you to [license](#licensing) your submission. However, you can submit pull requests for typos, factual corrections and ideas without one* -## Credits & Rewards - -Any contribution, big or small, will net you a digital copy of the book and your name and GitHub handle immortalized in a contributors list in the book (think of the glory it will bring your family). - -On a legal note, we're obligated to let you know that the above mentioned rewards will be a contributors sole consideration for their contributions. (That, of course, and our ever-lasting love :wink:.) - -### Write a recipe - -Contributors who write a full (or substantial portion) of a recipe will be listed as an author of that recipe. - -### Write 5+ recipes - -Make a substantial contribution to the book (five or more recipes) and we will send you a signed physical copy of the book once it hits the presses. - ## Recipes One of the most helpful contributions you could make to the cookbook is a complete recipe (anywhere from 1-10 pages, usually; the length of a typical blog post.) This section provides an overview of what a recipe looks like, where to draw ideas for recipes from and how to properly license your content so that it may be published in the final book. @@ -142,5 +118,3 @@ If you have any questions or concerns about the licensing model, please don't he ### Ideas You'll find a list of ideas under the [ideas tag](../../issues?labels=idea&milestone=&page=1&state=open) of our GitHub Issues page. Feel free to submit your own ideas with a title like: `Idea: recipe covering http-kit` or `Idea: Creating a leiningen plugin`. - -[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/clojure-cookbook/clojure-cookbook/trend.png)](https://bitdeli.com/free "Bitdeli Badge") diff --git a/README.md b/README.md index 4ac6b55f..5b799b6e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,47 @@ *Clojure Cookbook* doesn't just teach you Clojure, it also shows you how to use the language and many of its common libraries. The most difficult part of mastering any language is knowing how to apply it, in an idiomatic way, to tasks that real software developers encounter every day. This is especially true of Clojure. -With code recipes that teach you how to use the language in a variety of domains, *Clojure Cookbook* goes beyond simply teaching Clojure syntax and semantics. It contains annotated example code with detailed analysis and explanation for hundreds of real programming tasks. You can read the book straight through to gain insights about Clojure, or use it as a reference to solve particular problems +With code recipes that teach you how to use the language in a variety of domains, *Clojure Cookbook* goes beyond simply teaching Clojure syntax and semantics. It contains annotated example code with detailed analysis and explanation for hundreds of real programming tasks. You can read the book straight through to gain insights about Clojure, or use it as a reference to solve particular problems. + +## Exploring the Book + +If you're an Emacs-wielding Clojurist, you will probably want to read this book in Emacs, too. Here is a function that "turns the page" from one recipe to the next (find and open the next recipe, close the buffer with the previous recipe). + +```elisp +(defun increment-clojure-cookbook () + "When reading the Clojure cookbook, find the next section, and +close the buffer. If the next section is a sub-directory or in +the next chapter, open Dired so you can find it manually." + (interactive) + (let* ((cur (buffer-name)) + (split-cur (split-string cur "[-_]")) + (chap (car split-cur)) + (rec (car (cdr split-cur))) + (rec-num (string-to-number rec)) + (next-rec-num (1+ rec-num)) + (next-rec-s (number-to-string next-rec-num)) + (next-rec (if (< next-rec-num 10) + (concat "0" next-rec-s) + next-rec-s)) + (target (file-name-completion (concat chap "-" next-rec) ""))) + (progn + (if (equal target nil) + (dired (file-name-directory (buffer-file-name))) + (find-file target)) + (kill-buffer cur)))) +``` + +If you wish, you can then bind the function to a key: + +```elisp +(define-key adoc-mode-map (kbd "M-+") 'increment-clojure-cookbook) +``` + +Of course, this binding assumes you're using adoc-mode for reading .asciidoc files. We suggest CIDER for evaluating code interactively. Adding the following hook to your config will enable cider-mode every time you enter an AsciiDoc file. Once active, you can start a REPL and evaluate code like you would do in any regular Clojure file. + +```elisp +(add-hook 'adoc-mode-hook 'cider-mode) +``` ## Contributing @@ -21,19 +61,13 @@ installed and properly configured.) You must have the `asciidoc` and `source-highlight` command-line utilities installed and configured before attempting to build the book. -To install and configure the tools on OS X, -run the included [`bootstrap_osx.sh`](script/asciidoc/bootstrap_osx.sh) script: +To install and configure the tools on OS X or Linux, +run the included [`bootstrap.sh`](script/asciidoc/bootstrap.sh) script: ```sh -$ ./script/asciidoc/bootstrap_osx.sh +$ ./script/asciidoc/bootstrap.sh ``` -Linux users will need to follow a similar process to -[`bootstrap_osx.sh`](script/asciidoc/bootstrap_osx.sh), but we have not -automated it yet. The most important part after installing `asciidoc` and -`source-highlight` is to obtain and configure the proper bindings for Clojure -(and other) syntax highlighting. - ### Rendering With installation and configuration complete, all that is left is to run the `asciidoc` command. diff --git a/images/clcb_0402.png b/images/clcb_0402.png index 1165abd7..2e876335 100644 Binary files a/images/clcb_0402.png and b/images/clcb_0402.png differ diff --git a/images/clcb_0403.png b/images/clcb_0403.png index c22ab613..0f5ea2a8 100644 Binary files a/images/clcb_0403.png and b/images/clcb_0403.png differ diff --git a/script/asciidoc/bootstrap.sh b/script/asciidoc/bootstrap.sh index ac378c58..c6c1865e 100755 --- a/script/asciidoc/bootstrap.sh +++ b/script/asciidoc/bootstrap.sh @@ -32,7 +32,7 @@ elif command -v apt-get >/dev/null 2>&1; then elif command -v yum >/dev/null 2>&1; then PACKAGE_INSTALL="sudo yum install" elif command -v pacman >/dev/null 2>&1; then - PACKAGE_INSTALL="sudo pacman install" + PACKAGE_INSTALL="sudo pacman --noconfirm -S" else echo "Unable to determine local package manager. Provide one via \$PACKAGE_INSTALL like \"sudo install\"" fi @@ -58,6 +58,8 @@ elif [[ -d "/usr/local/share/source-highlight" ]] ; then SOURCE_HIGHLIGHT_DIR="/usr/local/share/source-highlight" elif [[ -d "/usr/share/source-highlight" ]] ; then SOURCE_HIGHLIGHT_DIR="/usr/share/source-highlight" +elif [[ -d "/opt/local/share/source-highlight" ]] ; then + SOURCE_HIGHLIGHT_DIR="/opt/local/share/source-highlight" else echo "source-highlight directory not found. Provide one via \$SOURCE_HIGHLIGHT_DIR" exit @@ -69,23 +71,23 @@ JSON_STYLE_FILE="${SOURCE_HIGHLIGHT_DIR}/json.style" TEXT_LANG_FILE="${SOURCE_HIGHLIGHT_DIR}/text.lang" CLOJURE_LANG_SOURCE="https://gist.github.com/alandipert/265810/raw/8dec04317e02187b03e96bb1e0206e154e0ae5e2/clojure.lang" -JSON_LANG_SOURCE="https://raw.github.com/freeformsystems/rlx/master/highlight/json.lang" -JSON_STYLE_SOURCE="https://raw.github.com/freeformsystems/rlx/master/highlight/json.style" +JSON_LANG_SOURCE="https://raw.githubusercontent.com/freeformsystems/rlx/13f9b3f728d9a64299e118fc8a2ecfa29b60d42e/lib/highlight/json.lang" +JSON_STYLE_SOURCE="https://raw.githubusercontent.com/freeformsystems/rlx/13f9b3f728d9a64299e118fc8a2ecfa29b60d42e/lib/highlight/json.style" # uncomment the following to debug/start fresh # does not clean LANG_MAP_FILE ## rm $CLOJURE_LANG_FILE $JSON_LANG_FILE $JSON_STYLE_FILE $TEXT_LANG_FILE # Clojure highlighting support -test -f "$CLOJURE_LANG_FILE" || (echo "** Adding $CLOJURE_LANG_FILE **" && $SUDO curl --location --silent --output "$CLOJURE_LANG_FILE" "$CLOJURE_LANG_SOURCE") +test -f "$CLOJURE_LANG_FILE" || (echo "** Adding $CLOJURE_LANG_FILE **" && sudo curl --location --silent --output "$CLOJURE_LANG_FILE" "$CLOJURE_LANG_SOURCE") # JSON highlighting support -test -f "$JSON_LANG_FILE" || (echo "** Adding $JSON_LANG_FILE **" && $SUDO curl --location --silent --output "$JSON_LANG_FILE" "$JSON_LANG_SOURCE") -test -f "$JSON_STYLE_FILE" || (echo "** Adding $JSON_STYLE_FILE **" && $SUDO curl --location --silent --output "$JSON_STYLE_FILE" "$JSON_STYLE_SOURCE") +test -f "$JSON_LANG_FILE" || (echo "** Adding $JSON_LANG_FILE **" && sudo curl --location --silent --output "$JSON_LANG_FILE" "$JSON_LANG_SOURCE") +test -f "$JSON_STYLE_FILE" || (echo "** Adding $JSON_STYLE_FILE **" && sudo curl --location --silent --output "$JSON_STYLE_FILE" "$JSON_STYLE_SOURCE") # CSV/text/text highlighting # This is to avoid errors more than actually highlight anything. -test -f "$TEXT_LANG_FILE" || (echo "** Adding $TEXT_LANG_FILE **" && cat </dev/null) +test -f "$TEXT_LANG_FILE" || (echo "** Adding $TEXT_LANG_FILE **" && cat </dev/null) include "number.lang" include "symbols.lang" cbracket = "{|}" @@ -99,10 +101,10 @@ LANG_MAP_FILE="$SOURCE_HIGHLIGHT_DIR/lang.map" grep clojure.lang "$LANG_MAP_FILE" >/dev/null && grep shell-session "$LANG_MAP_FILE" >/dev/null ) || { echo "** Backing up original $LANG_MAP_FILE to ${LANG_MAP_FILE}.bak" - $SUDO cp "$LANG_MAP_FILE" "${LANG_MAP_FILE}.bak" + sudo cp "$LANG_MAP_FILE" "${LANG_MAP_FILE}.bak" echo "** Appending to $LANG_MAP_FILE" - cat </dev/null + cat </dev/null clojure = clojure.lang csv = text.lang @@ -113,7 +115,7 @@ END echo "Cleaning $LANG_MAP_FILE" cleaned_contents="`sort "$LANG_MAP_FILE" | uniq`" - echo "$cleaned_contents" | $SUDO tee "$LANG_MAP_FILE" >/dev/null + echo "$cleaned_contents" | sudo tee "$LANG_MAP_FILE" >/dev/null } echo "Setup Complete"