Last active
September 22, 2022 01:22
-
-
Save boraseoksoon/1d592baa7f3cfde7aabd6707d8464a75 to your computer and use it in GitHub Desktop.
study note for Clojure macro
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; Clojure macro & homoiconicity (LISP) for newbies | |
;; | |
;; This is personal study note | |
;; | |
;; Disclaimer: | |
;; Here I am trying to guide the high level of overview of macro & homoiconicity based on Clojure. | |
;; I cannot say these will be 100% accurate, since I am also still in the middle of getting LISP and Clojure, | |
;; but at least I believe it could portray the big picture | |
;; rather than the technical detail inside, providing links of blogs and articles I found useful. | |
;; Please do your own research when in doubt if interested further by using this as the guidance. | |
;; and please correct me if I am wrong! | |
;; **** What is macro? | |
;; Macro is a just function that transforms `clojure code` into `anther clojure code` | |
;; in macro expansion time (compile time) | |
;; to feed to the evaluator that produces a final result. | |
;; whereas a function transforms values into other values. | |
;; | |
;; In addition, | |
;; LISP property being 'homoiconic' does help macro take LISP meta programming to the next level. | |
;; but 'homoiconicity' will make lisper's life way easier and more elegant when it come to doing meta programming, | |
;; OK, it's time to talk about homoiconicity then. | |
;; **** What is Homoiconicity?: | |
;; > "code is data, data is code" | |
;; This famous quote means | |
;; > a language that has a feature where the representation of code is same as the representation of data | |
;; can be considered "*homoiconic*" | |
;; To be specific, typically, from a high overview, | |
;; steps for compiling a programming language *very roughly* would be | |
;; **"text (source code)"** -> tokenize -> tokens -> parse -> **"AST"** -> do whatever with AST (take effect || IR optimization || down to machine code) | |
;; Here, the form of 'text(source code)' and 'AST' are equivalent so you can have the full aceess to AST | |
;; before run-time during the compile time, allowing us to treat 'AST' purely | |
;; like handling normal Clojure data structure programmatically. | |
;; let's take code examples: | |
;; Clojure code: | |
(+ 1 2) | |
(def res (+ 1 2)) | |
(sum [1 2 3 4 5]) ;; function call | |
;; They are Clojure code but at the same time, the above Clojure code will be tokenized and parsed into AST, which is the equivalent form as the original code, which is also a completely valid AST form. | |
;; When the condition is met, *We call the language "homoiconic"*, and | |
;; That's the meaning of *"code is data, data is code"* | |
;; Let's see other not homoiconic languages code: | |
;; 1 + 2 | |
;; var res = 1 + 2 | |
;; sum([1, 2, 3, 4, 5]) // function call | |
;; The above code (not homoiconic) will be tokenized and parsed into AST, which will be transformed into a completely different AST form from the original code. | |
;; This is good article to read about this : https://www.braveclojure.com/read-and-eval/ | |
;; In the same way you can handle data structure programmatically in other not homoiconic language, you can control your 'AST in Clojure (LISP)' | |
;; Take, for example, Swift: | |
;; [1, 2, 3, 4, 5] | |
;; .filter {$0 % 2 == 0} | |
;; .map { String ($0) } | |
;; Like above, Swift programmers can process, manipulate and handle data programmatically on compile time before run-time. | |
;; In the same sense, Clojure (LISP) programmers can process, manipulate and handle not only data but also 'AST' on compile time before run-time. | |
;; One more time, simply put, you may think of Clojure syntax `(+ 1 2)` as a valid AST (Abstract Syntax Tree), | |
;; A plain clojure code is already the valid AST and you write the `AST` as code and data. | |
;; **** Why macro and homoiconicity? | |
;; Since the form of AST and code are equivalent, You can take meta programming to the max to the point you can add new syntax to the language whereas in other languages, | |
;; for example, Java programmers had to wait for each for years until implemented. | |
;; Imagine you want to add list comprehension syntax into your language inpisred by Python. | |
;; [x for x in range (10)] | |
;; how? you can't in your language although you already know the concept due to the limit of the language. | |
;; In Clojure, This syntax was implemented already using macro by Rich Hickey who is the creator of Clojure. Adding new syntax is like adding a new library in Clojure (LISP) | |
(for [x (range 10)] x) | |
;; https://github.com/clojure/clojure/blob/b98ba84/src/clj/clojure/core.clj#L4590 | |
;; You don't have to wait for laugage designers to implement a new syntax for the unknown amount time. Using macro, LISP can be extended to as much as you can. That's why LISP is called 'programmable programming language' | |
;; | |
;; * How to use macro in Clojure : | |
;; | |
;; 'defmacro' defines a macro while 'defn' defines a function. | |
;; using defmacro and syntax quotes for macro (for convenience if needed) | |
;; | |
;; how to declare defmacro (similar as defn: function) | |
;; | |
(defmacro unless [test then] | |
(list 'if (list 'not test) then)) | |
;; **** Let's take real examples | |
;; Let's take real examples of Clojure. | |
;; Let's say you want to add new syntax to the language as below. | |
;; (1) C style imperative for loop syntax that Clojure doesn't have | |
(defmacro for-loop [[sym init check change :as params] & steps] | |
`(loop [~sym ~init value# nil] | |
(if ~check | |
(let [new-value# (do ~@steps)] | |
(recur ~change new-value#)) | |
value#))) | |
(for-loop [i 0, (< i 10), (inc i)] | |
(prn i)) | |
;; implemented by @mikera: https://stackoverflow.com/users/214010/mikera | |
;; (2) new range syntax close to english | |
(from 0 to 10) | |
(from 0 to 10 by 0.5) | |
(defmacro from | |
[x y z & more] | |
`(when (= '~y '~'to) | |
(if '~more (range ~x ~z (second '~more)) | |
(range ~x ~z)))) | |
;; There are syntax quotes to help make life easier when doing macro programming : | |
;; Syntax quote quote: ` | |
;; Syntax quote unquote: ~ | |
;; unquote splicing: ~@ | |
;; generated symbols: # | |
;; To learn Clojure Syntax quote for Macro: | |
;; Reference: https://blog.klipse.tech/clojure/2016/05/05/macro-tutorial-3.html | |
;; another good site: | |
;; http://bryangilbert.com/post/code/clojure/anatomy-of-a-clojure-macro/ | |
;; **** Comparison with other languages? | |
;; | |
;; In other non-homoiconic languages, for example, | |
;; so C language uses the define macro | |
;; It is the crude string substitution (C language) | |
;; It is of course far from the Clojure macro. | |
;; It just attempts to replace "string" as the preprocessor but nothing else. | |
;; | |
;; #include <stdio.h> | |
;; #define PI 3.1415 | |
;; #define circleArea(r) (PI*r*r) | |
;; int main() { ... | |
;; | |
;; Let's see other not homoiconic languages that supports meta programming well such as Ruby. | |
;; They helps programmer do meta programming well on runtime providing many APIs to replace them on run-time. | |
;; Python, for example, allows developers to have to access to AST anyways: | |
;; >>> program = "(begin (define r 10) (* pi (* r r)))" | |
;; >>> parse (program) | |
;; ['begin', ['define', 'r', 10], ['*', 'pi', ['*', 'r', 'r']]] | |
;; http://norvig.com/lispy.html | |
;; But These languages cannot add the new syntax. | |
;; They cannot do meta programming in the LISP level since they lack "homoiconicity". | |
;; | |
;; | |
;; | |
;; (cont. more examples needed ...) | |
;; **** Macro good to know: | |
;; (1) function vs macro | |
;; You could do something similar with functions, | |
;; but it’s not the same. Functions execute at run-time, | |
;; they take and produce data (values) . | |
;; Conceptually you can replace every function invocation with its value. | |
;; Macros execute at compile-time, they take and produce code. | |
;; Conceptually one can replace (expand) every occurrence of macro with its value. | |
;; Macros arguments are not evaluated and their return values are expanded-in-place and treated as code. | |
;; Yes. But if you can do it with a function, you generally should. | |
;; @refenence https://blogs.mulesoft.com/news/anypoint-platform/code-is-data-data-is-code/ | |
;; Macros are good for | |
;; deferred evaluation; | |
;; capturing forms; | |
;; re-organizing syntax; | |
;; none of which a function can do. | |
;; @reference: https://stackoverflow.com/questions/43973727/clojure-can-macros-do-something-that-couldnt-be-done-with-a-function |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment