
Why recursive data structures? - udfalkso
http://raganwald.com/2016/12/27/recursive-data-structures.html
======
Const-me
Apparently, every computer science problem has easy to understand solution
that’s easy to implement but awfully slow in practice.

This is one of them.

Rotating images that way isn’t CPU bound problem. It’s RAM bandwidth and RAM
latency bound problem. The optimal data structure for that is not recursive,
it’s 2D array of small square blocks of pixels. If pixels are bits, I’d bet on
either 8x8 or 16x16 blocks. If they are RGB, one good format for those images,
optimized for rotations, is BC7
[https://www.opengl.org/registry/specs/ARB/texture_compressio...](https://www.opengl.org/registry/specs/ARB/texture_compression_bptc.txt)
the decompressor being implemented right in the graphics hardware.

> If we were writing an image manipulation application, we’d provide much
> snappier behaviour using coloured quadtrees to represent images on screen.

That statement was true 30 years ago, no longer a case. Branch misprediction
is expensive, memory latency is huge, RAM is a block device now (the block
size typically being 16 bytes for dual-channel DDR). In addition, some
displays now show 8+ megapixels, and most show 16M colors.

For an image manipulation application, that recursive thing ain’t good at all.

~~~
munificent
> The optimal data structure for that is not recursive, it’s 2D array of small
> square blocks of pixels. If pixels are bits, I’d bet on either 8x8 or 16x16
> blocks.

If the image is very large and contains large contiguous regions of solid
colors, then a quadtree that doesn't subdivide homogeneous regions can be a
much faster approach than a flat pile-o-pixels.

Either way, this article isn't about rotating images.

~~~
coldtea
> _If the image is very large and contains large contiguous regions of solid
> colors_

In other words, if the image is not very image-y? (but more vector-y).

~~~
OskarS
It's an entirely plausible scenario. It will rarely be the case with
photographs, but graphical images often feature large solid color blocks. It's
large reason why the PNG file format exists.

------
iamleppert
Recursion is often times less efficient than a well designed and concrete
imperative solution, even if the imperative solution uses more memory. This is
because most languages, the stack is not free. There's a lot of meta data and
house keeping that goes on when you invoke a function to support things like
stack traces and introspection.

The provided example (at least to me) is also convoluted and fails to
illustrate the elegance of using a recursive data structure.

In general parlance, an affine transform or rotation matrix would both be more
elegant and readable.

~~~
devty
Are you aware of a programming language/compiler that optimizes on recursively
written code? I'm finding it difficult to imagine a PL where stack _isn't_
free except tail-recursive code.

~~~
andreareina
My understanding is that it's the opposite -- function calls expend a cost to
save (and later restore) the registers and push the locals onto the stack.
Tail recursive functions on the other hand can be implemented as a jump to the
top of the function, right after all the stack bookkeeping, and so saves on
that expense.

~~~
junke
Tail-merging doesn't only apply to recursive functions. As soon as you don't
need to store intermediate result, you can jump to another function without
allocating a frame.

------
johnfn
I really enjoyed this article because it introduced me to a new data structure
and some very elegant and (subjectively) beautiful algorithms. I came onto HN
expecting some insightful commentary and instead got 80 comments griping about
how it wasn't 100% optimized.

Sigh.

~~~
braythwayt
Author here. Nevertheless, there's learning gold in those comments. Take the
gold, leave the mud.

------
mypalmike

      maxIndex = size - 1
      for x in range(0, size):
        for y in range(0, size):
          # swap diagonally, invert horizontally
          out[x][y] = in[maxIndex - y][x]

------
teddyh
The algorithm to rotate images can be illustrated by running

    
    
        /usr/lib/xscreensaver/blitspin
    

_blitspin - rotate a bitmap in an interesting way_

 _The blitspin program repeatedly rotates a bitmap by 90 degrees by using
logical operations: the bitmap is divided into quadrants, and the quad‐ rants
are shifted clockwise. Then the same thing is done again with progressively
smaller quadrants, except that all sub-quadrants of a given size are rotated
in parallel. So this takes O(16×log2(N)) blits of size NxN, with the
limitation that the image must be square, and the size must be a power of 2._

Movie here, for those without XScreenSaver installed:

[https://www.youtube.com/watch?v=UTtcwb-
UWW8](https://www.youtube.com/watch?v=UTtcwb-UWW8)

~~~
braythwayt
Thanks, I have included the video and a credit to you in the essay.

------
cousin_it
Quadtrees are neat. Though I guess general purpose image processing would be
better served by either raster or vector representation. Maybe a better
showcase would be something like collision detection or geospatial indexing.

~~~
phkahler
I have a bit-rotting ray tracing library where I put all geometric primitives
in an oct-tree. When you fire a ray through a 2x2x2 cube it intersects at most
4 of the child nodes. You can see that because the ray must originate in one
octant, and it must cross one of the 3 partition planes to reach another
octant and it can only cross each one once. There are lots of other
interesting properties.

One of my favorite algorithms is one that takes an oct-tree and a triangle and
inserts the triangle into the tree. I allow primitives to occupy more than one
tree node, so first I decide what size node they will occupy and then I use a
recursive algorithm to "voxelize" the triangle. I found a heuristic for
determining optimal node size.

The ability for node to contain and arbitrary number of primitives and for
each primitive to be in an arbitrary number of nodes resulted in a very
interesting data structure that allows fast deletes and inserts of the
primitives. But I'm really starting to ramble now.

~~~
SomeStupidPoint
Could you go on a bit more (or post a link to somewhere you do)?

~~~
phkahler
I've been hoping to put the code online for a long time, but I'm suffering
from "it's not cleaned up enough" syndrome. But I'll talk a little here.

I'd been after real time ray tracing since the 1990's so I had a goal of
putting both static environments and dynamic objects/players in the same
acceleration structure. A lot of the old BSP-tree stuff or other BVH are used
only for static geometry and I didn't like that. The problem with those
structures is that if you move some things around it may require regeneration
of the entire structure. Or at the very least, local updates that result in a
different structure than if you regenerated the entire thing.

Part of the reason for that is trying to partition the geometric primitives
roughly in two groups so a partition roughly cuts the scene in half. I decided
that with a regular octtree structure, which nodes a primitive is contained in
could be independent of the other primitives. They may share tree nodes, but I
never decide whether to split a node based on content - instead each primitive
is inserted into the nodes that make sense for it. This does lead to a few
nodes having a lot of content - thing of the node that encloses a triangle fan
- but those are rare and you won't typically be shooting lot of rays through
them.

Also, with the primitives deciding what nodes they occupy, we can now do local
deletes and inserts without having to regenerate the whole world structure.

For inserting a primitive we take its bounding box. If that is outside the
root box of the world, I repeatedly double the size of the world by adding a
new root node and placing the existing roots children and some new parents
inside it - that's a neat recursion too. Next, we traverse the tree from the
root calling on only the children that overlap the bounding box until we reach
the node size that was requested. This decent is trivial for bounding boxes,
but doing it with high precision specifically for triangles was harder.

Once the nodes that need to contain a primitive are reached, it's a matter of
adding that primitive to a linked list off the tree node. But each primitive
will need to exist in multiple lists. This means not putting the primitives
directly in a list, but having a list of little link structures that are added
to the nodes list and point back to the primitives.

For deletions, each primitive will need a list of link nodes that point back
to it. Deletion consists of following this list of links and deleting them
from the list that contains them.

So the links themselves will each be a part of two lists. They are part of a
list coming off a primitive and they are part of a list coming off the tree
node. It took a lot of thought to shrink the link node data structure, but at
present it is 4 pointers in size. It needs to contain two "next" pointers, a
"previous" pointer (to allow deletion from the tree nodes list of primitives),
and it also has to have pointers to both the tree node AND the primitive.

The reasons for those last two pointers is thus: when checking which
primitives a ray intersects, we want to start at the tree node that contains
it and quickly get to the primitives for ray/object intersection tests. This
is done by following the list of links via their "next" pointer which also
following their links to "primitive". For deletions, we start at the primitive
and follow the other set of "next" pointers and do deletes from the other list
by using both the next and previous pointers. We never need a previous pointer
for the list off the primitive because that entire list is always deleted,
where the other list may still contain other primitives. There remains a
problem that when an Octree node becomes empty, I need to prune back the tree
- this is why the links need to also contain a pointer back to the tree node
that contained the link. So now we're looking at a link structure that
contains 5 pointers {2 nexts, one previous, one primitive, and one treenode}

Then I had the insight that no matter which list we're traversing, we always
have a pointer to either the treenode or the primitive. So those two pointers
can be combined via sum or XOR into a single value. If your deleting a
primitive you just XOR a pointer to the primitive with that value to recover a
pointer to the tree node. When you're doing intersection tests, you XOR the
treenode pointer with that value to recover a pointer to the primitive. This
change resulted in no significant performance change while reducing the
structure to a nice size of 4 pointers.

Another problem was that having a linked list with a previous pointer meant
the octree node needed to always contain one of these link structures because
deletion depended on that "back" pointer pointing at another link. Turns out
you can have the back pointer point directly to the "next" pointer of the
previous link. That means the octree node now only needs a single pointer to
the first "link" node in the list and that links back pointer points directly
to that pointer.

So for fitting an arbitrary number of things (primitives) each into an
arbitrary number of boxes (tree nodes) we add a single pointer to the thing
structure, a single pointer to the box structure, and then use these little
link structures that are the size of just 4 pointers.

It was a long journey to arrive at that data structure, but for a while it was
real point of pride for me. The structures are nice, but the code is still a
bit messy with the remains of several experiments still around. Also notice
that the list in one direction does not have a back pointer, so you can never
delete a box directly - well you could but it's not as easy. There isn't
complete symmetry between the boxes and things.

Is that enough rambling for now? ;-)

~~~
cousin_it
Nice puzzle! I think you can allow both kinds of deletions while still using
four pointers per link node:

* prev1 XOR next1

* prev2 XOR next2

* prev1 XOR prev2

* data1 XOR data2

Because while traversing you always know one of {prev1, prev2} and one of
{data1, data2}. Right?

------
skybrian
Before reading this, I would have said that we need to use recursion to model
constructs like programming languages that are specifically designed to have a
recursive structure. They are naturally recursive because someone designed the
language that way. (By contrast, building a basic database-backed website
doesn't require any recursion, unless you're modeling a hierarchy.)

But with a compiler, there is the same pattern of converting to a recursive
data structure (parsing), performing manipulations, and then converting back
to a non-recursive structure (object code generation).

------
dukerutledge
Reginald is definitely hinting at hylomorphisms here. I'd be curious to see if
stream fusion could be achieved in JS for a hylomorphism.

~~~
braythwayt
Isn’t `multirec` a function for making hylomorphisms? The recursive dividing
is the anamorphism, and the combining of the results is the catamorphism.

As written, however, `multirec` does not efficiently compose. For two
constructions to be compatible, they’d have to share their decomposition
strategy, including `indivisible` and `divide`. `value` probably composes
neatly, but `combine` is a little thorny as the current implementation does
two jobs: Processing the data and reconstructing the form of the data.

A composable `multirec` does sound interesting, however. Hmmm...

------
taeric
This is worth it, if only for the footnotes. Don't skip them!

------
c2the3rd
I liked this article because I enjoy reading about data structures that can be
used in places where the "obvious" implementation is a list of lists. A lists
of lists data structure is general enough for many applications and readily
available in many languages, but when using it, I often get the sense that I
am spinning wheels writing far more code than necessary to do the desired
manipulations. It makes me happy to read about more novel solutions.

------
Greek0
The argument would be stronger if he wasn't swapping an O(n) algorithm for O(n
log(n)). With the recursive implementation you have to touch every element
once for each recursion level.

~~~
braythwayt
For quadtrees come into their own when there are optimizations that fit the
problem domain, such as the coloured quadtrees explained at the end of the
post: They are much faster for images with large blank areas.

They aren’t discussed in the post, but quadtrees are also very amenable to
memoizing common operations.

------
eridius
The footnotes say that all the functions can be adjusted to deal with
non–power-of-2 squares, but it's not obvious how this is done when working
with region quadtrees.

~~~
braythwayt
One way is to represent squares as a square-that-is-a-power-of-two, plus a
“clipping size” that is used when the square is rastorized. If you’re using a
lot of optimizations for blank regions, the extra padding has a negligible
effect on performance.

~~~
eridius
I suppose that would work, but all the operations you do on the region
quadtree have to be aware of the "clipping size" as well. For example, the
rotation transformation that this article implements would also have to rotate
that "clipping size" as the unwanted portion of the square is now in a
different location.

~~~
braythwayt
That depends If you can live with evenly-sized squares, you can always center
the clipping square, and you’re good for rotations, flips, and so on.

If you want odd-sized squares, or other shapes, then yes, you’ll need to know
how to transform the clipping path/description as well.

All in a day’s work!

~~~
jlas
One can use recursive properties better if the structure divides equally. So
if not having a strict quadtree is okay, they can divide the square into 3x3,
5x5, etc

------
zachrose
The 'multirec' function here looks to have a lot in common with Clojure's
transducers. Can anyone more knowledgeable do a compare and contrast?

~~~
braythwayt
`multirec` is a combinator, it transforms functions into other functions.
`transducers` are "composable algorithmic transformations.”

`multirec` as presented here is not composable the way transducers are
composable. It is, however, an extremely stimulating thing to consider.

[https://news.ycombinator.com/item?id=13305176](https://news.ycombinator.com/item?id=13305176)

------
noobiemcfoob
Today I learned a different, much more harsh meaning for "bespoke"

I've never picked up on a hipster connotation from the word, though I did feel
it was coming back into vogue.

~~~
vostok
It's been popular in the derivatives space for a while now.

------
braythwayt
FYI, this article on recursive combinators from 2008:

[http://www.lazutkin.com/blog/2008/06/30/using-recursion-
comb...](http://www.lazutkin.com/blog/2008/06/30/using-recursion-combinators-
javascript/)

------
nemoniac
Interesting article. While reading it I wrote a version of the code in Racket.
YMMV but I think it turns out cleaner, especially towards the end.

[https://gist.github.com/bon/801fdb6dbced0efdb83a358bb4f6285f](https://gist.github.com/bon/801fdb6dbced0efdb83a358bb4f6285f)

------
maxdemarzi
Got an opportunity to do this recently using a graph mapped on to a K^2 Tree
to find out if a relationship existed or not. See
[https://maxdemarzi.com/2016/12/27/connected-2/](https://maxdemarzi.com/2016/12/27/connected-2/)

------
lucker
Umm... or, I could just rotate a square a quarter-turn by iterating over the
square's elements:

for (x = 0; x < N; x ++) { for (y = 0; y < N; y ++) { newSquare[x][y] =
oldSquare[y][N-1-x]; } }

Of course, in certain representations of the square (such as using a 1-d
array) even simpler algorithms can be used.

------
stephenboyd
As a web developer, I appreciate this rare use of JavaScript in a CS lesson.

~~~
braythwayt
JavaScript is the language most likely to be familiar to the audience I'm
trying to reach. But at a certain point, it gets in the way of explaining an
idea rather than making it easier.

I think this essay stays on the right side of that trade-off.

------
iamaelephant
Wait, the way he defines the quad tree color property requires that you
manually input the color for each region. If you wanted this to be part of a
general purpose algorithm in real software then you'd need another algorithm
that determines the color of each region - a very expensive operation if you
wanted the maximum benefit.

------
matt4077
Raganwald is the poet of code.

------
pjc50
But why recursive data structures?

(Both a recursion joke and a Zoolander joke)

~~~
nickkell
[https://news.ycombinator.com/item?id=13308230](https://news.ycombinator.com/item?id=13308230)

------
nitrix
I stopped reading at the first sentence when he improperly used `isomorphic`.
I thought it made no sense at all so I looked up the footnote... just to get a
big middle finger from the author.

What a con artist. Moving on.

~~~
Ericson2314
If you suspend your disbelief until the end, it's an inductive anti-
hypothesis. Induction is recursion under intuitionism.

------
ianamartin
I love the word "isomorphic". It's by far the most efficient word in the
English language.

When someone else uses it in conversation, it's absolutely guaranteed that the
person is a pretentious asshole.

When you use it in a conversation, the same is also true.

It's only ever acceptable in written contexts or if you happen to be Douglas
Hofstadter.

~~~
lacampbell
> When someone else uses it in conversation, it's absolutely guaranteed that
> the person is a pretentious asshole.

I don't get your angle here. You realise an isomorphism is actually a well-
defined mathematical concept, right? Are you advocating we use another,
plainer word for an isomorphism, or do just naturally get angry when you hear
words you don't understand?

~~~
mypalmike
Pretentious or not, the term is used erroneously here.

Recursive data structures and recursive algorithms are, quite plainly, not
isomorphic. They are not two equivalent representations of the same thing.

Something that is isomorphic to a recursive function that works on a quadtree
would be an imperative function that works on a quadtree with identical
functionality.

tl;dr: I think the author meant "synergistic".

~~~
braythwayt
I don’t know that an algorithm and a data structure have to be equivalent
representations of the same thing to be isomorphic. In the case of the
algorithm, is it the code that must be equivalent? Or the runtime behaviour?

The runtime behaviour of the ‘whirling regions’ algorithm clearly is not an
equivalent representation of the two-dimensional array representation of an
image. However, the runtime behaviour of the quadtree algorithm exactly
matches the quadtree data structure.

This is my proposition: That what matters is whether the algorithmic behaviour
is equivalent. I would say the same thing about `binrec` matching a binary
tree.

...Or at least, I would have, up until today. I am open to being disabused of
this notion.

~~~
gohrt
Isomorphic means "have the same shape". It means that there is some structural
property that the two objects have in common. The more interesting/complex the
property is (and the more superficially different the two objects look), the
more defensible the use of the word is.

------
pmiller2
That was a lot of words to say "because the languages we use to solve problems
is naturally recursive."

~~~
jgalt212
true, but sometimes people need to be convinced by a preponderance of
evidence.

------
logicallee
Using the same didactic style as the author, I would like to show how it is
possible to replace the image rotation algorithm with a NOOP. First, look at
any of the pictures shown. Next, turn your head to the side 90 degrees.
Although the computer's output hasn't chamged, by redefining requirements we
can achieve the the desired change. The fastest way to rotate a square is a
noop (or O(1) message to the user): turning your head. Never forget it!

