Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write Clojure Primer #99

Merged
merged 7 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
706 changes: 706 additions & 0 deletions docs/clojure_primer.index.html

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/clojure_primer.index_files/html-default0.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/clojure_primer.index_files/html-default1.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/clojure_primer.index_files/html-default2.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/clojure_primer.index_files/katex2.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/clojure_primer.index_files/katex3.js

Large diffs are not rendered by default.

258 changes: 258 additions & 0 deletions docs/notebooks/clojure_primer/clj-syntax-slide-1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
373 changes: 373 additions & 0 deletions docs/notebooks/clojure_primer/clj-syntax-slide-2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
587 changes: 587 additions & 0 deletions docs/notebooks/clojure_primer/clj-syntax-slide-3.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
258 changes: 258 additions & 0 deletions notebooks/clojure_primer/clj-syntax-slide-1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
373 changes: 373 additions & 0 deletions notebooks/clojure_primer/clj-syntax-slide-2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
587 changes: 587 additions & 0 deletions notebooks/clojure_primer/clj-syntax-slide-3.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
146 changes: 146 additions & 0 deletions notebooks/clojure_primer/index.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
^:kindly/hide-code
(ns clojure-primer.index
"A very brief introduction to Clojure for a would-be users of Wolframite not familiar with the language"
(:require [scicloj.kindly.v4.kind :as k]
[clojure.math :as math]
[wolframite.core :as wl]
[wolframite.wolfram :as w]))

;; Let's start Wolfram to be ready for our examples underneath:
(wl/start)

(k/md "# Clojure Primer

A very brief introduction to Clojure for a would-be users of Wolframite not familiar with the language.

## The essence of Clojure

It's impossible to summarize any language in just a few words. But we can say that simplicity and expressiveness
are some of the core values of Clojure. It has been designed to allow you to express your thoughts concisely,
to write programs that avoid some common sources of errors (especially those caused by shared mutable data),
and produce code that is well suited to evolution over time.

## Elementary syntax

Let's start by comparing how **adding a few elements to a list**
looks like in Wolfram, Python, and Clojure:
")

^:kindly/hide-code
(k/table
[["Wolfram" (k/md "```wolfram
Join[{}, {\"first\", 2, <|\"name\" -> \"Ava\"|>}]
```")]
["Python" (k/md "```python
[]+[\"first\",2,{\"name\": \"Ava\"}]
```")]
["Clojure" '(conj [] "first" 2 {"name" "Ava"})]])

;; We see here a few basic data structures: a `[vector]`, similar to Wolfram/Python lists, and a map `{"key": "value"}`,
;; similar to Python dictionaries / Wolfram associations. One interesting difference is that `,` commas in Clojure are optional.
;;
;; `"Strings"` and numbers `1, 2, ...` are the same. Clojure's `nil` is just like Python's `None`
;;
;; Now, how would we **define a function** that sums two numbers?

^:kindly/hide-code
(k/table
[["Wolfram" (k/md "```wolfram
plus[x_, y_] := Plus[x, y]
```")]
["Python" (k/md "```python
def plus(x, y):
return x + y
```")]
["Clojure" '(defn plus [x y] (+ x y))]])

;; Now we are ready to learn a little more about how to read Clojure code.

^:kindly/hide-code
(k/hiccup [:img {:src "./notebooks/clojure_primer/clj-syntax-slide-1.svg" :alt "Clojure syntax 1" :width "422" :height "150"}])

(k/md "Clojure is a Lisp, i.e. the code consists of the same data you use as a programmer: lists, vectors, maps, symbols, etc.

Lists and vectors are both sequential data structures. Vectors support efficient access to any element, while lists are always read from start.

Keywords are similar to strings, but tuned for use as keys in maps. Additionally, a keyword may be used as a function that takes a map and looks up the corresponding value:")

(:name {:name "Ava"})

(map :name [{:name "Ava"} {:name "Boudicca"}])

;; This is the syntax, i.e. how characters become data. Now, on to _semantics_, i.e. what does the data mean:

^:kindly/hide-code
(k/hiccup [:img {:src "./notebooks/clojure_primer/clj-syntax-slide-2.svg" :alt "Clojure syntax 1" :width "586" :height "193"}])

(k/md "
A list is _interpreted_ as an invocation (e.g. a function call), where the first element is a symbol that resolves to something invocable
(a function, a special form, or a macro; we don't need to care about their differences here). All the other elements are arguments to the function.

The same code structure is used for everything, including defining functions:")

^:kindly/hide-code
(k/hiccup [:img {:src "./notebooks/clojure_primer/clj-syntax-slide-3.svg" :alt "Clojure syntax 1" :width "640" :height "286"}])

(k/md "
The `defn`here defines a new function called `subtract-smaller`, taking two arguments `x` and `y`.

Noteworthy:

`>`, `-` are just function calls. `if` is a \"special form\" and `defn` is a macro, but they behave very similarly to functions.

Notice there is no `else`. There are no reserved/special words in Clojure.
")

(k/md "## Basics

* Everything returns a value (which may be `nil`). F.ex. `if` returns whatever the evaluated branch returned.
* Clojure is [very tolerant of `nil`](https://ericnormand.me/article/nil-punning), _most_ functions treat it as an empty value - you can
`map` over it (getting `nil` back), append to it (getting back a list with the appended element) etc.
* Whitespace is not significant. Commas are treated as a whitespace, and used occasionally for readability
* Booleans: `true` and `false`. All predicates and conditional expressions treat both `false` and `nil` as `false` and
everything else as `true` (also called \"truthy\").
* Clojure runs on the Java Virtual Machine (JVM) and you can directly call Java methods. (Learn more about [Clojure ↔ Java interoperability](https://clojure.org/reference/java_interop).)
* Clojure is intended for [_interactive development_](https://clojure.org/guides/repl/introduction),
also known as REPL-driven development, where you build and evolve your application while it is running.
You may be used to a very similar this style of development from Python or Wolfram notebooks.
")

(k/md "## Evaluation

Now is a good time to revisit [Understanding Wolframite](@sec-understanding-wolframite) and read about the three forms of Wolframite expressions
(raw, evaluated, and Wolfram string).

A point worth re-iterating is that Wolframite expressions are not evaluated by Clojure but are sent ± as-is to Wolfram for evaluation.
Thus, you may not mix Clojure and Wolframite expressions freely. But there is one exception to that - a Wolframite expression may contain Clojure expressions
that can be evaluated fully on the Clojure side and replaced with the result, before being sent to Wolfram. Let's have a look at a few examples:")

;; Clojure-side only:
(+ (* 3 (math/pow 2 2))) ; 12.0

;; Wolfram-only:
(wl/eval (w/+ (w/* 3 (w/Power 2 2))))

;; Mixed, with Clojure evaluated before sending the form to Wolfram:
(wl/eval (w/+ (w/* 3 (w/Power (+ 1 1) 2))))

;; This is how the expression is evaluated before we resolve aliases and turn it into Wolfram and send it over to the kernel:

(w/+ (w/* 3 (w/Power (+ 1 1) 2)))

;; Notice that you may nest Clojure-only expression, which does not depend on the surrounding Wolfram context, inside a Wolframite expression,
;; but you cannot do the opposite, i.e. nest a Wolframite expression inside a Clojure expression:
(try
(wl/eval (+ (w/* 3 (w/Power (+ 1 1) 2))))
(catch Exception e
(str e)))

;; This fail because we are passing a Wolframite expression (a list) to the Clojure `+` function, but it only works with numbers. We'd need to evaluate the expression first:

(+ (wl/eval (w/* 3 (w/Power (+ 1 1) 2))))

(k/md "## Resources for further learning

* The [Clojure cheatsheat](http://jafingerhut.github.io/cheatsheet/clojuredocs/cheatsheet-tiptip-cdocs-summary.html)\nis a good place to look for functions.
* [The 100 Most Used Clojure Expressions](https://ericnormand.me/article/100-most-used-clojure-expressions)")
178 changes: 178 additions & 0 deletions notebooks/clojure_primer/index.tmp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# TMP: Wolframite Clojure Primer, as markdown

A very brief introduction to Clojure for users of Wolframite not yet familiar with the language.

## Elementary syntax

Syntax of a programming language defines how text is turned into data. Fortunately, Clojure syntax is very simple and consistent:

<img src="clj-syntax-slide-1.svg" alt="Clojure syntax 1" width="422" height="150">

Clojure is a Lisp, i.e. the code consists of the same data you use as a programmer. (Primarily, symbols, lists, and vectors.)

_Vectors_ (`[ ... ]`) are similar to Java(Script) arrays.

_Keywords_ (`:whatever`) are like a special kind of strings, used primarily as keys in maps. (Plus, they can also be used as functions taking a map and returning the value stored there at that key.)

_Maps_ are heterogeneous pairs of arbitrary keys and values: `{:key 1, "another" 2, 42 ["arbitrary" "keys/values"]}`. Here, we have three keys (`:key`,`"another"`,`42`) and their values (`1`,`2`, `[ ... ]`).

This is the syntax, i.e. how characters become data. Now, on to _semantics_, i.e. what does the data mean:

<img src="clj-syntax-slide-2.svg" alt="Clojure syntax 2" width="586" height="193">

A list is _interpreted_ as an invocation (e.g. a function call). The first element is typically a symbol naming something invocable - a function, a macro, or a special form. (We don't really need to distinguish between the three here.) This is an elementary pattern of Clojure code:

<img src="clj-syntax-slide-3.svg" alt="Clojure syntax 3" width="640" height="286">

Here we have a piece of code which defines a new function (`defn`) named `subtract-smaller`, taking two arguments, and subtracting the smaller one from the larger one. We can see that defining a function looks just the same as invoking one. Highlights:

`>`, `-` are function calls - there are no operators in Clojure.

`if` is a "special form", i.e. just like a function call for our purposes.

There is no `else`, contrary to Java(Script), the (optional) third argument to `if` is the else-branch of code. Notice that there are no "reserved words" as in other languages - everything is just a function or friends. Also notice that everything in Clojure returns a value, including the `if` (namely, the value of the executed branch). Finally, there is no explicit `return` - the output of the last expression in a function body is the return value.

## Key building blocks of a Clojure program

The [Clojure Cheatsheet](http://jafingerhut.github.io/cheatsheet/clojuredocs/cheatsheet-tiptip-cdocs-summary.html) provides a great, well-organized overview of most things Clojure offers. Let's list some of the most important ones here:

**TODO**

* Definitions:
* [def](https://clojuredocs.org/clojure_core/clojure.spec.alpha/def) - define a global constant
* [fn](https://clojuredocs.org/clojure_core/clojure.core/fn) - make an anonymous function, also known as "lambda" (there is also the shorthand form `#(my-cool-fn %)`)
* [defn](https://clojuredocs.org/clojure_core/clojure.core/defn) - define a named function
* [ns](https://clojuredocs.org/clojure_core/clojure.core/ns) - at the start of a file to declare a new namespace, and `:require` (import) other namespaces
* "Control":
* [let](https://clojuredocs.org/clojure_core/clojure.core/let) - define _local_ constants
* [if](https://clojuredocs.org/clojure_core/clojure.core/if), [when](https://clojuredocs.org/clojure_core/clojure.core/when), ...
* Arithmetics: [+](https://clojuredocs.org/clojure_core/clojure.core/%2B), -, *, etc.
* [Data structures and literals](https://clojure.org/reference/data_structures): `[]` for vectors, `{:k "val"}` for maps, `nil` for nil/null/no data, ...
* Sequence processing:
* [map](https://clojuredocs.org/clojure_core/clojure.core/map) - "update" each element with a function
* [filter](https://clojuredocs.org/clojure_core/clojure.core/filter) - remove elements not passing a test
* [reduce](https://clojuredocs.org/clojure_core/clojure.core/reduce) - process each element, building an output value iteratively
* [apply](https://clojuredocs.org/clojure_core/clojure.core/apply) - apply a function expecting individual arguments to a sequence of their values; ex.: `(apply + [1 2]) ; => 3`
* ...
* Extracting parts of a sequence:
* [first](https://clojuredocs.org/clojure_core/clojure.core/first), [second](https://clojuredocs.org/clojure_core/clojure.core/second), [take](https://clojuredocs.org/clojure_core/clojure.core/take), [last](https://clojuredocs.org/clojure_core/clojure.core/last), ...
* "Changing" data - mutating operations return new data, without changing the original
* [conj](https://clojuredocs.org/clojure_core/clojure.core/conj), [assoc](https://clojuredocs.org/clojure_core/clojure.core/assoc), [dissoc](https://clojuredocs.org/clojure_core/clojure.core/dissoc) - add an element to a sequence, add/remove to/from a map
* Truth: everything but `nil` and `false` is "truthy" and considered as `true`, f.ex. in `if`
* Comments and whitespace: `;` is a line comment, and `,` is treated just as a space and can be used to improve readability
* Mutable data holders: `atom` - when you need to keep global state safely accessed from multiple threads

You will likely also want to [interoperate with Java](https://clojure.org/reference/java_interop) libraries and code, which Clojure is good at.

## Data, code, and evaluation

For Wolframite, you need to understand a little bit about the transformation from text ➡︎ data ➡︎ code ➡︎ behavior.

Text is ingested by the [Clojure Reader](https://clojure.org/reference/reader) at read time according to the rules of _syntax_ discussed above, to become _data_. At run time, this data is then _interpreted_ according to the rules of _semantics_ as _code_ and _evaluated_ accordingly, to become behavior. (There is also compile time between the two, when data becomes code, but we can safely ignore that for our purposes.)

These distinctions matter to us, because we are trying to do something unusual - namely turn text into data that we translate ourselves into Wolfram expressions and send to Wolfram for evaluation. (And, on the way back, we turn Wolfram responses back into Clojure data.)

Contrary to normal Clojure execution, we do not want Clojure to interpret data as code and try to evaluate it. We want our data as data, and we interpret and evaluate them ourselves.

Let's compare the result of evaluating Clojure code vs. Wolframite code, to make this distinction clearer. In Clojure we can do:

```clj
(+ (* 3 (math/pow 2 2)))
```

and we get back a number - the result of running the code. If we write analogous code in Wolframite:

```clj
(w/+ (w/* 3 (w/Power 2 2)))
```

we get back data representing this very expression. We could also write it directly as data:

```clj
(quote (+ (* 3 (Power 2 2))))
```

Or, leveraging a [hint to the Reader](https://clojure.org/reference/reader#_quote), in a shorter way:

```clj
'(+ (* 3 (Power 2 2)))
;; the Reader turns ' into (quote ...)
```

Here, both [`quote`](https://clojuredocs.org/clojure.core/quote) and `'` tell Clojure to skip interpreting and evaluating the data.

We interpret and evaluate this data ourselves, via `wl/eval`:

```clj
(wl/eval (w/+ (w/* 3 (w/Power 2 2))))

(wl/eval '(+ (* 3 (Power 2 2))))
```

The quoted form is the _raw Wolframite form_, while the the `w/...` form is the _evaluated Wolframite form_. The evaluated form is more convenient, because it supports code-completion, syntax checking, and other conveniences, such as combining expressions with Clojure threading macros (see below). However, it may not cover all your needs and thus you may need to fall back on the raw form. Notice that you may nest a raw form expression inside an evaluated form expression, but not vice versa. (At least not without unquoting, which we discuss below.) Beware that a raw from is, from the point of Clojure, a single, opaque piece of data - that's why it doesn't work with threading macros.

## A brief look at macros, syntax quote, and unquoting

Clojure macros enable you to write code, which generates code. (Or rather, data representing code.) They are a very powerful tool for abstracting away repetitive code patterns and creating custom "domain specific languages." You will use them regularly, most likely without even noticing they are macros. But as an end user, you will rarely, if ever, write one. Yet we need to discuss some tools from the macro author's toolbox, because we too write code, which produces code - only not for Clojure, but for Wolfram to evaluate.

A Clojure macro looks just like a function, only it starts with `defmacro`, and it returns data representing code. It could do so explicitly, having a body such as

```clj
(list '+ 1 2)
```

but this is rather verbose and thus macro authors typically leverage some [Reader](https://clojure.org/reference/reader) hints to make this simpler:

```clj
`(+ 1 2)
```

Notice that we've used here the so called syntax quote (backtick), which enables _unquoting_ and fully qualifies all used symbols with the namespace they belong to. Let's see everything it can do:

```clojure
(let [v1 [4 5]
v2 [6 7]]
`(apply ; <1>
~'+ ; <2>
~(+ 1 2) ; <3>
~@v1 ; <4>
~v2)) ; <5>
```

1. Here:
1. The _syntax quote_ \` in front of the target expression
2. Any symbol becomes fully quoted, i.e. including the namespace it belongs to (here we thus get `clojure.core/apply`)
2. We can use the `~'` trick to ensure a symbols is left as-is, unqualified
3. An expression following `~`(unquote) is evaluated normally (thus, we end up with `3` in the output)
4. The _splicing unquote_ is replaced with all the elements of the sequence given to it; contrast this with 5.
5. Here, unquote evaluates `v2` and replaces it with its value, the whole vector

This is everything you may need to know to wrote any Wolframite expression.

## Clojure threading macros

Nested expressions such as

```clj
(* (/ (- 100 20) 4) 3)
```

need to be read from inside out, which is hard. Clojure offers so called threading macros, primarily `->` and `->>`, which enable us to reverse this and write them in the inside out order. Essentially, it evaluates each expression one by one, and inserts its value into the next one, either as the first (`->`) or last (`->>`) argument:

```clojure
(-> 100
(- 20) ; same as (- 100 20) ;=> 80
(/ 4) ; same as (/ 80 4) ;=> 20
(* 3)) ; same as (* 20 3)
```

If we instead used the _thread last_ macro:

```clj
(->> 100
(- 20) ; same as (- 20 100) ;=> -80
(/ 4) ; same as (/ 4 -80) ;=> -1/20
(* 3)) ; same as (* 3 -1/20)
```

There are other, similar threading macros: `some->` and `some->>` (stops at the first nil, for nil-safe operations).