clojurecember

My attempt to learn at least a little bit of Clojure each day in December
git clone https://git.sr.ht/~jbauer/clojurecember
Log | Files | Refs | README | LICENSE

commit a280c92022d32e5004277e0cbe3c7384fafed1fd
parent c1e8becf58643c596f2fc3f26c39beb8a3cd5f6b
Author: Jake Bauer <jbauer@paritybit.ca>
Date:   Sun,  4 Dec 2022 17:18:20 -0500

Fourth day

Diffstat:
MREADME.md | 4++++
Aflow-control.clj | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anamespaces.clj | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 328 insertions(+), 0 deletions(-)

diff --git a/README.md b/README.md @@ -15,3 +15,7 @@ Continued following the Clojure Guide with [Functions](https://www.clojure.org/g ## DEC03 Finished up with Functions in Clojure and moved on to [Sequential](https://www.clojure.org/guides/learn/sequential_colls) and [Hashed](https://www.clojure.org/guides/learn/hashed_colls) collections. + +## DEC04 + +Moved onto learning Clojure [Flow Control](https://www.clojure.org/guides/learn/flow) and [Namespaces](https://www.clojure.org/guides/learn/namespaces) which finishes off learning the basics of Clojure. diff --git a/flow-control.clj b/flow-control.clj @@ -0,0 +1,195 @@ +; FLOW CONTROL + +; Satements vs. Expressions + +; In Java, expressions return values whereas statements do not. Howver, in +; Clojure everything is an expression so everything returns a value. Blocks of +; multiple expressions return the last value and expressions that exclusively +; have side effects return nil. + +; Accordingly, flow control operators are expressions, can be composed, and +; return their result. + +; if consists of a condition, a "then", and an "else". +; the else is optional + +(str "2 is " (if (even? 2) "even" "odd")) +; 2 is even +(if (true? false) "impossible!") +; nil + +; In Clojure, all values are logically true or false. The only "false" variables +; are false and nil. + +(if true :truthy :falsey) +; :truthy +(if (Object.) :truthy :falsey) ; objects are true +; :truthy +(if [] :truthy :falsey) ; empty collections are true +; :truthy +(if 0 :truthy :falsey) ; zero is true +; :truthy +(if false :truthy :falsey) +; :falsey +(if nil :truthy :falsey) +; :falsey + +; If only takes a single expression for the "then" and "else". Use do to create +; larger blocks that are a single expression. The only reason to do this is if +; your bodies have side-effects + +(if (even? 5) + (do (println "even") + true) + (do (println "odd") + false)) + +; when is an if with only a then branch, but it evaluates any number of +; statements as a body so no do is required + +(when (neg? x) + (throw (RuntimeException. (str "x must be positive: " x)))) + +; cond is a series of tests and expressions. Each test is evaluated in order and +; the expression is evaluated and returned for the first true test. + +; if no test is satisfied, nil is returned. A common idiom is to use a final +; test of :else (which always evaluates to true) + +(let [x 11] + (cond + (< x 2) "x is less than 2" + (< x 10) "x is less than 10" + :else "x is greater than or equal to 10")) + +; case compares an argument to a series of values to find a match. This is done +; in constant time. However, each value must be a compile-time literal (numbers, +; strings, keywords, etc.). Unlike cond, case will throw an exception if no +; value matches. + +(let [x 11] + (case + 5 "x is 5" + 10 "x is 10")) +; IllegalArgumentException No matching clause: 11 + +; case can have a final trailing expression that will be evaluated if no test +; matches + +(let [x 11] + (case + 5 "x is 5" + 10 "x is 10" + "x isn't 5 or 10")) +; x isn't 5 or 10 + +; ITERATION FOR SIDE EFFECTS + +; dotimes evaluates an expression n times then and returns nil + +(dotimes [i 3] + (println i)) +; 0 +; 1 +; 2 +; nil + +; doseq iterates over a sequence, forces evaluation of the sequence if it's +; lazy, and then returns nil + +(doseq [n (range 3)] + (println n)) +; 0 +; 1 +; 2 +; nil + +; doseq can have multiple bindings, similar to nested foreach loops. It +; processes all permutations of sequence content then returns nil. + +(doseq [letter [:a :b] + number (range 3)] ; list of 0, 1, 2 + (prn [letter number])) +; [:a 0] +; [:a 1] +; [:a 2] +; [:b 0] +; [:b 1] +; [:b 2] +; nil + +; CLOJURE'S FOR + +; It is a list comprehension, not a for loop, basically a generator function for +; sequence permutation +; Bindings behave like doseq +(for [letter [:a :b] + number (range 3)] ; list of 0, 1, 2 + [letter number]) +; ([:a 0] [:a 1] [:a 2] [:b 0] [:b 1] [:b 2]) + +; RECURSION + +; Clojure provides recur and the sequence abstraction +; recur is classic recursion (consumers don't control it, considered low-level) +; Sequences represent iteration as values (consumers can partially iterate) +; Reducers represent iteration as function composition (added in Clojure 1.5) + +; loop and recur are a functional looping construct where loop defines bindings +; and recur executes loop with new bindings. + +(loop [i 0] + (if (< i 10) + (recur (inc i)) + i)) + +; defn and recur -> function arguments are implicit loop bindings + +(defn increase [i] + (if (< i 10) + (recur (inc i)) + i)) + +; When using recur for recursion, recur must be in "tail position" (the last +; expression in a branch) and it must provide values for all bound symbols by +; position (i.e. can't call recur without passing values back to loop or defn/fn +; arguments). Recursion via recur does not consume stack. + +; EXCEPTIONS + +; Exception handling is done by throw/catch/finally as in Java: + +(try + (/ 2 1) + (catch ArithmeticException e + "divide by zero") + (finally + (println "cleanup"))) + +; Exceptions can also be thrown: + +(try + (throw (Exception. "something went wrong")) + (catch Exception e (.getMessage e))) + +; Exceptions with Clojure data: + +; ex-info takes a message and a map +; ex-data gets the map back out or nil of not created with ex-info + +(try + (throw (ex-info "There was a problem" {:detail 42})) + (catch Exception e + (prn (:detail (ex-data e))))) + +; Clojure has a with-open function similar to Python's "with open("file") as f": + +(let [f (clojure.java.io/writer "/tmp/new")] + (try + (.write f "some text") + (finally + (.close f)))) + +; Can be written: +(with-open [f (clojure.java.io/writer "/tmp/new")] + (.write f "some text")) diff --git a/namespaces.clj b/namespaces.clj @@ -0,0 +1,129 @@ +; NAMESPACES + +; Namespaces provide a means to organize code. They allow giving unambiguous +; names to functions or values. These full names are long, thus namespaces also +; provide a means to unambiguously reference the names of other functions and +; values but using names that are shorter and easier to type. + +; A namespace is both a name context and a container for vars. Namespace +; names are symbols where periods are used to separate namespace parts, such as +; clojure.string. By convention, namespace names are typically lowercase and use +; - to separate words, though this is not required. + +; VARS + +; vars in a namespace have a fully-qualified name that is the combination of the +; namespace and the var name. E.g. clojure.string/join is a fully-qualified var +; name where clojure.string is the namespace and join is the var inside. All +; vars are globally accessible by their fully-qualified name. By convention, +; var names are lowercase with words separated by -, though this is also not +; required. Var names may contain most non-whitespace characters. + +; vars are created using def and other special forms or macros that start with +; def such as defn. They are created in the current namespace, which Clojure +; tracks in the var clojure.core/*ns*. The current namespace can be changed with +; the in-ns function. + +; LOADING + +; In addition, namespace names provide a convention for where a namespace's code +; should be found for loading. A path is created based on the namespace's name. + +; Periods become directory separators +; Hyphens become underscores +; The file extension .clj is added + +; Thus, the namespace com.some-example.my-app becomes +; com/some_example/my_app.clj. + +; Load paths are searched using the JVM classpath, which is a series of +; directory locations or JAR files (which are essentially just zip files). + +; When a resource is needed, the JVM searches each classpath location in order +; for a file at the relative location of the load path. If the classpath was +; src:test, the load path would be checked at src/com/some_example/my_app.clj +; then test/com/some_example/my_app.clj. + +; There are several ways to load code in Clojure, but most commonly it's done +; using require. + +; Due to this loading convention, most Clojure is structured with a 1-to-1 +; mapping of namespaces to files, stored in hierarchical fashion that maps to +; the namespace structure. + +; DECLARING NAMESPACES + +; Most Clojure files represent a single namespace and declare the dependencies +; for that namespace at the top of the file using the ns macro which often looks +; like this: + +(ns com.some-example.my-app + "My app example" + (:require + [clojure.set :as set] + [clojure.string :as str])) + +; The ns macro specifies the namespace name (should match the file path location +; using the conventions above), an optional docstring, and then one or more +; clauses that declare things about the namespace. + +; By default, we can refer to or invoke vars in the current namespace without +; specifying the namespace. We can also usually refer to clojure.core library +; functions without fully qualifying them either. All clojure.core library vars +; have been referred into the current namespace. refer makes an entry in the +; current namespace's symbol table that refers to the var in the other +; namespace. + +; The clojure.core referral is done by the ns macro, and there are ways to +; suppress this if needed. + +; The :require clause corresponds to the require function which specifies one or +; more namespaces to load that this namespace depends on. For each namespace, +; require can do several things: + +; Load or reload the namespace +; Optionally assign an alias that can be used to refer to vars from the loaded +; namespace in this scope +; Optionally refer vars from the loaded namespace for use by unqualified name in +; this namespace. + +; The last two parts are all about making names easier to use. While vars can +; always be referred to by their fully-qualified name, we rarely want to type +; fully-qualified names in our code. Aliases let us use shorter versions and +; refer allows us to use names without a namespace qualifier at all. + +; In require, namespaces most commonly take one of three forms: + +; clojure.set (just loads clojure.set namespace if not already loaded) +; [clojure.set :as set] (load and create an alias set for the whole clojure.set +; namespace) +; [clojure.set :refer [union intersection]] (load and refer specific vars into +; this namespace) + +; This is similar to Python's: + +; import sys +; import sys as s +; from sys import argv + +; JAVA CLASSES AND IMPORTS + +; In addition to vars, Clojure also provides support for Java interop and access +; to Java classes which live in packages. Java classes can always be referred to +; using their FQN, e.g. java.util.Date. + +; The ns macro also imports the classes in the java.lang package so that they +; can be used as just the class name, rather than the fully-qualified class +; name. e.g. String instead of java.lang.String. + +; Similar to :refer, the ns macro has an :import clause (supported by the import +; macro) that lets you import other classes so they can be used with unqualified +; names: + +(ns com.some-example.my-app2 + (:import + [java.util Date UUID] + [java.io File])) + +; This example imports the Date and UUID class from the java.util package and +; the File class from the java.io package.