├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── deps.edn ├── doc └── intro.md ├── resources └── sensor_data │ ├── atmospheric_detector.json │ ├── consolidated_data.json │ ├── moon_detector.json │ └── planet_detector.json └── src └── clojure_by_example ├── data └── planets.clj ├── ex00_introduction.clj ├── ex01_fundamentally_functional.clj ├── ex02_domain_as_data.clj ├── ex03_data_and_functions.clj ├── ex04_api_design.clj ├── ex05_immutability_and_fp.clj ├── ex06_full_functional_firepower.clj ├── ex07_boldly_go.clj ├── ex08_but_before_we_go.clj ├── fun ├── inspect_nasa_planets.clj └── workshop_fmt.clj └── workshop ├── .gitignore └── README.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .setup.sh 13 | profiles.clj 14 | .idea 15 | .m2-for-inclojure 16 | *.iml 17 | .lsp 18 | .cpcache 19 | .clj-kondo 20 | .calva 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] - 2018-01-12 5 | ### Changed 6 | - The initial cut of this project, for use in the guided Clojure 7 | workshop at IN/Clojure 2018, Bangalore (http://inclojure.org/). 8 | 9 | ## [Unreleased] - 2018-01-18 10 | - Licensed under MIT, and assigned copyright to IN/Clojure for 11 | responsible stewardship, and in preparation for wider usage. 12 | 13 | ### Removed 14 | - A couple of NASA fact sheets that were committed while we were 15 | toying around with things. Removed due to unknown copyright status. 16 | 17 | ### Fixed 18 | - Fixed documentation, examples etc. based on workshop experience. 19 | 20 | ## [Unreleased] - 2018-01-19 21 | ### Fixed 22 | - Expanded setup in Readme to help more with editors and configurations. 23 | - Fixed project.clj to reflect the updated license 24 | 25 | [Unreleased]: https://github.com/inclojure-org/clojure-by-example/commit/035aa1e7f9c92ae04f639862e71b7148ac58864f 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | Retrieved from https://opensource.org/licenses/MIT 3 | Retrieved on Thursday, 18 January 2018, 1800hrs IST. 4 | 5 | Copyright (c) 2017-2018 IN/Clojure 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | - [Introduction](#introduction) 2 | - [Intended usage](#intended-usage) 3 | - [Contributions](#contributions) 4 | - [Workshop Goals](#workshop-goals) 5 | - [Workshop Anti-Goals](#workshop-anti-goals) 6 | - [Suggested learning mindset](#suggested-learning-mindset) 7 | - [Setup Instructions](#setup-instructions) 8 | - [Course Design Philosophy](#course-design-philosophy) 9 | - [Credits](#credits) 10 | - [Copyright and License](#copyright-and-license) 11 | 12 | # Introduction 13 | 14 | This workshop aims to get your brain and fingers accustomed to just enough of 15 | the [Clojure](https://clojure.org) programming language to start doing useful things with it. 16 | 17 | In other words, "What could one do with just a _little_ bit of Clojure?". 18 | 19 | ## What is Clojure? 20 | 21 | Clojure is an interactive functional programming language that can run on many platforms 22 | like the [JVM](https://clojure.org/about/jvm_hosted), [.NET CLR](https://clojure.org/about/clojureclr), [Javascript](https://clojurescript.org/) (browsers, nodeJS, React Native), as [native binaries](https://github.com/BrunoBonacci/graalvm-clojure) via Graalvm, and even as [shell scripts](https://babashka.org/)! 23 | 24 | It is [used by software teams worldwide](https://clojure.org/community/success_stories#) to deliver 25 | high-value software systems at giant companies like Apple, Walmart, to "decacorns" 26 | like GoJek, Nubank, to a wide array of startups, and one-person businesses like Partsbox.com. 27 | 28 | Its interactivity and dynamism foster a sense of playfulness that attracts all manner 29 | of [creative makers](http://radar.oreilly.com/2015/05/creative-computing-with-clojure.html)---hobbyist as well as serious artists and musicians. 30 | 31 | A small but vibrant [global community](https://clojure.org/community/user_groups) is [busy building amazing things](https://github.com/trending/clojure?since=monthly) with the language. 32 | 33 | ## Intended usage 34 | 35 | - Support a 1-day guided workshop for programmers new to Clojure (not absolute programming beginners). 36 | - Also function as at-home learning material for said programmers. 37 | - The `master` branch is heavily commented, for at-home use. 38 | - A `solutions` branch will be available, as a companion to `master`. 39 | But don't peek at it in advance! 40 | - You may see a `workshop-code` branch. Ignore it. It is meant only for 41 | workshop use, and is subject to deletion/re-creation. 42 | - Incidentally, if you landed here while searching for Hirokuni Kim's 43 | "[Clojure By Example](https://kimh.github.io/clojure-by-example/)", well, follow the link! 44 | 45 | ## Contributions 46 | 47 | - If you find bugs or errors, please send a PR (but please 48 | don't change the course structure or pedagogy). 49 | 50 | ## Workshop Goals 51 | 52 | - Acquire a "feel" of Clojure, for further self-study/exploration. 53 | - Learn how Clojurists usually think with Clojure to solve problems. 54 | - See how it's not so hard to do surprisingly powerful things with a 55 | mere handful of "primitive" functions, data structures, and ideas. 56 | - Get you started with a good development setup and workflow that will 57 | serve you well if (when) you continue to program with Clojure, as a 58 | hobby, or at work! 59 | 60 | ## Workshop Anti-Goals 61 | 62 | - Try to explain Functional Programming theory or Clojure's innards. 63 | (Many free and paid tutorials and books do so very well.) 64 | - Try to fully cover Clojure primitives/features. (That's homework!) 65 | - Devolve into language wars, editor wars, syntax wars, type wars... 66 | (Life's too short, people.) 67 | - Focus too much on tooling or operational things. (At least not 68 | while there's fun to be had!) 69 | 70 | # Suggested learning mindset 71 | 72 | - Think of this as an exercise in "constrained creativity". 73 | - Ignore details, achieve much with as little know-how as possible. 74 | - Focus on what things do; not what they are, or why they are. 75 | - Inform your _intuition for doing things_, and then use that to 76 | dive deeper into all the juicy details at your own pace, later. 77 | 78 | Take what is useful, discard the rest. 79 | 80 | # Setup Instructions 81 | 82 | Just do the following one by one, and you should be fine. 83 | 84 | ## Java 85 | 86 | You need Java installed. 87 | 88 | - Run `java -version` in your terminal. 89 | - If Java is not installed, please [download and install Java from here](https://adoptopenjdk.net/). 90 | - Any version should do, but prefer Java 8 or higher. We have not tested 91 | this project with Java 7. 92 | - Once you are done, `java -version` should show you a Java version. 93 | 94 | ## VSCode + Calva 95 | 96 | We support VSCode + Calva IDE in the classroom for this workshop. We suggest you use this setup, unless of course, you have already configured your favourite editor for Clojure development. We've listed alternate starter kits below (IntelliJ, Vim, Emacs, Atom), _but_ please avoid [bikeshedding](http://catb.org/jargon/html/B/bikeshedding.html) editors. Just complete the workshop first! 97 | 98 | - Download and Install [VSCode](https://code.visualstudio.com/). 99 | - Open VSCode and complete the initialization process. 100 | - Open the "Extensions" Tab and search for "Calva", Install the "Calva: 101 | Clojure & ClojureScript Interactive Programming" extension. 102 | - Alternatively you can visit the [Calva page](https://marketplace.visualstudio.com/items?itemName=betterthantomorrow.calva) to install it. 103 | 104 | Once installed: 105 | 106 | - Clone the repository on your machine. 107 | - In VSCode Use File > Open Folder... and open the cloned folder. 108 | - Notice that Calva activates. 109 | - Open the [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) in VSCode using `⇧⌘P` on Mac or `Ctrl+Shift+P` on other systems. 110 | - Type "Calva: Start Project REPL" and choose "Calva: Start a Project REPL and Connect (aka Jack-In)" from the list. 111 | - Select `deps.edn` when prompted for Project type. We are using [tools.deps](https://clojure.org/guides/deps_and_cli) for managing the project. You don't need to worry about it's details for this workshop. 112 | - VSCode will create a new pane called 'output.calva-repl' and you will see `clj꞉user꞉>` prompt in that screen. 113 | - You have a working REPL now! 114 | - Keep the [Paredit guide](https://calva.io/paredit/) handy, editing code will require some understanding of paredit. 115 | 116 | ## Your favourite editor: 117 | 118 | You may find instructions for your favourite editor at one of these pages. But there are only so many choices. Ultimately, you must pick your poison and run with it: 119 | 120 | - ["Clojure Tools" at clojure.org](https://clojure.org/community/tools) 121 | - ["Essentials" at clojure-doc.org](http://clojure-doc.org/articles/content.html#essentials) 122 | - [Christopher Bui says...](https://web.archive.org/web/20181223213500/https://cb.codes/what-editor-ide-to-use-for-clojure/) 123 | 124 | # Course Design Philosophy 125 | 126 | Just some peoples' opinion. You need not be slave to it ;-) 127 | 128 | Almost anyone can hope to do more with more. Up to a point, that is. 129 | 130 | Far too often, we end up doing _less_ with more; bogged down by the 131 | complexity and blinding glitter of too much choice, and overabundance. 132 | 133 | Figuring out how to do more with less feeds our curiosity, and it often 134 | satisfies and empowers us deeply. 135 | 136 | So, may you stay small and achieve important things. 137 | 138 | Live long, and prosper. 139 | \\\\//\_ 140 | 141 | # Credits 142 | 143 | - [clj-pune](https://github.com/clj-pune) people, especially [kapilreddy](https://github.com/kapilreddy), and [jaju](https://github.com/jaju) for critique while making ["pratham"](https://github.com/clj-pune/pratham), the precursor to this project. 144 | - [adityaathalye](https://github.com/adityaathalye), [jysandy](https://github.com/jysandy), and [kapilreddy](https://github.com/kapilreddy) for course design, code reviews, critique, commits, and being the core teaching staff at the first edition of this workshop at IN/Clojure 2018. 145 | - All the workshop participants, and the many Clojurists who generously donated their time to make it successful. 146 | - [inclojure-org](https://github.com/inclojure-org) for being the umbrella under which this work happened. 147 | 148 | ## Copyright and License 149 | 150 | Copyright © 2017-2024 [IN/Clojure](http://inclojure.org/). 151 | 152 | Distributed under the [MIT license](https://github.com/inclojure-org/clojure-by-example/blob/master/LICENSE). 153 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"], 2 | :deps {org.clojure/clojure {:mvn/version "1.10.0"} 3 | org.clojure/data.json {:mvn/version "0.2.6"} 4 | enlive/enlive {:mvn/version "1.1.6"} 5 | rewrite-clj/rewrite-clj {:mvn/version "0.6.1"}}} 6 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to Clojure By Example 2 | 3 | All that needed to be said has been said in the README.md file. 4 | -------------------------------------------------------------------------------- /resources/sensor_data/atmospheric_detector.json: -------------------------------------------------------------------------------- 1 | {"Mercury":{},"Venus":{"carbon-dioxide":96.45,"nitrogen":3.45,"sulphur-dioxide":0.015,"traces":0.095},"Earth":{"nitrogen":78.08,"oxygen":20.95,"carbon-dioxide":0.4,"water-vapour":0.1,"argon":0.33,"traces":0.14},"Mars":{"carbon-dioxide":95.97,"argon":1.93,"nitrogen":1.89,"oxygen":0.146,"carbon-monoxide":0.056,"traces":0.008},"Chlorine Planet":{"chlorine":100.0},"Insane Planet":{"sulphur-dioxide":80.0,"carbon-monoxide":10.0,"chlorine":5.0,"nitrogen":5.0}} -------------------------------------------------------------------------------- /resources/sensor_data/consolidated_data.json: -------------------------------------------------------------------------------- 1 | [{"mass":0.055,"radius":0.383,"moons":0,"atmosphere":{},"name":"Mercury"},{"mass":0.815,"radius":0.949,"moons":0,"atmosphere":{"carbon-dioxide":96.45,"nitrogen":3.45,"sulphur-dioxide":0.015,"traces":0.095},"name":"Venus"},{"mass":1,"radius":1,"moons":1,"atmosphere":{"nitrogen":78.08,"oxygen":20.95,"carbon-dioxide":0.4,"water-vapour":0.1,"argon":0.33,"traces":0.14},"name":"Earth"},{"mass":0.107,"radius":0.532,"moons":2,"atmosphere":{"carbon-dioxide":95.97,"argon":1.93,"nitrogen":1.89,"oxygen":0.146,"carbon-monoxide":0.056,"traces":0.008},"name":"Mars"},{"mass":2.5,"radius":1.3,"moons":4,"atmosphere":{"chlorine":100.0},"name":"Chlorine Planet"},{"mass":4.2,"radius":1.42,"moons":42,"atmosphere":{"sulphur-dioxide":80.0,"carbon-monoxide":10.0,"chlorine":5.0,"nitrogen":5.0},"name":"Insane Planet"}] -------------------------------------------------------------------------------- /resources/sensor_data/moon_detector.json: -------------------------------------------------------------------------------- 1 | {"Mercury":0,"Venus":0,"Earth":1,"Mars":2,"Chlorine Planet":4,"Insane Planet":42} -------------------------------------------------------------------------------- /resources/sensor_data/planet_detector.json: -------------------------------------------------------------------------------- 1 | {"Mercury":{"mass":0.055,"radius":0.383},"Venus":{"mass":0.815,"radius":0.949},"Earth":{"mass":1,"radius":1},"Mars":{"mass":0.107,"radius":0.532},"Chlorine Planet":{"mass":2.5,"radius":1.3},"Insane Planet":{"mass":4.2,"radius":1.42}} -------------------------------------------------------------------------------- /src/clojure_by_example/data/planets.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.data.planets) 2 | 3 | (def target-planets 4 | [{:pname "Mercury" 5 | :mass 0.055 6 | :radius 0.383 7 | :moons 0 8 | :gravity 0.378 9 | :surface-pressure 0 10 | :surface-temp-deg-c {:low -170 :high 449} 11 | :rocky? true 12 | :atmosphere {}} ; empty hash map means no atmosphere 13 | 14 | {:pname "Venus" 15 | :mass 0.815 16 | :radius 0.949 17 | :moons 0 18 | :gravity 0.907 19 | :surface-pressure 92 20 | :surface-temp-deg-c {:low 465 :high 465} 21 | :rocky? true 22 | :atmosphere {:carbon-dioxide 96.45 :nitrogen 3.45 23 | :sulphur-dioxide 0.015 :traces 0.095}} 24 | 25 | {:pname "Earth" 26 | :mass 1 27 | :radius 1 28 | :moons 1 29 | :gravity 1 30 | :surface-pressure 1 31 | :surface-temp-deg-c {:low -89 :high 58} 32 | :rocky? true 33 | :atmosphere {:nitrogen 78.08 :oxygen 20.95 :carbon-dioxide 0.4 34 | :water-vapour 0.10 :argon 0.33 :traces 0.14}} 35 | 36 | {:pname "Mars" 37 | :mass 0.107 38 | :radius 0.532 39 | :moons 2 40 | :gravity 0.377 41 | :surface-pressure 0.01 42 | :surface-temp-deg-c {:low -125 :high 20} 43 | :rocky? true 44 | :atmosphere {:carbon-dioxide 95.97 :argon 1.93 :nitrogen 1.89 45 | :oxygen 0.146 :carbon-monoxide 0.056 :traces 0.008}} 46 | 47 | {:pname "Chlorine Planet" 48 | :mass 2.5 49 | :radius 1.3 50 | :moons 4 51 | :gravity 1.5 52 | :surface-pressure 1 53 | :surface-temp-deg-c {:low -42 :high 24} 54 | :rocky? true 55 | :atmosphere {:chlorine 100.0}} 56 | 57 | {:pname "Insane Planet" 58 | :mass 42 59 | :radius 4.2 60 | :moons 42 61 | :gravity 10 62 | :surface-pressure 420 63 | :surface-temp-deg-c {:low 750 :high 750} 64 | :rocky? false 65 | :atmosphere {:sulphur-dioxide 80.0 :carbon-monoxide 10.0 66 | :chlorine 5.0 :nitrogen 5.0}}]) 67 | -------------------------------------------------------------------------------- /src/clojure_by_example/ex00_introduction.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.ex00-introduction) 2 | 3 | 4 | ;; IMPORTANT: 5 | ;; - The README file explains why this project exists. 6 | ;; - Begin in this "ex00..." file, and work through it step by step. 7 | ;; - Once you are done with "ex00...", open the next file and repeat. 8 | ;; - Keep going this way, until you have worked through all the files. 9 | ;; - Once done with a file, read the recap and try any additional 10 | ;; problem sets listed there, to get more practice. 11 | 12 | ;; EX00: LESSON GOAL: 13 | ;; - Drill some Clojure basics, required for the sections 14 | ;; that follow (and generally, to help follow code 15 | ;; in the wild) 16 | ;; - Familiarize one's eyes with Clojure syntax 17 | ;; - Understand Clojure's evaluation model 18 | ;; - Start using an interactive development workflow 19 | ;; right away --- this is what it means to be a 20 | ;; "Dynamic" programming language (not to be confused 21 | ;; with dynamically typed languages.) 22 | 23 | 24 | ;; All Clojure code is composed of "expressions": 25 | ;; - And, all Clojure expressions evaluate to a value. 26 | 27 | ;; - "Atomic" literals: 28 | "hello" ; strings 29 | :hello ; keywords 30 | \h \e \l \l \o ; characters 31 | 'hello ; symbols 32 | 42 ; numbers 33 | 22/7 ; fractional numbers 34 | nil ; yes, nil is a value 35 | 36 | ;; - Collection literals: 37 | [1 2 3 4 5] ; a vector 38 | {:a 1 :b 2} ; a hash-map (key-value pairs) 39 | #{1 2 3 4 5} ; a hash-set 40 | '(1 2 3 4 5) ; a list 41 | 42 | ;; - "Built-in" functions: 43 | + ; addition 44 | map ; map over a collection 45 | filter ; filter from a collection 46 | reduce ; transform a collection 47 | 48 | ;; - "Symbolic" expressions (or "s"-expression or s-expr) 49 | 50 | (+ 1 2) ; a simple s-expr 51 | 52 | (+ (+ 1 2) (+ 1 2)) ; an s-expr of nested s-exprs 53 | 54 | (+ (+ (+ 1 2) (+ 1 2)) 55 | (+ (+ 1 2) (+ 1 2))) ; an even more nested s-expr 56 | 57 | (defn same 58 | [x] 59 | x) ; function definitions are also s-exprs 60 | 61 | 62 | ;; Namespaces: 63 | ;; 64 | ;; - are how we organize and/or modularise code 65 | ;; - All Clojure code is defined and evaluated within namespaces 66 | 67 | 68 | ;; EXERCISE: 69 | ;; Evaluate the following expressions and see what you get. 70 | ;; - First, type the expression in the REPL 71 | ;; - Next, evaluate them straight from your codebase 72 | 73 | map ; is defined in the `clojure.core` ns (namespace) 74 | 75 | same ; is defined in the current ns 76 | 77 | #_(ns-name *ns*) ; What's the current ns? 78 | 79 | (comment 80 | ;; PROTIP: 81 | ;; 82 | ;; Your IDE or text editor would have a convenient shortcut to 83 | ;; evaluate any Clojure expression right from your codebase. 84 | ;; 85 | ;; Some editors allow you to "evaluate in-line", some would 86 | ;; tell you to "send to the REPL". Consult the documentation 87 | ;; that accompanies your editor's Clojure plugin, to learn 88 | ;; how to do this. 89 | ;; 90 | ;; Make a habit of interacting "dynamically" with Clojure 91 | ;; this way, right from inside your codebase; i.e. prefer 92 | ;;_not_ to type things directly into the REPL. 93 | ) 94 | 95 | 96 | 97 | ;; Clojure expression syntax rules: 98 | 99 | ;; - Literals: 100 | ;; - Just write them down 101 | 102 | ;; - Collection literals and s-expressions: 103 | ;; - ABC - Always. Be. Closing. :-D 104 | ;; - The Clojure "Reader" (the 'R' part of the R.E.P.L) 105 | ;; expects each open bracket to be accompanied by a 106 | ;; corresponding closing bracket. i.e. all parentheses 107 | ;; must be "balanced". 108 | 109 | ;; [1 2 3] ; OK 110 | ;; [1 2 3 ; FAIL 111 | 112 | ;; {:a 1 :b 2} ; OK 113 | ;; {:a 1 :b 2 ; FAIL 114 | 115 | ;; (+ 1 2) ; OK 116 | ;; (+ 1 2 ; FAIL 117 | 118 | ;; - Indentation, extra spaces, and commas are just for 119 | ;; our reading convenience. Example: all of the following 120 | ;; literal maps represent the same value: 121 | 122 | {:a 1 :b 2} 123 | 124 | {:a 1, :b 2} 125 | 126 | {:a 1, 127 | :b 2} 128 | 129 | {:a 1 130 | :b 2} 131 | 132 | {:a 1 133 | :b 134 | 2} 135 | 136 | 137 | 138 | ;; Clojure Expression Evaluation Rules: 139 | 140 | ;; - Wrap in parentheses to cause evaluation. 141 | ;; The first position is special, and must be 142 | ;; occupied by a function 143 | 144 | (+ 1 2) ; OK 145 | ;; (1 2) ; FAIL, because 1 is not a function 146 | 147 | ;; - Mentally evaluate nested expressions "inside-out". 148 | ;; Usually, all s-expressions--however deeply nested--evaluate 149 | ;; to a return value; a literal, or a collection, or a function, 150 | ;; or some legal object. 151 | 152 | (+ (+ (+ 1 2) (+ 1 2)) 153 | (+ (+ 1 2) (+ 1 2))) 154 | 155 | (+ (+ 3 3 ) 156 | (+ 3 3 )) 157 | 158 | (+ 6 159 | 6) 160 | 161 | 12 162 | 163 | ;; - _Prevent_ evaluation of s-expr by "quoting" it, 164 | ;; i.e. explicitly marking a list, by prefixing it 165 | ;; with a single quote `'`: 166 | 167 | '(+ 1 2) ; BUT the list will still remain in the evaluation path 168 | 169 | ;; - Leave an s-expression in-line, but remove it from 170 | ;; the evaluation path, by prefixing it with `#_`: 171 | 172 | (+ 1 2 #_(+ 1 2)) ; will evaluate to 3 173 | 174 | ;; - Comment out entirely, by prefixing code with one or more 175 | ;; semicolons, just like in-line comments. 176 | 177 | ;; (+ (+ 1 2) 178 | ;; (+ 1 2)) ; fully commented out 179 | 180 | 181 | ;; EXERCISE: 182 | ;; - Now, why will the following expression fail (throw an exception)? 183 | ;; Make an educated guess, then try it. 184 | 185 | ;; (+ 1 2 '(+ 1 2)) ; un-comment and evaluate; then comment it back 186 | 187 | 188 | (comment 189 | ;; PROTIP: 190 | ;; 191 | ;; The special "#_" syntax is called a "reader macro". 192 | ;; 193 | ;; For now, ignore what that means, just know the effect of 194 | ;; using it. You will see #_ often in code to follow. 195 | ;; 196 | ;; Incidentally, the single quote we used '(to mark a list) 197 | ;; is also a reader macro. Many more specialized reader macros 198 | ;; are available, but don't go there just yet. 199 | ) 200 | 201 | 202 | 203 | ;; Why is Clojure a Lisp ("LISt Processing") language? 204 | 205 | '(+ 1 2) ; Recall: this is a Clojure list, that Clojure evaluates 206 | ; as literal data. 207 | 208 | (+ 1 2) ; if we remove the single quote, Clojure treats the same list 209 | ; as an executable list, and tries to evaluate it as code. 210 | 211 | 212 | ;; More generally, Clojure code is written in terms of Clojure's 213 | ;; own data structures. For example: 214 | ;; 215 | ;; Here is a function definition. 216 | (defn hie 217 | [person message] 218 | (str "Hie, " person " : " message)) 219 | 220 | ;; What does it look like? 221 | ;; - Let's flatten it into one line for illustrative purposes: 222 | 223 | ;;[1] [2] [3] [4] 224 | (defn hie [person message] (str "Hie, " person " : " message)) ; [5] 225 | 226 | (comment 227 | ;; Here: 228 | ;; - [1] `defn` is a Clojure built-in primitive 229 | ;; - Notice, it's at the 1st position, and 230 | ;; - 2-4 are all arguments to defn 231 | ;; Further: 232 | ;; - [2] is a Clojure symbol, `hie`, which will name the function 233 | ;; - [3] is a Clojure vector of two named arguments 234 | ;; - [4] is a Clojure s-expression, and is treated as the body of 235 | ;; the function definition 236 | ;; - [5] the whole thing itself is a Clojure s-expression! 237 | ) 238 | 239 | 240 | ;; RECAP: 241 | ;; 242 | ;; - All Clojure code is a bunch of "expressions" 243 | ;; (literals, collections, s-expressions) 244 | ;; 245 | ;; - All Clojure expressions evaluate to a return value 246 | ;; 247 | ;; - All Clojure code is written in terms of its own data structures 248 | ;; 249 | ;; - All opening braces or parentheses must be matched by closing 250 | ;; braces or parentheses, to create legal Clojure expressions. 251 | 252 | ;; 4clojure Drills: Problems you could try now. 253 | ;; 254 | ;; - With the knowledge you have so far, you can try solving these 255 | ;; problems at 4clojure.com: 1 to 13, and 16, 47, 126, 161, and 162 256 | ;; e.g. https://4clojure.oxal.org/#/problem/1 257 | ;; e.g. https://4clojure.oxal.org/#/problem/16 258 | -------------------------------------------------------------------------------- /src/clojure_by_example/ex01_fundamentally_functional.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.ex01-fundamentally-functional) 2 | 3 | ;; EX01: LESSON GOAL: 4 | ;; - Realize that pure functions, and strict lexical scope 5 | ;; are the bedrock upon which Clojure programs are built 6 | ;; - Drill how to use functions, and how lexical scope works 7 | ;; - Get comfortable with how functions compose together 8 | ;; - At every step, further drill the interactive REPL workflow. 9 | ;; Figure out how to take advantage of the immediate feedback 10 | ;; that the live REPL gives you. Treat each exercise as a 11 | ;; tiny experimental setup. Run small experiments that will 12 | ;; help you discover answers... 13 | ;; -> Read the exercise 14 | ;; -> Make a testable guess (hypothesis) 15 | ;; -> Evaluate your solution (test your hypothesis) 16 | ;; -> Compare your guess with the solution 17 | ;; -> If it differs, update your guess (or fix the solution) and redo 18 | ;; That is, try to use the Scientific Method to solve exercises. 19 | 20 | 21 | ;; Basic Function Syntax 22 | ;; 23 | ;; - Named functions: 24 | ;; 25 | (defn function-name 26 | "Documentation string (optional)." 27 | [arg1 arg2 arg3 etc up to argN] 28 | 'function 'body 29 | 'goes 'here 30 | '...) 31 | 32 | ;; - Anonymous functions: 33 | ;; 34 | (fn [arg1 arg2 arg3 etc up to argN] 35 | 'function 'body 36 | 'goes 'here 37 | '...) 38 | 39 | 40 | 41 | ;; A dead-simple function: 42 | 43 | (defn same 44 | "Simply return the input unchanged." 45 | [x] 46 | x) 47 | 48 | 49 | (fn [x] x) ; just like `same`, but with no name 50 | 51 | 52 | 53 | ;; EXERCISE 54 | ;; 55 | ;; Evaluate and see: 56 | 57 | (same 42) 58 | 59 | (same [1 2 3 4 5]) 60 | 61 | (same {:pname "Earth" :moons 1}) 62 | 63 | ;; EXERCISE 64 | ;; How about the anonymous version of `same`? 65 | ;; - What's the evaluation model? Think before you tinker. 66 | ;; - Form your hypothesis -> Test it -> Learn from the feedback 67 | 68 | ((fn [x] x) 42) 69 | 70 | ((fn [x] x) [1 2 3 4 5]) 71 | 72 | ((fn [x] x) {:pname "Earth" :moons 1}) 73 | 74 | 75 | ;; EXERCISE 76 | ;; Fix the the following s-expr, so that it evaluates to true. 77 | ;; - First predict the solution in your head. 78 | ;; - Then replace 'FIX with your solution and evaluate to confirm. 79 | ;; - Think about the little experiment you just performed, and 80 | ;; form a theory about why the solution worked 81 | (= 'FIX 82 | (same same) 83 | ((fn [x] x) same)) 84 | 85 | 86 | ;; `identity` 87 | ;; - provided by Clojure 88 | ;; - is exactly like our`same` function 89 | ;; - is extremely general (accepts any value) 90 | ;; - is surprisingly useful, as we will discover later 91 | 92 | ;; EXERCISE 93 | ;; Fix this to prove `identity`, `same`, and the anonymous 94 | ;; version of `same`, all do the exact same thing: 95 | ;; - Note: Functions are values and can therefore be compared. 96 | ;; 97 | (= identity 98 | ('FIX identity) 99 | ('FIX identity) 100 | ('FIX identity)) 101 | ;; 102 | ;; Now, evaluate this in the REPL to _see_ the truth: 103 | ;; 104 | #_(clojure.repl/source identity) 105 | ;; 106 | (comment 107 | ;; This is another example of what "dynamic" means. 108 | ;; We can not only can we interact live with small bits of 109 | ;; our Clojure programs, we can also examine many aspects 110 | ;; of our programs at run time. The clojure.repl namespace 111 | ;; is one tool at our disposal. Try these in the REPL: 112 | #_(clojure.repl/dir clojure.repl) 113 | #_(clojure.repl/doc clojure.repl) 114 | ) 115 | 116 | 117 | ;; "Higher order" functions (HoFs): 118 | 119 | ;; Functions that: 120 | ;; - *accept* functions as arguments 121 | ;; and/or 122 | ;; - *return* functions as results 123 | ;; are called "higher order" functions. 124 | 125 | 126 | ;; EXERCISE 127 | ;; Have we seen HoFs so far? If yes, list them out below. 128 | 129 | 130 | ;; EXERCISE 131 | ;; Write a zero-argument function that returns the `identity` function 132 | 133 | (defn gen-identity 134 | [] ; zero arguments 135 | 'FIX) 136 | 137 | ;; EXERCISE 138 | ;; Fix this function so that it returns a function that _behaves_ 139 | ;; like the identity function (don't return `same`, or `identity`). 140 | 141 | (defn gen-identity-v2 142 | [] 143 | 'FIX) 144 | 145 | ;; EXERCISE 146 | ;; Replace 'FIX1 with a call to the `gen-identity` function, 147 | ;; and 'FIX2 with a call to the `gen-identity-v2` function, 148 | ;; such that the following evaluates to true. 149 | 150 | (= identity 151 | 'FIX1 152 | 'FIX2) 153 | 154 | 155 | ;; Composing Logic with Higher-order Functions (HoFs): 156 | (comment 157 | ;; Clojure programmers often write simple functions that 158 | ;; each do one task well, and use higher order functions 159 | ;; to "compose" these in creative ways, to produce more 160 | ;; useful pieces of logic. 161 | ;; 162 | ;; We treat "simple" functions as building blocks, and 163 | ;; HoFs as versatile mini-blueprints that help us organize 164 | ;; and glue together the simple functions. 165 | ) 166 | 167 | ;; EXERCISE 168 | ;; Reason about why this is working: 169 | 170 | (defn selfie 171 | "Given a function `f`, return the result of 172 | applying `f` to itself." 173 | [f] 174 | (f f)) 175 | 176 | (= 42 177 | (identity 42) 178 | ((selfie identity) 42) 179 | ((selfie (selfie identity)) 42) 180 | ((selfie (selfie (selfie identity))) 42)) ; ad-infinitum 181 | 182 | 183 | ;; Let's play with a couple of nifty HoFs built into Clojure 184 | ;; - `comp` 185 | ;; - `complement` 186 | 187 | 188 | ;; EXERCISE 189 | ;; Use `(comp vec str inc)` to make the following true 190 | ;; - `comp` accepts any number of functions as arguments, 191 | ;; and returns a function that behaves as a pipeline 192 | ;; (or chain) of the given functions 193 | 194 | (= [\4 \2] 195 | (vec (str (inc 41))) 196 | ('FIX 'FIX)) 197 | 198 | (comment 199 | ;; Reason about the order of evaluation and how inputs 200 | ;; and outputs should connect, for `comp` chains to 201 | ;; work correctly. 202 | ;; 203 | ;; To see if you reasoned correctly, try each of 204 | ;; seq, str, inc independently: 205 | (inc 41) ; increment a number 206 | (str 42) ; turn the input into a string 207 | (seq "42") ; turns a string into a character sequence 208 | ) 209 | 210 | 211 | ;; EXERCISE 212 | ;; Use `(complement string?)` to make the following true 213 | ;; - `complement` accepts a "predicate" function, and returns a 214 | ;; function that does the opposite of the given "predicate" 215 | (= (not (string? "hi")) 216 | ('FIX 'FIX)) 217 | 218 | (comment 219 | ;; "Predicate" is just a term we use to conveniently describe 220 | ;; any function that returns a truthy/falsey value, i.e. 221 | ;; any function that is used to test for some condition. 222 | ;; These so-called "predicates" are not inherently special. 223 | ) 224 | 225 | 226 | 227 | ;; "Lexical Scope" in Clojure 228 | ;; - Lexical scope guarantees that the reference to a value will be 229 | ;; "enclosed" in the scope in which it is being used. 230 | 231 | (comment 232 | ;; Strict lexical scope greatly simplifies our life, because 233 | ;; it allows us to mechanically follow code, and determine 234 | ;; where a value originated. 235 | ;; - Start at the place of reference of the value. 236 | ;; - Then "walk" outwards, until you meet the very first let binding, 237 | ;; or arg-list, or def, where the value was bound. 238 | ;; - Now you know where the value came from. 239 | ;; 240 | ;; This also helps reduce our mental burden of inventing 241 | ;; new names to refer to things, because we can re-use 242 | ;; a name within a limited scope, and be certain that 243 | ;; it will not destroy anything with the same name outside 244 | ;; the given scope. 245 | ) 246 | 247 | ;; EXERCISE: 248 | ;; - Develop an intuition for what "Lexical scope" might mean 249 | ;; by reasoning about the following exercises. 250 | ;; 251 | ;; - Mentally evaluate and predict the results; then check. 252 | 253 | (def x 42) ; Bind `x` to 42, globally ("top-level" binding) 254 | 255 | (identity x) ; obviously returns 42 256 | 257 | ((fn [x] x) x) ; also returns 42, but how? 258 | 259 | (let [x 10] ; We use `let` to bind things locally. 260 | x) ; This evaluates to the value of the "let-bound" `x`. 261 | 262 | (+ (let [x 10] 263 | x) 264 | x) ; So, this whole thing should evaluate to what? 265 | 266 | 267 | ;; EXERCISE 268 | ;; Read carefully, and compare these three function variants: 269 | 270 | (defn add-one-v1 271 | [x] 272 | (+ x 1)) ; which `x` will this `x` reference? 273 | 274 | (add-one-v1 1) ; should evaluate to what? 275 | (add-one-v1 x) ; should evaluate to what? 276 | 277 | 278 | (defn add-one-v2 279 | [z] 280 | (+ x 1)) ; which `x` will this `x` reference? 281 | 282 | (add-one-v2 1) ; should evaluate to what? 283 | (add-one-v2 x) ; should evaluate to what? 284 | 285 | 286 | (defn add-one-v3 287 | [x] 288 | (let [x 10] 289 | (+ x 1))) ; which `x` will this `x` reference? 290 | 291 | (add-one-v3 1) ; should evaluate to what? 292 | (add-one-v3 x) ; should evaluate to what? 293 | 294 | 295 | ;; EXERCISE 296 | ;; - Mentally evaluate the following, predict the results, 297 | ;; and try to infer the scoping rule. 298 | ;; - Then evaluate each expression to see if your 299 | ;; mental model agrees with the result you see. 300 | ;; - Start with any `x`, and mechanically work 301 | ;; your way around. 302 | 303 | ((fn [x] x) (let [x 10] x)) 304 | 305 | 306 | ((fn [x] x) (let [x x] x)) 307 | 308 | 309 | (let [x 10] ((fn [x] x) x)) 310 | 311 | 312 | ((let [x 10] (fn [x] x)) x) 313 | 314 | 315 | ;; Function "Closure" 316 | ;; - This is a way for a function to capture and "close over" 317 | ;; any value available at the time the function is defined 318 | 319 | (def PI 3.141592653589793) 320 | 321 | (defn scale-by-PI 322 | [n] 323 | (* n PI)) ; PI is captured within the body of `scale-by-PI` 324 | 325 | (scale-by-PI 10) 326 | 327 | 328 | ;; A more general way to "scale by": 329 | ;; - Thanks to lexical scope + the function closure property 330 | 331 | (defn scale-by 332 | "Given a number `x`, return a function that accepts 333 | another number `y`, and scales `y` by `x`." 334 | [x] 335 | (fn [y] (* y x))) ; whatever is passed as `x` is captured 336 | ; within the body of the returned function 337 | 338 | 339 | ;; EXERCISE 340 | ;; 341 | #_(= (scale-by-PI 10) 342 | ('FIX 10) 343 | (* PI 10)) 344 | 345 | (comment 346 | ;; BONUS EXERCISES 347 | ;; Define a few scaling functions, in terms of `scale-by` 348 | ;; 349 | (def scale-by-PI-v2 350 | 'FIX) 351 | 352 | (def quadruple 353 | "4x the given number." 354 | 'FIX) 355 | 356 | (def halve 357 | 'FIX)) 358 | 359 | 360 | ;; Sequences (or Collections) 361 | ;; 362 | ;; - and operations on Sequences 363 | ;; - Clojure provides _many_ sequence functions. 364 | ;; Here are some important ones: `map`, `filter`, and `reduce` 365 | ;; - Observe that all these functions are HoFs! 366 | 367 | map 368 | ;; Basic Syntax: 369 | ;; 370 | ;; (map a-function a-collection) 371 | ;; 372 | ;; Where the function must accept exactly one argument, because 373 | ;; it must transform only one item of the input at a time. 374 | 375 | (map inc [1 2 3 4 5 6]) 376 | ;; | | | | | | ; declare a mapping of each item of the input coll 377 | ;; inc inc inc ; via `inc` 378 | ;; | | | | | | 379 | ;; (2 3 4 5 6 7) ; to each item of the output coll 380 | ;; 381 | ;; Note: you may wonder why the result of map inc on [1 2 3 4], which is 382 | ;; square-bracketed results in an answer that's wrapped in parens (2 3 4 5). 383 | ;; 384 | ;; The short answer is: Ignore this pesky detail. 385 | ;; Think in terms of "sequence in, sequence out", instead of "this 'type' of 386 | ;; sequence in, and the same 'type' of sequence out". 387 | ;; 388 | ;; The more confusing answer is: 'map' returns a "lazy" sequence, which the REPL 389 | ;; _prints_ out visually, with round parens. 'filter' (below) does the same too. 390 | ;; 391 | ;; Usually we don't care if we have a vector or a list or a "lazy" sequence. 392 | ;; What we do care is what the sequence contains, and that the thing remains 393 | ;; sequential before/after. It only starts mattering when we definitely want 394 | ;; a particular sequence type for the very specific performance guarantees 395 | ;; that it provides. 396 | ;; 397 | ;; But really, you'll do better if you just ignore what this actually means 398 | ;; and/or the consequences of the distinction for now. 399 | 400 | 401 | filter 402 | ;; Basic Syntax: 403 | ;; 404 | ;; (filter a-predicate-fn a-collection) 405 | ;; 406 | ;; Where the function must accept exactly one argument and 407 | ;; return a truthy result (hence we term it a "predicate" function). 408 | 409 | (filter even? [1 2 3 4 5 6]) 410 | 411 | (filter identity [1 nil 3 nil 5 nil]) ; nil is falsey, non-nils are truthy 412 | 413 | 414 | reduce 415 | ;; Basic Syntax: 416 | ;; 417 | ;; (reduce a-function accumulator a-collection) 418 | ;; 419 | ;; Where the function must accept two arguments: 420 | ;; - first one is the value of the accumulator it manages, and 421 | ;; - the second one is bound to each item of the collection 422 | 423 | (reduce + 0 [0 0 1 2]) 424 | 425 | ;; Imagine each step of the above computation, like this: 426 | 427 | ;; ======================================= 428 | ;; Accumulator | Input collection (of number of moons) 429 | ;; ======================================= 430 | ;; 0 (start) | [0 0 1 2] ; just before first step 431 | ;; 0 | [0 1 2] ; at end of first step 432 | ;; 0 | [1 2] 433 | ;; 1 | [2] 434 | ;; 3 | [] ; reduce detects empty collection 435 | ;; --------------------------------------- 436 | ;; 3 (return value) ; reduce spits out the accumulator 437 | 438 | 439 | 440 | ;; Truthiness 441 | ;; 442 | ;; - Only `nil` and `false` are Falsey; everything else 443 | ;; is Truthy 444 | ;; - a "predicate" function can return Truthy/Falsey, 445 | ;; not just boolean true/false 446 | ;; - we can make good use of this behaviour, in Clojure 447 | 448 | 449 | (def a-bunch-of-values 450 | [nil, false, ; falsey 451 | 42, :a, "foo", true, ; truthy 452 | {:a 1, :b 2}, [1 2 3 4], ; truthy 453 | '(), {}, [], ""]) ; truthy 454 | 455 | 456 | ;; A quick proof: 457 | (map boolean ; coerces a given value to boolean true or false 458 | a-bunch-of-values) 459 | 460 | (filter boolean 461 | a-bunch-of-values) 462 | 463 | 464 | ;; Branching logic accepts Truthy/Falsey 465 | 466 | (if nil ; if condition is Truthy 467 | "hi!" ; then evaluate the first expression 468 | "boo!") ; else evaluate the second expression 469 | 470 | 471 | (when false ; only when the condition is truthy 472 | "boo!") ; evaluate the body. Otherwise, always return `nil` 473 | 474 | 475 | 476 | ;; RECAP 477 | ;; - Acquire a "scientific experimentation" mindset when 478 | ;; interactively developing and debugging Clojure code 479 | ;; ... The REPL is your friend. 480 | ;; - Learn to use lexical scope and function closures effectively. 481 | ;; - Learn to define small "single purpose" functions, such that 482 | ;; you can compose them together to produce higher order logic. 483 | 484 | ;; 485 | ;; 4clojure Drills: Problems you could try now. 486 | ;; 487 | ;; - #protip: Write the solutions as proper named functions in your code base, 488 | ;; without code-golfing or hacks. Then translate to anonymous function form 489 | ;; that 4clojure requires. 490 | ;; 491 | (comment 492 | (map (fn [problem-no] (str "https://4clojure.oxal.org/#/problem/" 493 | problem-no)) 494 | [14, 15, 19, 20, 48, 45])) 495 | -------------------------------------------------------------------------------- /src/clojure_by_example/ex02_domain_as_data.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.ex02-domain-as-data) 2 | 3 | ;; Ex02: LESSON GOAL: 4 | ;; - Model and query things using pure data 5 | ;; - Realize the flexibility and power of collections 6 | 7 | 8 | ;; Our Earth 9 | 10 | ;; "pname" "Earth" 11 | ;; "mass" 1 ; if Earth mass is 1, Jupiter's mass is 317.8 x Earth 12 | ;; "radius" 1 ; if Earth radius is 1, Jupiter's radius is 11.21 x Earth 13 | ;; "moons" 1 14 | ;; "atmosphere" "nitrogen" 78.08 15 | ;; "oxygen" 20.95 16 | ;; "CO2" 0.40 17 | ;; "water-vapour" 0.10 18 | ;; "argon" 0.33 19 | ;; "traces" 0.14 20 | 21 | 22 | ;; Recall: Literal syntax: 23 | ;; - If we just put curly braces in the right places, 24 | ;; we can turn the given table into a Clojure hash-map: 25 | 26 | (def earth 27 | {"pname" "Earth" 28 | "mass" 1 29 | "radius" 1 30 | "moons" 1 31 | "atmosphere" {"nitrogen" 78.08 32 | "oxygen" 20.95 33 | "carbon-dioxide" 0.4 34 | "water-vapour" 0.10 35 | "argon" 0.33 36 | "traces" 0.14}}) 37 | 38 | ;; Now we can look up any value using `get`, and `get-in`: 39 | 40 | ;; with `get` 41 | (get earth "pname") 42 | 43 | (get (get earth "atmosphere") 44 | "traces") 45 | 46 | 47 | ;; more conveniently, with `get-in` 48 | (get-in earth ["pname"]) 49 | 50 | (get-in earth ["atmosphere" "traces"]) 51 | ;; '--> imagine this as a "path" to the value 52 | 53 | 54 | 55 | ;; Alternatively, we can model the earth this way, 56 | ;; using keywords as keys, to great benefit: 57 | (def earth-alt 58 | {:pname "Earth" 59 | :mass 1 60 | :radius 1 61 | :moons 1 62 | :atmosphere {:nitrogen 78.08 63 | :oxygen 20.95 64 | :carbon-dioxide 0.4 65 | :water-vapour 0.10 66 | :argon 0.33 67 | :traces 0.14}}) 68 | 69 | ;; EXERCISE 70 | ;; `get` and `get-in` work as expected 71 | ;; - Use `get` to extract :traces from `earth-alt`'s atmosphere 72 | ;; - The use `get-in` to do the same 73 | 74 | #_(get 'FIX 75 | 'FIX) 76 | 77 | #_(get-in 'FIX 'FIX) 78 | 79 | 80 | ;; BUT, unlike plain old strings, keywords also behave as 81 | ;; _functions_ of hash-maps, and can look themselves up 82 | ;; in any given hash-map. 83 | 84 | ;; ("pname" earth) ; Will FAIL! 85 | 86 | (:pname earth-alt) ; Works! 87 | 88 | 89 | ;; EXERCISE 90 | ;; Extract `:argon` from the `:atmosphere` of `earth-alt` 91 | 92 | ('FIX ('FIX earth-alt)) 93 | 94 | 95 | ;; Which means we can use keywords in this manner: 96 | 97 | (def planets 98 | [{:pname "Mercury" :moons 0 :mass 0.0533} 99 | {:pname "Venus" :moons 0 :mass 0.815} 100 | {:pname "Earth" :moons 1 :mass 1} 101 | {:pname "Mars" :moons 2 :mass 0.107}]) 102 | 103 | 104 | ;; Instead of having to write functions to query planets: 105 | (map (fn [p] (get p :pname)) 106 | planets) 107 | 108 | ;; We can directly use keywords as functions: 109 | (map :pname 110 | planets) 111 | 112 | 113 | ;; EXERCISE 114 | ;; `filter` out planets with less `:mass` than the Earth 115 | 116 | (defn less-mass-than-earth? 117 | [planet] 118 | (< ('FIX planet) 1)) 119 | 120 | ('FIX 'FIX 'FIX) 121 | 122 | 123 | ;; EXERCISE 124 | ;; Recall how to use `filter`, `map`, and `reduce`: 125 | (filter even? [1 2 3 4]) 126 | (map inc [1 2 3 4]) 127 | (reduce + 0 [1 2 3 4]) 128 | ;; Use these to compute the total `:mass` of planets 129 | ;; having less mass than the Earth. 130 | 131 | 132 | 133 | ;; Maps, Vectors, and Sets also behave like functions! 134 | ;; - We don't normally use maps and vector in the function 135 | ;; position to perform lookups (there are a few problems 136 | ;; with doing so), but we often use _well-defined_ sets as 137 | ;; predicate functions, to test for set membership. 138 | 139 | ;; Maps can "self-look-up" keys 140 | 141 | ({:a "a", :b "b"} :a) 142 | 143 | ;; Vectors can "self-look-up" by index position 144 | 145 | (["a" "b" "c"] 0) 146 | 147 | ;; Sets can self-test set membership 148 | 149 | (#{"a" "b" "c"} "b") ; truthy: return set member if it exists 150 | (#{"a" "b" "c"} "boo") ; falsey: return `nil` if it doesn't 151 | 152 | ;; Lists do NOT behave like functions 153 | 154 | #_('("a" "b" "c") 0) ; FAIL 155 | 156 | 157 | ;; EXERCISE 158 | ;; Define a predicate `poison-gas?` which returns the 159 | ;; poison gas if it belongs to a set of known poison gases, 160 | ;; or `nil` (falsey) otherwise. These are some known poison gases: 161 | :carbon-monoxide, :chlorine, :helium 162 | :sulphur-dioxide, :hydrogen-chloride 163 | 164 | 165 | (def poison-gas? 166 | "Does the given gas belong to a set of known poison gases?" 167 | 'FIX) 168 | 169 | (poison-gas? :chlorine) ; truthy 170 | (poison-gas? :oxygen) ; falsey 171 | 172 | 173 | ;; Collections are "open", i.e. very flexible 174 | ;; - We can make collections out of almost anything 175 | 176 | ;; Recall: 177 | (def a-bunch-of-values 178 | [nil, false, ; falsey 179 | 42, :a, "foo", true, ; truthy 180 | {:a 1, :b 2}, [1 2 3 4], ; truthy 181 | '(), {}, [], ""]) ; truthy 182 | 183 | (map boolean a-bunch-of-values) 184 | 185 | 186 | ;; And since functions are values too, we can potentially use 187 | ;; collections of functions like this: 188 | (map (fn [f] (f 42)) 189 | [str identity inc dec (fn [x] x)]) 190 | 191 | 192 | ;; Domain Modeling in Clojure 193 | ;; - We use the flexibility of collections, to model 194 | ;; real-world objects and logic as we please 195 | 196 | 197 | ;; Predicates and operations 198 | {:number-checks [even? pos? integer? (fn [x] (> x 42))] 199 | :number-ops [str identity inc dec (fn [x] x)]} 200 | 201 | 202 | ;; A data table: 203 | [[:name :age :country] 204 | ["Foo" 10 "India"] 205 | ["Bar" 21 "Australia"] 206 | ["Baz" 18 "Turkey"] 207 | ["Qux" 42 "Chile"]] 208 | 209 | 210 | ;; HTML (ref: Hiccup templates) 211 | [:div {:class "wow-list"} 212 | [:ul (map (fn [x] [:li x]) 213 | [1 2 3 4])]] 214 | 215 | 216 | ;; Musical patterns (ref: github.com/ssrihari/ragavardhini) 217 | {:arohanam [:s :r3 :g3 :m1 :p :d1 :n2 :s.], 218 | :avarohanam [:s. :n2 :d1 :p :m1 :g3 :r3 :s]} 219 | 220 | 221 | ;; DB queries (ref: Datomic) 222 | #_[:find ?name ?duration 223 | :where [?e :artist/name "The Beatles"] 224 | [?track :track/artists ?e] 225 | [?track :track/name ?name] 226 | [?track :track/duration ?duration]] 227 | 228 | 229 | ;; Starfleet mission configurations 230 | {:inhabit {:starships 5, :battle-cruisers 5, 231 | :orbiters 5, :cargo-ships 5, 232 | :probes 30} 233 | :colonise {:starships 1, :probes 50} 234 | :probe {:orbiters 1, :probes 100} 235 | :observe {:orbiters 1, :probes 10}} 236 | 237 | 238 | ;; Only limited by your imagination! 239 | 240 | ;; 241 | ;; 4clojure Drills: Problems you could try now. 242 | ;; 243 | ;; - #protip: Write the solutions as proper named functions in your code base, 244 | ;; without code-golfing or hacks. Then translate to anonymous function form 245 | ;; that 4clojure requires. 246 | ;; 247 | (comment 248 | (map (fn [problem-no] (str "https://4clojure.oxal.org/#/problem/" 249 | problem-no)) 250 | [17, 18, 57, 71 251 | 134, 27, 26, 39])) 252 | -------------------------------------------------------------------------------- /src/clojure_by_example/ex03_data_and_functions.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.ex03-data-and-functions ; current namespace (ns) 2 | ;; "require" and alias another ns as `p`: 3 | (:require [clojure-by-example.data.planets :as p])) 4 | 5 | 6 | ;; Ex03: LESSON GOALS 7 | ;; - Explore various bits and bobs of the solution interactively 8 | ;; using the live environment at your disposal 9 | ;; - Get some ideas of how to take just a handful of pieces, 10 | ;; and build sophisticated logic with them 11 | ;; - Debug any issues that might arise 12 | ;; - We use only the concepts and standard library functions 13 | ;; we've seen so far, to build purely functional logic 14 | ;; in order to process a bunch of planets: 15 | ;; 16 | ;; - Standard Library (about 20 functions): 17 | ;; `def`, `defn`, `fn`, `let` ; to create/name simple data and small functions 18 | ;; `get`, `get-in`, `assoc` ; to query and associate data 19 | ;; `map`, `filter`, `reduce` ; to operate on collections 20 | ;; `if`, `when`, `cond` ; to decide things 21 | ;; `not`, `and`, `empty?`, `<=`, `count` ; for logic and quantities 22 | ;; `comp`, `complement` ; to glue higher-order logic 23 | ;; 24 | ;; - Concepts: 25 | ;; - Compute only with pure functions: 26 | ;; - Build higher-order logic with higher order functions 27 | ;; - Lexical scope and function closures to maximize modularity 28 | ;; - Collections as functions: 29 | ;; - Keywords as functions of hash-maps 30 | ;; - Well-defined Sets as predicates --- tests of set membership 31 | ;; - Hash-maps and collections to model domain entities: 32 | ;; - A planet, or atmospheric tolerances, or decision tables, 33 | ;; or collections of analysis criteria 34 | ;; - Truthy / Falsey logic: 35 | ;; - Instead of only Boolean true/false 36 | ;; - Namespaces: 37 | ;; - Making use of things defined elsewhere 38 | ;; 39 | ;; - Workflow: 40 | ;; - Apply the Scientific Method to design, debug, and to understand 41 | ;; - Run small fast experiments via the REPL 42 | ;; - Preserve your experiments in-line within your codebase itself 43 | ;; 44 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 45 | ;; Let's colonize planets! 46 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 47 | 48 | (comment 49 | ;; BACKGROUND 50 | ;; 51 | ;; The Office of Interstellar Affairs (OIA) is pushing hard 52 | ;; for all-out space exploration and colonization. 53 | ;; 54 | ;; The OIA intends to issue "mission directives"... 55 | ;; 56 | ;; They wish humanity to :inhabit, or :colonise, or :probe, 57 | ;; or :observe a given planet based on their analysis of 58 | ;; available planetary data. 59 | ;; 60 | ;; For a given "mission directive", like :probe, the OIA 61 | ;; intends to dispatch a collection of vessels. 62 | 63 | ;; GOAL 64 | ;; 65 | ;; Prototype a bit of planetary analysis logic, using criteria 66 | ;; that interest the OIA, such that they will be able to decide 67 | ;; what to do about a given planet. 68 | ;; 69 | ;; Criteria include questions like: 70 | ;; - co2-tolerable? 71 | ;; - gravity-tolerable? 72 | ;; - surface-temp-tolerable? 73 | ;; 74 | ;; How a planet stands up to such questions will let us assess 75 | ;; whether it is habitable? or colonisable? or observe-only?. 76 | ;; 77 | ;; Once we deliver the OIA our assessment, they may choose to 78 | ;; dispatch one or more kinds of Starfleet vessels to the planet. 79 | ) 80 | 81 | ;; Here are some target planets: 82 | clojure-by-example.data.planets/target-planets 83 | 84 | ;; Which we can access more conveniently as: 85 | p/target-planets 86 | 87 | (map :pname p/target-planets) 88 | 89 | 90 | 91 | (def starfleet-mission-configurations 92 | "Associate 'mission directives' like :inhabit, :colonise, :probe, 93 | and 'mission configurations' of Starfleet vessels. e.g. If our 94 | analysis of a planet says :probe, then we would send 1 'Orbiter' 95 | class Starship carrying a complement of 100 autonomous probes." 96 | 97 | {:inhabit {:starships 5, :battle-cruisers 5, 98 | :orbiters 5, :cargo-ships 5, 99 | :probes 30} 100 | 101 | :colonise {:starships 1, :probes 50} 102 | 103 | :probe {:orbiters 1, :probes 100} 104 | 105 | :observe {:orbiters 1, :probes 10}}) 106 | 107 | 108 | 109 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 110 | ;; Basic Planetary Analysis 111 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 112 | 113 | 114 | ;; Some basic constants, utility functions, and "predicate" 115 | ;; functions to test a given planet for particular conditions. 116 | 117 | 118 | (def tolerances 119 | "Define low/high bounds of planetary characteristics we care about." 120 | {:co2 {:low 0.1, :high 5.0} 121 | :gravity {:low 0.1, :high 2.0} 122 | :surface-temp-deg-c {:low -125, :high 60}}) 123 | 124 | 125 | (def poison-gas? 126 | "A set of poison gases." 127 | #{:chlorine, :sulphur-dioxide, :carbon-monoxide}) 128 | 129 | 130 | (defn lower-bound 131 | [tolerance-key] 132 | (get-in tolerances [tolerance-key :low])) 133 | 134 | 135 | (defn upper-bound 136 | [tolerance-key] 137 | (get-in tolerances [tolerance-key :high])) 138 | 139 | 140 | (defn atmosphere-present? 141 | [planet] 142 | (not (empty? (:atmosphere planet)))) 143 | 144 | #_(map :pname 145 | (filter atmosphere-present? p/target-planets)) 146 | 147 | 148 | (defn co2-tolerable? 149 | [planet] 150 | (let [co2 (get-in planet 151 | [:atmosphere :carbon-dioxide])] 152 | (when co2 153 | (<= (lower-bound :co2) 154 | co2 155 | (upper-bound :co2))))) 156 | 157 | #_(map :pname 158 | (filter co2-tolerable? p/target-planets)) 159 | 160 | 161 | (defn gravity-tolerable? 162 | [planet] 163 | (when (:gravity planet) 164 | (<= (lower-bound :gravity) 165 | (:gravity planet) 166 | (upper-bound :gravity)))) 167 | 168 | #_(map :pname 169 | (filter gravity-tolerable? p/target-planets)) 170 | 171 | 172 | (defn surface-temp-tolerable? 173 | [planet] 174 | (let [temp (:surface-temp-deg-c planet) 175 | low (:low temp) 176 | high (:high temp)] 177 | (when (and low high) 178 | (<= (lower-bound :surface-temp-deg-c) 179 | low 180 | high 181 | (upper-bound :surface-temp-deg-c))))) 182 | 183 | #_(map :pname 184 | (filter surface-temp-tolerable? p/target-planets)) 185 | 186 | 187 | (defn air-too-poisonous? 188 | "The atmosphere is too poisonous, if the concentration of 189 | any known poison gas exceeds 1.0% of atmospheric composition." 190 | [planet] 191 | (let [gas-too-poisonous? (fn [gas-key-pct-pair] 192 | (and (poison-gas? (gas-key-pct-pair 0)) 193 | (>= (gas-key-pct-pair 1) 1.0)))] 194 | (not 195 | (empty? 196 | (filter gas-too-poisonous? 197 | (:atmosphere planet)))))) 198 | 199 | 200 | (map :pname 201 | (filter air-too-poisonous? p/target-planets)) 202 | 203 | ;; Note: a hash-map is a collection of key-value pairs 204 | (map identity 205 | {:nitrogen 78.08, :oxygen 20.95, :carbon-dioxide 0.4, 206 | :water-vapour 0.1, :argon 0.33, :traces 0.14}) 207 | 208 | (map (fn [pair] 209 | (str (get pair 0) " % = " (get pair 1))) 210 | {:nitrogen 78.08, :oxygen 20.95, :carbon-dioxide 0.4, 211 | :water-vapour 0.1, :argon 0.33, :traces 0.14}) 212 | 213 | 214 | 215 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 216 | ;; Composite checks to perform on a given planet 217 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 218 | 219 | 220 | (def minimal-good-conditions 221 | "A collection of functions that tell us about the 222 | good-ness of planetary conditions." 223 | [co2-tolerable? 224 | gravity-tolerable? 225 | surface-temp-tolerable?]) 226 | 227 | 228 | (def fatal-conditions 229 | "A collection of functions that tell us about the 230 | fatality of planetary conditions." 231 | [complement atmosphere-present? 232 | air-too-poisonous?]) 233 | 234 | 235 | (defn conditions-met 236 | "Return only those condition fns that a planet meets. 237 | An empty collection means no conditions were met." 238 | [condition-fns planet] 239 | (filter (fn [condition-fn] 240 | (condition-fn planet)) 241 | condition-fns)) 242 | 243 | 244 | (defn planet-meets-no-condition? 245 | [conditions planet] 246 | (empty? (conditions-met conditions planet))) 247 | 248 | 249 | (def planet-meets-any-one-condition? 250 | (complement planet-meets-no-condition?)) 251 | 252 | 253 | (defn planet-meets-all-conditions? 254 | [conditions planet] 255 | (= (count conditions) 256 | (count (conditions-met conditions planet)))) 257 | 258 | 259 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 260 | ;; Composite checks to 261 | ;; - test whether a given planet meets a variety of conditions. 262 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 263 | 264 | 265 | (defn habitable? 266 | "We deem a planet habitable, if it has all minimally good conditions, 267 | and no fatal conditions." 268 | [planet] 269 | (when (and (planet-meets-no-condition? 270 | fatal-conditions 271 | planet) 272 | (planet-meets-all-conditions? 273 | minimal-good-conditions 274 | planet)) 275 | planet)) 276 | 277 | #_(map :pname 278 | (filter habitable? p/target-planets)) 279 | 280 | 281 | (defn colonisable? 282 | "We deem a planet colonisable, if it has at least one 283 | minimally good condition, and no fatal conditions." 284 | [planet] 285 | (when (and (planet-meets-any-one-condition? 286 | minimal-good-conditions 287 | planet) 288 | (planet-meets-no-condition? 289 | fatal-conditions 290 | planet)) 291 | planet)) 292 | 293 | #_(map :pname 294 | (filter colonisable? p/target-planets)) 295 | 296 | 297 | (defn observe-only? 298 | "We select a planet for orbital observation, if it only has harsh surface conditions." 299 | [planet] 300 | (when (and (planet-meets-any-one-condition? 301 | fatal-conditions 302 | planet) 303 | (planet-meets-no-condition? 304 | minimal-good-conditions 305 | planet)) 306 | planet)) 307 | 308 | #_(map :pname 309 | (filter observe-only? p/target-planets)) 310 | 311 | 312 | 313 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 314 | ;; Enrich planetary data with Starfleet mission information 315 | ;; from the Office of Interstellar Affairs. 316 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 317 | 318 | (defn issue-mission-directive 319 | [planet] 320 | (cond 321 | (habitable? planet) :inhabit 322 | (colonisable? planet) :colonise 323 | (observe-only? planet) :observe 324 | :else :probe)) 325 | 326 | 327 | (defn assign-vessels 328 | [planet] 329 | (let [mission-directive (issue-mission-directive planet)] 330 | (assoc planet 331 | :mission-directive mission-directive 332 | :mission-vessels (mission-directive starfleet-mission-configurations)))) 333 | 334 | 335 | #_(map assign-vessels p/target-planets) 336 | 337 | ;; Something's not right...The Office of Interstellar Affairs tells us we're not assigning vessels correctly?! 338 | ;; We've only deployed probes and orbiters, and no other vessels?! 339 | 340 | ;; We spent all this time and 300 lines of code to direct these vessels, and the orders aren't even correct! 341 | ;; We don't even see an error message! Clearly Clojure is the worst language ever made! 342 | 343 | ;; ...OR IS IT? 344 | 345 | (comment 346 | ;; It's time to learn how Clojure allows us to debug and understand our programs, using nothing more 347 | ;; than the REPL and our wits. 348 | 349 | ;; Let's look at our results again, shall we? Are we really only deploying probes and orbiters? 350 | 351 | (map assign-vessels p/target-planets) 352 | 353 | ;; That's a bit hard to visually parse, how about this: 354 | 355 | (map :mission-vessels (map assign-vessels p/target-planets)) 356 | 357 | ;; The OIA is right! But why is this happening? 358 | ;; Either our directive to fleet mapping is wrong, or our issued directives are wrong. 359 | 360 | starfleet-mission-configurations 361 | 362 | ;; The configurations look fine. What about the directives? 363 | 364 | (map :mission-directive (map assign-vessels p/target-planets)) 365 | 366 | ;; We're only probing and observing! Clearly issue-mission-directive is at fault. 367 | ;; Let's take a look at its source code again. 368 | 369 | ;; Does this mean that there are no planets which our code considers habitable or colonisable? 370 | 371 | ;; EXERCISE: 372 | ;; Check whether we have any habitable or colonisable planets according to the habitable? and colonisable? predicates. 373 | 374 | ;; Apparently we don't! 375 | ;; At the very least, the planet Earth should be both habitable and colonisable. 376 | ;; At least now we know that habitable? and colonisable? are problematic. But why? Let's look at their implementation. 377 | ;; We'll narrow in on habitable? for the time being, and worry about colonisable? later. 378 | 379 | ;; A planet is habitable iff: 380 | ;; 1. It has an atmosphere 381 | ;; 2. The air is not too poisonous 382 | ;; 3. The carbon dioxide, gravity and temperature levels are all tolerable 383 | 384 | ;; The following issues are possible: 385 | ;; 1. planet-meets-any-one-condition? is broken. 386 | ;; 2. planet-meets-no-condition? is broken. 387 | ;; 3. minimal-good-conditions is broken. 388 | ;; 4. fatal-conditions is broken. 389 | ;; 5. Any or all of the above. 390 | 391 | ;; EXERCISE: Check if planet-meets-any-one-condition? works correctly. 392 | ;; planet-meets-any-one-condition? accepts predicates as a parameter, and doesn't care about the predicates 393 | ;; themselves. Because of this, we can simplify our debugging by using simple and obvious predicates, 394 | ;; rather than using the predicates in the production code. 395 | 396 | ;; EXERCISE: Check if planet-meets-no-condition? works correctly. 397 | 398 | ;; If none of those work, clearly there's something wrong with our conditions themselves. 399 | 400 | ;; EXERCISE: Diagnose and fix the broken conditions. 401 | 402 | ;; Does everything work now? 403 | 404 | (map assign-vessels p/target-planets) 405 | ) 406 | 407 | ;; 408 | ;; RECAP 409 | ;; - Hopefully, you now have a better handle on the various aspects 410 | ;; of working with Clojure, listed in the exercise goals; viz. 411 | ;; - Reading: How to explore an unfamiliar Clojure code-base _interactively_? 412 | ;; - "Primitives": How to get a lot done with just 20-odd core functions? 413 | ;; - Concepts: What helps us model our domains and compose functional logic? 414 | ;; - Workflow: How to apply the scientific method to development and debugging? 415 | ;; 416 | ;; - REPL all the things! 417 | ;; Especially understand how the Clojure REPL is a powerful debugging tool 418 | ;; that supersedes more traditional step-through debuggers in many ways. 419 | ;; 420 | ;; You can: 421 | ;; 1. Test individual functions or constants to check if they're correct. 422 | ;; 2. Redefine a function to add tracing such as print statements, or other forms of instrumentation. 423 | ;; 3. Capture intermediate values such as function arguments or let bindings, and inspect them in the REPL 424 | ;; after the fact. 425 | ;; 4. Fix the problem and verify that it works immediately. 426 | ;; 5. Do all of the above either locally, or while connected to a remote server running in a staging or 427 | ;; production environment. 428 | ;; 429 | ;; We strongly recommend going through https://clojure.org/guides/repl/enhancing_your_repl_workflow#debugging-tools-and-techniques 430 | ;; for more tips, tricks and resources related to debugging. The entire REPL guide is useful, but the section about debugging 431 | ;; is particularly pertinent. 432 | 433 | ;; 434 | ;; 4clojure Drills: Problems you could try now. 435 | ;; 436 | ;; - #protip: Write the solutions as proper named functions in your code base, 437 | ;; without code-golfing or hacks. Then translate to anonymous function form 438 | ;; that 4clojure requires. 439 | (comment 440 | (map (fn [problem-no] (str "https://4clojure.oxal.org/#/problem/" 441 | problem-no)) 442 | [37, 64, 72, 21, 24, 25, 443 | 38, 29, 42, 31, 81, 107, 444 | 88, 157, 50, 46, 65])) 445 | -------------------------------------------------------------------------------- /src/clojure_by_example/ex04_api_design.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.ex04-api-design 2 | (:require [clojure-by-example.data.planets :as p])) 3 | 4 | ;; EX04: Lesson Goals: 5 | ;; - We use these conveniences for good effect in API design. 6 | ;; - See how to allow the same function to support different arities, 7 | ;; as well as a variable number of arguments 8 | ;; - See how to "de-structure" data (it's a powerful, flexible lookup mechanism) 9 | ;; - Leverage de-structuring to design a self-documenting function API 10 | 11 | 12 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 13 | ;; 14 | ;; Multiple arities 15 | ;; 16 | ;; - When we know for sure that a function must handle more than 17 | ;; one "arity". An "arity" is the number of arguments 18 | ;; 19 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 20 | 21 | 22 | (defn add-upto-three-nums 23 | ([] 0) ; identity of addition 24 | ([x] x) 25 | ([x y] (+ x y)) 26 | ([x y z] (+ x y z))) 27 | 28 | (add-upto-three-nums) 29 | (add-upto-three-nums 1) 30 | (add-upto-three-nums 1 2) 31 | (add-upto-three-nums 1 2 3) 32 | #_(add-upto-three-nums 1 2 3 4) ; will fail 33 | 34 | 35 | ;; Variable arity 36 | ;; - When we don't know in advance how many arguments we 37 | ;; will have to handle, but we want to handle them all. 38 | 39 | (defn add-any-numbers 40 | [& nums] 41 | (reduce + 0 nums)) 42 | 43 | (add-any-numbers) 44 | (add-any-numbers 1) 45 | (add-any-numbers 1 2) 46 | (add-any-numbers 1 2 3 4 5) 47 | 48 | 49 | ;; Multiple _and_ Variable arities, combined 50 | ;; - Guess what + actually is inside? 51 | ;; 52 | #_(clojure.repl/source +) ; evaluate, check the REPL 53 | ;; 54 | ;; See how + tries to implement each arity as a special case, 55 | ;; to compute results as optimally as possible? We can do 56 | ;; such things too, in functions we define. 57 | 58 | (+) 59 | (+ 1) 60 | (+ 1 2 3 4 5 6 7 8 9 0) 61 | 62 | 63 | ;; We can also use multiple arities to define sane fallbacks. 64 | 65 | ;; EXERCISE 66 | ;; - Recall `lower-bound`, and `upper-bound` from ex03 67 | ;; - Refactor these to support more than one arity. 68 | 69 | (def tolerances 70 | "Define low/high bounds of planetary characteristics we care about." 71 | {:co2 {:low 0.1, :high 5.0} 72 | :gravity {:low 0.1, :high 2.0} 73 | :surface-temp-deg-c {:low -125, :high 60}}) 74 | 75 | (defn lower-bound 76 | [tolerance-key] 77 | (get-in tolerances [tolerance-key :low])) 78 | 79 | (defn upper-bound 80 | [tolerance-key] 81 | (get-in tolerances [tolerance-key :high])) 82 | 83 | 84 | ;; Fix `lower-bound-v2`, to make this expression evaluate 85 | ;; to true. Pay close attention to what should go where. 86 | (defn lower-bound-v2 87 | "Look up the lower bound for the given tolerance key, in the 88 | given map of `tolerances`. Use a globally-defined `tolerances` 89 | map as a sane default if only tolerance-key is passed in." 90 | ([tolerance-key] 91 | (get-in tolerances 92 | [tolerance-key :low])) 93 | ([FIX1 FIX2] 94 | 'FIX)) 95 | 96 | #_(= (lower-bound :co2) 97 | (lower-bound-v2 :co2) 98 | (lower-bound-v2 :co2 tolerances) 99 | (lower-bound-v2 :co2 {:co2 {:low 0.1}})) 100 | 101 | 102 | (comment 103 | ;; BONUS EXERCISE 104 | ;; Do the same for `upper-bound-v2` 105 | #_(defn upper-bound-v2 106 | 'FIX 107 | 'FIX) 108 | 109 | #_(= (upper-bound :co2) 110 | (upper-bound-v2 :co2) 111 | (upper-bound-v2 :co2 tolerances) 112 | (upper-bound-v2 :co2 {:co2 {:low 0.1}})) 113 | ) 114 | 115 | 116 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 117 | ;; 118 | ;; A tiny bit of "De-structuring" 119 | ;; - For convenient access to items in collections. 120 | ;; 121 | ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 122 | 123 | (comment 124 | ;; If we _shape_ our domain information as a data "structure", 125 | ;; can we use our knowledge of the shape to pull it apart; 126 | ;; i.e. "destructure" it? 127 | 128 | ;; Yes. 129 | 130 | ;; We use destructuring: 131 | ;; - In `let` bindings, to cleanly reach into data 132 | ;; - In function arguments, to make the API clean and expressive 133 | ;; 134 | ;; Here are a couple of commonly-used ways to do it. 135 | ) 136 | 137 | ;; "Positional" De-structuring 138 | ;; 139 | ;; - Pull apart sequential, ordered data structures like 140 | ;; lists, vectors, and any other sequence with linear access 141 | ;; 142 | ;; - Follow the structure of the collection, and mechanically 143 | ;; bind values to symbols by position. 144 | 145 | 146 | ;; Evaluate and see what happens. Then form a theory of what might be going on. 147 | (def planet-names 148 | (map :pname p/target-planets)) 149 | 150 | 151 | (let [[pname1 pname2] planet-names] 152 | (str pname1 " is the 1st planet, and " 153 | pname2 " is the 2nd planet.")) 154 | 155 | 156 | (let [[:as pnames] planet-names] 157 | pnames) 158 | 159 | 160 | (let [[m v e :as pnames] (map :pname p/target-planets)] 161 | {:useless-trivia (str e " is the third rock from the Sun.") 162 | :planets-names pnames}) 163 | 164 | 165 | ;; "Associative" De-structuring 166 | ;; 167 | ;; - Syntax to reach into associative data structures 168 | ;; (having key-value semantics), in arbitrary ways. 169 | ;; 170 | ;; - Note: Clojure "Records" and vectors are associative too 171 | ;; 172 | ;; - Follow the structure of the collection, and mechanically 173 | ;; bind values to symbols by key name. 174 | 175 | ;; Evaluate one by one and see what happens. 176 | ;; Then form a theory of what might be going on. 177 | 178 | 179 | (let [[mercury] p/target-planets] 180 | (str (:pname mercury) " has mass " (:mass mercury))) 181 | 182 | 183 | (let [[{:keys [pname mass]}] p/target-planets] 184 | (str pname " has mass " mass)) 185 | 186 | 187 | (let [[{:keys [pname mass] :as mercury}] p/target-planets] 188 | (assoc mercury 189 | :useless-trivia (str pname " has mass " mass))) 190 | 191 | 192 | 193 | ;; And, putting it all together, a function with a 194 | ;; more self-documented API: 195 | 196 | (defn add-useless-trivia 197 | "Be Captain Obvious. Given a planet, add some self-evident trivia to it." 198 | [{:keys [pname mass] :as planet}] 199 | (assoc planet 200 | :useless-trivia (str pname " has mass " mass))) 201 | 202 | 203 | (map add-useless-trivia 204 | p/target-planets) 205 | 206 | #_(clojure.repl/doc add-useless-trivia) 207 | 208 | 209 | ;; EXERCISE: 210 | ;; Review the de-structured argument list in `add-useless-trivia`; 211 | ;; then try to recall and reinforce a key concept. 212 | ;; Hints: 213 | ;; - Relate it to our preferred way to model the world. 214 | ;; - What is the "world" here? 215 | ;; - What are we using to model/describe what property of what? 216 | ;; (Yes, that's three 'what's :-) 217 | 218 | 219 | ;; EXERCISE: 220 | ;; 221 | ;; - Use de-structuring to refactor the following functions 222 | ;; that we have copied over from ex03. 223 | ;; 224 | ;; - Develop a preliminary opinion about where and when 225 | ;; it might makes sense to de-structure, and where and when 226 | ;; it might not. 227 | 228 | 229 | (defn atmosphere-present? 230 | [planet] 231 | (not (empty? (:atmosphere planet)))) 232 | 233 | 234 | (defn atmosphere-present?-refactored 235 | [FIXME] 236 | FIXME) 237 | 238 | #_(= (map :pname 239 | (filter atmosphere-present? p/target-planets)) 240 | (map :pname 241 | (filter atmosphere-present?-refactored 242 | p/target-planets))) 243 | 244 | (defn co2-tolerable? 245 | [planet] 246 | (let [co2 (get-in planet 247 | [:atmosphere :carbon-dioxide])] 248 | (when co2 249 | (<= (lower-bound :co2) 250 | co2 251 | (upper-bound :co2))))) 252 | 253 | 254 | (defn co2-tolerable?-refactored 255 | [FIXME] 256 | FIXME) 257 | 258 | #_(= (map :pname 259 | (filter co2-tolerable? p/target-planets)) 260 | (map :pname 261 | (filter co2-tolerable?-refactored 262 | p/target-planets))) 263 | 264 | 265 | ;; EXERCISE: 266 | ;; - Fix the body of the refactored function 267 | ;; - Carefully review the function APIs, and develop a preliminary 268 | ;; opinion whether the refactored version is better than the original. 269 | 270 | 271 | (defn surface-temp-tolerable? 272 | [planet] 273 | (let [temp (:surface-temp-deg-c planet) 274 | low (:low temp) 275 | high (:high temp)] 276 | (when (and low high) 277 | (<= (lower-bound :surface-temp-deg-c) 278 | low 279 | high 280 | (upper-bound :surface-temp-deg-c))))) 281 | 282 | 283 | (defn surface-temp-tolerable?-refactored 284 | [{:keys [FIXME] :as planet}] 285 | FIXME) 286 | 287 | 288 | (defn surface-temp-tolerable?-refactored-v2 289 | [{{:keys [low high]} :surface-temp-deg-c 290 | :as planet}] 291 | 'FIXME) 292 | 293 | 294 | #_(= (map :pname 295 | (filter surface-temp-tolerable? 296 | p/target-planets)) 297 | (map :pname 298 | (filter surface-temp-tolerable?-refactored 299 | p/target-planets)) 300 | (map :pname 301 | (filter surface-temp-tolerable?-refactored-v2 302 | p/target-planets))) 303 | 304 | 305 | ;; RECAP: 306 | ;; 307 | ;; - We model the world by composing data structures and then use 308 | ;; "de-structuring" to conveniently reach into those structures. 309 | ;; - We can design function apis to accept more than one arity, 310 | ;; and then define custom logic for each arity. 311 | ;; - When, where, and how much to de-structure is a matter of 312 | ;; taste; a design choice. There is no One True Way. 313 | ;; - There are _many_ many ways of de-structuring. 314 | ;; Here's a really nice post detailing it: 315 | ;; cf. http://blog.jayfields.com/2010/07/clojure-destructuring.html 316 | 317 | ;; 4clojure Drills: Problems you could try now. 318 | ;; 319 | ;; - #protip: Write the solutions as proper named functions in your code base, 320 | ;; without code-golfing or hacks. Then translate to anonymous function form 321 | ;; that 4clojure requires. 322 | ;; 323 | (comment 324 | (map (fn [problem-no] (str "https://4clojure.oxal.org/#/problem/" 325 | problem-no)) 326 | [35, 36, 68, 145, 52, 327 | 156, 22, 23, 32, 30, 328 | 34, 28, 33, 40, 83, 329 | 61, 99, 120, 56, 55, 330 | 43, 67, 74, 80, 69, 75])) 331 | -------------------------------------------------------------------------------- /src/clojure_by_example/ex05_immutability_and_fp.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.ex05-immutability-and-fp) 2 | 3 | ;; Ex05: Lesson Goals 4 | ;; - This section is more conceptual, than exercise-oriented. 5 | ;; 6 | ;; - Set you up with some important ideas, which we will use heavily 7 | ;; in the final section (and in all our Clojure programs) 8 | ;; - All values are immutable by default (and we like it this way) 9 | ;; - What `def` is 10 | ;; - Why you should avoid global defs 11 | ;; - What are "pure functions"? 12 | ;; 13 | ;; - Don't forget to evaluate all s-expressions that interest you, 14 | ;; and also feel free to write and play around with your own ones. 15 | 16 | 17 | 18 | ;; Previously, in Clojure by Example: 19 | ;; 20 | ;; - We either only queried one or more planets, 21 | ;; - Or we checked if values satisfy some "predicate" 22 | ;; - Or we calculated new values using (reduce + ...) etc. 23 | ;; - Or we defined globals (with def), and locals (with let) 24 | ;; 25 | ;; - But we did not really try to modify or change any quantity 26 | ;; or sequence. 27 | 28 | 29 | ;; What if we try to "change" things? 30 | 31 | 32 | (def pi 3.141) 33 | 34 | (+ pi 1) ; add one to pi 35 | 36 | ;; EXERCISE: 37 | ;; 38 | ;; Predict the value of pi: 39 | 40 | pi ; evaluate to confirm 41 | 42 | 43 | ;; Hm, let's try vectors and maps. 44 | ;; 45 | ;; - We can "associate" new key-vals into an existing map 46 | (assoc {:a 1} 47 | :b 2 48 | :c 3) 49 | ;; 50 | ;; - With assoc, we can also update existing key-value pairs 51 | (assoc {:a 1 :b 2} 52 | :b 99) 53 | ;; 54 | ;; - And, finally, we can "dissociate" existing key-vals 55 | (dissoc {:a 1 :b 2 :c 3} 56 | :b 57 | :c) 58 | 59 | 60 | ;; - So suppose we define... 61 | 62 | (def planets [{:pname "Earth" :moons 1} 63 | {:pname "Mars" :moons 2}]) 64 | 65 | ;; - Then, maybe, we can `assoc` a new k-v pair into all planets: 66 | ;; - And while we're at it, also dissoc an existing one: 67 | ;; 68 | (map (fn [planet] 69 | (assoc (dissoc planet :moons) 70 | :habitable? true)) 71 | planets) 72 | 73 | 74 | ;; EXERCISE: 75 | ;; 76 | ;; Predict the result of filtering by the value of `:habitable?`: 77 | 78 | (filter :habitable? planets) 79 | 80 | planets ; confirm by checking the value of this 81 | 82 | 83 | 84 | ;; WHY IS Clojure DOING THIS TO US???!!! 85 | ;; 86 | ;; Why are we not allowed to mutate these things? 87 | ;; 88 | ;; How will we get _anything_ done in the real world? 89 | ;; 90 | ;; Well, actually, you've _already_ been programming with such 91 | ;; "immutable" values, and it hasn't stopped you from being awesome! 92 | ;; 93 | ;; Now, we just need to learn "The art of fighting, without fighting", 94 | ;; or, how to change the world, _without_ using _things that change_. 95 | ;; 96 | ;; Immutability v/s mutability is a deeply unsettling topic, so 97 | ;; we will park the discussion for now and come back to it in the 98 | ;; next "chapter". 99 | ;; 100 | ;; But first, a few important practicalities. 101 | 102 | 103 | ;; On `def`: 104 | ;; 105 | ;; `def` creates a mutable reference. Technically, it is a way 106 | ;; "to maintain a persistent reference to a changing value". 107 | ;; 108 | ;; Note the difference: 109 | ;; - We can mutate the _reference_, but not the value. 110 | ;; - Values are by definition immutable. 111 | ;; 112 | ;; Warning: 113 | ;; - DO NOT use `def` to _emulate_ mutation. 114 | 115 | ;; Firstly: 116 | ;; - Because you'll cause errors of understanding: 117 | 118 | (def weird-pi 3.141) ; bind weird-pi to 3.141 119 | 120 | (def other-pi weird-pi) ; bind other-pi to the value of weird-pi, 121 | ; which at this point is 3.141 122 | 123 | (def weird-pi 42) ; re-bind weird-pi to some other value 124 | 125 | weird-pi ; changes to 42 126 | 127 | other-pi ; what should this be? 128 | 129 | ;; See the problem? 130 | ;; - We re-bound weird-pi to a new value, but other-pi's binding 131 | ;; remained constant. 132 | ;; - Spread enough re-definitions across your program, and you'll be 133 | ;; in trouble; unable to reason about who's using what version 134 | ;; of the binding. 135 | 136 | 137 | ;; Secondly: 138 | ;; - It's dangerous because re-defining a var alters it globally. 139 | ;; - Why so dangerous? 140 | ;; 141 | ;; Well, compare the following. 142 | ;; - All three are _incorrect_, because pi is wrong. 143 | ;; - But only the third one is actually dangerous. 144 | 145 | (defn scale-by-pi-v1 146 | [n] 147 | (let [pi 42] 148 | (* pi n))) 149 | 150 | (defn scale-by-pi-v2 151 | [n] 152 | (* weird-pi n)) 153 | 154 | #_(defn scale-by-pi-v3 155 | [n] 156 | (def pi 42) 157 | (* n pi)) 158 | 159 | 160 | ;; Also, remember functions are values? 161 | 162 | (fn [x] x) ; is a value (which shall remain anonymous) 163 | 164 | (defn same 165 | [x] 166 | x) ; `same` is the name of a function. 167 | ; Therefore `same` names a value. 168 | 169 | ;; So what if we: 170 | (def same-same 171 | (fn [x] x)) ; hah! 172 | 173 | ;; As it happens: 174 | ;; - `defn` is really just a convenience wrapper over `def`. 175 | ;; - Because we can't live without defining functions, in Clojure-land 176 | 177 | (macroexpand '(defn same [x] x)) ; yes, it is 178 | 179 | ;; So, truthfully: 180 | ;; - `same` and `same-same` are mutable references to immutable 181 | ;; function definitions. 182 | ;; 183 | ;; Now: 184 | ;; - This mutable binding is very useful during development. 185 | ;; - It lets us interactively improve-and-rebind functions bit by bit. 186 | ;; - Stay in "flow". 187 | ;; 188 | ;; But: 189 | ;; - Imagine the horror of someone or something mutating the definition 190 | ;; of your functions from under you, while your program is running! 191 | ;; 192 | ;; Yet: 193 | ;; - In extremely rare cases, the ability to re-define things in a 194 | ;; live production system can be incredibly useful. Read the 195 | ;; paragraph starting with "The Remote Agent software" on this page: 196 | ;; http://flownet.com/gat/jpl-lisp.html 197 | ;; 198 | ;; Still: 199 | ;; - Don't try this in production, unless you really really really 200 | ;; know what your are doing. 201 | 202 | 203 | ;; Lesson: 204 | ;; 205 | ;; - Clojure programmers use `def` _only_ to attach values to globally 206 | ;; referenced names, for re-use. For example: 207 | ;; - The value of `pi` would be good global. 208 | ;; - The value of a function is often a good global. 209 | ;; (Recall: defn just wraps over def). 210 | ;; 211 | ;; - We rely on lexical scope to bind values as close as possible 212 | ;; to the place in code where the value is used. This makes code 213 | ;; much easier to reason about. 214 | ;; 215 | ;; - "Lexical scope" is super-important. Understand it and use it well, 216 | ;; for great good. 217 | 218 | 219 | 220 | ;; "Pure Functions" 221 | 222 | 223 | ;; This function is "pure" 224 | ;; - It is a mapping of input data -> output data and nothing more. 225 | (defn add-one 226 | [x] 227 | (+ x 1)) 228 | 229 | 230 | ;; This function is "impure" 231 | ;; - Although it adds one to the input, it also changes the world on 232 | ;; the side, by sending out a value to some other place 233 | ;; - printing is a "side effect" who's outcome we cannot always predict 234 | (defn add-one! 235 | [x] 236 | (println x) 237 | (+ x 1)) 238 | ;; - other examples of side-effects include: 239 | ;; - writing to a db (what if the db gets slow or unavailable?) 240 | ;; - or logging to console (what if the log file gets corrupted?) 241 | 242 | 243 | (add-one 1) ; adds one, but never changes the outside world 244 | 245 | (add-one! 1) ; adds one, and also changes the outside world 246 | 247 | 248 | ;; Pure functions are drop-in replacements for each other. 249 | 250 | (= 2 251 | ((fn [x] (+ x 1)) 1) 252 | (add-one 1) 253 | (inc 1)) 254 | 255 | ;; Impure functions cannot be used as drop-in replacements for pure 256 | ;; functions, or for other impure functions for that matter. 257 | 258 | 259 | ;; RECAP: 260 | ;; 261 | ;; - Clojure values are immutable by default, and we prefer it that way 262 | ;; 263 | ;; - `def` is best used only to define names for truly global values. 264 | ;; Use `let` to bind local values, to get all the benefits of strict 265 | ;; lexical scope. 266 | ;; 267 | ;; - `defn` is just a wrapper over `def`, designed specifically to 268 | ;; define functions. 269 | ;; 270 | ;; - Write pure functions as far as possible. 271 | 272 | ;; 4clojure Drills: Problems you could try now. 273 | ;; 274 | ;; - #protip: Write the solutions as proper named functions in your code base, 275 | ;; without code-golfing or hacks. Then translate to anonymous function form 276 | ;; that 4clojure requires. 277 | ;; 278 | (comment 279 | (map (fn [problem-no] (str "https://4clojure.oxal.org/#/problem/" 280 | problem-no)) 281 | [51, 77, 60, 102, 86, 115])) 282 | -------------------------------------------------------------------------------- /src/clojure_by_example/ex06_full_functional_firepower.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.ex06-full-functional-firepower 2 | (:require [clojure.data.json :as json] 3 | [clojure.java.io :as io])) 4 | 5 | ;; Ex06: Lesson Goals 6 | ;; - This is more of a code-reading section, designed to: 7 | ;; 8 | ;; - Show how to "keep state at the boundary"; e.g.: 9 | ;; - ingest sophisticated planets from a JSON file 10 | ;; ----------------Input boundary---------------- 11 | ;; - punch it into a purely functional data processing pipeline 12 | ;; - do awesome things with just 13 | ;; - `map`, `filter`, `reduce`, 14 | ;; - simple helper functions (with/without control flow), and 15 | ;; - models of the world as pure data structures 16 | ;; ----------------Output boundary--------------- 17 | ;; - spit out various kinds of outputs into output files 18 | ;; 19 | ;; 20 | ;; - Show where mutability is useful and desirable; e.g. I/O 21 | ;; (but where it isn't we try hard to avoid it) 22 | ;; 23 | ;; - Also give a feel for how we can use all the lessons learned 24 | ;; so far, to design flexible function APIs for good "composability". 25 | 26 | 27 | 28 | 29 | ;; Previously, in Clojure By Example... 30 | ;; 31 | ;; We lamented our immutable fate. 32 | ;; 33 | ;; WHY IS Clojure DOING THIS TO US???!!! 34 | ;; 35 | ;; And how are we to get anything done if we can't change things??? 36 | ;; 37 | ;; HOW??? 38 | 39 | 40 | 41 | ;; First, picture what a pure function does: 42 | ;; 43 | ;; [ Input Data ] 44 | ;; | 45 | ;; v 46 | ;; [ Function ] --->x NO side-effects allowed! 47 | ;; | 48 | ;; v 49 | ;; [ Output Data ] 50 | ;; 51 | ;; i.e. a pure function is a data -to-> data transformation. 52 | 53 | 54 | 55 | ;; Now, picture what _we_ want to do: 56 | ;; 57 | ;; [ Outside World ] 58 | ;; | 59 | ;; v 60 | ;; -------------------------------------------------- 61 | ;; [ I/O Boundary ] ; Ingest the state of the World. 62 | ;; -------------------------------------------------- 63 | ;; | 64 | ;; v 65 | ;; [ Input Data ] ; Values that we can't ever mutate. 66 | ;; | 67 | ;; v 68 | ;; [ Our Program ] ; A chain / pipeline of pure functions? 69 | ;; | 70 | ;; v 71 | ;; [ Output Data ] ; Values that we can't ever mutate. 72 | ;; | 73 | ;; v 74 | ;; -------------------------------------------------- 75 | ;; [ I/O Boundary ] ; Update the state of the World. 76 | ;; -------------------------------------------------- 77 | ;; | 78 | ;; v 79 | ;; [ Outside World ] ; New and Improved! 80 | ;; 81 | ;; 82 | ;; i.e., barring I/O Boundaries, where we read/write state, 83 | ;; could our whole program be a pure data-to-> data transformation? 84 | 85 | 86 | ;; Let's do this... 87 | 88 | 89 | ;; Outside World 90 | ;; 91 | ;; - A bunch of planetary sensors have written some JSON-formatted data 92 | ;; into some files on disk. 93 | ;; 94 | ;; - Our Planetary Data Scientists have told us the format for each 95 | ;; JSON file. 96 | ;; 97 | ;; - They want us to help them consolidate that data into a single file. 98 | ;; 99 | ;; - Maybe they will run batch jobs later? Who knows... 100 | ;; 101 | ;; - For now, we only need to prove to them that we can read their data, 102 | ;; and write to the file they want. 103 | 104 | 105 | ;; They told us sensors would dump files here... 106 | ;; 107 | (def sensor-data-dir 108 | "resources/sensor_data/") 109 | 110 | 111 | ;; They gave us: 112 | ;; - The file names for each sensor, and 113 | ;; - Told us the schema for the JSON in each file 114 | ;; 115 | ;; We decided to: 116 | ;; - Make a look-up table of files (as a hash-map), and 117 | ;; - Document the schema as in-line comments, for this dirty prototype. 118 | ;; 119 | (def sensor-data-files 120 | ;; {"Planet Name":{"radius":}, ...} 121 | {:planets "planet_detector.json" 122 | 123 | ;; {"Planet Name":, ...} 124 | :moons "moon_detector.json" 125 | 126 | ;; {"Planet Name":, ...} 127 | :atmosphere "atmospheric_detector.json"}) 128 | 129 | 130 | ;; They told us: 131 | ;; - The file name to put the fully denormalized planetary data. 132 | ;; 133 | (def consolidated-data-file 134 | "consolidated_data.json") 135 | ;; 136 | ;; And, they said the JSON inside should follow this schema: 137 | ;; 138 | ;; [{"pname":"Planet Name", 139 | ;; "moons":, 140 | ;; "atmosphere":}, 141 | ;; 142 | ;; {"pname" ...}, 143 | ;; 144 | ;; {"pname" ...}] 145 | 146 | 147 | ;; ---------------------- Input Boundary begins ------------------------ 148 | 149 | (defn ingest-json-file! 150 | [dir-path file-name] 151 | (let [file-path (str dir-path file-name)] 152 | (with-open [reader (io/reader file-path)] 153 | (json/read reader 154 | :key-fn keyword)))) 155 | 156 | 157 | (defn gather-all-sensor-data! 158 | "Use our global collection of sensor keys to look up all the 159 | sensor data files, and ingest them at one go. Return a hash-map 160 | containing each sensor's data mapped to the corresponding sensor key." 161 | [data-dir sensor-files-map] 162 | (let [ingest-sensor-data 163 | (fn [out-map [sensor-key sensor-file]] 164 | (assoc out-map 165 | sensor-key (ingest-json-file! data-dir 166 | sensor-file)))] 167 | (reduce ingest-sensor-data 168 | {} ; out-map starts empty 169 | sensor-files-map))) 170 | 171 | #_(gather-all-sensor-data! sensor-data-dir 172 | sensor-data-files) ; try it 173 | 174 | ;; --------------------- Purely Functional Program begins -------------- 175 | 176 | 177 | (defn add-sensor-data 178 | "Given a sensor key, a planetary hash-map, and a tuple having 179 | sensor data for a planet, inject the sensor data under the 180 | planet name found in the planetary hash-map." 181 | [sensor-key planets [pname sensor-pdata]] 182 | (assoc-in planets ; given these planets as a hash-map 183 | [pname sensor-key] ; follow this nested path 184 | sensor-pdata)) ; and assoc this value under that path 185 | ;; Note: 186 | ;; Just like `assoc` (into map) is the partner of `get` (from map), 187 | ;; `assoc-in` (nested map) is the partner of `get-in` (from nested map). 188 | 189 | 190 | (defn add-moon-data 191 | "Inject moons sensor data, into planets sensor data." 192 | [moons planets] 193 | (reduce (partial add-sensor-data :moons) 194 | planets 195 | moons)) 196 | 197 | 198 | (defn add-atmosphere-data 199 | "Inject atmosphere sensor data, into planets sensor data." 200 | [atmosphere planets] 201 | (reduce (partial add-sensor-data :atmosphere) 202 | planets 203 | atmosphere)) 204 | 205 | 206 | (defn denormalise-planetary-data* 207 | "Given a hash-map of planetary data (keyed by planet names), 208 | return just the planetary data, with the planet's names added in. 209 | 210 | Also ensure all keys are keywordized, for convenient look-ups." 211 | [planets] 212 | (map (fn [[pname pdata]] 213 | (assoc pdata 214 | :name (name pname))) 215 | planets)) 216 | 217 | 218 | (defn denormalise-planetary-data 219 | "Given all sensor data, produce a collection of denormalized 220 | planetary data." 221 | [{:keys [planets atmosphere moons] 222 | :as all-sensor-data}] 223 | ((comp denormalise-planetary-data* 224 | (partial add-atmosphere-data atmosphere) 225 | (partial add-moon-data moons)) 226 | planets)) 227 | 228 | 229 | ;; ---------------------- Output Boundary begins ----------------------- 230 | 231 | 232 | ;; First try this, to check packaged data... 233 | #_(denormalise-planetary-data 234 | (gather-all-sensor-data! sensor-data-dir 235 | sensor-data-files)) 236 | 237 | ;; Does the result look familiar? 238 | 239 | 240 | (defn write-out-json-file! 241 | [dir-path file-name data] 242 | (let [file-path (str dir-path file-name)] 243 | (with-open [writer (io/writer file-path)] 244 | (json/write data writer)))) 245 | 246 | 247 | (defn ingest-export-sensor-data! 248 | [data-dir source-data-files dest-data-file] 249 | (write-out-json-file! 250 | data-dir 251 | dest-data-file 252 | (denormalise-planetary-data 253 | (gather-all-sensor-data! data-dir source-data-files)))) 254 | 255 | 256 | ;; Try it! 257 | #_(ingest-export-sensor-data! 258 | sensor-data-dir 259 | sensor-data-files 260 | consolidated-data-file) 261 | 262 | ;; --------------------- Output Boundary ends ------------------------ 263 | 264 | ;; Done! 265 | 266 | ;; Or are we??? 267 | 268 | ;; Because now... 269 | 270 | ;; The Planetary Data Scientists reveal their nefarious intentions: 271 | 272 | ;; (colonize-habitable-planets! ; deviously implemented by them, elsewhere 273 | ;; (denormalise-planetary-data 274 | ;; (gather-all-sensor-data! sensor-data-dir 275 | ;; sensor-data-files))) 276 | 277 | 278 | ;; RECAP: 279 | ;; - If we carefully keep side-effecting functions at our program 280 | ;; "boundary", we can design purely functional "core" logic. 281 | ;; This makes it much easier to reason about most of our program, 282 | ;; and to test it. 283 | ;; 284 | ;; - We could have written much more concise and well-abstracted 285 | ;; code, by using more Clojure features and utilities. However 286 | ;; this is not bad at all, and the point was to show we could 287 | ;; get this far quite easily, just with a handful of moving parts. 288 | ;; 289 | ;; - The next section will show you how to turn this file into 290 | ;; a complete project, that you can build and run as a 291 | ;; standalone executable. 292 | -------------------------------------------------------------------------------- /src/clojure_by_example/ex07_boldly_go.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.ex07-boldly-go) 2 | 3 | 4 | ;; EX07: Lesson Goals 5 | ;; 6 | ;; - Quickly see how to start and architect your own project. 7 | ;; - namespaces, dependencies, project structuring etc... 8 | ;; 9 | ;; - To be able to run your project as a binary (jar) like this: 10 | ;; 11 | ;; java -jar ./path/to/the_library_jarfile.jar "arg1" "arg2" "arg3" 12 | 13 | 14 | 15 | ;; Your First Clojure Library 16 | ;; - Let's turn code from ex06 into a library for planetary exploration. 17 | ;; - Follow the procedure below, step by step. 18 | ;; 19 | ;; 20 | ;; * Use leiningen to create a new project skeleton. 21 | ;; 22 | ;; - Open a terminal session and execute: 23 | ;; 24 | ;; lein new planet_coloniser 25 | ;; 26 | ;; 27 | ;; * Open the directory in your IDE and observe its structure. 28 | ;; 29 | ;; 30 | ;; * Create a `utils` directory in which to put I/O utility functions. 31 | ;; (Putting utility functions in `utils` is only a convention, 32 | ;; not a rule.) 33 | ;; 34 | ;; - Terminal: 35 | ;; mkdir src/planet_coloniser/utils 36 | ;; 37 | ;; - Or, in your IDE: 38 | ;; - right-click on src/planet_coloniser, and create new folder 39 | ;; 40 | ;; 41 | ;; * Inside this new `utils` directory, create two new files: 42 | ;; - `ingest.clj` and 43 | ;; - `export.clj` 44 | ;; 45 | ;; - Again, you can right-click on your `utils` dir in your IDE, 46 | ;; and create New File from the pop-up menu. 47 | ;; 48 | ;; 49 | ;; * Update `ingest.clj` with ingest utility functions. 50 | ;; 51 | ;; - Open the file and add this "ns declaration" at the top: 52 | ;; 53 | ;; (ns planet-coloniser.utils.ingest) 54 | ;; 55 | ;; - Observe the dir name is planet_coloniser, but the ns 56 | ;; declaration has planet-coloniser. This is the convention: 57 | ;; - hyphens separate words in ns names 58 | ;; - dots separate directories and files in ns names 59 | ;; - underscores from dir or file names become hyphens in ns names 60 | ;; - and ns names are all lower case 61 | ;; 62 | ;; - Copy-paste the "input" function definitions from ex06, 63 | ;; below the ns declaration. 64 | ;; 65 | ;; 66 | ;; * Update `ingest.clj`, and project, for external dependency 67 | ;; 68 | ;; - `ingest` uses `clojure.data.json`, which is an external 69 | ;; dependency that we have to specify explicitly. 70 | ;; 71 | ;; - Inside `project.clj`, update :dependencies value to look like: 72 | ;; 73 | ;; :dependencies [[org.clojure/clojure "1.10.0" ; latest as of 01 Jan 2019 74 | ;; [org.clojure/data.json "0.2.6"]] ; add for `ingest` 75 | ;; 76 | ;; - Inside `injest.clj`, update the ns declaration to look like: 77 | ;; 78 | ;; (ns planet-coloniser.utils.ingest 79 | ;; (:require [clojure.data.json :as json] 80 | ;; [clojure.java.io :as io])) 81 | ;; 82 | ;; 83 | ;; * Update `export.clj`. Open the file and: 84 | ;; 85 | ;; - Add the appropriate "ns declaration" at the top 86 | ;; 87 | ;; - Copy-paste only the `write-out-json-file` function here. 88 | ;; 89 | ;; - Later, we will port code from the other function to another file. 90 | ;; 91 | ;; - Also ensure, the ns form looks like this: 92 | ;; 93 | ;; (ns planet-coloniser.utils.ingest 94 | ;; (:require [clojure.data.json :as json] 95 | ;; [clojure.java.io :as io])) 96 | ;; 97 | ;; 98 | ;; * Create `sensor_processor.clj`, for our core "pure" logic: 99 | ;; 100 | ;; - Create it under `src/planet_coloniser/` 101 | ;; 102 | ;; - Add the appropriate ns declaration 103 | ;; 104 | ;; - Copy-paste all the pure functions in there 105 | ;; 106 | ;; - Update the ns form to also require clojure.walk 107 | ;; 108 | ;; 109 | ;; * Update `src/planet_coloniser/core.clj` 110 | ;; - This will contain the entry point for the outside world, 111 | ;; into our planet processor. 112 | ;; 113 | ;; - Delete the dummy function from the namespace 114 | ;; 115 | ;; - Update the `ns` declaration to look like this: 116 | ;; 117 | ;; (ns planet-coloniser.core 118 | ;; (:gen-class) ; add this, and the :require expression below: 119 | ;; (:require [planet-coloniser.sensor-processor :as sensproc] 120 | ;; [planet-coloniser.utils.ingest :as ingest] 121 | ;; [planet-coloniser.utils.export :as export])) 122 | ;; 123 | ;; - Copy `ingest-export-sensor-data!` from ex06 124 | ;; 125 | ;; - Rename it to `-main`. 126 | ;; 127 | ;; - Now, make the body of `-main` look like the function below: 128 | ;; - notice prefixes to functions, like export/, ingest/, sensproc/ 129 | ;; - Why do we do this? 130 | ;; 131 | ;; (defn -main 132 | ;; [data-dir source-data-files dest-data-file] 133 | ;; (let [source-data-files (ingest/ingest-json-file! data-dir 134 | ;; source-data-files) 135 | ;; export-as-json (partial export/write-out-json-file! 136 | ;; data-dir 137 | ;; dest-data-file)] 138 | ;; (export-as-json 139 | ;; (sensproc/denormalise-planetary-data 140 | ;; (ingest/gather-all-sensor-data! data-dir 141 | ;; source-data-files))))) 142 | ;; 143 | ;; 144 | ;; * Let's bring in sensor data, for convenience: 145 | ;; - Copy over `sensor_data` directory from clojure-by-example. 146 | ;; - From: 147 | ;; /Path/To/clojure-by-example/resources/sensor_data/ 148 | ;; - To: 149 | ;; /Path/To/planet_coloniser/resources/sensor_data/ 150 | ;; 151 | ;; 152 | ;; * In the new `sensor_data` directory, create a new JSON file: 153 | ;; 154 | ;; - call it `sensor_data_files.json` 155 | ;; 156 | ;; - add the following JSON to it: 157 | ;; 158 | ;; {"planets":"planet_detector.json", 159 | ;; "moons":"moon_detector.json", 160 | ;; "atmosphere":"atmospheric_detector.json"} 161 | ;; 162 | ;; 163 | ;; * Let's make the project runnable: 164 | ;; 165 | ;; - Update `project.clj` to specify entry point. 166 | ;; 167 | ;; - After :dependencies, add the location of the main function: 168 | ;; 169 | ;; :main planet-coloniser.core 170 | ;; 171 | ;; 172 | ;; * Check if the project has indeed become runnable: 173 | ;; 174 | ;; - In the terminal, cd to the root directory of your 175 | ;; `planet_coloniser` project. 176 | ;; 177 | ;; - Use leiningen to run the library: 178 | ;; 179 | ;; lein run "resources/sensor_data/" "sensor_data_files.json" "lein_out.json" 180 | ;; 181 | ;; - Did that work? 182 | ;; - Check resources/lein_out.json 183 | ;; 184 | ;; 185 | ;; * Let's bump our project version to `0.1.0` :-D 186 | ;; - In project.clj, delete `-SNAPSHOT` from the project version 187 | ;; (see it at the top of the project definition) 188 | ;; 189 | ;; 190 | ;; * Finally, let's prep the project for building a java executable. 191 | ;; - Update `project.clj` for Ahead-Of-Time compilation, 192 | ;; required in order to produce a standalone Java executable jar. 193 | ;; 194 | ;; - Below :main, add this: 195 | ;; 196 | ;; :aot :all 197 | ;; 198 | ;; - Use leiningen to Build the jar: 199 | ;; - In your terminal, cd into the root of your project, 200 | ;; and execute: 201 | ;; 202 | ;; lein uberjar 203 | ;; 204 | ;; - Observe the build output in your terminal. 205 | ;; 206 | ;; - Do you see two jar files created under `planet_coloniser/target`? 207 | ;; 208 | ;; 209 | ;; * Now run the "standalone" jar as: 210 | ;; 211 | ;; java -jar target/planet_coloniser-0.1.0-standalone.jar "resources/sensor_data/" "sensor_data_files.json" "jar_out.json" 212 | ;; 213 | ;; - Did that work? 214 | ;; - Check the output file 215 | ;; 216 | ;; 217 | ;; * If you've reached this far... 218 | ;; - Congratulations, you just built a brand new Clojure project 219 | ;; from scratch! 220 | ;; 221 | ;; 222 | 223 | 224 | ;; HOMEWORK: 225 | ;; 226 | ;; * Why did we have to do all this? 227 | ;; Ben Vandgrift has a nice explanation at: 228 | ;; "Clojure: 'Hello World' from the Command Line" 229 | ;; http://ben.vandgrift.com/2013/03/13/clojure-hello-world.html 230 | -------------------------------------------------------------------------------- /src/clojure_by_example/ex08_but_before_we_go.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.ex08-but-before-we-go) 2 | 3 | ;; But before we boldly go, here are some resources to help us on our journey! 4 | 5 | ;; Check out the official website: https://clojure.org 6 | 7 | ;; It has been accumulating many useful tutorials, guides, book references, 8 | ;; community resources, and information about companies using Clojure. 9 | 10 | ;; Also, here are a few things we really like and find useful to aid understanding 11 | ;; at various levels; from the philosophical to the here and now: 12 | 13 | ;; Talks/Philosphy: 14 | ;; https://www.youtube.com/watch?v=wASCH_gPnDw -- Inside Clojure with Brian Beckman and Rich Hickey 15 | ;; https://www.infoq.com/presentations/Value-Values -- The Value of Values by Rich Hickey 16 | ;; https://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey -- Persistent Data Structures and Managed References by Rich Hickey 17 | ;; https://www.infoq.com/presentations/Simple-Made-Easy -- Simple Made Easy by Rich Hickey 18 | 19 | ;; Debugging: 20 | ;; 21 | ;; - "Inside-out"/"Bottom-up" REPL-driven debugging, particularly how 22 | ;; Stu Halloway explains, in "Debugging With the Scientific Method". 23 | ;; https://www.youtube.com/watch?v=FihU5JxmnBg 24 | ;; 25 | ;; - Aphyr's post is neat! 26 | ;; - Scroll down to the "Debugging Clojure" section: 27 | ;; https://aphyr.com/posts/319-clojure-from-the-ground-up-debugging 28 | 29 | ;; Handy REPL utils: 30 | ;; 31 | ;; - clojure.repl/ 32 | ;; - source, doc, pp, pprint, print-table 33 | ;; - see the bottom of the `clojure-by-example.utils.core` ns 34 | ;; 35 | ;; - *1, *2, *3, *e 36 | ;; 37 | ;; - A sane REPL workflow (buffer/file-based, rather than inside REPL) 38 | ;; 39 | -------------------------------------------------------------------------------- /src/clojure_by_example/fun/inspect_nasa_planets.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.fun.inspect-nasa-planets 2 | (:require [net.cgrand.enlive-html :as html] 3 | [clojure.string :as cs] 4 | [clojure.inspector :as inspect])) 5 | 6 | ;; Warning: 7 | ;; Code here may not be "professional" grade. 8 | ;; 9 | ;; This is just some fun messing around with some planetary data 10 | ;; published by NASA. 11 | ;; 12 | ;; Important: 13 | ;; Be kind to their servers, if you start tinkering with this code. 14 | ;; 15 | ;; First download a copy of the HTML pages to local disk, 16 | ;; and replace the URL with the absolute path to the file on disk. 17 | ;; Everything else should work the same. 18 | 19 | 20 | (def planets-to-earth-ratios-table 21 | {:table-name :planet-to-earth-ratios 22 | :table-data-file "https://nssdc.gsfc.nasa.gov/planetary/factsheet/" 23 | :num-cols 10 24 | :num-rows 20 25 | :rows-label-path [:table :tr [:td html/first-child] :a] 26 | :cols-label-path [:table [:tr html/first-child] :td :a] 27 | :stats-path [:table :tr (html/attr= :align "center")]}) 28 | 29 | 30 | (def small-worlds-table 31 | {:table-name :small-worlds 32 | :table-data-file "https://nssdc.gsfc.nasa.gov/planetary/factsheet/galileanfact_table.html" 33 | ;; Of 12 cols, 11 have data, one is empty and must be removed 34 | ;; prior to processing. 35 | :num-cols 11 36 | :num-rows 16 37 | :rows-label-path [:table :tr [:td html/first-child] :b] 38 | :cols-label-path [:table [:tr html/first-child] :td :b] 39 | :stats-path [:table :tr (html/attr= :align "center")]}) 40 | 41 | 42 | (defn file->html-resource 43 | "Given a path to a file, produce an Enlive 'html resource'." 44 | [file-or-url] 45 | (html/html-resource 46 | (java.io.StringReader. 47 | (slurp file-or-url)))) 48 | 49 | 50 | (comment 51 | (let [checker (fn [table xy-key] 52 | (map :content 53 | (html/select 54 | (file->html-resource (:table-data-file table)) 55 | (xy-key table)))) 56 | check-pte (partial checker planets-to-earth-ratios-table) 57 | check-sw (partial checker small-worlds-table) 58 | check-all (juxt check-pte check-sw)] 59 | [(check-all :rows-label-path) 60 | (check-all :cols-label-path)])) 61 | 62 | 63 | (defn massage-table-info 64 | [{:keys [table-data-file num-cols num-rows] :as table-info}] 65 | (assoc table-info 66 | :num-stats (* num-cols num-rows) 67 | ;; Maybe not such a good idea to inject html-resource. 68 | ;; It could get really big. But then again, how to design the 69 | ;; api so that we can avoid parsing the DOM repeatedly? 70 | :html-resource (file->html-resource table-data-file))) 71 | 72 | 73 | (comment 74 | (massage-table-info planets-to-earth-ratios-table)) 75 | 76 | 77 | (defn extract-raw-data 78 | [{:keys [html-resource stats-path 79 | rows-label-path cols-label-path] 80 | :as table-info}] 81 | (let [extract (partial html/select html-resource)] 82 | (dissoc (assoc table-info 83 | :col-labels (extract cols-label-path) 84 | :row-labels (extract rows-label-path) 85 | :stats-by-rows (extract stats-path)) 86 | :html-resource))) 87 | 88 | 89 | (comment 90 | (-> planets-to-earth-ratios-table 91 | massage-table-info 92 | extract-raw-data)) 93 | 94 | 95 | (def cleanup-table-data nil) 96 | 97 | (defmulti cleanup-table-data :table-name) 98 | 99 | 100 | (defmethod cleanup-table-data :planet-to-earth-ratios 101 | [{:keys [col-labels row-labels stats-by-rows 102 | num-cols num-stats] 103 | :as raw-table-info}] 104 | (let [keywordize (comp keyword 105 | #(cs/replace % #"\s+" "-") 106 | cs/lower-case) 107 | cleanup-labels #(->> % 108 | (map :content) 109 | flatten 110 | (map keywordize))] 111 | ;; replace labels and stats with cleaned up versions 112 | (assoc raw-table-info 113 | :row-labels (cleanup-labels row-labels) 114 | :col-labels (cleanup-labels col-labels) 115 | :stats-by-rows ((comp #(map flatten %) 116 | #(partition num-cols %) 117 | #(map :content %) 118 | #(take num-stats %) 119 | #(drop num-cols %)) 120 | stats-by-rows)))) 121 | 122 | 123 | (defmethod cleanup-table-data :small-worlds 124 | [{:keys [col-labels row-labels stats-by-rows 125 | num-cols num-stats] 126 | :as raw-table-data}] 127 | (let [keywordize (comp keyword 128 | #(cs/replace % #"\s+" "-") 129 | cs/lower-case) 130 | cleanup-cols #(->> % 131 | (map :content) 132 | flatten 133 | ((partial map 134 | (fn [s] (cs/replace s #"�" "")))) 135 | rest ; get rid of empty column's label 136 | (map keywordize)) 137 | massage-row-label #(cond (#{:a} (:tag %)) 138 | (:content %) 139 | (#{:sup} (:tag %)) 140 | (apply str "^" (:content %)) 141 | :else %) 142 | cleanup-rows (comp rest butlast ; get rid of empty row labels 143 | (partial 144 | map (comp keywordize 145 | #(cs/replace % #"_|\(|\)" "") 146 | #(apply str %) 147 | flatten 148 | (partial map massage-row-label) 149 | :content))) 150 | cleanup-stats (comp (partial partition num-cols) 151 | (partial filter #(not= "�" %)) 152 | flatten 153 | #(map :content %) 154 | #(take num-stats %) 155 | #(drop (inc num-cols) %))] 156 | (assoc raw-table-data 157 | :col-labels (cleanup-cols col-labels) 158 | :row-labels (cleanup-rows row-labels) 159 | :stats-by-rows (cleanup-stats stats-by-rows)))) 160 | 161 | 162 | (comment 163 | (-> planets-to-earth-ratios-table 164 | massage-table-info 165 | extract-raw-data 166 | cleanup-table-data) 167 | 168 | (-> small-worlds-table 169 | massage-table-info 170 | extract-raw-data 171 | cleanup-table-data)) 172 | 173 | 174 | (defn transpose-matrix 175 | "The matrix must be a vector of vectors, where all nested vectors 176 | have the same number of items. " 177 | [matrix] 178 | (apply map vector matrix)) 179 | 180 | 181 | (defn planet-properties 182 | [{:keys [col-labels row-labels stats-by-rows] :as table-data}] 183 | (let [stats-by-cols (transpose-matrix stats-by-rows) 184 | denormalise-row (fn [m k v] 185 | (assoc m k (zipmap row-labels v)))] 186 | (reduce-kv denormalise-row 187 | {} 188 | (zipmap col-labels stats-by-cols)))) 189 | 190 | 191 | (defn planets-rel-earth 192 | [] 193 | (-> planets-to-earth-ratios-table 194 | massage-table-info 195 | extract-raw-data 196 | cleanup-table-data 197 | planet-properties)) 198 | 199 | 200 | (defn small-worlds 201 | [] 202 | (-> small-worlds-table 203 | massage-table-info 204 | extract-raw-data 205 | cleanup-table-data 206 | planet-properties)) 207 | 208 | 209 | (comment 210 | ;; Some repl-inspection 211 | 212 | ;; Try stuff 213 | (:jupiter (planets-rel-earth)) 214 | 215 | (:callisto (small-worlds)) 216 | 217 | ;; repl-print out the table like it appears in the HTML page 218 | (let [{:keys [col-labels row-labels stats-by-rows]} 219 | (-> planets-to-earth-ratios-table 220 | massage-table-info 221 | extract-raw-data 222 | cleanup-table-data) 223 | col-labels+ (into [:prop] col-labels) 224 | table (map (partial zipmap col-labels+) 225 | (map (fn [x xs] (into [x] xs)) 226 | row-labels stats-by-rows))] 227 | (clojure.pprint/print-table col-labels+ table)) 228 | 229 | ;; repl-print the inverted table (relative to input table) 230 | (let [table-data (planets-rel-earth) 231 | massage-for-pprint (fn [xs k v] (conj xs (assoc v :planet k))) 232 | table-data (reduce-kv massage-for-pprint [] table-data)] 233 | (clojure.pprint/print-table 234 | [:planet :mass :number-of-moons :distance-from-sun] 235 | table-data)) 236 | 237 | ;; walk the tree, in a swing UI 238 | (inspect/inspect-tree (small-worlds))) 239 | -------------------------------------------------------------------------------- /src/clojure_by_example/fun/workshop_fmt.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-by-example.fun.workshop-fmt 2 | (:require [clojure.edn :as edn] 3 | [clojure.java.io :as io] 4 | [rewrite-clj.zip :as z]) 5 | (:import java.io.PushbackReader)) 6 | 7 | (def relative-src-path "src/clojure_by_example/") 8 | (def relative-dest-path (str relative-src-path "workshop/")) 9 | (def workshop-file-pattern (re-pattern "ex.*.clj")) 10 | 11 | 12 | (defn elide-if-comment 13 | [zloc] 14 | (if (= (z/value zloc) 'comment) 15 | (-> zloc z/up z/remove) 16 | zloc)) 17 | 18 | 19 | (defn elide-comment-forms 20 | [zloc] 21 | (if (z/end? zloc) 22 | zloc 23 | (recur (-> zloc elide-if-comment z/next)))) 24 | 25 | 26 | (defn workshop-files! 27 | [sourcedir file-pattern] 28 | (->> (io/file sourcedir) 29 | .listFiles 30 | (filter #(not (.isDirectory %))) 31 | (map #(.getName %)) 32 | (filterv (fnil #(re-find file-pattern %) "")))) 33 | 34 | 35 | (defn spit-root! 36 | [zloc outfile] 37 | (with-open [w (io/writer outfile :encoding "UTF-8")] 38 | (z/print-root zloc w))) 39 | 40 | 41 | (defn prep-workshop-code! 42 | ([infile outfile] 43 | (-> (z/of-file infile) 44 | elide-comment-forms 45 | (spit-root! outfile))) 46 | ([infile indir outdir] 47 | (prep-workshop-code! 48 | (str indir infile) 49 | (str outdir infile)))) 50 | 51 | 52 | (comment 53 | ;; Try one... 54 | (prep-workshop-code! "ex06_full_functional_firepower.clj" 55 | relative-src-path 56 | relative-dest-path) 57 | 58 | ;; Review output using: 59 | ;; ls -1 | grep ex | xargs -I {} diff -s {} "workshop/"{} 60 | (doseq [f (sort (workshop-files! relative-src-path 61 | workshop-file-pattern))] 62 | (prep-workshop-code! f 63 | relative-src-path 64 | relative-dest-path)) 65 | ) 66 | 67 | 68 | (comment 69 | ;; Experiment with file as EDN 70 | (defn read-forms [file] 71 | ;; https://stackoverflow.com/questions/39976680/reading-another-clojure-program-as-a-list-of-s-expressions 72 | (let [rdr (-> file io/file io/reader PushbackReader.) 73 | sentinel (Object.)] 74 | (loop [forms []] 75 | (let [form (edn/read {:eof sentinel} rdr)] 76 | (if (= sentinel form) 77 | forms 78 | (recur (conj forms form))))))) 79 | ) 80 | 81 | (comment 82 | ;; Experiments with zippers 83 | 84 | ((comp z/child-sexprs z/down) (z/of-file "foobar.clj")) 85 | 86 | (spit-root! 87 | (z/prewalk (z/of-file "foobar.clj") 88 | #(= (z/value %) 'comment) 89 | z/remove) 90 | "foobar-cleaned.clj") 91 | 92 | 93 | (let [zloc (z/of-file (str sourcedir "foobar.clj")) 94 | outfile (str sourcedir "foobar-cleaned.clj")] 95 | (with-open [w (clojure.java.io/writer outfile :encoding "UTF-8")] 96 | (loop [znodes zloc] 97 | (if (z/end? znodes) 98 | (z/print-root znodes w) 99 | (recur (if (= (z/value znodes) 'comment) ; for fanciness, this could be a comment macro by another name, like `extra-explanation' 100 | ((comp z/next z/remove z/up) znodes) 101 | (z/next znodes))))))) 102 | ) 103 | -------------------------------------------------------------------------------- /src/clojure_by_example/workshop/.gitignore: -------------------------------------------------------------------------------- 1 | *.clj 2 | -------------------------------------------------------------------------------- /src/clojure_by_example/workshop/README.txt: -------------------------------------------------------------------------------- 1 | DO NOT commit clj files here. 2 | 3 | This directory is a target for generated clj source files. 4 | 5 | A reformatter will read "full featured" source meant for at-home use, 6 | and dump an elided, leaner version here for in-class use (for the 7 | teacher's convenience). 8 | --------------------------------------------------------------------------------