Forth Next
The Robot World Repo on GitHub
The Forth Repo on GitHub
I’m not sure what to do next. I’ll reflect on some ideas exchanged with GeePaw Hill and then pick something, probably something easy.
I’ve been toying with implementing the whole robot game, world and all, in Forth, for no particular reason. Once you start using a language, you start thinking about what you could do with it. It’s kind of natural.
The robot world is a large array of discrete cells with integer coordinates. Each cell can contain zero or one things, though that might change. A thing might be a robot or a block or a thing yet to be invented. There are “few” things compared to the number of cells, so that a more compact structure than a full array is tempting.
I /chatted with Hill about it and he said that in a real Forth he might just allocate the array on the heap, but otherwise use some searchable structure allocated in chunks on the heap with <coordinate, value> kind of structure. He spoke of having the basic world access as primary words in Forth. That surprised me, though it shouldn’t have.
Thinking about Forth as defined and as we see it implemented, at least in the info that I have found so far, we have a little language that processes bytes and integers, that even struggles with long integers, and that has a very weak relationship with strings, which are represented by an address and a count on the stack, so you really have to know what’s going on to deal with them.
But those are all just the basics. A real forth, running on some controller computer, might have specialized words for accessing the hardware ports or for issuing instructions for driving the I/O or whatever the thing did. You would not code up a specialized word for accessing, say, some kind of hashed structure. You would code that in Forth because it would be easier and better than doing it in assembler or binary.
But we are building our Forth with Python, not in assembler. So we can, if we choose, create specialized operators that let us keep some kinds of information in Python, with Forth words that expose or change that information. So we don’t have to drop all the way back to bytes and ints if we don’t want to, instead providing a vocabulary of words that make sense in Forth and do magic behind the scenes in Python.
I had gotten myself into such a classical Forth mode of thinking that I felt actually astounded. Oh! Right! We can do that! I forgot!
I have not yet fully absorbed Hill’s idea. Our /chat was short and by nature a bit clipped and cryptic, so I am confident that he has a better and more clear notion than I have, but I probably have enough. That said, I will probably probe further with him, and if you have thoughts, drop me a line or a toot.
What are you thinking so far, then?
I think that the thing to do is to imagine / figure out what a Forth program to drive robots around would want to do, and how it would like to do it, and then devise built-in words that facilitate that use. All too often I have read hardware specs or APIs that seemed to have been designed for the convenience of the hardware or the API. But no, the point of an interface is to be useful. So what will bots want to know and how might they want to know it, in terms of Forth?
Yesterday, I sketched the notion of a “vision”, which in the sketch was an array of, say, three integers, representing the cell to one’s forward left, forward center, and forward right. I then defined a few tiny words, which allowed me to code up a word can_take
, that answers true if there is a block in front of you and a blank space on at least one side of it:
: can_take front_block left_blank right_blank or and ;
We do not have OR
and AND
implemented, so that won’t actually work yet.
Here are two tests:
def test_or(self):
f = Forth()
f.compile('0 0 or')
assert f.stack.pop() == 0
f.compile('1 0 or')
assert f.stack.pop() == 1
f.compile('0 1 or')
assert f.stack.pop() == 1
f.compile('1 1 or')
assert f.stack.pop() == 1
def test_and(self):
f = Forth()
f.compile('0 0 and')
assert f.stack.pop() == 0
f.compile('1 0 and')
assert f.stack.pop() == 0
f.compile('0 1 and')
assert f.stack.pop() == 0
f.compile('1 1 and')
assert f.stack.pop() == 1
And the definitions:
def define_logical_operators(self):
def _or(forth):
a = forth.stack.pop()
b = forth.stack.pop()
forth.stack.push(0 if a==0 and b==0 else 1)
def _and(forth):
a = forth.stack.pop()
b = forth.stack.pop()
forth.stack.push(1 if a!=0 and b!=0 else 0)
self.pw('OR', _or)
self.pw('AND', _and)
Commit: AND and OR
Now let’s refactor those, using Inline.
def define_logical_operators(self):
def _or(forth):
forth.stack.push(0 if forth.stack.pop() == 0 and forth.stack.pop() == 0 else 1)
def _and(forth):
forth.stack.push(1 if forth.stack.pop() != 0 and forth.stack.pop() != 0 else 0)
self.pw('OR', _or)
self.pw('AND', _and)
Now we can use lambda for these. The lines will be long but I prefer the more compact form.
Rename the parameters:
def define_logical_operators(self):
def _or(f):
f.stack.push(0 if f.stack.pop() == 0 and f.stack.pop() == 0 else 1)
def _and(f):
f.stack.push(1 if f.stack.pop() != 0 and f.stack.pop() != 0 else 0)
self.pw('OR', _or)
self.pw('AND', _and)
Now the lambdas:
def define_logical_operators(self):
self.pw('OR', lambda f: f.stack.push(0 if f.stack.pop() == 0 and f.stack.pop() == 0 else 1))
self.pw('AND', lambda f: f.stack.push(1 if f.stack.pop() != 0 and f.stack.pop() != 0 else 0))
Commit: convert AND and OR to lambdas.
We need INVERT, which changes true to false and vice versa. Test:
def test_invert(self):
f = Forth()
f.compile('1 invert')
assert f.stack.pop() == 0
f.compile('0 invert')
assert f.stack.pop() == 1
And …
self.pw('INVERT', lambda f: f.stack.push(1 if f.stack.pop() == 0 else 0))
Commit: INVERT
So that’s nice. I could actually test that idea for vision now, I think.
Let’s try it. We need to get facile with Forth if we’re going to do a good job of creating a world API.
variable vision 3 allot
: v.l vision @ ; ( fetches vision left)
: v.l! vision ! ; ( sets vision left)
: v.f! vision 1 + ! ; ( forward)
: v.f vision 1 + @ ;
: v.r! vision 2 + ! ; ( right)
: v.r vision 2 + @ ;
: front_block v.f 1 = ; ( is a block (type 1) in front?)
: left_empty v.l 0 = ;
: right_empty v.r 0 = ;
: can_take front_block left_empty right_empty or and ;
0 v.l!
1 v.f!
33 v.r!
can_take .
1 ok
1 v.l!
can_take .
0 ok
I’ve removed the ‘Forth>’ prompts and most of the ‘ok’ responses. We see that when the vision is 0, 1, 33, we can take, and when it is 1, 1, 33, we cannot.
I think that if working on a real program, I might name those words vision.left
, and so on, rather than v.l
, but the scheme seems to work as intended.
Reflection
Thinking back to what we might provide as a world API, we could imagine providing vision.left
as a special Python-implemented word that calls vision
on the currently-running Bot and returns cell zero, and so on.
We just implemented those three words, AND, OR, and INVERT, but they have given us that delightfully compact implementation of can_take
. I hope you can kind of begin to see how a Forth program can be small and compact while also making sense in terms of the domain.
What’s next? I don’t know. Stop by and find out, next time!