├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── materials ├── 1-first-things-first │ ├── 1-introduction.md │ ├── 2-installation.md │ ├── 3-clojure-and-IDE.md │ └── README.md ├── 2-little-bit-of-repl │ ├── 1-leiningen-101.md │ ├── 2-using-repl.md │ ├── 3-hands-on-repl.md │ └── README.md ├── 3-first-project │ ├── 1-minimum-viable-product.md │ ├── 2-running-and-building-with-leiningen.md │ ├── 3-refactoring-project.md │ ├── 4-testing-in-clojure.md │ └── README.md ├── 4-state-in-clojure │ ├── 1-adding-to-data-structures.md │ ├── 2-storing-state-with-atom.md │ ├── 3-conditional-love.md │ ├── 4-terminal-interface.md │ ├── 5-creating-mvp.md │ ├── 6-not-so-mvp.md │ ├── 7-no-need-for-atoms.md │ └── README.md ├── 5-requests-for-data │ ├── 1-http-requests.md │ ├── 2-filtering-data.md │ ├── 3-reducing-complexity.md │ ├── 4-reducing-with-our-rules.md │ ├── 5-partial-meetings.md │ ├── 6-give-me-links.md │ ├── 7-console-ui.md │ ├── 8-function-with-no-name.md │ ├── 9-meet-threading-macros.md │ └── README.md ├── 6-simple-web-application │ ├── 1-setting-up.md │ ├── 2-hello-reitit.md │ ├── 3-query-parameters.md │ ├── 4-body-parameters.md │ ├── 5-path-parameters.md │ ├── 6-request-coercion.md │ ├── 7-response-coercion.md │ ├── 8-testing-web-applications.md │ ├── 9-mocking.md │ └── README.md ├── README.md └── final-words.md └── projects ├── calculator-api ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc │ └── intro.md ├── project.clj ├── src │ └── calculator_api │ │ ├── core.clj │ │ └── server │ │ └── server.clj └── test │ └── calculator_api │ └── server │ └── server_test.clj ├── no-atom-shopping-list ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc │ └── intro.md ├── no-atom-shopping-list.iml ├── project.clj ├── src │ └── no_atom_shopping_list │ │ └── core.clj ├── test │ └── no_atom_shopping_list │ │ └── core_test.clj └── things-to-buy.txt ├── reddit-analyser ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc │ └── intro.md ├── project.clj ├── src │ └── reddit_analyser │ │ └── core.clj └── test │ └── reddit_analyser │ └── core_test.clj ├── shopping-list ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc │ └── intro.md ├── project.clj ├── shopping-list.iml ├── src │ └── shopping_list │ │ └── core.clj ├── test │ └── shopping_list │ │ └── core_test.clj └── things-to-buy.txt └── sum-em-up ├── .gitignore ├── .java-version ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc └── intro.md ├── numbers.txt ├── project.clj ├── src └── sum_em_up │ └── core.clj ├── sum-em-up.iml └── test └── sum_em_up └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.vscode/ 3 | /sandbox 4 | *.idea/ 5 | /teachers-notes 6 | *.iml 7 | 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at itstartswithclojure@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to It Starts with Clojure 2 | 3 | The project is very open for contributions and all contributors are welcomed with open arms. 4 | If you feel like contributing to this project there is few ways that can be done. 5 | Additionally, if you come up with other helpful ways, don't be afraid to suggest your approach. 6 | 7 | ## How to contribute 8 | 9 | ### Easy way: Fixing issues 10 | 11 | The easiest way to contribute to the project is by solving issues. 12 | There are many kinds of issues available in the project ranging from: 13 | "I don't have time for this for now" to "I can't do this by myself". 14 | Feel free to choose any of the open issues and get your hands dirty. 15 | 16 | ### Fast way: Creating new issues 17 | 18 | It sounds bad, but it is not. 19 | We love issues. They are a great way to communicate flaws, needs and ideas. 20 | 21 | If you find a mistake, but don't have time to fix it. Create an issue. 22 | 23 | If you find a bug, but don't know how to fix it. Create an issue. 24 | 25 | If you find part, section or chapter confusing or misleading. Create an issue. 26 | 27 | If you think we are generally missing an important topic. Create an issue. 28 | 29 | ### Creative way: Creating practice projects 30 | 31 | I sometimes have hard time coming up with interesting and educational case projects. 32 | If you think you have a cool idea for a project that the course could cover in order to learn valuable lessons, 33 | you can create pull request of such a project. 34 | 35 | Place your project idea into *it-starts-with-clojure/projects/* 36 | 37 | If the project fits the course, I will try to embed it into the materials and write chapters around it. 38 | 39 | Please note that writing instructions for the code takes much more time than writing the code, 40 | thus i might not be able to do this immediately. 41 | 42 | ### Punctual way: Fixing typos and proof reading 43 | 44 | This is a bit personal, but I have light dyslexia. Even though I use plugins to check for typos, 45 | they still sometimes slip in. 46 | 47 | Secondly, I am not a native speaker and I write rather fast, 48 | thus I make mistakes and/or just write weirdly. 49 | 50 | In both of these cases I invite you to help me to make this guide better by fixing my mistakes. 51 | If you see issues like this, feel free to fix the and make pull request. 52 | 53 | ## All help is always welcome 54 | 55 | Regardless how you contribute, I welcome your assistance. 56 | I never intended this guide to be a solo project, 57 | thus I really hope there is others who are interested in this sort of projects. 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vili Heikkilä 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # It starts with Clojure: a practical guide to Clojure 2 | 3 | This a guide (something between a tiny book and a course) how to get started with Clojure. 4 | The actual reading materials are under **./materials/** where they are grouped by sections. 5 | Materials are in Markdown so you can easily read the directly from GitHub, 6 | or you can fork the repo to your own computer and make notes while reading. 7 | When you come across a section that you feel has issues or could be somehow better, 8 | feel free to make pull requests or issues out of them. 9 | 10 | The goal is to add materials, projects and exercises to this project, 11 | so it can be used as perfect getting started point for to-be-clojurists. 12 | 13 | To get started with the guide click link below 14 | 15 | ## [Get Started with materials](./materials/) 16 | -------------------------------------------------------------------------------- /materials/1-first-things-first/1-introduction.md: -------------------------------------------------------------------------------- 1 | # It starts with Clojure: a practical guide to Clojure 2 | 3 | ## Who is this guide for 4 | 5 | This guide is aimed for programers with prior experience in some other language. 6 | We will not be covering very basics of programming (at least intentionally). 7 | Unlike most clojure materials, 8 | this guide does not expect the reader to have experience in Java, 9 | even though can be helpful. 10 | 11 | ## Origins of this guide 12 | 13 | While getting started with clojure myself I noticed that there was relatively little 14 | materials available in comparison to many other languages. 15 | This is obviously expected, 16 | since Clojure is not as mainstream as Python, Java or JavaScript. 17 | But the other thing I noticed was that most of the material available is rather academic on their approach, 18 | and they tended to focus strongly on Function Programming (FP) instead of how to build things on Clojure. 19 | 20 | I found this quite annoying since even though I was interested in FP, 21 | my primary interest was to start building actual things with Clojure. 22 | Many guides/books play extensively in REPL (Read-Eval-Print-Loop), 23 | instead of actually building anything that can be deployed or even run from command line. 24 | 25 | ## Purpose of this guide 26 | 27 | Purpose of this guide is to help anyone interested in Clojure to get started and to 28 | build real world applications with the language instead of going to the deep-end 29 | with wonderful functionalities of FP and Clojure. 30 | If favor of getting up and running with Clojure, 31 | this guide focuses on building real world applications, 32 | instead of taking a deep-dive to fine functionalities of Clojure and FP paradigm. 33 | When purposefully skipping parts of the language, 34 | we will try to at least mention them, 35 | or provide some links and 3rd party materials to learn about these things. 36 | 37 | **In no way is this guide aiming to be a complete guide or even complete introduction to the language or the FP paradigm.** 38 | The purpose is to provide enough knowledge to get started and guide readers who wish to learn more to other sources. 39 | 40 | The guide is aiming to be a Open Source living document, 41 | to which others are welcome to commit to and be part of. 42 | 43 | ## How to read this guide 44 | 45 | Each sub folder of **./materials/** can be considered as a chapter, 46 | and each .md file inside a folder can be considered a section. 47 | 48 | While reading this guide it is recommended writing all the parts on your own, 49 | not just copying what is written here. 50 | No one ever learned anything from copy-pasting. 51 | 52 | In the context of evaluating code or using REPL, 53 | we will be using symbol `;=>` to express the output of the code. 54 | 55 | Like this: 56 | 57 | ```clojure 58 | (+ 1 2 3) 59 | ;=> 6 60 | ``` 61 | 62 | Many of the headings in these documents are actually hyperlinks, 63 | and in most of the cases they lead to a section of official Clojure documentation. 64 | If the explanations of this guide feel vague or too brief, 65 | or you simply wish to know more about the topic at hand, 66 | it is encouraged to browse through these documentations. 67 | (IDE section makes an exception here. Links there lead to god knows where.) 68 | 69 | Next: [Installation](2-installation.md) 70 | -------------------------------------------------------------------------------- /materials/1-first-things-first/2-installation.md: -------------------------------------------------------------------------------- 1 | # Installing Clojure 2 | 3 | Installing Clojure can be sometimes be more difficult than it should be. 4 | In general you always need three basic parts. 5 | 6 | 1. Java 7 | 2. Clojure 8 | 3. Leiningen (build automation and dependency management tool) 9 | 10 | Even though Clojure itself supports latest version of Java, 11 | many libraries still do not. 12 | To avoid some pain with this we will be using Java 1.8. 13 | Feel free to use newer versions. 14 | If things stop working try changing your Java version. 15 | 16 | ## Linux 17 | 18 | ### Installing Java 1.8 on Linux 19 | 20 | Installation of Java on Linux varies by distribution. 21 | On Fedora, the package names and even the package manager has changed over time. 22 | On Ubuntu there are similar complications. 23 | However, if you're smart enough to work out Linux, 24 | you can work out how to install Java. 25 | 26 | On Ubuntu 18.04: 27 | 28 | ```sh 29 | sudo apt update 30 | sudo apt install openjdk-8-jdk 31 | ``` 32 | 33 | On Fedora 30 (and later): 34 | 35 | ```sh 36 | sudo dnf install java-1.8.0-openjdk-devel 37 | ``` 38 | 39 | ### Installing Clojure on Linux 40 | 41 | On any Linux distribution you can get the latest Clojure by following 42 | the [instructions](https://clojure.org/guides/getting_started#_installation_on_linux) on clojure.org. 43 | 44 | ### Installing Leiningen on Linux 45 | 46 | On any Linux distributionyou can get the latest Leiningen by following 47 | the [instructions](https://leiningen.org) on leiningen.org 48 | 49 | ## Mac 50 | 51 | My rule of thumb for Mac is to install everything with brew if possible with ease. 52 | Some might disagree and they are free to do so. 53 | Feel welcome to write non-brew part of Mac installations here. 54 | 55 | ### Install homebrew 56 | 57 | As mentioned above this installation guide leans heavily on homebrew (a.k.a. brew). 58 | Thus we will start by installing the said brew. 59 | 60 | The official instructions are available [here](https://brew.sh). 61 | 62 | But really the only thing you need to do is to execute this: 63 | 64 | ```sh 65 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 66 | ``` 67 | 68 | That being done you have brew installed on your Mac. 69 | 70 | ### Install Java 1.8 on Mac 71 | 72 | ```bash 73 | brew update 74 | brew tap adoptopenjdk/openjdk 75 | brew install --cask homebrew/cask-versions/adoptopenjdk8 76 | ``` 77 | 78 | ### Install Clojure on Mac 79 | 80 | ```bash 81 | brew update 82 | brew install clojure 83 | ``` 84 | 85 | ### Install Leiningen on Mac 86 | 87 | ```bash 88 | brew update 89 | brew install leiningen 90 | ``` 91 | 92 | ### Install jEnv (non-obligatory) 93 | 94 | Many of us need newer Java versions for our other projects. 95 | To handle multiple Java versions I recommend using jEnv. 96 | jEnv is wonderful little tool for deciding, 97 | which Java version you wish to use in each project. 98 | 99 | ```bash 100 | brew update 101 | brew install jenv 102 | ``` 103 | 104 | jEnv requires some configuring. 105 | Please see instructions from their [website](https://www.jenv.be/). 106 | It is really easy, so there is no reason to worry. 107 | 108 | If regardless of my advice or for absolute necessity you decide to pursue the Windows installation, 109 | follow the instructions below. 110 | I will not try to write exact instructions here, 111 | but instead I'll provide sources you should use. 112 | This is due to the fact that these things might change, 113 | and I don't use Windows, 114 | so I cannot guarantee that the instructions stay up-to-date. 115 | 116 | ## Windows 117 | 118 | Clojure on Windows is not on very mature state, 119 | so if you have an option to use Linux or Mac I would advise you to do so. 120 | 121 | ### Installing Java 1.8 on Windows 122 | 123 | Follow the instructions from [adoptopenjdk.net](https://adoptopenjdk.net/). 124 | 125 | ### Installing Clojure on Windows 126 | 127 | Follow the instructions from official Clojure [documentation](https://clojure.org/guides/getting_started#_installation_on_windows). 128 | 129 | ### Installing Leiningen on Windows 130 | 131 | Follow the instructions from official Leiningen [documentation](https://leiningen.org/) 132 | 133 | Next: [Clojure and IDEs](3-clojure-and-IDE.md) 134 | -------------------------------------------------------------------------------- /materials/1-first-things-first/3-clojure-and-IDE.md: -------------------------------------------------------------------------------- 1 | # Clojure and IDEs 2 | 3 | When I personally first time ever encountered Clojure and tried toying with it, 4 | the editors were HUGE pain in the ass and one of the reasons why I did not find the language pleasant at all. 5 | Due to this I decided to dedicate a tiny chapter for this issue in the guide. 6 | No one should give up on Clojure due to poor IDE as I did in 2015. 7 | 8 | For programming in general there is a ton of great editors available. 9 | Some of them are better suited for some languages than others. 10 | As an example of this, writing Java with IntelliJ IDEA is quite much easier than doing the said task with [Atom](https://atom.io/). 11 | In other words different tools are suitable for different tasks. 12 | Due to the fact that Clojure is a LISP it requires an editor with a bit different capabilities than most other languages. 13 | Also due to the fact that it is not Object Oriented Programming (OOP) language, 14 | IntelliSense is not going to provide as much help as it does in other languages. 15 | 16 | Below is rather opinionated guide to IDEs for those who aim to get their hands dirty with Clojure. 17 | 18 | ## [Visual Studio Code](https://code.visualstudio.com/) + [Calva](https://marketplace.visualstudio.com/items?itemName=betterthantomorrow.calva) (recommended for getting started) 19 | 20 | Visual Studio Code (VSC) is a great editor in general. 21 | It might not be the ideal editor for Clojure, 22 | but I recommend it because it very easy to setup and get started with. 23 | 24 | ### How to install Visual Studio Code 25 | 26 | 1. Easiest way to install VSC is to download it from their website: 27 | https://code.visualstudio.com/ 28 | 29 | 2. After this you have to install [Calva plugin](https://marketplace.visualstudio.com/items?itemName=betterthantomorrow.calva). 30 | This will provide linting and easy access to REPL. Calva plugin can be installed from VSC Marketplace. 31 | 32 | 3. With Calva plugin correctly installed we can connect VSC to running REPL 33 | 34 | ## Other options 35 | 36 | ### [IntelliJ IDEA](https://www.jetbrains.com/idea/) + [Cursive](https://cursive-ide.com/) 37 | 38 | Even though IDEA is superb editor for Java and many other languages, 39 | it does not really do well with Clojure without some additional help. 40 | This additional help is a magnificent plugin known as Cursive. 41 | Together these two create a programming environment loved by many. 42 | The unfortunately this requires a bit of configuring and is not completely free. 43 | Neither IDEA or Cursive is free, even though there is free options available from both. 44 | If you decide to use this combination, 45 | please make sure that you either purchase the required licenses, 46 | or only work in projects that are suitable for the free licenses of both of the products. 47 | 48 | As I mentioned there are plenty of editors out there. 49 | As usual [Slant](https://www.slant.co/topics/11929/~ide-for-clojure) provides a nice comparison between the options. 50 | 51 | ### [Emacs](https://www.gnu.org/software/emacs/) + [Cider](https://github.com/clojure-emacs/cider) 52 | 53 | Emacs is a classic editor in Clojure circles for a reason. 54 | It is powerful and extremely well suited for for LISPs like Clojure. 55 | With that being said I cannot with good conscience recommend it for a new born Clojurist. 56 | While being a powerful editor, Emacs has a bit nasty learning curve. 57 | Clojure requires a rather different way of thinking than many other languages. 58 | While getting used to this new way of thinking, 59 | I would not recommend trying to learn a new kind of editor at the same time. 60 | 61 | ### [Vim](https://www.vim.org/) + [Fireplace](https://github.com/tpope/vim-fireplace) 62 | 63 | Vim and Fireplace both take time to learn and configure. 64 | But some people are really passionate about Vim so this option has to be pointed out for them. 65 | 66 | ### [NightCode](https://sekao.net/nightcode/) 67 | 68 | Editor especially made for Clojure. 69 | Worth testing out. 70 | It provides instant REPL and is especially nice for learning the language. 71 | 72 | ### [Atom](https://atom.io/) 73 | 74 | Jacek Schae has written a [nice article](https://medium.com/@jacekschae/slick-clojure-editor-setup-with-atom-a3c1b528b722) about using Atom in Clojure development. 75 | In case you interested read follow his suggestions. 76 | 77 | ## Structural Editing 78 | 79 | Practically all Clojure IDEs support something known as Structural Editing. 80 | It is a method of editing your code where we are able to move parentheses around with flexibility. 81 | This is common way of editing for all LISPs. 82 | In general there is usually two options for Structural editing. 83 | Namely Paredit and Parinfer. 84 | Almost all editors provide support for Paredit, but many also support Parinfer. 85 | 86 | The way how these features work in different editors vary a bit, 87 | but regardless of that the basic ideas are very similar. 88 | 89 | I will not walk you through how the these features work, 90 | since it dependent on your editor of choice. 91 | Nevertheless I want you to look for these features in your editor and play around with them a bit when you write your code. 92 | 93 | Using Structural Editing can feel a bit weird in the beginning, 94 | but you will quickly get grasp of it and it will enhance your coding experience when working with Clojure. 95 | 96 | In Calva you can try pressing 97 | 98 | - ctrl + left/right arrow key 99 | - ctrl + shift + left/right arrow key 100 | 101 | See how these commands move the beginning and ending brackets of the form that your cursor is in. 102 | This a very helpful tool when you will be editing your Cloure code. 103 | 104 | Try experimenting with this feature while you code. 105 | 106 | With other editor is suggest that you google for the instructions for Structural Editing and Paredit. 107 | 108 | Next: [Chapter 2 - Little Bit of REPL](../2-little-bit-of-repl) 109 | -------------------------------------------------------------------------------- /materials/1-first-things-first/README.md: -------------------------------------------------------------------------------- 1 | # First things first 2 | 3 | We wont code yet. 4 | We will have a short intro to this guide and cover the necessary environment. 5 | After this chapter you should be ready to get learning Clojure like a champion. 6 | 7 | ## [1. Introduction](./1-introduction.md) 8 | 9 | This section is a brief introduction to this guide in general. 10 | It should be your starting point to the whole things, 11 | since it goes through who this guide is for and how it should be read. 12 | 13 | ## [2. Installation](./2-installation.md) 14 | 15 | In this section we work through all the necessary installation creating Clojure applications on your local machine. 16 | Includes easy step by step installation guides that you need. 17 | 18 | ## [3. Clojure and IDE](./3-clojure-and-IDE.md) 19 | 20 | In previous section we went through necessary Clojure installations. 21 | In this one we install proper Intelligent Development Enviornment (IDE) for you, 22 | so your development experience will be enjoyable and productive. 23 | 24 | ## Recap 25 | 26 | After this chapter you should have: 27 | 28 | - Understanding what this course is and how to read it 29 | 30 | - Installed Clojure, Java and Leiningen 31 | 32 | - Installed a suitable IDE for writing Clojure 33 | -------------------------------------------------------------------------------- /materials/2-little-bit-of-repl/1-leiningen-101.md: -------------------------------------------------------------------------------- 1 | # Leiningen: The basics 2 | 3 | Leiningen is a great and powerful tool used in Clojure. 4 | It is automated build tool and dependency manager. 5 | Along your journey to Clojure you will get more and more familiar with it, 6 | but for now we are going to cover only the very basics. 7 | 8 | For more details visit [Leiningen tutorial](https://github.com/technomancy/leiningen/blob/stable/doc/TUTORIAL.md). 9 | 10 | ## Your very own project 11 | 12 | Let's create you a very real Clojure project, 13 | where you can mess around as much as you want. 14 | We call this project **sandbox**. 15 | For the rest of the guide feel free to use this project for anything you wish to try out. 16 | We will not build anything to this project after this chapter (2-little-bit-of-repl), 17 | so there is no reason to worry if you will mess up something that is going to be needed later. 18 | 19 | ### Creating project 20 | 21 | When we call Leiningen from terminal we use short version **lein**. 22 | To create a new project we use following pattern: 23 | 24 | ```bash 25 | lein new [template-name] [project-name] 26 | ``` 27 | **Template-name** is non-obligatory. 28 | If left empty a default template is used. 29 | **Project-name** will be the name of the project that is being created. 30 | 31 | Now navigate to your desired location in your terminal and there write following command: 32 | 33 | ```bash 34 | lein new sandbox 35 | ``` 36 | 37 | Assuming you have everything installed correctly as instructed in previously, 38 | you should end up with a project looking something like this: 39 | 40 | ```bash 41 | . 42 | └─ sandbox 43 | ├── CHANGELOG.md 44 | ├── LICENSE 45 | ├── README.md 46 | ├── doc 47 | │   └── intro.md 48 | ├── project.clj 49 | ├── resources 50 | ├── sandbox.iml 51 | ├── src 52 | │   └── sandbox 53 | │   └── core.clj 54 | ├── target 55 | │   ├── classes 56 | │   │   └── META-INF 57 | │   │   └── maven 58 | │   │   └── sandbox 59 | │   │   └── sandbox 60 | │   │   └── pom.properties 61 | │   └── stale 62 | │   └── leiningen.core.classpath.extract-native-dependencies 63 | └── test 64 | └── sandbox 65 | └── core_test.clj 66 | ``` 67 | 68 | This is all that we are going to cover here at this point, 69 | but we will be returning to leiningen later when we will be building something. 70 | 71 | Next: [Using REPL](2-using-repl.md) 72 | -------------------------------------------------------------------------------- /materials/2-little-bit-of-repl/2-using-repl.md: -------------------------------------------------------------------------------- 1 | # Introduction: Rebel with a REPL 2 | 3 | In 1-first-things-first/introduction I said that we will focus on real world applications, 4 | but it would be incredibly irresponsible of me not to start with REPL. 5 | Many programming languages start with classic hello world printing, 6 | but in Clojure we like to start from [REPL (Read-Eval-Print-Loop)](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop). 7 | Even though REPL does exist in many other languages, 8 | as a dialect of Lisp (stands for List Processor) Clojure's REPL is quite powerful tool, 9 | and thus it is used constantly in Clojure development. 10 | 11 | There are many ways to use REPL in Clojure. 12 | In a moment we are going through two of them. 13 | 14 | ## [What is REPL actually](https://clojure.org/guides/repl/introduction) 15 | 16 | Before we spoke about REPL but we did not really cover what it is or why it used. 17 | So typically in Clojure the development cycle slightly differs from many other languages, 18 | especially Object Oriented Programming (OOP) languages. 19 | I won't go to the details of the difference here, 20 | but the main reasons why Clojure's REPL is so powerful is because FP applications have very little [state](https://en.wikipedia.org/wiki/State_(computer_science)). 21 | 22 | In typical a OOP application the development cycle is often Test Driven Development (TDD). 23 | In TDD we often write a test call for the functionality we aim to create, 24 | and then create the functionality so that the test passes. 25 | Or it is sometimes possible write the functionality first and see if it works by manual testing. 26 | This often involves running the whole application, 27 | which may be quite time consuming. 28 | 29 | REPL allows the us to evaluate individual functions without running the whole application. 30 | When the application does not really have state, 31 | or most of the code base does not heavily rely on the said state, 32 | it is possible to develop functions *in a void*. 33 | FP programming relies on so called [Pure Functions](https://en.wikipedia.org/wiki/Pure_function), 34 | which always return the same output with same inputs. 35 | I am not going to go into the details of Functional Programming or Pure Functions here. 36 | In case you are interested to learn more, 37 | read the article above. 38 | 39 | ## [REPL in command line](https://clojure.org/guides/repl/launching_a_basic_repl#_leiningen) 40 | 41 | Using REPL from command line is perhaps the easiest way to use REPL, 42 | but it is not very convenient. 43 | Nevertheless, we are going to quickly go through it, 44 | since it is definitely useful skill sometimes. 45 | 46 | Let's navigate to the sandbox project we created previously. 47 | 48 | ```bash 49 | cd sandbox 50 | ``` 51 | 52 | When you are inside the project, simply request lein to open you a REPL. 53 | 54 | ```bash 55 | lein repl 56 | ``` 57 | 58 | After this something like this should appear to your terminal: 59 | 60 | ```bash 61 | nREPL server started on port 63715 on host 127.0.0.1 - nrepl://127.0.0.1:63715 62 | REPL-y 0.4.3, nREPL 0.6.0 63 | Clojure 1.10.0 64 | Java HotSpot(TM) 64-Bit Server VM 1.8.0_202-b08 65 | Docs: (doc function-name-here) 66 | (find-doc "part-of-name-here") 67 | Source: (source function-name-here) 68 | Javadoc: (javadoc java-object-or-class-here) 69 | Exit: Control+D or (exit) or (quit) 70 | Results: Stored in vars *1, *2, *3, an exception in *e 71 | 72 | sandbox.core=> 73 | ``` 74 | 75 | Now that we have our REPL open, 76 | let's try if it works with a simple form: 77 | 78 | ```clojure 79 | (+ 1 1) 80 | ``` 81 | 82 | After pressing enter the result (2) should be shown in terminal. 83 | 84 | If this notation (+ 1 1) confuses you, 85 | don't be worried. 86 | It is something called [Polish Notation](https://en.wikipedia.org/wiki/Polish_notation). 87 | It is something Clojure uses, 88 | and you will get used to it way faster than you would think. 89 | We will discuss arithmetics and how they work in clojure in near future. 90 | 91 | Using REPL like this is easy, 92 | but you don't necessarily be typing your whole function in constantly. 93 | Because of this I would recommend using REPL from your IDE. 94 | 95 | ## REPL in VSC 96 | 97 | First launch REPL in terminal with Leiningen as we did previously. 98 | 99 | Open the sandbox project in your VSC. 100 | Not just the file, but the whole project. 101 | 102 | Open VSC Command Pallet. 103 | Command pallet can be accessed with **cmd+shift+p** (or **ctrl+shift+p** depending on your computer). 104 | From Command Pallet select **Calva: Connect to a running REPL**. 105 | The editor will now ask you few questions, which should be easy to answer (just press enter). 106 | 107 | After the connection is made navigate to the file sandbox/src/sandbox/core.clj, 108 | and write following code at the bottom of the file: 109 | 110 | ```clojure 111 | (str "hello" "-" "parens") 112 | ``` 113 | 114 | Now move your cursor to end of this line and select: 115 | **"Evaluate current top level form, and print to output"** 116 | from the the command pallet. 117 | 118 | Now the results should be visible in the output tab at the bottom of the editor. 119 | 120 | ```bash 121 | ;=> "hello-parens!" 122 | ``` 123 | 124 | Calva provides plenty of REPL options and this is only one of them. 125 | Feel free to toy around with different options if you feel like trying them out. 126 | 127 | Calva provides plenty of keyboard shortcuts, explore is they are suitable for you, 128 | if not VSC enables easy modification of keyboard shortcuts. 129 | Keyboard shortcuts can easily be accessed via command pallet. 130 | Just open the pallet and search for the work shortcut. 131 | 132 | ### WARNING for Vim mode users 133 | 134 | In case you are using Vim mode in VSC, 135 | you need to remap Calva's "Clear all inline display evaluations" shortcut, 136 | which is set to ESC by default. 137 | 138 | If you are not using Vim mode in VSC, you don't have to worry about this. 139 | 140 | ## REPL in Cursive 141 | 142 | TODO - Write instructions for using REPL in Cursive 143 | 144 | Next: [Hands on REPL](3-hands-on-repl.md) 145 | -------------------------------------------------------------------------------- /materials/2-little-bit-of-repl/README.md: -------------------------------------------------------------------------------- 1 | # Little bit of REPL 2 | 3 | In this chapter you will get a first touch with the Clojure itself. 4 | We will create a sandbox project and play with REPL. 5 | Also we will start building up the knowledge on the language itself. 6 | Some things might feel like they are hanging out without too much context, 7 | but bare with us. 8 | This is just to let you know these things exist, 9 | so we can cover more interesting topics more easily in the following chapters. 10 | 11 | ## [1. Leiningen 101](./1-leiningen-101.md) 12 | 13 | In this section we will create your first Clojure project with Leiningen. 14 | You will be able to use this project later for trying out things and such. 15 | You can use this project as scratch paper type of thing after wards. 16 | 17 | ## [2. Using REPL](./2-using-repl.md) 18 | 19 | Here we will get familiar with how to use REPL, 20 | your new best friend. 21 | This is critical for the future adventures. 22 | 23 | ## [3. Hands on REPL](./3-hands-on-repl.md) 24 | 25 | We get more hands on with REPL. 26 | Also will cover some basic Arithmetic functions and basic data types. 27 | 28 | ## Recap 29 | 30 | After this chapter you should: 31 | 32 | - Be able to create Clojure Projects with Leiningen 33 | 34 | - Be familiar with the use of REPL from both Command Line and IDE 35 | 36 | - Be able to call existing Clojure functions 37 | 38 | - Have a basic knowledge of Arithmetic functions in Clojure 39 | 40 | - Have knowledge of basic data types in Clojure 41 | -------------------------------------------------------------------------------- /materials/3-first-project/2-running-and-building-with-leiningen.md: -------------------------------------------------------------------------------- 1 | # [2. Running and building with Leiningen](https://github.com/technomancy/leiningen/blob/stable/doc/TUTORIAL.md) 2 | 3 | So far we have used Leiningen for creating projects from templates, 4 | and running REPL for our repl code. 5 | But Leiningen is actually very powerful and diverse tool capable of many more things. 6 | 7 | ## [Running with Leiningen](https://github.com/technomancy/leiningen/blob/stable/doc/TUTORIAL.md#running-code) 8 | 9 | Next natural step will be running code with Leiningen. 10 | This is the defacto basic way of running Clojure code from command line. 11 | 12 | Navigate to the root of our sum-em-up project and run the bellow command from the command line. 13 | 14 | ```clojure 15 | lein run 16 | ;=> 325 17 | ``` 18 | 19 | If this won't work for some reason, 20 | make sure that you completed all the steps from the previous part. 21 | Also remember to create the numbers.txt file to the root of your project. 22 | 23 | But all in all running clojure projects with Leiningen is rather straight forward. 24 | 25 | ## [Building with Leiningen](https://github.com/technomancy/leiningen/blob/stable/doc/TUTORIAL.md#uberjar) 26 | 27 | Running your project with Leiningen is often not enough. 28 | It has fundamentally two issues. 29 | 30 | 1. It requires you to deploy the whole project source code 31 | 32 | 2. It requires the deployment environment to have Leiningen, Clojure and all the dependencies installed 33 | 34 | Both of these issues kinda suck. 35 | Luckily Leiningen has more functionalities available for us. 36 | 37 | Solution for both of these issues is the Uberjar. 38 | Uberjars are [JAR](https://en.wikipedia.org/wiki/JAR_(file_format)) files with all the necessary dependencies embedded. 39 | 40 | Uberjar still requires that the deployment environment has Java (JRE) installed, 41 | but this is rather easy requirement to fill. 42 | Since so many things use Java, 43 | JRE is often already available. 44 | If it is not then it is not hard to install. 45 | 46 | So how do we create an uberjar? 47 | Navigate again to the root of our project and execute: 48 | 49 | `lein uberjar` 50 | 51 | After this we can execute this with following command: 52 | 53 | ```clojure 54 | java -jar target/uberjar/sum-em-up2-0.1.0-SNAPSHOT-standalone.jar 55 | ;=> 325 56 | ``` 57 | 58 | Remember that there has to be the numbers.txt file in the project root folder when you execute this. 59 | (assuming that you execute it from project root folder) 60 | 61 | Since at the very core Clojure is just another Library, 62 | it will be able to build normal jar files. 63 | At their core these files are just like any other Java program you might come across. 64 | 65 | With this we have covered running and building projects with Leiningen. 66 | 67 | Next: [Refactoring The Project](3-refactoring-project.md) 68 | -------------------------------------------------------------------------------- /materials/3-first-project/3-refactoring-project.md: -------------------------------------------------------------------------------- 1 | # [3. Refactoring the project](https://en.wikipedia.org/wiki/Code_refactoring) 2 | 3 | Writing real-world code never should end with first working MVP. 4 | It would be generally frowned upon to leave the code in such condition. 5 | Yes it might work, 6 | but even with such a small amount of code we managed to somehow make it a bit messy or unclear. 7 | 8 | We will pick up from where we left our code in the part 3.1. 9 | From there we will try to make the code more developer friendly. 10 | 11 | To make code more friendly idiomatic and friendly for eyes it is a good idea to follow the [Clojure Style Guide](https://guide.clojure.style/). 12 | Our code is mostly already in good order regarding this, 13 | but there is still room for improvement. 14 | 15 | One of the main issues with our code now is the `-main` function. 16 | 17 | ```clojure 18 | (defn -main 19 | [& args] 20 | (println (sum (map str->long (clojure.string/split (slurp "numbers.txt") #"\s+"))))) 21 | ``` 22 | 23 | It is now what we often refer as one-liner. 24 | The fact that you can write your whole program on single line, 25 | does not mean you should. 26 | 27 | So how do we fix this? 28 | Well first we can try to make some sense to it by breaking it to multiple lines. 29 | It sometimes helps. 30 | 31 | ```clojure 32 | (defn -main 33 | [& args] 34 | (println 35 | (sum 36 | (map 37 | str->long 38 | (clojure.string/split 39 | (slurp "numbers.txt") 40 | #"\s+"))))) 41 | 42 | ``` 43 | 44 | Well at least we don't have to be scrolling sideways with our editor, 45 | but other than that it is still quite much a mess. 46 | 47 | In other programming languages we would often try to solve similar issues with saving the results of each function to a variables. 48 | Even though clojure does provide possibility to save values to variables with [`def`](https://clojuredocs.org/clojure.core/def), 49 | it is not meant for this kind of usage. 50 | We will not even try that. 51 | (we will cover `def` later, so don't wonder about it now.) 52 | 53 | So how will we solve this? Well the go-to tool for this sort of situation is [`let`](https://clojuredocs.org/clojure.core/let) function. 54 | 55 | ## [Let us be friends](https://clojurebridge.org/community-docs/docs/clojure/let/) 56 | 57 | `let` is a special form that binds values to names. 58 | These are referred as lexical bindings in Clojure. 59 | Many other languages refer similar concepts as local variables. 60 | So with let we can create bindings that stop existing after the let form ends. 61 | I think it is easier to show than to explain how this actually works. 62 | 63 | ```clojure 64 | (let [x 10] 65 | (+ x x)) 66 | ;=> 20 67 | 68 | (let [y 15] 69 | (+ y x)) 70 | ;=> Syntax error compiling at (core.clj:17:3). 71 | ;Unable to resolve symbol: x in this context 72 | ``` 73 | 74 | Values bind with `let` are usable inside the `let` form, 75 | but not outside of it. 76 | `let` form always return the last form within it, 77 | much like functions in Clojure. 78 | 79 | ```clojure 80 | (let [x 5 81 | y 20] 82 | (+ x y)) 83 | ;=> 25 84 | ``` 85 | 86 | `let` is not limited to single binding. 87 | In fact we could bind as many values as we please. 88 | 89 | ```clojure 90 | (let [x 5 91 | y (+ x 100)] 92 | y) 93 | ;=> 105 94 | ``` 95 | 96 | Also values that have been bind first are usable when binding following values. 97 | Due to this it is never necessary nor a good practice to have nested let forms. 98 | 99 | I hope the examples above gave you some perspective on the essence of `let`. 100 | It is a tool that you will be using more and more when you dive deeper to the rabbit hole of Clojure. 101 | 102 | ## Refactoring with let 103 | 104 | We are now familiar with `let`, 105 | and we can start using it to clean up our `-main` function. 106 | 107 | So what we are going to do is to bind temporary results with `let` to make whole thing clearer. 108 | 109 | ```clojure 110 | (defn -main 111 | [& args] 112 | (let [text (slurp "numbers.txt") 113 | str-coll (clojure.string/split text #"\s+") 114 | long-coll (map str->long str-coll) 115 | total (sum long-coll)] 116 | (println total))) 117 | ``` 118 | 119 | Here is what we did: 120 | 121 | 1. We bind all the text from file to **text**. 122 | 123 | 2. We bind a collection numbers in string format to **str-coll**. 124 | 125 | 3. We bind collection of numbers in long format to **long-coll** 126 | 127 | 4. We sum of all the numbers in long-coll. 128 | 129 | 5. Finally print the total 130 | 131 | In each step we used the values defined in the previous steps. 132 | Not only is this code easier to read, 133 | but it is also is far easier to debug if something seems fishy. 134 | Just simply print out the value/values that you think might be something else than it should be. 135 | 136 | ```clojure 137 | (defn -main 138 | [& args] 139 | (let [text (slurp "numbers.txt") 140 | str-coll (clojure.string/split text #"\s+") 141 | long-coll (map str->long str-coll) 142 | total (sum long-coll)] 143 | (println str-coll) ;; like this 144 | (println total))) 145 | ``` 146 | 147 | Even though using `let` can make wonders to the readability of your code, 148 | there is also a golden line. 149 | Not every single tiny value needs to be bind with let. 150 | It could easily be argued that we might have gone a bit over the edge with our example here. 151 | In the end you will be the judge of what is the right amount of binding for readability of your code. 152 | Experiment with different approaches while writing Clojure, 153 | and in time you will find the balance between too little and too much `let`. 154 | 155 | In addition to let Clojure also offers something called [Threading Macros](https://clojure.org/guides/threading_macros). 156 | They can be used to achieve similar results in code clarity. 157 | They might not be the most beginner friendly tool, 158 | but they are very popular and effective. 159 | 160 | We won't dive into details with them now, 161 | but we will be getting comfortable with them as well in the future. 162 | 163 | ## Using args 164 | 165 | Currently our project is always dependent on file _numbers.txt_, 166 | and this kinda sucks. 167 | What if we would to check sum of numbers in some other file? 168 | We would have to either rewrite our code and recompile the uberjar to do so. 169 | Or we would have to rename the file. 170 | Neither of these options really seems tempting. 171 | It would be much better if we could define the file name when running the application. 172 | Luckily this is possible. 173 | 174 | It is possible to define functions that take unlimited number of arguments. 175 | Our `-main` function is such a function. 176 | This functionality is rather simple. 177 | We use `& args` to specify that there might be more arguments. 178 | The word used after & can be anything you wish. 179 | 180 | ```clojure 181 | (defn all-but-first 182 | "returns a collection with all but the first argument" 183 | [x & more] 184 | more) 185 | 186 | (all-but-first 1 2 3 4) 187 | ;=> (2 3 4) 188 | ``` 189 | 190 | Main takes no mandatory arguments, 191 | but it can be provided with unlimited number of arguments that will all be saved to args. 192 | Let's change a our `-main` function a bit more, 193 | so it will receive the name file we wish to read the numbers from. 194 | 195 | ```clojure 196 | (defn -main 197 | [& args] 198 | (let [file-name (first args) 199 | text (slurp file-name) 200 | str-coll (clojure.string/split text #"\s+") 201 | long-coll (map str->long str-coll) 202 | total (sum long-coll)] 203 | (println total))) 204 | ``` 205 | 206 | Now we need to call our program a bit differently if when running it from command line. 207 | Like this: 208 | 209 | ```bash 210 | lein run ourFileName 211 | 212 | # for example like this 213 | lein run numbers.txt 214 | ``` 215 | 216 | That should cover our refactoring for now. 217 | There would for sure be room for more improvements, 218 | but for now this should be enough. 219 | 220 | In the end of this part your project should look something like this: 221 | 222 | ```clojure 223 | (ns sum-em-up.core 224 | (:gen-class)) 225 | 226 | (defn sum 227 | "Sums a vector of numbers" 228 | [a-seq] 229 | (apply + a-seq)) 230 | 231 | (defn str->long 232 | [x] 233 | (Long/valueOf x)) 234 | 235 | (defn -main 236 | [& args] 237 | (let [file-name (first args) 238 | text (slurp file-name) 239 | str-coll (clojure.string/split text #"\s+") 240 | long-coll (map str->long str-coll) 241 | total (sum long-coll)] 242 | (println total))) 243 | ``` 244 | 245 | Next: [Testing in Clojure](4-testing-in-clojure.md) 246 | -------------------------------------------------------------------------------- /materials/3-first-project/README.md: -------------------------------------------------------------------------------- 1 | # Your first project - Sum 'em up 2 | 3 | Since this is a __PRACTICAL__ guide to Clojure, 4 | it is time to get out of new found safety of REPL and build something real! 5 | 6 | In this chapter we will build a simple application that: 7 | 8 | 1. Read numbers from a file 9 | 10 | 2. Sums the numbers up 11 | 12 | 3. Prints the numbers to command line 13 | 14 | This might and does sound extremely trivial, 15 | but in the progress we will learn plenty of useful skills. 16 | Further more we will immediately connect Clojure to real world, 17 | which might often feel hard to get started with. 18 | 19 | ## [1. Minimum Viable Product](./1-minimum-viable-product.md) 20 | 21 | Here we will create the version that barely meets standards of our software. 22 | Aim is to make just some thing that works, 23 | so we can improve it later. 24 | 25 | ## [2. Running and Building with Leiningen](./2-running-and-building-with-leiningen.md) 26 | 27 | This part will teach us how to run code from command line with Leiningen. 28 | After that we will look into building and running runnables. 29 | 30 | ## [3. Refactoring the Project](./3-refactoring-project.md) 31 | 32 | Here we will refactor our project, 33 | so it won't be just MVP anymore. 34 | The code will be nicer and some improvements will be made 35 | 36 | ## [4. Testing in Clojure](./4-testing-in-clojure.md) 37 | 38 | This part teaches you about testing in Clojure. 39 | We will also be building some tests for our project. 40 | 41 | ## Recap 42 | 43 | Among other things after this chapter you: 44 | 45 | - Know how to create (a.k.a. define) new functions. 46 | 47 | - Are able to perform basic operations in Clojure. Such as: 48 | - Reading from file. 49 | - Parsing text. 50 | - Parsing numbers from a text. 51 | - Operations to whole collection with map and apply 52 | - Basic Java interop 53 | 54 | - Can run Clojure applications from command line 55 | 56 | - Can build a runnable Clojure application 57 | 58 | - Are able to write and run tests in Clojure 59 | -------------------------------------------------------------------------------- /materials/4-state-in-clojure/1-adding-to-data-structures.md: -------------------------------------------------------------------------------- 1 | # 1. Adding to data structures 2 | 3 | First things first. 4 | Like in previous chapter, 5 | go ahead and create a new Leiningen project using app template. 6 | Name the project as _shopping-list_. 7 | After the project is created, 8 | navigate to the project, 9 | launch REPL and open src/shopping-list/core.clj in your editor. 10 | In case you have problems with this try refreshing your memory in previous chapters. 11 | 12 | With the necessities out of the way we can actually get started. 13 | We will play a bit in REPL first to learn few new tricks, 14 | and after wards we will use them in our project. 15 | 16 | In the previous project we made an acquaintance with some data structures. 17 | Now we will learn more about them. 18 | As we will progress on this guide, 19 | you will learn that most of our work with Clojure is more or less about playing with those basic data structures. 20 | 21 | The structures that we will be using in this chapter are vectors and maps, 22 | both of which you have met previously already. 23 | Let's get to know them a bit better. 24 | 25 | So vectors are a structure similar to lists in many other languages. 26 | But unlike the collections of other languages, 27 | Clojure's collections are not necessarily separated by commas, 28 | but they can be if it adds clarity to the code. 29 | In case the commas are being used, 30 | the compiler will ignore them. 31 | In other words, 32 | commas in collections are just syntax sugar. 33 | 34 | Previously we have been just seeing collections that we either got from somewhere, 35 | or we created them from some other data. 36 | So far we have not modified any of the data structures we have used. 37 | That is about to change soon. 38 | 39 | ```clojure 40 | ;; vectors can be declared in two ways 41 | (vector 1 2 3) 42 | ;=> [1 2 3] 43 | ;; or 44 | [1 2 3] 45 | ;=> [1 2 3] 46 | ;;Both result in the same 47 | (= (vector 1 2 3) [1 2 3]) 48 | ;=> true 49 | ``` 50 | 51 | There is few ways to add additional elements to the vectors, 52 | and these same methods work with other collections as well. 53 | 54 | It is also important to note that these functions DO NOT actually add items to the existing collection. 55 | Clojure's data structures are always [immutable](https://en.wikipedia.org/wiki/Immutable_object) and cannot be changed after their creation. 56 | Instead actually adding items to existing structures, 57 | Clojure returns a new collection with based on the input values. 58 | Depending on your background this might feel odd or slow, 59 | but don't worry about it too much. 60 | You will get used to it quite fast. 61 | And regarding performance: It's fast. 62 | 63 | ## [conj](https://clojuredocs.org/clojure.core/conj) 64 | 65 | `conj` (short for conjoin) adds the element/elements to that part of collection, 66 | where it is most optimal from the performance perspective to do so. 67 | That means the outcomes differ based on the type of collection being used. 68 | With vectors the elements are added to the end, 69 | and with lists the are added to the beginning. 70 | On maps and sets the elements are kinda added where ever, 71 | since they don't guarantee the order of their elements. 72 | Keep this in mind when selecting the collection type that you will be using. 73 | With both lists and vectors it is possible to add elements to other places in the collection as well, 74 | but it is always "cheapest"/fastest performance-wise to add the elements where Clojure naturally does it with `conj`. 75 | 76 | ```clojure 77 | (conj [1 2 3] 4) 78 | ;=> [1 2 3 4] 79 | (conj [1 2 3] 4 5) 80 | ;=> [1 2 3 4 5] 81 | 82 | (conj '(1 2 3) 4) 83 | ;=> '(4 1 2 3) 84 | ``` 85 | 86 | In most cases `conj` should be your goto tool for adding items to collections, 87 | but there is other tools as well that have their use cases. 88 | 89 | ## [concat](https://clojuredocs.org/clojure.core/concat) 90 | 91 | `concat` returns a seq that is the combination of its arguments. 92 | It is rather straight forward to use, 93 | and it can be easily used to add items at beginning of vectors and end of lists, 94 | if that is necessary. 95 | 96 | ```clojure 97 | (concat [1] [2] [3] [4]) 98 | ;=> (1 2 3 4) 99 | (concat [1 2 3] [4]) 100 | ;=> (1 2 3 4) 101 | (concat [1] #{2} '(3) [4]) 102 | ;=> (1 2 3 4) 103 | ``` 104 | 105 | `concat` is very useful, 106 | but it is good to be aware that it might be a bit slower than `conj`. 107 | Yet in the most cases the speed should not be an issue to your application. 108 | 109 | ## [cons](https://clojuredocs.org/clojure.core/cons) 110 | 111 | `cons` adds an element to the front of the collection and returns it as a seq. 112 | `cons` is rather popular and it definitely has its uses. 113 | But unless you really want to deal with seqs, 114 | AND you want the items added to beginning of a collection then you should consider other options. 115 | 116 | ```clojure 117 | (cons 1 [2 3 4]) 118 | ;=> (1 2 3 4) 119 | (seq? (cons 1 [2 3 4])) 120 | ;=> true 121 | ``` 122 | 123 | `cons` has its uses but it is a good idea to prefer previous functions when in doubt. 124 | 125 | ## Adding to shopping list 126 | 127 | We will be keeping our shopping list as vector of maps. 128 | The aim will be to make the shopping list object to look something like this: 129 | 130 | ```clojure 131 | [{:product "milk" :amount "2 liters"} {:product "bread" :amount "1"}] 132 | ``` 133 | 134 | (To keep things simple we wont be using numbers as amount for now) 135 | 136 | So how will we add items to the shopping list vector? 137 | 138 | ```clojure 139 | (conj [] {:product "milk" :amount "2 liters"}) 140 | ;=> [{:product "milk" :amount "2 liters"}] 141 | ``` 142 | 143 | I would suggest that you will try playing around with adding elements to collections in REPL. 144 | This will help you to get comfortable with the collections modifying them. 145 | 146 | Next: [Storing State with Atom](2-storing-state-with-atom.md) 147 | -------------------------------------------------------------------------------- /materials/4-state-in-clojure/2-storing-state-with-atom.md: -------------------------------------------------------------------------------- 1 | # 2. Storing state with atom 2 | 3 | Now that we are familiar with data structures and adding data to them, 4 | we still need few more things to store the state of the application. 5 | 6 | ## [def](https://clojuredocs.org/clojure.core/def) - saving constants 7 | 8 | defs let us define what most languages call variables. 9 | With def we can do the same as with let, 10 | but instead of function the values will be usable from the whole namespace. 11 | 12 | `def`s are really simple actually: 13 | 14 | ```clojure 15 | (def x 10) 16 | (def y 20) 17 | 18 | (+ y x) 19 | ;=> 30 20 | ``` 21 | 22 | There is not much more to say about `def`s for now. 23 | Almost anything can be stored to them. 24 | Not only values, collections, and objects but functions too. 25 | This is because Clojure's functions are [first class citizens](https://en.wikipedia.org/wiki/First-class_function), 26 | which means that they can be treated as any other values. 27 | They can be passed as arguments or used as return values. 28 | You have already done this with `map` function, 29 | where we gave a function as parameter, 30 | so this should not be completely new to you. 31 | 32 | All the values defined with def are immutables, 33 | like constants and finals are in other languages. 34 | Due to this we will need something extra to get around with the mutable state. 35 | That something is `atom`. 36 | 37 | ## [atom](https://clojuredocs.org/clojure.core/atom) 38 | 39 | `atom` is a special object that we can define and later modify with functions. 40 | With `atom`s Clojure applications can take care of state in concurrent applications with out much of a hustle. 41 | If you have tried doing this in other languages like Java or Python, 42 | might know that it is not entirely trivial task usually. 43 | [Race condition](https://en.wikipedia.org/wiki/Race_condition) is a good example of an issue caused by poorly managed state. 44 | We won't be doing concurrency on this guide, 45 | since it is way out of our scope. 46 | But I wanted you to know why such an unusual tool as `atom` is the goto tool for saving mutable variables in Clojure. 47 | 48 | Let's do a quick number incrementation exercise to get familiar with the functionalities of atom. 49 | 50 | We will define an `atom` called _number_ and give it a value of 1 51 | 52 | ```clojure 53 | (def number (atom 1)) 54 | ``` 55 | 56 | To see the value in an `atom` we need to use a special operator `@`. 57 | 58 | ```clojure 59 | @number 60 | ;=> 1 61 | ``` 62 | 63 | `@` is actually a short hand for the function [`deref`](https://clojuredocs.org/clojure.core/deref), 64 | but you will almost never use `deref` since `@` is so clear. 65 | Nevertheless `deref` is something you should know of. 66 | 67 | Altering a value in an atom might feel a bit tricky, 68 | in the beginning but it is quite simple actually. 69 | To alter a value we need to provide the atom a function that will take the current value as a parameter, 70 | and then the return value will be stored as the new value for the atom. 71 | This whole thing will be done using [`swap!`](https://clojuredocs.org/clojure.core/swap!) function. 72 | Notice that exclamation mark is part of the function name. 73 | Exclamation mark is often used to notify that the function has side effects. 74 | 75 | ### [swap!](https://clojuredocs.org/clojure.core/swap!) 76 | 77 | `swap!` takes atom and a function as parameters. 78 | It can also take additional parameters which will also be passed to the function. 79 | 80 | To increment a number the idiomatic way in Clojure is to use [`inc`](https://clojuredocs.org/clojure.core/inc). 81 | It is a function that takes a number and returns that number +1. 82 | 83 | ```clojure 84 | (swap! number inc) 85 | ;=> 2 86 | (swap! number inc) 87 | ;=> 3 88 | @number 89 | ;=> 3 90 | ``` 91 | 92 | The number can also be decreased by using `inc`'s counterpart [`dec`](https://clojuredocs.org/clojure.core/dec) 93 | 94 | ```clojure 95 | (swap! number dec) 96 | ;=> 2 97 | ``` 98 | 99 | If we want to add a number to our atom that is easy, too. 100 | As mentioned before, 101 | `swap!` can also take additional arguments that will be passed to the function 102 | 103 | ```clojure 104 | (swap! number + 5) 105 | ;=> 7 106 | ``` 107 | 108 | ### [reset!](https://clojuredocs.org/clojure.core/reset!) 109 | 110 | The atom's value can also be completely reset to new value with `reset!` function. 111 | This function should be used mostly when the atom needs to be reset regardless of its current value. 112 | If current value plays any role in resetting of the value, 113 | is usually better to use swap! 114 | 115 | ```clojure 116 | (reset! number 1) 117 | ;=> 1 118 | @number 119 | ;=> 1 120 | ``` 121 | 122 | ### few words about atom 123 | 124 | Atom is definitely a handy tool, 125 | but it is good to acknowledge that is not needed that often. 126 | Clojure's functional approach to programming is most of the time able to solve issues without using any mutable state. 127 | I simply wanted to introduce this tool to you, 128 | so you won't waste your time wondering how certain things can be accomplished with Clojure (as I did when I was learning the basics). 129 | Try not to over use atom. 130 | It is there for you if you need it, 131 | but it is a power that you should not overuse. 132 | 133 | Another thing that you should know about atom, 134 | is that it might perform the same function more than once to your value. 135 | Thus you should never use functions with any side effects with atoms, 136 | since those side effects might take place multiple times. 137 | This is because of the internals of the atom that make sure that no race conditions occur. 138 | 139 | When we call `swap!` atom stores the current value it holds and puts it aside, 140 | then it performs the given functions with its current value. 141 | before setting the new value to atom it checks if the current value is still the same as it put aside in the beginning. 142 | If something else has managed to change the current value of the atom in the meanwhile, 143 | it will start from the beginning using the new value as parameter. 144 | This is how atom guarantees that no race conditions occur, 145 | but it also may cause your function to run multiple times. 146 | 147 | Atom also has cousins [`ref`](https://clojuredocs.org/clojure.core/ref) and [`agent`](https://clojuredocs.org/clojure.core/agent), 148 | which are both somewhat similar but a bit different. 149 | They are solutions to state problems that atom cannot solve. 150 | They are needed very rarely and you should stick to atom when ever possible. 151 | Due to rarity of their use cases I won't include them into this guide. 152 | You know they exist and you know where to look for them if you happen to need them. 153 | 154 | ## Updating shopping list 155 | 156 | Now we should know enough to be able to create a shopping list that we can add new items to. 157 | Define an atom called _shoppings_ to our shopping-list.core and initialize it as an empty vector 158 | 159 | ```clojure 160 | (def shoppings (atom [])) 161 | ``` 162 | 163 | With that being done, 164 | we can create a function that we can add elements to shoppings. 165 | You should already posses all the necessary skills to do so, 166 | but will give you hand just in case. 167 | 168 | ```clojure 169 | (defn add-shopping 170 | [shopping] 171 | (swap! shoppings conj shopping)) 172 | ``` 173 | 174 | This function can take any sort of object as a parameter and it will add it to the shoppings. 175 | Remember to try it from REPL. 176 | 177 | ```clojure 178 | (add-shopping {:product "Milk" :amount "1 bottle"}) 179 | ;=> [{:product "Milk" :amount "1 bottle"}] 180 | 181 | (add-shopping {:product "Candies" :amount "1 bag"}) 182 | ;=> [{:product "Milk", :amount "1 bottle"} {:product "Candies", :amount "1 bag"}] 183 | ``` 184 | 185 | Great! It works. 186 | Now with this being done we will have few more steps to complete the program. 187 | 188 | We still need to create some kind of terminal interface that will prompts for values from user. 189 | Also we need to somehow save our list to a file (and hopefully in human readable format). 190 | These are the tasks that we will struggle with in next parts. 191 | 192 | Next: [Conditional Love](3-conditional-love.md) 193 | -------------------------------------------------------------------------------- /materials/4-state-in-clojure/3-conditional-love.md: -------------------------------------------------------------------------------- 1 | # 4.3. Conditional Love 2 | 3 | We will now build the command line interface, 4 | so the user will be able to communicate with our application. 5 | This is for sure a challenge you have faced multiple times in other languages, 6 | and it is often quite trivial. 7 | This is also the case in Clojure. 8 | 9 | To be able to interface with the application, 10 | we need to learn about some very basic building blocks in Clojure. 11 | [Flow Control Expressions](https://clojure.org/guides/learn/flow#_flow_control_expressions). 12 | More precisely we will learn about if. 13 | 14 | ## [if](https://clojure.org/guides/learn/flow#_if) 15 | 16 | First of all, 17 | this section is rather late in this course, 18 | and I apologize for that. 19 | There simply was not suitable place to slide it in earlier. 20 | Back to the topic. 21 | 22 | `if` in Clojure is rather different from `if` statements in other programming languages. 23 | In face `if` is something called a [_special form_](https://clojure.org/reference/special_forms). 24 | `if` in Clojure is kinda like a function, 25 | just like almost everything else is. 26 | Only difference to actual functions is that you cannot pass `if` around, 27 | in other words it is not a 1st class citizen. 28 | So how can `if` be like a function? 29 | "That makes no sense." 30 | Well actually it does, 31 | and it is rather simple really. 32 | 33 | `if` takes 3 parameters. 34 | Those being test, then, else? 35 | Question mark in the end of else indicates that is is non-obligatory. 36 | So what does this mean? 37 | 38 | ```clojure 39 | (if true "yes" "no") 40 | ;=> "yes" 41 | 42 | (if false "yes" "no") 43 | ;=> "no" 44 | 45 | (if false "yes") 46 | ;=> nil 47 | ``` 48 | 49 | So unlike in so many programming languages Clojure does not provide if statement, 50 | which allows us to take different paths in our programs. 51 | This would be rather imperative manner. 52 | Instead if in Clojure returns one of two values. 53 | Yet nothing stops us from returning results of completely different functions, 54 | which will effectively lead to more functional style solutions. 55 | 56 | This would look like something like this: 57 | 58 | ```clojure 59 | (if (is-sold? product) 60 | (ship-to-customer product customer) 61 | (hype-on-marketplace product marketplace)) 62 | ``` 63 | 64 | You might also have noticed how I split the `if` statement to three lines. 65 | This is an idiomatic way especially with any longer than super short `if` statements. 66 | For more idiomatic Clojure code remember to visit [the style guide](https://guide.clojure.style/). 67 | 68 | When handling boolean operations in Clojure, 69 | it is good to note that almost all values are what we would consider truthy. 70 | Like in many languages the value does not have to be explicitly true to be considered truthy. 71 | In fact only `nil` and `false` are considered falsy. 72 | Everything else will always evaluate to truthy in boolean terms. 73 | 74 | Obviously Clojure as a mature language offers many alternatives for `if`, 75 | but we will leave them for later for now. 76 | 77 | Next: [Terminal Interface](4-terminal-interface.md) 78 | -------------------------------------------------------------------------------- /materials/4-state-in-clojure/4-terminal-interface.md: -------------------------------------------------------------------------------- 1 | # 4.4. Terminal interface 2 | 3 | With conditions in our pocket we will need just few more pieces to build a simple terminal interface. 4 | Firstly we will be needing a [read-line](https://clojuredocs.org/clojure.core/read-line). 5 | 6 | ## [read-line](https://clojuredocs.org/clojure.core/read-line) 7 | 8 | `read-line` reads the [stdin](https://en.wikipedia.org/wiki/Standard_streams) (known as \*in\*) and returns that value. 9 | In our case the stdin is the terminal as it very often is. 10 | `read-line` is fairly simple to use since it takes no arguments what so ever. 11 | 12 | ```clojure 13 | (read-line) ;; now write hello to your stdin 14 | ;=> "hello" 15 | ``` 16 | 17 | There is not much more to say about the `read-line` function, 18 | so let's move on. Next we will meet `loop`. 19 | 20 | ## [loop](https://clojuredocs.org/clojure.core/loop) and [recur](https://clojuredocs.org/clojure.core/recur) 21 | 22 | Clojure's [special form](https://clojure.org/reference/special_forms) `loop` can be a bit tricky to explain, 23 | but we will soon see how good of a job I will manage to do in it. 24 | 25 | `loop` is a [lexical context](https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexical_scope_vs._dynamic_scope) just like [let](https://clojuredocs.org/clojure.core/let), 26 | with a significant difference. 27 | When combined with `recur` function `loop` enables [tail recursion](https://en.wikipedia.org/wiki/Tail_call) in Clojure. 28 | [Recursion](https://en.wikipedia.org/wiki/Recursion) is a powerful technique that plays a crucial role in Functional Programming. 29 | Unfortunately the JVM (on which Clojure is hosted) [does not have a very good support for recursion](https://purelyfunctional.tv/article/problems-with-the-jvm/), 30 | and using default recursion will easily lead to StackOverflowError. 31 | Thus we have loop/recur. 32 | 33 | So without further ado, 34 | we will explore loop/recur by examples. 35 | 36 | Let's count from 0 to 10 using loop/recur. 37 | 38 | ```clojure 39 | (loop [num 0] 40 | (if (> num 10) 41 | (println "done") 42 | (do (println num) 43 | (recur (inc num))))) 44 | ``` 45 | 46 | WOW! Thats a handful! 47 | Let's dissect it a bit and see what it actually is. 48 | 49 | hint: [do](https://clojuredocs.org/clojure.core/do) let's us evaluate multiple expressions and return the value of the last. 50 | This is often used with `println`, 51 | but it has other uses as well. 52 | 53 | ```clojure 54 | (loop [num 0] ;; bind 0 to num in lexical context 55 | (if (> num 10) ;; check if num is larger than 0 56 | (println "done") ;; if so, print "done" 57 | (do (println num) ;; start do expression and print num 58 | (recur (inc num))))) ;; call recur inside the do statement 59 | ``` 60 | 61 | So what does `recur` actually do? 62 | [recur](https://clojuredocs.org/clojure.core/recur) is a [special form](https://clojure.org/reference/special_forms#recur), 63 | which allows us to to recall `loop` with new bindings. 64 | In other words we can start running the code again in loop-ish manner, 65 | but with new _num_ value. 66 | In our case we are rebinding num to num + 1. 67 | Just like `let`, 68 | `loop` as well can take multiple bindings and it is rather common. 69 | 70 | I cannot in good conscience tell you about `recur` without mentioning, 71 | that it can also be called outside a loop to recall a function that it is in with new bindings. 72 | This is rather rare and I'm having a hard time coming up with any examples. 73 | But you should know it is possible. 74 | 75 | I suggest that you play around with loop/recur. 76 | It can feel a bit weird or intimidating, 77 | but you will get ahold of it quite quickly. 78 | 79 | Below are a few examples that might help you to get started: 80 | 81 | Hint: check [rest](https://clojuredocs.org/clojure.core/rest) and [first](https://clojuredocs.org/clojure.core/first) 82 | 83 | ```clojure 84 | (loop [a-vec [] ;; bind empty vector to a-vec 85 | word "Hickey"] ;; bind "Hickey" to word 86 | (if (empty? word) ;; check if there is characters left in word 87 | a-vec ;; if there is not, then return a-vec 88 | (recur (conj a-vec (first word)) ;; if there is, then recur loop adding first letter of the word to a-vec 89 | (rest word)))) ;; and binding rest of the letter (except first) to word 90 | ``` 91 | 92 | `first` and `rest` play a crucial role in how we process collections in clojure. 93 | You will see them pretty much every where. 94 | `rest` returns all but the first element of a collection. 95 | `first` does exactly the opposite. 96 | 97 | ```clojure 98 | (first [:a :b :c]) 99 | ;=> :a 100 | 101 | (rest [:a :b :c] 102 | ;=> (:b :c)) 103 | ``` 104 | 105 | Also notice that rest returns a seq regardless of the input type. 106 | 107 | ```clojure 108 | (loop [a-list '() 109 | a-vec [1 3 5 3 23 21 12 39]] 110 | (if (empty? a-vec) 111 | a-list 112 | (recur (conj a-list (* (first a-vec) 2)) 113 | (rest a-vec)))) 114 | ``` 115 | 116 | Here we double all the elements in a-vec and return them in a reversed list. 117 | Can you figure out why the list is being reversed? 118 | 119 | Hint: try to remember what `conj` does with with different collection types. 120 | 121 | If you are looking for an extra challenge, 122 | try modifying this so that it does not reverse the list. 123 | 124 | ### About loop and laziness 125 | 126 | After you get the hang of `loop`, 127 | it might be tempting to use it for all of your collection manipulation and filtering needs. 128 | 129 | It can indeed perform the tasks of [filter](https://clojuredocs.org/clojure.core/filter), 130 | [map](https://clojuredocs.org/clojure.core/map), 131 | and [reduce](https://clojuredocs.org/clojure.core/reduce). 132 | 133 | (These are three stages of data manipulation that are present in almost all Functional Programming code) 134 | 135 | But even though `loop` can perform all of these tasks, 136 | you should avoid using it and favour other functions when possible. 137 | This is due to the fact that `loop` is not [lazy](http://clojure-doc.org/articles/language/laziness.html), 138 | thus by using `loop` you reduce Clojure's ability to optimize its performance. 139 | This is something that might not be an issue for you, 140 | since regardless what you do Clojure is rather performant, 141 | but it never hurts to write better code, right? 142 | 143 | With that being said, 144 | don't stress about whether you are using `loop` too much. 145 | It is often justified and it will soon become one of your very good friends. 146 | 147 | ## Basic terminal interface 148 | 149 | So let's make a very basic terminal interface. 150 | This is kinda example, 151 | so we won't be using this exact interface in our shopping-list. 152 | But we will be building something very similar. 153 | 154 | **WARNING:** If you are using VSCode with Calva, please do not enter the code below into your editor. It may [break Calva Jack-In ](https://github.com/BetterThanTomorrow/calva/issues/377). 155 | 156 | So we will have `loop`, 157 | where the bind value is read-line 158 | (yes this is totally possible, we can bind value to output of a function). 159 | 160 | ```clojure 161 | (loop [you-say (read-line)] 162 | (if (= you-say "quit") 163 | (println "we are done") 164 | (recur (do (println you-say) 165 | (read-line))))) 166 | ``` 167 | 168 | This function will prompt the user for an input, print out the input and continue this way until the user enters _quit_. 169 | I am sure you have seen similar solutions in other languages, 170 | since they are rather popular when learning to program. 171 | 172 | So now we should know enough to build an actual MVP. 173 | 174 | Next: [Creating MVP](5-creating-mvp.md) 175 | -------------------------------------------------------------------------------- /materials/4-state-in-clojure/5-creating-mvp.md: -------------------------------------------------------------------------------- 1 | # 4.5. Creating MVP 2 | 3 | So now we should know enough to actually build an interface for our shopping application. 4 | (That took a while, right.) 5 | 6 | So far your code should have atom shoppings and function add-shopping. 7 | That's not much, 8 | but it is a start. 9 | 10 | ```clojure 11 | (def shoppings (atom [])) 12 | 13 | (defn add-shopping 14 | [shopping] 15 | (swap! shoppings conj shopping)) 16 | ``` 17 | 18 | So what we want next? 19 | Our end goal is to be able to save shopping objects to a vector. 20 | We also want to save it out of the vector to a file. 21 | We are also to keen to communicate with out user effortlessly, 22 | so we don't have to worry about that aspect constantly. 23 | 24 | ## Prompting 25 | 26 | So let's start by creating nice function for prompting information from the user. 27 | read-line is nice, 28 | but we want to wrap it up a bit to make it more convenient for us. 29 | 30 | ```clojure 31 | (defn prompt 32 | [msg] 33 | (println msg) 34 | (read-line)) 35 | ``` 36 | 37 | Here we have nice prompt function that takes a message that will be printed to the user, 38 | and then what ever the user writes to the command line will be received by the program. 39 | So essentially it works very same way as prompt() in python. 40 | 41 | As before, 42 | always remember to try your code in REPL. 43 | It is crucial part of development in Clojure. 44 | Give it a try (don't leave this part to your code, 45 | it is not part of the final solution): 46 | 47 | ```clojure 48 | (prompt "Who are you?") ;; now write your name to terminal 49 | ;=> "John" 50 | ``` 51 | 52 | Great! Our function seems to work as intended. 53 | 54 | ## Combining functions to building blocks 55 | 56 | Next thing we want to do is to create a function for the whole process of adding a new product from terminal to our atom. 57 | Let's create a function add-product-to-shoppings. 58 | This function will wrap prompt's and our add-shopping function to single function. 59 | 60 | ```clojure 61 | (defn add-product-to-shoppings 62 | [] 63 | (let [product (prompt "What to buy?") 64 | amount (prompt "How many?")] 65 | (add-shopping {:product product 66 | :amount amount})) 67 | ``` 68 | 69 | Remember to write this all of this code by yourself. 70 | Copy-pasting will do you no good. 71 | 72 | So what did we do here? 73 | 74 | Well we bind out puts of two prompt functions to _product_ and _amount_. 75 | Then we create a map where `:product` is product and `:amount` is amount. 76 | Finally we call our add-shopping with this newly created map as parameter. 77 | 78 | Remember to try this in REPL. 79 | It is a good idea to call it few times to make sure that the list is built as we intend it to be. 80 | 81 | ## Interface 82 | 83 | Now we should have enough to build a raw prototype of our interface. 84 | For now we are still going to ignore the need to print the list to a file, 85 | and focus to creating that list from the interface. 86 | 87 | So we will create an interface, 88 | which will ask us if we want to add items or print the list. 89 | Since this is the main part of our code, 90 | we will be writing it directly to the `-main` function that runs when the program is being run. 91 | 92 | ```clojure 93 | (defn -main 94 | [& args] 95 | (loop [choice (prompt "Enter a number:\n1. Add product\n2. Save shopping list")] 96 | (if (= choice "1") 97 | (do (add-product-to-shoppings) 98 | (recur (prompt "Enter a number:\n1. Add product\n2. Save shopping list"))) 99 | (if (= choice "2") 100 | (println "Shopping list saved") 101 | (do (println "Invalid choice!!! Try again") 102 | (recur (prompt "Enter a number:\n1. Add product\n2. Save shopping list"))))))) 103 | ``` 104 | 105 | So `-main` function always take arguments `[& args]`, 106 | but we won't wont be using those for now. 107 | What we are interested here is the `loop`. 108 | So in `loop` we bind output of the prompt to _choice_. 109 | 110 | If the the choice is "1", 111 | we will call add-product-to-shoppings and `recur` the `loop` with the same prompt. 112 | Remember that add-product-to-shoppings takes no params. 113 | If the choice is not 1 we will proceed to the second `if` statement. 114 | 115 | Here we will check if the choice is "2". 116 | If it is, 117 | we will will print "Shopping list saved". 118 | Please note that we don't actually save anything at this point. 119 | We will simply leave that as a future problem to be solved. 120 | 121 | Finally, if the choice is not "2" we will print a message informing the user of invalid input, 122 | and we will `recur` the `loop` again with the same message. 123 | 124 | All in all the functionality is rather simple, 125 | but the nested if-statements might look hostile. 126 | I am sure we can do something about those later, 127 | but let's finalize our MVP first. 128 | 129 | If you test this in REPL, 130 | depending on your editor you might reach weird results. 131 | Like the prompt text might appear after you actually gave the answer. 132 | Don't worry about this. It should work just fine, 133 | when we actually compile and run the code. 134 | 135 | ## Saving to file 136 | 137 | So now that we have our application partially working, 138 | it is time to write the shopping list to a file. 139 | 140 | This is rather straightforward. 141 | To write to a file we use [spit](https://clojuredocs.org/clojure.core/spit). 142 | 143 | ### [spit](https://clojuredocs.org/clojure.core/spit) 144 | 145 | `split` is cousin of `slurp` that we met before. 146 | It does exactly the opposite of what `slurp` does. 147 | 148 | It can be used to write to different outputs, 149 | but we will now use it to write to a file. 150 | 151 | Let's give it a try: 152 | 153 | ```clojure 154 | (spit "./spit-test.txt" "Hello from the file") 155 | ;=> nil 156 | ``` 157 | 158 | You should now we able to find the file _spit-test.txt_ from your project root. 159 | You can also verify that that the file has the provided text inside. 160 | 161 | Another handy thing is that we can append to the file by providing option for that. 162 | 163 | ```clojure 164 | (spit "./spit-test.txt" "\n second line" :append true) 165 | ;=> nil 166 | ``` 167 | 168 | If you look to the previous file you can see that a second line has appeared there. 169 | 170 | ### Writing shopping list to file 171 | 172 | So now that we know how to write a shopping list to a file, 173 | let's modify our `-main` function to do so. 174 | 175 | ```clojure 176 | (defn -main 177 | [& args] 178 | (loop [choice (prompt "Enter a number:\n1. Add product\n2. Save shopping list")] 179 | (if (= choice "1") 180 | (do (add-product-to-shoppings) 181 | (recur (prompt "Enter a number:\n1. Add product\n2. Save shopping list"))) 182 | (if (= choice "2") 183 | (do (spit "./things-to-buy.txt" @shoppings) ;; We changed this part 184 | (println "Shopping list saved")) 185 | (do (println "Invalid choice!!! Try again") 186 | (recur (prompt "Enter a number:\n1. Add product\n2. Save shopping list"))))))) 187 | 188 | ``` 189 | 190 | So we added `do` statement and a `spit` function inside it it. 191 | Also note that the previous `println` is inside this function as well. 192 | 193 | Let's run this version. 194 | Write few products and quantities, 195 | when the application prompts you. 196 | After you are done, 197 | select the option for saving the shopping list. 198 | 199 | You can see that a file is indeed created on you computer. 200 | But unfortunately the file does not look very human friendly. 201 | 202 | things-to-by.txt should look something like this (depending what you wrote): 203 | 204 | ```text 205 | [{:product "Milk", :amount "3"} {:product "Cola", :amount "15"}] 206 | ``` 207 | 208 | Nevertheless it does work, 209 | and you can kind of read it, 210 | thus it meets the requirements of MVP. 211 | 212 | In the next section we will refactor the code a bit. 213 | We will also improve the functionality so that it is no longer MVP 214 | 215 | Next: [Not so MVP](6-not-so-mvp.md) 216 | -------------------------------------------------------------------------------- /materials/4-state-in-clojure/7-no-need-for-atoms.md: -------------------------------------------------------------------------------- 1 | # 4.7. No Need for Atoms 2 | 3 | Atom is useful tool and I wanted to tell you about it, 4 | so that not knowing about it won't bother you like it did bother me. 5 | 6 | Even though `atom`s are useful, 7 | it should not be over used. 8 | Especially when the reality is that we rarely really need it. 9 | In many cases we can store the state in other ways, 10 | recursion being the most common way to do so. 11 | 12 | As mentioned before Clojure does not handle classical recursion very well, 13 | thus we rely on loop/recur when utilizing recursion. 14 | 15 | We built this application using an `atom`, 16 | since it was an easy way to demonstrate the functionality of `atom`s. 17 | But the fact that we did build this application utilizing `atom`s does not mean we had to do so, 18 | nor that we should do so. 19 | In fact this sort of application should be built using recursion. 20 | 21 | We will now refactor the application to work without `atom`s. 22 | 23 | Let's start by deleting the `def` shoppings form. 24 | We won't be needing that anymore. 25 | We can also delete the add-shopping function, 26 | since that only directly deals with the `atom`. 27 | Nor will we need add-product-to-shoppings, 28 | so let's delete that too. 29 | 30 | At this point we have relatively little code left, 31 | and our main function is nonfunctional, 32 | since it relies on the functions we deleted. 33 | 34 | Let's change the main function a bit: 35 | 36 | ```clojure 37 | (defn -main 38 | [& args] 39 | (loop [shoppings [] 40 | choice (prompt options)] 41 | (case choice 42 | "1" (recur (conj shoppings {:product (prompt "What to buy?") 43 | :amount (prompt "How many?")}) 44 | (prompt options)) 45 | "2" (do (save-shopping-list output-file shoppings) 46 | (println "Shopping list saved")) 47 | (do (println "Invalid choice!!! Try again") 48 | (recur shoppings 49 | (prompt options)))))) 50 | ``` 51 | 52 | So quite a bit has changed here, 53 | but not too much to make it unrecognizable. 54 | 55 | Let's walk through the changes we did. 56 | 57 | We added another parameter to the `loop`. 58 | It now has bindings *shoppings* and *choice*. 59 | Because we changed the `loop`'s bindings, 60 | we had to also change the `recur`s accordingly. 61 | So in case the user selects 1, 62 | we are now recurring the loop with a new shoppings vector that is the result of old vector with new item added to it. 63 | The item is created directly by prompting for the product and amount inside the map. 64 | For the default case, `recur` now recurs with the same shoppings as we had on the current iteration, 65 | so the state won't change if this occurs. 66 | Lastly we also modified the final part where we save the list to the file by referring to our shopping vector instead of an `atom`. 67 | 68 | As you can see we have now far less code, 69 | our state is handled by recursion and the code is easier to read, 70 | since there is less going on in general. 71 | 72 | If we would like to, 73 | we could abstract the creation of single shopping to a dedicated function. 74 | 75 | Such a function would look something like this: 76 | 77 | ```clojure 78 | (defn prompt-shopping 79 | [] 80 | {:product (prompt "What to buy?") 81 | :amount (prompt "How many?")}) 82 | ``` 83 | 84 | You are free to do so if you wish to. 85 | It could be argued that it would clarify the `loop` in main function, 86 | but it is also rather unnecessary, 87 | since this function does very little. 88 | 89 | All in all the final solution looks something like this: 90 | 91 | ```clojure 92 | (ns no-atom-shopping-list.core 93 | (:gen-class)) 94 | 95 | (def output-file "./things-to-buy.txt") 96 | (def options "Enter a number:\n1. Add product\n2. Save shopping list") 97 | 98 | (defn prompt 99 | [msg] 100 | (println msg) 101 | (read-line)) 102 | 103 | 104 | (defn shopping->str 105 | [x] 106 | (str (:product x) " * " (:amount x))) 107 | 108 | (defn shopping-printable 109 | [shoppings] 110 | (clojure.string/join 111 | "\n" 112 | (map shopping->str shoppings))) 113 | 114 | (defn save-shopping-list 115 | [file shoppings] 116 | (spit file (shopping-printable shoppings))) 117 | 118 | 119 | (defn -main 120 | [& args] 121 | (loop [shoppings [] 122 | choice (prompt options)] 123 | (case choice 124 | "1" (recur (conj shoppings {:product (prompt "What to buy?") 125 | :amount (prompt "How many?")}) 126 | (prompt options)) 127 | "2" (do (save-shopping-list output-file shoppings) 128 | (println "Shopping list saved")) 129 | (do (println "Invalid choice!!! Try again") 130 | (recur shoppings 131 | (prompt options)))))) 132 | ``` 133 | 134 | ## Last words on the topic 135 | 136 | I am sure some authors would disagree with me telling you about the `atom` this early. 137 | I indeed did have a few deep conversations regarding this decision with my colleagues, 138 | since they too doubted it a bit. 139 | 140 | I truly hope that I have managed to convince you to not use `atom`s when not needed. 141 | It can be very tempting but is not necessary very often. 142 | 143 | New programmers sometimes feel tempted to create `atom`s inside functions to create a state inside the lexical scope. 144 | This is something I would strongly advice against. 145 | I have never met or heard about a situation where that would be a good idea. 146 | 147 | As a rule of thumb always try solving your state issues with recursion. If that fails, 148 | then look for other options such as `atom`s or databases or etc. 149 | 150 | With this final warning we finalize the section on `atom`s and state in Clojure. 151 | 152 | Next: [Chapter 5 - Requests for Data](../5-requests-for-data) 153 | -------------------------------------------------------------------------------- /materials/4-state-in-clojure/README.md: -------------------------------------------------------------------------------- 1 | # State in Clojure 2 | 3 | ## Full Disclaimer 4 | 5 | After some consideration I have decided to teach you about management [state](https://en.wikipedia.org/wiki/State_(computer_science)) in Clojure a bit earlier than most other sources. 6 | This is mostly due to the fact that when I was learning the basics of the language, 7 | it heavily annoyed me to not understand how this was done until much later. 8 | It indeed bugged me so much that it was hard for me to focus on learning the language and the functional paradigm. 9 | That being said, 10 | please keep in mind that this comes perhaps a bit earlier than it should, 11 | but it should not be an issue for you. 12 | 13 | ## About the Project 14 | 15 | As with the first project, 16 | we will be building some simple (and rather useless) application, 17 | and in the meanwhile we will learn some new Clojure. 18 | 19 | This time we are building a command line application for creating a shopping list. 20 | The application will have following requirements. 21 | 22 | 1. It prompts the user from command line for: 23 | - Product name 24 | - Quantity to be bought 25 | 26 | 2. Saves user inputs to data structure 27 | 28 | 3. Keep prompting user until user tells she is finished with the shopping-list 29 | 30 | 4. In the end the shopping list is saved to a text file in human readable form 31 | 32 | Requirements for this are quite simple. 33 | But some of these things are not as simple in functional paradigm, 34 | as they would be in [imperative programming](https://en.wikipedia.org/wiki/Imperative_programming). 35 | This of course does not mean that would be especially hard to create a such a solution in Clojure, as we will shortly see. 36 | 37 | We will try to get some repetition also involved with the assignment, 38 | to enforce learnings from the past chapters. 39 | But some things are left to you at this point. 40 | 41 | It is expected that you will try all the functions in the REPL as we go, 42 | I will also recommend that you will try to write tests for some of these functions as we go. 43 | Again try to avoid copying code from the guide or just reading along. 44 | The learning is most effective when you write and run the code on your own machine. 45 | 46 | ## [1. Adding to Data Structures](./1-adding-to-data-structures.md) 47 | 48 | We will learn how to add elements to data structures. 49 | You will learn about conj, concat and cons. 50 | 51 | ## [2. Storing State with Atom](./2-storing-state-with-atom.md) 52 | 53 | You get familiar with Atom and learn to mutate state in Clojure. 54 | We'll also learn about def for saving variables. 55 | 56 | ## [3. Conditional Love](./3-conditional-love.md) 57 | 58 | We will learn about boolean logic in Clojure. 59 | 60 | ## [4. Terminal Interface](./4-terminal-interface.md) 61 | 62 | We will make new friends with loop and recur functions, 63 | as well as with read-line and do functions. 64 | 65 | ## [5. Creating MVP](./5-creating-mvp.md) 66 | 67 | We will create a first functional minimum viable product of our application. 68 | 69 | ## [6. Not so MVP](./6-not-so-mvp.md) 70 | 71 | We will improve the functionality of the previous application, 72 | and do some refactoring to enhance the quality of the code. 73 | 74 | ## [7. No Need for Atoms](./7-no-need-for-atoms.md) 75 | 76 | We'll majorly refactor our application, 77 | and discover how atoms are not necessary for managing state in Clojure. 78 | -------------------------------------------------------------------------------- /materials/5-requests-for-data/2-filtering-data.md: -------------------------------------------------------------------------------- 1 | # 5.2 Filtering Data 2 | 3 | Thanks to Reddit and its amazing API, 4 | we now have access to large sets of real data. 5 | Processing data is something that Clojure really shines at. 6 | 7 | We have already met [`loop`](https://clojuredocs.org/clojure.core/loop), 8 | [`recur`](https://clojuredocs.org/clojure.core/recur), 9 | and [`map`](https://clojuredocs.org/clojure.core/map). 10 | 11 | Now we are going to get familiar with other tool for working with sequences. 12 | Meet [`filter`](https://clojuredocs.org/clojure.core/filter) 13 | 14 | Filter does exactly what it says. 15 | It filters values from a sequence. 16 | Many languages have similar features, 17 | so this might not be something novel. 18 | Regardless of that Clojure's filter works wonders. 19 | 20 | It is a clean and simple solution for remove unwanted items from sequence. 21 | It works somewhat like this: 22 | 23 | ```clojure 24 | (filter odd? [1 2 3 4 5 6 7 8 9]) 25 | ;=> (1 3 5 7 9) 26 | ``` 27 | 28 | [`odd?`](https://clojuredocs.org/clojure.core/odd_q) is a clojure function that returns true if the number in question is odd number. 29 | In otherwise it returns false. 30 | 31 | So filter takes two arguments: predicate and a collection. 32 | It applies the given predicate function to each value in the collection and removes all values that do not return true. 33 | 34 | The out put is always a sequence. 35 | So do not expect to get vector back if you input a vector. 36 | 37 | ## Predicates 38 | 39 | Filter itself are very much straight forward, and there is not much to talk about them. But they do lead us to the concept of [predicates](https://www.tutorialspoint.com/clojure/clojure_predicates.htm). 40 | 41 | Predicates are functions that evaluate to true or false. 42 | In Clojure these are commonly the functions with question marks at the end of them. 43 | 44 | Clojure core naturally offers many predicates, 45 | but we should not let that limit us, 46 | since it is really easy to write our own predicates as well. 47 | 48 | Let's write our own example predicate: 49 | 50 | ```clojure 51 | (defn larger-than-5? 52 | [x] 53 | (> x 5)) 54 | ``` 55 | 56 | This function returns true if the number given is larger than 5: 57 | 58 | ```clojure 59 | (larger-than-5? 6) 60 | ;=> true 61 | (larger-than-5? 3) 62 | ;=> false 63 | ``` 64 | 65 | After this we can effortlessly use our new predicate with filter: 66 | 67 | ```clojure 68 | (filter larger-than-5? [1 2 3 4 5 6 7 8 9]) 69 | ;=> (6 7 8 9) 70 | ``` 71 | 72 | ## Real Data, Real Issues 73 | 74 | Now we are familiar with `filter`. 75 | Before we get to filtering, 76 | we'll have to work with some data structures. 77 | 78 | Most of our tasks we set to ourselves are related to the posts and the data regarding them. 79 | The API does provide us with all this data, 80 | but it is a bit nested so we'll have to do some data parsing to access the data effectively. 81 | 82 | ### Parsing JSON 83 | 84 | Before we can access JSON data from response body affectively, 85 | we will have parse it from string into sensible data structures. 86 | Such as vectors and maps. 87 | In order to do the said parsing, 88 | we'll be using external dependency cheshire. 89 | We already added the necessary dependencies for cheshire to our project in the beginning of this chapter. 90 | Now we will have add a require statement for it into our code, 91 | and use it into our advantage. 92 | 93 | In the begin of our core file we should have the ns form. 94 | It should look currently something like this: 95 | 96 | ```clojure 97 | (ns reddit-analyser.core 98 | (:require [clj-http.client :as client]) 99 | (:gen-class)) 100 | ``` 101 | 102 | We'll make the changes so that it will look like this: 103 | 104 | ```clojure 105 | (ns reddit-analyser.core 106 | (:require [clj-http.client :as client] 107 | [cheshire.core :refer [parse-string]]) 108 | (:gen-class)) 109 | ``` 110 | 111 | Don't forget to eval the ns form in your REPL after making the changes, 112 | otherwise you might be up for bad time (errors). 113 | 114 | Now that we have the require clause under control, 115 | we can try our new tools. 116 | 117 | Let's start by accessing body of our response. 118 | This is simply done by using `:body` with our statement from previous part: 119 | 120 | ```clojure 121 | (:body (client/get url options)) 122 | ;=> "{\"kind\": \"Listing\", \"data\": {\"modhash\": \"\", \"dist\": 100,... 123 | ``` 124 | 125 | As you can see, 126 | we are able to access the body, 127 | but it is all just one massive string. 128 | That is not very informative nor fun to use. 129 | 130 | Our new friend cheshire offers funky tool as `parse-string` that we just imported to the project. 131 | It is able to parse data structures from JSON strings. 132 | Let's give it a try: 133 | 134 | ```clojure 135 | (parse-string (:body (client/get url options))) 136 | ;=> 137 | ;{"kind" "Listing", 138 | ; "data" {"modhash" "", 139 | ; "dist" 100, 140 | ;... 141 | ``` 142 | 143 | As you can se we get out the same JSON data, 144 | but this time in Clojure data structures. 145 | This is already something we can work with, 146 | and if you come from Languages like Java or C#, 147 | you might actually be rather impressed with this already. 148 | This is not a trivial trick with strongly typed languages 149 | 150 | You might also notice that the maps in this structure use strings as keys. 151 | This is fine, 152 | but it is not very idiomatical Clojure. 153 | In Clojure we like to use `:keywords`as key values. 154 | It not only looks better, 155 | but provides some convenience when using the maps. 156 | 157 | Luckily for us, 158 | the parse-string takes additional parameter that we can use. 159 | By passing additional `true` value as parameter, 160 | we'll inform the function that we would prefer using keywords instead of strings. 161 | 162 | Let's give it a shot: 163 | 164 | ```clojure 165 | (parse-string (:body (client/get url options)) true) 166 | ;=> 167 | ;{:kind "Listing", 168 | ; :data {:modhash "", 169 | ; :dist 100, 170 | ;... 171 | ``` 172 | 173 | Thats cool, right? 174 | At least I think it is very cool. 175 | 176 | ### Parsing data 177 | 178 | We have already previously learned how data from maps is access, 179 | so this should not be nothing fundamentally new. 180 | We will just use our skills from previous chapters to real world data. 181 | 182 | Now that we have nicely parsed the whole body into sensible data structures, 183 | it is time to get more familiar with it. 184 | I'll recommend you spend few minutes looking how the body works. 185 | It has a ton of data, 186 | but it is rather neatly structured so it is rather easy to work with. 187 | If you understand at least a bit what the body contains, 188 | the following content will be more interesting to you, 189 | since you won't be just blindly following my instructions. 190 | 191 | _Few minutes pass_. 192 | Now that you have gotten familiar with the body's content, 193 | we will start accessing it a bit more. 194 | 195 | We are most interested in the posts themselves. 196 | As you should now know, 197 | the posts themselves are inside data->children. 198 | This section then again contains kind and data. 199 | This last data is the actual details regarding single post. 200 | 201 | So next we wish to have a collection of only this data. 202 | 203 | Since fetching the data and parsing the data into our required format is going to take some work, 204 | let's define it all into single function `get-posts`. 205 | 206 | ```clojure 207 | (defn get-posts 208 | [] 209 | (let [body (:body (client/get url options))] 210 | (map :data (:children (:data (parse-string body true)))))) 211 | ``` 212 | 213 | Here we have all the logic required for fetching all the posts and parsing them into one collection. 214 | In this implementation we have a rather much happening on single line. 215 | Let's use the tools we have already learned to make it a bit more readable. 216 | `let`should do the job for now. 217 | 218 | ```clojure 219 | (defn get-posts 220 | [] 221 | (let [body (:body (client/get url options)) 222 | parsed-body (parse-string body true) 223 | children (:children (:data parsed-body))] 224 | (map :data children))) 225 | ``` 226 | 227 | It is a bit more code, 228 | but that is a price we often have to pay for clarity. 229 | 230 | With this we have the posts data parsed into nice format for our next tasks. 231 | 232 | ### Only good posts filter 233 | 234 | A bit earlier we learned the basics of filtering. 235 | Next we will use those skills to our real data from Reddit. 236 | 237 | There is a lot of posts in every sub-reddit, 238 | but we are busy people with no time to read them all. 239 | What if we would only be interested in posts that community has validated as "good posts". 240 | We could assume that a post with Score over 15 is probably a good post. 241 | 242 | So how would we get around filtering out all the posts that have score less than 15? 243 | 244 | Let's start by defining a predicate for defining if a post is good or not. 245 | Let's call this predicate function `good-post?`. 246 | It should look like something like this: 247 | 248 | ```clojure 249 | (defn good-post? 250 | [post] 251 | (> (:score post) 15)) 252 | ``` 253 | 254 | It is a simple function that returns true if the post in question has a score over 15, 255 | otherwise it false will be returned. 256 | 257 | With this done, 258 | let's use this predicate to filter down our posts. 259 | 260 | ```clojure 261 | (defn only-good-posts 262 | [posts] 263 | (filter good-post? posts)) 264 | ``` 265 | 266 | Easiest way to see if our function works, 267 | is to see if the count of posts is reduced below 100. 268 | 269 | ```clojure 270 | (count (only-good-posts (get-posts))) 271 | ;=> 74 272 | ``` 273 | 274 | Your number will most likely differ from mine since you are doing this on different time. 275 | 276 | If you wish to see what posts remain after filtering you can call the functions without `count`: 277 | 278 | ```clojure 279 | (only-good-posts (get-posts)) 280 | ``` 281 | 282 | This concludes our part regarding filters. 283 | At this point I recommend that you will take a small break from this guide, 284 | and build a few filters of your own to this reddit data. 285 | 286 | You should grasp how it works after no time! 287 | 288 | Next: [Reducing complexity](3-reducing-complexity.md) -------------------------------------------------------------------------------- /materials/5-requests-for-data/3-reducing-complexity.md: -------------------------------------------------------------------------------- 1 | # 5.3. Reducing Complexity 2 | 3 | Well I am not completely sure how I feel about this title but let's go with it, 4 | since I did not come up with anything better on the spot.A 5 | 6 | So previously we have learned about working through collections with `loop`/`recur`, `map` and `filter`. 7 | We could without lying too much state that you could get pretty far with these alone. 8 | Nevertheless, this merry band of functions is still missing one partner. 9 | 10 | A certain course I studied when I was getting started with Clojure put it this way: 11 | [_One Function to rule the all_](http://iloveponies.github.io/120-hour-epic-sax-marathon/one-function-to-rule-them-all.html). 12 | 13 | And this is not very far from the truth when we are talking about functional programming. 14 | 15 | Without further ado! 16 | Our next quest: 17 | 18 | ## [Reduce](https://clojuredocs.org/clojure.core/reduce) 19 | 20 | In addition to the recursion, 21 | `reduce` is one of the primary ways to iterate in Functional Programming. 22 | And thus it will become your dear friend when goofing around with Clojure. 23 | 24 | `reduce` can feel both soothingly simple and annoying complicated, 25 | and oddly even both at once. 26 | It is a powerful tool, 27 | but I am not going to lie to you. 28 | If you have not used `reduce` in your previous coding adventures, 29 | it might take a moment to get comfortable with `reduce`. 30 | 31 | `reduce` is a tool that can be found in one form or another from most of the programming languages. 32 | At least ones that have [first class functions](https://en.wikipedia.org/wiki/First-class_function). 33 | 34 | Without chanting more about radiance of `reduce`, 35 | let's take a look at it: 36 | 37 | ```clojure 38 | (reduce + 0 [1 2 3 4 5]) 39 | ;=> 15 40 | ``` 41 | 42 | So what does it do? 43 | Reduce takes 3 arguments. 44 | Those being function, value, and collection, 45 | or as official documentation puts it: 46 | _f, val, coll_. 47 | 48 | Reduce cannot take just any function, 49 | instead it has to be a function that takes two(2) arguments. 50 | Value is something like a starting value. 51 | It is a bit unnecessarily complex to explain, 52 | so I think it is better to just demonstrate it in a moment. 53 | Collection is the simply the collection we wish to iterate through. 54 | 55 | So what does `reduce` do with these parameters? 56 | In short it works in iterations repeatedly calling the function again and again with different arguments. 57 | On the first iteration the function calls the function by providing value and first item in the collection as arguments. 58 | The return value of the function is then stored into what we refer as "accumulator", 59 | which again is used as argument together with second item in the collection. 60 | The patter repeats until all the values in the collection has been iterated through. 61 | After no items are left to be iterated, 62 | the accumulator value is returned. 63 | 64 | I am personally not very happy with this explanation, 65 | but nether am I with almost any explanation for reduce that I have seen. 66 | Instead I believe it is much better explained with code examples. 67 | 68 | Let's have above example written out in kinda step by step representation: 69 | 70 | ```clojure 71 | (reduce + 0 [1 2 3 4 5]) 72 | (reduce + 1 [2 3 4 5]) 73 | (reduce + 3 [3 4 5]) 74 | (reduce + 6 [4 5]) 75 | (reduce + 10 [5]) 76 | (reduce + 15 []) 77 | ;=> 15 78 | ``` 79 | 80 | Value argument can be considered as initial value for accumulator, 81 | because that is quite accurately what it really is. 82 | 83 | Value itself has little limitations, 84 | and it can be either numbers, strings, vectors, maps, sets, or combinations of those. 85 | Map of sets? 86 | Why not. 87 | Vector of maps? 88 | If you wish so. 89 | 90 | Here is an example for turning a vector into set with `reduce`: 91 | 92 | ```clojure 93 | (reduce conj #{} [:a :b :c]) 94 | ;=> #{:c :b :a} 95 | ``` 96 | 97 | Here we split a string into vector of characters utilizing `reduce`: 98 | 99 | ```clojure 100 | (reduce conj [] "Rich Hickey") 101 | ;=> => [\R \i \c \h \space \H \i \c \k \e \y] 102 | ``` 103 | 104 | ([Rich Hickey](https://github.com/richhickey) is the [BDFL](https://en.wikipedia.org/wiki/Benevolent_dictator_for_life) for Clojure) 105 | 106 | `reduce` can actually be purposed to perform the actions of map or filter, 107 | even though I would not recommend you to over use it like that in the long run. 108 | But it can be a fun way to get comfortable with reduce. 109 | 110 | Another thing about reduce is that the value argument in non-obligatory. 111 | In some cases we could leave it out and get the same result. 112 | 113 | ```clojure 114 | (reduce + [1 2 3 4 5]) 115 | ;=> 15 116 | ``` 117 | 118 | If value argument is not provided first iteration of reduce will use first and second items on the collection as arguments. 119 | We mostly use the value argument we need to define the type of the output for our solution. 120 | 121 | For example this works: 122 | 123 | ```clojure 124 | (reduce conj #{} [:a :b :c :a :a :b]) 125 | ;=> #{:c :b a} 126 | ``` 127 | 128 | and this doesn't: 129 | 130 | ```clojure 131 | (reduce conj [:a :b :c :a :a :b]) 132 | ;=> CompilerException... 133 | ``` 134 | 135 | This is simply because we cannot conjoin :b to :a. 136 | 137 | ```clojure 138 | (conj :a :b) 139 | ;=> CompilerException... 140 | ``` 141 | 142 | ## Calculating average 143 | 144 | If we look at our list of requirements, 145 | next one in line is calculating the average score for all the posts. 146 | In calculating this we will be utilizing `reduce`. 147 | 148 | To calculate the average score we need two things: 149 | The total number of posts and total score of all the posts. 150 | We will write the function in manner that it can used for any number of posts, 151 | not only for predefined number. 152 | 153 | Our solution is rather straight forward: 154 | 155 | ```clojure 156 | (defn average-score 157 | [posts] 158 | (let [post-count (count posts) 159 | total-score (reduce + (map :score posts))] 160 | (/ total-score post-count))) 161 | ``` 162 | 163 | Here we use `let` to create two variables in lexical scope. 164 | One being post-count and one being total-score. 165 | After this we return the total-score divided post-count. 166 | 167 | `post-count` is rather straight forward, 168 | so won't be going through that. 169 | 170 | `total-score` instead is a bit more juicy. 171 | Let's see what that is about: 172 | In total-score we first use `map` to get score for each post by utilizing the `:score` keyword. 173 | (Remember, we can use key words as functions.) 174 | After this we use combination of `reduce` and `+` to get total of these scores. 175 | 176 | Let's see how this works: 177 | 178 | ```clojure 179 | (average-score (get-posts)) 180 | ;=> 698/25 181 | ``` 182 | 183 | (Your result probably differs from mine, 184 | since we are doing this on different times) 185 | 186 | As we can see, 187 | the average is displayed as [ratio](https://clojure.org/reference/data_structures#_ratio) instead of a decimal number. 188 | By default Clojure always displays divisions as ratios. 189 | Nevertheless, 190 | this ratio is rather nonsense, 191 | since we cannot really tell much by just looking at it. 192 | In our case it would be much nicer just to have a plain old decimal number. 193 | Thankfully we can achieve this rather effortlessly. 194 | 195 | Let's make a small adjustment to our code: 196 | 197 | ```clojure 198 | (defn average-score 199 | [posts] 200 | (let [post-count (count posts) 201 | total-score (reduce + (map :score posts))] 202 | (float (/ total-score post-count)))) 203 | 204 | (average-score (get-posts)) 205 | ;=> 27.92 206 | ``` 207 | 208 | By running our return value through [`float`](https://clojuredocs.org/clojure.core/float) function, 209 | it will be presented as decimal number instead of ratio. 210 | 211 | Small note regarding reddit api: 212 | I have noticed that the scores returned by reddit are not very stable, 213 | they constantly go up and down every time you call the API. 214 | So don't be alarmed if you get different result by each call. 215 | 216 | Next: [Reducing with our rules](4-reducing-with-our-rules.md) 217 | -------------------------------------------------------------------------------- /materials/5-requests-for-data/4-reducing-with-our-rules.md: -------------------------------------------------------------------------------- 1 | # 5.4 Reducing with our rules 2 | 3 | Even though Clojure's core functions are plenty and powerful, 4 | our business logic requirements are often rather specific, 5 | thus we very often end up writing our own functions to use with reduce to achieve desired outcomes. 6 | 7 | This will be the case with some parts for our reddit-analyzer as well. 8 | Next we will write the logic for calculating number of posts per author. 9 | On the way we are going to learn few new things, 10 | but by now that should be business as usual already. 11 | 12 | So in order to calculate how many posts has each author written, 13 | we need to write a custom function to use with reduce. 14 | Such functions are often referred as _helper functions_. 15 | 16 | Our helper function will look something like this: 17 | 18 | ```clojure 19 | (defn post-count-helper 20 | [acc x] 21 | (update acc x (fnil inc 0))) 22 | ``` 23 | 24 | Wow, that probably looks like non-sense to you. 25 | Well no reason to panic, 26 | let's work through it! 27 | 28 | `acc` stands for accumulator, 29 | and it is commonly used shorthand that I suggest you will also use. 30 | 31 | `x` is the next value iterated by reduce. 32 | In our case it could be also referred as author-name, 33 | but since `acc` and `x` are such a common naming patterns, 34 | that we will use them for now. 35 | 36 | ## updating value in map 37 | 38 | [`update`](https://clojuredocs.org/clojure.core/update) is very heavily used function for updating values in maps. 39 | 40 | `update` takes 3 parameters: map, key and function. 41 | Update returns you a new map with changes made. 42 | The original map will remain untouched. 43 | This is a practice in Clojure that you will grow to love. 44 | It is everywhere in the language. 45 | 46 | So how will `update` work? 47 | Well it updates the value corresponding the provided key by calling the provided functions with the corresponding value as parameter. 48 | 49 | God damn that is an awful explanation. 50 | Just horrible. 51 | Let's hope that code sample will make up for it, 52 | since this is not very difficult concept. 53 | 54 | ```clojure 55 | (update {:a 1 :b 10 :c 100} :b inc) 56 | ;=> {:a 1, :b 11, :c 100} 57 | ``` 58 | 59 | As we can see, 60 | the value at `:b` has been incremented by 1, 61 | which is exactly what `inc` does. 62 | 63 | I hope that was good enough clarification. 64 | In case my explanation left you doubtful, 65 | I recommend checking out the official documentation (and comments) for update. 66 | Playing around with update might also be a good idea. 67 | 68 | ## fnil for nil safety 69 | 70 | The other new function we are using here is [`fnil`](https://clojuredocs.org/clojure.core/fnil). 71 | 72 | `fnil` gives us power to use default values in case of `nil` values. 73 | 74 | So what does this mean? 75 | 76 | Well many functions have annoying way of not working if we give them `nil` as an argument. 77 | `inc` for example is one of such functions. 78 | 79 | ```clojure 80 | (inc nil) 81 | ;=> NullPointerException clojure.lang.Numbers.ops (Numbers.java:1013) 82 | ``` 83 | 84 | (If you are Java programmer this exception might cause cold sweat dripping on your back.) 85 | 86 | By utilizing `fnil` we can create a new function, 87 | that uses default values incase a `nil` argument is provided. 88 | 89 | ```clojure 90 | (def nil-inc (fnil inc 0)) 91 | 92 | (nil-inc 10) 93 | ;=> 11 94 | 95 | (nil-inc nil) 96 | ;=> 1 97 | ``` 98 | 99 | So here we use `fnil` and `def`to define a new function `nil-inc`. 100 | If we use `nil-inc` to normal numeric values, 101 | it works as normal inc. 102 | Only difference is if argument `nil` is used. 103 | Then the function works just as `inc` would with argument 0. 104 | 105 | By no means are we forced to predefine our `fnil` functions like this. 106 | We can just create one on the go. 107 | Like this: 108 | 109 | ```clojure 110 | ((fnil inc 0) 10) 111 | ;=> 11 112 | 113 | ((fnil inc 0) nil) 114 | ;=> 1 115 | ``` 116 | 117 | ## Back to reducing 118 | 119 | Now that we know about fnil and update we can proceed with our helper function. 120 | 121 | ```clojure 122 | (defn post-count-helper 123 | [acc x] 124 | (update acc x (fnil inc 0))) 125 | ``` 126 | 127 | So what happens here, 128 | is that our accumulator (which is a map) is being updated by incrementing the value in the key (which will be the author). 129 | If the value is missing (and we get `nil`), 130 | then default value 0 is being used. 131 | 132 | Let's give this function a go with some made up fake data: 133 | 134 | ```clojure 135 | (reduce post-count-helper {} ["batman" "robin" "superman" "batman"]) 136 | ;=> {"batman" 2, "robin" 1, "superman" 1} 137 | ``` 138 | 139 | Great! 140 | Our functions seems to be working wonders. 141 | 142 | Now that we have the helper function written down, 143 | all that is left is to write the actual post-count function. 144 | This task is easy, since our fake data example has this part mostly covered already. 145 | 146 | Whole thing should look something like this: 147 | 148 | ```clojure 149 | (defn post-count-helper 150 | [acc x] 151 | (update acc x (fnil inc 0))) 152 | 153 | (defn author-post-count 154 | [posts] 155 | (let [authors (map :author posts)] 156 | (reduce post-count-helper {} authors))) 157 | ``` 158 | 159 | We can call it easily by passing it some posts: 160 | 161 | ```clojure 162 | (author-post-count (get-posts)) 163 | ;=> 164 | ;{"mac" 5, 165 | ; "k0t0n0" 1, 166 | ; "okusername3" 1, 167 | ; "dotemacs" 1, 168 | ; ... 169 | ``` 170 | 171 | Obviously your return value will be completely different from mine, 172 | since you are going to be running on different time. 173 | 174 | This solution is not perfect, 175 | but it works rather well. 176 | 177 | With that being said we can move on to the next challenge on our list. 178 | Let's move forward to author total score! 179 | 180 | Next: [Partial meetings](5-partial-meetings.md) -------------------------------------------------------------------------------- /materials/5-requests-for-data/5-partial-meetings.md: -------------------------------------------------------------------------------- 1 | # 5.5. Partial meetings 2 | 3 | In order to perform the task of calculating the total score of each author, 4 | we are going to define a helper function to utilize it with `reduce`. 5 | 6 | We'll use [`partial`](https://clojuredocs.org/clojure.core/partial) while defining our helper function. 7 | `partial` is a handy tool for defining new functions based on existing ones. 8 | It is especially commonly used together with iterators such as `map` or `filter`. 9 | Like `fnil`, 10 | `partial` returns a new functions that is based on the function given as an argument. 11 | 12 | So what does `partial` do? 13 | It is fairly simple function. 14 | `partial` takes a function and n number of arguments. 15 | It then returns a function where these arguments are hard coded, 16 | but remaining arguments are still to be provided by user when invoking the new function. 17 | 18 | Let's look at this in action: 19 | 20 | ```clojure 21 | ((partial + 10) 5) 22 | ;=> 15 23 | 24 | ((partial str "Hello, ") "World") 25 | ;=> "Hello, World" 26 | ``` 27 | 28 | Number of arguments has to be less or equal to what the provided function takes, 29 | or it will naturally result in an exception. 30 | 31 | So why is partial used? 32 | We well we often have cases where we would like to always use the same parameter or parameters, 33 | or we would like to use a function as a parameter to another function, 34 | which has limitations regarding the number of arguments the provided function should accept. 35 | Such is the case here. 36 | 37 | Our helper function looks something like this: 38 | 39 | ```clojure 40 | (defn total-score-helper 41 | [acc x] 42 | (update acc (:author x) (fnil (partial + (:score x)) 0))) 43 | ``` 44 | 45 | That is a quite a lot happening on a single line of code. 46 | Let's try breaking it up, 47 | so it is easier to focus on one argument at a time. 48 | (This is often a handy trick when slapped in the face with long one liner functions) 49 | 50 | ```clojure 51 | (defn total-score-helper 52 | [acc x] 53 | (update acc 54 | (:author x) 55 | (fnil (partial + (:score x)) 0))) 56 | ``` 57 | 58 | So here we again have `acc` (accumulator) and `x` (a single post), 59 | nothing new there. 60 | Then we are again calling `update` on `acc` as we did before, 61 | so still clear and simple. 62 | As key argument we are providing what ever is behind `:author` keyword in the post, 63 | which is obviously the author of the post. 64 | 65 | Then comes the difficult part. 66 | 67 | When reading LISP such as Clojure, 68 | it is good to start from the inner most parentheses. 69 | In (:score x) we get the score on the post given. 70 | We then use that in partial together with `+` function. 71 | So we are creating a function, 72 | which with single argument will return score plus the score of this post. 73 | Then that function we are passing to `fnil` with default 0. 74 | 75 | So essentially the function takes a number and then adds score of post to it. 76 | This is then the function we use together with `update`. 77 | All of this might seem overly complicated for now for, 78 | but it rather safe way to iterate and sum things together in comparison to traditional for and while loops. 79 | Furthermore, 80 | it only feels difficult until you get used to this kind of functional thinking. 81 | 82 | Next we should test try our helper function. 83 | 84 | ```clojure 85 | (total-score-helper {"Rich" 15 "Bob" 10} {:author "Steve" :score 2}) 86 | ;=> {"Rich" 15, "Bob" 10, "Steve" 2} 87 | 88 | (total-score-helper {"Rich" 15 "Bob" 10} {:author "Bob" :score 2}) 89 | ;=> {"Rich" 15, "Bob" 12} 90 | ``` 91 | 92 | Great! The function seems to be working wonders. 93 | Now we only have to tie it together with `reduce`: 94 | 95 | ```clojure 96 | (defn author-total-score 97 | [posts] 98 | (reduce total-score-helper {} posts)) 99 | 100 | (author-total-score (get-posts2)) 101 | ;=> {"mac" 192, 102 | ; "k0t0n0" 36, 103 | ; "okusername3" 16, 104 | ; "dotemacs" 27, 105 | ; "yogthos" 375, 106 | ; "tscrowley" 15, 107 | ; "childofsol" 30, 108 | ; ... 109 | ``` 110 | 111 | As usual your numbers will probably differ from mine. 112 | 113 | Next we will work on filtering down to all the posts that have links. 114 | 115 | Next: [Give me links](6-give-me-links.md) 116 | -------------------------------------------------------------------------------- /materials/5-requests-for-data/6-give-me-links.md: -------------------------------------------------------------------------------- 1 | # 5.6. Give me links 2 | 3 | Now we will utilize bunch of skills we learned so far. 4 | Our next goal is to grab all the links posted and return the list of them. 5 | 6 | It is rather common in reddit to just post a link and engage in the conversation regarding it. 7 | So what if we would like to list all the links posted? 8 | 9 | Let's get started! 10 | 11 | In reddit datastructure if a links beging posted are stored in `:url`. 12 | Text written by author of the post is stored in `:selftext`. 13 | So if the particular post contains nothing inside `:selftext`, 14 | it is safe to assume that the post in question contains only a link. 15 | 16 | We will iterate through all the posts and collect the `:url`s of those posts that are lacking content in `:selftext`. 17 | 18 | As you might have guessed, 19 | we will be needing a helper function again. 20 | 21 | ```clojure 22 | (defn links-posted-helper 23 | [acc x] 24 | (if (empty? (:selftext x)) 25 | (conj acc (:url x)) 26 | acc)) 27 | ``` 28 | 29 | You should be able to figure out what this helper does just by reading the code. 30 | All of the pieces are already familiar to you from earlier. 31 | 32 | Just in case we are going to work through this together, 33 | but you should try deducing the functionality by yourself first. 34 | 35 | So what does our function do? 36 | 37 | It takes an `acc` (accumulator) and map `x` (post in our case). 38 | If the `:selftext` is empty, 39 | `:url` is added to `acc`. 40 | If there is content behind `:selftext`, 41 | then the previous `acc` is returned. 42 | 43 | What is left for us is to define a function to iterate through the posts with reduce and our helper function. 44 | 45 | ```clojure 46 | (defn links-posted 47 | [posts] 48 | (reduce links-posted-helper [] posts)) 49 | ``` 50 | 51 | With all of this done, 52 | we should be able to call our creation and get a list of links. 53 | 54 | ```clojure 55 | (links-posted (get-posts)) 56 | ;=> ["https://github.com/gnarroway/mongo-driver-3" 57 | ; "https://stuartsierra.com/2019/12/21/clojure-start-time-in-2019" 58 | ; "https://github.com/Provisdom/spectomic" 59 | ; "https://link.medium.com/G4s6eRQFA2" 60 | ; ... 61 | ``` 62 | 63 | Seems like our creation is working! 64 | There is of course other ways to perform this kinda action, 65 | but this is rather straight forward so we will stick with this. 66 | 67 | This concludes our work on filtering the links. 68 | It was also the last operational functionality for our application. 69 | Next will just have to tie it all up with simple console UI. 70 | 71 | Next: [Console UI](7-console-ui.md) 72 | -------------------------------------------------------------------------------- /materials/5-requests-for-data/7-console-ui.md: -------------------------------------------------------------------------------- 1 | # 5.7. Console UI 2 | 3 | We have done all of this before already, 4 | but we will keep on doing it. 5 | I don't want to teach you how to write functions what only work in void. 6 | 7 | To keep it all at least bit interesting, 8 | let's try some new way to create this UI. 9 | The UI will be creating won't be pretty, 10 | mostly because we don't want to waste time on things like that now. 11 | We will be creating using recursion. 12 | It is much more idiomatic way of performing tasks like this than for example `loop`that we used before. 13 | 14 | (Note: I have heard that some REPL implementation in certain editors might act funny when using `read-line` inside a recursive function, 15 | so if you run into issues, try using `lein run` from command line instead.) 16 | 17 | First lets define the choices we wish to have in our UI: 18 | 19 | ```clojure 20 | (def ui-choices 21 | "1: All posts 22 | 2: Only good posts 23 | 3: Average score 24 | 4: Author post count 25 | 5: Author total score 26 | 6: Links posted 27 | Enter choice:\n") 28 | ``` 29 | 30 | We are defining this as a separate `def` statement, 31 | so a lengthy text like this won't pollute our business logic with unnecessary boilerplate. 32 | 33 | Next we will define our run-ui function: 34 | 35 | ```clojure 36 | (defn run-ui 37 | [choice posts] 38 | (if (empty? choice) 39 | (println "done.") 40 | (do (clojure.pprint/pprint (case choice 41 | "1" posts 42 | "2" (only-good-posts posts) 43 | "3" (average-score posts) 44 | "4" (author-post-count posts) 45 | "5" (author-total-score posts) 46 | "6" (links-posted posts) 47 | (println ui-choices))) 48 | (run-ui (read-line) posts)))) 49 | 50 | ``` 51 | 52 | This function takes two arguments. 53 | The first being the choice made by the user. 54 | User input in other words. 55 | The second one is posts. 56 | The function check is the choice is empty. 57 | So if the user provided no input, 58 | the function will print out "done." and return nil (remember clojure print statements return nil). 59 | If the choice is any number from 1 to 6 the corresponding function is called, 60 | and the output of the function is printed by `clojure.pprint/pprint`. 61 | If the choice is something else than 1-6, 62 | the choices will pre printed again. 63 | After printing the result of `case`, 64 | the run-ui will recursively call itself with new user input and same posts. 65 | 66 | So what is [`clojure.pprint/pprint`](https://clojuredocs.org/clojure.pprint/pprint)? 67 | It is definitely something we have not seen before. 68 | 69 | Well in clojure not all functions are part of clojure.core. 70 | With these functions we have to either include them in the `:require` statement in the beginning of the file, 71 | or alternatively specify their namespace when calling them. 72 | When you need a single function from a namespace just once, 73 | like we do here, 74 | it can be a good idea to just refer to the function with its namespace. 75 | But if you are using the function in question more than once or using multiple functions from the same namespace, 76 | it is definitely a better idea to include the functions to `:require` statement. 77 | 78 | All that is left to do, 79 | is to add `run-ui` into the main function that is being called when the program runs. 80 | 81 | ```clojure 82 | (defn -main 83 | [& args] 84 | (do (println ui-choices) 85 | (run-ui (read-line) (get-posts)))) 86 | ``` 87 | 88 | Great. 89 | In a world of poor software development this would conclude our work here, 90 | but luckily we are learning good practices here. 91 | 92 | Currently our code looks something like this: 93 | 94 | ```clojure 95 | (ns reddit-analyser.core 96 | (:require [clj-http.client :as client] 97 | [cheshire.core :refer [parse-string]]) 98 | (:gen-class)) 99 | 100 | (def options {:headers {"User-agent" "mega-secret-1337"} 101 | :query-params {:limit 100}}) 102 | 103 | (def url "https://www.reddit.com/r/Clojure.json") 104 | 105 | (defn get-posts 106 | [] 107 | (let [body (:body (client/get url options))] 108 | (->> (parse-string body true) 109 | :data 110 | :children 111 | (map :data)))) 112 | 113 | (defn good-post? 114 | [post] 115 | (> (:score post) 15)) 116 | 117 | (defn only-good-posts 118 | [posts] 119 | (filter good-post? posts)) 120 | 121 | (defn average-score 122 | [posts] 123 | (let [post-count (count posts) 124 | total-score (reduce + (map :score posts))] 125 | (float (/ total-score post-count))))) 126 | 127 | (defn post-count-helper 128 | [acc x] 129 | (update acc x (fnil inc 0))) 130 | 131 | (defn author-post-count 132 | [posts] 133 | (let [authors (map :author posts)] 134 | (reduce post-count-helper {} authors))) 135 | 136 | (defn total-score-helper 137 | [acc x] 138 | (update acc 139 | (:author x) 140 | (fnil (partial + (:score x)) 0)))) 141 | 142 | (defn author-total-score 143 | [posts] 144 | (reduce total-score-helper {} posts)) 145 | 146 | (defn links-posted-helper 147 | [acc x] 148 | (if (empty? (:selftext x)) 149 | (conj acc (:url x)) 150 | acc)) 151 | 152 | (defn links-posted 153 | [posts] 154 | (reduce links-posted-helper [] posts)) 155 | 156 | (def ui-choices 157 | "1: All posts 158 | 2: Only good posts 159 | 3: Average score 160 | 4: Author post count 161 | 5: Author total score 162 | 6: Links posted 163 | Enter choice:\n") 164 | 165 | (defn run-ui 166 | [choice posts] 167 | (if (empty? choice) 168 | (println "done.") 169 | (do (clojure.pprint/pprint (case choice 170 | "1" posts 171 | "2" (only-good-posts posts) 172 | "3" (average-score posts) 173 | "4" (author-post-count posts) 174 | "5" (author-total-score posts) 175 | "6" (links-posted posts) 176 | (println ui-choices))) 177 | (run-ui (read-line) posts)))) 178 | 179 | (defn -main 180 | [& args] 181 | (do (println ui-choices) 182 | (run-ui (read-line) (get-posts)))) 183 | ``` 184 | 185 | In the next section we are going to refactor this code to be more idiomatic clojure. 186 | In the mean while we will learn few more new tricks. 187 | 188 | Before we start refactoring I would advice you to write some test for this code using your learning from [3.4. Testing in Clojure](../3-first-project/4-testing-in-clojure.md). 189 | Writing tests is a great way to learn to code, 190 | so I will not show you what kind of test are required here. 191 | I leave that to be decided by you. 192 | 193 | After you are done with writing some tests, 194 | and you feel like moving forward, 195 | we can jump to the next section 196 | 197 | Next: [Function with no name](8-function-with-no-name.md) 198 | -------------------------------------------------------------------------------- /materials/5-requests-for-data/9-meet-threading-macros.md: -------------------------------------------------------------------------------- 1 | # 5.9. Meet Threading 2 | 3 | We are almost done with this application, 4 | but there is still few more places that require some polishing up. 5 | 6 | Next we are going to learn of one of the most loved tools in all Clojure. 7 | 8 | ## [Threading Macros](https://clojure.org/guides/threading_macros) 9 | 10 | Treading macros, also sometimes referred as arrow macros can be very intimidating for the new Clojurists, 11 | at least I remember looking them with terror when I first encountered them. 12 | 13 | Arrow macros come in two formats thread-first [`->`](https://clojuredocs.org/clojure.core/-%3E), 14 | and thread-last [`->>`](https://clojuredocs.org/clojure.core/-%3E%3E). 15 | There is also some more advanced versions of threading macros, 16 | but we won't be looking into them now. 17 | In case you are interested you can read more from this [guide](https://clojure.org/guides/threading_macros). 18 | 19 | So what are threading macros? 20 | Well in his immense wisdom Sensei Rich Hickey foresaw the __impending doom of thousand nested parentheses__. 21 | He acted fiercely to prevent this disaster and thus the `->` and `->>` were created. 22 | 23 | So in short, 24 | threading macros help us to write clearer code instead of dozens and dozens of nested forms. 25 | Code written with treading macro can be read from top to bottom, 26 | unlike normal Clojure code that is being read inside out. 27 | 28 | This is again something that that is much easier to show, 29 | instead of trying to explain how it works. 30 | 31 | Here is completely made up function that is supposed to create numbers of magical quality as strings. 32 | 33 | ```clojure 34 | (defn get-magic-numbers 35 | [x] 36 | (take x (map str (map #(* % 8) (filter #(= 0 (mod % 7 ))(filter even? (range))))))) 37 | 38 | (get-magic-numbers 10) 39 | ;=> ("0" "112" "224" "336" "448" "560" "672" "784" "896" "1008") 40 | ``` 41 | 42 | Try to interpret what this function does. 43 | It is not impossible, 44 | not even that hard, 45 | but damn it is tedious. 46 | 47 | We can obviously try to make this function clearer with some line breaks, 48 | but it will still look rather terrible. 49 | 50 | ```clojure 51 | (defn get-magic-numbers 52 | [x] 53 | (take x 54 | (map str 55 | (map #(* % 8) 56 | (filter #(= 0 (mod % 7)) 57 | (filter even? 58 | (range))))))) 59 | ``` 60 | 61 | Another thing we could try is to use `let` statement extensively to bring some order to this chaos. 62 | 63 | ```clojure 64 | (defn get-magic-numbers-with-let 65 | [x] 66 | (let [evens (filter even? (range)) 67 | div-7 (filter #(= 0 (mod % 7)) evens) 68 | times-8 (map #(* 8 %) div-7) 69 | strings (map str times-8)] 70 | (take x strings))) 71 | ``` 72 | 73 | Now we have made the code look like your average piece of Java. 74 | Yes it is easy to read, 75 | but don't you feel like there is far too much code here. 76 | To make sense of what is happening, 77 | we ended up writing so much more code. 78 | 79 | This is where threading macros come in. 80 | In this particular case we will use `->>` also known as thread-last. 81 | 82 | ```clojure 83 | (defn get-magic-numbers-threading 84 | [x] 85 | (->> (range) 86 | (filter even?) 87 | (filter #(= 0 (mod % 7))) 88 | (map #(* 8 %)) 89 | (map str) 90 | (take x))) 91 | ``` 92 | 93 | Boom! 94 | Significantly less code, 95 | and we can read what happens from top to down, 96 | instead of trying to figure out where one form ends and another begins. 97 | 98 | So what `->>` thread-last does, 99 | is that it resolves the first form and then adds it as a last argument to the next form in line. 100 | It will continue doing this until all the forms are solved, 101 | after which the final result is returned. 102 | 103 | Thread-first `->` is very similar to `->>`, 104 | only difference is that the result of the previous form is being passed as the first argument instead of the last. 105 | It is useful sometimes, 106 | but most of the time you will be using thread-last, 107 | since it is the far most popular of the threading macros. 108 | 109 | I recommend you will get nice and friendly with `->>` soon as possible, 110 | it will not only make your code easier to understand, 111 | but it it will also make your transition to LISP easier, 112 | since you don't have to read the code inside out. 113 | 114 | Now that we know of threading macros, 115 | we can move forward and implement one into our code. 116 | 117 | ## Get posts with threading 118 | 119 | Next we are going to refactor our `get-posts` so that it will utilize `->>`. 120 | Our current solution looks like this: 121 | 122 | ```clojure 123 | (defn get-posts 124 | [] 125 | (let [body (:body (client/get url options)) 126 | parsed-body (parse-string body true) 127 | children (:children (:data parsed-body))] 128 | (map :data children))) 129 | ``` 130 | 131 | And with little tinkering we managed to turn it into this: 132 | 133 | ```clojure 134 | (defn get-posts 135 | [] 136 | (let [body (:body (client/get url options))] 137 | (->> (parse-string body true) 138 | (:data) 139 | (:children) 140 | (map :data)))) 141 | 142 | ``` 143 | 144 | We use let for the body, 145 | since we want to pass `true` as the last argument for `parse-string`. 146 | After this it is pure threading. 147 | 148 | But there is actually one more step that we can make to improve this solution. 149 | we pass a single argument function to threading macro, 150 | such as a keyword for example, 151 | we can omit the parentheses. 152 | 153 | Like this: 154 | 155 | ```clojure 156 | (defn get-posts 157 | [] 158 | (let [body (:body (client/get url options))] 159 | (->> (parse-string body true) 160 | :data 161 | :children 162 | (map :data)))) 163 | ``` 164 | 165 | This concludes our work on threading. 166 | I recommend that you play around with it, 167 | so it will feel less intimidating. 168 | It is not a difficult concept, 169 | but rather different from how other programming languages handle things. 170 | I am sure you will master it after no time! 171 | 172 | This concludes our work on this chapter. 173 | We went through plenty of new things, 174 | which I recommend that you put to the use soon as possible, 175 | so they are not forgotten. 176 | 177 | In the end of all of this refactoring your code should look something like this: 178 | 179 | ```clojure 180 | (ns reddit-analyser.core 181 | (:require [clj-http.client :as client] 182 | [cheshire.core :refer [parse-string]]) 183 | (:gen-class)) 184 | 185 | (def options {:headers {"User-agent" "mega-secret-1337"} 186 | :query-params {:limit 100}}) 187 | 188 | (def url "https://www.reddit.com/r/Clojure.json") 189 | 190 | (defn get-posts 191 | [] 192 | (let [body (:body (client/get url options))] 193 | (->> (parse-string body true) 194 | (:data) 195 | (:children) 196 | (map :data)))) 197 | 198 | (defn only-good-posts 199 | [posts] 200 | (filter #(> (:score %) 15) posts)) 201 | 202 | (defn average-score 203 | [posts] 204 | (let [post-count (count posts) 205 | total-score (reduce + (map :score posts))] 206 | (float (/ total-score post-count)))) 207 | 208 | 209 | 210 | (defn author-post-count 211 | [posts] 212 | (frequencies (map :author posts))) 213 | 214 | (defn author-total-score 215 | [posts] 216 | (reduce (fn [acc m] 217 | (update acc 218 | (:author m) 219 | (fnil (partial + (:score m)) 0))) 220 | {} 221 | posts)) 222 | 223 | (defn links-posted 224 | [posts] 225 | (reduce (fn [acc post] 226 | (if (empty? (:selftext post)) 227 | (conj acc (:url post)) 228 | acc)) 229 | [] 230 | posts)) 231 | 232 | (def ui-choices 233 | "1: All posts 234 | 2: Only good posts 235 | 3: Average score 236 | 4: Author post count 237 | 5: Author total score 238 | 6: Links posted 239 | Enter choice:\n") 240 | 241 | (defn run-ui 242 | [choice posts] 243 | (if (empty? choice) 244 | (println "done.") 245 | (do (clojure.pprint/pprint (case choice 246 | "1" posts 247 | "2" (only-good-posts posts) 248 | "3" (average-score posts) 249 | "4" (author-post-count posts) 250 | "5" (author-total-score posts) 251 | "6" (links-posted posts) 252 | (println ui-choices))) 253 | (run-ui (read-line) posts)))) 254 | 255 | (defn -main 256 | [& args] 257 | (do (println ui-choices) 258 | (run-ui (read-line) (get-posts)))) 259 | ``` 260 | 261 | I would say that we achieved rather much functionality with about 80 lines of code. 262 | But hey, 263 | that is Clojure for you! 264 | 265 | Next we will be learning how to create a simple web backend with Clojure. 266 | 267 | Next: [Chapter 6 - Simple Web Application](../6-simple-web-application) 268 | -------------------------------------------------------------------------------- /materials/5-requests-for-data/README.md: -------------------------------------------------------------------------------- 1 | # Requests for Data 2 | 3 | In this chapter we will be playing with some real life data and making some [http requests](https://www.tutorialspoint.com/http/http_requests.htm). 4 | This is a rather common part for modern applications, 5 | so you probably might have done something similar in your previous programming adventures. 6 | 7 | Performing this sort of tasks is something Clojure is really amazing at, 8 | so I want to show you some of the better parts of our favorite language. 9 | 10 | Some of the documentation for the tools we will be using is a bit lengthy or tricky, 11 | so we'll get you started with them. 12 | We will show you the ropes of it, 13 | as well as point you to right direction for the future readings. 14 | 15 | ## Our project - Reddit Analyser 16 | 17 | More specifically we will be building a small application that fetches data from Reddit. 18 | We will be analyzing the data for [Clojure subreddit](https://www.reddit.com/r/Clojure/). 19 | 20 | Our solution will have following requirements: 21 | 22 | 1. Get 100 most recent posts from /r/Clojure from Reddit. 23 | 24 | 2. Print out only the good posts that have score higher than 25. 25 | 26 | 3. Print out the average score of a post. 27 | 28 | 4. Print out the number of posts per author. 29 | 30 | 5. Print out total score of each author. 31 | 32 | 6. Print out all links posted 33 | 34 | 7. Software can be requested to perform all the requested operations based on arguments given to it. 35 | 36 | ## Clojurians Community 37 | 38 | Since we will scratched Reddit in this chapter, 39 | I'll use that as an opportunity to talk a little bit about Clojure Community. 40 | 41 | Clojure has very friendly and welcoming community. 42 | When in doubt it is smart to ask for help, 43 | rather than bang your head endlessly against the wall. 44 | Some places are better for asking for help than others. 45 | Instead of shouting to the wind I would recommend using following options: 46 | 47 | ### [/r/Clojure in Reddit](https://www.reddit.com/r/Clojure/) 48 | 49 | If you are not familiar with this /r/Clojure I strongly recommend following it. 50 | Not only is it a great source for latest Clojure news, 51 | but there are lot of great discussions regarding the language and its ecosystem. 52 | 53 | ### [Clojurians Slack](http://clojurians.net/) 54 | 55 | Another great media for Clojure is Clojurians Slack. 56 | There you have numerous channels for discussion and many friendly and helpful fellow Clojurians to help you. 57 | Additionally many Clojure's Core team members also participate to discussions regularly here. 58 | 59 | There is also a dedicated beginners channel that you might wanna checkout! 60 | 61 | ### [Clojure User Groups](https://clojure.org/community/user_groups) 62 | 63 | There are many Clojure user groups. 64 | You can find more a bout them from the link above. 65 | If such an old-school approach suits you better you might want to check them out. 66 | 67 | ## [1. HTTP Requests](1-http-requests.md) 68 | 69 | We will learn how to make http requests in Clojure using clj-http library 70 | 71 | ## [2. Filtering Data](2-filtering-data.md) 72 | 73 | We will learn about predicates and filters with real world data. 74 | 75 | ## [3. Reducing Complexity](3-reducing-complexity.md) 76 | 77 | We will learn about reduce and how to effectively iterate data with it. 78 | 79 | ## [4. Reducing with Our Rules](4-reducing-with-our-rules.md) 80 | 81 | We will learn how to use our own functions with reduce to create more custom fit solutions. 82 | 83 | ## [5. Partial Meetings](5-partial-meetings.md) 84 | 85 | We will learn about partial and how to make use of it. 86 | 87 | ## [6. Give Me Links](6-give-me-links.md) 88 | 89 | We will combine skills we learned in this section and before to get list of all the links posted to Reddit. 90 | 91 | ## [7. Console UI](7-console-ui.md) 92 | 93 | We will build a console UI with recursion. 94 | 95 | ## [8. Function with No Name](8-function-with-no-name.md) 96 | 97 | We learn about Anonymous Functions in Clojure and how to get rid of unnecessary helper functions. 98 | 99 | ## [9. Meet threading](9-meet-threading-macros.md) 100 | 101 | We learn of threading macros and how to use the to simplify our code. 102 | -------------------------------------------------------------------------------- /materials/6-simple-web-application/3-query-parameters.md: -------------------------------------------------------------------------------- 1 | # 6.3. Query Parameters 2 | 3 | Now that we have made our first route, 4 | we can try to spice things up with some basic Reitit & Ring concepts. 5 | 6 | In this section we will create a math-routes that will perform basic arithmetic operations. 7 | Application like this has close to no real life applications, 8 | but it is a good way to work through the basics of Clojure Web Development. 9 | 10 | In a real world scenario you would like create similar interfaces for your endpoints. 11 | When the endpoints follow logical pattern, 12 | it is easier for the users to use it. 13 | In our case we will start by using different styles of interfaces. 14 | This would not necessarily be a good practice, 15 | but we will suffer it for the sake of learning. 16 | 17 | Let's try to keep this one short and get started right away. 18 | 19 | We will begin by by defining math-routes. 20 | 21 | ```clojure 22 | (def math-routes 23 | ["/math" 24 | []]) 25 | ``` 26 | 27 | Unlike last time we don't follow up the Route Path with a map, 28 | but instead we use a vector. 29 | For now the vector is empty, 30 | but this will no longer be the case after a moment. 31 | This kind of data structure allows us to group several routes under single route. 32 | 33 | This way we can represent our api like this: 34 | 35 | ```clojure 36 | ["/person" 37 | ["/age" {...}] 38 | ["/name" {...} 39 | ["/address" {...}]]] 40 | ``` 41 | 42 | Instead of this: 43 | 44 | ```clojure 45 | ["person/age" {...}] 46 | ["person/name" {...}] 47 | ["person/address" {...}] 48 | ``` 49 | 50 | This sort of grouping can significantly clarify our code. 51 | This is especially the case when the paths are longer and there is more of them. 52 | In [Retit documentation](https://cljdoc.org/d/metosin/reitit/0.5.5/doc/introduction) this referred as Nested Route Data. 53 | Nested Routes would also allow us to group multiple routes, 54 | so same middleware is used with them. 55 | This too can be really handy. 56 | (If you don't know what middleware is, 57 | don't worry about it. 58 | We will learn about it later on this course.) 59 | 60 | Ok! We got a bit side tracked there. 61 | Let us refocus and get back to coding. 62 | 63 | Our skeleton `math-routes` is not going to do anything in its current state. 64 | So lets add some meat to those bones. 65 | What we want to do is to create basic addition endpoint. 66 | This addition endpoint should ready two query parameters x and y. 67 | Then it should return the total of those numbers. 68 | 69 | Ok lets give it a go: 70 | 71 | ```clojure 72 | (def math-routes-wip 73 | ["/math" 74 | ["/addition" {:get (fn [request] 75 | (let [params (:query-params request) 76 | x (Long/parseLong (get params "x")) 77 | y (Long/parseLong (get params "y"))] 78 | (response/ok {:total (+ x y)})))}]]) 79 | 80 | ``` 81 | 82 | Furthermore, 83 | we also have to add our math-routes into our router: 84 | 85 | ```clojure 86 | (def app 87 | (ring/ring-handler 88 | (ring/router 89 | [hello-routes 90 | math-routes] 91 | {:data {:muuntaja m/instance 92 | :middleware [params/wrap-params 93 | muuntaja/format-middleware 94 | coercion/coerce-exceptions-middleware 95 | coercion/coerce-request-middleware 96 | coercion/coerce-response-middleware]}}) 97 | (ring/create-default-handler))) 98 | ``` 99 | 100 | After making the changes you might want to reload the namespace, 101 | stop the router and start it all over again. 102 | After that we should be able to call our endpoint. 103 | 104 | ```sh 105 | curl --request GET --url 'http://localhost:3000/math/addition?x=10&y=5' 106 | # {"total": 15} 107 | ``` 108 | 109 | Great it works! 110 | 111 | In case you are not familiar with the query parameters, 112 | those are the following part in our HTTP request: `?x=10&y=5` 113 | 114 | So what is going on with our code? 115 | Let's walk through it real quick. 116 | There is a lot of familiar stuff there. 117 | Route Path`"/addition"`, 118 | HTTP method `:get`, 119 | and handler `fn` we already met in the previous section. 120 | You might notice that we did not use the keyword `:handler` to specify our handler function. 121 | `:handler` is actually not required if only handler function is provided. 122 | Major differences here are inside the handler. 123 | This time we have named our single parameter `request`. 124 | Some people call this `req` but we won't be doing that since we like to be precise. 125 | 126 | Any query parameters used by the user can be found from the request. 127 | So the first thing we do inside let statement is to get those parameters with`(:query-params request)`. 128 | After this we get both the params `x` and `y`. 129 | All query parameters are always strings, 130 | so we need to parse them into numbers. 131 | We can achieve this by using `Long/parseLong`. 132 | After the `x` and `y` have been successfully parsed into Longs, 133 | we can create the ok response with total by using `response/ok` and standard maps and `+` function. 134 | 135 | Most of this should be familiar to you from the prior chapters. 136 | New stuff should be `request` & `:query-params` and perhaps `Long/parseLong`. 137 | `Long/parseLong` is very similar `Long/valueOf`, 138 | so I wanted to show it you. 139 | The difference is in the other return Java's `long` and the other Java's `Long`. 140 | From Clojure perspective this does not make much of a difference. 141 | 142 | `request` is an interesting creature. 143 | It contains a lot of information regarding the request made by the user, 144 | and it can be further enriched with middleware. 145 | All the data in `request` is access with basic data structure operations that we are already familiar with. 146 | 147 | Some of you might have been proactive and tried to calling this endpoint with decimal values. 148 | That will not work, 149 | since decimals cannot be parsed into `long`. 150 | We will not care about this now, 151 | but we might fix it later. 152 | 153 | Reminder: 154 | In order to utilize query parameters like we just did it is necessary to have `params/wrap-params` set into middleware. 155 | We did this already in the first section, 156 | you should have this part covered. 157 | 158 | That completes the brief introduction into query parameters in Ring & Reitit. 159 | 160 | Next up: [Body Parameters](4-body-parameters.md) 161 | -------------------------------------------------------------------------------- /materials/6-simple-web-application/4-body-parameters.md: -------------------------------------------------------------------------------- 1 | # 6.4. Body Parameters 2 | 3 | Previously we learned about query parameters. 4 | They are great. 5 | They work with all HTTP methods and are fairly easy to use. 6 | The problem with query parameters is that they don't work so nicely with more complex data structures. 7 | Furthermore all the characters in query parameters need to be URL-encoded, 8 | which as well might be a pain. 9 | 10 | Because of previously listed reasons it is quite common to use body parameters. 11 | This allows us to transfer data in the request body. 12 | One thing to know about body parameters is that they do not work with HTTP GET, HEAD, DELETE or OPTIONS requests, 13 | since those do not have body. 14 | 15 | That concludes our pre-lecture this time. 16 | Let's get to the point. 17 | 18 | Let's add the following code after our addition endpoint: 19 | 20 | ```clojure 21 | ["/subtraction" {:post {:description "Returns x subtracted by y" 22 | :handler (fn [request] 23 | (let [x (-> request :body-params :x) 24 | y (-> request :body-params :y)] 25 | (response/ok {:difference (- x y)})))}}] 26 | 27 | ``` 28 | 29 | So what is going on here? 30 | Well this is rather straight forward, 31 | and you might be actually be able to figure this out by yourself. 32 | Perhaps you should try to? 33 | 34 | Well I'll explain it all anyway just in case. 35 | 36 | Just like before we create a new Route. 37 | We use `"/subtraction"` as the path. 38 | Instead of HTTP GET we use POST this time. 39 | This is done with the `:post` keyword. 40 | 41 | In our Route Data we have now two keywords. 42 | 43 | `:description` in our case does not do anything, 44 | except it allows us to describe our endpoint. 45 | In theory we could use any key we wish here. 46 | But using `:description` is rather descriptive, 47 | and there is some tools in the Reitit Ecosystem that do rely on this keyword, 48 | so I would advice using it. 49 | Using descriptions is a good practice, 50 | especially when working with other developers. 51 | 52 | `:handler` specifies the handler function just like with `/hello` endpoint. 53 | Our handler function takes a request, 54 | from which we get body parameters x and y. 55 | You can see that we use the familiar thread macro `->`. 56 | You might also notice that the x & y are keywords. 57 | For the existence of `:body-params` we can thank our [Muuntaja](https://github.com/metosin/muuntaja) middleware, 58 | which we added into our app earlier. 59 | 60 | After fetching x & y all we have to do is to calculate the difference and create the response, 61 | but that should all be familiar to you at this point. 62 | 63 | You might have notice that unlike with query parameters, 64 | we did not have to parse the values from strings. 65 | This is because body parameters can actually be numbers. 66 | 67 | Ok let's see if our endpoint actually works: 68 | 69 | ```sh 70 | curl --request POST \ 71 | --url http://localhost:3000/math/subtraction \ 72 | --header 'content-type: application/json' \ 73 | --data '{ 74 | "x": 10, 75 | "y": 5.5 76 | }' 77 | # {"difference":4.5}% 78 | ``` 79 | 80 | Great! Everything seems to be in order. 81 | You might also have noticed that unlike our addition with query parameters, 82 | this implementation actually works with decimal numbers as well. 83 | This is because we did not parse our numbers into `long`s this time. 84 | 85 | If you try calling our endpoint without setting x or y, 86 | you will get some ugly java.lang.NullPointerException accompanied with HTTP status 500. 87 | You might as well try providing some other value types x or y and receive nasty looking exceptions. 88 | That kinda sucks. 89 | 90 | In the next part we will see if we could do something about those. 91 | 92 | Next up: [Request Coercion](5-path-parameters.md) 93 | -------------------------------------------------------------------------------- /materials/6-simple-web-application/5-path-parameters.md: -------------------------------------------------------------------------------- 1 | # 6.5. Path Parameters 2 | 3 | This one will be a short one. 4 | 5 | We already covered the query params and body params. 6 | That leaves us with third and last type of params. 7 | Path params. 8 | 9 | Let's make another endpoint for subtracting. 10 | This one will be using path params. 11 | Wouldn't it be cool if our endpoint would work like a sentence? 12 | 13 | ```clojure 14 | ["/subtract/:y/from/:x" {:get {:description "Returns x subracted by y" 15 | :handler (fn [request] 16 | (let [x (-> request :path-params :x Long/parseLong) 17 | y (-> request :path-params :y Long/parseLong)] 18 | (response/ok {:difference (- x y)})))}}] 19 | ``` 20 | 21 | Our code looks rather similar to before, 22 | but this time the path does looks a bit different. 23 | You can see that we have `:y` and `:x` inside the path looking a bit like keywords. 24 | Since we are not using body parameters, 25 | we can use HTTP get. 26 | Furthermore we need to parse numbers again with `Long/parseLong`. 27 | Just like the query parameters, 28 | the path parameters are also strings. 29 | 30 | Rest of this is just same old stuff that you are already familiar with. 31 | 32 | So let's give our new endpoint a call: 33 | 34 | ```sh 35 | curl --request GET \ 36 | --url http://localhost:3000/math/subtract/10/from/100 \ 37 | --header 'content-type: application/json' 38 | # {"difference":90} 39 | ``` 40 | 41 | It sees to work like charm. 42 | And the sentence path does look pretty nice. 43 | 44 | As promised it was rather short section. 45 | Let's move forward to coercions. 46 | 47 | Next: [Request Coercion](6-request-coercion.md) 48 | -------------------------------------------------------------------------------- /materials/6-simple-web-application/6-request-coercion.md: -------------------------------------------------------------------------------- 1 | # 6.6. Request Coercion 2 | 3 | Previously we successfully utilized `:body-params` to read values from the request body. 4 | It worked nicely as long as the values were provided. 5 | Yet in the case where the values were missing the results were not as pleasing. 6 | 7 | Luckily a bunch of nice Clojure people have prepared solutions for us, 8 | so we don't have to suffer this sorts of issues when building our web service. 9 | 10 | As the title might have given away, 11 | solution to our troubles is known as [Coercion](https://github.com/metosin/reitit/blob/master/doc/coercion/coercion.md). 12 | 13 | Metosin describes it as follows: 14 | 15 | _Coercion is a process of transforming parameters (and responses) from one format into another._ 16 | _Reitit separates routing and coercion into two separate steps._ 17 | 18 | With Reitit there is few different way's to do coercion, 19 | but we will be using what is known as [spec](https://clojure.org/guides/spec). 20 | Spec is a Clojure library that allows us to describe data structures as specs, 21 | and then validate data against those specs. 22 | These specs can be used a sort of predicate functions to describe if the data is valid or not. 23 | 24 | That might have been a rather poor explanation and I should rewrite it at some point, 25 | if I come up with better explanation. 26 | 27 | Luckily coercion is not actually a very difficult concept, 28 | so it should be clear how it works after we are finished with our next endpoint. 29 | 30 | Let's add the following code under our `/subtraction` endpoint: 31 | 32 | ```clojure 33 | ["/division" {:post {:description "Returns x divided by y." 34 | :coercion rcs/coercion 35 | :parameters {:body {:x number? 36 | :y number?}} 37 | :handler (fn [req] 38 | (let [x (-> req :parameters :body :x) 39 | y (-> req :parameters :body :y)] 40 | (response/ok {:quotient (/ x y)})))}}] 41 | ``` 42 | 43 | There is a lot of same going on here, 44 | but few things are a bit different. 45 | The key differences are that we have few new keywords and :body-params seem to be replaced with with something else. 46 | 47 | So let's walk through this. 48 | We have added `:coercion` keyword with `rcs/coercion` associated with it. 49 | If you scroll all the way to the top your file, 50 | you notice that rcs is actually `reitit.coercion.spec`. 51 | This specifies to Reitit that we are using spec as our coercion method. 52 | Another new keyword is `:parameters`. Here we specify what sort of parameters this endpoint is expecting. 53 | This is the spec part. 54 | Here we specify that the body of the request should contain a map with keys `:x` & `:y`. 55 | And both of those values should be numbers. 56 | `number?` is standard Clojure predicate function, 57 | which validates if something is a number or not. 58 | 59 | You can test it in your REPL. 60 | It works like this: 61 | 62 | ```clojure 63 | (number? 4) 64 | ;=> true 65 | (number? 4.3) 66 | ;=> true 67 | (number? 4/3) 68 | ;=> true 69 | (number? "John") 70 | ;=> false 71 | ``` 72 | 73 | So why are we not using `:body-params` anymore? 74 | Well the thing with coercion in Reitit is that all the coerced parameters will be available at under the key `:parameters`in the request. 75 | So when we are using coercion that is where we should be accessing our params. 76 | The same params are still also available under the `:body-params`. 77 | That information has not disappeared anywhere, 78 | it is just now also available at `:parameters` 79 | You can also notice that our body parameters are placed under the key `:body` in the parameters. 80 | If we would be coercing also path parameters and/or query parameters those would have their dedicated keys as well. 81 | 82 | So let's try calling our new endpoint. 83 | 84 | ```sh 85 | curl --request POST \ 86 | --url http://localhost:3000/math/division \ 87 | --header 'content-type: application/json' \ 88 | --data '{ 89 | "x": 10, 90 | "y": 3 91 | }' 92 | # {"quotient":3.333333333333333} 93 | ``` 94 | 95 | Proper call works as intended. 96 | But this is not really the part that we were interested about. 97 | 98 | Let's try to make a call that is a bit less as what the endpoint is intended for. 99 | What if we would use y as a string instead of an integer? 100 | 101 | ```sh 102 | curl --request POST \ 103 | --url http://localhost:3000/math/division \ 104 | --header 'content-type: application/json' \ 105 | --data '{ 106 | "x": 10, 107 | "y": "3" 108 | }' 109 | ``` 110 | 111 | Out put was a bit longer so I formatted it for us: 112 | 113 | ```json 114 | { 115 | "spec": "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:spec$8774/x :spec$8774/y]), :type :map, :leaf? false})", 116 | "problems": [ 117 | { 118 | "path": [ 119 | "y" 120 | ], 121 | "pred": "clojure.core/number?", 122 | "val": "3", 123 | "via": [ 124 | "spec$8774/y" 125 | ], 126 | "in": [ 127 | "y" 128 | ] 129 | } 130 | ], 131 | "type": "reitit.coercion/request-coercion", 132 | "coercion": "spec", 133 | "value": { 134 | "y": "3", 135 | "x": 10 136 | }, 137 | "in": [ 138 | "request", 139 | "body-params" 140 | ] 141 | } 142 | ``` 143 | 144 | So what do we have here? 145 | Seems like our application returned information that we had a problem with our input. 146 | In the path `"y"` the value was supposed to be a number, 147 | but it was not. 148 | 149 | This is quite cool. 150 | I am sure our user will appreciate the feed back, 151 | if they get their calls incorrect. 152 | 153 | Try calling the endpoint with missing params. 154 | See what happens. 155 | 156 | ## Don't divide by zero 157 | 158 | Our spec works quite ok, 159 | but it could be a bit better. 160 | We all know that arithmetically considered, 161 | it is not considered cool to divide by zero. 162 | And yet application accepts zero as y. 163 | 164 | I am sure we can fix that. 165 | And I doubt it will be not that hard. 166 | All we have to do is to create a tiny bit better spec. 167 | 168 | Any predicate is valid for spec. 169 | So we are going to use [`and`](https://clojuredocs.org/clojure.core/and) as our predicate. 170 | It is similar to `&&` used in many other languages. 171 | It takes any number of expressions as arguments, 172 | and then returns either the first falsy one or the last last truthy one. 173 | 174 | Like this: 175 | 176 | ```clojure 177 | (and true "yes" 10) 178 | ;=> 10 179 | (and true nil 10) 180 | ;=> nil 181 | ``` 182 | 183 | We will wrap `number?` and our `not-zero?` function into `and`, 184 | and use this as a predicate. 185 | And then wrap that and call into a function. 186 | The only problem is that Clojure has not `not-zero?` function. 187 | Luckily it does have [`complement`](https://clojuredocs.org/clojure.core/complement). 188 | `complement` turns any predicate into exact opposite version of itself. 189 | This is obviously super handy. 190 | 191 | So our solution will look like something like this: 192 | 193 | ```clojure 194 | ["/division" {:post {:description "Returns x divided by y." 195 | :coercion rcs/coercion 196 | :parameters {:body {:x number? 197 | :y #(and (number? %) ((complement zero?) %))}} 198 | :handler (fn [req] 199 | (let [x (-> req :parameters :body :x) 200 | y (-> req :parameters :body :y)] 201 | (response/ok {:quotient (/ x y)})))}}] 202 | ``` 203 | 204 | Let's replace our old solution with this and try calling our endpoint again with zero. 205 | 206 | ```sh 207 | curl --request POST \ 208 | --url http://localhost:3000/math/division \ 209 | --header 'content-type: application/json' \ 210 | --data '{ 211 | "x": 10, 212 | "y": 0 213 | }' 214 | ``` 215 | 216 | Again formatted response should be something like this: 217 | 218 | ```json 219 | { 220 | "spec": "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:spec$8846/x :spec$8846/y]), :type :map, :leaf? false})", 221 | "problems": [ 222 | { 223 | "path": [ 224 | "y" 225 | ], 226 | "pred": ":clojure.spec.alpha/unknown", 227 | "val": 0, 228 | "via": [ 229 | "spec$8846/y" 230 | ], 231 | "in": [ 232 | "y" 233 | ] 234 | } 235 | ], 236 | "type": "reitit.coercion/request-coercion", 237 | "coercion": "spec", 238 | "value": { 239 | "y": 0, 240 | "x": 10 241 | }, 242 | "in": [ 243 | "request", 244 | "body-params" 245 | ] 246 | } 247 | ``` 248 | 249 | This is already almost perfect. 250 | Except now the predicate says ":clojure.spec.alpha/unknown". 251 | That is kind of a bummer, 252 | since now the users won't really know what was wrong with their request. 253 | 254 | We can fix this simply by creating a predicate function with suitable name, 255 | and using that in our spec. 256 | That should be rather straight forward. 257 | Our predicate should look something like this: 258 | 259 | ```clojure 260 | (defn not-zero-number? 261 | [x] 262 | (and (number? x) ((complement zero?) x))) 263 | ``` 264 | 265 | And we make tiny change into our spec. 266 | 267 | ```clojure 268 | :parameters {:body {:x number? 269 | :y not-zero-number?}} 270 | ``` 271 | 272 | If you now call for the endpoint with zero value, 273 | you can see that the response nicely states that the value failed to be not-zero-number? 274 | 275 | ```json 276 | // I removed some of the less interesting lines from the response 277 | { 278 | "path": [ 279 | "y" 280 | ], 281 | "pred": "calculator-api.server.server/not-zero-number?", 282 | "val": 0, 283 | "via": [ 284 | "spec$9156/y" 285 | ], 286 | "in": [ 287 | "y" 288 | ] 289 | } 290 | ``` 291 | 292 | ## Middleware for request coercion 293 | 294 | You might have been wondering where do the coercion exception responses come from? 295 | They contain detailed information regarding what went wrong with our request, 296 | yet they seem to be appearing out of thin air. 297 | 298 | Well they are not actually appearing out of nowhere. 299 | The responses are actually provided by `coercion/coerce-exceptions-middleware` that we added to our app earlier. 300 | From the top of the file you can see that that the `coercion` is actually `reitit.ring.coercion`. 301 | Try removing this middleware and see what kind of responses you get when making bad requests. 302 | 303 | Furthermore, the coercion itself does require some middleware. 304 | If you remove the middleware `coercion/coerce-request-middleware` from your app, 305 | you can see that the bad request go straight through, 306 | and you are again faced with the ugly java.lang.NullPointerException. 307 | 308 | So when using coercion in your web application, 309 | do not forget to add the necessary middleware. 310 | These are actually surprisingly easy to forget, 311 | since you only need to add them once and then you can forget about them. 312 | This might or might not have happened to me more than once. 313 | 314 | With this we conclude our section on request coercion. 315 | 316 | Next up: [Response Coercion](7-response-coercion.md) 317 | -------------------------------------------------------------------------------- /materials/6-simple-web-application/7-response-coercion.md: -------------------------------------------------------------------------------- 1 | # 6.7. Response Coercion 2 | 3 | So far we have now only coerced the request data, 4 | but that is not where this ends. 5 | Our setup allow us to coerce the response data as well. 6 | 7 | So what does that actually mean coercion of response data? 8 | Well basically we can tell our application what kinds of responses we intend our application to return. 9 | If the response does not match any of the predefined responses, 10 | we get an exception. 11 | This is a nice way to make sure that our responses are indeed exactly what we expected of them to be. 12 | Furthermore, it acts as a nice documentation. 13 | 14 | So let's make a multiplication endpoint. 15 | 16 | ```clojure 17 | ["/multiplication" {:post {:description "Returns x multiplied by y" 18 | :coercion rcs/coercion 19 | :parameters {:body {:x number? 20 | :y number?}} 21 | :responses {200 {:body {:product number?}}} 22 | :handler (fn [req] 23 | (let [x (-> req :parameters :body :x) 24 | y (-> req :parameters :body :y)] 25 | (response/ok {:total (* x y)})))}}] 26 | ``` 27 | 28 | So again we have a rather familiar looking setup, 29 | but with single new key present. 30 | This time we have introduced `:responses`. 31 | Behind that seems to be a map with numbers as keys. 32 | These numbers represent HTTP status codes and those numbers are followed by maps that look suspiciously much like specs. 33 | This spec specifies the shape of our response on HTTP code 200 (that is same as OK). 34 | 35 | So let's give this endpoint a go: 36 | 37 | ```sh 38 | curl --request POST \ 39 | --url http://localhost:3000/math/multiplication \ 40 | --header 'content-type: application/json' \ 41 | --data '{ 42 | "x": 10, 43 | "y": 15.5 44 | }' 45 | ``` 46 | 47 | Again I added a bit of formation to the response: 48 | 49 | ```json 50 | { 51 | "spec": "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:spec$8384/product]), :type :map, :leaf? false})", 52 | "problems": [ 53 | { 54 | "path": [], 55 | "pred": "(clojure.core/fn [%] (clojure.core/contains? % :product))", 56 | "val": { 57 | "total": 155.0 58 | }, 59 | "via": [], 60 | "in": [] 61 | } 62 | ], 63 | "type": "reitit.coercion/response-coercion", 64 | "coercion": "spec", 65 | "value": { 66 | "total": 155.0 67 | }, 68 | "in": [ 69 | "response", 70 | "body" 71 | ] 72 | } 73 | ``` 74 | 75 | After investigating the response a bit, 76 | we can see that there seems to be an issue with our response body. 77 | It seems like the response was supposed to contain `:product`. 78 | 79 | After quick review on our code, 80 | we seem to be having a mistake in the way how we formed our response. 81 | 82 | ```clojure 83 | (response/ok {:total (* x y)}) 84 | ``` 85 | 86 | It looks like we accidentally named our response value in the body `:total`. 87 | Lets fix this and try again. 88 | 89 | ```clojure 90 | (response/ok {:product (* x y)}) 91 | ``` 92 | 93 | Let's try it again. 94 | This time the response seems to be fine: 95 | 96 | ```json 97 | { 98 | "product": 155.0 99 | } 100 | ``` 101 | 102 | So here we saw the response coercion in action. 103 | It helps us to catch silly mistakes like that. 104 | We could also specify different shapes for different response codes. 105 | For example different exceptions might have their own specs. 106 | Using both request and response coercions can be really handy. 107 | Utilizing them both helps us to overcome some of the short coming of Clojure's dynamic typing. 108 | Yet in the end is up to you as the developer to decide if you will utilize these tools or not. 109 | 110 | We have now gone through much of the basics required for building web application with Clojure using Ring & Reitit. 111 | Some of the stuff is probably even beyond the basics. 112 | This is because I really wanted to show you the power of Clojure in web application development. 113 | 114 | Our application should now look something like this: 115 | 116 | ```clojure 117 | (ns calculator-api.server.server 118 | (:require [ring.adapter.jetty :as jetty] 119 | [ring.middleware.params :as params] 120 | [ring.util.http-response :as response] 121 | [reitit.ring.middleware.muuntaja :as muuntaja] 122 | [muuntaja.core :as m] 123 | [reitit.ring.coercion :as coercion] 124 | [reitit.coercion.spec :as rcs] 125 | [reitit.ring :as ring])) 126 | 127 | (defn not-zero-number? 128 | [x] 129 | (and (number? x) ((complement zero?) x))) 130 | 131 | (def math-routes 132 | ["/math" 133 | ["/addition" {:get (fn [request] 134 | (let [params (:query-params request) 135 | x (Long/parseLong (get params "x")) 136 | y (Long/parseLong (get params "y"))] 137 | (response/ok {:total (+ x y)})))}] 138 | 139 | ["/subtraction" {:post {:description "Returns x subtracted by y" 140 | :handler (fn [request] 141 | (let [x (-> request :body-params :x) 142 | y (-> request :body-params :y)] 143 | (response/ok {:difference (- x y)})))}}] 144 | ["/subtract/:y/from/:x" {:get {:description "Returns x subracted by y" 145 | :handler (fn [request] 146 | (let [x (-> request :path-params :x Long/parseLong) 147 | y (-> request :path-params :y Long/parseLong)] 148 | (response/ok {:difference (- x y)})))}}] 149 | ["/division" {:post {:description "Returns x divided by y." 150 | :coercion rcs/coercion 151 | :parameters {:body {:x number? 152 | :y not-zero-number?}} 153 | :handler (fn [req] 154 | (let [x (-> req :parameters :body :x) 155 | y (-> req :parameters :body :y)] 156 | (response/ok {:quotient (/ x y)})))}}] 157 | ["/multiplication" {:post {:description "Returns x multiplied by y" 158 | :coercion rcs/coercion 159 | :parameters {:body {:x number? 160 | :y number?}} 161 | :responses {200 {:body {:product number?}}} 162 | :handler (fn [req] 163 | (let [x (-> req :parameters :body :x) 164 | y (-> req :parameters :body :y)] 165 | (response/ok {:product (* x y)})))}}]]) 166 | 167 | (def hello-routes 168 | ["/hello" {:get {:handler (fn [_] 169 | (response/ok {:message "Hello Reitit!"}))}}]) 170 | 171 | (def app 172 | (ring/ring-handler 173 | (ring/router 174 | [hello-routes 175 | math-routes] 176 | {:data {:muuntaja m/instance 177 | :middleware [params/wrap-params 178 | muuntaja/format-middleware 179 | coercion/coerce-exceptions-middleware 180 | coercion/coerce-request-middleware 181 | coercion/coerce-response-middleware]}}) 182 | (ring/create-default-handler))) 183 | 184 | (defonce running-server (atom nil)) 185 | 186 | (defn start 187 | [] 188 | (when (nil? @running-server) 189 | (reset! running-server (jetty/run-jetty #'app {:port 3000 190 | :join? false}))) 191 | (println "Server running in port 3000")) 192 | 193 | (defn stop 194 | [] 195 | (when-not (nil? @running-server) 196 | (.stop @running-server) 197 | (reset! running-server nil)) 198 | (println "Server stopped")) 199 | ``` 200 | 201 | This software was great for teaching these concepts to you, 202 | but it also is a bit of a mess. 203 | But this time we might just let that slide. 204 | Not every application has to be pretty or useful, 205 | especially when we are learning. 206 | 207 | Before we leave this mess behind, 208 | let's learn how to test this kind of application. 209 | 210 | Next: [Testing Web Applications](8-testing-web-applications.md) -------------------------------------------------------------------------------- /materials/6-simple-web-application/9-mocking.md: -------------------------------------------------------------------------------- 1 | # 6.9. Mocking 2 | 3 | Our current web application does not really have any features that require mocking, 4 | but lets make up a scenario: 5 | 6 | Our boss asks us to create a experimental MVP feature that that he is looking forward to pitch to the customer. 7 | This thing he is visioning is called BetterMath. 8 | It is an AI powered super feature. 9 | In stead of calculating numbers together like a boring nerd person, 10 | it uses Super Hyper Internet Technologies to predict the number you need. 11 | Boss says he has perfect slogan: 12 | 13 | _"Forget the formulas, focus on results!"_ 14 | 15 | Lucky to us, our boss did not leave us with any kind of specifications how all of this works, 16 | so it is up to us to implement this. 17 | 18 | So let's get started: 19 | 20 | We come up with perfect algorithm for predicting the number user needs, 21 | which is leaving it all to lady luck. 22 | 23 | ```clojure 24 | (defn predict 25 | [] 26 | (rand-int 1000000)) 27 | ``` 28 | 29 | But then we realize that one is going to believe that this is based on some Super Hyper technology if the results are calculated instantly. 30 | This is supposed to be advanced science, 31 | so to fake it all we need to slow the calculation down a bit. 32 | So we throw in a `Thread/sleep` to wait a bit. 33 | `Thread/sleep` takes an int and waits that many milliseconds before continuing the execution. 34 | 35 | ```clojure 36 | (defn predict 37 | [] 38 | (Thread/sleep 3000) 39 | (rand-int 1000000)) 40 | ``` 41 | 42 | Next we will define new experimental-routes where our predict route is located. 43 | 44 | ```clojure 45 | (def experimental-routes 46 | ["/experimental-math" 47 | ["/predict" {:get {:description "Returns the one number you really need. WARNING: Calculation takes time!" 48 | :coercion rcs/coercion 49 | :responses {200 {:body {:prediction number?}}} 50 | :handler (fn [_] 51 | (let [prediction (predict)] 52 | (response/ok {:prediction prediction})))}}]]) 53 | ``` 54 | 55 | Don't forget to include the experimental-routes to the app! 56 | 57 | ```clojure 58 | (def app 59 | (ring/ring-handler 60 | (ring/router 61 | [hello-routes 62 | math-routes 63 | experimental-routes] 64 | {:data {:muuntaja m/instance 65 | :middleware [params/wrap-params 66 | muuntaja/format-middleware 67 | coercion/coerce-exceptions-middleware 68 | coercion/coerce-request-middleware 69 | coercion/coerce-response-middleware]}}) 70 | (ring/create-default-handler))) 71 | ``` 72 | 73 | Next we will write tests for this route, 74 | but how? 75 | This leaves us in a pickle for two reasons. 76 | First, we really don't want to wait three seconds every time this route is tested. 77 | Everyone knows time is money! 78 | Second, we have no clue what number is going to be returned, 79 | so what are supposed to evaluate against? 80 | 81 | Luckily mocking exists just for this. 82 | 83 | What we are going to do is to mock the predict function. 84 | This way we can still test that the /predict route response look the right way, 85 | but we don't have to wait and we can always get the same number from predict function. 86 | 87 | So how is mocking done? 88 | 89 | ## Meet [with-redefs](https://clojuredocs.org/clojure.core/with-redefs) 90 | 91 | Mocking in Clojure is much simpler than in majority of the languages. 92 | All we have to do is to use `with-redefs` function. 93 | `with-redefs` 94 | 95 | Let's add the following code to our server_test.clj 96 | 97 | ```clojure 98 | (deftest experimental 99 | (testing "GET /experimental-math/predict" 100 | (testing "prediction is returned" 101 | (with-redefs 102 | [calculator-api.server.server/predict (fn [] 42)] 103 | (let [{:keys [status body]} (test-app (-> (mock/request :get "/experimental-math/predict")))] 104 | (is (= 200 status)) 105 | (is (= {:prediction 42} body))))))) 106 | ``` 107 | 108 | As you can see the `let` form has been wrapped in `with-redefs` form. 109 | `with-redefs` has similar structure as `let` so it takes vector of bindings and a body. 110 | Bindings are pairs where left side is a function to be mocked (referred with its full name including the namespace), 111 | and the right side is the new function we want to temporarily redefine it with. 112 | If any code inside of `with-redefs` form calls our mocked function, 113 | our mock functions is called instead. 114 | Just as with let, 115 | we can provide as many bindings as we wish within one `with-redefs` form. 116 | 117 | Here we have decided to redefine `calculator-api.server.server/predict` to be function that always returns 42. 118 | This new mock does not include any kind of waiting or sleeping, 119 | so our tests will run fast and smooth. 120 | 121 | Rest of the test is just as our previous tests, 122 | so nothing new there. 123 | 124 | Try running the new test with and without the mock, 125 | so you can see the difference. 126 | 127 | ## Mocking made easy with [constantly](https://clojuredocs.org/clojure.core/constantly) 128 | 129 | We can make a tiny improvement to our previous implementation by utilizing Clojure function `constantly`. 130 | `constantly` is a funny function. 131 | It returns a new function that always regardless of the parameters provided will return the given value. 132 | This new function can be called with any number of arguments, 133 | but the result is always the same. 134 | 135 | Give it a try: 136 | 137 | Run following things in your REPL: 138 | 139 | ```clojure 140 | (def constant 141 | (constantly "Cute kittens")) 142 | 143 | (constant 1 {:map 6} "Mr. President") 144 | ;=> "Cute kittens" 145 | ``` 146 | 147 | So let's replace our `(fn [] 42)` with `(constantly 42)`: 148 | 149 | ```clojure 150 | (deftest experimental 151 | (testing "GET /experimental-math/predict" 152 | (testing "prediction is returned" 153 | (with-redefs 154 | [calculator-api.server.server/predict (constantly 42)] 155 | (let [{:keys [status body]} (test-app (-> (mock/request :get "/experimental-math/predict")))] 156 | (is (= 200 status)) 157 | (is (= {:prediction 42} body))))))) 158 | ``` 159 | 160 | You have now learned all there is to mocking with Clojure. 161 | It is really all that simple. 162 | You can use `with-redefs` to mock pretty much anything in Clojure. 163 | 164 | There is more advanced features you can use with `with-redefs`, 165 | but we won't cover them here. 166 | 167 | In case you are curious and want to do even more advanced mocking, 168 | I advice you to familiarize yourself with [tortue/spy](https://cljdoc.org/d/tortue/spy). 169 | `tortue/spy` offers some great functionality that you will definitely find useful in your projects. 170 | 171 | This finalizes our journey to web applications with Clojure. 172 | You have now familiarized yourself with all the basics required for starting to work with real life Clojure applications. 173 | 174 | Next: [Final Words](../final-words.md) 175 | -------------------------------------------------------------------------------- /materials/6-simple-web-application/README.md: -------------------------------------------------------------------------------- 1 | # Simple web application 2 | 3 | In this chapter we will be focusing on the thing that Clojure really shines on. 4 | Web applications. 5 | And it is easy to say that web development with Clojure is just amazing. 6 | This is mainly due two factors: 7 | Functional programming is a really good fit with web development, 8 | and Clojure ecosystem rather well geared towards the web development. 9 | 10 | There is many ways to build a Clojure web application. 11 | Unlike in many other languages, 12 | in Clojure it is popular to build web application from scratch without use of an existing framework. 13 | Most other languages have their goto web framework (like Spring, Ruby On Rails, Django), 14 | but in Clojure relying on such a framework is not as common, 15 | and we tend to see more modular web application patched together from various components. 16 | 17 | ## Our project - Calculator API 18 | 19 | We are going to create extremely simple calculator API. 20 | Since we want to focus on creating web application and the basic we components, 21 | we won't be creating anything super cool and fancy, 22 | which would distract us from the main components. 23 | 24 | When we are familiar with the basic components, 25 | we can build something more useful and cool. 26 | 27 | Our solution will have following requirements: 28 | 29 | 1. API has /hello endoint that returns a message: Hello World! 30 | 31 | 2. API has endpoints for following arithmetic operations. 32 | 33 | - addition 34 | - subtraction 35 | - multiplication 36 | - division 37 | 38 | 3. Each endpoint can take two variables. Namely x and y 39 | 40 | 4. All endpoints are are tested 41 | 42 | As you can see we have a rather short list of requirements this time, 43 | but we will add a small twist to this while we build the application. 44 | We will try a bit different approach with each endpoint, 45 | so you will be familiariesed with different techniques. 46 | 47 | ## [1. Setting up](1-setting-up.md) 48 | 49 | We will cover boring parts of setting up the web application such as boiler plates and dependencies. 50 | We will also scratch the surface of Clojure Web Development components such as Ring, Reitit and Muuntaja. 51 | This helps you to setup your own web applications and gives a nice template to use for your Clojure Web Projects 52 | 53 | ## [2. Hello Reitit](2-hello-reitit.md) 54 | 55 | We will make our first route and learn how craft responses in Ring. 56 | In the mean while we will learn a bit more about muuntaja and handy tools for our responses. 57 | 58 | ## [3. Query Parameters](3-query-parameters.md) 59 | 60 | We will start crafting the calculator API and learn about Query Parameters and how to access them. 61 | We will also briefly visit how we can create some structure to our routes with Nested Route Data. 62 | 63 | ## [4. Body Parameters](4-body-parameters.md) 64 | 65 | While continuing to work with calculator API we will quickly learn about Body parameters and how to access them. 66 | 67 | ## [5. Path Parameters](5-path-parameters.md) 68 | 69 | We will quickly go through how to use path parameters with Reitit. 70 | 71 | ## [6. Request Coercion](6-request-coercion.md) 72 | 73 | After covering the necessary basics we are finally ready to dive to the good stuff. 74 | We will get to know request coercion and have our first interactions with Spec. 75 | We will also go through some useful middleware that is necessary for coercion. 76 | 77 | ## [7. Response Coercion](7-response-coercion.md) 78 | 79 | We will continue our adventures with Coercion. 80 | This time we meet Response Coercion and learn what that is all about or why would you want to use it? 81 | 82 | ## [8. Testing Web Applications](8-testing-web-applications.md) 83 | 84 | We will get familiar with easy patters for testing web applications 85 | 86 | ## [9. Mocking](9-mocking.md) 87 | 88 | We will learn how to mock functions in Clojure 89 | -------------------------------------------------------------------------------- /materials/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to the It starts with Clojure -guide 2 | 3 | Currently available chapters are: 4 | 5 | ## [1. First Things First](./1-first-things-first) 6 | 7 | A section that will guide you through all annoying stuff like installations and such. 8 | Some introductions to the guide itself are also provided. 9 | All of this should be very brief. 10 | 11 | ## [2. Little bit of REPL](./2-little-bit-of-repl) 12 | 13 | Introduction to Clojure's arguably most powerful feature in Clojure. 14 | Learn it first and learn it well and it will guide you through all sorts of adventures. 15 | 16 | ## [3. Your first Project](./3-first-project) 17 | 18 | We will learn how to create functions and build simple Clojure applications. 19 | In the progress we will learn how perform basic I/O operations with Clojure. 20 | 21 | ## [4. State in Clojure](./4-state-in-clojure) 22 | 23 | We will build a super simple application for writing down a shopping list in command line. 24 | While doing so we'll learn about state, loops, and few other things. 25 | 26 | ## [5. Requests for Data](./5-requests-for-data) 27 | 28 | We will build an application for fetching and parsing data from reddit API. 29 | In the progress we will learn about data manipulation techniques and HTTP request in Clojure. 30 | Furthermore, we we will meet Anonymous Functions and many other handy tools to make our code cleaner. 31 | 32 | ## [6. Simple Web Applicaiton](./6-simple-web-application) 33 | 34 | We will build a simple web application by utilizing Ring and Reitit. 35 | In the progress we will learn how to work with key components of Clojure web development. 36 | Further more we will take a swing at testing Clojure web applications. 37 | 38 | ### Additional reading suggestions 39 | 40 | - [Living Clojure](https://www.oreilly.com/library/view/living-clojure/9781491909270/) 41 | - Easy to understand and approach hands on Clojure book that starts from very beginning 42 | - [Clojure for the Brave and True](https://www.braveclojure.com/) 43 | - A Classic Clojure book. 44 | - [Programming Clojure, Third Edition](https://pragprog.com/book/shcloj3/programming-clojure-third-edition) 45 | - More in depth book about Clojure. 46 | - [Web Development With Clojure](https://pragprog.com/book/dswdcloj3/web-development-with-clojure-third-edition) 47 | - Domain specific book for Web Development 48 | -------------------------------------------------------------------------------- /materials/final-words.md: -------------------------------------------------------------------------------- 1 | # Final Words 2 | 3 | First of all I wish to congratulate you dear reader. 4 | You have made it all the way to the end of this guide/course/what-ever-this-is! 5 | 6 | Secondly I would like to thank you for letting me be part of beginning of your awesome journey to Clojure. 7 | I hope you will soon get to write Clojure to production! 8 | That is when your learning really starts and you will start to see the power of this language. 9 | If that does not happen, 10 | it is still possible to take a lot of good ideas, patterns & practices from Clojure let them influence how you generally write code in any language. 11 | 12 | I personally believe that every programming language we learn makes us better programmer with all the languages we use. 13 | Each language has their unique set of ideas and their way of approaching things. 14 | Many of these ideas are not language dependent and can easily be utilized in other languages as well. 15 | 16 | When I started writing this thing, 17 | I was rather novice with Clojure 18 | I had not written any production Clojure that point, 19 | but I had read bunch of books about the language. 20 | That being said it is not anywhere the same as writing production code with it. 21 | Soon after starting to do this thing I got an opportunity to write a lot Clojure for living. 22 | 23 | ## Thoughts on teaching 24 | 25 | Dear reader: 26 | 27 | In case you ever want to learn a new language or a technology, 28 | I highly recommend you to teach it to others. 29 | I have now taught Clojure to people online and offline, 30 | and without a doubt I can vouch that it is a wonderful way to enforce your own understanding of any technology. 31 | 32 | Teaching forces your to reorganize your thoughts and look deeper into how things actually work. Without doing this teaching is not possible. But luckily this kind of happens by itself when we teach others. 33 | 34 | ## Future progress of this project 35 | 36 | I will keep on maintaining this project for the forseeable future, 37 | but I do not intend to add new content. 38 | 39 | That being said, 40 | I will keep on making fixes and minor additions nevertheless! 41 | So in case you notice any issues with the content, 42 | I would kindly ask you to create an issue to github, 43 | that way I will know something requires fixing. 44 | Pull requests are of course appreciated as well! 45 | 46 | ## Thanks for all the help! 47 | 48 | I would like to give a special thanks to all the people who have raised issues with this course and written pull requests! 49 | 50 | Your help has been much appreciated! 51 | -------------------------------------------------------------------------------- /projects/calculator-api/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | -------------------------------------------------------------------------------- /projects/calculator-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2020-05-17 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2020-05-17 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/your-name/calculator-api/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/calculator-api/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /projects/calculator-api/README.md: -------------------------------------------------------------------------------- 1 | # calculator-api 2 | 3 | FIXME: description 4 | 5 | ## Installation 6 | 7 | Download from http://example.com/FIXME. 8 | 9 | ## Usage 10 | 11 | FIXME: explanation 12 | 13 | $ java -jar calculator-api-0.1.0-standalone.jar [args] 14 | 15 | ## Options 16 | 17 | FIXME: listing of options this app accepts. 18 | 19 | ## Examples 20 | 21 | ... 22 | 23 | ### Bugs 24 | 25 | ... 26 | 27 | ### Any Other Sections 28 | ### That You Think 29 | ### Might be Useful 30 | 31 | ## License 32 | 33 | Copyright © 2020 FIXME 34 | 35 | This program and the accompanying materials are made available under the 36 | terms of the Eclipse Public License 2.0 which is available at 37 | http://www.eclipse.org/legal/epl-2.0. 38 | 39 | This Source Code may also be made available under the following Secondary 40 | Licenses when the conditions for such availability set forth in the Eclipse 41 | Public License, v. 2.0 are satisfied: GNU General Public License as published by 42 | the Free Software Foundation, either version 2 of the License, or (at your 43 | option) any later version, with the GNU Classpath Exception which is available 44 | at https://www.gnu.org/software/classpath/license.html. 45 | -------------------------------------------------------------------------------- /projects/calculator-api/doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to calculator-api 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /projects/calculator-api/project.clj: -------------------------------------------------------------------------------- 1 | (defproject calculator-api "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 5 | :url "https://www.eclipse.org/legal/epl-2.0/"} 6 | :dependencies [[org.clojure/clojure "1.10.1"] 7 | [ring "1.8.1"] 8 | [ring/ring-mock "0.4.0"] 9 | [metosin/reitit "0.4.2"] 10 | [metosin/ring-http-response "0.9.1"]] 11 | 12 | :main ^:skip-aot calculator-api.core 13 | :target-path "target/%s" 14 | :profiles {:uberjar {:aot :all}}) 15 | -------------------------------------------------------------------------------- /projects/calculator-api/src/calculator_api/core.clj: -------------------------------------------------------------------------------- 1 | (ns calculator-api.core 2 | (:gen-class)) 3 | 4 | (require '[calculator-api.server.server :as calculator-server]) 5 | 6 | (defn -main 7 | "I don't do a whole lot ... yet." 8 | [& args] 9 | (calculator-server/start)) 10 | -------------------------------------------------------------------------------- /projects/calculator-api/src/calculator_api/server/server.clj: -------------------------------------------------------------------------------- 1 | (ns calculator-api.server.server 2 | (:require [ring.adapter.jetty :as jetty] 3 | [ring.middleware.params :as params] 4 | [ring.util.http-response :as response] 5 | [reitit.ring.middleware.muuntaja :as muuntaja] 6 | [muuntaja.core :as m] 7 | [reitit.ring.coercion :as coercion] 8 | [reitit.coercion.spec :as rcs] 9 | [reitit.ring :as ring])) 10 | 11 | (defn not-zero-number? 12 | [x] 13 | (and (number? x) ((complement zero?) x))) 14 | 15 | (defn predict 16 | [] 17 | (Thread/sleep 3000) 18 | (rand-int 1000000)) 19 | 20 | (def experimental-routes 21 | ["/experimental-math" 22 | ["/predict" {:get {:description "Returns the one number you really need. WARNING: Calculation takes time!" 23 | :coercion rcs/coercion 24 | :responses {200 {:body {:prediction number?}}} 25 | :handler (fn [_] 26 | (let [prediction (predict)] 27 | (response/ok {:prediction prediction})))}}]]) 28 | 29 | (def math-routes 30 | ["/math" 31 | ["/addition" {:get (fn [request] 32 | (let [params (:query-params request) 33 | x (Long/parseLong (get params "x")) 34 | y (Long/parseLong (get params "y"))] 35 | (response/ok {:total (+ x y)})))}] 36 | 37 | ["/subtraction" {:post {:description "Returns x subtracted by y" 38 | :handler (fn [request] 39 | (let [x (-> request :body-params :x) 40 | y (-> request :body-params :y)] 41 | (response/ok {:difference (- x y)})))}}] 42 | ["/subtract/:y/from/:x" {:get {:description "Returns x subracted by y" 43 | :handler (fn [request] 44 | (let [x (-> request :path-params :x Long/parseLong) 45 | y (-> request :path-params :y Long/parseLong)] 46 | (response/ok {:difference (- x y)})))}}] 47 | ["/division" {:post {:description "Returns x divided by y." 48 | :coercion rcs/coercion 49 | :parameters {:body {:x number? 50 | :y not-zero-number?}} 51 | :handler (fn [req] 52 | (let [x (-> req :parameters :body :x) 53 | y (-> req :parameters :body :y)] 54 | (response/ok {:quotient (/ x y)})))}}] 55 | ["/multiplication" {:post {:description "Returns x multiplied by y" 56 | :coercion rcs/coercion 57 | :parameters {:body {:x number? 58 | :y number?}} 59 | :responses {200 {:body {:product number?}}} 60 | :handler (fn [req] 61 | (let [x (-> req :parameters :body :x) 62 | y (-> req :parameters :body :y)] 63 | (response/ok {:product (* x y)})))}}]]) 64 | 65 | (def hello-routes 66 | ["/hello" {:get {:handler (fn [_] 67 | (response/ok {:message "Hello Reitit!"}))}}]) 68 | 69 | (def app 70 | (ring/ring-handler 71 | (ring/router 72 | [hello-routes 73 | math-routes 74 | experimental-routes] 75 | {:data {:muuntaja m/instance 76 | :middleware [params/wrap-params 77 | muuntaja/format-middleware 78 | coercion/coerce-exceptions-middleware 79 | coercion/coerce-request-middleware 80 | coercion/coerce-response-middleware]}}) 81 | (ring/create-default-handler))) 82 | 83 | (defonce running-server (atom nil)) 84 | 85 | (defn start 86 | [] 87 | (when (nil? @running-server) 88 | (reset! running-server (jetty/run-jetty #'app {:port 3000 89 | :join? false}))) 90 | (println "Server running in port 3000")) 91 | 92 | (defn stop 93 | [] 94 | (when-not (nil? @running-server) 95 | (.stop @running-server) 96 | (reset! running-server nil)) 97 | (println "Server stopped")) 98 | -------------------------------------------------------------------------------- /projects/calculator-api/test/calculator_api/server/server_test.clj: -------------------------------------------------------------------------------- 1 | (ns calculator-api.server.server_test 2 | (:require [clojure.test :refer :all] 3 | [muuntaja.core :as m] 4 | [reitit.ring :as ring] 5 | [ring.middleware.params :as params] 6 | [ring.mock.request :as mock] 7 | [reitit.coercion.spec :as rcs] 8 | [reitit.ring.middleware.muuntaja :as muuntaja] 9 | [reitit.ring.coercion :as coercion] 10 | [calculator-api.server.server :refer [math-routes experimental-routes]])) 11 | 12 | 13 | (def test-app 14 | (ring/ring-handler 15 | (ring/router 16 | [math-routes 17 | experimental-routes] 18 | {:data {:coercion rcs/coercion 19 | :muuntaja m/instance 20 | :middleware [params/wrap-params 21 | muuntaja/format-negotiate-middleware 22 | muuntaja/format-request-middleware 23 | coercion/coerce-exceptions-middleware 24 | coercion/coerce-request-middleware 25 | coercion/coerce-response-middleware]}}))) 26 | 27 | (deftest math 28 | (testing "GET /math/addition" 29 | (testing "x and y are added up" 30 | (let [{:keys [status body]} (test-app (-> (mock/request :get "/math/addition") 31 | (mock/query-string {:x 5 :y 5})))] 32 | (is (= 200 status)) 33 | (is (= {:total 10} body))))) 34 | 35 | (testing "GET /math/subtract/:y/from/:x" 36 | (testing "y is subtracted from x" 37 | (let [{:keys [status body]} (test-app (-> (mock/request :get "/math/subtract/10/from/25")))] 38 | (is (= 200 status)) 39 | (is (= {:difference 15} body))))) 40 | 41 | (testing "GET /math/subtraction" 42 | (testing "y is subtracted from x" 43 | (let [{:keys [status body]} (test-app (-> (mock/request :post "/math/subtraction") 44 | (mock/json-body {:x 10 :y 5})))] 45 | (is (= 200 status)) 46 | (is (= {:difference 5} body))))) 47 | 48 | 49 | (testing "GET /math/division" 50 | (testing "x is divided by y" 51 | (let [{:keys [status body]} (test-app (-> (mock/request :post "/math/division") 52 | (mock/json-body {:x 15 :y 5})))] 53 | (is (= 200 status)) 54 | (is (= {:quotient 3} body)))) 55 | 56 | (testing "divide by zero throws an error" 57 | (let [{:keys [status]} (test-app (-> (mock/request :post "/math/division") 58 | (mock/json-body {:x 15 :y 0})))] 59 | (is (= 400 status))))) 60 | 61 | (testing "get /math/multiplication" 62 | (testing "x is multiplied by y" 63 | (let [{:keys [status body]} (test-app (-> (mock/request :post "/math/multiplication") 64 | (mock/json-body {:x 25 :y 3})))] 65 | (is (= 200 status)) 66 | (is (= {:product 75} body)))))) 67 | 68 | (deftest experimental 69 | (testing "GET /experimental-math/predict" 70 | (testing "prediction is returned" 71 | (with-redefs 72 | [calculator-api.server.server/predict (constantly 42)] 73 | (let [{:keys [status body]} (test-app (-> (mock/request :get "/experimental-math/predict")))] 74 | (is (= 200 status)) 75 | (is (= {:prediction 42} body))))))) 76 | 77 | -------------------------------------------------------------------------------- /projects/no-atom-shopping-list/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | -------------------------------------------------------------------------------- /projects/no-atom-shopping-list/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2019-09-23 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2019-09-23 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/your-name/no-atom-shopping-list/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/no-atom-shopping-list/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /projects/no-atom-shopping-list/README.md: -------------------------------------------------------------------------------- 1 | # no-atom-shopping-list 2 | 3 | FIXME: description 4 | 5 | ## Installation 6 | 7 | Download from http://example.com/FIXME. 8 | 9 | ## Usage 10 | 11 | FIXME: explanation 12 | 13 | $ java -jar no-atom-shopping-list-0.1.0-standalone.jar [args] 14 | 15 | ## Options 16 | 17 | FIXME: listing of options this app accepts. 18 | 19 | ## Examples 20 | 21 | ... 22 | 23 | ### Bugs 24 | 25 | ... 26 | 27 | ### Any Other Sections 28 | ### That You Think 29 | ### Might be Useful 30 | 31 | ## License 32 | 33 | Copyright © 2019 FIXME 34 | 35 | This program and the accompanying materials are made available under the 36 | terms of the Eclipse Public License 2.0 which is available at 37 | http://www.eclipse.org/legal/epl-2.0. 38 | 39 | This Source Code may also be made available under the following Secondary 40 | Licenses when the conditions for such availability set forth in the Eclipse 41 | Public License, v. 2.0 are satisfied: GNU General Public License as published by 42 | the Free Software Foundation, either version 2 of the License, or (at your 43 | option) any later version, with the GNU Classpath Exception which is available 44 | at https://www.gnu.org/software/classpath/license.html. 45 | -------------------------------------------------------------------------------- /projects/no-atom-shopping-list/doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to no-atom-shopping-list 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /projects/no-atom-shopping-list/no-atom-shopping-list.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /projects/no-atom-shopping-list/project.clj: -------------------------------------------------------------------------------- 1 | (defproject no-atom-shopping-list "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 5 | :url "https://www.eclipse.org/legal/epl-2.0/"} 6 | :dependencies [[org.clojure/clojure "1.10.0"]] 7 | :main ^:skip-aot no-atom-shopping-list.core 8 | :target-path "target/%s" 9 | :profiles {:uberjar {:aot :all}}) 10 | -------------------------------------------------------------------------------- /projects/no-atom-shopping-list/src/no_atom_shopping_list/core.clj: -------------------------------------------------------------------------------- 1 | (ns no-atom-shopping-list.core 2 | (:gen-class)) 3 | 4 | (def output-file "./things-to-buy.txt") 5 | (def options "Enter a number:\n1. Add product\n2. Save shopping list") 6 | 7 | (defn prompt 8 | [msg] 9 | (println msg) 10 | (read-line)) 11 | 12 | 13 | (defn shopping->str 14 | [x] 15 | (str (:product x) " * " (:amount x))) 16 | 17 | (defn shopping-printable 18 | [shoppings] 19 | (clojure.string/join 20 | "\n" 21 | (map shopping->str shoppings))) 22 | 23 | (defn save-shopping-list 24 | [file shoppings] 25 | (spit file (shopping-printable shoppings))) 26 | 27 | 28 | (defn -main 29 | [& args] 30 | (loop [shoppings [] 31 | choice (prompt options)] 32 | (case choice 33 | "1" (recur (conj shoppings {:product (prompt "What to buy?") 34 | :amount (prompt "How many?")}) 35 | (prompt options)) 36 | "2" (do (save-shopping-list output-file shoppings) 37 | (println "Shopping list saved")) 38 | (do (println "Invalid choice!!! Try again") 39 | (recur shoppings 40 | (prompt options)))))) 41 | 42 | -------------------------------------------------------------------------------- /projects/no-atom-shopping-list/test/no_atom_shopping_list/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns no-atom-shopping-list.core-test 2 | (:require [clojure.test :refer :all] 3 | [no-atom-shopping-list.core :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | -------------------------------------------------------------------------------- /projects/no-atom-shopping-list/things-to-buy.txt: -------------------------------------------------------------------------------- 1 | Milk * 2 2 | Juice * 10 3 | Beer * 5 -------------------------------------------------------------------------------- /projects/reddit-analyser/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /projects/reddit-analyser/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2019-10-01 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2019-10-01 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/your-name/reddit-analyser/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/reddit-analyser/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /projects/reddit-analyser/README.md: -------------------------------------------------------------------------------- 1 | # reddit-analyser 2 | 3 | FIXME: description 4 | 5 | ## Installation 6 | 7 | Download from http://example.com/FIXME. 8 | 9 | ## Usage 10 | 11 | FIXME: explanation 12 | 13 | $ java -jar reddit-analyser-0.1.0-standalone.jar [args] 14 | 15 | ## Options 16 | 17 | FIXME: listing of options this app accepts. 18 | 19 | ## Examples 20 | 21 | ... 22 | 23 | ### Bugs 24 | 25 | ... 26 | 27 | ### Any Other Sections 28 | ### That You Think 29 | ### Might be Useful 30 | 31 | ## License 32 | 33 | Copyright © 2019 FIXME 34 | 35 | Distributed under the Eclipse Public License either version 1.0 or (at 36 | your option) any later version. 37 | -------------------------------------------------------------------------------- /projects/reddit-analyser/doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to reddit-analyser 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /projects/reddit-analyser/project.clj: -------------------------------------------------------------------------------- 1 | (defproject reddit-analyser "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [cheshire "5.9.0"] 8 | [clj-http "3.10.0"]] 9 | :main ^:skip-aot reddit-analyser.core 10 | :target-path "target/%s" 11 | :profiles {:uberjar {:aot :all}}) 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/reddit-analyser/src/reddit_analyser/core.clj: -------------------------------------------------------------------------------- 1 | (ns reddit-analyser.core 2 | (:require [clj-http.client :as client] 3 | [cheshire.core :refer [parse-string]]) 4 | (:gen-class)) 5 | 6 | (def options {:headers {"User-agent" "mega-secret-1337"} 7 | :query-params {:limit 100}}) 8 | 9 | (def url "https://www.reddit.com/r/Clojure.json") 10 | 11 | (defn get-posts 12 | [] 13 | (let [body (:body (client/get url options))] 14 | (->> (parse-string body true) 15 | (:data) 16 | (:children) 17 | (map :data)))) 18 | 19 | (defn only-good-posts 20 | [posts] 21 | (filter #(> (:score %) 15) posts)) 22 | 23 | (defn average-score 24 | [posts] 25 | (let [post-count (count posts) 26 | total-score (reduce + (map :score posts))] 27 | (float (/ total-score post-count)))) 28 | 29 | (defn author-post-count 30 | [posts] 31 | (frequencies (map :author posts))) 32 | 33 | (defn author-total-score 34 | [posts] 35 | (reduce (fn [acc m] 36 | (update acc 37 | (:author m) 38 | (fnil (partial + (:score m)) 0))) 39 | {} 40 | posts)) 41 | 42 | (defn links-posted 43 | [posts] 44 | (reduce (fn [acc post] 45 | (if (empty? (:selftext post)) 46 | (conj acc (:url post)) 47 | acc)) 48 | [] 49 | posts)) 50 | 51 | (def ui-choices 52 | "1: All posts 53 | 2: Only good posts 54 | 3: Average score 55 | 4: Author post count 56 | 5: Author total score 57 | 6: Links posted 58 | Enter choice:\n") 59 | 60 | (defn run-ui 61 | [choice posts] 62 | (if (empty? choice) 63 | (println "done.") 64 | (do (clojure.pprint/pprint (case choice 65 | "1" posts 66 | "2" (only-good-posts posts) 67 | "3" (average-score posts) 68 | "4" (author-post-count posts) 69 | "5" (author-total-score posts) 70 | "6" (links-posted posts) 71 | (println ui-choices))) 72 | (run-ui (read-line) posts)))) 73 | 74 | (defn -main 75 | [& args] 76 | (do (println ui-choices) 77 | (run-ui (read-line) (get-posts)))) 78 | -------------------------------------------------------------------------------- /projects/reddit-analyser/test/reddit_analyser/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns reddit-analyser.core-test 2 | (:require [clojure.test :refer :all] 3 | [reddit-analyser.core :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | -------------------------------------------------------------------------------- /projects/shopping-list/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | -------------------------------------------------------------------------------- /projects/shopping-list/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2019-08-18 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2019-08-18 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/your-name/shopping-list/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/shopping-list/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /projects/shopping-list/README.md: -------------------------------------------------------------------------------- 1 | # shopping-list 2 | 3 | FIXME: description 4 | 5 | ## Installation 6 | 7 | Download from http://example.com/FIXME. 8 | 9 | ## Usage 10 | 11 | FIXME: explanation 12 | 13 | $ java -jar shopping-list-0.1.0-standalone.jar [args] 14 | 15 | ## Options 16 | 17 | FIXME: listing of options this app accepts. 18 | 19 | ## Examples 20 | 21 | ... 22 | 23 | ### Bugs 24 | 25 | ... 26 | 27 | ### Any Other Sections 28 | ### That You Think 29 | ### Might be Useful 30 | 31 | ## License 32 | 33 | Copyright © 2019 FIXME 34 | 35 | This program and the accompanying materials are made available under the 36 | terms of the Eclipse Public License 2.0 which is available at 37 | http://www.eclipse.org/legal/epl-2.0. 38 | 39 | This Source Code may also be made available under the following Secondary 40 | Licenses when the conditions for such availability set forth in the Eclipse 41 | Public License, v. 2.0 are satisfied: GNU General Public License as published by 42 | the Free Software Foundation, either version 2 of the License, or (at your 43 | option) any later version, with the GNU Classpath Exception which is available 44 | at https://www.gnu.org/software/classpath/license.html. 45 | -------------------------------------------------------------------------------- /projects/shopping-list/doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to shopping-list 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /projects/shopping-list/project.clj: -------------------------------------------------------------------------------- 1 | (defproject shopping-list "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 5 | :url "https://www.eclipse.org/legal/epl-2.0/"} 6 | :dependencies [[org.clojure/clojure "1.10.0"]] 7 | :main ^:skip-aot shopping-list.core 8 | :target-path "target/%s" 9 | :profiles {:uberjar {:aot :all}}) 10 | -------------------------------------------------------------------------------- /projects/shopping-list/shopping-list.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /projects/shopping-list/src/shopping_list/core.clj: -------------------------------------------------------------------------------- 1 | (ns shopping-list.core 2 | (:gen-class)) 3 | 4 | (def shoppings (atom [])) 5 | (def output-file "./things-to-buy.txt") 6 | (def options "Enter a number:\n1. Add product\n2. Save shopping list") 7 | 8 | (defn prompt 9 | [msg] 10 | (println msg) 11 | (read-line)) 12 | 13 | (defn add-shopping 14 | [shopping] 15 | (swap! shoppings conj shopping)) 16 | 17 | (defn add-product-to-shoppings 18 | [] 19 | (let [product (prompt "What to buy?") 20 | amount (prompt "How many?")] 21 | (add-shopping {:product product 22 | :amount amount}))) 23 | 24 | (defn shopping->str 25 | [x] 26 | (str (:product x) " * " (:amount x))) 27 | 28 | (defn shopping-printable 29 | [shoppings] 30 | (clojure.string/join 31 | "\n" 32 | (map shopping->str shoppings))) 33 | 34 | (defn save-shopping-list 35 | [file shoppings] 36 | (spit file (shopping-printable shoppings))) 37 | 38 | 39 | (defn -main 40 | [& args] 41 | (loop [choice (prompt options)] 42 | (case choice 43 | "1" (do (add-product-to-shoppings) 44 | (recur (prompt options))) 45 | "2" (do (save-shopping-list output-file @shoppings) 46 | (println "Shopping list saved")) 47 | (do (println "Invalid choice!!! Try again") 48 | (recur (prompt options)))))) 49 | 50 | -------------------------------------------------------------------------------- /projects/shopping-list/test/shopping_list/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns shopping-list.core-test 2 | (:require [clojure.test :refer :all] 3 | [shopping-list.core :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | -------------------------------------------------------------------------------- /projects/shopping-list/things-to-buy.txt: -------------------------------------------------------------------------------- 1 | Milk * 4 2 | Eggs * 6 -------------------------------------------------------------------------------- /projects/sum-em-up/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | -------------------------------------------------------------------------------- /projects/sum-em-up/.java-version: -------------------------------------------------------------------------------- 1 | 1.8.0.202 2 | -------------------------------------------------------------------------------- /projects/sum-em-up/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2019-08-06 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2019-08-06 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/your-name/sum-em-up/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/sum-em-up/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /projects/sum-em-up/README.md: -------------------------------------------------------------------------------- 1 | # sum-em-up 2 | 3 | FIXME: description 4 | 5 | ## Installation 6 | 7 | Download from http://example.com/FIXME. 8 | 9 | ## Usage 10 | 11 | FIXME: explanation 12 | 13 | $ java -jar sum-em-up-0.1.0-standalone.jar [args] 14 | 15 | ## Options 16 | 17 | FIXME: listing of options this app accepts. 18 | 19 | ## Examples 20 | 21 | ... 22 | 23 | ### Bugs 24 | 25 | ... 26 | 27 | ### Any Other Sections 28 | ### That You Think 29 | ### Might be Useful 30 | 31 | ## License 32 | 33 | Copyright © 2019 FIXME 34 | 35 | This program and the accompanying materials are made available under the 36 | terms of the Eclipse Public License 2.0 which is available at 37 | http://www.eclipse.org/legal/epl-2.0. 38 | 39 | This Source Code may also be made available under the following Secondary 40 | Licenses when the conditions for such availability set forth in the Eclipse 41 | Public License, v. 2.0 are satisfied: GNU General Public License as published by 42 | the Free Software Foundation, either version 2 of the License, or (at your 43 | option) any later version, with the GNU Classpath Exception which is available 44 | at https://www.gnu.org/software/classpath/license.html. 45 | -------------------------------------------------------------------------------- /projects/sum-em-up/doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to sum-em-up 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /projects/sum-em-up/numbers.txt: -------------------------------------------------------------------------------- 1 | 1 2 3 4 5 10 300 -------------------------------------------------------------------------------- /projects/sum-em-up/project.clj: -------------------------------------------------------------------------------- 1 | (defproject sum-em-up "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 5 | :url "https://www.eclipse.org/legal/epl-2.0/"} 6 | :dependencies [[org.clojure/clojure "1.10.0"]] 7 | :main ^:skip-aot sum-em-up.core 8 | :target-path "target/%s" 9 | :profiles {:uberjar {:aot :all}}) 10 | -------------------------------------------------------------------------------- /projects/sum-em-up/src/sum_em_up/core.clj: -------------------------------------------------------------------------------- 1 | (ns sum-em-up.core 2 | (:gen-class)) 3 | 4 | (defn sum 5 | "Sums a vector of numbers" 6 | [a-seq] 7 | (apply + a-seq)) 8 | 9 | (defn str->long 10 | [x] 11 | (Long/valueOf x)) 12 | 13 | (defn split-words 14 | "Turns a string of numbers into collection of numbers" 15 | [text] 16 | (clojure.string/split text #"\s+")) 17 | 18 | 19 | (= ["1" "2" "3"] (split-words "1 2 3")) 20 | (split-words "") 21 | 22 | (defn -main 23 | [& args] 24 | (let [file-name (first args) 25 | text (slurp file-name) 26 | str-coll (split-words text) 27 | long-coll (map str->long str-coll) 28 | total (sum long-coll)] 29 | (println total))) -------------------------------------------------------------------------------- /projects/sum-em-up/sum-em-up.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /projects/sum-em-up/test/sum_em_up/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns sum-em-up.core-test 2 | (:require [clojure.test :refer :all] 3 | [sum-em-up.core :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 1 1)))) 8 | 9 | (deftest str->long-test 10 | (testing "Casting text to numbers" 11 | (is (= 15 (str->long "15"))) 12 | (is (= -100 (str->long "-100"))))) 13 | 14 | (deftest sum-test 15 | (testing "Adding up numbers in collections of different lengths and types." 16 | (is (= 10 (sum [5 5]))) 17 | (is (= 0 (sum '(10 -5 -3 -1 -1)))) 18 | (is (= 100 (sum #{25 75}))) 19 | (is (= 0 (sum []))))) 20 | 21 | (deftest split-words-test 22 | (testing "Tests splitting words from string" 23 | (is (= ["1" "2" "3"] (split-words "1 2 3"))) 24 | (is (= ["Hello" "World" (split-words "Hello World")])))) 25 | --------------------------------------------------------------------------------