Another Déjà Vu post. If you don’t care about the design and implementation of programming languages, this post is not for you.
Anyway, now that I’ve lost the good majority of my readership, I’d like to talk about types.
In Déjà Vu, there used to be 6 types:
- Idents (symbols would probably have been a better and more familiar name)
And now I’ve added a 7th: pairs. A pair is similar to a two-element tuple in Python or to a cons cell in Lisp. You can create a pair by calling
&, like this:
& "hi" "there" print &< dup # prints the first element print &> # prints the second element
Big deal, right? We already have lists. Anything pairs can do, lists can do better. Lists can do anything better than you. Erm… than pairs.
The thing is, pairs are still useful. Pairs are immutable. Pairs can hold only elements of simple types (strings, idents, numbers and other pairs). These two things mean that a pair can never be a part of a reference cycle, which means they don’t have to be garbage collected. Objects of a simple type are only reference counted, which is better performance-wise and memory wise.
Pairs are small too: except for the minimum amount of data a Value has to have (16 bytes), it only has to hold two pointers to the other Values. Pairs are small and easy on the GC, which means you can create heaps of them for a low price.
I already used them for a number of purposes, among them lightweight binary trees and Lisp-style lists.
Here’s a function that is basically the equivalent of
map for pair-based trees:
treemap f: if = :pair type dup: treemap @f &> dup treemap @f &< swap & else: f . treemap @++ & & 0 10 & & & 5 7 -2 & 0 0 # prints & & 1 11 & & & 6 8 -1 & 1 1
For larger literal trees, the syntax can get rather hard to read. The nature of Déjà Vu makes that a bit hard to fix, unfortunately.
Anyway, that’s it.
It has been a while since I posted here. Not that I’ve been sitting on my hands. A lot has happened the past three months.
For example, I took up Déjà Vu again. The implementation had some problems, or rather one big problem: the C stack.
You see, the Déjà interpreter has two functions
getnextnode which call each other recursively, while walking the source tree. A simple function in Déjà Vu is often three or four nodes deep, so calling a function in Déjà Vu can easily add eight functions that are called recursively. CPython uses the C stack for that. The C stack is rather limited in size, so the interpreter usually can’t handle more than a node depth of a few hundred nodes, and that includes recursive function calls.
Now, there are other implementations of Python that don’t have that limitation, but I don’t want to require the user to use Stackless or PyPy or something else. It is possible to increase the size of the C stack, but that’s a workaround and only works for applications of less than an arbitrary size. I deemed neither solution acceptable.
So what then?
My reasoning was as follows: the recursion of the interpreter is the problem, so I have to make it not recurse any more. Thus I wrote a function called
flatten, which made the tree a list of instructions. (Actually, some parts kept a tree shape, but those were limited and unlikely to cause problems). Then I did nothing with it for several months.
When I got started on it again, I decided the code to flatten the tree could easily be used to generate byte code. I then spent a while thinking of useful opcodes and other low-level decisions. When that was done, I had a pretty good idea how I wanted to do the byte code and the virtual machine. I wrote several functions and adapted flatten, to produce proper byte code.
Although this byte code is a binary format, I wrote it for portability, so the compiler would not be needed by users of Déjà Vu programs.
I started writing a virtual machine in C, in code as clean and portable as possible.
As regular readers* will know, I am a high-level thinking guy, so C isn’t my forte, especially for writing virtual machines. I found a sensible way to implement stacks and values that can have multiple types, and I think I know how I can handle return stacks and closure stacks, but there’s still a long road ahead.
At the current state, it has a garbage collector: reference counting plus the synchronous method of cycle collection described in Concurrent Cycle Collection in Reference Counted Systems.
* Ha! As if!
Last week, our assignment for Object Orientation was to implement a version of Mastermind in Java. The implementation of the AI was okay, but it could have been better. It was a strategy I wrote as a temporary solution. Sometimes it won — sometimes in four or five times — but other times it lost. This was the entire reasoning of the AI:
- Each position gets a list with all possible values (A, B, C, D, E, F).
- I choose a random value from each list to select the next attempt.
- If the score I get for attempt the attempt (for example ABCD) has no red points, I know that:
- A cannot be in the first list, so I remove it.
- B cannot be in the second list, so I remove it.
- C cannot be in the third list, so I remove it.
- D cannot be in the fourth list, so I remove it.
- If the score I get for the attempt has no white points either, I know that all the colours in the attempt must be removed from all the lists.
- If the right solution hasn’t been found yet, go to 2.
That’s it. That’s the whole of the algorithm. I didn’t think it would work. But sometimes, it did. It was more luck than wisdom, but still.
As you might know, our major is Artificial Intelligence, so I wanted to make it smarter, once we dealt with the annoyance of MVC — I can see how that can be useful in certain situations, but we never have practical assignments that do the pattern justice.
Anyway, the Wikipedia page I linked to earlier had an interesting section on algorithms. I wanted to implement one of them, but we soon abandoned the idea. Firstly, the computer playing a near-perfect game each time seemed boring. Secondly and more importantly, Java couldn’t let us do it easily or elegantly.
The problem seemed to be object orientation to me. It simply wasn’t cut out for this. I thought functional implementation would be much better. Java really sucks at this, so I made up my mind to write an implementation of Knuth’s five-guess algorithm in Python, using every functional trick I could think of.
That’s exactly what I did last weekend. The complete program, with CLI, is only 42 lines long (yes, that was intentional). And it works. You can supply a code as a command line argument, or enter it later. The first attempt is always AABB. The second attempt takes a while. Depending on the score at the first attempt, it may take twenty or forty seconds on my machine, but I have seen it take only three.
At first, it didn’t work. It took me some time to find the problem: the search space was too limited. Because of careless reading, I used the wrong function to generate the list of all possibilities. It actually generated a list of permutations of ABCDEF of length four, meaning codes containing duplicates (AABC or DABD, for example) could not be found. Using the right function solved all that.
I made a Gist, enjoy.
PS: I don’t like using a global to keep track of the number of attempts. If you know of an elegant and functional solution, please let me know!
As I was trying to make a proper programming language out of Lith, I noticed something: it became less and less elegant. And although it now had functions, to truly work as a programming language, I needed to add even more inelegant cruft.
When reading about the history of Haskell, I had an idea: I would return Lith to a former state, without functions, so that it could be used to describe data and nothing more, while guaranteeing any valid source code has an equivalent canonical form (only containing
[ ]). This could not be guaranteed when using functions, which have no canonical representation. The other part of the idea was that I would copy the Lith project to a new project named Lazy-Lith, which would be a lazily evaluated superset of Lith. Functions (in the form of
( )) are completely gone now: Lith does not have a way to suspend evaluation, while in Lazy-Lith, every list is a lazy list.
This means I have to let go of the idea of a stack, rendering the -th in Lith obsolete, because when all lists are lazily evaluated, there is no way of knowing beforehand when the stack will be used.
I have added alphabetical names and assignment with
= in the parser, they’re not yet evaluated.
That’s pretty much all there is to say about Lazy-Lith at the moment.
A while back, I wrote about Lith, a minimalistic language.
Yesterday, I decided to implement the rest of the language, which was surprisingly easy. (Of course, Lith is still a simple language, and I have more experience with this sort of thing now.) It’s up on GitHub now.
In today’s post, I discuss the theory and implementation of the latest version of Lith.
Last Sunday, I decided to write an implementation of tic-tac-toe, complete with computer opponent. I ended up implementing a minimax algorithm, which worked well enough. The relatively small amount of states in tic-tac-toe (3⁹ = 19 683, but this includes unreachable states) made this rather simple.
I pitted two minimax players against each other. Predictably enough, it resulted in a draw. Always the same, since the algorithm is deterministic:
X X O O O X X O X
The total running time was consistently just under 3 seconds.
That got me thinking, surely a heuristic could do things faster? So I drew up some heuristic rules the enhanced computer player would use:
- If the board is empty, just play the top-left corner.
- If you can win, play the winning move.
- If your opponent can win, prevent them from winning.
- If you can find a nice corner you can use to force a win (a number means it is empty, a period means it doesn’t matter), that is:
X 2 3 . . 6 . . X
X 2 3 . 5 . X . .
(possibly rotated), do that move.
- If the opponent has above opportunity, prevent them from doing so.
I tried the heuristics, and sure enough, they helped. Drasticly. But only when the heuristic went first (total time: just over 0.2 seconds). Otherwise, the timing didn’t change at all. Not even just a little bit. They did nothing.
After a bit of experimenting — not too much, you can probably tell what’s going on here just by reading the previous paragraph, I just wanted to confirm it — I found out what was going on:
None of the heuristics did anything. Except for the one for the empty field. You see, the further you get in the game, the smaller the number of possible future game states gets. Reduction early on is likely to matter more. Especially if you can throw away 8/9 of the search space.
The nature of minimax did the rest: if you can win in one move, it will already be selected immediately by the algorithm. If your opponent can win in one move, the algorithm will find you lose quickly after choosing anything but that one soon enough. And those fancy heuristics for finding ways to trick the opponent into inevitable failure won’t be needed, because the opponent won’t let you get that far (remember, this is minimax vs minimax).
So I removed all but the first heuristic.
I put it up on GitHub as a Gist. If you scroll up, you can see the old implementation of the heuristic (much longer, a lot less elegant, and not a bit faster than the new one).
What have we learned? I’d say the lesson is that, when relying on a minimax as fall-back, you only need to implement heuristics for the opening moves, which will dramatically decrease the search space.
I’ve been thinking a lot about Déjà Vu (the programming language I invented) lately.
A while ago, I’ve written a post about it. I couldn’t quite get it to work. After much distress and failed attempts to fix things, I let it rest.
Once in a while I revisited it. For example, when I (re)discovered Parrot, it seemed like a good idea to implement it that way. The problem was that you pretty much need to do each part in one of Perl, some variant of Perl, or Parrot assembly. Perl is horribly opaque and I never learned it, which was problematic. I’m a high-level thinker, so assembly works against me. But the worst of all is that writing grammars and parsers and whatnot is just plain hard. No, scratch that: the absolute worst is the combination of those things. Learning a language you’ll probably never like is much harder if your equivalent to a “hello world” is a top-down grammar parser.
So I let that rest as well.
But I kept thinking about Déjà Vu. Not on how to implement it — I’m a high-level thinker, remember? No, I was thinking about concepts, about constructs, about connotations…
All ideas were kept in a file called “idea”. The language grew and matured, even though it had no implementation. I added new kinds of statements, full lexical scoping and new datatypes, and improved the function definition syntax.
This spring break, I decided to try it one more time last Sunday. Following the success of implementing a tree parser for Lith, (by the way, the answer to the question posed in that post, “Will I ever implement more?”, is “No, I will not.”) I decided to start simple: read the file line by line, perform a series of simple text transformations to deal with indentation, strings, comments, words and statements (it had to be in that order — I did spend some time thinking about implementation). After the transformations, produce a tree. I’d see what would happen next if I finished it.
I finished it within a day. I was shocked. Sure, Déjà Vu is not a large language — I kept the amount of syntax and the number of statements purposefully small — but I had so much problems with it before, and now it worked! In a day! It had everything. It had full lexical scoping! It took Lua 8 years to get that, and this implementation of Déjà Vu had it within a day! All I had to do now is find some way to deal with that parsed tree.
Let’s see. I had a tree filled with conveniently semantically typed nodes. Why not just traverse it, instead of struggling to convert it to byte code? (And probably back again at that.) So I started on that. When I went to bed that day, the first bits where in place for interpreting trees and what’s more: it looked like it was going to work.
The following day, Monday (it is past midnight as I’m writing this post), I implemented the interpreter. It was unbelievably easy. After figuring out some minor problems, I was even able to implement the For statement without much trouble, and I had expected that to hold up the project for a while.
It was the second day, and the language worked. It just worked. I had hastily added an interactive interpreter, modeled after Python’s. That worked too. I have written a neat standard library, implemented in Python. My idea file is over 400 lines, and I had implemented everything I proposed in it, save for those that were superseded by other ideas.
It’s all up on GitHub.
Tomorrow, I will write the much-needed documentation.
One more thing: the implementation as a whole defines 40 classes to parse, interpret, and represent Déjà Vu source. 26 of which are subclasses of the Node class alone. I think that’s the highest number of classes I’ve ever used for a single project.