Hacker News new | ask | show | jobs
by mark-probst 1890 days ago
I'm the original author. Thank you for your explanations! I added a link to this post to the README.

I believe the reason I did all the stack juggling was because I wanted to write it "the Forth way", or maybe the "pure stack-based way", and using variables seemed like cheating.

I certainly won't be pursuing this further (I wrote it 20 years ago as a programming exercise), but I hope somebody will learn from your exposition :-)

1 comments

I'm glad they were enjoyable!

I think "using variables seemed like cheating" was a lot of my motivation, too, and it led me into a great deal of mischief. Despite what I thought at first, I think "the Forth way" does use variables pretty often, although I guess different people's "Forth way" is different. But consider Chuck Moore's Forth Way:

> A Forth word should not have more than one or two arguments. This stack which people have so much trouble manipulating should never be more than three or four deep. ... But as to stack parameters, the stacks should be shallow. On the i21 we have an on-chip stack 18 deep. This size was chosen as a number effectively infinite.

> The words that manipulate that stack are DUP, DROP and OVER period. There's no ..., well SWAP is very convenient and you want it, but it isn't a machine instruction. But no PICK[,] no ROLL, none of the complex operators to let you index down into the stack. This is the only part of the stack, these first two elements, that you have any business worrying about.

> The others are on the stack because you put them there and you are going to use them later after the stack falls back to their position. They are not there because [you're] using them now. You don't want too many of those things on the stack because you are going to forget what they are.

> So people who draw stack diagrams or pictures of things on the stack should immediately realize that they are doing something wrong. Even the little parameter pictures that are so popular. You know if you are defining a word and then you put in a comment showing what the stack effects are and it indicates F and x and y

    F ( x - y )
> I used to appreciate this back in the days when I let my stacks get too complicated, but no more. We don't need this kind of information.

http://www.ultratechnology.com/1xforth.htm

I was trying to find the "sheesh, just use a variable" quote I seem to remember from him, but I can't find it. Maybe I'm inadvertently attributing my own ideas to him. But if you look at his code (there are some excerpts in http://www.ultratechnology.com/fsc98.htm and http://www.ultratechnology.com/tape1.htm) you'll see he's pretty sparing with stack operations and uses variables (in memory) pretty regularly.

Certainly my recommendation here—start with statements and expressions, use lots of variables—differs from, say, Jeff Fox's recommendation. And I'm pretty sure Jeff Fox was a better Forth programmer than I am. And I think it's common that, with enough thought, you can find a better way to design the code that reduces the amount of state you have to keep at memory addresses. But I think a programmer already experienced in another language is much more likely to shoot herself in the foot in Forth by using too many stack operations and too many values on the stack, than by using too many variables, so I think it's probably a better learning path.

(FWIW, I think the advice to not write stack comments is probably bad advice, even though Chuck Moore was and probably is a much better Forth programmer than I am.)

Also, though, and I feel like I should have emphasized this more from the outset, I have never shipped code to users in Forth. In fact, I don't even have any personal utility programs written in Forth. That's because I still find Forth hard to read, write, and debug, despite being fascinated with it for 25 years. I think my motivation for writing the above readprint.fs code was to sort of see how terrible I was at writing Forth (answer: it took me 2 hours to write 30 lines of code, so still pretty terrible, but at least I did manage to write a working parser.) So please take my opinions on the matter with a grain of salt.

The Lisp adage about '<something, something> 100 functions with 12 data types vs <something, something>' seems to relate treating the argument stack as the tuple so that you can have multiple functions that take the var-arg union of the shape of the stack. I don't think I am explaining it very well, but I think this is the gist of how one constructs algebras or monoids over the shape of the stack.
Perlis's epigram: "It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures."

It's not strictly about Lisp; Perlis was fond of Lisp but his true love was APL. But you could use it to advocate JSON, bytestream shell pipelines, or even TCP/IP. Or a flat byte-addressable memory, I suppose, like Forth or amd64.

Are you thinking of something like the static typing system of Christopher Diggins's "Cat" language, or its children Kitten and Mlatu?

Heh, the kragen[hashmap] comes through. Thanks for the quote.

While those are interesting, probably in the same way that Shen is interesting, I was thinking more along the lines that stack is an open ended product type (I made that up) and that operations on the stack are like a zipper, map, fold, product. That there is a projectional aspect to the stack, its expansion and contraction and shape over time.

The engines that do protein folding feel like they have similarities.

I still haven't grokked your whole description of your Lisp reader, I'll have to sleep on it. Is it related in structure to the METAII meta compiler or parser combinators?

That sounds like the insight cdiggins based Cat's type system on, but I'm not entirely sure in part because I don't really understand Cat's type system. As an example, though, for the code

    popop    = { pop pop}
Cat infers the parametrically polymorphic typing judgment

    popop    : ('t0 't1 't2 -> 't2) 
where 't2 is the type of the rest of the stack, I guess, and juxtaposition of types is a sort of product operation (noncommutative, but I think associative, and thus perhaps a monoid).

Not sure what you mean about "projectional" — do you mean that "pop" is a "projection" in the relational sense, in that it maps, for example, the stack 3 8 1 (with the top on the left) and the stack 7 8 1 to the same resulting stack state 8 1?

I don't know anything about protein folding.

> I still haven't grokked your whole description of your Lisp reader, I'll have to sleep on it. Is it related in structure to the METAII meta compiler or parser combinators?

Well, I didn't really write much of a description of the reader. It's a recursive-descent predictive parser, same as Probst's reader, and probably most Lisp readers. READ sets up the input pointers and calls (READ), which is ((READ)), which discriminates between lists and atoms (which are numbers) by looking for a "(". If it doesn't find one, it calls READ-NUM to read a number. But if it does, it calls READ-TAIL, which recursively reads the list contents (by calling (READ)) until it finds a ")", then returns back up, consing up the list as it goes. Probst's code works the same way, with the correspondences READ ↔ LISP-LOAD-FROM-STRING, (READ) ↔ LISP-READ-LISP, ((READ)) ↔ _LISP-READ-LISP, READ-NUM ↔ LISP-READ-TOKEN/-NUMBER/-SYMBOL, and READ-TAIL ↔ LISP-READ-LIST.

META II has the similarity that it generates recursive-descent parsers with predictive parsing, but the dissimilarity that it's a domain-specific language for writing parsers. Parser combinators are a technique for embedding any domain-specific parsing language in a general-purpose host language, regardless of what parsing algorithm is used, though Packrat may be the most common choice, and Packrat has certain similarities to recursive-descent parsing.

Is that helpful?

You could follow Lisp's car/caar/caaar cdr/cddr/cdddr cdadar/cadadar/caaddaar naming conventions in Forth ("waiting for the other shoe to drop") or PostScript ("waiting for the other shoe to pop"):

Forth:

    : droop drop drop ;
    : drooop drop drop drop ;
    : droooop drop drop drop drop ;
PostScript:

    /poop { pop pop } def
    /pooop { pop pop pop } def
    /poooop { pop pop pop pop } def
> Is that helpful?

Absolutely. It made my day. Thank you.

Very