What We'll Cover
- Lisp
- Data Handling in Clojure
Lisp
List Processing
THIS IS A VERTICAL SLIDE
Code is Data. Data is Code.
It turns out that Lisp source code is actually a valid data structure called an abstract syntax tree
(AST). This makes metaprogramming easier since code can be treated as a basic data structure that
the programming language knows how to access.
Metaprogramming?
It means that a program could be designed to read, generate, analyse or transform other programs,
and even modify itself while running
Homoiconicity
basically means the program structure is similar to its syntax, and therefore the program’s internal
representation can be inferred by reading the text’s layout. What this allows you to do is extend
the functionality of the language and manipulate it on the fly at runtime. In other languages, we
would have to recompile from human-language source code into valid syntax tree before it is compiled into code.
THIS IS A VERTICAL SLIDE
Homoiconicity Example
(+ 2 4)
; => 6
This adds 2 and 4 but it is also list of 3 items in a valid data structure.
(def example (list '+ (+ 1 1) (+ 2 2)))
; => (+ 2 4)
(eval example)
; => 6
(read-string "(+ 2 4)")
; => (+ 2 4)
(eval (read-string "(+ 2 4)"))
; => 6
Macros and Clojure
So what are some of the benefits of a language being homoiconic?
It makes it relatively easy to write DSLs. One of the more powerful effects of being homoiconic
though is the ability to define custom macros.
Macros in essence allow you to define new language features as a developer. In most languages you
would need to wait for a new release of the language to get new syntax – in Lisp (and Clojure) you
can just extend the core language with macros and add the features yourself.
'when' macro
(when (true) (println “hello”))
; => “hello”
(macroexpand ‘(when (true) (println “hello”)))
; (if (true) (do (println “hello”)))
(defmacro when
"Evaluates test. If logical true, evaluates body in an implicit do."
{:added "1.0"}
[test & body]
(list 'if test (cons 'do body)))
Aren't macros just functions?
You could do something similar with functions, but it’s not the same. Functions execute at run-time,
they take and produce data (values). Macros execute at compile-time, they take and produce code.
Macros arguments are not evaluated and their return values are expanded-in-place and treated as code.
Functions
Macros
Execute at runtime
Execute at compile-time
Take and produce data (values)
Take and produce code
So Clojure is a Lisp... What else?
Data Manipulation Guarantees
- The old version remains accessible
- The new version can be produced efficiently
- Both versions satisfy the complexity guarantees
The new version can be produced while meeting the time complexity guarantees of the data structure.
For example, adding to the end of a list should take constant time.
Some people have attempted in the past to cheat where the latest version is as fast as you would
expect but older versions sort of decay. There is no cheating going on in Clojure so even old versions meet
the complexity guarantees.
How?
- Data structures are hash array mapped tries
- Shallow with high branching factors
- Structural Sharing
is an implementation of an associative array that combines the characteristics of a hash table and an
array mapped trie. It is a refined version of the more general notion of a hash tree.
draw a HAMT
What does it mean to add to the end?
1. Take the last array, copy it (bounded size of 32 so it's constant time)
2. copy the path to the root
3. New root, new path to the right side, but everything else is shared
So the left trie has a million items in it, and you only change 3 nodes on the right side to add
something to the end.
What does this mean?
So instead of saying "change this thing in place", you say "call this transformation function which
gives a new thing and give me that".
That changes your life. Now you can share these things in a way that you could never before because
with an object, you don't know if you have to clone it, if it's going to change, etc.
Structure of your program
C#, Java, etc.
Clojure
Direct references to things that can change
Indirect references to thing that never change
Concurrency is all convention, all manual
Concurrency semantics are built in
Traditionally composed of direct references to things that can change. As soon as you've done that, you are
stuck with locking and managing synchronization. Why is this bad? Everything is manual and everything is
convention (we're locking A before B, we're locking alphabetically, there's a lock over here for these 3 things,
don't forget to lock before you do this). Where does all this go?
Contrast with Clojure: Rather than having things actually change, you have indirect references to things that
can never change. But those references can be made to refer to other things over the course of the application.
Gives the same effect of changing over time. Clojure has concurrency semantics for these references. Because
they are indirect, your program isn't just twiddling them, your program has to use functions to make them
point to different things. And those functions have concurrency semantics. Which means that they're automatic,
the language helps you change the things to point to other things and not mess that up, and it's enforced. For
instance, some references can only be changed inside a transaction; if you try to change it outside a transaction
it throws an exception.
Big difference - all convention, all manual. Clojure - things never change. If you dereference one of the references,
you get an immutable object. If someone makes that reference point to something else while you're doing calculations, do you care? No.
There are also no locks in the application program.
Review
- Lisp (List Processing)
- Structural Sharing
- Concurrency differences
Clojure
Created by Andrew Scott