![]() |
By default, links look like links. So web developers have to go out of their way to make this happen. Pretty strange, eh? An entire planet seemingly dedicated to user interface obfuscation.
If being delusional is having a false persistent belief or opinion not substantiated by sensory or objective evidence, then this blog is about what happens just before that. So, as you read and respond, keep the goal in mind. There is a big difference between being almost insane and being barely insane. This blog is about that difference.
![]() |
It's time for the Mars hoax email again. You may get email that says that Mars will appear as big as the full Moon, or even There will be two moons in the sky, and with a date, like August 27th. It comes from the 2003 Mars opposition announcement. In late August of 2003, Mars had a particularly close opposition, and in a telescope at 75x, Mars appeared as big as the Full Moon does with the unaided eye. Each summer, the email gets modified a bit more, and is pretty far from reality.
There is something about using your eyeball and a telescope and seeing it for yourself. It's different. There's still a month or so of Saturn's big show this summer. So get out to your local astronomy club's next event and take a look.
When i'm asked what i do for a living, it is often the case that i'm talking to someone who doesn't do what i do. I assume they don't know the details of computers at first. So i don't launch into the details unless asked. I say, "I do something with computers". It's pretty generic. I've been asked what my job title is, and i tell them. But when they ask me what that means, and it's as generic as Systems Specialist, the only really good answer is "My job is so secret, not even i know what i'm doing".
#!/bin/sh
for y in `cnt 1901 2000`; do
for m in `cnt 1 12`; do
cal $m $y | grep "^ 1"
done
done
1 2 3 4 5 6 7
It's been suggested that college students should be graded on attendance.
http://www.wired.com/wiredscience/2011/06/do-you-get-better-grades-with-better-attendance/
First, a couple anecdotes, and then a better idea.
I had an engineering class called Stress, with a low pass rate. The prof did everything he could think of to get students to pass. Tons of office hour time, etc. 1st day, he passed out a syllabus noting the 3 open book exam dates and all homework. All homework counted the same as an exam. Of the 4 grades, the lowest was dropped. I started the 1st week's homework, but it was clear there was only time for half of it. I was careful to hit all types of problems. I passed it in, but, of course, 50% is not a passing grade. It became clear that the prof taught the material in the book. But i could read faster than he could talk, so i stopped going to class. I did OK on the first exam. One Friday, i came in for what i thought was the 2nd exam, but he passed out a quiz. At a glance, it covered next week's homework. I hadn't done that yet. I rechecked the syllabus, and it said that the 2nd exam was the following Monday - i hadn't missed it. I handed the quiz back. The prof told me that i'd likely fail the course. I looked him in the eye, but didn't tell him i'd do OK if i didn't miss any exams. The last exam was held at the start of the last week. It didn't cover the last week's homework. I did half of that anyway. And, i passed the course.
I would never have skipped half the homework if it weren't impossible to finish. I would never have skipped the classes if the pressure to optimize my time weren't so severe. Exams have high time pressure, and i already knew that my performance would be awful on them. It was just the only choice available. I liked the course material and the prof, but it still pisses me off that the course was set up that way. If i'd known in advance, i'd have dropped another course from my schedule, so i could devote twice the time to it. So the degree means what? These are the students who managed to get through by cheating, optimization, or by being unbelievably quick? Are these skills one needs in industry? Not as far as i can tell.
There was a brief break. Then Stress 2 was taught by the same prof. I figured he'd pass out a syllabus on the first day, so i didn't bring my books or calculator. He passed out an exam! I ran home, grabbed my stuff and ran back in 20 minutes. Panting, i asked for an exam. He found me a seat and an exam that didn't match my neighbors. At the 50 minute mark, i was done (!). I looked around and there was panic on 109 faces. I figured i must have done something wrong. All these kids are brilliant. I checked my answers. No errors. So at the 55 minute mark, i got up and handed it in.
Next day, the prof writes something like this on the board:
| 97: | 1 |
| 60-70: | 4 |
| 50-60: | 7 |
| 40-50: | 19 |
| 30-40: | 37 |
| 20-30: | 23 |
| 10-20: | 13 |
| 0-10: | 6 |
At first i wondered how i'd lost 3 points. But then i realized that i'd done the course right. One of the four questions was on that last week's homework, and i was the only one who'd done even half of it. In this course, there'd be 4 exams, homework with the lowest dropped. I did half the homework. Then it was announced that the lowest two grades would be dropped. But i took the 4th exam even though i'd already passed the course.
Years later i auditioned for a chorus. It involved sight singing and a bunch of music things i'd never done before. That morning, i ran 4 miles, showered, and took the train. The director gave me my starting tenor note and played the accompaniment. I dived in, but stumbled. But then i started getting where the piece was going, relaxed a bit and read ahead. At the start of the last line, i turned the page over (while singing), but it was blank. I looked up and finished the page, adding dynamics. The director was stunned. So was i. I'm simply not that good. But a 4 mile warm up does wonders. Phys Ed should be held for 20 minutes every morning for all students. They don't have to be awake for it.
Prime numbers are positive integers that can only be divided evenly by themselves and the number one. So there are no factors for prime numbers. Prime numbers are great for factoring, because if there are no prime factors, then there are no factors. The first few interesting prime numbers are 2, 3, 5, 7, 11, 13, 17, 19. If you're trying to factor a number, you only have to attempt to divide the number by prime numbers up to the square root of the number. An example should make it clear why. Let's say you're trying to tell if 25 is prime. You try 25 / 2, and get a 12 with a remainder of 1. You try 25 / 3, and get 8 with a remainder of 1. You try 25 / 5, and get 5 with a remainder of 0. So 25 is divisible by 5. But notice that you divided by 5 and got 5. The number 5 is the square root of 25. If you divide 25 by 6, you get 4 with a remainder of 1. But you already checked division by 4 (it's not a prime, but it's between 3 and 5, so we have effectively checked it). We don't want to check anything twice, so we only have to check up to the square root of the number. The short list of prime numbers given is good enough to check numbers up to 528. That's because the next prime after 19 is 23, which when squared id 529. So a small list of primes allows checking of large numbers. However, memory for storing prime numbers is in short supply on the HP-28C, so we'll check all numbers up to the square root, not just the primes.
The HP-28 RPL language has pretty much everything we need. There's a function to compute square root (√). There's a division function that returns the remainder called MOD. There are loops and conditionals. Yet my first version ran into a snag. It was convenient to use a FOR loop with a local variable. Having a local variable is handy because it doesn't clutter the stack, it doesn't need to be created before you start and purged when you're done with it. And local variables can be easily recalled. However, when a remainder of zero is found, indicating a factor has been found, the next thing to do is exit the loop and report the answer. It turns out that you can't exit a FOR loop in RPL. There's no "GOTO" in the language. The loops don't have any other "break" function, for example, as the C language has. There are two solutions. One is to let the loop finish. The other is to use a WHILE loop. In my first version, I continued with the FOR loop, but changed the problem to factoring a number. It got ugly. For example, if the number was 12, it divided by 2, but failed to check to see if it could continue dividing by 2. So it didn't always result in prime factors. A second version returns the focus to primes, using WHILE.
«
DUP √ FLOOR WHILE DUP 2 ≥ REPEAT
DUP2 MOD IF 0 == THEN
DROP 1
END
1 -
END
» 'ISP' STO
There are better ways to check for prime numbers. For example, a simple optimization is to first check if the number is divisible by 2. If it isn't even, then only the odd numbers up to the square root need to be checked. For large numbers, this could double the speed. There are other ideas as well. Some require lots of memory. Some are quite complicated. So we won't consider them now.
The main point of this exercise was to show how searching can be accomplished on this machine. Searching requires breaking from a loop that's in progress. A design decision in RPL was to not provide an explicit loop break mechanism. In fact, there is no GO-TO like function in the language. While anything you could want to implement can be implemented in this language, it isn't necessarily very easy. The loop structures available that work for this are WHILE-REPEAT-END and DO-UNTIL-END. My first thought was that these loops are equivalent and have their test at the start of the loop. But it turns out that the test can be placed anywhere in the loop. That is, any arbitrary commands can be placed between the WHILE and REPEAT clauses, and nothing need be placed between REPEAT and END. It turns out that one can not use WHILE-REPEAT-REPEAT-END to get the effect of two exits in one loop.
This bit about having to change the condition of the loop to break out of it is part of a wider issue. In 1968, Edsger W. Dijkstra published an often quoted paper entitled Go To Statement Considered Harmful. The paper starts with this line: For a number of years I have been familiar with the observation that the quality of programmers is a decreasing function of the density of go to statements in the programs they produce. Note that he has a value judgment not on the programs written, but on the programmers who wrote them. Programs in languages like FORTran (at the time) absolutely required the use of go-to statements. And in such programs, it was easy to end up with spaghetti code that was very difficult to follow. And that is the gist of the paper. But the opening line suggests that the fewer go-to's in a program, the better the programmer. It had been proven that go-to statements were never necessary in languages that support if-then-else and loops or recursion. The logical conclusion is that programmers who never use go-to statements are as good as one can get. This conclusion isn't correct. Once the right languages appeared, the goal of zero go-to's became possible, but a new issue reared it's ugly head. The issue is breaking from a loop. There are two easy ways to do this in a language like C. First, one can jump out of the loop with a go-to. Second, one can create a state variable and set it so that when the loop test is executed, the loop exits. And what happened in the go-to's are evil era was that all such loops were exited using a state variable. There are at least two problems with this approach. First, extra instructions to set up a test must be executed. Often there are extra instructions in the test itself that are executed each loop. Second, compilers of the day had no idea how to optimize this sort of code. Third, state variables can be just as difficult to follow as go-to's are. For example, if there are four state variables, it's just as difficult to understand as four go-to's. Except that if all four go-to's are clearly loop exits, then they're very clear after all. The Internet didn't exist back in the '70s, so most people never read the original paper. They just latched onto that first statement, and used it as Gospel. These days, we have less of an excuse. It's an easy read. It's quite clear that the go-to's are always bad concept is an over-generalization. After all, Dijkstra makes an exception for machine language. Why make an exception there? Well, the flow control in machine language generally is all built on some form of go-to. There's no choice. And besides assembly language programmers are generally better than those who can't code in assembler. What i mean by this is that assembly language is not generally the first computer language that programmers are taught. By the time one is writing assembly language, they have considerably more experience. It turns out that there are other exceptions to go-to's being poor practice.
In any case, the designers of RPL decided not to include a go-to. And, the decision made it sometimes more difficult for the programmer to achieve certain goals. But nothing was made impossible. And, there may have been a performance reason for not including a go-to. Some of the Scheme language environments allow go-to, but performance for all structures suffers all the time because of it. That means that performance for the Scheme implementation suffers. It's a big deal.
There was one other observation of note during this project. I ran across a list of software errors for the HP-28 (they're at the bottom). I tried some of these on my unit, and they were all problems. So one issue with having a system strictly in hardware is that it can't be fixed. The software in something as complicated as the HP-28 series is way too large to have any hope of being bug-free. The bright side is that the language can't change out from under your program. I've been bitten by Light Year conversions getting inaccurate results.
Lots of applications written for the HP-28 series are available through this site.
Conclusion:The HP-28 is a highly capable machine. The RPL language is concise, making it a good match for limited machines. However, it can be difficult to estimate how much memory a structure or chunk of code might consume without actually trying it. The stack manipulation slows writing of functions, and can make reading functions difficult. Since tail recursion optimization is not supported, the system is not a particularly good place to experiment with recursion.
As always, any questions, leave a comment. I'll get it and respond, even if this post is ancient history by the time you read it.
In part six, a full runnable program was presented. But the startup time, generating the random puzzle, takes forever. How to speed it up? One thing to try is to abandon strings, which aren't randomly writable, and use arrays, which are. Characters aren't allowed as a data type in arrays. Only numbers are allowed. All we need are 9 different numbers. But some numbers are easier to convert to characters than others. In particular, the ASCII values of characters can be directly converted. The code for lower case "a" is 97, with the alphabet following sequentially. An array is created with the highly imaginitive name "a". Here's the code:
« 97 105 FOR x x NEXT { 9 } →ARRY 'a' STO » 'MKARRR' STO
Given the array, we need a function to shuffle it. But such a function will probably need a helper function that swaps values at two positions. It's a bit more complex. Two positions from 1 to 9 are placed on the stack before called.
«
DUP2 'a' SWAP 1 →LIST GET
SWAP 'a' SWAP 1 →LIST GET
ROT 1 →LIST 'a' SWAP ROT PUT
'a' SWAP ROT 1 →LIST SWAP PUT
» 'SWAPA' STO
| start | x | y | ||||
|---|---|---|---|---|---|---|
| DUP2 | x | y | x | y | ||
| 'a' | x | y | x | y | 'a' | |
| SWAP | x | y | x | 'a' | y | |
| 1 | x | y | x | 'a' | y | 1 |
| →LIST | x | y | x | 'a' | { y } | |
| GET | x | y | x | yval | ||
| SWAP | x | y | yval | x | ||
| 'a' | x | y | yval | x | 'a' | |
| SWAP | x | y | yval | 'a' | x | |
| 1 | x | y | yval | 'a' | x | 1 |
| →LIST | x | y | yval | 'a' | { x } | |
| GET | x | y | yval | xval | ||
| ROT | x | yval | xval | y | ||
| 1 | x | yval | xval | y | 1 | |
| →LIST | x | yval | xval | { y } | ||
| 'a' | x | yval | xval | { y } | 'a' | |
| SWAP | x | yval | xval | 'a' | { y } | |
| ROT | x | yval | 'a' | { y } | xval | |
| PUT | x | yval | ||||
| 'a' | x | yval | 'a' | |||
| SWAP | x | 'a' | yval | |||
| ROT | 'a' | yval | x | |||
| 1 | 'a' | yval | x | 1 | ||
| →LIST | 'a' | yval | { x } | |||
| SWAP | 'a' | { x } | yval | |||
| PUT | ||||||
We need a new shuffle that uses this routine to create a puzzle.
«
1 9 FOR x
x x RAND * FLOOR 1 + SWAPA
NEXT
» 'SHFLA' STO
We still need a function which turns the array into a puzzle string. It's quite simple. The CHR function turns an ASCII number into a single character string. The '+' operator concatenates strings.
«
a ARRY→ DROP ""
1 9 START
SWAP CHR +
NEXT
» 'A2S' STO
Finally, a modification of 'BACK' is needed that uses the new shuffle.
« CLLCD MKARRR SHFLA A2S
1 'BMV' STO
WHILE DUP "abcdefghi" ≠ REPEAT
BMV 1 DISP 'BMV' 1 STO+ DUP 2 DISP
DO UNTIL KEY END STR→ REV
END
2 DISP
"You win" 3 DISP
'BMV' PURGE
'a' PURGE
» 'ABACK' STO
At this point, a diversion to playing the game is in order. If you examine the program, you might notice that when the program ends, it always says "You win!". You could quit before winning, but the game isn't that difficult. You always win. Compare that to a simple eye-hand coordination game like Tetris. There, you always lose. Always. With this positive reinforcement, one might expect that players would never stop playing.
There's a very simple algorithm for coming up with a solution to any problem. And, it turns out that 2 * n - 3 (on the HP-28, that's n 2 * 3 -) moves are required, as a maximum, and usually fewer. And when i learned this game so many ages ago, that's about all i knew about it. I had to figure out what the algorithm was. And it's pretty simple.
Note that if you get the last symbol, the "i" into the right most position, you should never have to move it again. So, if you build the string backwards one symbol at a time, you're done. What you do is search for "i", reverse it to the left most position. Then with "9", reverse it to the right most position. Then search for "h" and do the same. This seems to take two reverses per digit, so for a string with n symbols, you should have to perform 2 * n moves. But when you get to the first symbol, a in this case, it's already where you need it. So that's two moves you don't need, or 2 * n - 2. The remaining exercise is to figure out what other move you don't need. In any case, often, when you search for a symbol, it's already in the first position, so you don't need to get it there. Sometimes it's already in the position you need it to go to, so you save both moves. So the formula gets you a maximum number of moves. It may be fewer.
One might ask if there are optimum solutions that don't follow this algorithm. And there are. And in my opinion, that's where the real fun for this game lies. I've written optimal solution finders in C and Perl. And even the Perl version can find all optimal solutions for puzzles of length 9 in an hour on a modern computer. The C version can do it in under a second. How many puzzles of length 9 are there? Here's how to figure this out. If you're looking for a random puzzle, the first position can be any of 9 symbols. But having chosen one of these, there are only 8 symbols to choose from for the next symbol. So the first two symbols can be 9 * 8. Continue this way until the last symbol, from which there is only one left. The answer turns out to be 9! - that is 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 = 362,880. I'd like to say that this was computed with my factorial program, but it had already been deleted. So the built in FACT function was used instead. Blame it on the low total memory area of the HP-28C.
Once one has an optimal solution generator, then one can compare the simply algorithm performance to the optimal solutions. In this table, Moves is that number of moves required. Optimal is the number of games with that many moves, and Auto is the number of games with that many moves using the simple algorithm. You can see that zero moves (starting with the solution) through two moves have the same number of games. That's because for those games, the optimal solution is the same as the easy solution. But there are no games where the optimal solutin requires more than ten moves. This table is for games of length nine.
| Moves | Optimal | Auto |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 8 | 8 |
| 2 | 56 | 56 |
| 3 | 391 | 252 |
| 4 | 2278 | 980 |
| 5 | 10666 | 2968 |
| 6 | 38015 | 7798 |
| 7 | 93585 | 16836 |
| 8 | 132697 | 31396 |
| 9 | 79379 | 48636 |
| 10 | 5804 | 63868 |
| 11 | 0 | 68432 |
| 12 | 0 | 59233 |
| 13 | 0 | 39268 |
| 14 | 0 | 18108 |
| 15 | 0 | 5040 |
My evolution for puzzle solvers for this game over the years is interesting in it's own right. The first solvers were brute force with minor optimizations. They were written in C, but ran on machines that required hours of CPU time to solve a single puzzle. It would take years to generate all solutions of length 9. Modern computers are easily 10,000 times faster, so it would only be days now. But an idea came to me for solving all puzzles of length 9. One started with the solution. Each possible last move was generated, and that puzzle with the final solution move was recorded. Then, each of those puzzles was subjected to a single move as well. If the puzzle had already been recorded, the solution was ignored. Otherwise the puzzle and the move to get to a solved puzzle was recorded. This proceeded until all puzzles were recorded. In the early 1980's 362,880 puzzles would not fit into RAM except on large and expensive machines. For home machines the answers had to be stored on disk, and searched. By the late 1980's, everything could be kept in RAM even on home machines. But then my buddy Karl came up with an algorithm that could get an optimal solution to a single puzzle essentially instantly. It was so fast that it was quicker to generate puzzle solutions independently than all at once. And, no significant data needed to be stored. Further, with it's principals, one could solve any puzzle by hand, and quickly. Now that this has happened, i don't play the game very much. It's peculiar, but one of the goals of writting these puzzle solvers is to render the game less interesting. And this is done by attempting new techniques, and using the computer to prove that they are, in fact, essentially perfect. In this case, the computer didn't teach me the techniques, however. I have other, more complicated games, where the computer's brute force searching has lead to strategies that can be learned from study of the computer's play.
One of the problems that had yet to be solved in the previous post is creating a new puzzle. And, the cat was let out of the bag, in that the HP-28 has no way to write into the middle of a string. However, we have already created a function, REV, that changes a string. Further, if the string starts out with no duplicate characters, it stays having no duplicates. So one idea is to call it a bunch of times with random reversals. The idea is to call the function often enough to randomize the string, but not so often as to take too long. After some experimentation, 30 calls was settled on.
« 1 30 START
RAND 8 * 2 + FLOOR REV
NEXT
» 'SHFL' STO
Back in 1988, i had an 7.16 MHz 8088 based PC clone (switch selectable to 4.77 MHz). There were two C compilers. Microsoft C 5.0, and Borland's Turbo C. Turbo C seemed like it was infinitely faster. But timing tests showed that it was only three times faster at compiling programs. The expectation was that Borland must have been at least ten times faster. But here's what was really going on. Compiling a program that took 20 seconds in Microsoft's compiler would take about 7 seconds to compile with Borland's Turbo C. It turned out that typical C program files acted exactly this way. For larger projects, there were multiple files, and each turned out to take about 7 seconds to compile with Turbo C. When modifications were made to a program, typically only one file would be edited, and the compiler was smart enough to compile only the edited files. The developer experience was that Turbo C was always quick, while Microsoft C was always slow. It was worse than this, though. Turbo C really understood the new ANSI (now ISO) C standard, where Microsoft C 5.0 would only happily ignore the new syntax. And i encountered real working C code that would actually crash the Microsoft C compiler. Microsoft could refund me the more than three times higher cost any time now, and i wouldn't be quite so pissed off.
All this to say that while this solution is quick to write, it isn't very satisfactory. However, i went on to write the rest of the game.
« CLLCD
"abcdefghi" SHFL
1 'BMV' STO
WHILE DUP "abcdefghi" ≠ REPEAT
BMV 1 DISP 'BMV' 1 STO+ DUP 2 DISP
DO UNTIL KEY END STR→ REV
END
2 DISP
"You win" 3 DISP
'BMV' PURGE
» 'BACK' STO
A closer look at the program shows that it starts with the solution and passes it to SHFL. It creates the variable BMV, with the value 1. This is the count of moves so far. It might have meant "Backwords MoVes". It then loops until the string is the solution string. It displays the current puzzle and the current move. The DO UNTIL KEY END is an idiom to get a character for input. The KEY function does not wait for a key press. There isn't a built-in function to wait for a character. So you must provide the wait loop yourself. The key comes back as a string. What was needed was a number. The STR→ function converts the string into a number. It wasn't my first guess for a function that might do that. It's passed to REV without any error checking. I mean, you could press anything. If you press the SOLV button, it turns out that STR→ can't convert that to anything. The machine beeps, prints "Bad Argument Type" on the screen and stops. There's lots of crap on the stack, too. Not very graceful. If you press the zero key, the first character is duplicated in the string. Pressing the '1' key reverses one character, which is to say that it does nothing. But it does increment your move count. So, confine your entries to 2 through 9. Back in the day, i might have performed better data entry checking. Call me lazy.
Next up is a diversion about playing this game.
How about a game that fits on this tiny machine that's fun to play? With 2 KB of RAM, this is tough. Fortunately, i have a copy of 101 BASIC Computer Games, which was written mostly using a PDP-11/70 (i got to log into the actual system, briefly, way back when). Anyway, there's a fairly simple game called REVERSE. I call it Backwords. Yes, it's misspelled. I've used the game for a couple purposes over the years. One is as an early program to write in a variety of languages. So, I have versions in C, Perl and OPL.
The game is pretty simple to play. In the original version, the digits from one through nine are used, but as ordered symbols. They start out in non-repeated random order. At each move, the user can reverse the first few digits. That is, the first two, or the first three, or, ... and on up to the first nine digits. The goal is to sort the digits from 1 to 9. Here's an example.
342156789 - Reverse 2 to get
432156789 - Reverse 4 to get
123456789 - you win.
The first obviously hard problem to solve is to take a string and reverse the first n symbols in it. Imagine a function named REV that takes two arguments, a string, and a number, and returns a string. First, my solution.
« DUP2
DUP 'ACNT' STO 1 SWAP START
DUP 1 1 SUB
SWAP 2 20 SUB
NEXT
DROP "" SWAP 1 ACNT START
SWAP +
NEXT
SWAP DROP SWAP
ACNT 1 + 20 SUB +
'ACNT' PURGE
» 'REV' STO
But back to this function. You feed it a string, like "abcdefghi", and a number, like 3, and it spits out "cbadefghi". A little study of the function reveals that 'ACNT' is a variable used to remember something. It is incremented in the 2nd loop. The value is used with SUB, which is a built in string function to get part of a string. This variable is deleted before the function exits. Beyond that, we really need some stack diagrams to see what's going on. Here, the function is called with a string, and is directed to reverse the first 3 symbols. The stack trace is transposed so that time goes down, and the top of the stack is to the right. That's because web pages are potentially infinitely long, but in this forum, narrow. Stacks are often quite shallow. I'd only seen stack traces horizontally, but i find that i like them vertically now. Note that the Start at the begining is just to show what's on the stack before this segment runs. The START at the end is the loop START function.
| Start | "abcdefghi" | 3 | ||||
|---|---|---|---|---|---|---|
| DUP2 | "abcdefghi" | 3 | "abcdefghi" | 3 | ||
| DUP | "abcdefghi" | 3 | "abcdefghi" | 3 | 3 | |
| 'ACNT' | "abcdefghi" | 3 | "abcdefghi" | 3 | 3 | 'ACNT' |
| STO | "abcdefghi" | 3 | "abcdefghi" | 3 | ||
| 1 | "abcdefghi" | 3 | "abcdefghi" | 3 | 1 | |
| SWAP | "abcdefghi" | 3 | "abcdefghi" | 1 | 3 | |
| START | "abcdefghi" | 3 | "abcdefghi" |
So, let's consider the loop. Here, the original arguments are not modified in the loop. So, they're not considered in the stack trace.
| Start | "abcdefghi" | |||
|---|---|---|---|---|
| DUP | "abcdefghi" | "abcdefghi" | ||
| 1 | "abcdefghi" | "abcdefghi" | 1 | |
| 1 | "abcdefghi" | "abcdefghi" | 1 | 1 |
| SUB | "abcdefghi" | "a" | ||
| SWAP | "a" | "abcdefhgi" | ||
| 2 | "a" | "abcdefghi" | 2 | |
| 20 | "a" | "abcdefghi" | 2 | 20 |
| SUB | "a" | "bcdefghi" | ||
| NEXT | "a" | "bcdefghi" |
The next code segment pops each of these characters off the stack and builds a new string. Since they were pushed onto the stack in order "a", "b", "c", they are popped off of the stack in the order "c", "b", "a", which is the reversing that we want.
DROP "" SWAP 1 ACNT START
SWAP +
NEXT
The last bit does some stack cleanup, gets the rest of the string from the original, and pastes the reversed part and the rest of the string together. There's no loop here, but you may have forgotten some of the stack from the start of the function. So, we'll do a stack trace.
SWAP DROP SWAP
ACNT 1 + 20 SUB +
'ACNT' PURGE
| Start | "abcdefghi" | 3 | "cba" | |
|---|---|---|---|---|
| SWAP | "abcdefghi" | "cba" | 3 | |
| DROP | "abcdefghi" | "cba" | ||
| SWAP | "cba" | "abcdefghi" | ||
| ACNT | "cba" | "abcdefghi" | 3 | |
| 1 | "cba" | "abcdefghi" | 3 | 1 |
| + | "cba" | "abcdefghi" | 4 | |
| 20 | "cba" | "abcdefghi" | 4 | 20 |
| SUB | "cba" | "defghi" | ||
| + | "cbadefghi" | |||
| 'ACNT' | "cbadefghi" | 'ACNT' | ||
| PURGE | "cbadefghi" |
At this point, the Forth programmer is thinking, "Why was 3, which is the number of characters to reverse, remembered on the stack for all this time, just to DROP it? And indeed, code like "SWAP 1 + SWAP ROT ROT 20 SUB +" would work as a replacement for the above sequence. It's the same length and doesn't need the 'ACNT' variable. Since it is the same length, it is likely to run in about the same amount of time. Then, one could also rewrite the rest of the function to eliminate the 'ACNT' variable. And that rewrite might be slightly shorter or longer. It's highly likely that Forth programmers do this sort of thing all the time. But consider that the 'ACNT' variable is labeled, whereas none of the stack positions are labeled. It's kind of like Star Trek, where on the bridge, there are huge panels full of unlabeled buttons. Labeled variables are easy to read.
It must be admitted that it was my intent to rewrite the function to eliminate the variable. It was easiest to get the gist of the function working by using the variable. I thought of it as a crutch to get the function written. But having gotten it written, it seems to me that it's better the way it is. A down side of using a variable is that some other program might accidently use the same name. Since programs all use the same name space, this function could possibly delete a variable that is in use by some other program. Yet, i have total control over the calling program, so it's simply not going to be an issue. And, a naming convention, such has using the name of the function as a variable name suffix, would also make this a non-issue. On the HP-28C, there's only 2 KB of RAM. That's not much for code and data. On the HP-28S, there's 32 KB of RAM. But even there, only the calling program parts of this function need be considered. Only one program can run at a time.
This function is basically enough to play the game. Key in "abcdefghi", the enter button, and a number like 3. It runs pretty fast, and returns "cbadefghi". It's reasonably fast because the two loops combined have ten functions. If you reverse 9, that's 90 functions. There are another 10 odd functions, so the maximum is about 100 functions. That takes place in a hair over a second. One should see how it performs on unexpected input. Key in other numbers and make sure they return the right result. Does it work right if you try to reverse 1 character? How about something longer than the string, like 20? How about zero? How about -2? Frankly, i was satisfied with it's behavior with unexpected input. This can be controlled by calling code anyway.
One of the design notes that was skipped is interesting. The stack is used to effectively reverse characters in the string. But in a language like C or Perl, it's more likely that pairs of characters would be swapped, using a temporary variable. And that's because in those languages, one can subscript into a string and write whatever you want there. The HP-28 RPL language does not have a function to put a character into the middle of a string. So this simply isn't possible. In fact, i considered not using strings at all. I considered using an array for everything. Arrays can't hold characters or strings. So i'd have to either convert the result array to a string or come up with some other way to display it. It's very likely that using arrays on this calculator would require less code that executes faster on this machine, despite conversion.
This function isn't the whole game. It doesn't know if you've "won", and it doesn't create a randomized string for a starting problem. What is amazing is how much code is needed to turn this almost-game into a game. And that's in the next post.
GCD implements an interesting way to compute Greatest Common Divisor. You remember fractions? Let's say you need to reduce the fraction
210
---
462
210 105
--- = ---
462 231
210 105 35
--- = --- = --
462 231 77
210 105 35 5
--- = --- = -- = -
462 231 77 11
int gcd(int a, int b) {
if (a == b) return a;
if (a > b) {
return gcd(a - b, a);
} else {
return gcd(a, b - a);
}
}
Anyway, the recursive version of this function on the HP-28 looks like this.
« DUP2 IF == THEN
DROP ; return a
ELSE
DUP2 IF > THEN
SWAP DUP ROT - SWAP GCD ; return gcd(a-b, a)
ELSE
SWAP DUP ROT SWAP - GCD ; return gcd(a, b-a)
END
END
» 'GCD' STO
| Level | Start | SWAP | DUP | ROT | - | SWAP |
|---|---|---|---|---|---|---|
| 3 | b | a | ||||
| 2 | a | b | a | a | a | a - b |
| 1 | b | a | a | b | a - b | a |
| Level | Start | SWAP | DUP | ROT | SWAP | - |
|---|---|---|---|---|---|---|
| 3 | b | a | a | |||
| 2 | a | b | a | a | b | a |
| 1 | b | a | a | b | a | b - a |
« 50 200 FOR x
x
x 1 + x GCD
DROP DROP
NEXT
» 'G' STO
int gcd(int a, int b) {
while (a != b) {
if (a > b) {
a = a - b;
} else {
b = b - a;
}
}
return a;
}
« DUP2 WHILE ≠ REPEAT
DUP2 IF > THEN
SWAP DUP ROT - SWAP ; repeat with gcd(a-b, a)
ELSE
SWAP DUP ROT SWAP - ; repeat with gcd(a, b-a)
END
DUP2 ; for the != in WHILE
END
DROP ; drop one of the equal a, b.
» 'GCD2' STO
Is the iterative version easier or harder to follow than the recursive version? This is an important maintenance question. And it isn't clear that either one are very easy. After all, even having stepped through an example, it's not clear how the subtractions end up giving you what amounts to division. And the begining of the answer is that repeated subtraction can be used in division. Think about how you have to guess the next digit in long division. You don't really have to guess. You can try the digit one, do a subtraction of the divisor, and repeat this (while incrementing the answer digit) until just before it goes negative. But that's just a hint. On the other hand, the nonrecursive version demonstrates that nothing needs to be remembered and that the only time something is returned is when a == b.