A Friendly Introduction to Racket
geometridae.bearblog.dev
geometridae.bearblog.dev
(list '(1. . #\#)
-5/6+7.s-8i
`(1 ,@2)
1@1 ;hmmm, no unquote splicing comma ;-)
10# ;surprised?
(list #i+1 +1i 1+i) ;complicated or complex?
#e-1e10i ;Old MacDonald?
"(* 9 10)" #())Not necessarily a bad thing but also not exactly what I would call "expressive".
You may be able to build anything you want --- which may be efficient when communicating with the machine. But I wonder about now well this would communicate with other programmers.
I won't respond further.
I am deeply jaded by my experience, but as you seem to imply your experience is different I am glad for you.
Still, far less than most other programming languages, virtue of having parentheses making things unambiguous. Outliers are Smalltalk and maybe (?) Forth.
What is "it"? The GP clearly doesn't understand that code and utterly misconstrues its nature and the nature of the Racket/Scheme/LISP language. If someone has zero knowledge of the topic at hand (even when it's a seminal programming language that's been around for nearly 70 years), they have an obligation to at least glance at TFA before commenting.
> the claim of "no special syntax" is unfortunately not true
That's a completely different issue than the one I addressed. (And I really don't think the point is made in good faith--the sense in which that phrase was meant is well understood, certainly by the GGP who is so very familiar with Racket's token syntax and quasiquotes.)
I won't respond further.
It was far less complex to code, and human readable. Unicon was pretty cool also, but never became popular. Erlang/Elixir will probably become more relevant as people move into massively parallel constraint solvers.
The monolith centric languages are simply no longer appropriate for many classes of problems. =3
Like it or not, popularity is a significant attribute if you have to work with others.
import cmath
from fractions import Fraction
And then have to write numbers as strings to input them. From their examples: >>> Fraction('1.414213 \t\n')
Fraction(1414213, 1000000)
>>> cmath.sqrt(-2-0j)
-1.4142135623730951j
Not really great, but workable. I don't like having to import things for basic math stuff like exact numbers, but having a lot of special syntax is maybe also not that great. I am guessing except for maybe macros, there is no other way, if one wants to take input (code) literally as exact numbers in all cases. If one were to just write down a number like `1.414213562373`, and expect it to be exactly represented in the program, then it cannot be read as a floating point number first and then converted into an exact number internally, because reading it as floating point number could already lose precision. So this kind of logic to interpret exact numbers would have to live somewhere before that. So it is in the reader. I am not sure how one could employ a macro instead, and then maybe access the digits of a written number, or whether that is possible. I think it should be possible. If it is, then of course things could be made into macros, after which one could use something like: (exact 1.4142135623730951)
[1]: https://docs.python.org/3/library/cmath.html
[2]: https://docs.python.org/3/library/fractions.html #i is the prefix for inexact
#e is the prefix for exact
So `#e0.1` is exact 0.1 which means it will be read as an exact fraction 1/10
and `#i0.1` will be read as a floating point number.The `i` in `8i` is the imaginary unit. The `1@1` is the polar notation for complex numbers.
The `10#` is a compatibility remain (the Scheme authors wanted a way to see how many signifant digits were known. In `10#` there 2. So `#` stands for "some digit". In most implementations this is read as 0. More details at [1]
An `#` followed by a list datum is read as a vector. Thus `#()` is an empty vector and `()` is an empty list.
[1] https://stackoverflow.com/a/10936403/23567
Complicated? Well, to support both inexact (floating point) and exact numbers (bignums and fractions) it makes sense to have `#i` and `#e` to explicitl choose. The default is (more or less): numbers with . are inexact the others are exact.
This was a wild ride. I'm torn on whether this feels like the Reader is bloated/polluted or whether this is the coolest numerical parser I've ever used. You can really do a lot to coerce the these numbers just by inserting an {o,s,e,i,b,#} into the number, with different semantics depending on the position of the character.
Somewhat overwhelming, I can only hope there's some elegance underlying it all...
'#0=(0 . #0#)
...and I should have included `#|block comments|#`, datum `#;comments`, and probably `|symbols with spaces|`. (let ((|1 + 2| (+ 1 2)))
(display |1 + 2|)) #lang racket/base
(let ((a '#0=(0 . #0#)))
(display (car a)))
...over athttps://onecompiler.com/racket
...and it didn't compile, complaining:
read-syntax: `#...=` forms not enabled for `read-syntax` mode
...maybe there is a switch needed to enable it? #lang racket
(let ((a (read (open-input-string "#0=(0 . #0#)"))))
(display (car a)))
I think the problem is that `read` / `write` support shared data constructed using `shared`. But programs (read by `read-syntax` are not allow to have cycles.https://docs.racket-lang.org/reference/Reading.html#%28def._...
#lang racket
(read-syntax-accept-graph #t)
(let ((a '#0=(0 . #0#)))
(display (car a)))
...which of course doesn't work, because `(read-syntax-accept-graph)` is a run-time thing, and the circular-list structure is a `read`-time thing.I couldn't figure it out with this either:
https://stackoverflow.com/questions/51942188/read-syntax-for...
...just to make sure I wasn't going crazy, this:
(let ((a '#0=(0 . #0#)))
(display (car a)))
...does work as expected with this online scheme interpreter:That read-syntax doesn't just accept everything accepted by plain read is one of my biggest annoyances with Racket.
Obviously the more you use the language these go away but still at least to me it doesn't seem to be a coherent syntax.
It's always surprising to me how many things Scheme leaves undefined. Other examples are evaluation order (in some rather surprising places) or the value of: (make-bytevector 4)
> `(1 ,@2)
'(1 . 2)
This is a pair. The notations is called "dotted pair".> If an (unquote-splicing <expression> ...) form appears inside a <qq template>, then the <expression>s must evaluate to lists; the opening and closing parentheses of the lists are then “stripped away” and the elements of the lists are inserted in place of the unquote-splicing form.
r3rs, r4rs & r5rs (section 4.2.6):
https://standards.scheme.org/official/r3rs.pdf
https://standards.scheme.org/official/r4rs.pdf
https://conservatory.scheme.org/schemers/Documents/Standards...
"If a comma appears followed immediately by an at- sign (@), then the following expression must evaluate to a list;"
r6rs (section 11.17):
https://standards.scheme.org/official/r6rs.pdf
"If an (unquote-splicing 〈expression〉 . . . ) form appears inside a〈qq template〉, then the〈expression〉s must evaluate to lists;"
r7rs (4.2.8):
https://standards.scheme.org/official/r7rs.pdf
"If a comma appears followed without intervening whitespace by a commercial at-sign (@), then it is an error if the following expression does not evaluate to a list;"
I’d also encourage you to give it a try and have some fun with it! I’m using it for some of the 3D demos in my book. It’s definitely not a silver bullet, but I’ve found it can be pretty productive.
Personally, my favorite Lisps are Racket, Common Lisp, and Clojure, in that order. I’ve also been tempted to give Chicken Scheme a try! :)
Nice touch. Explains how Caine returns in episode 9: Lisp continuations allow for graceful error recovery.
GLS moved to CMU in the early 80's... I remember taking "Comparative Programming Languages" from him. Good teacher and impressive guy. In addition to the obvious, he also covered SNOBOL and APL in that course. Memories :-)
When an introduction says “friendly”, I don’t expect it to assume that I know what lambda is.
When an introduction says “friendly”, I don’t expect syntax rules to appear in it. At all.
Wtf kind of intro to a programming language doesn’t include syntax rules? We are all informed, experienced, technologically inclined people here. Not dumb children.
I’m struggling to see your comment as anything more than contrarian babble.
PP could have been more clear and say syntax-rules (note the dash), which means macros. Macros are usually considered an advanced topic.
Given that most languages don’t have syntax rules or anything like it, I’m gonna go out on a limb and say that most language introductions/overviews/tutorials don’t mention it.
In fact, what languages can you say do not have syntax rules?
Since macros are a very important part of Racket, complaining that a friendly introduction mentions them is [insert non-friendly characterization].
https://users.cs.northwestern.edu/~robby/logos/
The skull didn't last long.
The Rocq Prover implements a high-level program specification and mathematical language called Gallina that is based on an expressive formal language called the Polymorphic, Cumulative Calculus of Inductive Constructions that itself combines both a higher-order logic and a richly-typed functional programming language. Through a vernacular language of commands, the Rocq Prover allows:
to define data structures, functions or predicates, that can be evaluated efficiently;
to state mathematical theorems and software specifications;
to interactively develop formal proofs of these theorems;
to machine-check these proofs by a relatively small certification "kernel";
to extract certified programs to languages like OCaml, Haskell or Scheme.
As a proof development system, the Rocq Prover provides interactive proof methods, decision and semi-decision algorithms, and a tactic language for letting the user define its own proof methods. Connection with external computer algebra systems or theorem provers is available.As a platform for the formalization of mathematics or the development of programs, the Rocq Prover provides support for high-level notations, implicit contents and other mechanisms for formalization at scale.
Misses out on LispWorks and Allegro Common Lisp for the Common Lisp entry, which contrary to SBCL provide a similar graphics experience as the Lisp machines of yore, here missing out on TI and Xerox as well, Genera wasn't alone.
Are there seriously any emacs users who are completely unaware of elisp? Isn't elisp the entire point of picking emacs?
I have not had a single coworker, who has even started to learn these things, even after I have presented a Scheme on a Friday afternoon, and showed off some cool threading/pipeline syntax-rules macro, and showed how this compared to Python, showing how it reduces boilerplate in a way impossible to do in Python.
Granted, this is in Germany, where CS education is not really all that great, and most universities teach boring Java or C++ or something.
At least we had a RacketCon in Berlin some time ago, and I was fortunate enough to have a prof at university, who at least introduced Dr.Scheme. His motto was: "I am not here to teach you C, Java, or Python. I am here to teach you computer programming." (translated) He went on to introduce languages of various paradigms, from Prolog, to Dr.Scheme (predecessor of Racket), to C, and Python. Thank you Prof. von Löwis.
GOAL and GOOL were used in Jak and Dexter, Crash Bandicoot, and apparently brought back in The Last of Us:
https://en.wikipedia.org/wiki/Game_Oriented_Assembly_Lisp
Unsynced: The Last of Us Melee System
https://www.youtube.com/watch?v=Ox2H3kUQByo&t=2260s
In this 2014 GDC session, Naughty Dog's Anthony Newman shares an overview of the design and implementation of the melee system for The Last of Us.
https://opengoal.dev/blog/progress-report-sept-2020
The OpenGOAL project is a project to reverse engineer the Jak and Daxter video games and the custom programming language they were developed in: GOAL.
The long-term objective of the project is to make Jak and Daxter run natively on a PC by decompiling and recompiling it with OpenGOAL, our reverse engineered version of GOAL for x86-64 PCs. We do not host any game assets or copyrighted material from the game – we only host tools for reverse engineering and porting the game. The user must provide a copy of the game to use our tools.
You can read the about it here: https://defn.io/2020/01/04/remember-internals/
In teaching our material we use a dialect of the programming language Lisp. We never formally teach the language, because we don’t have to. We just use it, and students pick it up in a few days. This is one great advantage of Lisp-like languages: They have very few ways of forming compound expressions, and almost no syntactic structure. All of the formal properties can be covered in an hour, like the rules of chess. After a short time we forget about syntactic details of the language (because there are none) and get on with the real issues—figuring out what we want to compute, how we will decompose problems into manageable parts, and how we will work on the parts. Another advantage of Lisp is that it supports (but does not enforce) more of the large-scale strategies for modular decomposition of programs than any other language we know. We can make procedural and data abstractions, we can use higher-order functions to capture common patterns of usage, we can model local state using assignment and data mutation, we can link parts of a program with streams and delayed evaluation, and we can easily implement embedded languages. All of this is embedded in an interactive environment with excellent support for incremental program design, construction, testing, and debugging. We thank all the generations of Lisp wizards, starting with John McCarthy, who have fashioned a fine tool of unprecedented power and elegance.
See: https://en.wikipedia.org/wiki/Arc_(programming_language)#His...
https://paulgraham.com/avg.html
In the summer of 1995, my friend Robert Morris and I started a startup called Viaweb. Our plan was to write software that would let end users build online stores. What was novel about this software, at the time, was that it ran on our server, using ordinary Web pages as the interface.
A lot of people could have been having this idea at the same time, of course, but as far as I know, Viaweb was the first Web-based application. It seemed such a novel idea to us that we named the company after it: Viaweb, because our software worked via the Web, instead of running on your desktop computer.
Another unusual thing about this software was that it was written primarily in a programming language called Lisp. It was one of the first big end-user applications to be written in Lisp, which up till then had been used mostly in universities and research labs. ...
I honestly am curious. I've seen a few examples presented for it, but they always seem like bad software engineering to me. Where's an example that does something in a cleaner way than alternatives present in other languages while remaining compatible with local reasoning?
You can abstract everything away. Not like Haskell where laziness accounts for some and typeclasses for some (and often an exponential growth in compile times). No, it property let's you change the language.
I got tired of loops sucking and made this, for example: https://rikspucko.koketteriet.se/bjoli/goof-loop
Chez compiles a project of about 35000 lines (with heavy macro usage) in less than 0.5s on -O2.
That's a perspective. If you're looking for a low-level language, then Scheme isn't it. (Forget iconicity - Scheme is garbage-collected. And supports continuations!)
If you don't program in machine code - which would maximize local reasoning - then you must know the language with the Correct balance of local reasoning and higher-level constructs. Knowing which language that is would add specificity to this discussion...
Assembly makes non-local reasoning mandatory, as any code can update any location in memory without restriction. All memory accesses are global. References need not even be by name - they can be via computed addresses. There are no restrictions in place allowing the structure of the program to provide boundaries on what pieces of code may be understood as units.
Mutation in scheme is possible, via set!, and set-car! and set-cdr!, but it's not recommended. Functional program design side-steps the issue.
see also:
SICP: 3.1.3 The Costs of Introducing Assignment
https://sarabander.github.io/sicp/html/3_002e1.xhtml#g_t3_00...
Coalton is an efficient, statically typed functional programming language that supercharges Common Lisp by taking great ideas from Haskell, Scheme, and OCaml.
https://coalton-lang.github.io/
Not well known, but surely good software engineering adding features. Lisp* is called the Programmable Programming Language for a reason.
pg wrote a treatise on Lisp macros (free download):
https://www.paulgraham.com/onlisp.html
(add to that something about great power - great responsibility and not holding it wrong)
Pre-Scheme is a statically typed dialect of the Scheme programming language, combining the flexibility of Scheme with the efficiency and low-level machine access of C. The compiler uses type inference, partial evaluation, and other correctness-preserving transformations to compile a subset of Scheme into C with no additional runtime overhead. This makes Pre-Scheme a viable alternative to C for programming virtual machines, operating systems, and embedded systems where the runtime overhead of a complete Scheme implementation is not desirable.
https://ryansuchocki.github.io/microscheme/
Microscheme, or (ms) for short, is a functional programming language for the Arduino, and for Atmel 8-bit AVR microcontrollers in general. Microscheme is a subset of Scheme, in the sense that every valid (ms) program is also a valid Scheme program (with the exception of Arduino hardware-specific primitives). The (ms) compiler performs function inlining, and features an aggressive tree-shaker, eliminating unused top-level definitions. Microscheme has a robust FFI (Foreign Function Interface) meaning that C code may be invoked directly from (ms) programs. Therefore, the power of the existing wealth of Arduino libraries is available within Microscheme.
CRUNCH is an embedded compiler for a statically typed subset of R7RS Scheme, generating C code. The compiler uses type inference to decorate the code with type information without requiring declarations. CRUNCH can be used to translate embedded Scheme code sections, whole programs or multiple source modules into standalone executables or compiled code that can be invoked from Scheme.
The generated C code uses a small runtime-system contained completely in a single C header file. Reference counting is used for managing aggregate data like strings which removes the need for full tracing garbage collection or manual memory management while still having a relatively small overhead.
Since more or less a direct translation of Scheme to C is done, the generated code should run at roughly the same performance as C. No type-checking takes place as the types of all values have been inferred at compile time, and values are not tagged. With the exception of reference counted objects there is no additional runtime overhead and Scheme and C can directly interchange data. This makes CRUNCH very appropriate for writing programs that need a maximum of speed or that are target for constrained environments like deeply embedded systems. The code is portable to all systems that at least have a C compiler.
UNICODE strings are supported and can optionally be disabled for improving performance and reducing code size.
CRUNCH is heavily inspired by PreScheme, the low-level compiler that is originally part of the Scheme48 project. In fact, CRUNCH can be considered a modern reimplementation of PreScheme written in and for use with CHICKEN.
See also:
Crunch – a Scheme compiler with a minimal runtime (more-magic.net)
190 points by sjamaan on Dec 17, 2024 | hide | past | favorite | 72 comments
https://news.ycombinator.com/item?id=42440767If you don't like it, you can try https://rhombus-lang.org/ that is build on Racket and also has macros but uses a Python-like syntax.
Well, getting the interaction between modules and syntax transformations (macros) right is not an easy task.
"Composable and Compilable Macros: You Want it When?" Matthew Flatt http://dl.acm.org/authorize?24908
If the term has any meaning, it's about how program source code can be represented and manipulated easily by the user. (And I say 'can' deliberately, because even Lisps still allow you to treat source code as a big flat string, if you want to.)
I don't think homoiconicity is all that much of a useful concept. It's very hard to pin down. Eg C can represent its own source code, too, if a bit clunkily. And Lisps generally don't execute by walking over s-expressions: their internal representation of their own logic typically uses more sophisticated structures (and adding native code compilers to the mix complicates matters further).
These sorts of shallow complaints can be made of any feature than one is not familiar with, hasn't used, and doesn't know or understand the benefits of. In any case, no one is forcing people to use this language or take advantage of this feature.
P.S. The "response" actually ignores all points made, attacks strawmen, moves the goalposts, and is intellectually dishonest (the original comment was clearly a complaint, and besides it wouldn't matter if some other word like "criticism" or "dissatisfaction" were substituted--my point remains). Again, no one is forcing anyone.
The point is that these things are subjective, and there will never be a programming language that everyone likes the best.
Of course, you can. C is perfectly capable of writing C interpreters and compilers.
> And even if you could, strings + eval alone is not homoiconicity--programs are not manipulated by the compiler as unstructured text strings.
Lisps (typically) don't execute by walking over s-expressions, either. Lisp interpreters and compilers use more sophisticated representations.
Is the standard library part of the language? Would a variant of C that came with an interpreter in the standard library (but no other changes) count as homoiconic?
Homoiconicity is a fundamental language property, not an algorithm nor an implementation issue. Lisp implemented in Pascal is still homoiconic. C is not homoiconic, whether the compiler is implemented in C or in Pascal, and C cannot be made homoiconic without adding fundamentally new and different constructs to the language definition.
Further sources:
https://wiki.c2.com/?HomoiconicLanguages
https://wiki.c2.com/?HomoiconicExampleInManyProgrammingLangu...
The syntax lends itself to easy structural editing way before LSPs..
Why Racket? Why Lisp?
https://beautifulracket.com/appendix/why-racket-why-lisp.htm...
Why language-oriented programming? Why Racket?
https://beautifulracket.com/appendix/why-lop-why-racket.html
Creating Languages in Racket: Sometimes you just have to make a better mousetrap.
https://queue.acm.org/detail.cfm?id=2068896
Lisp is clay: the power of composable DSLs
https://fosdem.org/2026/schedule/event/HDE7JZ-lisp-is-clay/
The other powers of Lisps (interactive development, hot reloadability, restarts on error, etc) are more tied to the sophistication of their implementations and runtime environments.
I feel like the good parts of lisps have been adopted by typed languages...
> For decades, Lisp was the language of artificial intelligence. [...] Then came the "AI winter," funding dried up, and Lisp went from star to cult language.
Lisp had fallen from relevance before then. Only the United States was still using it, and mostly out of technical debt and a stubborn refusal to move on. Prolog displaced it in the late 1970s, and even within the US, the Lisp part was an unfortunate implementation detail to get to a Prolog-shaped object. It's not surprising that the US fell way behind Japan in this area in the 1980s. Anybody who would imply they would take a Lisp machine over a PIM is either not interested in symbolic computation and just likes Lisp, or they're doing so out of total ignorance of how much more advanced the PIMs were for symbolic AI.
The cutting edge was with Prolog, and that's still the case today. Nobody's researching MIL in Lisp, even in America.
Anyways the embedded inference engine as an idea failed a long time ago, Americans convinced themselves it was the way to do things and just refused to ever let it go. It's just extra baggage on the important bit, which is the symbolic computation. miniKanren has a niche more in line with a theorem solver.
Prolog is one of the two classic languages for symbolic artificial intelligence programming (the other classic language being Lisp). Prolog excels at implementing symbolic rule-based systems in which declarative knowledge is encoded in first-order logic. The language is optimized for expressiveness and efficiency for these types of applications, sometimes at the expense of logical purity. For example, by default Prolog does not use the "occur check" in unification. From a math/logic standpoint, this version of unification is incorrect. However, the occur check is expensive, and in most cases the lack of the occur check is not a problem. This is a very pragmatic design decision, as is Prolog's use of depth-first search, and use of cut (!) to control backtracking. I'm sure these decisions were absolutely necessary when running on the hardware of the 1970s, and today are very useful when working on large problems, and when dealing with huge (often infinite!) search spaces.
Prolog supports many "extra-logical" or "non-logical" features, including cut, assert and retract, projection of variables for arithmetic using is, and so forth. Many of these features make it easier to express complex control flow, and to manipulate Prolog's global database of facts. One very interesting feature of Prolog is that Prolog code is itself stored in the global database of facts, and can be queried against at run time. This makes it trivial to write meta-interpreters that modify the behavior of Prolog code under interpretation. For example, it is possible to encode breadth-first search in Prolog using a meta-interpreter that changes the search order. This is an extremely powerful technique that is not well known outside of the Prolog world. 'The Art of Prolog' describes this technique in detail.
Tremendous effort has gone into improving Prolog implementations, most of which are based on the Warren Abstract Machine (WAM). The WAM uses a side-effecting model in which values are destructively assigned to logic variables, with these side-effects being undone upon backtracking. Many features can be added to Prolog by extending the instructions of the WAM. One disadvantage of this approach is that Prolog implementation papers can be difficult to read without a solid understanding of the WAM. On the other hand, Prolog implementer have a common model for discussing implementation issues. There has been a great deal of research in parallel Prolog, culminating in Andorra Prolog in the 1990s. At least some of these ideas live on in Ciao Prolog. (Ciao Prolog is full of interesting ideas, many of which go far beyond the Prolog standard.)
Prolog has a beautiful unification-based "pattern-matching"-style syntax that results in very succinct programs. Prologers love their syntax, just like Lispers love their s-expressions. Prolog also has a large library of standard predicates. Due to all of the engineering that has gone into making the WAM fast, there are very capable and mature Prolog implementations. As a result, many large knowledge-based systems have been written entirely in Prolog.
So, why do you say that Prolog is a "poor general purpose language"?
This visualization of relative programming language popularity shows Lisp was the 3rd most popular general purpose language in Q1 of 1984 behind Pascal (a learning language?) and C.
(Prolog shows at 12th, then drops off the chart)
Most Popular Programming Languages: Data from 1958 to 2025
https://youtu.be/ZTPrbAKmcdo?t=116
Gabriel's perspective from 1990:
Lisp: Good News, Bad News, How to Win Big - Richard P. Gabriel - Lucid, Inc
> This visualization of relative programming language popularity shows Lisp was the 3rd most popular general purpose language
Which is fine, and makes sense. But I'm addressing a claim about Lisp being "the Language" for the era of symbolic AI. Which is decidedly not high volume, nor is it general at all, it's really quite specific.
I'm correcting it, because I know Americans are heavily culturally silo'd on this, and symbolic AI is especially esoteric. Most people probably can't name a single commercial expert system off the top of their head, and that's not unreasonable. Our cultural view of symbolic AI is rife with American exceptionalism (I theorize partially because of government involvement) that just doesn't track with reality. Speaking as an American.
The ranking then was due to Turbo Pascal which came out in November 1983. It was probaly the first IDE and compiler on PCs
.-.
/ λ \
'---'I first met Common Lisp during my college years in early 2000s: we had an enthusiast teacher who taught it for free, in his free time; it was interesting for me as a freshman to learn something new, but also I literally had no idea how to write Lisp code properly, without resorting to using `progn` here and there. For those not aware, `progn` is a form that allows you to write expressions in a sequential way, pretty much like you use the C compound statement, a `{ ... }` block. It works, and after some time you find that you're writing in some crappy imperative programming language with a lot of brackets but you don't learn much about the functional programming.
It took me more than a year, and definitely more than any short “friendly introduction”, to understand the functional approach.
Now, Racket. I first noticed it while doing my daily LeetCode; they added Racket as a supported language and you can go use it to solve a task. After not using Lisp for 20-something years, I appreciated the opportunity, but I found the type contract syntax infuriating. This is LeetCode #1, “Two sum”:
(define/contract (two-sum nums target)
(-> (listof exact-integer?) exact-integer? (listof exact-integer?))
)
I don't know what you feel, but this makes me want to close this tab and never return. So if I find myself wanting to write some Lisp and there's a daily LeetCode task I want to solve, the first thing I do is I delete all this garbage and come back to the clean option: (define (two-sum nums target) ...)
which, except for the minor syntax differences, brings me back to my college Lisp years and I can actually enjoy it.For example, here's a simple CL macro:
(defmacro defer (cleanup &body action)
`(unwind-protect (progn ,@action) ,cleanup))
`unwind-protect` is like a `try/finally` in other languages, and I used that above to create something similar to `defer` in Go/Zig which is related to it, but with the operands inverted.The ` symbol is a quasiquote. Unlike quote `'` it lets you unquote symbols inside with the , operator. That's why you'll always see a bunch of '`' and ',' in macros.
The @, thing is a "spread" (looks different in Racket from what I saw in the post, which used `...`). It just spreads whatever was on the list in the place you put that on, so if `action` is `(p 1) (p 2)`, then `(progn ,@action)` becomes `(progn (p 1) (p 2))`.
You can inspect what the actual code that will be compiled looks like with `macroexpand`:
CL-USER> (macroexpand '(defer (cleanup) (do-something)))
(UNWIND-PROTECT (PROGN (DO-SOMETHING)) (CLEANUP))
`progn` is a "special operator" that's needed when you want more than one expression to be evaluated in order.Example calling the macro:
CL-USER> (defer (print "done")
(print "hello")
(print "again"))
"hello"
"again"
"done"
As you can see, it executed the deferred expression last.We can prove that macros disappear after compile-time with an example:
CL-USER> (disassemble (lambda (x) (+ x x)))
; disassembly for (LAMBDA (X))
; Size: 36 bytes. Origin: #x8005F70664 ; (LAMBDA (X))
; 64: AA0A40F9 LDR R0, [THREAD, #16] ; binding-stack-pointer
; 68: AA0B00F9 STR R0, [CFP, #16]
; 6C: EA030CAA MOV R0, R2
; 70: EB030CAA MOV R1, R2
; 74: 297E80D2 MOVZ TMP, #1009
; 78: 5E6B69F8 LDR LR, [NULL, TMP] ; SB-KERNEL:TWO-ARG-+
; 7C: DE130091 ADD LR, LR, #4
; 80: C0031FD6 BR LR
; 84: E00120D4 BRK #15 ; Invalid argument count trap
You can see the Assembly is very simple for `(+ x x)` (the ADD instruction plus a bunch of stack/error maintenance).If we instead had a macro that did this:
(defmacro my-macro (x) `(+ ,x ,x))
And a function used that: (defun f (x) (my-macro x))
Now, disassembling the function: CL-USER> (disassemble #'f)
; disassembly for F
; Size: 36 bytes. Origin: #x8005C00374 ; F
; 74: AA0A40F9 LDR R0, [THREAD, #16] ; binding-stack-pointer
; 78: AA0B00F9 STR R0, [CFP, #16]
; 7C: EA030CAA MOV R0, R2
; 80: EB030CAA MOV R1, R2
; 84: 297E80D2 MOVZ TMP, #1009
; 88: 5E6B69F8 LDR LR, [NULL, TMP] ; SB-KERNEL:TWO-ARG-+
; 8C: DE130091 ADD LR, LR, #4
; 90: C0031FD6 BR LR
; 94: E00120D4 BRK #15 ; Invalid argument count trap
Same thing exactly.I don't know Racket, but knowing it can compile to binary, I expect macros in Racket would work exactly the same.
Racket ---expand the macros---> Simplified Racket "Kernel"
Simplified Racket "Kernel"---schemify---> Rumble (that is Chez Scheme + a few libraries and macros)
Rumble ---expand the macros---> Simplified Chez Scheme
Simplified Chez Scheme ---constants propagation/folding/other---> Simplified Chez Scheme
Simplified Chez Scheme ---compiler---> native code
You can see the result with https://pkgs.racket-lang.org/package/disassemble