Skip to content

Instantly share code, notes, and snippets.

@countvajhula
Created December 11, 2024 02:58
Show Gist options
  • Select an option

  • Save countvajhula/6c7af5867867c59321fc1aadf40a6b3c to your computer and use it in GitHub Desktop.

Select an option

Save countvajhula/6c7af5867867c59321fc1aadf40a6b3c to your computer and use it in GitHub Desktop.
#langs that fit in your head -- diff from initial version
-(Written for the Racket community for Day 8 of the Racket Advent Calendar)
+This post was written for Day 8 of the Racket Advent Calendar. “#lang” is a Racket term referring to declaring the programming language in use, and this post uses Racket as a platform to discuss one approach to writing good programming languages. I’ve made an effort to explain the Racket-specific ideas as they are introduced, so if you are interested in language design but aren’t a Racketeer, I hope you’ll still read it and that you’ll find it interesting.
When a way of expressing a complex idea becomes small enough to fit in your head and doesn’t require special effort on your part to remember, it has, in a useful sense, become a language.
-Take Racket's module system, for example. By just writing "require," which is of course what we mean to do, we gain the ability to describe with great and effortless nuance in just what manner we mean to include those modules -- whether to retain the names of the included definitions or change them in some particular way, or include or exclude particular definitions, and more. And these various ways are composable, so we could do, without any fuss:
+Take Racket’s module system, for example (which, just like those of other languages, allows you to group source code into units typically contained in files). By just writing “require” (like python’s import), we gain the ability to describe with great and effortless nuance in just what manner we mean to include those modules — whether to retain the names of the included definitions or change them in some particular way, or include or exclude particular definitions, and more. And these various ways are composable, so we could do, without any fuss:
(require (prefix-in r: (rename-in (only-in racket/list range)
[range ronge])))
(r:ronge 1 10 2) ;=> '(1 3 5 7 9)
@@
Now, it’s easy to get the wrong idea here, and think that a language necessarily means something very clever, if we recall the example of languages like Common Lisp’s LOOP (and, I think, some stylistically similar ones nowadays like, I am told, Cucumber). Those languages are powerful and expressive, but they present the appearance of human language-like fluency without actually exhibiting that level of flexibility. So they end up just feeling gratuitous and fragile, idiosyncratic languages you have to learn though they resemble something you already know. Yeah I know, shots fired. Ouch. Why are people stoning me??? I hath no sin in my heart!!!
-(Disclaimer: I've never used either loop or cucumber!)
+(Disclaimer: I’ve never used either loop or cucumber! So if you feel this is a mistaken view, please feel free to stone, er, chime in in the comments.)
In any case, since we’re going boldly (and foolishly) down this road, please indulge me as I share some modest experiments I have actually done along these lines.
+One of the main projects I work on in the Racket community is Qi, which is a language for expressing flow-oriented computations that is provided as an ordinary library. Like any such language (typically called a domain-specific language or DSL), the way you normally use it in your code is via a macro. Qi’s macro happens to be ☯, so that you can write, in Racket, (map (☯ (~> sqr add1)) (list 1 2 3 4 5)) to square each element in a list and add 1 to it, producing '(2 5 10 17 26). “map” here is just the usual higher-order function familiar from functional programming languages. This function accepts a function as its first argument, which will be used to transform the elements of the list (the second argument).
+As we see here, instead of writing a name of a function in that second position or writing out a lambda inline, we use ☯ to describe the function using Qi, a language purpose-built for describing functions. We are able to do this even though the entire surrounding expression is a Racket expression. That is, we are embedding Qi into Racket here using a macro — a standard approach to DSLs in Racket and beyond.
Not long ago, Ben Knoble developed curlique (pronounced “curli-cue,” for non-native English speakers), a way to embed Qi into Racket syntactically without explicitly writing a macro form, so that something like this works:
-(map {~> sqr add1} (list 1 2 my-var 4 5))
+(map {~> sqr add1} (list 1 2 3 4 5))
-(i.e. note the Qi flow (~> sqr add1) isn't wrapped with a macro like (☯ ...)).
+(i.e. note the curly brackets instead of (☯ …)).
-That got some of us in the community thinking about "#lang qi" -- would it be useful to provide Qi as a top-level language, or otherwise integrate it into the top level language in a seamless way like this?
+That got some of us in the community thinking about “#lang qi” — would it be useful to provide a top-level language (like Racket itself, which is indicated in source code via #lang racket. In the Racket ecosystem, even the Racket language isn’t special — by design!) that seamlessly supports flow-oriented expressions in this way?
This is, perhaps, how the righteous lose their way. The power of macros proves too great, and mortals can’t resist the temptation to just fix every annoyance and make everything just so.
Like, take that map expression above. Always having to type (list ...) is annoying! Why, if only we could use box brackets there, that would be nice, wouldn’t it?
-I had always assumed the reason Racket didn't use box brackets for lists was to preserve the ambivalence about paren shape in other settings like let binding forms.
+I had always assumed the reason Racket didn’t use box brackets for lists was to preserve the ambivalence about paren shape in other settings like let binding forms, where either (let ((a 1)) ...) or (let ([a 1]) ...) bind the variable a to the value 1, and the latter is favored for improved readability.
But Ben pointed out (without necessarily endorsing it) that by using syntax-parse‘s paren-shape parsing, together with Racket’s interposition points (which are really an under-sung superpower of Racket — the fact that even the most basic semantics of the language are facilitated, behind the scenes, using macros that we can override just like we can any other macro), we can have box brackets mean unquoted lists, so that this works:
-(map {~> sqr add1} [1 2 my-var 4 5])
+(map {~> sqr add1} [1 2 3 4 5])
-… even while retaining ambivalence about bracket shape in settings like this:
+… even while retaining ambivalence about bracket shape in binding and other settings:
(let ([a 3]
[b 5])
(map {~> sqr add1} [1 2 a 4 b]))
+Note that a and b above are variables. Usually in Lisp languages if we use '(1 2 a 4 b), called a “quoted list,” a and b would be elements of the list as symbols and not their values. But [1 2 a 4 b] evaluates a and b, which is usually what we mean.
Pretty cool, eh!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment