commit a280c92022d32e5004277e0cbe3c7384fafed1fd
parent c1e8becf58643c596f2fc3f26c39beb8a3cd5f6b
Author: Jake Bauer <jbauer@paritybit.ca>
Date: Sun, 4 Dec 2022 17:18:20 -0500
Fourth day
Diffstat:
M | README.md | | | 4 | ++++ |
A | flow-control.clj | | | 195 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | namespaces.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.