diff --git a/.gitignore b/.gitignore index e436df5..fbc11d8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,10 @@ pom.xml.asc .hg/ .setup.sh profiles.clj +.idea +.m2-for-inclojure +*.iml +.lsp +.cpcache +.clj-kondo +.calva diff --git a/README.md b/README.md index 61b43c4..730b1dc 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,125 @@ -# Clojure By Example +- [Introduction](#introduction) + - [Intended usage](#intended-usage) + - [Contributions](#contributions) + - [Workshop Goals](#workshop-goals) + - [Workshop Anti-Goals](#workshop-anti-goals) +- [Suggested learning mindset](#suggested-learning-mindset) +- [Setup Instructions](#setup-instructions) +- [Course Design Philosophy](#course-design-philosophy) +- [Credits](#credits) +- [Copyright and License](#copyright-and-license) -What could one do with just a _little_ bit of Clojure? - -## Intended usage - - Support a 1-day guided workshop for programmers new to Clojure (not absolute programming beginners). - - Also function as at-home learning material for said programmers. - - The `master` branch is heavily commented, for at-home use - - A `solutions` branch will be available, as a companion to `master`. - But don't peek at it in advance! - - Ignore the `workshop-code` branch. It is only for workshop use, - and subject to deletion/re-creation. - -## Contributions - - If you find bugs or errors, please send a PR (but please - don't change the course structure or pedagogy). - -## Workshop Goals - - Acquire a "feel" of Clojure, for further self-study/exploration. - - Learn how Clojurists usually think with Clojure to solve problems. - - See how it's not so hard to do surprisingly powerful things with a - mere handful of "primitive" functions, data structures, and ideas. - -## Workshop Anti-Goals - - Try to explain Functional Programming theory or Clojure's innards. - (Many free and paid tutorials and books do so very well.) - - Try to fully cover Clojure primitives/features. (That's homework!) - - Devolve into language wars, editor wars, syntax wars, type wars... - (Life's too short, people.) - - Focus too much on tooling or operational things. (At least not - while there's fun to be had!) +# Introduction +This workshop aims to get your brain and fingers accustomed to just enough of +the [Clojure](https://clojure.org) programming language to start doing useful things with it. -# Suggested learning mindset - - Think of this as an exercise in "constrained creativity". - - Ignore details, achieve much with as little know-how as possible. - - Focus on what things do; not what they are, or why they are. - - Inform your _intuition for doing things_, and then use that to - dive deeper into all the juicy details at your own pace, later. +In other words, "What could one do with just a _little_ bit of Clojure?". -Take what is useful, discard the rest. +## What is Clojure? +Clojure is an interactive functional programming language that can run on many platforms +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/)! -# Setup Instructions +It is [used by software teams worldwide](https://clojure.org/community/success_stories#) to deliver +high-value software systems at giant companies like Apple, Walmart, to "decacorns" +like GoJek, Nubank, to a wide array of startups, and one-person businesses like Partsbox.com. -It's a liiitle bit of work. But not too bad. +Its interactivity and dynamism foster a sense of playfulness that attracts all manner +of [creative makers](http://radar.oreilly.com/2015/05/creative-computing-with-clojure.html)---hobbyist as well as serious artists and musicians. -Just do the following one by one, and you should be fine. +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. -## Java 8 +## Intended usage -You will need Java to work with this Clojure workshop content. +- Support a 1-day guided workshop for programmers new to Clojure (not absolute programming beginners). +- Also function as at-home learning material for said programmers. +- The `master` branch is heavily commented, for at-home use. +- A `solutions` branch will be available, as a companion to `master`. + But don't peek at it in advance! +- You may see a `workshop-code` branch. Ignore it. It is meant only for + workshop use, and is subject to deletion/re-creation. +- Incidentally, if you landed here while searching for Hirokuni Kim's + "[Clojure By Example](https://kimh.github.io/clojure-by-example/)", well, follow the link! -First, make sure you have Java 8. +## Contributions - - Run `java -version` in your terminal. - - If Java is not installed, please [download and install Java 8 from here](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html). - - Once you are done, `java -version` should show you a Java 1.8.x version. +- If you find bugs or errors, please send a PR (but please + don't change the course structure or pedagogy). -Notes: +## Workshop Goals - - If you have Java 9, that should be OK too. - - The LightTable editor is known to break with Java 9. Use Java 8 instead. - - We have not tested this project with Java 7. +- Acquire a "feel" of Clojure, for further self-study/exploration. +- Learn how Clojurists usually think with Clojure to solve problems. +- See how it's not so hard to do surprisingly powerful things with a + mere handful of "primitive" functions, data structures, and ideas. +- Get you started with a good development setup and workflow that will + serve you well if (when) you continue to program with Clojure, as a + hobby, or at work! +## Workshop Anti-Goals -## Leiningen +- Try to explain Functional Programming theory or Clojure's innards. + (Many free and paid tutorials and books do so very well.) +- Try to fully cover Clojure primitives/features. (That's homework!) +- Devolve into language wars, editor wars, syntax wars, type wars... + (Life's too short, people.) +- Focus too much on tooling or operational things. (At least not + while there's fun to be had!) -Follow [Leiningen setup instructions here](https://leiningen.org/). +# Suggested learning mindset -### Fire up a REPL +- Think of this as an exercise in "constrained creativity". +- Ignore details, achieve much with as little know-how as possible. +- Focus on what things do; not what they are, or why they are. +- Inform your _intuition for doing things_, and then use that to + dive deeper into all the juicy details at your own pace, later. - - Clone this project - - Open your terminal, and do the following. - - `cd` into this project's root directory - - Use `lein repl` command to start a REPL with Leiningen. - - Wait for it... the REPL will start and print out a message with some - useful information - - Locate the `port` and `host` information in the message. We will need this information soon. +Take what is useful, discard the rest. -Note: +# Setup Instructions - - [Boot](http://boot-clj.com/) should be fine too, but we have not tested this project with it. +Just do the following one by one, and you should be fine. +## Java -## Code Editor and Tooling +You need Java installed. -Set up an editor and figure out how to evaluate Clojure code with it. +- Run `java -version` in your terminal. +- If Java is not installed, please [download and install Java from here](https://adoptopenjdk.net/). +- Any version should do, but prefer Java 8 or higher. We have not tested + this project with Java 7. +- Once you are done, `java -version` should show you a Java version. -### LightTable +## VSCode + Calva -We used LightTable for our workshop. We suggest you do so too, unless of course, you have already set up your favourite editor for Clojure development. Avoid [bikeshedding](http://catb.org/jargon/html/B/bikeshedding.html) editors. Just complete the workshop first! +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! - - You may install LightTable from the [official website](http://lighttable.com/). - - But you must have Java 8. LightTable breaks with Java 9. - - On Mac OS, you may have to allow running the app in your security preferences to be able to open it. +- Download and Install [VSCode](https://code.visualstudio.com/). +- Open VSCode and complete the initialization process. +- Open the "Extensions" Tab and search for "Calva", Install the "Calva: + Clojure & ClojureScript Interactive Programming" extension. +- Alternatively you can visit the [Calva page](https://marketplace.visualstudio.com/items?itemName=betterthantomorrow.calva) to install it. Once installed: - - Use LightTable's file menu to open this project. - - In the left pane, navigate down to the first file `ex00...`, under the `src` folder. - - Under `View` menu, click `Connections`. A right pane should open. - - Under `Add Connection`, click `Clojure (remote REPL)` and complete the port number. Recall host:port information was printed to the terminal when you fired up a REPL in the previous section. - - In the `ex00..` file, scroll down a little, till you see `(+ 1 2)`. - - Place your cursor after the closing parenthesis `)` and hit Ctrl+Enter (Win/Linux), or Cmd+Enter (Mac). - - You should see `3` appear in-line. This means you successfully connected and evaluated an expression. - - Now you may start from the top of ex00 and work through the material. - -Also keep [LightTable's documentation](http://docs.lighttable.com/tutorials/full/) handy in case you need editor help, as you solve the workshop material. - - -Optionally, add Parinfer for easier editing: - - - In LightTable, go to View -> Plugin Manager and search for "parinfer". - - Install the Parinfer plugin by Maurício Szabo. - - Parinfer is an editing system for Clojure that makes it easy for you to move Clojure code around without unbalancing parentheses. - - We recommend going through the [Parinfer documentation here](https://shaunlebron.github.io/parinfer/). But don't get stuck there, just keep it handy. +- Clone the repository on your machine. +- In VSCode Use File > Open Folder... and open the cloned folder. +- Notice that Calva activates. +- 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. +- Type "Calva: Start Project REPL" and choose "Calva: Start a Project REPL and Connect (aka Jack-In)" from the list. + - 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. + - VSCode will create a new pane called 'output.calva-repl' and you will see `clj꞉user꞉>` prompt in that screen. +- You have a working REPL now! +- Keep the [Paredit guide](https://calva.io/paredit/) handy, editing code will require some understanding of paredit. - -### Alternative Starter Kits: - -If you can't use LightTable for some reason (like can't downgrade to Java 8 from Java 9). You may try one of these. Although we haven't tested with these setups, the workshop material should work fine. - - - A [snazzy setup with Atom](https://medium.com/@jacekschae/slick-clojure-editor-setup-with-atom-a3c1b528b722). - - Brave Clojure walks you through [a basic Emacs setup for learning Clojure](https://www.braveclojure.com/basic-emacs/). - - -### Your favourite editor: +## Your favourite editor: 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: - - ["IDEs and Editors" at dev.clojure.org](https://dev.clojure.org/display/doc/IDEs+and+Editors) - - ["Essentials" at clojure-doc.org](http://clojure-doc.org/articles/content.html#essentials) - - [Christopher Bui says...](https://cb.codes/what-editor-ide-to-use-for-clojure/) - +- ["Clojure Tools" at clojure.org](https://clojure.org/community/tools) +- ["Essentials" at clojure-doc.org](http://clojure-doc.org/articles/content.html#essentials) +- [Christopher Bui says...](https://web.archive.org/web/20181223213500/https://cb.codes/what-editor-ide-to-use-for-clojure/) # Course Design Philosophy @@ -149,17 +136,17 @@ satisfies and empowers us deeply. So, may you stay small and achieve important things. Live long, and prosper. -\\\\//_ - +\\\\//\_ # Credits - - [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. - - [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. - - All the workshop participants, and the many Clojurists who generously donated their time to make it successful. - - [inclojure-org](https://github.com/inclojure-org) for being the umbrella under which this work happened. + +- [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. +- [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. +- All the workshop participants, and the many Clojurists who generously donated their time to make it successful. +- [inclojure-org](https://github.com/inclojure-org) for being the umbrella under which this work happened. ## Copyright and License -Copyright © 2017-2018 [IN/Clojure](http://inclojure.org/). +Copyright © 2017-2024 [IN/Clojure](http://inclojure.org/). Distributed under the [MIT license](https://github.com/inclojure-org/clojure-by-example/blob/master/LICENSE). diff --git a/deps.edn b/deps.edn new file mode 100644 index 0000000..394857e --- /dev/null +++ b/deps.edn @@ -0,0 +1,5 @@ +{:paths ["src" "resources"], + :deps {org.clojure/clojure {:mvn/version "1.10.0"} + org.clojure/data.json {:mvn/version "0.2.6"} + enlive/enlive {:mvn/version "1.1.6"} + rewrite-clj/rewrite-clj {:mvn/version "0.6.1"}}} diff --git a/project.clj b/project.clj deleted file mode 100644 index 8e26e27..0000000 --- a/project.clj +++ /dev/null @@ -1,13 +0,0 @@ -(defproject clojure_by_example "0.1.0-SNAPSHOT" - :description "A workshop to introduce Clojure, to programmers new to Clojure." - :url "https://github.com/inclojure-org/clojure-by-example" - :license {:name "MIT" - :url "https://opensource.org/licenses/MIT"} - :dependencies [[org.clojure/clojure "1.8.0"]] - ;; LightTable dependencies - ;; https://github.com/LightTable/Clojure/#connect-to-remote-nrepl - :profiles {:dev {:dependencies [[lein-light-nrepl "0.3.3"] - [enlive "1.1.6"] - [cheshire "5.8.0"] - [criterium "0.4.4"]]}} - :repl-options {:nrepl-middleware [lighttable.nrepl.handler/lighttable-ops]}) diff --git a/src/clojure_by_example/data/planets.clj b/src/clojure_by_example/data/planets.clj new file mode 100644 index 0000000..eac8756 --- /dev/null +++ b/src/clojure_by_example/data/planets.clj @@ -0,0 +1,66 @@ +(ns clojure-by-example.data.planets) + +(def target-planets + [{:pname "Mercury" + :mass 0.055 + :radius 0.383 + :moons 0 + :gravity 0.378 + :surface-pressure 0 + :surface-temp-deg-c {:low -170 :high 449} + :rocky? true + :atmosphere {}} ; empty hash map means no atmosphere + + {:pname "Venus" + :mass 0.815 + :radius 0.949 + :moons 0 + :gravity 0.907 + :surface-pressure 92 + :surface-temp-deg-c {:low 465 :high 465} + :rocky? true + :atmosphere {:carbon-dioxide 96.45 :nitrogen 3.45 + :sulphur-dioxide 0.015 :traces 0.095}} + + {:pname "Earth" + :mass 1 + :radius 1 + :moons 1 + :gravity 1 + :surface-pressure 1 + :surface-temp-deg-c {:low -89 :high 58} + :rocky? true + :atmosphere {:nitrogen 78.08 :oxygen 20.95 :carbon-dioxide 0.4 + :water-vapour 0.10 :argon 0.33 :traces 0.14}} + + {:pname "Mars" + :mass 0.107 + :radius 0.532 + :moons 2 + :gravity 0.377 + :surface-pressure 0.01 + :surface-temp-deg-c {:low -125 :high 20} + :rocky? true + :atmosphere {:carbon-dioxide 95.97 :argon 1.93 :nitrogen 1.89 + :oxygen 0.146 :carbon-monoxide 0.056 :traces 0.008}} + + {:pname "Chlorine Planet" + :mass 2.5 + :radius 1.3 + :moons 4 + :gravity 1.5 + :surface-pressure 1 + :surface-temp-deg-c {:low -42 :high 24} + :rocky? true + :atmosphere {:chlorine 100.0}} + + {:pname "Insane Planet" + :mass 42 + :radius 4.2 + :moons 42 + :gravity 10 + :surface-pressure 420 + :surface-temp-deg-c {:low 750 :high 750} + :rocky? false + :atmosphere {:sulphur-dioxide 80.0 :carbon-monoxide 10.0 + :chlorine 5.0 :nitrogen 5.0}}]) diff --git a/src/clojure_by_example/ex00_introduction.clj b/src/clojure_by_example/ex00_introduction.clj index 39737d0..6f4fd88 100644 --- a/src/clojure_by_example/ex00_introduction.clj +++ b/src/clojure_by_example/ex00_introduction.clj @@ -6,154 +6,201 @@ ;; - Begin in this "ex00..." file, and work through it step by step. ;; - Once you are done with "ex00...", open the next file and repeat. ;; - Keep going this way, until you have worked through all the files. - +;; - Once done with a file, read the recap and try any additional +;; problem sets listed there, to get more practice. ;; EX00: LESSON GOAL: -;; - A very quick intro to Clojure syntax, just to familiarize your -;; eyes with it. -;; -;; - Don't get stuck here! -;; - Run through it once, try evaluating expressions of interest -;; and move on to EX01. -;; - Your eyes and brain will adjust fairly quickly, as you -;; work through the examples to follow. +;; - Drill some Clojure basics, required for the sections +;; that follow (and generally, to help follow code +;; in the wild) +;; - Familiarize one's eyes with Clojure syntax +;; - Understand Clojure's evaluation model +;; - Start using an interactive development workflow +;; right away --- this is what it means to be a +;; "Dynamic" programming language (not to be confused +;; with dynamically typed languages.) + + +;; All Clojure code is composed of "expressions": +;; - And, all Clojure expressions evaluate to a value. +;; - "Atomic" literals: +"hello" ; strings +:hello ; keywords +\h \e \l \l \o ; characters +'hello ; symbols +42 ; numbers +22/7 ; fractional numbers +nil ; yes, nil is a value +;; - Collection literals: +[1 2 3 4 5] ; a vector +{:a 1 :b 2} ; a hash-map (key-value pairs) +#{1 2 3 4 5} ; a hash-set +'(1 2 3 4 5) ; a list -;; Clojure is a "Lisp" -;; - Lisp is short for List Processing -;; - It's just another way to design a programming language -;; (Ignore "But, Why?" for now... Just use it as it is, and try to -;; do something practical with it.) +;; - "Built-in" functions: ++ ; addition +map ; map over a collection +filter ; filter from a collection +reduce ; transform a collection +;; - "Symbolic" expressions (or "s"-expression or s-expr) +(+ 1 2) ; a simple s-expr -;; Clojure code is composed of "expressions": +(+ (+ 1 2) (+ 1 2)) ; an s-expr of nested s-exprs -;; These literal values are Clojure "expressions" -"hello" ; strings -:hello ; keywords -'hello ; symbols -42 ; numbers -22/7 ; fractional numbers +(+ (+ (+ 1 2) (+ 1 2)) + (+ (+ 1 2) (+ 1 2))) ; an even more nested s-expr -;; "Built-in" functions are also "expressions" -;; - We will meet all of these again, very soon. -+ ; addition -map ; map over a collection -filter ; filter from a collection -reduce ; transform a collection +(defn same + [x] + x) ; function definitions are also s-exprs -;; Collection "literals" are expressions too: -;; - We will extensively use such "collection" data structures. -[1 2 3 4 5] ; a vector -{:a 1 :b 2} ; a hash-map -#{1 2 3 4 5} ; a hash-set -'(1 2 3 4 5) ; a list +;; Namespaces: +;; +;; - are how we organize and/or modularise code +;; - All Clojure code is defined and evaluated within namespaces -;; Clojure code is also composed of expressions; -;; - we refer to them as "symbolic" expressions (or "s"-expression) +;; EXERCISE: +;; Evaluate the following expressions and see what you get. +;; - First, type the expression in the REPL +;; - Next, evaluate them straight from your codebase -(+ 1 2) ; an s-expression +map ; is defined in the `clojure.core` ns (namespace) -(+ (+ 1 2) (+ 1 2)) ; an s-expression of nested expressions +same ; is defined in the current ns -(+ (+ (+ 1 2) (+ 1 2)) - (+ (+ 1 2) (+ 1 2))) ; an even more nested s-expression +#_(ns-name *ns*) ; What's the current ns? +(comment + ;; PROTIP: + ;; + ;; Your IDE or text editor would have a convenient shortcut to + ;; evaluate any Clojure expression right from your codebase. + ;; + ;; Some editors allow you to "evaluate in-line", some would + ;; tell you to "send to the REPL". Consult the documentation + ;; that accompanies your editor's Clojure plugin, to learn + ;; how to do this. + ;; + ;; Make a habit of interacting "dynamically" with Clojure + ;; this way, right from inside your codebase; i.e. prefer + ;;_not_ to type things directly into the REPL. + ) -;; In fact, ALL Clojure code is just "expressions" -;; - And, all Clojure expressions evaluate to a value. -;; -;; - All literals evaluate to themselves. They are values. -;; (Hence "literal": a literal is what it is. :-D) -;; -;; - All collection literals also evaluate to themselves. -;; (A literal collection is what it is, too.) -;; -;; - All functions are values. -;; (More on this a little later) -;; -;; - All s-expressions, however deeply nested, finally evaluate -;; to a return value. Expressions evaluate to either a literal, -;; or a collection, or a function. ;; Clojure expression syntax rules: -;; + ;; - Literals: ;; - Just write them down -;; -;; - Collection Literals: -;; - Just write them down too, but also -;; - make sure opening brackets are always matched by closing brackets -;; [1 2 3] is a well-formed vector representation -;; [1 2 3 is an "unbalanced" vector and will cause an error. -;; -;; - Symbolic expressions ("s-expressions"): -;; - Make sure the round parentheses close over the intended/required -;; sub-expressions -;; (+ 1 2) is a well-formed expression that will be evaluated -;; (+ 1 2 is an "unbalanced" s-expression and will cause an error. +;; - Collection literals and s-expressions: +;; - ABC - Always. Be. Closing. :-D +;; - The Clojure "Reader" (the 'R' part of the R.E.P.L) +;; expects each open bracket to be accompanied by a +;; corresponding closing bracket. i.e. all parentheses +;; must be "balanced". -;; Clojure Code Evaluation Rules: -;; -;; - To instruct Clojure to evaluate a list of expressions, -;; enclose the expressions in round parentheses. -;; - Recall: (+ 1 2) -;; -;; - The very first expression after an opening paren MUST be -;; a function. -;; - So: (1 2) will fail, because 1 is not a function -;; -;; - All expressions or sub-expressions that follow the first expression -;; will first be fully evaluated into values, and then passed to -;; the first expression as arguments. -;; - Recall: (+ (+ (+ 1 2) (+ 1 2)) -;; (+ (+ 1 2) (+ 1 2))) -;; -;; - You may mentally evaluate the above form "inside-out", like this: -;; - Evaluate the smallest and innermost expressions first, -;; - Mentally replace them with their return values -;; - Pass those values as arguments to the next higher expression -;; - Continue until you are left with a literal value. -;; -;; (+ (+ (+ 1 2) (+ 1 2)) -;; (+ (+ 1 2) (+ 1 2))) -;; -;; (+ (+ 3 3 ) -;; (+ 3 3 )) -;; -;; (+ 6 -;; 6) -;; -;; 12 -;; -;; - Keep this evaluation model in mind, when you read Clojure code, -;; to figure out how the code will evaluate. -;; -;; - To prevent evaluation, explicitly mark an expression as a list -;; '(1 2) put a single quote in front of an expression to tell Clojure -;; you don't want it to evaluate that expression. -;; -;; - To comment out in-line comment text, or even an expression, -;; place one or more semi-colons before the text/expression. -;; -;; - `#_` is a clean way to comment out multi-line s-expressions -;; Compare this: -#_(+ 1 2 - 3 4 - 5 6) -;; With this: -;; (+ 1 2 -;; 3 4 -;; 5 6) +;; [1 2 3] ; OK +;; [1 2 3 ; FAIL + +;; {:a 1 :b 2} ; OK +;; {:a 1 :b 2 ; FAIL + +;; (+ 1 2) ; OK +;; (+ 1 2 ; FAIL + +;; - Indentation, extra spaces, and commas are just for +;; our reading convenience. Example: all of the following +;; literal maps represent the same value: + +{:a 1 :b 2} +{:a 1, :b 2} -;; Why is Clojure a "List Processing" language? +{:a 1, + :b 2} + +{:a 1 + :b 2} + +{:a 1 + :b + 2} + + + +;; Clojure Expression Evaluation Rules: + +;; - Wrap in parentheses to cause evaluation. +;; The first position is special, and must be +;; occupied by a function + +(+ 1 2) ; OK +;; (1 2) ; FAIL, because 1 is not a function + +;; - Mentally evaluate nested expressions "inside-out". +;; Usually, all s-expressions--however deeply nested--evaluate +;; to a return value; a literal, or a collection, or a function, +;; or some legal object. + +(+ (+ (+ 1 2) (+ 1 2)) + (+ (+ 1 2) (+ 1 2))) + +(+ (+ 3 3 ) + (+ 3 3 )) + +(+ 6 + 6) + +12 + +;; - _Prevent_ evaluation of s-expr by "quoting" it, +;; i.e. explicitly marking a list, by prefixing it +;; with a single quote `'`: + +'(+ 1 2) ; BUT the list will still remain in the evaluation path + +;; - Leave an s-expression in-line, but remove it from +;; the evaluation path, by prefixing it with `#_`: + +(+ 1 2 #_(+ 1 2)) ; will evaluate to 3 + +;; - Comment out entirely, by prefixing code with one or more +;; semicolons, just like in-line comments. + +;; (+ (+ 1 2) +;; (+ 1 2)) ; fully commented out + + +;; EXERCISE: +;; - Now, why will the following expression fail (throw an exception)? +;; Make an educated guess, then try it. + +;; (+ 1 2 '(+ 1 2)) ; un-comment and evaluate; then comment it back + + +(comment + ;; PROTIP: + ;; + ;; The special "#_" syntax is called a "reader macro". + ;; + ;; For now, ignore what that means, just know the effect of + ;; using it. You will see #_ often in code to follow. + ;; + ;; Incidentally, the single quote we used '(to mark a list) + ;; is also a reader macro. Many more specialized reader macros + ;; are available, but don't go there just yet. + ) + + + +;; Why is Clojure a Lisp ("LISt Processing") language? '(+ 1 2) ; Recall: this is a Clojure list, that Clojure evaluates ; as literal data. @@ -162,8 +209,8 @@ reduce ; transform a collection ; as an executable list, and tries to evaluate it as code. -;; More generally, Clojure code, like other lisps, is written -;; in terms of its own data structures. For example: +;; More generally, Clojure code is written in terms of Clojure's +;; own data structures. For example: ;; ;; Here is a function definition. (defn hie @@ -173,20 +220,21 @@ reduce ; transform a collection ;; What does it look like? ;; - Let's flatten it into one line for illustrative purposes: - -;;[1] [2] [3] [4] +;;[1] [2] [3] [4] (defn hie [person message] (str "Hie, " person " : " message)) ; [5] -;; Where: -;; - [1] `defn` is a Clojure built-in primitive -;; - Notice, it's at the 1st position, and -;; - 2-4 are all arguments to defn -;; Further: -;; - [2] is a Clojure symbol, `hello`, which will name the function -;; - [3] is a Clojure vector of two named arguments -;; - [4] is a Clojure s-expression, and is treated as the body of -;; the function definition -;; - [5] the whole thing itself is a Clojure s-expression! +(comment + ;; Here: + ;; - [1] `defn` is a Clojure built-in primitive + ;; - Notice, it's at the 1st position, and + ;; - 2-4 are all arguments to defn + ;; Further: + ;; - [2] is a Clojure symbol, `hie`, which will name the function + ;; - [3] is a Clojure vector of two named arguments + ;; - [4] is a Clojure s-expression, and is treated as the body of + ;; the function definition + ;; - [5] the whole thing itself is a Clojure s-expression! + ) ;; RECAP: @@ -200,3 +248,10 @@ reduce ; transform a collection ;; ;; - All opening braces or parentheses must be matched by closing ;; braces or parentheses, to create legal Clojure expressions. + +;; 4clojure Drills: Problems you could try now. +;; +;; - With the knowledge you have so far, you can try solving these +;; problems at 4clojure.com: 1 to 13, and 16, 47, 126, 161, and 162 +;; e.g. https://4clojure.oxal.org/#/problem/1 +;; e.g. https://4clojure.oxal.org/#/problem/16 diff --git a/src/clojure_by_example/ex01_fundamentally_functional.clj b/src/clojure_by_example/ex01_fundamentally_functional.clj new file mode 100644 index 0000000..cde2352 --- /dev/null +++ b/src/clojure_by_example/ex01_fundamentally_functional.clj @@ -0,0 +1,494 @@ +(ns clojure-by-example.ex01-fundamentally-functional) + +;; EX01: LESSON GOAL: +;; - Realize that pure functions, and strict lexical scope +;; are the bedrock upon which Clojure programs are built +;; - Drill how to use functions, and how lexical scope works +;; - Get comfortable with how functions compose together +;; - At every step, further drill the interactive REPL workflow. +;; Figure out how to take advantage of the immediate feedback +;; that the live REPL gives you. Treat each exercise as a +;; tiny experimental setup. Run small experiments that will +;; help you discover answers... +;; -> Read the exercise +;; -> Make a testable guess (hypothesis) +;; -> Evaluate your solution (test your hypothesis) +;; -> Compare your guess with the solution +;; -> If it differs, update your guess (or fix the solution) and redo +;; That is, try to use the Scientific Method to solve exercises. + + +;; Basic Function Syntax +;; +;; - Named functions: +;; +(defn function-name + "Documentation string (optional)." + [arg1 arg2 arg3 etc up to argN] + 'function 'body + 'goes 'here + '...) + +;; - Anonymous functions: +;; +(fn [arg1 arg2 arg3 etc up to argN] + 'function 'body + 'goes 'here + '...) + + + +;; A dead-simple function: + +(defn same + "Simply return the input unchanged." + [x] + x) + + +(fn [x] x) ; just like `same`, but with no name + + + +;; EXERCISE +;; +;; Evaluate and see: + +(same 42) + +(same [1 2 3 4 5]) + +(same {:pname "Earth" :moons 1}) + +;; EXERCISE +;; How about the anonymous version of `same`? +;; - What's the evaluation model? Think before you tinker. +;; - Form your hypothesis -> Test it -> Learn from the feedback + +((fn [x] x) 42) + +((fn [x] x) [1 2 3 4 5]) + +((fn [x] x) {:pname "Earth" :moons 1}) + + +;; EXERCISE +;; Fix the the following s-expr, so that it evaluates to true. +;; - First predict the solution in your head. +;; - Then replace 'FIX with your solution and evaluate to confirm. +;; - Think about the little experiment you just performed, and +;; form a theory about why the solution worked +(= 'FIX + (same same) + ((fn [x] x) same)) + + +;; `identity` +;; - provided by Clojure +;; - is exactly like our`same` function +;; - is extremely general (accepts any value) +;; - is surprisingly useful, as we will discover later + +;; EXERCISE +;; Fix this to prove `identity`, `same`, and the anonymous +;; version of `same`, all do the exact same thing: +;; - Note: Functions are values and can therefore be compared. +;; +(= identity + ('FIX identity) + ('FIX identity) + ('FIX identity)) +;; +;; Now, evaluate this in the REPL to _see_ the truth: +;; +#_(clojure.repl/source identity) +;; +(comment + ;; This is another example of what "dynamic" means. + ;; We can not only can we interact live with small bits of + ;; our Clojure programs, we can also examine many aspects + ;; of our programs at run time. The clojure.repl namespace + ;; is one tool at our disposal. Try these in the REPL: + #_(clojure.repl/dir clojure.repl) + #_(clojure.repl/doc clojure.repl) + ) + + +;; "Higher order" functions (HoFs): + +;; Functions that: +;; - *accept* functions as arguments +;; and/or +;; - *return* functions as results +;; are called "higher order" functions. + + +;; EXERCISE +;; Have we seen HoFs so far? If yes, list them out below. + + +;; EXERCISE +;; Write a zero-argument function that returns the `identity` function + +(defn gen-identity + [] ; zero arguments + 'FIX) + +;; EXERCISE +;; Fix this function so that it returns a function that _behaves_ +;; like the identity function (don't return `same`, or `identity`). + +(defn gen-identity-v2 + [] + 'FIX) + +;; EXERCISE +;; Replace 'FIX1 with a call to the `gen-identity` function, +;; and 'FIX2 with a call to the `gen-identity-v2` function, +;; such that the following evaluates to true. + +(= identity + 'FIX1 + 'FIX2) + + +;; Composing Logic with Higher-order Functions (HoFs): +(comment + ;; Clojure programmers often write simple functions that + ;; each do one task well, and use higher order functions + ;; to "compose" these in creative ways, to produce more + ;; useful pieces of logic. + ;; + ;; We treat "simple" functions as building blocks, and + ;; HoFs as versatile mini-blueprints that help us organize + ;; and glue together the simple functions. + ) + +;; EXERCISE +;; Reason about why this is working: + +(defn selfie + "Given a function `f`, return the result of + applying `f` to itself." + [f] + (f f)) + +(= 42 + (identity 42) + ((selfie identity) 42) + ((selfie (selfie identity)) 42) + ((selfie (selfie (selfie identity))) 42)) ; ad-infinitum + + +;; Let's play with a couple of nifty HoFs built into Clojure +;; - `comp` +;; - `complement` + + +;; EXERCISE +;; Use `(comp vec str inc)` to make the following true +;; - `comp` accepts any number of functions as arguments, +;; and returns a function that behaves as a pipeline +;; (or chain) of the given functions + +(= [\4 \2] + (vec (str (inc 41))) + ('FIX 'FIX)) + +(comment + ;; Reason about the order of evaluation and how inputs + ;; and outputs should connect, for `comp` chains to + ;; work correctly. + ;; + ;; To see if you reasoned correctly, try each of + ;; seq, str, inc independently: + (inc 41) ; increment a number + (str 42) ; turn the input into a string + (seq "42") ; turns a string into a character sequence + ) + + +;; EXERCISE +;; Use `(complement string?)` to make the following true +;; - `complement` accepts a "predicate" function, and returns a +;; function that does the opposite of the given "predicate" +(= (not (string? "hi")) + ('FIX 'FIX)) + +(comment + ;; "Predicate" is just a term we use to conveniently describe + ;; any function that returns a truthy/falsey value, i.e. + ;; any function that is used to test for some condition. + ;; These so-called "predicates" are not inherently special. +) + + + +;; "Lexical Scope" in Clojure +;; - Lexical scope guarantees that the reference to a value will be +;; "enclosed" in the scope in which it is being used. + +(comment + ;; Strict lexical scope greatly simplifies our life, because + ;; it allows us to mechanically follow code, and determine + ;; where a value originated. + ;; - Start at the place of reference of the value. + ;; - Then "walk" outwards, until you meet the very first let binding, + ;; or arg-list, or def, where the value was bound. + ;; - Now you know where the value came from. + ;; + ;; This also helps reduce our mental burden of inventing + ;; new names to refer to things, because we can re-use + ;; a name within a limited scope, and be certain that + ;; it will not destroy anything with the same name outside + ;; the given scope. + ) + +;; EXERCISE: +;; - Develop an intuition for what "Lexical scope" might mean +;; by reasoning about the following exercises. +;; +;; - Mentally evaluate and predict the results; then check. + +(def x 42) ; Bind `x` to 42, globally ("top-level" binding) + +(identity x) ; obviously returns 42 + +((fn [x] x) x) ; also returns 42, but how? + +(let [x 10] ; We use `let` to bind things locally. + x) ; This evaluates to the value of the "let-bound" `x`. + +(+ (let [x 10] + x) + x) ; So, this whole thing should evaluate to what? + + +;; EXERCISE +;; Read carefully, and compare these three function variants: + +(defn add-one-v1 + [x] + (+ x 1)) ; which `x` will this `x` reference? + +(add-one-v1 1) ; should evaluate to what? +(add-one-v1 x) ; should evaluate to what? + + +(defn add-one-v2 + [z] + (+ x 1)) ; which `x` will this `x` reference? + +(add-one-v2 1) ; should evaluate to what? +(add-one-v2 x) ; should evaluate to what? + + +(defn add-one-v3 + [x] + (let [x 10] + (+ x 1))) ; which `x` will this `x` reference? + +(add-one-v3 1) ; should evaluate to what? +(add-one-v3 x) ; should evaluate to what? + + +;; EXERCISE +;; - Mentally evaluate the following, predict the results, +;; and try to infer the scoping rule. +;; - Then evaluate each expression to see if your +;; mental model agrees with the result you see. +;; - Start with any `x`, and mechanically work +;; your way around. + +((fn [x] x) (let [x 10] x)) + + +((fn [x] x) (let [x x] x)) + + +(let [x 10] ((fn [x] x) x)) + + +((let [x 10] (fn [x] x)) x) + + +;; Function "Closure" +;; - This is a way for a function to capture and "close over" +;; any value available at the time the function is defined + +(def PI 3.141592653589793) + +(defn scale-by-PI + [n] + (* n PI)) ; PI is captured within the body of `scale-by-PI` + +(scale-by-PI 10) + + +;; A more general way to "scale by": +;; - Thanks to lexical scope + the function closure property + +(defn scale-by + "Given a number `x`, return a function that accepts + another number `y`, and scales `y` by `x`." + [x] + (fn [y] (* y x))) ; whatever is passed as `x` is captured + ; within the body of the returned function + + +;; EXERCISE +;; +#_(= (scale-by-PI 10) + ('FIX 10) + (* PI 10)) + +(comment + ;; BONUS EXERCISES + ;; Define a few scaling functions, in terms of `scale-by` + ;; + (def scale-by-PI-v2 + 'FIX) + + (def quadruple + "4x the given number." + 'FIX) + + (def halve + 'FIX)) + + +;; Sequences (or Collections) +;; +;; - and operations on Sequences +;; - Clojure provides _many_ sequence functions. +;; Here are some important ones: `map`, `filter`, and `reduce` +;; - Observe that all these functions are HoFs! + +map +;; Basic Syntax: +;; +;; (map a-function a-collection) +;; +;; Where the function must accept exactly one argument, because +;; it must transform only one item of the input at a time. + +(map inc [1 2 3 4 5 6]) +;; | | | | | | ; declare a mapping of each item of the input coll +;; inc inc inc ; via `inc` +;; | | | | | | +;; (2 3 4 5 6 7) ; to each item of the output coll +;; +;; Note: you may wonder why the result of map inc on [1 2 3 4], which is +;; square-bracketed results in an answer that's wrapped in parens (2 3 4 5). +;; +;; The short answer is: Ignore this pesky detail. +;; Think in terms of "sequence in, sequence out", instead of "this 'type' of +;; sequence in, and the same 'type' of sequence out". +;; +;; The more confusing answer is: 'map' returns a "lazy" sequence, which the REPL +;; _prints_ out visually, with round parens. 'filter' (below) does the same too. +;; +;; Usually we don't care if we have a vector or a list or a "lazy" sequence. +;; What we do care is what the sequence contains, and that the thing remains +;; sequential before/after. It only starts mattering when we definitely want +;; a particular sequence type for the very specific performance guarantees +;; that it provides. +;; +;; But really, you'll do better if you just ignore what this actually means +;; and/or the consequences of the distinction for now. + + +filter +;; Basic Syntax: +;; +;; (filter a-predicate-fn a-collection) +;; +;; Where the function must accept exactly one argument and +;; return a truthy result (hence we term it a "predicate" function). + +(filter even? [1 2 3 4 5 6]) + +(filter identity [1 nil 3 nil 5 nil]) ; nil is falsey, non-nils are truthy + + +reduce +;; Basic Syntax: +;; +;; (reduce a-function accumulator a-collection) +;; +;; Where the function must accept two arguments: +;; - first one is the value of the accumulator it manages, and +;; - the second one is bound to each item of the collection + +(reduce + 0 [0 0 1 2]) + +;; Imagine each step of the above computation, like this: + +;; ======================================= +;; Accumulator | Input collection (of number of moons) +;; ======================================= +;; 0 (start) | [0 0 1 2] ; just before first step +;; 0 | [0 1 2] ; at end of first step +;; 0 | [1 2] +;; 1 | [2] +;; 3 | [] ; reduce detects empty collection +;; --------------------------------------- +;; 3 (return value) ; reduce spits out the accumulator + + + +;; Truthiness +;; +;; - Only `nil` and `false` are Falsey; everything else +;; is Truthy +;; - a "predicate" function can return Truthy/Falsey, +;; not just boolean true/false +;; - we can make good use of this behaviour, in Clojure + + +(def a-bunch-of-values + [nil, false, ; falsey + 42, :a, "foo", true, ; truthy + {:a 1, :b 2}, [1 2 3 4], ; truthy + '(), {}, [], ""]) ; truthy + + +;; A quick proof: +(map boolean ; coerces a given value to boolean true or false + a-bunch-of-values) + +(filter boolean + a-bunch-of-values) + + +;; Branching logic accepts Truthy/Falsey + +(if nil ; if condition is Truthy + "hi!" ; then evaluate the first expression + "boo!") ; else evaluate the second expression + + +(when false ; only when the condition is truthy + "boo!") ; evaluate the body. Otherwise, always return `nil` + + + +;; RECAP +;; - Acquire a "scientific experimentation" mindset when +;; interactively developing and debugging Clojure code +;; ... The REPL is your friend. +;; - Learn to use lexical scope and function closures effectively. +;; - Learn to define small "single purpose" functions, such that +;; you can compose them together to produce higher order logic. + +;; +;; 4clojure Drills: Problems you could try now. +;; +;; - #protip: Write the solutions as proper named functions in your code base, +;; without code-golfing or hacks. Then translate to anonymous function form +;; that 4clojure requires. +;; +(comment + (map (fn [problem-no] (str "https://4clojure.oxal.org/#/problem/" + problem-no)) + [14, 15, 19, 20, 48, 45])) diff --git a/src/clojure_by_example/ex01_small_beginnings.clj b/src/clojure_by_example/ex01_small_beginnings.clj deleted file mode 100644 index 102dd51..0000000 --- a/src/clojure_by_example/ex01_small_beginnings.clj +++ /dev/null @@ -1,464 +0,0 @@ -(ns clojure-by-example.ex01-small-beginnings) - - -;; Ex01: LESSON GOAL: -;; -;; - Show a way to model things with pure data, in the form of hash-maps -;; - Show how to query hash-maps -;; - Introduce the idea of a function -;; - Use the above to show how we can "do more with less" - - -;; Our Earth - -;; "pname" "Earth" -;; "mass" 1 ; if Earth mass is 1, Jupiter's mass is 317.8 x Earth -;; "radius" 1 ; if Earth radius is 1, Jupiter's radius is 11.21 x Earth -;; "moons" 1 -;; "atmosphere" "nitrogen" 78.08 -;; "oxygen" 20.95 -;; "CO2" 0.40 -;; "water-vapour" 0.10 -;; "other-gases" "argon" 0.33 -;; "traces" 0.14 - - -;; Looks like a collection of name-value pairs. To some, it will -;; look like JSON. -;; -;; This intuition is correct. We can describe the Earth by its -;; properties, written as name-value pairs or "key"-value pairs. - -;; If we put curly braces in the right places, it becomes a -;; Clojure "hash-map": - -{"pname" "Earth" - "mass" 1 - "radius" 1 - "moons" 1 - "atmosphere" {"nitrogen" 78.08 - "oxygen" 20.95 - "CO2" 0.40 - "water-vapour" 0.10 - "other-gases" {"argon" 0.33 - "traces" 0.14}}} - - -;; Let's query the Earth. - -;; But first, let's create a global reference to our hash-map, -;; for convenience. - -;; Let's call it `earth`. - - -(def earth - {"pname" "Earth" - "mass" 1 - "radius" 1 - "moons" 1 - "atmosphere" {"nitrogen" 78.08 - "oxygen" 20.95 - "CO2" 0.40 - "water-vapour" 0.10 - "other-gases" {"argon" 0.33 - "traces" 0.14}}}) ; <- evaluate this -;; To evaluate the above `def`: -;; - Place the cursor just after the closing paren `)`, and -;; - evaluate it using your editor's evaluate feature -;; (in LightTable, hit ctrl+Enter on Win/Linux, and cmd+Enter on Mac) - -;; Evaluation will attach (or 'bind') the hash-map to the symbol -;; we have called `earth`. - -earth ; evaluate and check the hash-map - - - -;; _Now_ let's query the 'earth' global... - - -;; Top-level access: - -;; Wait! -;; -;; What do you _expect_ 'get' to do, in the expression below? -;; -;; Try to predict, before you evaluate. - -(get earth "pname") ; <- place cursor after closing paren and evaluate. - - - -;; EXERCISE: -;; -;; How to get number of moons? -;; - Uncomment, and fix the expression below: - -;; (get earth 'FIX) - - -;; EXERCISE: -;; -;; What does the atmosphere contain? -;; - Type your expression below: -;; -;; ('FIX 'FIX 'FIX) - - -;; Lesson: -;; - Given a hash-map and a "key", `get` returns the value -;; associated with the key. -;; - A value can be a string, or a number, or even another hash-map. - - - -;; Nested access: - -;; EXERCISE: -;; -;; Now, how to find what the other gases are, in the atmosphere? -;; - Hint: Mentally replace FIX with the value of "atmosphere". -;; - Now ask yourself, what expression will return that value? - -;; (get 'FIX "other-gases") - - -;; EXERCISE: -;; -;; Now, try to go even deeper, to find how much argon we have? -;; - Hint: now you have to replace 'FIX with a nested expression -;; -;; (get 'FIX "argon") - - - -;; Lesson: -;; - You can put s-expressions inside s-expressions and evaluate the -;; whole thing as one s-expression. - -;; Notes: -;; - You may choose to indent a deeply nested s-expression, for clarity. -;; - For now, indent or don't indent, as per your comfort level. -;; - Later, learn about generally-accepted Clojure code style. -;; (https://github.com/bbatsov/clojure-style-guide) - - -;; A Simple "Function" - -;; Let's make our own function to access any "third" level value... - -(defn get-level-3 ; function name - [planet level1-key level2-key level3-key] ; arguments list - ;; function "body": - (get (get (get planet level1-key) - level2-key) - level3-key)) ; What does the function's "body" look like? - - - -;; Now we can... -(get-level-3 earth "atmosphere" "other-gases" "argon") -(get-level-3 earth "atmosphere" "other-gases" "traces") - - - -;; Keywords as Keys of Hash-maps - -;; Hash-maps so widely-used, and can so conveniently represent things, -;; that Clojure provides a far more convenient way to define hash-maps -;; and query them. - -;; Instead of plain old strings as keys, we can use -;; Clojure "keywords" as keys. - -"moons" ; a string - -:moons ; a keyword - - -;; Like strings and numbers, keywords directly "represent" themselves. -;; (A keyword is a fundamental data type.) -;; _Unlike_ strings, keywords are designed to do special things. - -;; To find out, we must first define an alternative hash-map, -;; - that represents the same data about the Earth, -;; - but with keywords as keys, instead of strings -;; - to let us super-easily query the hash-map, using just keywords -(def earth-alt {:pname "Earth" - :mass 1 - :radius 1 - :moons 1 - :atmosphere {:nitrogen 78.08 - :oxygen 20.95 - :CO2 0.4 - :water-vapour 0.10 - :other-gases {:argon 0.33 - :traces 0.14}}}) - -;; Easier top-level access - -;; EXERCISE: -;; -;; What will these return? - -(:pname earth-alt) - -(:mass earth-alt) - - -;; EXERCISE: -;; -;; How to find the atmosphere? Uncomment,fix, and evaluate: - -;; ('FIX earth-alt) - - -;; EXERCISE: -;; -;; What are the other gases, in the atmosphere? -;; - Hint: Remember, we can nest s-expressions inside s-expressions. -;; - Replace each 'FIX with the appropriate value or s-expression. -;; -;; ('FIX 'FIX) - - -;; EXERCISE: -;; -;; How much argon is present in the atmosphere? -;; Hint: once again, 'FIX with value(s) or nested s-expression(s). - -;; ('FIX 'FIX) - - -;; Clojure provides `get-in`, because nested access is so common. -;; - `get-in` is the cousin of `get` (and the granddaddy of our -;; get-level-3 function!) - -;; Try evaluating each one of these... -(get-in earth-alt [:atmosphere]) -(get-in earth-alt [:atmosphere :other-gases]) -(get-in earth-alt [:atmosphere :other-gases :argon]) -;; '--> imagine this as a "path" to the value - - -;; By the way, the function we defined earlier, is general enough -;; to work with keywords too! -(get-level-3 earth-alt :atmosphere :other-gases :argon) - - -;; EXERCISE: -;; -;; We saw `get-in` work for keywords. Does it work for strings too? -;; Uncomment, fix, and evaluate: - -;; (get-in earth 'FIX) - - -;; Did that work? Why or why not? - - -;; EXERCISE: -;; -;; Use get-in to query other gases from the `earth` hash-map. -;; Type your expression below and evaluate it: - - - -;; EXERCISE: -;; -;; Use get-in to query argon from `earth`'s atmosphere -;; Type your expression below and evaluate it: - - - -;; Lesson: -;; - Given a hash-map and a path "key", `get` returns the value -;; associated with the key. - - - -;; Clojure "Vectors": - -;; The square bracketed things we used with `get-in` are in fact a -;; Clojure datastructure. (Other languages may call these "Arrays".) -["atmosphere"] ; is a vector of one string -[:atmosphere :other-gases] ; is a vector of two keywords - - -;; These are `indexed` collections, i.e. we can query a value in -;; a Vector, if we know what "index" position it occupies in the vector. - - -;; EXERCISE: -;; -;; What will this return? - -(get [:foo :bar :baz] 0) -;; Note: We count position starting at `0`, not `1`, in Clojure - - -;; EXERCISE: -;; -;; What will this return? - -(get-in [:foo [:bar :baz]] [1 1]) - - -;; But we are actually just trying to find the "nth" item, -;; and Clojure gives us... -(nth [:foo :bar :baz] 0) - -(nth (nth [:foo [:bar :baz]] 1) - 1) - -;; Lesson: -;; - `get` and `get-in` are general enough to query Vectors too, -;; using index number. -;; - but we'd rather just look up the `nth` item in Vectors - - -;; Basic Data Modeling: - -;; We rarely use vectors to model objects like the Earth. -;; In Clojure, a hash-map is almost always the best way to model -;; an object that we need to query. -;; -;; But why? - -;; What if we model the earth as a vector, instead of a hash-map? -(def earth-as-a-vector - "Docstring: This vector represents Earth this way: - [Name, Radius, Mass, Moons]" - ["Earth" 1 1 1]) - - -;; Now, how do we query Earth? - - -(defn get-earth-name [] ; empty vector means zero arguments - (nth earth-as-a-vector 0)) - -(get-earth-name) ; call with zero arguments - -(defn get-earth-radius [] - (nth earth-as-a-vector 1)) - -(get-earth-radius) - -(defn get-earth-moons [] - (nth earth-as-a-vector 2)) ; Uh, was it 2 or 3? - -(get-earth-moons) ; did this return Mass, or Moons? - -(:moons earth-alt) ; compare: how obviously we can query :moons in earth-alt - - -;; Further, our custom "getter" functions for Earth's properties, -;; are practically useless for other planets we may wish to also define -;; as vectors. -;; -;; Why? -;; -;; Property positions for other planets may differ from earth. -;; And in vector look-up, position matters. -;; -;; Said another way: "Positional semantics do not scale" -;; -;; Consider the function below: -;; -(defn get-planet-prop - "A function with a dangerous, brittle assumption about - planetary properties." - [planet-as-vector prop-position] - (nth planet-as-vector - prop-position)) - - -;; Lesson: Doing More With Less: - -;; Clojure programmers rely on the power, and general-purpose -;; flexibility of hash-maps, as well as general-purpose functions, -;; to avoid getting stuck in such situations. - -;; While nobody stops us from doing so, using vectors to model an object -;; (like the Earth) is clearly awkward. -;; - We must maintain label/name information about values separately -;; (perhaps in the docstring) -;; - Our custom `get-xyz` functions are also far too "specialized", -;; i.e. we can only sensibly use them to query _only_ the earth. -;; - And it opens us up to a whole host of errors: -;; - we can easily lose track of what value represents what property -;; - what if someone decides to add the number of man-made satellites -;; between mass and moon? - - -;; Lesson-end Exercises: - -;; EXERCISE: -;; -;; Define another planet `mercury`, using keywords as keys. -;; - Ensure all keys are keywords -;; - Ensure braces {} are in the right places, to nest data correctly. -;; -;; Use the information below. -;; -#_(;; FIXME - pname "Mercury" ; has... - moons 0 - mass 0.0553 ; recall we assume Earth mass is 1 - radius 0.383 ; recall we assume Earth radius is 1 - atmosphere ; % of total volume - oxygen 42.0 - sodium 29.0 - hydrogen 22.0 - helium 6.0 - potassium 0.5 - other-gases 0.5) - - - -;; EXERCISE: -;; -;; Query the planet `mercury` in 3 ways: -;; - with nested `get` -;; - with get-in -;; - with keywords -;; Type your solutions below: - - - -;; EXERCISE: -;; -;; Write a custom function to do a two-level deep query on `mercury`. -;; - It should be able to query earth, and earth-alt as well. -;; - name it `get-level-2` -;; -;; Fix the function below: - -#_(defn get-level-2 - ['FIX ...] - 'FIX) - -;; Uncomment and evaluate to check you get the correct values -#_(get-level-2 earth "atmosphere" "oxygen") - -#_(get-level-2 earth-alt :atmosphere :oxygen) - -#_(get-level-2 mercury :atmosphere :oxygen) - - -;; RECAP: -;; -;; - hash-maps let us conveniently represent objects we wish to -;; model and query -;; - We can query hash-maps variously with keywords, `get`, and `get-in` -;; - If we use keywords as keys in hash-maps, querying is dead-simple -;; - We can define our own functions with `defn`, using this syntax: -;; -;; (defn function-name -;; [arg1 arg2 arg3 ... argN] -;; (body of the function)) -;; -;; - Using general-purpose data structures, and writing general-purpose -;; functions lets us do more with less diff --git a/src/clojure_by_example/ex02_domain_as_data.clj b/src/clojure_by_example/ex02_domain_as_data.clj new file mode 100644 index 0000000..04e1089 --- /dev/null +++ b/src/clojure_by_example/ex02_domain_as_data.clj @@ -0,0 +1,251 @@ +(ns clojure-by-example.ex02-domain-as-data) + +;; Ex02: LESSON GOAL: +;; - Model and query things using pure data +;; - Realize the flexibility and power of collections + + +;; Our Earth + +;; "pname" "Earth" +;; "mass" 1 ; if Earth mass is 1, Jupiter's mass is 317.8 x Earth +;; "radius" 1 ; if Earth radius is 1, Jupiter's radius is 11.21 x Earth +;; "moons" 1 +;; "atmosphere" "nitrogen" 78.08 +;; "oxygen" 20.95 +;; "CO2" 0.40 +;; "water-vapour" 0.10 +;; "argon" 0.33 +;; "traces" 0.14 + + +;; Recall: Literal syntax: +;; - If we just put curly braces in the right places, +;; we can turn the given table into a Clojure hash-map: + +(def earth + {"pname" "Earth" + "mass" 1 + "radius" 1 + "moons" 1 + "atmosphere" {"nitrogen" 78.08 + "oxygen" 20.95 + "carbon-dioxide" 0.4 + "water-vapour" 0.10 + "argon" 0.33 + "traces" 0.14}}) + +;; Now we can look up any value using `get`, and `get-in`: + +;; with `get` +(get earth "pname") + +(get (get earth "atmosphere") + "traces") + + +;; more conveniently, with `get-in` +(get-in earth ["pname"]) + +(get-in earth ["atmosphere" "traces"]) +;; '--> imagine this as a "path" to the value + + + +;; Alternatively, we can model the earth this way, +;; using keywords as keys, to great benefit: +(def earth-alt + {:pname "Earth" + :mass 1 + :radius 1 + :moons 1 + :atmosphere {:nitrogen 78.08 + :oxygen 20.95 + :carbon-dioxide 0.4 + :water-vapour 0.10 + :argon 0.33 + :traces 0.14}}) + +;; EXERCISE +;; `get` and `get-in` work as expected +;; - Use `get` to extract :traces from `earth-alt`'s atmosphere +;; - The use `get-in` to do the same + +#_(get 'FIX + 'FIX) + +#_(get-in 'FIX 'FIX) + + +;; BUT, unlike plain old strings, keywords also behave as +;; _functions_ of hash-maps, and can look themselves up +;; in any given hash-map. + +;; ("pname" earth) ; Will FAIL! + +(:pname earth-alt) ; Works! + + +;; EXERCISE +;; Extract `:argon` from the `:atmosphere` of `earth-alt` + +('FIX ('FIX earth-alt)) + + +;; Which means we can use keywords in this manner: + +(def planets + [{:pname "Mercury" :moons 0 :mass 0.0533} + {:pname "Venus" :moons 0 :mass 0.815} + {:pname "Earth" :moons 1 :mass 1} + {:pname "Mars" :moons 2 :mass 0.107}]) + + +;; Instead of having to write functions to query planets: +(map (fn [p] (get p :pname)) + planets) + +;; We can directly use keywords as functions: +(map :pname + planets) + + +;; EXERCISE +;; `filter` out planets with less `:mass` than the Earth + +(defn less-mass-than-earth? + [planet] + (< ('FIX planet) 1)) + +('FIX 'FIX 'FIX) + + +;; EXERCISE +;; Recall how to use `filter`, `map`, and `reduce`: +(filter even? [1 2 3 4]) +(map inc [1 2 3 4]) +(reduce + 0 [1 2 3 4]) +;; Use these to compute the total `:mass` of planets +;; having less mass than the Earth. + + + +;; Maps, Vectors, and Sets also behave like functions! +;; - We don't normally use maps and vector in the function +;; position to perform lookups (there are a few problems +;; with doing so), but we often use _well-defined_ sets as +;; predicate functions, to test for set membership. + +;; Maps can "self-look-up" keys + +({:a "a", :b "b"} :a) + +;; Vectors can "self-look-up" by index position + +(["a" "b" "c"] 0) + +;; Sets can self-test set membership + +(#{"a" "b" "c"} "b") ; truthy: return set member if it exists +(#{"a" "b" "c"} "boo") ; falsey: return `nil` if it doesn't + +;; Lists do NOT behave like functions + +#_('("a" "b" "c") 0) ; FAIL + + +;; EXERCISE +;; Define a predicate `poison-gas?` which returns the +;; poison gas if it belongs to a set of known poison gases, +;; or `nil` (falsey) otherwise. These are some known poison gases: +:carbon-monoxide, :chlorine, :helium +:sulphur-dioxide, :hydrogen-chloride + + +(def poison-gas? + "Does the given gas belong to a set of known poison gases?" + 'FIX) + +(poison-gas? :chlorine) ; truthy +(poison-gas? :oxygen) ; falsey + + +;; Collections are "open", i.e. very flexible +;; - We can make collections out of almost anything + +;; Recall: +(def a-bunch-of-values + [nil, false, ; falsey + 42, :a, "foo", true, ; truthy + {:a 1, :b 2}, [1 2 3 4], ; truthy + '(), {}, [], ""]) ; truthy + +(map boolean a-bunch-of-values) + + +;; And since functions are values too, we can potentially use +;; collections of functions like this: +(map (fn [f] (f 42)) + [str identity inc dec (fn [x] x)]) + + +;; Domain Modeling in Clojure +;; - We use the flexibility of collections, to model +;; real-world objects and logic as we please + + +;; Predicates and operations +{:number-checks [even? pos? integer? (fn [x] (> x 42))] + :number-ops [str identity inc dec (fn [x] x)]} + + +;; A data table: +[[:name :age :country] + ["Foo" 10 "India"] + ["Bar" 21 "Australia"] + ["Baz" 18 "Turkey"] + ["Qux" 42 "Chile"]] + + +;; HTML (ref: Hiccup templates) +[:div {:class "wow-list"} + [:ul (map (fn [x] [:li x]) + [1 2 3 4])]] + + +;; Musical patterns (ref: github.com/ssrihari/ragavardhini) +{:arohanam [:s :r3 :g3 :m1 :p :d1 :n2 :s.], + :avarohanam [:s. :n2 :d1 :p :m1 :g3 :r3 :s]} + + +;; DB queries (ref: Datomic) +#_[:find ?name ?duration + :where [?e :artist/name "The Beatles"] + [?track :track/artists ?e] + [?track :track/name ?name] + [?track :track/duration ?duration]] + + +;; Starfleet mission configurations +{:inhabit {:starships 5, :battle-cruisers 5, + :orbiters 5, :cargo-ships 5, + :probes 30} + :colonise {:starships 1, :probes 50} + :probe {:orbiters 1, :probes 100} + :observe {:orbiters 1, :probes 10}} + + +;; Only limited by your imagination! + +;; +;; 4clojure Drills: Problems you could try now. +;; +;; - #protip: Write the solutions as proper named functions in your code base, +;; without code-golfing or hacks. Then translate to anonymous function form +;; that 4clojure requires. +;; +(comment + (map (fn [problem-no] (str "https://4clojure.oxal.org/#/problem/" + problem-no)) + [17, 18, 57, 71 + 134, 27, 26, 39])) diff --git a/src/clojure_by_example/ex02_small_functions.clj b/src/clojure_by_example/ex02_small_functions.clj deleted file mode 100644 index 632452b..0000000 --- a/src/clojure_by_example/ex02_small_functions.clj +++ /dev/null @@ -1,341 +0,0 @@ -(ns clojure-by-example.ex02-small-functions) - -;; Ex02: LESSON GOALS -;; -;; - Learn to define simple functions, and use them -;; - Learn a little bit about how functions behave -;; - Learn about a few useful built-in Clojure functions -;; (We will use these in later exercises.) -;; - Stitch up ideas in this section with a small insight -;; into how we can use functions on vectors and hash-maps. -;; (Set up your intuition, for later exercises.) - - -;; First, some simple functions: - - -;; We define functions like this: -;; -;; (defn function-name -;; "Documentation string (optional)." -;; [arg1 arg2 arg3 ... argN] -;; (body of the function)) - - -;; `defn` stands for: -;; - "DEfine a FuNction, -;; - _and_ give it a globally-referenced name" - - -(defn same - "Simply return the input unchanged." - [x] - x) - -;; EXERCISE -;; -;; What will these return? - -(same 42) - -(same {:pname "Earth" :moons 1}) - -(same [1 2 3 4 5]) - - -;; We can define name-less functions too, like this: -;; -;; (fn [arg1 arg2 arg3 ... argN] -;; (body of the function)) -;; -;; `fn` is short for "define a FuNction, but _do not_ name it at all" -;; -;; E.g. This behaves _exactly_ like `same`, but it does not have a name: - -(fn [x] x) - - - -;; EXERCISE: -;; -;; What will these return? -;; - Hint: _mentally replace_ the function definition `(fn [x] x)` -;; with _any name you like_, and imagine that is the function name. -;; BUT make sure it behaves just like `(fn [x] x)`. - -((fn [x] x) 42) - -((fn [x] x) {:pname "Earth" :moons 1}) - -((fn [x] x) [1 2 3 4 5]) - - -;; We will make good use of such "anonymous" functions soon. -;; Just understand they are just like 'regular' functions, except -;; they do not have a globally-referenced name (hence "anonymous"). - - -;; Reminder: Do More With Less! -;; - Observe that `same` as well as `(fn [x] x)` appear to be capable -;; of accepting _any_ kind of value and returning it. -;; - This function definition is very general-purpose indeed. -;; - We will frequently use this "general-purpose" idea. - - -;; Some "built-in" functions - -;; Clojure has a function called `fn?`, that returns true if we -;; pass it something that is a function, and false otherwise. - -;; None of these are functions. So, return `false`. - -(fn? 42) - -(fn? "moons") - -(fn? [1 2 3 4 5]) - - -;; We defined the function `same`, and truly, it is a function... -(fn? same) - -;; Our "anonymous" function, is a function too. But of course! -(fn? (fn [x] x)) - -;; And `fn?` itself is a... ______ ? -(fn? fn?) - - -;; Wait a minute...!!! -;; -;; We just passed functions as arguments to `fn?`, and it worked! -;; -;; Is `fn?` special, or can we pass any function to any function? - -;; Well we know `fn?` is a function... -;; - Suppose we pass `fn?` to `same`, do we get back `fn?` unchanged? - -(= fn? - (same fn?)) - - -;; How about the anonymous version of `same`? -;; Does it return `fn?` unchanged as well? -(= 'FIX - ((fn [x] x) fn?)) - -;; How about this? -;; What do we get if we pass `same` to itself? -(= 'FIX - (same same)) - - -;; Lesson: Functions also behave like "values": -;; -;; - `42` is a value. `"moons"` is a value. `:moons` is a value. -;; - Values represent themselves directly -;; - Values are unique in the whole universe (42 is NOT "forty two") -;; - Values never change. They are constant forever and ever. -;; -;; - All Clojure function also are "values", including user-defined -;; functions (like `same`). And, just like the other values... -;; - We can pass functions as arguments to other functions, -;; without any special syntax or declarations. -;; - Functions can _return_ functions as results. -;; - We can compare any two functions and tell if they are -;; exactly the same thing. - - -;; Some other "built-in" Clojure functions - -;; Return `true` if even, `false` otherwise -(even? 2) - -;; Return `true` if odd, `false` otherwise -(odd? 3) - -;; Increment by one -(inc 42) - -;; Clojure's `identity` function is exactly like our `same` function. -;; - To prove it, fix the s-expression below so it evaluates to `true`: -(= 'FIX - (identity :moon) - (same :moon)) -;; We will use `identity` in surprisingly useful ways later. - - - -;; Now, let's use simple functions to process Collections - - -;; `map` -;; -;; `map` a function over a collection... -(map inc [1 2 3 4 5 6]) -;; Such that... -#_( 1 2 3 4 5 6) ; each item of input -;; | | | | | | ; is incremented by `inc` to give a result where -#_( 2 3 4 5 6 7) ; each output item "maps" back to an input item - -;; Note: -;; - Ignore the following subtlety for now: -;; You may have noticed that we passed a vector [] to `map` above, -;; but the result looks like a list (). Well it really isn't a -;; concrete list, but a "sequence" representation of the vector. -;; Clojure wraps a "sequence" in parens (), for display purposes only. -;; -;; - Think of `map` in general terms, as a way to express -;; one-to-one "mappings" of an input sequence to an output sequence, -;; by way of a function. -;; -;; - The syntax of `map` is: -;; -;; (map your-function input-collection) -;; -;; Where 'your-function' must accept exactly one argument, because -;; it must transform only one item of the input at a time. - - -;; EXERCISE: -;; -;; What should the following `map` expression return? -;; - First predict the answer, then evaluate to confirm. -;; - Hint: mentally apply the `even?` function to each item, one by one, -;; and build up a collection of results of each function application. -;; - And ignore the [] v/s () subtlety of input v/s output display. -;; Just predict the sequence of output items, in the correct order. - -(map even? [1 2 3 4 5 6]) - - -;; How about this? - -(map odd? [1 2 3 4 5 6]) - - -;; And this? - -(map identity [1 2 3 4 5 6]) ; Recall: `identity` is just like `same` - - -;; And this? - -(map (fn [x] x) [1 2 3 4 5 6]) - -;; Nice! Our anonymous "identity" function is a drop-in replacement -;; for `identity`, as well as `same`. - - - -;; Now for a little bit of fun with `map` and some planets. - - -;; This is a Clojure vector... of numbers - -[1 2 3 4 5 6] - - -;; This is a Clojure vector... of Clojure hash-maps. - -[{:pname "Mercury" :moons 0} - {:pname "Venus" :moons 0} - {:pname "Earth" :moons 1} - {:pname "Mars" :moons 2}] ; (Yes we can do this. More on this later!) - - -;; Let's name our collection of planets as, um... `planets` - -(def planets [{:pname "Mercury" :moons 0} - {:pname "Venus" :moons 0} - {:pname "Earth" :moons 1} - {:pname "Mars" :moons 2}]) - - -;; Recall that we can query a map, like this: - -(:pname {:pname "Mercury" :moons 0}) - -;; That is, a keyword _behaves like a function_ of a hash-map. - - -;; EXERCISE -;; -;; What if we pass a keyword instead of a real function, to `map`? -;; - What should the following map expression return? -;; - Predict the answer, and then evaluate to confirm. - -(map :pname planets) - -;; Read as: -;; "map :pname over `planets`, which is vector of planet hash-maps" - - - -;; Stitching it all together... - - -;; Let's define another little function. - -(defn planet-has-moons? - "Given a 'planet' (assume it's a hash-map), return true if - it has at least one moon." - [planet] - (> (:moons planet) - 0)) - -(planet-has-moons? {:pname "Mercury" :moons 0}) - -(planet-has-moons? {:pname "Earth" :moons 1}) - -(planet-has-moons? {:pname "Mars" :moons 2}) - - -;; EXERCISE -;; -;; Instead of querying each map, why not query all of them at one go? - -#_(map 'FIX 'FIX) - - -;; Also, as we now know, we can use anonymous functions creatively... - - -;; EXERCISE -;; -;; Replace 'FIX with your own anonymous function that works -;; just like `planet-has-moons?`. - -#_(map 'FIX planets) - - -;; EXERCISE -;; -;; And, finally, prove that both variants do exactly the same thing: - -#_(= (map planet-has-moons? 'FIX) - - ('FIX 'FIX 'FIX) ; use anonymous function - - [false false true true]) - - - -;; RECAP: -;; - Functions are easy to define -;; - Functions can _accept_ functions as arguments -;; - Functions can _return_ functions as arguments -;; - Clojure has nifty "built-in" functions -;; - e.g. the function `map` lets us express a mapping of an -;; input collection to an output collection, by way of a function. - - -;; "Do more with less": -;; - Even the most simple functions can be general-purpose -;; - Clojure keywords behave like functions of hash-maps -;; - Since keywords can query hash-maps, we can completely avoid writing -;; custom "getter" functions to query values in our hash-maps. -;; - We can put hash-maps in vectors, to make vectors of hash-maps. -;; - Since `map` accepts keywords in place of functions, we can -;; combine the above tiny set of ideas, to query many planetary hash- -;; maps at one go. diff --git a/src/clojure_by_example/ex03_data_and_functions.clj b/src/clojure_by_example/ex03_data_and_functions.clj index 6549b41..9165b0d 100644 --- a/src/clojure_by_example/ex03_data_and_functions.clj +++ b/src/clojure_by_example/ex03_data_and_functions.clj @@ -1,412 +1,444 @@ -(ns clojure-by-example.ex03-data-and-functions) +(ns clojure-by-example.ex03-data-and-functions ; current namespace (ns) + ;; "require" and alias another ns as `p`: + (:require [clojure-by-example.data.planets :as p])) + + +;; Ex03: LESSON GOALS +;; - Explore various bits and bobs of the solution interactively +;; using the live environment at your disposal +;; - Get some ideas of how to take just a handful of pieces, +;; and build sophisticated logic with them +;; - Debug any issues that might arise +;; - We use only the concepts and standard library functions +;; we've seen so far, to build purely functional logic +;; in order to process a bunch of planets: +;; +;; - Standard Library (about 20 functions): +;; `def`, `defn`, `fn`, `let` ; to create/name simple data and small functions +;; `get`, `get-in`, `assoc` ; to query and associate data +;; `map`, `filter`, `reduce` ; to operate on collections +;; `if`, `when`, `cond` ; to decide things +;; `not`, `and`, `empty?`, `<=`, `count` ; for logic and quantities +;; `comp`, `complement` ; to glue higher-order logic +;; +;; - Concepts: +;; - Compute only with pure functions: +;; - Build higher-order logic with higher order functions +;; - Lexical scope and function closures to maximize modularity +;; - Collections as functions: +;; - Keywords as functions of hash-maps +;; - Well-defined Sets as predicates --- tests of set membership +;; - Hash-maps and collections to model domain entities: +;; - A planet, or atmospheric tolerances, or decision tables, +;; or collections of analysis criteria +;; - Truthy / Falsey logic: +;; - Instead of only Boolean true/false +;; - Namespaces: +;; - Making use of things defined elsewhere +;; +;; - Workflow: +;; - Apply the Scientific Method to design, debug, and to understand +;; - Run small fast experiments via the REPL +;; - Preserve your experiments in-line within your codebase itself +;; +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Let's colonize planets! +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(comment + ;; BACKGROUND + ;; + ;; The Office of Interstellar Affairs (OIA) is pushing hard + ;; for all-out space exploration and colonization. + ;; + ;; The OIA intends to issue "mission directives"... + ;; + ;; They wish humanity to :inhabit, or :colonise, or :probe, + ;; or :observe a given planet based on their analysis of + ;; available planetary data. + ;; + ;; For a given "mission directive", like :probe, the OIA + ;; intends to dispatch a collection of vessels. -;; Ex03: LESSON GOALS (build upon ex01, and ex02) -;; -;; - Learn a few more nifty functions on collections (cousins of `map`) -;; - Learn to combine simple functions into more powerful functions -;; - Get a sense of how to model with data, and compute with functions -;; - Get a feel for some of the flexibility of Clojure data structures + ;; GOAL + ;; + ;; Prototype a bit of planetary analysis logic, using criteria + ;; that interest the OIA, such that they will be able to decide + ;; what to do about a given planet. + ;; + ;; Criteria include questions like: + ;; - co2-tolerable? + ;; - gravity-tolerable? + ;; - surface-temp-tolerable? + ;; + ;; How a planet stands up to such questions will let us assess + ;; whether it is habitable? or colonisable? or observe-only?. + ;; + ;; Once we deliver the OIA our assessment, they may choose to + ;; dispatch one or more kinds of Starfleet vessels to the planet. + ) +;; Here are some target planets: +clojure-by-example.data.planets/target-planets -;; Let's begin again, with our planets, our moons checker, and `map` -(def planets [{:name "Mercury" :moons 0 :mass 0.0533} - {:name "Venus" :moons 0 :mass 0.815} - {:name "Earth" :moons 1 :mass 1} - {:name "Mars" :moons 2 :mass 0.107} - {:name "Jupiter" :moons 69 :mass 317.8} - {:name "Saturn" :moons 62 :mass 95.2}]) +;; Which we can access more conveniently as: +p/target-planets +(map :pname p/target-planets) -;; Recall: Our little "helper" function, to check if a planet has moons. -(defn planet-with-moons? - "Given a 'planet' (assume it's a hash-map), return true if - it has at least one moon." - [planet] - (> (:moons planet) - 0)) -;; The following results make some sense: -(map :name planets) ; ok, we can now print just the names of planets +(def starfleet-mission-configurations + "Associate 'mission directives' like :inhabit, :colonise, :probe, + and 'mission configurations' of Starfleet vessels. e.g. If our + analysis of a planet says :probe, then we would send 1 'Orbiter' + class Starship carrying a complement of 100 autonomous probes." -(map :moons planets) ; ok, we can now count total number of moons + {:inhabit {:starships 5, :battle-cruisers 5, + :orbiters 5, :cargo-ships 5, + :probes 30} + :colonise {:starships 1, :probes 50} -;; But what do we do with a bunch of boolean results? + :probe {:orbiters 1, :probes 100} -(map planet-with-moons? planets) + :observe {:orbiters 1, :probes 10}}) -;; More usefully, I would like to know _which_ planets have moons. -;; -;; Even more usefully, given a collection of planets, I want to -;; filter all planets with moons. +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Basic Planetary Analysis +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; EXERCISE: -;; -;; Predict the result of this expression: -(filter planet-with-moons? - planets) +;; Some basic constants, utility functions, and "predicate" +;; functions to test a given planet for particular conditions. -;; Can we do the opposite? +(def tolerances + "Define low/high bounds of planetary characteristics we care about." + {:co2 {:low 0.1, :high 5.0} + :gravity {:low 0.1, :high 2.0} + :surface-temp-deg-c {:low -125, :high 60}}) -(filter (fn [p] (not (planet-with-moons? p))) - planets) +(def poison-gas? + "A set of poison gases." + #{:chlorine, :sulphur-dioxide, :carbon-monoxide}) -;; EXERCISE -;; -;; You can define a new "helper" function, in terms of earlier helper -;; functions... Fix the function body, and make it work. - -(defn planet-without-moons? - "Planets without moons are exactly opposite to planets with moons." - [FIX] - FIX) +(defn lower-bound + [tolerance-key] + (get-in tolerances [tolerance-key :low])) -;; EXERCISE -;; -;; Now, filter out planets without moons... - -;; (FIX FIX FIX) ; fixme and evaluate +(defn upper-bound + [tolerance-key] + (get-in tolerances [tolerance-key :high])) -;; Can we write a function to filter planets into two groups? -;; - planets with moons, and -;; - planets without moons? - -;; EXERCISE: -;; -;; Will this work? Why? -;; - Take a guess... and then evaluate to check. -;; -{:planets-with-moons (filter planet-with-moons? planets) - :planets-without-moons (filter planet-without-moons? planets)} +(defn atmosphere-present? + [planet] + (not (empty? (:atmosphere planet)))) +#_(map :pname + (filter atmosphere-present? p/target-planets)) -;; Note: Do More With Less -;; -;; - Clojure lets us simply write down the structure of hash-maps, -;; even if some values need to be computed. No special constructor -;; required. -;; -;; - If an s-expression like (filter ...) is associated with a key, -;; you may imagine that Clojure will replace the s-expression -;; with the result of evaluating that expression, if we try to -;; evaluate the whole hash-map at one go. -;; -;; By the way it's not just hash-maps, this works for vectors too, -;; for the same reason: -[(filter planet-with-moons? planets) - (filter planet-without-moons? planets)] +(defn co2-tolerable? + [planet] + (let [co2 (get-in planet + [:atmosphere :carbon-dioxide])] + (when co2 + (<= (lower-bound :co2) + co2 + (upper-bound :co2))))) +#_(map :pname + (filter co2-tolerable? p/target-planets)) -;; EXERCISE: -;; -;; So now, to group planets by moons, we can... do what? -(defn group-planets-by-moons - [planets] - ;; FIX: return a data structure here that represents the grouping. - 'FIX) +(defn gravity-tolerable? + [planet] + (when (:gravity planet) + (<= (lower-bound :gravity) + (:gravity planet) + (upper-bound :gravity)))) +#_(map :pname + (filter gravity-tolerable? p/target-planets)) -;; EXERCISE: -;; -;; Use `group-planets-by-moons` to group planets. -;; Write your solution below: -#_('FIX 'FIX) +(defn surface-temp-tolerable? + [planet] + (let [temp (:surface-temp-deg-c planet) + low (:low temp) + high (:high temp)] + (when (and low high) + (<= (lower-bound :surface-temp-deg-c) + low + high + (upper-bound :surface-temp-deg-c))))) + +#_(map :pname + (filter surface-temp-tolerable? p/target-planets)) + + +(defn air-too-poisonous? + "The atmosphere is too poisonous, if the concentration of + any known poison gas exceeds 1.0% of atmospheric composition." + [planet] + (let [gas-too-poisonous? (fn [gas-key-pct-pair] + (and (poison-gas? (gas-key-pct-pair 0)) + (>= (gas-key-pct-pair 1) 1.0)))] + (not + (empty? + (filter gas-too-poisonous? + (:atmosphere planet)))))) -;; If you did it right, this is what happened: -;; -;; - When we called the function with `planets`, it did this: -;; - evaluated each filter expression one by one -;; - put the results in the respective places in the hash-map -;; - returned the whole hash-map +(map :pname + (filter air-too-poisonous? p/target-planets)) -;; Now we can further find... +;; Note: a hash-map is a collection of key-value pairs +(map identity + {:nitrogen 78.08, :oxygen 20.95, :carbon-dioxide 0.4, + :water-vapour 0.1, :argon 0.33, :traces 0.14}) -(:planets-with-moons (group-planets-by-moons planets)) +(map (fn [pair] + (str (get pair 0) " % = " (get pair 1))) + {:nitrogen 78.08, :oxygen 20.95, :carbon-dioxide 0.4, + :water-vapour 0.1, :argon 0.33, :traces 0.14}) -;; EXERCISE: -;; -;; Find names of those planets that have moons... +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Composite checks to perform on a given planet +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -#_('FIX 'FIX 'FIX) +(def minimal-good-conditions + "A collection of functions that tell us about the + good-ness of planetary conditions." + [co2-tolerable? + gravity-tolerable? + surface-temp-tolerable?]) -;; So far, we did some pretty cool things with sequence operations like -;; `map` and `filter`. Now for the big boss of sequence operations... +(def fatal-conditions + "A collection of functions that tell us about the + fatality of planetary conditions." + [complement atmosphere-present? + air-too-poisonous?]) -;; REDUCE! +(defn conditions-met + "Return only those condition fns that a planet meets. + An empty collection means no conditions were met." + [condition-fns planet] + (filter (fn [condition-fn] + (condition-fn planet)) + condition-fns)) -;; Remember we can collect moons from planets? -(map :moons planets) +(defn planet-meets-no-condition? + [conditions planet] + (empty? (conditions-met conditions planet))) -;; Now, how to "reduce" the collection into one number? -;; i.e., how to count the sum total of moons of all given planets? +(def planet-meets-any-one-condition? + (complement planet-meets-no-condition?)) -;; Well... -(reduce + 0 (map :moons planets)) -;; It computes this: 0 + p1-moons + p2-moons + .... + pN-moons +(defn planet-meets-all-conditions? + [conditions planet] + (= (count conditions) + (count (conditions-met conditions planet)))) -;; `reduce` takes a function, an "accumulator" value, and an input -;; collection, and returns an "accumulated" value. -;; Imagine each step of the above computation, like this: +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Composite checks to +;; - test whether a given planet meets a variety of conditions. +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; ======================================= -;; Accumulator | Input collection (of number of moons) -;; ======================================= -;; 0 (start) | [0 0 1 2] ; just before first step -;; 0 | [0 1 2] ; at end of first step -;; 0 | [1 2] -;; 1 | [2] -;; 3 | [] ; reduce detects empty collection -;; --------------------------------------- -;; 3 (return value) ; reduce spits out the accumulator -;; In words... -;; -;; - To begin with, `reduce` passes the accumulator and the first item -;; from the input collection to the function. -;; -;; - `reduce` "manages" the accumulator for us, such that the function -;; can update the accumulator, and `reduce` will feed the updated -;; accumulator back into the function, along with the next item -;; in the input collection. -;; -;; - This continues until the input collection is exhausted. +(defn habitable? + "We deem a planet habitable, if it has all minimally good conditions, + and no fatal conditions." + [planet] + (when (and (planet-meets-no-condition? + fatal-conditions + planet) + (planet-meets-all-conditions? + minimal-good-conditions + planet)) + planet)) -;; `reductions` is a convenience function that helps us visualize -;; the "accumulator" at each step of the `reduce` computation: +#_(map :pname + (filter habitable? p/target-planets)) -(reductions + 0 [0 0 1 2]) ; compare with the table -(reductions + 0 (map :moons planets)) ; calculate total number of moons +(defn colonisable? + "We deem a planet colonisable, if it has at least one + minimally good condition, and no fatal conditions." + [planet] + (when (and (planet-meets-any-one-condition? + minimal-good-conditions + planet) + (planet-meets-no-condition? + fatal-conditions + planet)) + planet)) -(reductions + 2 (map :moons planets)) ; accumulator can be any number +#_(map :pname + (filter colonisable? p/target-planets)) -;; Lesson-end Exercises: +(defn observe-only? + "We select a planet for orbital observation, if it only has harsh surface conditions." + [planet] + (when (and (planet-meets-any-one-condition? + fatal-conditions + planet) + (planet-meets-no-condition? + minimal-good-conditions + planet)) + planet)) -;; IMPORTANT: -;; -;; - Don't refer back to the code above unless you're _really_ stuck. -;; -;; - Try to reason from first principles - use the basic ideas we have -;; acquired so far. -;; -;; - Write your own functions _from scratch_, if you need them. +#_(map :pname + (filter observe-only? p/target-planets)) -;; EXERCISE: -;; -;; Calculate the total mass of all `planets` -;; - Hint: this will be a one-liner s-expression -;; - Write your solution below: +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Enrich planetary data with Starfleet mission information +;; from the Office of Interstellar Affairs. +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; EXERCISE: -;; -;; Count the number of `planets` that have moons. -;; - Reuse the `planet-with-moons?` function that we already defined. -;; - Use the `count` function to find the counts. +(defn issue-mission-directive + [planet] + (cond + (habitable? planet) :inhabit + (colonisable? planet) :colonise + (observe-only? planet) :observe + :else :probe)) -(count [42 43 44 45]) ; try this example -;; Write your solution below: +(defn assign-vessels + [planet] + (let [mission-directive (issue-mission-directive planet)] + (assoc planet + :mission-directive mission-directive + :mission-vessels (mission-directive starfleet-mission-configurations)))) +#_(map assign-vessels p/target-planets) -;; EXERCISE: -;; -;; Calculate the total mass of planets having moons. -;; - Reuse the `planet-with-moons?` function that we already defined. +;; Something's not right...The Office of Interstellar Affairs tells us we're not assigning vessels correctly?! +;; We've only deployed probes and orbiters, and no other vessels?! +;; We spent all this time and 300 lines of code to direct these vessels, and the orders aren't even correct! +;; We don't even see an error message! Clearly Clojure is the worst language ever made! +;; ...OR IS IT? -;; EXERCISE: -;; -;; Calculate the total mass of planets _without_ moons. -;; - Reuse the `planet-without-moons?` function that we defined earlier. +(comment + ;; It's time to learn how Clojure allows us to debug and understand our programs, using nothing more + ;; than the REPL and our wits. + ;; Let's look at our results again, shall we? Are we really only deploying probes and orbiters? + (map assign-vessels p/target-planets) -;; EXERCISE: -;; -;; Write a function `massier-than-earth?` which, -;; - given a planet, -;; - returns true if the planet's mass exceeds Earth's mass -;; - returns false otherwise + ;; That's a bit hard to visually parse, how about this: + (map :mission-vessels (map assign-vessels p/target-planets)) + ;; The OIA is right! But why is this happening? + ;; Either our directive to fleet mapping is wrong, or our issued directives are wrong. -;; EXERCISE: -;; -;; Write a function `planetary-stats` that: -;; -;; Takes TWO arguments: -;; 1. a "predicate" function like `massier-than-earth?` -;; 2. a sequence of some planets, -;; -;; Returns the following stats about those input planets -;; that match the filter criteria: -;; - count of the planets -;; - names of the planets -;; - total mass of the planets -;; -;; Important: -;; - The return value must be, an easy-to-query data structure. + starfleet-mission-configurations -;; Uncomment and fix: -#_(defn planetary-stats - [pred-fn given-planets] - ;; `let` is a way to define ("bind") function-local names to values - (let [filtered-planets 'FIX] - {:count 'FIX - :names 'FIX - :total-mass 'FIX})) + ;; The configurations look fine. What about the directives? + (map :mission-directive (map assign-vessels p/target-planets)) + ;; We're only probing and observing! Clearly issue-mission-directive is at fault. + ;; Let's take a look at its source code again. -;; EXERCISE: -;; -;; Calculate `planetary-stats` for: -;; -;; - planets with moons: + ;; Does this mean that there are no planets which our code considers habitable or colonisable? + ;; EXERCISE: + ;; Check whether we have any habitable or colonisable planets according to the habitable? and colonisable? predicates. -;; EXERCISE: -;; -;; Calculate `planetary-stats` for: -;; -;; - planets without moons: + ;; Apparently we don't! + ;; At the very least, the planet Earth should be both habitable and colonisable. + ;; At least now we know that habitable? and colonisable? are problematic. But why? Let's look at their implementation. + ;; We'll narrow in on habitable? for the time being, and worry about colonisable? later. + ;; A planet is habitable iff: + ;; 1. It has an atmosphere + ;; 2. The air is not too poisonous + ;; 3. The carbon dioxide, gravity and temperature levels are all tolerable + ;; The following issues are possible: + ;; 1. planet-meets-any-one-condition? is broken. + ;; 2. planet-meets-no-condition? is broken. + ;; 3. minimal-good-conditions is broken. + ;; 4. fatal-conditions is broken. + ;; 5. Any or all of the above. -;; - planets without moons, using `complement`: -;; compare, understand, use: + ;; EXERCISE: Check if planet-meets-any-one-condition? works correctly. + ;; planet-meets-any-one-condition? accepts predicates as a parameter, and doesn't care about the predicates + ;; themselves. Because of this, we can simplify our debugging by using simple and obvious predicates, + ;; rather than using the predicates in the production code. -(planet-with-moons? {:name "Earth" :moons 1}) + ;; EXERCISE: Check if planet-meets-no-condition? works correctly. -((complement planet-with-moons?) {:name "Earth" :moons 1}) + ;; If none of those work, clearly there's something wrong with our conditions themselves. -;; Fix the expression below: + ;; EXERCISE: Diagnose and fix the broken conditions. -#_(planetary-stats - 'FIX - planets) + ;; Does everything work now? + (map assign-vessels p/target-planets) + ) -;; EXERCISE: ;; -;; Calculate `planetary-stats' for: +;; RECAP +;; - Hopefully, you now have a better handle on the various aspects +;; of working with Clojure, listed in the exercise goals; viz. +;; - Reading: How to explore an unfamiliar Clojure code-base _interactively_? +;; - "Primitives": How to get a lot done with just 20-odd core functions? +;; - Concepts: What helps us model our domains and compose functional logic? +;; - Workflow: How to apply the scientific method to development and debugging? ;; -;; - planets with more mass than the earth: - - - -;; - planets with less mass than the earth, using `comp`: -;; compare, understand, use: -#_(massier-than-earth? {:name "Jupiter" :mass 317.8}) - -#_((comp not massier-than-earth?) {:name "Jupiter" :mass 317.8}) - -;; Type your solution here: - - - -;; EXERCISE: +;; - REPL all the things! +;; Especially understand how the Clojure REPL is a powerful debugging tool +;; that supersedes more traditional step-through debuggers in many ways. ;; -;; Calculate `planetary-stats' for: +;; You can: +;; 1. Test individual functions or constants to check if they're correct. +;; 2. Redefine a function to add tracing such as print statements, or other forms of instrumentation. +;; 3. Capture intermediate values such as function arguments or let bindings, and inspect them in the REPL +;; after the fact. +;; 4. Fix the problem and verify that it works immediately. +;; 5. Do all of the above either locally, or while connected to a remote server running in a staging or +;; production environment. ;; -;; - all `planets` (hint: use `identity`): - +;; We strongly recommend going through https://clojure.org/guides/repl/enhancing_your_repl_workflow#debugging-tools-and-techniques +;; for more tips, tricks and resources related to debugging. The entire REPL guide is useful, but the section about debugging +;; is particularly pertinent. - -;; EXERCISE: ;; -;; Write a function `more-planetary-stats` that: -;; - Takes a sequence of planets, and -;; - Returns an easy-to-query data structure containing -;; the following stats: +;; 4clojure Drills: Problems you could try now. ;; -;; given planets -;; count -;; names -;; total mass -;; planets with moons -;; count -;; names -;; total mass -;; planets without moons -;; count -;; names -;; total mass -;; planets with more mass than earth -;; count -;; names -;; total mass -;; planets having less mass than earth -;; count -;; names -;; total mass - - -;; Fix the `more-planetary-stats` function below: - -(defn more-planetary-stats - [FIX] - {:given-planets FIX - :with-moons FIX - :without-moons FIX - :massier-than-earth FIX - :less-massy-than-earth FIX}) - - - -;; Check your results. Uncomment and evaluate: +;; - #protip: Write the solutions as proper named functions in your code base, +;; without code-golfing or hacks. Then translate to anonymous function form +;; that 4clojure requires. (comment - (more-planetary-stats planets) - - (more-planetary-stats (take 2 planets)) - - (more-planetary-stats (drop 2 planets))) - - - -;; RECAP: -;; -;; - `map`, `filter`, and `reduce` are powerful sequence-processing -;; functions. Clojure programmers use these heavily. -;; -;; - Clojure programmers define small functions that do one task well, -;; and then build up sophisticated solutions by combining many such -;; small functions. -;; -;; - We can directly write down the actual structure of a data structure, -;; and Clojure will evaluate any un-evaluated values at run-time, -;; and return us the same data structure, but with computed values. -;; -;; - Clojure data structures are very flexible, and allow us to -;; model almost any real-world object. Previously we put hash-maps -;; inside a sequence to represent a collection of planets. And, in -;; this exercise, we put sequences inside a hash-map to group -;; planets by moons. + (map (fn [problem-no] (str "https://4clojure.oxal.org/#/problem/" + problem-no)) + [37, 64, 72, 21, 24, 25, + 38, 29, 42, 31, 81, 107, + 88, 157, 50, 46, 65])) diff --git a/src/clojure_by_example/ex04_api_design.clj b/src/clojure_by_example/ex04_api_design.clj new file mode 100644 index 0000000..2c23769 --- /dev/null +++ b/src/clojure_by_example/ex04_api_design.clj @@ -0,0 +1,330 @@ +(ns clojure-by-example.ex04-api-design + (:require [clojure-by-example.data.planets :as p])) + +;; EX04: Lesson Goals: +;; - We use these conveniences for good effect in API design. +;; - See how to allow the same function to support different arities, +;; as well as a variable number of arguments +;; - See how to "de-structure" data (it's a powerful, flexible lookup mechanism) +;; - Leverage de-structuring to design a self-documenting function API + + +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Multiple arities +;; +;; - When we know for sure that a function must handle more than +;; one "arity". An "arity" is the number of arguments +;; +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + +(defn add-upto-three-nums + ([] 0) ; identity of addition + ([x] x) + ([x y] (+ x y)) + ([x y z] (+ x y z))) + +(add-upto-three-nums) +(add-upto-three-nums 1) +(add-upto-three-nums 1 2) +(add-upto-three-nums 1 2 3) +#_(add-upto-three-nums 1 2 3 4) ; will fail + + +;; Variable arity +;; - When we don't know in advance how many arguments we +;; will have to handle, but we want to handle them all. + +(defn add-any-numbers + [& nums] + (reduce + 0 nums)) + +(add-any-numbers) +(add-any-numbers 1) +(add-any-numbers 1 2) +(add-any-numbers 1 2 3 4 5) + + +;; Multiple _and_ Variable arities, combined +;; - Guess what + actually is inside? +;; +#_(clojure.repl/source +) ; evaluate, check the REPL +;; +;; See how + tries to implement each arity as a special case, +;; to compute results as optimally as possible? We can do +;; such things too, in functions we define. + +(+) +(+ 1) +(+ 1 2 3 4 5 6 7 8 9 0) + + +;; We can also use multiple arities to define sane fallbacks. + +;; EXERCISE +;; - Recall `lower-bound`, and `upper-bound` from ex03 +;; - Refactor these to support more than one arity. + +(def tolerances + "Define low/high bounds of planetary characteristics we care about." + {:co2 {:low 0.1, :high 5.0} + :gravity {:low 0.1, :high 2.0} + :surface-temp-deg-c {:low -125, :high 60}}) + +(defn lower-bound + [tolerance-key] + (get-in tolerances [tolerance-key :low])) + +(defn upper-bound + [tolerance-key] + (get-in tolerances [tolerance-key :high])) + + +;; Fix `lower-bound-v2`, to make this expression evaluate +;; to true. Pay close attention to what should go where. +(defn lower-bound-v2 + "Look up the lower bound for the given tolerance key, in the + given map of `tolerances`. Use a globally-defined `tolerances` + map as a sane default if only tolerance-key is passed in." + ([tolerance-key] + (get-in tolerances + [tolerance-key :low])) + ([FIX1 FIX2] + 'FIX)) + +#_(= (lower-bound :co2) + (lower-bound-v2 :co2) + (lower-bound-v2 :co2 tolerances) + (lower-bound-v2 :co2 {:co2 {:low 0.1}})) + + +(comment + ;; BONUS EXERCISE + ;; Do the same for `upper-bound-v2` + #_(defn upper-bound-v2 + 'FIX + 'FIX) + + #_(= (upper-bound :co2) + (upper-bound-v2 :co2) + (upper-bound-v2 :co2 tolerances) + (upper-bound-v2 :co2 {:co2 {:low 0.1}})) + ) + + +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; A tiny bit of "De-structuring" +;; - For convenient access to items in collections. +;; +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(comment + ;; If we _shape_ our domain information as a data "structure", + ;; can we use our knowledge of the shape to pull it apart; + ;; i.e. "destructure" it? + + ;; Yes. + + ;; We use destructuring: + ;; - In `let` bindings, to cleanly reach into data + ;; - In function arguments, to make the API clean and expressive + ;; + ;; Here are a couple of commonly-used ways to do it. + ) + +;; "Positional" De-structuring +;; +;; - Pull apart sequential, ordered data structures like +;; lists, vectors, and any other sequence with linear access +;; +;; - Follow the structure of the collection, and mechanically +;; bind values to symbols by position. + + +;; Evaluate and see what happens. Then form a theory of what might be going on. +(def planet-names + (map :pname p/target-planets)) + + +(let [[pname1 pname2] planet-names] + (str pname1 " is the 1st planet, and " + pname2 " is the 2nd planet.")) + + +(let [[:as pnames] planet-names] + pnames) + + +(let [[m v e :as pnames] (map :pname p/target-planets)] + {:useless-trivia (str e " is the third rock from the Sun.") + :planets-names pnames}) + + +;; "Associative" De-structuring +;; +;; - Syntax to reach into associative data structures +;; (having key-value semantics), in arbitrary ways. +;; +;; - Note: Clojure "Records" and vectors are associative too +;; +;; - Follow the structure of the collection, and mechanically +;; bind values to symbols by key name. + +;; Evaluate one by one and see what happens. +;; Then form a theory of what might be going on. + + +(let [[mercury] p/target-planets] + (str (:pname mercury) " has mass " (:mass mercury))) + + +(let [[{:keys [pname mass]}] p/target-planets] + (str pname " has mass " mass)) + + +(let [[{:keys [pname mass] :as mercury}] p/target-planets] + (assoc mercury + :useless-trivia (str pname " has mass " mass))) + + + +;; And, putting it all together, a function with a +;; more self-documented API: + +(defn add-useless-trivia + "Be Captain Obvious. Given a planet, add some self-evident trivia to it." + [{:keys [pname mass] :as planet}] + (assoc planet + :useless-trivia (str pname " has mass " mass))) + + +(map add-useless-trivia + p/target-planets) + +#_(clojure.repl/doc add-useless-trivia) + + +;; EXERCISE: +;; Review the de-structured argument list in `add-useless-trivia`; +;; then try to recall and reinforce a key concept. +;; Hints: +;; - Relate it to our preferred way to model the world. +;; - What is the "world" here? +;; - What are we using to model/describe what property of what? +;; (Yes, that's three 'what's :-) + + +;; EXERCISE: +;; +;; - Use de-structuring to refactor the following functions +;; that we have copied over from ex03. +;; +;; - Develop a preliminary opinion about where and when +;; it might makes sense to de-structure, and where and when +;; it might not. + + +(defn atmosphere-present? + [planet] + (not (empty? (:atmosphere planet)))) + + +(defn atmosphere-present?-refactored + [FIXME] + FIXME) + +#_(= (map :pname + (filter atmosphere-present? p/target-planets)) + (map :pname + (filter atmosphere-present?-refactored + p/target-planets))) + +(defn co2-tolerable? + [planet] + (let [co2 (get-in planet + [:atmosphere :carbon-dioxide])] + (when co2 + (<= (lower-bound :co2) + co2 + (upper-bound :co2))))) + + +(defn co2-tolerable?-refactored + [FIXME] + FIXME) + +#_(= (map :pname + (filter co2-tolerable? p/target-planets)) + (map :pname + (filter co2-tolerable?-refactored + p/target-planets))) + + +;; EXERCISE: +;; - Fix the body of the refactored function +;; - Carefully review the function APIs, and develop a preliminary +;; opinion whether the refactored version is better than the original. + + +(defn surface-temp-tolerable? + [planet] + (let [temp (:surface-temp-deg-c planet) + low (:low temp) + high (:high temp)] + (when (and low high) + (<= (lower-bound :surface-temp-deg-c) + low + high + (upper-bound :surface-temp-deg-c))))) + + +(defn surface-temp-tolerable?-refactored + [{:keys [FIXME] :as planet}] + FIXME) + + +(defn surface-temp-tolerable?-refactored-v2 + [{{:keys [low high]} :surface-temp-deg-c + :as planet}] + 'FIXME) + + +#_(= (map :pname + (filter surface-temp-tolerable? + p/target-planets)) + (map :pname + (filter surface-temp-tolerable?-refactored + p/target-planets)) + (map :pname + (filter surface-temp-tolerable?-refactored-v2 + p/target-planets))) + + +;; RECAP: +;; +;; - We model the world by composing data structures and then use +;; "de-structuring" to conveniently reach into those structures. +;; - We can design function apis to accept more than one arity, +;; and then define custom logic for each arity. +;; - When, where, and how much to de-structure is a matter of +;; taste; a design choice. There is no One True Way. +;; - There are _many_ many ways of de-structuring. +;; Here's a really nice post detailing it: +;; cf. http://blog.jayfields.com/2010/07/clojure-destructuring.html + +;; 4clojure Drills: Problems you could try now. +;; +;; - #protip: Write the solutions as proper named functions in your code base, +;; without code-golfing or hacks. Then translate to anonymous function form +;; that 4clojure requires. +;; +(comment + (map (fn [problem-no] (str "https://4clojure.oxal.org/#/problem/" + problem-no)) + [35, 36, 68, 145, 52, + 156, 22, 23, 32, 30, + 34, 28, 33, 40, 83, + 61, 99, 120, 56, 55, + 43, 67, 74, 80, 69, 75])) diff --git a/src/clojure_by_example/ex04_control_flow.clj b/src/clojure_by_example/ex04_control_flow.clj deleted file mode 100644 index 4af2961..0000000 --- a/src/clojure_by_example/ex04_control_flow.clj +++ /dev/null @@ -1,750 +0,0 @@ -(ns clojure-by-example.ex04-control-flow) - - -;; Ex04: LESSON GOALS -;; -;; - Introduce different ways to do data-processing logic in Clojure -;; - with branching control structures (if, when, case, cond) -;; - without branching structures (we have already sneakily done this) -;; - predicates and boolean expressions -;; -;; - Have some more fun with much more sophisticated planets, -;; using control structures, and the stuff we learned so far - - -;; The logical base for logic: - -;; Boolean - -(true? true) ; `true` is boolean true - -(true? false) ; `false` is boolean false - - -;; Falsey - -;; `nil` is the only non-boolean "falsey" value - - -;; Truthy -;; - basically any non-nil value is truthy - -42 ; truthy -:a ; truthy -"foo" ; truthy -[7 3] ; truthy -[] ; turthy -"" ; truthy - - -;; Truthy/Falsey are NOT Boolean - -(true? 42) ; Is Truthy 42 a boolean true? - -(false? nil) ; Is Falsey nil a boolean false? - - -;; Truthy/Falsey can be cast to boolean true/false - -(boolean nil) ; coerce nil to `false` - -(map boolean - [42 :a "foo" [1 2 3 4] [] ""]) ; coerce non-nils to `true` - - -;; However, we need not coerce truthy/falsey to booleans to do -;; branching logic, as Clojure control structures understand -;; truthy and falsey values too: - -;; false is, well, false - -(if false ; if condition - :hello ; "then" expression - :bye-bye) ; "else" expression - - -;; `nil` is falsey - -(if nil - :hello - :bye-bye) - - -;; true is true, and every non-nil thing is truthy - -(if true - :hello - :bye-bye) - - -(if "Oi" - :hello - :bye-bye) - - -(if 42 - :hello - :bye-bye) - - -(if [1 2] - :hello - :bye-bye) - - - -;; `when` piggy-backs on the falsy-ness of `nil` -;; - when a condition is true, it evaluates the body and -;; returns its value -;; - otherwise, it does nothing, and returns `nil`, i.e. _falsey_ -;; - you may think of `when` as half an `if`, that always returns -;; `nil` (falsey) when the test expression is false or falsey. - -(when 42 - :hello) - - -(when false - :bye-bye) - - -(when nil - :bye-bye) - - -(when (nil? nil) - :bye-bye) - - -;; MENTAL EXERCISES -;; -;; Mental exercises to develop your intuition for how we use -;; "proper" booleans as well as truthy/falsey-ness. - - -;; EXERCISE: -;; -;; Predict what will happen... - -(map (fn [x] (if x - :hi - :bye)) - [1 2 nil 4 5 nil 7 8]) - - - -;; EXERCISE: -;; -;; Predict what will happen... - -(reduce (fn [acc x] (if x - (inc acc) - acc)) - 0 ; initial accumulator - [1 2 nil 4 5 nil 7 8]) - - -;; EXERCISE: -;; -;; Predict and compare the result of these two... - -(filter nil? [1 2 nil 4 5 nil 7 8]) - -(filter false? [1 2 nil 4 5 nil 7 8]) - - -;; EXERCISE: -;; -;; Predict and compare these three... - -(map identity - [1 2 nil 4 5 nil 7 8]) - -(filter identity - [1 2 nil 4 5 nil 7 8]) ; Ha! What happened here?! - -;; Compare the result of filter identity, with the following: -;; - And, reason about why the two look alike. -(filter (comp not nil?) - [1 2 nil 4 5 nil 7 8]) -;; Recall: `comp` composes functions into a pipeline - - - -;; INTERLUDE... -;; -;; The logic and ill-logic of `nil` in Clojure -;; -;; `nil` -;; -;; is Good and Evil, -;; something and nothing, -;; dead and alive. -;; -;; Love it or hate it, -;; you _will_ face `nil`. -;; Sooner than later, -;; in Clojure. -;; -;; Embrace it. -;; Guard against it. -;; But don't fear it. -;; -;; `nil` isn't the Enemy. -;; Fear is. -;; -;; Wield `nil` as -;; a double-edged sword. -;; For it cuts both ways. -;; -;; Ignore this, -;; and you will know -;; true suffering. - - -;; Good - `filter` knows `nil` is falsey - -(filter identity - [1 2 nil 4 5 nil 7 8]) - -;; Evil - `even?` cannot handle nothing... so, this fails: - -#_(filter even? - [1 2 nil 4 5 nil 7 8]) - -;; So... Guard functions like `even?` against the evil of nil - -(filter (fn [x] (when x - (even? x))) - [1 2 nil 4 5 nil 7 8]) - -;; `fnil` is also handy, to "patch" nil input to a function - -;; We might use it as a guard for `even?` like this: -((fnil even? 1) nil) ; pass 1 to `even?`, instead of nil -((fnil even? 2) nil) ; pass 2 to `even?`, instead of nil - -;; What's happening here? -;; - `fnil` takes a function f and some "fallback" value x, and returns -;; a function that calls f, replacing a nil first argument to f with -;; the supplied "fallback" value x. - - -;; Suppose for some strange reason, we want to treat `nil` as non-even. -;; We can nil-patch `even?` as: -(filter (fnil even? 1) - [1 2 nil 4 5 nil 7 8]) - - - -;; Lesson: -;; - Keep `nil` handling in mind, when you write your own functions. - - -;; Demonstration: -;; -;; - It's possible to use `nil` for good, and make life easier. -;; -;; - How might someone use `nil` to advantage? - -(def planets [{:pname "Venus" :moons 0} - {:pname "Mars" :moons 2} - {:pname "Jupiter" :moons 69}]) - - -;; Using `when` ... -;; -;; We might design a function that sends rockets to all moons of -;; a planet only when the planet has moons. (Instead of actually -;; sending rockets, let's just return a hash-map to express this -;; behaviour.) - - -(defn send-rockets-1 - [planet] - ;; Recall: we can "let-bind" local variables - (let [num-moons (:moons planet)] - (when (> num-moons 0) - {:send-rockets num-moons - :to-moons-of (:pname planet)}))) - -(send-rockets-1 {:pname "Venus" :moons 0}) -(send-rockets-1 {:pname "Mars" :moons 2}) - -(map send-rockets-1 planets) ; see, nil rockets for Venus - - -;; Later, someone may ask us what we did! And, we might design -;; a function to answer their question: -(defn good-heavens-what-did-you-do? - [rocket-info] - (if rocket-info ; we will treat rocket-info as truthy/falsey - ;; do/return this if true... - (format "I sent %d rockets to the moons of %s! Traa la laaa..." - (:send-rockets rocket-info) - (:to-moons-of rocket-info)) - ;; do/return this if false... - "No rockets sent. Waaah!")) - - -;; And we will answer... -(map good-heavens-what-did-you-do? - (map send-rockets-1 planets)) - - - -;; But suppose, using `if` ... -;; - we re-design the function like this: - -(defn send-rockets-2 [planet] - (let [num-moons (:moons planet)] - (if (> num-moons 0) - {:send-rockets num-moons - :to-moons-of (:pname planet)} - "Do nothing."))) - -(send-rockets-2 {:pname "Venus" :moons 0}) -(send-rockets-2 {:pname "Mars" :moons 2}) - -(map send-rockets-2 planets) ; see, truthy output (string) for Venus - -;; Now, suppose we use `send-rockets-2`, and later somebody asks us -;; what we did. -;; - How do we design a function to produce the same result as we got -;; earlier? -;; -#_(defn good-heavens-what-did-you-do-again??? - [rocket-info] - ;; Fix to ensure the same output as we produced earlier. - (if 'FIX - 'FIX - 'FIX)) - - -;; We should be able to provide the same answers as before... - -#_(map good-heavens-what-did-you-do-again??? - (map send-rockets-2 planets)) - - -;; Lesson: -;; - Using `nil` this way can reduce the need for writing explicit -;; `if` conditionals. This give us more generality, because now -;; we don't have to keep thinking of specialized literal values. - - -;; `case` and `cond` -;; - are also available to do branching logic: - -(map (fn [num-moons] - ;; Use `cond` when you have to decide what to do based on - ;; testing the value of a thing. - (cond - (nil? num-moons) "Do nothing!" - (zero? num-moons) "Send zero rockets." - (= num-moons 1) "Send a rocket." - :else (str "Send " num-moons " rockets!"))) - - [nil 0 1 42]) - - -(map (fn [num-moons] - ;; Use case when you can decide what to do based on the - ;; actual value of a thing. - (case num-moons - nil "Do nothing!" - 0 "Send zero rockets." - 1 "Send a rocket." - (str "Send " num-moons " rockets!"))) ; default expression - - [nil 0 1 42]) - - -;; EXERCISE: -;; -;; Try to reason from first principles: -;; -;; Why does cond require `:else` to mark the last / default condition, -;; but case simply treats the last expression as default? -;; -;; (Hint: is `:else` an expression or a value?) - - - - -;; And now for something completely different. - - - -;; Clojure hash-sets -;; - can be used as predicates (and often are used this way) - -(= #{:a :b :c} ; A hash-set of three things, :a, :b, and :c. - (hash-set :a :b :b :c :a :c :c :c)) - - -;; A hash-set behaves like a function to test for set membership. - -(#{:a :b :c} :a) ; Does the set contain :a? Truthy. - -(#{:a :b :c} :z) ; Does the set contain :z? Falsey. - - -;; How do Clojure programmers use sets as predicates? - -(def colonize-it? #{"Earth" "Mars"}) - - -((comp colonize-it? :pname) {:pname "Earth"}) - - -((comp colonize-it? :pname) {:pname "Venus"}) - - -(filter (comp colonize-it? :pname) - [{:pname "Mercury"} - {:pname "Venus"} - {:pname "Earth"} - {:pname "Mars"} - {:pname "Jupiter"}]) - - - -;; Lesson-end exercise - - -;; LET'S COLONIZE PLANETS!!!! -;; \\//_ - - -(def target-planets - [{:pname "Mercury" - :mass 0.055 - :radius 0.383 - :moons 0 - :atmosphere {}} ; empty hash map means no atmosphere - - {:pname "Venus" - :mass 0.815 - :radius 0.949 - :moons 0 - :atmosphere {:carbon-dioxide 96.45 :nitrogen 3.45 - :sulphur-dioxide 0.015 :traces 0.095}} - - {:pname "Earth" - :mass 1 - :radius 1 - :moons 1 - :atmosphere {:nitrogen 78.08 :oxygen 20.95 :carbon-dioxide 0.4 - :water-vapour 0.10 :argon 0.33 :traces 0.14}} - - {:pname "Mars" - :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}} - - {:pname "Chlorine Planet" - :mass 2.5 - :radius 1.3 - :moons 4 - :atmosphere {:chlorine 100.0}} - - {:pname "Insane Planet" - :mass 4.2 - :radius 1.42 - :moons 42 - :atmosphere {:sulphur-dioxide 80.0 :carbon-monoxide 10.0 - :chlorine 5.0 :nitrogen 5.0}}]) - - -;; EXERCISE: -;; -;; Define a set of `poison-gases` -;; - Let's say :chlorine, :sulphur-dioxide, :carbon-monoxide are poisons - -(def poison-gases 'FIX) - -;; Is the gas poisonous? -#_(poison-gases :oxygen) -#_(poison-gases :chlorine) - - -;; EXERCISE: -;; -;; Write a "predicate" function to check if a given planet is "Earth". -;; - -(defn earth? - [planet] - 'FIX) - - -;; EXERCISE: -;; -;; Write a predicate function to check if a planet has -;; at least 0.1% :carbon-dioxide in its atmosphere. - -(defn carbon-dioxide? - [planet] - 'FIX) - - -#_(map :pname - (filter carbon-dioxide? target-planets)) - -;; EXERCISE: -;; -;; Having no atmosphere is a bad thing, you know. -;; -;; Write a "predicate" function that returns truthy -;; if a planet has no atmosphere. It should return falsey -;; if the planet _has_ an atmosphere. -;; -;; Call it `no-atmosphere?` -;; -;; Use `empty?` to check if the value of :atmosphere is empty. -(empty? {}) ; is true. It's an empty hash-map. -;; -;; Type your solution below - - - -;; Quick-n-dirty test -#_(filter no-atmosphere? target-planets) - - - -;; EXERCISE: -;; -;; Let's say the air is too poisonous if the atmosphere contains -;; over 1.0 percent of _any_ poison gas. -;; -;; Write a "predicate" function that checks this, given a planet. -;; -;; Call it `air-too-poisonous?`. -;; -;; Use the following five ideas: -;; -;; [1] The `poison-gases` set we defined previously can be used -;; as a truthy/falsey predicate. Check: -(poison-gases :oxygen) -(poison-gases :chlorine) -;; -;; [2] A hash-map is a _collection_ of key-value _pairs_ / "kv" tuples -(map identity {:a 1 :b 2 :c 3}) ; see?! -;; -;; [3] Given a `kv` pair from a hash-map, (first kv) will always be -;; the key part, and (second kv) will always be the value part. -(map first {:a 1 :b 2 :c 3}) -(map second {:a 1 :b 2 :c 3}) -;; -;; [4] So, we can do something like this: -(filter (fn [kv] - (when (#{:a :b :c} - (first kv)) - (even? (second kv)))) - {:a 1 :b 2 :c 4 :d 8 :e 10 :f 12}) -;; -;; [5] Finally, recall that we can test for empty? collections: -(empty? []) -(not (empty? [])) -;; -;; -;; Now, combine ideas [1] to [5] to fix the function below: - - -(defn air-too-poisonous? - [planet] - (let [atmosphere 'FIX] - ;; Re-purpose, and fix the logic above to do the needful. - 'FIX)) - - -;; Quick-n-dirty test -#_(map :pname - (filter air-too-poisonous? target-planets)) - - - -;; EXERCISE: -;; -;; Understand the next few functions. - - -(defn planet-has-some-good-conds? - "Given a collection of functions that check a planet for - 'good conditions', return true if a given planet satisfies at least - one 'good condition'." - [good-condition-fns planet] - ;;`some` takes a predicate and a collection, and returns true - ;; as soon as it finds an item that returns true for the predicate. - (some (fn [good?] (good? planet)) - good-condition-fns)) - -;; Quick-n-dirty test: -;; - Let's say it's good to be Earth (yay), and -;; - It's good to have carbon dioxide in the atmosphere. - -#_(filter (fn [planet] - (planet-has-some-good-conds? - [earth? carbon-dioxide?] - planet)) - target-planets) - -;; OR we could use `partial`: - -#_(filter (partial planet-has-some-good-conds? - [earth? carbon-dioxide?]) - target-planets) - -;; What does `partial` do? -;; - Retrieve documentation for `partial` using `clojure.repl/doc`. -;; (More on "REPL" utilities later.) - -(clojure.repl/doc partial) ; evaluate and check the console/repl window - -;; Then fix these to make them work: - -#_((partial (fn [a b c] (+ a b c)) - 1) - 'FIX 'FIX) - -#_((partial (fn [a b c] (+ a b c)) - 1 2) - 'FIX) - - -;; EXERCISE: -;; -;; Given a collection of bad conditions, and a planet, return true if -;; the planet has _no_ bad conditions. - -(defn planet-has-no-bad-conds? - [bad-condition-fns planet] - 'FIX) - -;; Quick-n-dirty test: -#_(filter (partial planet-has-no-bad-conds? - [air-too-poisonous? no-atmosphere?]) - target-planets) - -#_(filter (complement ; Aha! Remember `complement`? - (partial planet-has-no-bad-conds? - [air-too-poisonous? no-atmosphere?])) - target-planets) - - - -;; EXERCISE: -;; -;; Let's say that a habitable planet has some good conditions, -;; and NO bad conditions. -;; -;; Define a function that checks true/truthy for this, given -;; good-condition-fns, bad-condition-fns, and a planet. -;; -;; Re-use: -;; - `planet-has-some-good-conds?`, and -;; - `planet-has-no-bad-conds?` - -(defn habitable-planet? - [good-condition-fns FIX1 FIX2] ; <- Fix args - 'FIX) - - -;; EXERCISE: -;; -;; And finally, re-use anything you can from the discussion so far, -;; to write a function that groups a given collection of planets into -;; :habitable, and :inhospitable. -;; -;; The function must internally know what functions will check for -;; good conditions, and what functions check for bad conditions. -;; -;; - Assume it's good to be earth, OR to have carbon-dioxide in the air. -;; - Assume it's bad to have no atmosphere, or to have poison gases. -;; -;; Hint: here's your chance to make good use of `let`, `partial`, -;; and `complement`. And to cleanly return the grouping as a hash-map. - -(defn group-by-habitable - [FIX] - 'FIX) - - -;; Quick-n-dirty test: -(defn colonize-habitable-planets! - [planets] - (let [send-rockets! (fn [p] - (str "Send rockets to " (:pname p) " now!"))] - ((comp (partial map send-rockets!) - :habitable - group-by-habitable) - planets))) - - -#_(colonize-habitable-planets! target-planets) - - - -;; RECAP: -;; -;; Phew! That was a _lot_ of computing. -;; -;; But we did it, with a rather small set of core things and core ideas: -;; -;; Core Clojure things: -;; -;; - named functions -;; - anonymous functions -;; - `let`-bound locals -;; - Sequences: -;; - hash-maps (plus keywords + keyword access) -;; - vectors (plus first and second to get the 1st and 2nd item) -;; - hash-sets (plus their great utility as predicates) -;; - Sequence functions: -;; - map -;; - filter -;; - reduce -;; - some -;; - empty? -;; - Branching and boolean logic: -;; - true/false and truthy/falsey -;; - nil handling -;; - if -;; - when -;; - case -;; - cond -;; - not -;; - "Higher Order" Functions (HOFs), for convenience. HOFs are -;; functions that either take functions as arguments, or -;; return functions as results, or do both, i.e. take functions -;; as arguments _and_ return functions as results. -;; - We introduced these HOFs: -;; - comp -;; - complement -;; - partial -;; - fnil -;; - And you'll see how identity, map, reduce, filter are HOFs too! -;; -;; Core Clojure ideas: -;; -;; - Functions are values. That's why we can: -;; - pass them as arguments, -;; - return them as results of functions, and -;; - even make collections of them (like our "good conditions" and -;; "bad conditions".) -;; -;; - Good functions "compose": -;; We write many small functions that each do one simple task well. -;; And then, we combine and mix-and-match those functions to do -;; increasingly sophisticated tasks. -;; -;; - We tend to think in terms of sequences, and sequence operations. -;; (As opposed to looping operations on items of sequences.) -;; -;; - We strongly prefer to model real-world objects as pure data, -;; then and use many small functions to progressively transform our -;; data models into real-world outcomes. diff --git a/src/clojure_by_example/ex05_immutability_and_fp.clj b/src/clojure_by_example/ex05_immutability_and_fp.clj index 48c80dd..b3e771a 100644 --- a/src/clojure_by_example/ex05_immutability_and_fp.clj +++ b/src/clojure_by_example/ex05_immutability_and_fp.clj @@ -1,6 +1,5 @@ (ns clojure-by-example.ex05-immutability-and-fp) - ;; Ex05: Lesson Goals ;; - This section is more conceptual, than exercise-oriented. ;; @@ -8,10 +7,8 @@ ;; in the final section (and in all our Clojure programs) ;; - All values are immutable by default (and we like it this way) ;; - What `def` is -;; - Lexical scope, and why you should avoid global defs +;; - Why you should avoid global defs ;; - What are "pure functions"? -;; - Convenient syntax for functions with multiple arities, -;; variable arities, and hash-maps or vectors as arguments. ;; ;; - Don't forget to evaluate all s-expressions that interest you, ;; and also feel free to write and play around with your own ones. @@ -141,7 +138,26 @@ other-pi ; what should this be? ;; - It's dangerous because re-defining a var alters it globally. ;; - Why so dangerous? ;; -;; Well, remember functions are values? +;; Well, compare the following. +;; - All three are _incorrect_, because pi is wrong. +;; - But only the third one is actually dangerous. + +(defn scale-by-pi-v1 + [n] + (let [pi 42] + (* pi n))) + +(defn scale-by-pi-v2 + [n] + (* weird-pi n)) + +#_(defn scale-by-pi-v3 + [n] + (def pi 42) + (* n pi)) + + +;; Also, remember functions are values? (fn [x] x) ; is a value (which shall remain anonymous) @@ -184,79 +200,6 @@ other-pi ; what should this be? ;; know what your are doing. - -;; Lexical Scope, and Global Vars - - -(def x 42) ; `x` is a global "var" - -(defn x+ - [y] - (+ x 1)) ; this `x` refers to the global `x` - -(x+ 1) ; will return 43 -(x+ 9) ; will still return 43 - - -(defn x++ - [x] ; this `x` is local to the scope of x++, - ; and will "shadow" the global `x` - (+ x 1)) - -(x++ 1) ; will return 2 -(x++ 9) ; will return 10 - - -(defn x+++ - [x] ; this `x` will shadow the global `x` - (let [x 10] ; but this `x` is local to the let, - ; and will shadow all "outer" x-es - (+ x 1))) - -(x+++ 1) ; will return 11 -(x+++ 9) ; will still return 11 - - -;; Lexical scope guarantees that the reference to a value will be -;; "enclosed" in the scope in which it is being used. - -;; This makes it very easy to reason about where a value originated. -;; - Start at the place of reference of the value. -;; - Then "walk" outwards, until you meet the very first let binding, -;; or arg-list, or def, where the value was bound. -;; - Now you know where the value came from. - - -;; EXERCISE: -;; -;; Reason about what is happening here: - -(let [x 3.141] - ((fn [x] x) x)) - - -;; How about this? What will happen here? - -((fn [x] x) x) - - -;; How about this? - -(let [x x] - ((fn [x] x) x)) - - -;; This? - -((fn [x] x) (let [x x] - x)) - -;; And finally, this? - -((let [x 3.141] - (fn [x] x)) x) - - ;; Lesson: ;; ;; - Clojure programmers use `def` _only_ to attach values to globally @@ -313,222 +256,26 @@ other-pi ; what should this be? ;; functions, or for other impure functions for that matter. - - -;; Convenient Syntax for functions -;; -;; - We use these conveniences for good effect in API design. - - -;; Multiple arities -;; - When we know for sure we have to handle some known numbers -;; of arguments. - -(defn add-upto-three-nums - ([] 0) ; identity of addition - ([x] x) - ([x y] (+ x y)) - ([x y z] (+ x y z))) - -(add-upto-three-nums) -(add-upto-three-nums 1) -(add-upto-three-nums 1 2) -(add-upto-three-nums 1 2 3) -#_(add-upto-three-nums 1 2 3 4) ; will fail - - -;; Variable arity -;; - When we don't know in advance how many arguments we -;; will have to handle, but we want to handle them all. - -(defn add-any-numbers - [& nums] - (reduce + 0 nums)) - -(add-any-numbers) -(add-any-numbers 1) -(add-any-numbers 1 2) -(add-any-numbers 1 2 3 4 5) - - -;; Multiple _and_ Variable arities, combined -;; - Guess what + actually is inside? -;; -#_(clojure.repl/source +) ; evaluate, check the REPL/LightTable console -;; -;; We can implement each arity as a special case, to compute results -;; as optimally as possible. - -(+) -(+ 1) -(+ 1 2 3 4 5 6 7 8 9 0) - - - -;; "De-structuring" -;; - For convenient access to items in collections. - - -;; Suppose a function expects a two-item sequence, we can... - -(defn destructure-tuple-in-strange-ways - [[a b]] ; expects a two-item vector - [[b] - [a] - ["abba" [a b b a]] - {:b b :a a} - (str "baa " b a a " black sheep.")]) - -(destructure-tuple-in-strange-ways [1 2]) - -;; It's like visually matching shapes to shapes. -;; - [1 2] ; structure 1 and 2 in a vector -;; | | -;; - [a b] ; name by position, and use each named value however we wish -;; -;; Said another way: -;; - When we put data into a data structure, we... structure the data. -;; - When we follow the structure of the data structure, but -;; reference each item by name, and "unpack" it for use, we have -;; just "de-structured" the data. - - -;; De-structuring works in `let` and functions: - -(let [[k v] [:a 42]] - {:a 42}) - -((fn [[k v]] {k v}) [:a 42]) - - -;; We can mix-and match de-structuring, for great good. -;; Compare: - -(reduce (fn [acc-map kv-pair] - (assoc acc-map - (first kv-pair) (second kv-pair))) - {:a 42} ; acc-map - {:b 0 :c 7 :d 10}) ; a hash-map is a collection of kv pairs - -(reduce (fn [acc-map [k v]] ; second arg is a tuple, so just destructure - (assoc acc-map k v)) - {:a 42} - {:b 0 :c 7 :d 10}) - - -;; Vectors are ordered collections, which we de-structure by _position_. - -;; Hash-maps are _unordered_ collections. -;; - BUT, they are keyed by named keys. -;; - We can exploit this "pattern" as follows: - -;; Compare this: -(let [make-message (fn [planet] - (str "Planet " (:pname planet) " has " - (:moons planet) " moons."))] - (make-message - {:pname "Mars" :moons 2})) - - -;; With this... -(let [make-message (fn [{:keys [pname moons]}] - (str "Planet " pname " has " - moons " moons."))] - (make-message - {:pname "Mars" :moons 2})) - - -;; And we can further... -(let [make-message (fn [{:keys [pname moons]}] - (str "Planet " pname " has " - (or moons 0) " moons."))] - (map make-message [{:pname "Earth" :moons 1} - {:pname "Mars" :moons 2} - {:pname "Moonless"}])) - - -;; We can also alias the whole hash-map: - -(defn add-message-1 - [{:keys [pname moons] - :as planet}] ; alias the hash-map as `planet` - (assoc planet - :message (str "Planet " pname " has " - (or moons 0) " moons."))) - -(map add-message-1 [{:pname "Earth" :moons 1} - {:pname "Mars" :moons 2} - {:pname "Moonless"}]) - - -;; Finally, we can specify default values directly in the destructuring: - -(defn add-message-2 - [{:keys [pname moons] - :or {moons 0} ; use 0, if :moons is absent - :as planet}] - (assoc planet - :message (str "Planet " pname " has " - moons " moons."))) - -(map add-message-2 [{:pname "Earth" :moons 1} - {:pname "Mars" :moons 2} - {:pname "Moonless"}]) - - -;; Further, we can exploit combinations of de-structuring -;; -;; - Suppose we have a hash map, keyed by planet names: -;; -{"Earth" {:moons 1} - "Mars" {:moons 2} - "Moonless" {}} -;; -;; - Recall: a hash-map is like a collection of key-value pairs/tuples -;; -;; - Now, we can exploit vector and map de-structuring, in combination: -;; - -(defn add-message-3 - [acc-map [pname {:keys [moons] - :or {moons 0} - :as pdata}]] - (let [msg (str "Planet " pname " has " moons " moons.")] - (assoc acc-map - pname (assoc pdata :message msg)))) - -(reduce add-message-3 - {} ; acc-map - {"Earth" {:moons 1} - "Mars" {:moons 2} - "Moonless" {} - "Nomoon" nil}) - - -;; There are _many_ many ways of de-structuring. -;; - Here's a really nice post detailing it: -;; cf. http://blog.jayfields.com/2010/07/clojure-destructuring.html - - - ;; RECAP: +;; ;; - Clojure values are immutable by default, and we prefer it that way ;; ;; - `def` is best used only to define names for truly global values. +;; Use `let` to bind local values, to get all the benefits of strict +;; lexical scope. ;; ;; - `defn` is just a wrapper over `def`, designed specifically to ;; define functions. ;; -;; - We exploit lexical scope to bind values as close as possible to the -;; point of use in code. This greatly improves our ability to reason -;; about our code. And it prevents an explosion of global `def`s. -;; ;; - Write pure functions as far as possible. + +;; 4clojure Drills: Problems you could try now. ;; -;; - Conveniences like multi-arity and variable-arity functions, with -;; argument de-structuring, help us design better functional APIs. -;; -;; - We can mix-and match these facilities, for even more convenience. +;; - #protip: Write the solutions as proper named functions in your code base, +;; without code-golfing or hacks. Then translate to anonymous function form +;; that 4clojure requires. ;; -;; - Next, we will see how to "keep state at the boundary", and -;; keep the majority of our core logic purely functional. +(comment + (map (fn [problem-no] (str "https://4clojure.oxal.org/#/problem/" + problem-no)) + [51, 77, 60, 102, 86, 115])) diff --git a/src/clojure_by_example/ex06_full_functional_firepower.clj b/src/clojure_by_example/ex06_full_functional_firepower.clj index 088590f..3524172 100644 --- a/src/clojure_by_example/ex06_full_functional_firepower.clj +++ b/src/clojure_by_example/ex06_full_functional_firepower.clj @@ -1,4 +1,6 @@ -(ns clojure-by-example.ex06-full-functional-firepower) +(ns clojure-by-example.ex06-full-functional-firepower + (:require [clojure.data.json :as json] + [clojure.java.io :as io])) ;; Ex06: Lesson Goals ;; - This is more of a code-reading section, designed to: @@ -115,8 +117,8 @@ ;; - Document the schema as in-line comments, for this dirty prototype. ;; (def sensor-data-files - {;; {"Planet Name":{"radius":}, ...} - :planets "planet_detector.json" + ;; {"Planet Name":{"radius":}, ...} + {:planets "planet_detector.json" ;; {"Planet Name":, ...} :moons "moon_detector.json" @@ -144,11 +146,12 @@ ;; ---------------------- Input Boundary begins ------------------------ -(defn ingest-json-file +(defn ingest-json-file! [dir-path file-name] (let [file-path (str dir-path file-name)] - (with-open [reader (clojure.java.io/reader file-path)] - (clojure.data.json/read reader)))) + (with-open [reader (io/reader file-path)] + (json/read reader + :key-fn keyword)))) (defn gather-all-sensor-data! @@ -159,8 +162,8 @@ (let [ingest-sensor-data (fn [out-map [sensor-key sensor-file]] (assoc out-map - sensor-key (ingest-json-file data-dir - sensor-file)))] + sensor-key (ingest-json-file! data-dir + sensor-file)))] (reduce ingest-sensor-data {} ; out-map starts empty sensor-files-map))) @@ -200,26 +203,24 @@ atmosphere)) -(defn denormalise-planetary-data +(defn denormalise-planetary-data* "Given a hash-map of planetary data (keyed by planet names), return just the planetary data, with the planet's names added in. Also ensure all keys are keywordized, for convenient look-ups." [planets] (map (fn [[pname pdata]] - (let [keywordized-pdata (clojure.walk/keywordize-keys - pdata)] - (assoc keywordized-pdata - :name pname))) + (assoc pdata + :name (name pname))) planets)) -(defn denormalised-planetary-data +(defn denormalise-planetary-data "Given all sensor data, produce a collection of denormalized planetary data." [{:keys [planets atmosphere moons] :as all-sensor-data}] - ((comp denormalise-planetary-data + ((comp denormalise-planetary-data* (partial add-atmosphere-data atmosphere) (partial add-moon-data moons)) planets)) @@ -229,26 +230,26 @@ ;; First try this, to check packaged data... -#_(denormalised-planetary-data +#_(denormalise-planetary-data (gather-all-sensor-data! sensor-data-dir sensor-data-files)) ;; Does the result look familiar? -(defn write-out-json-file +(defn write-out-json-file! [dir-path file-name data] (let [file-path (str dir-path file-name)] - (with-open [writer (clojure.java.io/writer file-path)] - (clojure.data.json/write data writer)))) + (with-open [writer (io/writer file-path)] + (json/write data writer)))) (defn ingest-export-sensor-data! [data-dir source-data-files dest-data-file] - (write-out-json-file + (write-out-json-file! data-dir dest-data-file - (denormalised-planetary-data + (denormalise-planetary-data (gather-all-sensor-data! data-dir source-data-files)))) @@ -268,15 +269,10 @@ ;; The Planetary Data Scientists reveal their nefarious intentions: -#_(clojure-by-example.ex04-control-flow/colonize-habitable-planets! - (denormalised-planetary-data - (gather-all-sensor-data! sensor-data-dir - sensor-data-files))) -;; Note: -;; - For this to work, colonize-habitable-planets must be complete -;; - And you should have first evaluated it, to make it unable -;; - Which means, you have to first correctly solve ex04 - +;; (colonize-habitable-planets! ; deviously implemented by them, elsewhere +;; (denormalise-planetary-data +;; (gather-all-sensor-data! sensor-data-dir +;; sensor-data-files))) ;; RECAP: diff --git a/src/clojure_by_example/ex07_boldly_go.clj b/src/clojure_by_example/ex07_boldly_go.clj index e586f04..8e7caf1 100644 --- a/src/clojure_by_example/ex07_boldly_go.clj +++ b/src/clojure_by_example/ex07_boldly_go.clj @@ -1,4 +1,4 @@ -(ns clojure-by-example.utils.ex07-boldly-go) +(ns clojure-by-example.ex07-boldly-go) ;; EX07: Lesson Goals @@ -24,7 +24,7 @@ ;; lein new planet_coloniser ;; ;; -;; * Open the directory in LightTable and observe its structure. +;; * Open the directory in your IDE and observe its structure. ;; ;; ;; * Create a `utils` directory in which to put I/O utility functions. @@ -34,7 +34,7 @@ ;; - Terminal: ;; mkdir src/planet_coloniser/utils ;; -;; - Or, LightTable: +;; - Or, in your IDE: ;; - right-click on src/planet_coloniser, and create new folder ;; ;; @@ -42,7 +42,7 @@ ;; - `ingest.clj` and ;; - `export.clj` ;; -;; - Again, you can right-click on your `utils` dir in LightTable, +;; - Again, you can right-click on your `utils` dir in your IDE, ;; and create New File from the pop-up menu. ;; ;; @@ -53,7 +53,11 @@ ;; (ns planet-coloniser.utils.ingest) ;; ;; - Observe the dir name is planet_coloniser, but the ns -;; declaration has planet-coloniser. Why? +;; declaration has planet-coloniser. This is the convention: +;; - hyphens separate words in ns names +;; - dots separate directories and files in ns names +;; - underscores from dir or file names become hyphens in ns names +;; - and ns names are all lower case ;; ;; - Copy-paste the "input" function definitions from ex06, ;; below the ns declaration. @@ -66,13 +70,14 @@ ;; ;; - Inside `project.clj`, update :dependencies value to look like: ;; -;; :dependencies [[org.clojure/clojure "1.8.0" ; pre-existing +;; :dependencies [[org.clojure/clojure "1.10.0" ; latest as of 01 Jan 2019 ;; [org.clojure/data.json "0.2.6"]] ; add for `ingest` ;; ;; - Inside `injest.clj`, update the ns declaration to look like: ;; ;; (ns planet-coloniser.utils.ingest -;; (:require [clojure.data.json])) +;; (:require [clojure.data.json :as json] +;; [clojure.java.io :as io])) ;; ;; ;; * Update `export.clj`. Open the file and: @@ -85,8 +90,9 @@ ;; ;; - Also ensure, the ns form looks like this: ;; -;; (ns planet-coloniser.utils.export -;; (:require [clojure.data.json])) ; we use this in export too +;; (ns planet-coloniser.utils.ingest +;; (:require [clojure.data.json :as json] +;; [clojure.java.io :as io])) ;; ;; ;; * Create `sensor_processor.clj`, for our core "pure" logic: @@ -112,28 +118,27 @@ ;; (:gen-class) ; add this, and the :require expression below: ;; (:require [planet-coloniser.sensor-processor :as sensproc] ;; [planet-coloniser.utils.ingest :as ingest] -;; [planet-coloniser.utils.export :as export] -;; [clojure.walk :as cwalk])) +;; [planet-coloniser.utils.export :as export])) ;; ;; - Copy `ingest-export-sensor-data!` from ex06 ;; ;; - Rename it to `-main`. ;; ;; - Now, make the body of `-main` look like the function below: -;; - notice prefixes to functions, like cwalk/, export/, ingest/ +;; - notice prefixes to functions, like export/, ingest/, sensproc/ ;; - Why do we do this? ;; ;; (defn -main ;; [data-dir source-data-files dest-data-file] -;; (let [source-data-files -;; (cwalk/keywordize-keys -;; (ingest/ingest-json-file data-dir -;; source-data-files))] -;; (export/write-out-json-file -;; data-dir -;; dest-data-file -;; (sensproc/denormalized-planetary-data -;; (ingest/gather-all-sensor-data! data-dir source-data-files))))) +;; (let [source-data-files (ingest/ingest-json-file! data-dir +;; source-data-files) +;; export-as-json (partial export/write-out-json-file! +;; data-dir +;; dest-data-file)] +;; (export-as-json +;; (sensproc/denormalise-planetary-data +;; (ingest/gather-all-sensor-data! data-dir +;; source-data-files))))) ;; ;; ;; * Let's bring in sensor data, for convenience: diff --git a/src/clojure_by_example/ex08_but_before_we_go.clj b/src/clojure_by_example/ex08_but_before_we_go.clj index 837c0c1..bd1df7b 100644 --- a/src/clojure_by_example/ex08_but_before_we_go.clj +++ b/src/clojure_by_example/ex08_but_before_we_go.clj @@ -2,16 +2,13 @@ ;; But before we boldly go, here are some resources to help us on our journey! -;; Communities: -;; Clojurians Slack: http://clojurians.net/ -;; IN/Clojure Open Relay: https://open.relay-chat.com/signup_user_complete/?id=inclojure -;; IN/Clojure Slack: https://join.slack.com/t/inclojure/shared_invite/enQtMjkzNDcyMjk1NDYwLWE4MzljNGRlZjcwZTRlYWFkODM3Mzc1NmU0M2Q3NjIxZmQ2NTYyZGU3MGVmZGJlMmFjYzBlNWM2Y2IwMjk0Y2Q -;; Clojure Subreddit: https://www.reddit.com/r/Clojure/ +;; Check out the official website: https://clojure.org -;; Books: -;; https://www.braveclojure.com/ -- free to read online -;; The Joy of Clojure -- http://www.joyofclojure.com/ +;; It has been accumulating many useful tutorials, guides, book references, +;; community resources, and information about companies using Clojure. +;; Also, here are a few things we really like and find useful to aid understanding +;; at various levels; from the philosophical to the here and now: ;; Talks/Philosphy: ;; https://www.youtube.com/watch?v=wASCH_gPnDw -- Inside Clojure with Brian Beckman and Rich Hickey @@ -29,7 +26,6 @@ ;; - Scroll down to the "Debugging Clojure" section: ;; https://aphyr.com/posts/319-clojure-from-the-ground-up-debugging - ;; Handy REPL utils: ;; ;; - clojure.repl/ diff --git a/src/clojure_by_example/utils/core.clj b/src/clojure_by_example/fun/inspect_nasa_planets.clj similarity index 97% rename from src/clojure_by_example/utils/core.clj rename to src/clojure_by_example/fun/inspect_nasa_planets.clj index 54a48a3..ac5dc56 100644 --- a/src/clojure_by_example/utils/core.clj +++ b/src/clojure_by_example/fun/inspect_nasa_planets.clj @@ -1,4 +1,4 @@ -(ns clojure-by-example.utils.core +(ns clojure-by-example.fun.inspect-nasa-planets (:require [net.cgrand.enlive-html :as html] [clojure.string :as cs] [clojure.inspector :as inspect])) @@ -131,7 +131,7 @@ (map :content) flatten ((partial map - (fn [s] (cs/replace s #" " "")))) + (fn [s] (cs/replace s #"�" "")))) rest ; get rid of empty column's label (map keywordize)) massage-row-label #(cond (#{:a} (:tag %)) @@ -148,7 +148,7 @@ (partial map massage-row-label) :content))) cleanup-stats (comp (partial partition num-cols) - (partial filter #(not= " " %)) + (partial filter #(not= "�" %)) flatten #(map :content %) #(take num-stats %) diff --git a/src/clojure_by_example/fun/workshop_fmt.clj b/src/clojure_by_example/fun/workshop_fmt.clj new file mode 100644 index 0000000..ff6d423 --- /dev/null +++ b/src/clojure_by_example/fun/workshop_fmt.clj @@ -0,0 +1,102 @@ +(ns clojure-by-example.fun.workshop-fmt + (:require [clojure.edn :as edn] + [clojure.java.io :as io] + [rewrite-clj.zip :as z]) + (:import java.io.PushbackReader)) + +(def relative-src-path "src/clojure_by_example/") +(def relative-dest-path (str relative-src-path "workshop/")) +(def workshop-file-pattern (re-pattern "ex.*.clj")) + + +(defn elide-if-comment + [zloc] + (if (= (z/value zloc) 'comment) + (-> zloc z/up z/remove) + zloc)) + + +(defn elide-comment-forms + [zloc] + (if (z/end? zloc) + zloc + (recur (-> zloc elide-if-comment z/next)))) + + +(defn workshop-files! + [sourcedir file-pattern] + (->> (io/file sourcedir) + .listFiles + (filter #(not (.isDirectory %))) + (map #(.getName %)) + (filterv (fnil #(re-find file-pattern %) "")))) + + +(defn spit-root! + [zloc outfile] + (with-open [w (io/writer outfile :encoding "UTF-8")] + (z/print-root zloc w))) + + +(defn prep-workshop-code! + ([infile outfile] + (-> (z/of-file infile) + elide-comment-forms + (spit-root! outfile))) + ([infile indir outdir] + (prep-workshop-code! + (str indir infile) + (str outdir infile)))) + + +(comment + ;; Try one... + (prep-workshop-code! "ex06_full_functional_firepower.clj" + relative-src-path + relative-dest-path) + + ;; Review output using: + ;; ls -1 | grep ex | xargs -I {} diff -s {} "workshop/"{} + (doseq [f (sort (workshop-files! relative-src-path + workshop-file-pattern))] + (prep-workshop-code! f + relative-src-path + relative-dest-path)) + ) + + +(comment + ;; Experiment with file as EDN + (defn read-forms [file] + ;; https://stackoverflow.com/questions/39976680/reading-another-clojure-program-as-a-list-of-s-expressions + (let [rdr (-> file io/file io/reader PushbackReader.) + sentinel (Object.)] + (loop [forms []] + (let [form (edn/read {:eof sentinel} rdr)] + (if (= sentinel form) + forms + (recur (conj forms form))))))) + ) + +(comment + ;; Experiments with zippers + + ((comp z/child-sexprs z/down) (z/of-file "foobar.clj")) + + (spit-root! + (z/prewalk (z/of-file "foobar.clj") + #(= (z/value %) 'comment) + z/remove) + "foobar-cleaned.clj") + + + (let [zloc (z/of-file (str sourcedir "foobar.clj")) + outfile (str sourcedir "foobar-cleaned.clj")] + (with-open [w (clojure.java.io/writer outfile :encoding "UTF-8")] + (loop [znodes zloc] + (if (z/end? znodes) + (z/print-root znodes w) + (recur (if (= (z/value znodes) 'comment) ; for fanciness, this could be a comment macro by another name, like `extra-explanation' + ((comp z/next z/remove z/up) znodes) + (z/next znodes))))))) + ) diff --git a/src/clojure_by_example/workshop/.gitignore b/src/clojure_by_example/workshop/.gitignore new file mode 100644 index 0000000..7ddd3d9 --- /dev/null +++ b/src/clojure_by_example/workshop/.gitignore @@ -0,0 +1 @@ +*.clj diff --git a/src/clojure_by_example/workshop/README.txt b/src/clojure_by_example/workshop/README.txt new file mode 100644 index 0000000..8396100 --- /dev/null +++ b/src/clojure_by_example/workshop/README.txt @@ -0,0 +1,7 @@ +DO NOT commit clj files here. + +This directory is a target for generated clj source files. + +A reformatter will read "full featured" source meant for at-home use, +and dump an elided, leaner version here for in-class use (for the +teacher's convenience).