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.