view 6-Clojure/README.txt @ 82:cf7182bca068

Notes on infinite sequences Plus a clarification on "some"
author IBBoard <dev@ibboard.co.uk>
date Sat, 08 Jun 2019 14:00:25 +0100
parents 0f57e5c2ae82
children eccc649d49a2
line wrap: on
line source

Install Clojure with "curl -O https://download.clojure.org/install/linux-install-1.10.0.442.sh; chmod +x linux-install-1.10.0.442.sh; sudo ./linux-install-1.10.0.442.sh" (it's a downloader that downloads the real package)

Clojure has a repl (clj), which needs rlwrap ("sudo zypper in rlwrap"). This then downloads some MORE files from Maven. Don't you just LOVE the Java ecosystem?!

Clojure is a Lisp dialect. It feels a little like Prolog in that it's logic and brackets galore, but Prolog is about predicates and pattern matching whereas Lisp has more of an imperative language with odd syntax
but sensible things like "if" statements. It uses strong, dynamic typing.

Running commands at the repl and prints output *and* the return value (which can be "nil").

Commands are prefix, not infix:
  "Normal" languages: 1 + 1
  Clojure: (+ 1 1)

Division of integers creates a (normalised) "ratio" type to maintain accuracy:
  (/ 2 4) → 1/2
  (/ 2.0 4) → 0.5

Prefix does have its advantages, though - it's more like a function and can have variable numbers of parameters:
  (+ 2 2 2 2) → 8

And it can be used to check order:
  (< 1 2 3) → true
  (< 1 3 2 4) → false

Strings are double-quoted, but characters can be escaped with a backslash outside quotes to become a string. When done with "str" it makes a string:
  (str \h \e \l \l \o) → "hello"
This seems like it would rarely be useful.

Strings and characters are different (just like in Java)
  (= "a" \a) → false

Clojure can pass code as arguments, e.g.:
  (if true (println "It's true!"))
but that's quite normal for functional languages.

The "if" function is basically "if <condition> <true>[ <false>]".

Note: 0 and "" are true but nil is false, so only false and the null value are falsey.

Clojure has four main data structures - lists (for code), vectors (for data), sets and maps. These are types of sequence. Other things may also be sequences (strings, file system structures, etc).
Sequences can then be operated on with "first", "last", "rest" and "cons" (construct from head and tail) functions.

You can't build a list Python style with "(1 2 3)". You need either "(list 1 2 3)" or the odd syntax "'(1 2 3)".
There are also functions like "nth" (nth <list> <0-based index>)

Vectors use sequare brackets. They're like a list, but you can implicitly index them (i.e. you don't need "nth") by doing ([vector] pos) because they're treated as functions.

first, last and rest can be used for pattern matching.

Sets use hash followed by squiggly brackets (which is easily confused with a map). Sorted sets use "(sorted-set <values> …)".
Manipulating sets is less clean - (clojure.set/union <set1> <set2>) for unioning sets and (clojure.set/difference <set1> <set2>) for difference.
Sets are also functions and so you can check if an item is in a set with (#{set} <value>). Returns "nil" if not in the set, else returns the value.

Maps are defined with squiggly brackets and have implicit key-value positions, so you can write"{:chewie :wookie :lea :human}" for keys :chewie and :lea with values :wookie and :human.
You can also put in commas, though.


":text" appears to be a unique identifier ("keyword"), but the book doesn't mention this until three pages after it started using it. They're like Ruby symbols and Prolog/Erland atoms.
Symbols point to something else and keywords point to themselves (although it isn't made clear what that means).
Keywords are also functions, so (map key) and (key map) are equivalent.

Maps can be merged with "merge" or with "merge-with", which takes a function to calculate the merged value.

Variables are assigned to with "(def <variable-name> <value>)". Functions are defined with "(defn <func-name> [<params list>] (<body>))".
There's also an optional docstring after <func-name>. Docs can be read with "(doc <func-name>)".

Function arguments can be quite complex and use pattern matching, e.g. getting the second value in a vector with:
  (defn line-end [[_ second]] second)
which takes a single arg (the vector "[_ second]") and the body just returns "second". Clojure calls this "destructuring". It ignores any additional values (e.g. "[_ c]" matches "[1 2 3 4 5]" and puts "2" in c and ignores the trailing 3,4,5).

Where "def" is used for long-term variables (which are created in user-space - the return result is #'user/<def-name>), "let" is used for local variables, which is useful within function definitions, e.g.:
  (defn centre_space [board] (let [[_ [_ c]] board] c))
which is a function with one argument (board) where the function body uses "let" with a vector containing a pattern to match and a variable to match against, and then a function that uses that vector.

There are also anonymous functions that use "(fn [args] (body))" - like a defn, but without a name. Alternatively, "#(function-body-with-%-as-a-variable)" defines an anonymous "reader macro" function with % bound to each value.
  (map (fn [w] (* 2 (count w))) my_list)
is equivalent to:
  (map #(* 2 (count %)) my_list)

As well as the map function, there's "apply" and "filter".

Also, some helper functions exist, like "odd?". Terminating in a question mark seems to be the Clojure approach to predicates. Sometimes you end up with multiple question marks:

(every? number? [1 2 3 :four])
;false

not-every? and not-any? both have question marks as well. But not everything is question marked:

(some nil? [1 2 nil])
; true

This seems oddly inconsistent. The footnote explains that it's because "some returns the first value that is not nil or false", so "nil?" return false for 1 and 2 and then return true for nil and so some returns true.
With other "some" filters, like "(some odd? …)", it works because nil (if it doesn't find anything) is falsey and numbers (if it finds an odd one) are truthy.

It's not a predicate because "(some nil? [1 2]) returns nil, not false.

Functional languages do lazy tail recursion. Unless they're Clojure, because the JVM doesn't support it. Clojure does it with a  "loop" and a "recur" function. "loop" takes x and y with initial values and a function to call. See loop_recur.clj.

For loops take the form (for [val collection<, val2 collection2<, …>>] (body)), which is a bit like "for X in Collection", but doesn't look like it at first.
Multiple val/collection pairs give nested for loops (so every val with every val2).

But then for loops can take a ":when" test keyword (or :let or :while). Which is odd, because :xxx has only ever been a user atom, but this one already has meaning. And they can be mixed anywhere in the parameters and Clojure knows what to do.

Reduce is more familiar: (reduce func list)

(reduce + [1 2 3 4]) ; sums
(reduce * [1 2 3 4 5]) ; factorial

As well as sorting a list with (sort list) you can use a custom funcion with (sort-by function list) where "function" takes a single parameter and generates a key.

Infinite sequences can be built with (repeat obj), (cycle [list]) and (iterate func start_obj). (take count list) takes the number of items that you want from a potentially infinite sequence (which is lazily evaluated). There's also a (drop count list) function to skip items.
If you don't like the "inside function happens first" Lisp-like function order then the slightly bizarre "->>" function (left-to-right operator) applies a number of functions in order, e.g.:

(->> [:later :rinse : repeat] (cycle) (drop 2) (take 5))
; equivalent to (take 5 (drop 2 (cycle [:later :rinse : repeat])))

You can also (interpose obj list) to insert something between each item and (interleave list list) to interleave two lists.

The book seems to use "(take n (iterate inc m))" rather than "(range m n+1)" but doesn't say why. Range is lazy, as is iterate. The only benefit to iterate is if you're doing an 'infinite' list with a high start, as it starts high rather than spending time generating and dropping.