Stack Complexity
Thursday, July 11, 2024
We had a recent discussion on the Factor Discord server about how to deal with stack shuffling, and when and how you might use the various dataflow combinators that are available in Factor. Some of the impetus for this discussion was my article about intersecting ranges and the contrasting implementations that it presented.
The question that started it all:
Overall very complex stack states.
How do you write code like that?
Are you in the listener or debugger making these changes one by one?
When I write even simple code I get lost when there’s 3+ things on the stack I have to work with and spend more time figuring out stack order and correct combinators than with the actual problem. I wonder if there’s a good workflow where I can write the code iteratively.
Let’s go over some of the advice that came up in that conversation:
Mockups
I often write Factor code iteratively lately. Usually I come up with a tiny increment that preserves the stack effect, then reload the code to compile it.
For example, if I need to create a word with effect
( a b -- )
, I will start with the minimal implementation like: word ( a b -- ) 2drop ;
Mocking stuff every step of the way, compiling after each step.
For stuff inside a word that seems to be complex from the outset, I would create a mock
(word)
with defined stack effect. I will inline it later if it turns out to be simpler than expected (because I find something in the standard library that would do most of the work).
Combinators
Some things come with practice, that’s true. But some tips:
- Keep the stack shallow, 1-3 things and it often works best.
- More than that and don’t worry about using named local variables.
- Try and use spread / apply / cleave combinators to represent your logic as dataflow.
Monoliths
I like writing monoliths, so sometimes i write a
::
version first and refactor to:
later, especially if I don’t know how the data will flow exactly.When I’m porting code from other languages, I start with locals to keep the code blocks similar. Structure starts to become obvious once you have a big block working.
Practice
Sometimes writing inline comments showing what the stack effect is after each block of code, is useful to visualize the stack effects of the intermediate parts.
Sometimes there are words like vector operations that clean up an iterative piece of code.
But mostly, keep practicing. Ask questions. Maybe get feedback on your code from us here or other devs you like to work with.
And sometimes despite trying; it stays a big block of code that works, you shrug and move on to the next thing.
Variables
One way to reduce stack depth is, for example, to put some state on the namestack as a variable. That is how our stream words work (e.g.,
read
,write
,vector
orhash-set
variable to accumulate into.
Reordering
Oh, and one advanced tip: sometimes reordering the arguments on the stack to avoid shuffles or allow using
with
/map
/curry
/fry
things helps a lot.It takes a long time to “get” it, but there are some rules that help; the order of your arguments to your words matter a lot. If you find yourself juggling a lot, consider a different argument order of your word. Stuff new context “under” your arguments with
dip
, e.g. a vector to collect stuff in. Look at accessors and see why they’re designed with a specific word order; typically things that are “parameters” go on the top of the stack (e.g. quotations). Alsoassocs
have a certain logical order to them. If you understand that, it becomes more natural, with the occasional stack shuffle still needed.And don’t forget about tuples; that quickly reduces the amount of things on the stack
Good luck and happy coding!