From kragen at canonical.org Thu Jul 3 03:37:02 2008 From: kragen at canonical.org (Kragen Javier Sitaker) Date: Thu Jul 3 03:37:04 2008 Subject: worms.c: formatting of complicated conditionals Message-ID: <20080523223404.26702183496@panacea.canonical.org> // Diomidis Spinellis's book "Code Reading: An Open Source // Perspective" has this messy code in chapter 2 // > http://www.spinellis.gr/codereading/spinellisch02.pdf // from netbsdsrc/games/worms/worms.c:419: op = &(!x ? (!y ? upleft : (y == bottom ? lowleft : left)) : (x == last ? (!y ? upright : (y == bottom ? lowright : right)) : (!y ? upper : (y == bottom ? lower : normal))))[w->orientation]; // He rewrites it as follows, the idea being to improve its // readability: struct options *locations[3][3] = { {upleft, upper, upright}, {left, normal, right}, {lowleft, lower, lowright}, }; int xlocation, ylocation; // Determine x offset if (x == 0) xlocation = 0; else if (x == last) xlocation = 2; else xlocation = 1; // Determine y offset if (y == 0) ylocation = 0; else if (y == bottom) ylocation = 2; else ylocation = 1; op = &(locations[ylocation][xlocation])[w->orientation]; // In my opinion, the above is actually worse than the original, as a // result of being so much longer. However, then Spinellis suggests // this version, which he credits to Guy Steele: op = &( !y ? (!x ? upleft : x!=last ? upper : upright ) : y!=bottom ? (!x ? left : x!=last ? normal : right ) : (!x ? lowleft : x!=last ? lower : lowright ) )[w->orientation]; // I agree that the code needs rewriting, but inspired by Scheme, I // would do it without magic numbers, without nine separate variables // for the struct options pointers, and with conditions and their // consequents on the same line: struct bar { struct options *left, *middle, *right } upper, middle, lower; struct bar *a = (y == 0 ? &upper : y == bottom ? &lower : &middle); struct options *b = (x == 0 ? a->left : x == last ? a->right : a->middle); op = &b[w->orientation]; From kragen at canonical.org Mon Jul 7 03:37:01 2008 From: kragen at canonical.org (Kragen Javier Sitaker) Date: Mon Jul 7 03:37:02 2008 Subject: better relative URLs than the current ".." system Message-ID: <20080523223404.36643183494@panacea.canonical.org> Relative links are great; they let you move your whole tree of HTML files from one place to another and still retain the internal link structure. However, they start to suffer when you have multiple levels of directory structure: is that `href="../../style.css"` or `href="../../../style.css"`? It's a bit confusing, and even if you don't get confused, you still have to modify links when you copy them from one file to another. What would be more helpful would be the ability to say "up to a directory named foo". Suppose you have this setup: kragen/ index.html resume.html style/style.css images/ kragenlogo.png headshot.jpg blog/ 1.html 2.html archive/ 2008-03.html Now, suppose there's some text in `2008-03.html` that was originally in `2.html` or one of its siblings. It would be nice if that text didn't have to be changed from `` to ``. You can write ``, but in addition to being verbose, that makes it hard to use a tree of HTML that you've downloaded with `wget -r` or something similar. Suppose you could instead write ``, meaning "go up until you find an ancestor directory named `blog`, then use its children". Now you can write things like `` freely, and copy and paste them among all the files. By itself, this would be a backwards-incompatible change to browsers and the URL spec, but it could degrade gracefully. You could program your web server to generate redirects for backwards-compatibility, while implementing the change in newer browsers. Compatibility problems would only arise if someone had a relative link to a directory whose name began with "$" whose name otherwise duplicated that of a directory higher up in the hierarchy. From kragen at canonical.org Thu Jul 10 03:37:01 2008 From: kragen at canonical.org (Kragen Javier Sitaker) Date: Thu Jul 10 03:37:02 2008 Subject: web services, operations as a strategic advantage, and decentralization Message-ID: <20080608210845.34A6C18349B@panacea.canonical.org> >From a [comment on the Smoothspan blog, by Damon Edwards][0] of dev2ops.org: > A troubling trend I've noticed is how the benefits of "rock > star" software development teams (small, highly skilled, > highly motivated) are increasingly neutralized by poor > operations. > > In an ops heavy SaaS and on-demand world, the software > development phase becomes an increasingly smaller part of an > application's overall lifecycle. Time and time again we see > great code sitting behind the bottleneck of QA, staging, > performance testing, and then production deployment. > > On the project plan, the "rock star" teams repeatedly deliver > great code in record time... but at all but the smallest of > enterprises, their "delivery" of code is a long way from where > the business is actually realizing the benefit. I don't know whether this is true or false, but if it's true, it represents the reversal of a [trend touted by Philip Greenspun in 1998][1]: > When I graduated from MIT in 1982, my classmates and I had but one > choice if we wanted to get an idea to market: Join a big > organization. When products, even software, needed to be distributed > physically, you needed people to design packaging, write and mail > brochures, set up an assembly line, fill shelves in a warehouse, > fulfill customer orders, etc. We went to work for big companies like > IBM and Hewlett-Packard. Our first rude surprise was learning that > even the best engineers earned a pittance compared with senior > management. Moreover, because of the vast resources that were needed > to turn a working design into an on-sale product, most finished > designs never made it to market. "My project was killed" was the > familiar refrain among Route 128 and Silicon Valley engineers in > 1982. > > How does the Web/db world circa 1998 look to a programmer? If Joe > Programmer can grab an IP address on a computer already running a > well-maintained relational database, he can build an interesting > service in a matter of weeks. By himself. If built for fun, this > service can be delivered free to the entire Internet at minimal > cost. If built for a customer, this service can be launched without > further effort. Either way, there is only a brief period of several > weeks during which a project can be killed. That won't stop the site > from being killed months or years down the road, but very seldom > will a Web programmer build something that never sees the light of > day (during my entire career of Web/db application development, > 1994-1998, I have never wasted time on an application that failed to > reach the public). And [by Paul Graham in 2001][2]: > One of the most important changes in this new world is the way > you do releases. In the desktop software business, doing a > release is a huge trauma, in which the whole company sweats > and strains to push out a single, giant piece of code. Obvious > comparisons suggest themselves, both to the process and the > resulting product. > > With server-based software, you can make changes almost as you > would in a program you were writing for yourself. You release > software as a series of incremental changes instead of an > occasional big explosion. A typical desktop software company > might do one or two releases a year. At Viaweb we often did > three to five releases a day. I see four possible interpretations: 1. Damon Edwards is wrong, and in fact the software development phase is not "becoming an increasingly smaller part of an application's overall lifecycle" as a result of "SaaS", which is what Graham called "server-based software" and what Philip Greenspun called "a service", and in fact the per-user cost to deploy software is continuing to shrink. This is a plausible answer; Moore's Law continues to grind away giving us more MIPS per watt and MIPS per CPU, the cost of bandwidth was still falling last I checked, and perhaps more importantly, EC2 and S3 and Hadoop and Puppet and MogileFS and `aptitude` and Xen and `monit` and Cacti and `backuppc` and `nginx` and `perlbal` and `memcached` and Nagios and Erlang and Varnish and Capistrano are reducing the amount of human effort it takes to administer a given number of CPUs and increasing the number of users each MIPS can support. Maybe Damon sees operations becoming more difficult because the internet is still growing, and so the biggest services have to deal with more users now than in 2001 or 1998. 2. The effort required to deploy software on centralized servers really is growing out of proportion to the effort required to write it in the first place, and that's because of the dependence on centralized servers. If the software could run on the machines of its users, the way Firefox or BitTorrent or Skype or Emacs does, the users would be the ones deploying it. Of course, they might still find that difficult, but that wouldn't be visible to the software authors; they would just see that nobody was using their software, not the hours of frustration expended trying to install it. But a lot of that deployment effort can still be moved into software (that's the point of InstallShield, `aptitude`, `easy_install`, RubyGems, CPAN.pm, Fink, Darwin Ports, Xen, AppEngine, and so on, although the diversity of items in that list suggests that the job is far from over), and with the effort that can't be, people can avoid duplication of effort by sharing solutions online. Just because the software runs on its users' machines doesn't mean it can't be providing a networked service; consider BitTorrent or Skype or, for that matter, Sendmail, ircd, or INN. 3. The effort required is growing, but not because of centralized servers. But I do not know of any other plausible candidates. 4. A [post by Jesse Robbins on O'Reilly Radar][3] suggests that some startups get their operations highly automated early on, so they can easily deploy their changes, while others screw up and end up with a mess, and spend lots of time on operations. If this is correct, then Damon Edwards is wrong in thinking that operations *inevitably* consumes a greater and greater proportion of the resources available; he's just working with dumb groups who dig themselves into big holes. [0]: http://smoothspan.wordpress.com/2007/11/27/why-small-software-teams-grow-large-and-other-software-development-conundrums/#comment-1114 [1]: http://philip.greenspun.com/panda/future "The Future chapter of Philip and Alex's Guide to Web Programming" [2]: http://www.paulgraham.com/road.html "Paul Graham's Road Ahead" [3]: http://radar.oreilly.com/archives/2007/10/operations-advantage.html "Operations is a competitive advantage" From kragen at canonical.org Mon Jul 14 03:37:01 2008 From: kragen at canonical.org (Kragen Javier Sitaker) Date: Mon Jul 14 03:37:03 2008 Subject: enumerating binary trees and their elements Message-ID: <20080604190933.5C5BE1834C2@panacea.canonical.org> [Raphael Finkel's book](http://www.nondot.org/sabre/Mirrored/AdvProgLangDesign/) says, "Enumerating binary trees (see Chapter 2) is quite difficult in most languages, but quite easy in CLU." Of course I am not enthusiastic about the idea that CLU has any merits not shared by my own favorite languages, and so I am curious how hard it would be in, say, Python or Scheme. So, I thought, enumerating the binary trees of a particular size should be fairly straightforward in Python: # With a generator. def enum_bin_tree(n): if n == 0: yield None for leftsize in range(n): for left_tree in enum_bin_tree(leftsize): for right_tree in enum_bin_tree(n - leftsize - 1): yield left_tree, right_tree # With a multi-for list comprehension. def enum_bin_tree_eager(n): if n == 0: return [None] return [(left_tree, right_tree) for leftsize in range(n) for left_tree in enum_bin_tree_eager(leftsize) for right_tree in enum_bin_tree_eager(n - leftsize - 1)] # With a simple nested loop. def enum_bin_tree_simple(n): if n == 0: return [None] rv = [] for leftsize in range(n): for left_tree in enum_bin_tree_simple(leftsize): for right_tree in enum_bin_tree_simple(n - leftsize - 1): rv.append((left_tree, right_tree)) return rv Or in Scheme: (define (enum-bin-tree n) (if (= n 0) '(()) (mapappend (lambda (left-size) (mapappend (lambda (left-tree) (map (lambda (right-tree) (list left-tree right-tree)) (enum-bin-tree (- (- n left-size) 1)))) (enum-bin-tree left-size))) (iota n)))) (define (iota n) (iotaplus (- n 1) '())) (define (iotaplus n rest) (if (< n 0) rest (iotaplus (- n 1) (cons n rest)))) (define (mapappend fn lst) (if (null? lst) '() (append (fn (car lst)) (mapappend fn (cdr lst))))) Then I looked at Finkel's pseudo-CLU version; it is 24 lines, compared to 14 for the Scheme version and 6-8 for the Python versions. However, it happens to be very similar to the first (7-line) Python version above; the only differences in the algorithm are the position of the -1 and its use of side effects in place of constructing new tree nodes. A variant of the approach above can be used to enumerate the sentences of a given length in at least some context-free languages; the tricky part is making sure that the recursion terminates. I think it will work for grammars with no epsilon-productions and no cycles in which a nonterminal can expand to itself A -> A. I'm not quite sure if it can be expanded to include those languages; I'm pretty sure allowing the cycles don't add any power (you can rewrite the grammar to an equivalent grammar without them) but I'm not sure about the epsilon-productions. Enumerating binary search tree keys ----------------------------------- Another example Finkel's book gives, which is perhaps more to the point, is comparing two binary trees to see if their nodes have the same value in inorder traversal. This is very similar to the samefringe problem, in that the recursive formulation of inorder traversal is very straightforward: def inorder_traverse(fn, bintree): if type(bintree) is type(()): left, middle, right = bintree inorder_traverse(fn, left) fn(middle) inorder_traverse(fn, right) A nonrecursive procedural formulation, on the other hand, is considerably more opaque. def inorder_traverse_nr(fn, bintree): stack = [("node", bintree)] while stack: action, arg = stack.pop() if action == "node": if type(arg) is type(()): left, middle, right = arg stack.push(("node", right)) stack.push(("visit", middle)) stack.push(("node", left)) else: # action == "visit" fn(arg) And if you want to be able to get those items on demand, for example so that you can compare them with the items being produced from another such traversal, you end up structuring it into some objects: class Inorder_Iterator: def __init__(self, bintree): self.stack = [Node(bintree)] def next(self): if self.stack: return self.stack.pop().next(self) raise StopIteration def push(self, other): self.stack.push(other) def __iter__(self): return self class Node: def __init__(self, bintree): self.node = bintree def next(self, stack): if type(self.node) is type(()): left, middle, right = self.node stack.push(Node(right)) stack.push(Visit(middle)) stack.push(Node(left)) return stack.next() class Visit: def __init__(self, val): self.val = val def next(self, stack): return self.val By contrast, Python generators let you write this: def inorder_traverse(bintree): if type(bintree) is type(()): left, middle, right = bintree for item in inorder_traverse(left): yield item yield middle for item in inorder_traverse(right): yield item That's 6 lines of code instead of the 19 of the explicit object version. Both are noticeably shorter, however, than the 25-line pseudo-Simula version with coroutines that Finkel presents (in chapter 2, subsection 2, p.33, figure 2.8). From kragen at canonical.org Thu Jul 17 03:37:01 2008 From: kragen at canonical.org (Kragen Javier Sitaker) Date: Thu Jul 17 03:37:02 2008 Subject: the Gelfand Principle in programming Message-ID: <20080604190933.CFEA71834CA@panacea.canonical.org> I was reading [Doron Zeilberger's Opinion 65](http://www.math.rutgers.edu/~zeilberg/Opinion65.html) about the Gelfand Principle, which he ascribes to Israel Gelfand. I'll restate what he says here. He says it is better to begin by explaining that 1+2 = 2+1, and then go on to explain that this is true for all pairs a+b = b+a and that this is called the commutative property of addition, than to begin by giving the name, explaining that it means that always a+b = b+a, and then by giving an example. And he points out that 1+2 = 2+1 is the best example to use, because there's nothing particularly special about it. 0+1 = 1+0 is true, but in general 0+x = x+0 = x, and so someone might think that this commutative property is a special feature of 0. (In linear algebra, the identity matrix works this way: for all conformable M and identity matrices I, IM = MI, even though matrix multiplication is not commutative in general.) And 1+1 = 1+1, but that's because of the reflexive property of equality, not the commutative property of addition. So 1+2 = 2+1 is the simplest nontrivial example. It's a better example than, say, 424 + 501 = 501 + 424, because it's easier to prove: 1+2 is 1+(1+1), and 2+1 is (1+1)+1, and addition is associative, so those are the same. In general, he argues, "Whenever you state a new concept, definition, or theorem, (and better still, right before you do) [you should] give the *simplest* possible non-trivial example." He also says: > The Gelfand Principle should also be used in research articles. It > is much easier to follow a new definition or theorem after a simple > example is first given. Even proofs would be easier to follow if > they are first spelled out concretely for a special case. I agree. In fact, I've often groused to myself about mathematical papers being written in the opposite style. (I've spent some time lately reading John Backus's "Can Programming Be Liberated from the von Neumann Style?", which is not technically a math paper but contains theorems anyway, and it would be considerably improved by an application of this principle.) I've wondered whether other people --- certain mathematicians maybe --- actually find it easier to understand things in what seems to me like a backwards order: theorem first, then example. (Amusingly, Zeilberger states the principle before he gives the above example of it, thus violating the principle he is trying to promote; however, he follows it in part, in that the commutative principle is probably the simplest possible nontrivial example of stating a theorem.) Apparently, however, other people also feel that the Gelfand approach is the correct one. In response to Zeilberger's article, Tim Gowers wrote: > I've just looked at your opinions page for the first time for a > while, and read your article on two pedagogical principles. I was > particularly interested in the first [the Gelfand Principle], > because as a result of editing the Princeton Companion I have become > incredibly conscious of it myself -- I'm tempted to say that I > discovered it independently. Of course, it doesn't bother me that > Gelfand got there first -- it is SO clearly correct that it would be > a miracle if I had not been anticipated. Instead, we have the > depressing miracle that something so obvious should be practised by > such a small percentage of mathematicians. I feel quite evangelistic > about this, and have already started a one-man (except that now I > see that you are an ally) campaign to publicize the principle. For > example, a few weeks ago I was asked to give a talk about the > Princeton Companion, and EXAMPLES FIRST was one of the main themes > (which I illustrated by an example first: I gave a ridiculous and > unmemorable definition of a "C-space" which was in fact a > mathematical model of a car, and as soon as the word "car" was > uttered, the definition was magically easier to remember). > > I had always been aware, of course, of the value of giving the > simplest non-trivial example. The thing that has really struck me is > the value of giving it FIRST. I think it is very important to stress > that this is an independently important part of the Gelfand > principle (or else, if you were not including it, a separate and > equally important principle). > > Here is my "proof" that it is better to start with concrete examples > and proceed to abstract definitions than it is to begin with the > abstract definitions. If you give the example first, then it is easy > for the reader to understand, so not much effort is needed to > remember anything. Then, when you are presented with the abstract > definition, you have a mental picture of an example, so the various > components of the abstract definition become labels that you attach > to this picture. If, on the other hand, you give the abstract > definition first, then the components are meaningless, so you have > no choice but to memorize them as if you were learning Chinese > vocabulary or something. Then when you see the example, you have to > go back and see how this meaningless stuff does in fact mean > something. But that effort of memorization should have been > unnecessary! Dijkstra, unsurprisingly, disagrees (in EWD757-3): > There exists a school (I wouldn't call it a "school of thought") > that believes in "teaching by example" and in "discovery by > example". I don't. I concluded EWD376, which describes in detail > the actual steps in which I had solved a problem from graph theory, > with: > > > "Finally we draw attention to the fact that we did not draw a > > single example to explain what we were talking about or (even > > worse!) to discover what the program should do. And this, of > > course, is as it should be." So what's the analogue in computer programming? You could argue that it's test-first programming, where you write the unit test before you write the code, and hopefully people will read it in the same sequence. The unit test is usually a better unit test if it's exactly this simplest non-trivial case. (Maybe it's better if there's an additional test that doesn't try to be simple, just in case you were mistaken about how trivial the case was.) But in the mathematical case, you don't just write down the premises of the example, write down the conclusion, baldly assert that some theorem exists to connect the two, and then proceed to explaining what that theorem is and proving it in the general case. Instead, you demonstrate that the conclusion is true of that particular example, and then state the theorem and proof for the general case. This is much more similar to walking through the program as it executes the test case in a debugger and looking at all the intermediate values. There was a thread on LtU about "[Ivory Towers and Gelfand's Principle](http://lambda-the-ultimate.org/node/924)", on motivating language features with examples: > If an example has a solution that is nearly as good without a given > language feature, then that example is not a good motivation for > that feature. Perhaps not following this principle is partly what > earned FP it's ivory tower reputation. There was also [a thread about this on LiveJournal](http://jcreed.livejournal.com/899682.html?view=2631778#t2631778). From kragen at canonical.org Mon Jul 21 03:37:01 2008 From: kragen at canonical.org (Kragen Javier Sitaker) Date: Mon Jul 21 03:37:02 2008 Subject: relativistic dimensional analysis in programming Message-ID: <20080523223404.4BE56183497@panacea.canonical.org> I was reading Raphael Finkel's book "Advanced Programming Language Design", and he mentions a language somebody named Finkel invented in the 1970s called AL. AL had dimensional analysis built in, with four base dimensions: 'time', 'distance', 'angle', and 'mass'. (As Finkel says, this is the kind of "type safety" that's really needed in everyday calculations. His language did it statically, but you can do it dynamically too, as units(1) does.) He points out that 'angle' really shouldn't be included since radians are really dimensionless, which leaves us with 'time', 'distance', and 'mass'. But no 'speed', 'force' or 'energy'. However, they can be derived from the base units; speed is a ratio distance/time --- e.g. m/s. Acceleration is speed/time, or m/s?, and force can be thought of as mass * acceleration, or kg*m/s?. Finally, energy is force * distance, so you can express it in units of kg*m?/s?. This is in fact how units(1) represents it. But do we really need all three of 'time', 'distance', and 'mass'? The speed of light provides a natural conversion factor between time and distance, and mass can be equivalently measured as energy, according to the well-known formula E = mc?. So speed is really just a dimensionless quantity, and acceleration is the reciprocal of a time interval, namely the time to reach the speed of light at that constant acceleration; so its units are really 1/s. So force can really be measured merely with kg/s. Unfortunately this doesn't really help us get rid of mass: energy is now expressed in kg*s/s, or merely kg. So our two relativity-based equivalences (mass as energy, and time as distance) turned out to be merely two facets of the same equivalence. But I suspect that in everyday calculations, the equivalence of mass and energy is rarely useful; it's far more likely to hide errors than to reduce the amount of work necessary. With time, distance, and mass, the only incommensurable quantities I commonly run into with commensurable units are: - torque and energy; - various dimensionless quantities; - perhaps pressure and stress; - stress and young's modulus. Collapsing mass with energy and time with distance creates many more "units collisions". From kragen at canonical.org Thu Jul 24 03:37:01 2008 From: kragen at canonical.org (Kragen Javier Sitaker) Date: Thu Jul 24 03:37:03 2008 Subject: the value restriction and Modula-3 Message-ID: <20080604190933.358471834C2@panacea.canonical.org> (Warning, this note is kind of rambling with no real point.) [Finkel](http://www.nondot.org/sabre/Mirrored/AdvProgLangDesign/) says, describing Modula-3's subtyping rules: > If every value of one type is a value of the second, then the first > type is a called a 'subtype' of the second. For example, a record > type TypeA is a subtype of another record type TypeB only if their > fields have the same names and the same order, and all of the types > of the fields of TypeA are subtypes of their counterparts in TypeB. Initially, this struck me as violating ML's "value restriction" and therefore being unsafe, but I was wrong. Suppose we say type Ushort = 0..65535; Long = -2147483648..2147483647; TypeB = record Data: Long; end; TypeA = record Data: Ushort; end; Now it seems that `Ushort` is a subtype of `Long`, because every value of type `Ushort` is a value of type `Long`. So it is safe to assign a value of type `Ushort` to a variable of type `Long`. `TypeA` and `TypeB` have fields of the same names in the same order, and the single field in `TypeA` has a type that is a subtype of the single field in `TypeB`. However, I guess if you're passing by value, you can safely copy a record of type `TypeA` into a variable declared as a record of type `TypeB`, and you're still safe with no run-time type checks. Mutable Aliases Introduce Problems ---------------------------------- It's only once you get into aliasing that you start running into problems; if you have a record that is mutably accessible through both `TypeA` and `TypeB` pointers, you can set its `Data` to `-1` through the `TypeB` pointer and then access it through the `TypeA` pointer. It seems that if you could ensure that the `TypeB` pointer were `const` --- that is, didn't allow modification of the pointed-to value --- you could avoid this. That would allow the following subtyping rule: if TypeA is a subtype of TypeB ------------------------------------------------------- then pointer to TypeA is a subtype of pointer to const TypeB That allows you to copy a pointer to `TypeA` into a variable declared as pointer to const `TypeB`. Which is pretty much equivalent to copying a `TypeA` into a variable declared as `TypeB`, but more efficient. Arrays ------ When it comes to arrays of the same element type but varying sizes, the definition of "A is a subtype of B" that works in the above subtyping rule is "A's indices are a superset of B's indices". That is, if you have an `int[100]` array, it's perfectly OK to copy a pointer to it someplace that is expecting a pointer to a `const int[10]` array; no run-time type checks will be needed. Actually, though, that's true even if that pointer isn't `const` --- which I guess is Finkel's point when he distinguishes "extensions" from "subtypes". Lack of Conclusions ------------------- Anyway, just some interesting things I hadn't realized before about safe static typing. There's a subtyping relation that still applies after you take mutable references, and another one that doesn't, and the one that doesn't can be pretty broad. From kragen at canonical.org Mon Jul 28 03:37:02 2008 From: kragen at canonical.org (Kragen Javier Sitaker) Date: Mon Jul 28 03:37:03 2008 Subject: deep and shallow binding, and a hashing middle way Message-ID: <20080604190933.952901834C2@panacea.canonical.org> Richard Gabriel writes, in [Performance and Evaluation of Lisp Systems](http://www.dreamsongs.com/Books.html): > The performance profiles for free/special lookup and binding are > very different depending on whether you have deep or shallow > binding. In shallow-binding implementations, times for function > call and internal binding of free/special variables are inflated > because of the additional work of swapping bindings. On some > deep-binding systems, referencing a dynamically bound variable > (which includes all variable references from the interpreter) can > require a search along the access path to find the value. Other > systems cache pointers to the value cells of freely referenced > free/special variables on top of the stack; caching can take place > upon variable reference/assignment or upon entry to a new lexical > contour, and at each of these points the search can be one > variable at a time or all/some variables in parallel. > Shallow-binding systems look up and store into value cells, the > pointers to which are computed at load time. Deep-binding systems > bind and unbind faster than shallow-binding systems, but > shallow-binding systems look up and store values faster. > Context-switching can be performed much faster in a deep-binding > implementation than in a shallow-binding one. Deep binding > therefore may be the better strategy for a multi-processing Lisp. It occurred to me that there is actually a continuum between these two, using hash tables, as was done in many Forth implementations. Suppose you hash each symbol into one of eight buckets, and have one alist hanging off each bucket. Then saving a context requires saving up to eight cells --- more than one, as in deep binding, but potentially fewer than the total number of variable values being restored, as in shallow binding; and looking up a value requires a linear search along only one eighth of the total number of dynamic variables. Also, the hash table size need not be fixed; it can change over time, or from program to program. When the hash table size is 1 --- that is, there's only one hash bucket --- you have traditional deep binding. When it's large enough that no hash bucket has more than one variable in it, you have traditional shallow binding. If you had about sqrt(number of variables) buckets, then both lookup and swapping of bindings would be O(sqrt(number of variables)). This may seem a bit irrelevant today, since "special variables" (i.e. dynamically-scoped variables) are used very rarely.