In the previous post I was struggling with the noise generated in my code by stack manipulation-related functions. Having reached a dead end, I promised you a shiny new direction, which I’ll be introducing in this post.
To do that, we’ll have to recap one of the Joy basics, namely, its homoiconicity. In the introduction, I mentioned how Joy’s list primitive is actually a quoted program; this should be familiar if you’re acquainted with Lisp lists. So… we can easily go meta on the language by employing list manipulation. Here’s a simple example to illustrate the point:
1
|
|
(i
evaluates the list as a program)
As a real program may be an arbitrarily nested list, manipulating it won’t be that simple. Say I want to turn this piece of code [1 [2 3 +] *]
into [1 [2 3 *] *]
. I can do this manually like so:
1 2 3 4 5 |
|
(which assumes the availability of the insert-at
function we were messing about with the last time)
And obviously, this approach is too clumsy to scale up to any real use case. What we need is some generic way to find and replace bits of lists with other bits. To do this, I’ll introduce yet another step in the meta direction. In the standard library, we have the name
function, which takes a symbol, e.g. a non-literal item in a list, and returns its name as a string, like so:
1
|
|
Armed with this function, we can treat quoted programs as lists of strings, and do any sort of string manipulation we may want with it (we also have the inverse of name
– intern
, which takes a string and returns a symbol). And how is this useful in our quest for list manipulation nirvana? It means that we can treat finding and replacing of program parts as finding and replacing (splicing) strings in a list, and that’s a rather down-to-earth non-meta operation.
Inspired by Lisp macros, I decided to implement a splicing scheme illustrated by this example:
1 2 |
|
The first list is the argument list and the second is the target program skeleton. Each ~X
symbol is treated as an index X
in the argument list. Upon application of the splice-from-list
function, each occurrence of the ~X
pattern is replaced by the corresponding item in the argument list.
The code for the implementation is in the repository in the meta.joy file; let’s look at it:
1 2 3 4 5 6 |
|
The function treemap
does most of the heavy lifting. treemap
takes a tree (which is an arbitrarily nested list where each non-list value is treated as a leaf) and applies a given function to each leaf; it is rather simple to implement using the built-in treerec
combinator. In our case, the tree is the program skeleton, its symbols are the leaves and the function to be applied is the ifte
expression. The ifte
expression checks whether a symbol matches the splicing pattern (is-num-splice-pattern
). If it does, the ifte
expression converts it to a number (splice-to-num
) and fetches the corresponding list item, otherwise, it leaves it as is. It’s in the is-num-splice-pattern
and splice-to-num
functions that we apply our ability to treat symbols as text.
It is important to note that the ~X
patterns don’t have any special significance in Joy; they are just valid identifiers. Trying to evaluate a piece of code that contains them without any replacement will probably result in an error, as they won’t have any predefined meaning.
Because we are after a more convenient way to write functions, a more likely scenario is to splice values directly from the stack, so we have
1
|
|
where take-from-stack
takes a fixed number of items from the stack and places them in a list. Now we can write
1 2 |
|
Note how the "two" "four" "five"
items are on the stack and not in a list. In most cases, it may feel redundant to write the number of items to take from the stack, it is evident from the maximal value of X
in the different ~X
patterns, so we define
1
|
|
And the final product:
1
|
|
which actually evaluates the resulting program, as in
1
|
|
Now we are ready for a first attempt at rewriting the insert-at
function from the previous post using our cool new metaprogramming techniques. First, let’s recall the original function:
1 2 3 4 5 6 |
|
And here’s the new version:
1 2 3 4 5 6 |
|
Success! We have no stack primitives visible in the code. But… I’m not quite satisfied with the fact that the function arguments don’t have explicit names. You see, I have this issue, I like naming things: variables, functions, babies, you name it. I mean, I name it, that is, I will name it once you give me the thing. As Joy is devoid of either variables or babies, the only thing left to name is functions, and the only way to do this is to include them in a top level DEFINE
, so we can’t have function-local definitions (something along the lines of Lisp’s let
definitions).
Splicing to the rescue. Instead of splicing from a list (or the stack), we can do the splicing from a map, like so:
1 2 3 |
|
The first nested list is treated as a map where each entry is another list. In each entry, the first symbol is treated as a key and the rest as the value. The second list is the program skeleton, where we replace every occurrence of a symbol that appears in the map with the value it is mapped to. The corresponding implementation:
1 2 3 4 5 6 |
|
which is quite similar to splice-from-list
. Instead of looking for ~X
patterns, we look for items that appear in our map. Instead of fetching from a list, we fetch from a map. The last difference is that instead of mapping with treemap
we use treemap-concat
, which maps a function that takes a leaf and results in a list merged into the tree instead of the original leaf. The reason we need that is to be able to use multiple symbols as values in the map (like "f" "ive" concat
in the example). The implementation of treemap-concat
was a bit tricky to get right at 4 a.m., I’ll leave the figuring out how it works as an exercise.
To slightly simplify the usage of splice-from-map
, we’ll allow to join the map and program skeleton into a single list, as in:
1 2 3 4 |
|
The definition of let-map
:
1
|
|
And now we have a simple implementation of local definitions. To make this more usable in the context of naming function arguments, we’ll compose this with stack splicing:
1
|
|
Using let
we can rewrite the insert-at
function as:
1 2 3 4 5 6 7 |
|
The combination of list and map splicing acts somewhat like named arguments in regular languages. It is definitely more explicit than the previous version, but I find it a bit clumsy to write. In simple cases where we only need items from the stack without any transformations, we can use this function:
1
|
|
Instead of taking a map like let
, it takes a list of symbols and converts the list into a map where the symbols are the keywords and consecutive ~X
patterns are the values (list-to-splice
). With this, we arrive at our final version of insert-at
:
1 2 3 4 5 6 7 |
|
The first line of the definition declares the arguments, saying: “I’ll need three arguments from the stack, and I’ll be using these names for them”. let-splice
does the magic of figuring out the mapping between the arguments and the values on the stack. This syntax is less flexible than let
, as we can only name values from the stack and not any other expression. But I think it works well in this example, yielding the most readable code thus far.
As you may remember, but probably don’t, this whole quest for stack manipulation cleansing started out with my wish to write a readable version of the build-tree-with-value
function, which traverses a tree looking for a value and uses a pair of functions to handle success or failure. In pseudocode it looked like this:
1 2 3 4 5 6 7 |
|
And to remind you of the atrocity you had to go through last time, heeere’s Johnny:
1 2 3 4 5 6 7 |
|
You can open your eyes now, I won’t be doing that again. Can our new meta-power-tools help us alleviate the pain? It so happens that yes, they definitely can. Here’s Johnny reformed:
1 2 3 4 5 6 7 8 |
|
The original algorithm was slightly modified to make this more readable. Namely, the value being searched for is not passed around – it is wired into the skeleton of the code; this affects the signature of the handler functions, which is now tree value -> tree
. It also inverts the comparison sign in the last predicate. The full new implementation can be found here.
I actually wasn’t expecting that much similarity to the pseudocode, I wrote it long before I had a readable version of build-tree-with-value
. And what’s more important is that we are using a generic solution, not something tailor-made for this particular problem. Another thing I quite like about this solution is how natural it was to build it in incremental steps: write a function, compose it with another one to refine it, rinse and repeat. That’s a general property of Joy (and probably any other concatenative language), making it easy to write simple, bite-size pieces of code.
Now, one might expect me to go OCD on the rest of my binary tree implementation, and prune out as much of the remaining stack manipulation bits as possible, but I won’t be doing that. No, seriously, not going to bother, I’m totally fine with how it is…
There are some reservations about my meta solution, though. First off, the implementation is far from being complete. The let
/splice
expressions cannot be properly nested in all cases, e.g.:
1 2 |
|
And you can’t make a definition and use it in the same map, so this is not valid:
1
|
|
Bearing in mind that the whole implementation is ~70 LOC (no, that’s not a leftover splicing pattern, and no, I’m not using a Joy-based templating engine), that’s probably not so bad. Fixing these issues shouldn’t be too complicated, but what we have should suffice as a proof of concept.
A completely different issue is performance related, though performance is of little significance to me in this case; it seems that using this kind of meta-programming really stresses out the GC. Running the new tree implementation on the usual Joy interpreter (the one you’re likely to compile if you’re fetching it from the Joy site, it has “NOBDW” in its title) crashes when trying to insert around 4 items. To circumvent the problem, I used the Joy interpreter compiled with the BDW GC (there’s a special make
file for that purpose). This fixes the issue, and you can easily insert more than 100 items in either implementation, though the meta-programming implementation is still considerably slower.
But the most important issue, in my opinion, is that it feels as if I’m trying to shoehorn my approach to programming into Joy’s. In this whole exercise, I’m essentially trying to emulate named function arguments, which is one of the things that Joy seems to purposefully avoid. My feeling is that there must exist somewhere “The Joy Way ™”, which would allow me to achieve the same level of readability without resorting to metaprogramming tricks. On the other hand, the fact that it was that simple to achieve this goal might be an indicator that maybe this approach was not an act of complete heresy. Any insight on this issue will be greatly appreciated.
Anyways, this concludes our excursion into the green lands of meta-trees. Stay tuned for the next time.