A place to be (re)educated in Newspeak

Sunday, May 24, 2020

Bits of History, Words of Advice

"Why do you jackasses use these inferior linguistic vehicles when we have something here that’s so
precious, so elegant, which gives me so much pleasure? How can you be so blind and so foolish?"
That debate you’ll never win, and I don’t think you ought to try.

- Alan Perlis, 1978


In the late 1970s, researchers at Xerox Parc invented modern computing.  Of course, there were others
elsewhere - but Parc made a vastly disproportionate contribution.

A large part of that was done in, and based upon, the Smalltalk programming language. Forty years
ago, Smalltalk's dynamic update and reflection capabilities were more advanced than in any
mainstream language today. The language leveraged those capabilities to provide an IDE that in
many ways still puts the eclipses, black holes, red dwarfs and other travesties that currently
masquerade under that term to shame.  The Smalltalk image provided a much better Docker than
Docker.

Smalltalk and Smalltalkers invented not only IDEs, but window systems and their related paraphernalia
(pop-up menus, scroll bars, the bit-blt primitives that make them possible) as well as GUI builders,
unit testing, refactoring and agile development (ok, so nobody's perfect).

And yet, today Smalltalk is relegated to a small niche of true believers.  Whenever two or more
Smalltalkers gather over drinks, the question is debated: Why? 

The answer is unknowable, since we cannot run parallel universes and tweak things to see which
makes a difference

 I did describe such an alternate universe in a talk in 2016; it may be the best talk I ever gave.

Nevertheless, I think we can learn something from looking into this question. I'll relate parts of history
that I deem relevant, as I know them. I'm sure there are inaccuracies in the account below.
There are certainly people who were closer to the history than I.  My hope is that they'll expand on my
comments and correct me as needed. I'm sure I'll be yelled at for some of this. See if I care.

On with the show.

Lack of a Standard. Smalltalk had (and still has) multiple implementations - more so than much
more widely used languages. In a traditional business, having multiple sources for a technology
would be considered an advantage. However, in Smalltalk's case, things proved to be quite different. 

Each vendor had a slightly different version - not so much a different language, as a different platform.
In particular, Smalltalk classes do not have a conventional syntax; instead, they are defined via
reflective method invocation. Slight differences in the reflection API among vendors meant that the
program definitions themselves were not portable, irrespective of other differences in APIs used by the
programs. 

There were of course efforts to remedy this. Smalltalk standardization efforts go back to the late 80s,
but were pushed further in the 90s. Alas, in practice they had very little impact.

Newspeak of course, fixed this problem thoroughly, along with many others. But we were poorly funded
after the 2008 crash, and never garnered much interest from the Smalltalk community.
The community's lack of interest in addressing weaknesses in the Smalltalk-80 model will be a
recurring theme throughout this post.

Business model. Smalltalk vendors had the quaint belief in the notion of "build a better mousetrap
and the world will beat a path to your door".  Since they had built a vastly better mousetrap, they
thought they might charge money for it. 

This was before the notion of open source was even proposed; though the Smalltalk compilers, tools
and libraries were provided in source form; only the VMs were closed source.

Alas, most software developers would rather carve their programs onto stone tablets using flint tools
held between their teeth than pay for tools, no matter how exquisite. Indeed, some vendors charged
not per-developer-seat, but per deployed instance of the software. Greedy algorithms are often
suboptimal, and this approach was greedier and less optimal than most. Its evident success speaks
for itself.

In one particularly egregious and tragic case, I'm told ParcPlace declined an offer from Sun
Microsystems to allow ParcPlace Smalltalk to be distributed on Sun workstations. Sun would pay a per
machine license fee, but it was nowhere near what ParcPlace was used to charging.

Eventually, Sun developed another language; something to do with beans, I forget. Fava, maybe?
Again, dwell on that and what alternative universe might have come about.


Performance and/or the illusion thereof.

Smalltalk was and is a lot slower than C, and more demanding in terms of memory. In the 1980s and
early 1990s, these were a real concern. In the mid-1990s, when we worked on Strongtalk, Swiss
banks were among our most promising potential customers. They already had Smalltalk applications
in the field. They could afford to do so where others could not. For example, they were willing to equip
their tellers with powerful computers that most companies found cost-prohibitive - IBM PCS with a
massive 32Mb of memory! 

It took a long time for implementation technology to catch up, and when it did, it got applied to lesser
languages. This too was a cruel irony. JITs originated in APL, but Smalltalk was also a pioneer in that
field (the Deutsch-Schiffman work), and even more so Self, where adaptive JITs were invented. 

Strongtalk applied Self's technology to Smalltalk, and made it practical. 

Examples: Self needed 64Mb, preferably 96, and only ran on Sun workstations. Strongtalk ran in 8Mb
on a PC. This mattered a lot. And Strongtalk had an FFI, see below.

Then, Java happened.  Strongtalk was faster than Java in 1997, but Strongtalk was acquired by Sun;
the VM technology was put in the service of making Java run fast.  

The Smalltalk component of Strongtalk was buried alive until it was too late. By the time I finally got it
open-sourced , bits had rotted or disappeared, the system had no support, and the world had moved on.
And yet, the fact that the Smalltalk community took almost no interest in the project is still telling.

Imagine if all the engineering efforts sunk into the JVM had focused on Smalltalk VMs.

It's also worth dwelling on the fact that raw speed is often much less relevant than people think.
Java was introduced as a client technology (anyone remember applets?). The vision was programs
running in web pages. Alas, Java was a terrible client technology. In contrast, even a Squeak
interpreter, let alone Strongtalk, had much better start up times than Java, and better interactive
response as well. It also had much smaller footprint. It was a much better basis for performant client
software than Java. The implications are staggering. 

On the one hand, Netscape developed a scripting language for the browser. After all Java wouldn't cut
it. Sun gave them permission to use the Java name for their language.  You may have heard of this
scripting language; it's called Javascript. 

Eventually, people found a way to make Javascript fast. Which people? Literally some of the same
people who made Strongtalk fast (Lars Bak), using much the same principles.

Imagine if Sun had a workable client technology. Maybe the Hot Java web browser would still be
around.

On the other hand, the failure of Java on the client led to an emphasis on server side Java instead.
This seemed like a good idea at the time, but ultimately commoditized Sun's product and contributed
directly to Sun's downfall. Sun had a superb client technology in Strongtalk, but the company's
leadership would not listen. 

Of course, why would they? They had shut down the Self project some years earlier to focus on Java.
Two years later, they spent an order of magnitude more money than it cost to develop Self, to buy back
essentially the same technology so they could make Java performant.


Interaction with the outside world.

Smalltalk had its unique way of doing things. Often, though not always, these ways were much better
than mainstream practice. Regardless, it was difficult to interact with the surrounding software
environment. Examples:

FFIs. Smalltalk FFIs were awkward, restrictive and inefficient. After all, why would you want to reach
outside the safe, beautiful bubble into the dirty dangerous world outside?

We addressed this back in the mid-90s in Strongtalk, and much later, again, in Newspeak.  

Windowing. Smalltalk was the birthplace of windowing. Ironically, Smalltalks continued to run on top
of their own idiosyncratic window systems, locked inside a single OS window. 

Strongtalk addressed this too; occasionally, so did others, but the main efforts remained focused on
their own isolated world, graphically as in every other way. Later, we had a native UI for Newspeak as
well. 

Source control. The lack of a conventional syntax meant that Smalltalk code could not be managed
with conventional source control systems. Instead, there were custom tools. Some were great - but
they were very expensive.

In general, saving Smalltalk code in something so mundane as a file was problematic. Smalltalk used
something called file-out format, which is charitably described as a series of reflective API calls, along
with meta-data that includes things like times and dates when the code was filed out. This compounded
the source control problem.


Deployment. Smalltalk made it very difficult to deploy an application separate from the programming
environment. The reason for this is that Smalltalk was never a programming language in the traditional
sense. It was a holistically conceived programming system. In particular, the idea is that computation
take place among communicating objects, which all exist in some universe,  a "sea of objects". Some
of these object know how to create new ones; we call them classes (and that is why there was no
syntax for declaring a class, see above).

What happens when you try to take some objects out of the sea in which they were created (the IDE)?
Well, it's a tricky serialization problem.  Untangling the object graph is very problematic.

If you want to deploy an application by separating it from the IDE (to reduce footprint, or protect your IP,
or avoid paying license fees for the IDE on each deployed copy) it turns out to be very hard.


The Self transporter addressed this problem in a clever way. Newspeak addressed it much more
fundamentally and simply, both by recognizing that the traditional linguistic perspective need not
contradict the Smalltalk model, and by making the language strictly modular.

The problem of IP exposure is much less of a concern today. It doesn't matter much for server based
applications, or for open source software. Wasted footprint is still a concern, though in many cases you
can do just fine. Avi Bryant once explained to me how he organized the server for the late, great
Dabble DB. It was so simple you could just cry, and it performed like a charm using Squeak images.
Another example of the often illusory focus on raw performance.


So why didn't Smalltalk take over the world? 

With 20/20 hindsight, we can see that from the pointy-headed boss perspective, the Smalltalk value
proposition was: 

Pay a lot of money to be locked in to slow software that exposes your IP, looks weird on screen and
cannot interact well with anything else; it is much easier to maintain and develop though!

On top of that, a fair amount of bad luck.

And yet, those who saw past that, are still running Smalltalk systems today with great results; efforts to
replace them with modern languages typically fail at huge cost.

All of the problems I've cited have solutions and could have been addressed. 
Those of us who have tried to address them have found that the wider world did not want to listen -
even when it was in its own best interest. This was true not only of short sighted corporate leadership,
but of the Smalltalk community itself.  

My good friends in the Smalltalk community effectively ignored both Strongtalk and Newspeak.
It required commitment and a willingness to go outside their comfort zone.

I believe the community has been self-selected to consist of those who are not bothered by Smalltalk's
initial limitations, and so are unmotivated to address them or support those who do. In fact, they often
could not even see these limitations staring them in the face, causing them to adopt unrealistic
business policies that hurt them more than anyone else.

Perhaps an even deeper problem with Smalltalk is that it attracts people who are a tad too creative and
imaginative; organizing them into a cohesive movement is like herding cats.

Nevertheless, Smalltalk remains in use, much more so than most people realize. Brave souls continue
to work on Smalltalk systems, both commercial and open source. Some of the issues I cite have been
addressed to a certain degree, even if I feel they haven't been dealt with as thoroughly and effectively
as they might. More power to them. Likewise, we still spend time trying to bring Newspeak back to a
more usable state. Real progress is not made by the pedantic and mundane, but by the dreamers who
realize that we can do so much better.



Eppur si muove

Monday, January 06, 2020

The Build is Always Broken

Programmers are always talking about broken builds: "The build is broken", "I broke the build" etc. However, the real problem is that the very concept of the build is broken. The idea that every time an application is modified, it needs to be reconstructed from scratch is fundamentally flawed. A practical problem with the concept is that it induces a very long and painful feedback loop during development. Some systems address this by being wicked fast. They argue that even if they compile the world when you make a change, it's not an issue since it'll be done before you know it. One problem is that some systems get so large that this doesn't work anymore. A deeper problem is that even if you rebuild instantly, you find that you need to restart your application on every change, and somehow get it back to the stage where you were when you found a problem and decided to make a change. In other words, builds are antithetical to live programming. The feedback loop will always be too long. Fundamentally, one does not recreate the universe every time one changes something. You don't tear down and reconstruct a skyscraper everytime you need to replace a light bulb. A build, no matter how optimized, will never give us true liveness It follows that tools like make and its ilk can never provide a solution. Besides, these tools have a host of other problems. For example, they force us to replicate dependency information that is already embedded in our code: things like imports, includes, uses or extern declarations etc. give us the same file/module level information that we manually enter into build tools. This replication is tedious and error prone. It is also too coarse grain, done at the granularity of files. A compiler can manage these dependencies more precisely, tracking what functions are used where for example. Caveats: Some tools, like GN, can be fed dependency files created by cooperating compilers. That is still too coarse grain though. In addition, the languages these tools provide have poor abstraction mechanisms (compare make to your favorite programming language) and tooling support (what kind of debugger does your build tool provide?). The traditional response to the ills of make is to introduce additional layers of tooling of a similar nature, like Cmake. Enough!

A better response is to produce a better DSL for builds. Internal DSLs, based on a real programming language, are one way to improve matters. Examples are rake and scons, which use Ruby and Python respectively. These tools make defining builds easier - but they are still defining builds, which is the root problem I am concerned with here. So, if we aren't going to use traditional build systems to manage our dependencies, what are we to do? We can start by realizing that many of our dependencies are not fundamental; things like executables, shared libraries, object files and binaries of whatever kind. The only thing one really needs to "build" is source code. After all, when you use an interpreter, you can create only the source you need to get started, and then incrementally edit/grow the source. Using interpreters allows us to avoid the problems of building binary artifacts. The cost is performance. Compilation is an optimization, albeit an important, often essential, one. Compilation relies on a more global analysis than an interpreter, and on pre-computing the conclusions so we need not repeat work during execution. In a sense, the compiler is memoizing some of the work of the interpreter. This is literally the case for many dynamic JITs, but is fundamentally true for static compilation as well - you just memoize in advance. Seen in this light, builds are a form of staged execution, and the binary artifacts that we are constantly building are just caches. One can address the performance difficulties of interpreters by mixing interpretation with compilation. Many systems with JIT compilers do exactly that. One advantage is that we don't have to wait for the optimization before starting our application. Another is that we can make changes, and have them take effect immediately by reverting to interpretation, while re-optimizing. Of course, not all JITs do that; but it has been done for decades, in, e.g., Smalltalk VMs. One of the many beauties of working in Smalltalk is that you rarely confront the ugliness of builds. And yet, even assuming you have an engine with a JIT that incrementally (re)optimizes code as it evolves, you may still be confronted with barriers to live development, barriers that seem to require a build. Types. What if your code is inconsistent, say, due to type errors? Again, there is no need for a build step to detect this. Incremental typecheckers should catch these problems the moment inconsistent code is saved. Of course, incremental typecheckers have traditionally been very rare; it is not a coincidence that live systems have historically been developed using dynamically typed languages. Nevertheless, there is no fundamental reason why statically typed languages cannot support incremental development. The techniques go back at least as far as Cecil; See this paper on Scala.js for an excellent discussion of incremental compilation in a statically typed language. Tests. A lot of times, the build process incorporates tests, and the broken build is due to a logical error in the application detected by the tests. However, tests are not themselves part of the build, and need not rely on one - the build is just one way to obtain an updated application. In a live system, the updated application immediately reflects the source code. In such an environment, tests can be run on each update, but the developer need not wait for them. Resources. An application may incorporate resources of various kinds - media, documentation, data of varying kinds (source files or binaries, or tables or machine learning models etc.). Some of these resources may require computation of their own (say, producing PDF or HTML from documentation sources like TeX or markdown), adding stages that are seldom live or incremental. Even if the resources are ready to consume, we can induce problems through gratuitous reliance on file system structure. The resources are typically represented as files. The deployed structure may differ from the source repository. Editing components in the source repo won't change them in the built structure. It isn't easy to correct these problems, and software engineers usually don't even try. Instead, they lean on the build process more and more. It doesn't have to be that way. We can treat the resources as cached objects and generate them on demand. When we deploy the application, we ensure that all the resources are precomputed and cached at locations that are fixed relative to the application - and these should be the same relative locations where the application will place them during development in case of a cache miss. The software should always be able to tell where it was installed, and therefore where cached resources stored at application-relative locations can be found. The line of reasoning above makes sense when the resource is accessed via application logic. What about resources that are not used by the application, but made available to the user? In some cases, documentation and sample code and attached resources might fall under this category. The handling of such resources is not part of the application proper, and so it is not a build issue, but a deployment issue. That said, deployment is simply computation of a suitable object to serialized to a given location, and should be viewed in much the same way as the build; maybe I'll elaborate on that in separate post. Dealing with Multiple Languages. Once we are dealing with multiple languages, we may be pushed into using a build system because some of the languages do not support incremental development. Assuming that the heart of our application is in a live language, we should treat other languages as resources; their binaries are resources to be dynamically computed during development and cached.

Summary


  • Builds kill liveness.
  • Compilation artifacts are a form of cached resource, the result of staged execution.
  • To achieve liveness in industrial settings, we need to structure our development environments so that any staging is strictly an optimization
    • Staged results should be cached and invalidated automatically when the underlying basis for the cached value is out of date.
    • This applies regardless of whether the staged value is a resource, a shared library/binary or anything else. 
    • The data necessary to compute the cached value, and to determine the cache's validity, must be kept at a fixed location, relative to the application. 

It's high time we build a new, brave, build-free world.