Snowsuit Zine // issue 03

Table of Contents

What May Come Next

In recent history, there have been five technological revolutions. The most important was the Industrial Revolution, but the effects of all five have changed the direction of society forever. The men behind these revolutions have been a source of controversy due to both dubious behavior and the huge fortunes they amassed along the way.

Capitalism is an on-going experiment. The fundamental idea is to decentralize economic influence. This is something Friedman, Smith, Schumpeter, and Piketty all agree on. Smith begins with the basics of letting everyone make their own decisions and over time we get better at understanding how this can go wrong. In 2008, for example, we learned that an unregulated system might destroy itself and benefits from both identifying and regulating particular behaviors.

Five is a significant number. Three is usually enough to start believing a cohort exists and five takes that further and suggests a pattern may exist. And if it exists, what can we say about the future?

There is no certainty when discussing the behavior of the planet. Scientific revolutions don't occur as a logical progression from one idea to the next, either. Evolution comes in spurts. Something important happens, some species figures out how to use that idea basically everywhere, and they experience a huge upgrade. Agriculture or the Industrial revolution make great examples.

It's been 250 years since the Industrial Revolution. Since then we had Steam, Railroad, Electricity, Steel, Oil, Mass Production, Automobiles, and more lately, the Information Age.

Carlota Perez, from the London School of Economics, describes a framework for understanding our technological progress from the point of view of its relationship with capital. She describes a process that lasts roughly 60 years, is made up of two main phases, beginning with a big-bang that kicks it off.

A technology begins with hobbyists working on something unproven and not yet useful. Their ideas seem too crazy to work but the hobbyists are undeterred and continue working until some value is demonstrated. A lot of progress until a particular event changes the technology from a hobby to something of meaningful economic value. The first Model-T demonstrated what Mass Production was about to do to the world in 1908. Intel announced its plan in 1971, beginning the Information Age.

The economic value of the Information Age was starting to seem apparent by 1971. It then took 30 years of work to integrate computing heavily enough in society that a huge bubble could both grow and crash. The previous revolutions had bubbles too. Perez argues that the burst of the bubble marks that society is transitioning from the "installation phase" to the "deployment phase". That is to say, society is no longer trying to install computers in every day life. That already happened. We will soon experience a long period of productive growth as the technology is deployed across most aspects of society.

This pattern exists in all of the revolutions. In her words, the age of steel made it possible to build cross-country railroads, upgraded shipping from sails to steamships, and transoceanic telegraph. All that led to the first globalization, with worldwide sourcing (and pricing) of minerals as well as counter-seasonal trade in agriculture. According to Bessemer, the one who patented the process that started the Age of Steel, he was inspired by a conversation with Napoleon about creating better arms and decided to work on building better guns. After guns came furnaces and eventually patent licensing for other uses. It was 20 years later when a young Andrew Carnegie got involved.

The two phases are themselves split in half for a total of four pieces. The second half of the installation phase is the frenzy phase. Modern hackers will remember this coming to a close when the Internet bubble blew up. A lot of cash was thrown at dreams of wealth and so a lot of stuff gets built. Plenty of it is not useful and eventually disappears. In the early 1900's, the US saw as many as 1800 US car companies.

The unfortunate reality for many of the people involved is that such a huge number of companies isn't necessary and so the economics eventually work against this momentum and many of these companies die off as the economy stabilizes. According to Perez, the way society learns a correction is necessary is by creating huge bubbles that collapse catastrophically. Lots of players get wiped out somewhat suddenly, but society itself matures relative to the technology.

According to Perez's ideas, we may be 10 years into the Deployment Phase for the Information Age. There are 20'ish years left before the Information Age is so integrated as to itself appear insignificant. If the Information Age is winding down, what comes next and what will it mean?

One theory is that the merger of electricity, cars, and AI will begin a 6th revolution, of similar magnitude to the smaller of the five revolutions. With phones in our pockets, a characteristic of how far we are in Perez's deployment phase, we can summon vehicles on demand. Computers will be ordinary, so of course the self-driving cars will have powerful computers inside them. It is then reasonable to consider that they will talk to each other as an Internet-of-Cars.

A transition to self-driving cars would be massively disruptive to society in both positive and negative ways. Donald Shoup, a professor at UCLA, estimates that cars are parked 95% of the time. If Uber continues it's current trajectory, we can imagine a future where it rents access to fleets of self-driving cars that are in use almost 100% of the time. The environmentalists don't yet realize how aligned they are with the controversial Travis Kalanick. Pew Research Center explored the combination of AI, Robotics, and Jobs and found that truck driving is the most common job in the US. Pew concluded that the obsolescence of humans in driving based work directly threatens up to 50% of all jobs! Adjusting to such a massive change will not happen easily.

In a world where cars drive themselves and technology is constantly improving, it seems reasonable to think the max speed of transportation will also rise. Once the self-driving infrastructure is fully in place, computers could coordinate cars to drive faster than any human could reasonably manage, bringing large geographies closer together while opening up massive amounts of space in between for suburban housing.

In the US in 2015, a typical 30-year mortgage will pay over 100% of the principle in interest payments and may require the full 30 years to pay off. One can't help but wonder why housing is so expensive, beyond satisfying the interests of those who run the housing markets. Innovations in housing could be deployed when self-driving cars reach the necessary speeds to make open land accessible.

The human population continues to go. Our cities go upwards and our suburbs go outwards. It seems another transportation revolution, building off the shoulders of the revolutions that came before it, is similar enough to the previous revolutions that it may just happen.

Articulations

This is not the Polymorphism You're Looking for

Virtually every language in modern use provides some mechanism to reuse code. For most popular languages mechanism is the same: the exact code to execute when a function or method is called is determined at run-time. This is called run-time subtyping. This is the only mechanism Java and C# provided for code reuse prior to adding generics. This is currently the only mechanism Go provides. While run-time subtyping is clearly a popular solution, and does have its place, it comes with a drawback in ones ability to reason about a program. Compile-time subtyping, on the other hand, flips the problem on its head and provides a powerful mechanism for reusing code without the cost in readability.

No matter if it is Java, C# or Go the underlying implementation for run-time subtyping is the same. A value is represented as a tuple containing an opaque state and a dispatch table. When a method is called, such as foo.bar(), the bar method is being looked up in the dispatch table and applied to the opaque state held in foo. In a language like Java or C# the compiler will make an effort to remove the indirection if possible. C++ provides the developer with control over if a method call should go through the dispatch table (in C++ the dispatch table is called a vtable).

One can implement run-time subtyping one's self, and in-fact this is common in a large enough C code base, such as the Linux kernel. In C, one can create a struct with function pointers and some area to hold the state information. When constructing an object, one assigns values to the function pointers and the state. Because it's manual, the calling syntax is not foo.bar() but generally something like: foo.bar(foo).

Run-time subtyping is powerful. In languages like Ruby and Python, every method call goes through a dispatch table which allows users to override almost any functionality at run-time. Monkey patching, for example, is a result of a dispatch table being between every method call and its execution.

Regardless of if one has language support or implements it, the end effect is that one cannot say with certainty what code will be executed simply by reading the code. Take the following contrived example, where Collection is an interface that any type of collection:

void addOneToCollection(Collection c) {
    c.add(1);
}

For some reason add is not working. Why? One does not know which add is being called which makes that question difficult to answer. Consider if the bug is triggered infrequently, tracking it down involves figuring out how to trigger it under controlled conditions, possibly while running under a debugger, or surveying each of the types that implement the Collection interface and determining if there is a possible bug in add.

For many, this lose of readability does not necessarily seem so dangerous. Most people who have grown up in a mainstream software stack that provides nothing else. However, along with the increasing complexity of software systems being built, they are also being required to be more reliable. In the case of SaaS, an outage can be extremely damaging to a company. In financial systems, calling the wrong method implementation in a tight loop can mean the difference between making money and going out of business. Testing can only demonstrate so much in this case. Because the actual code to be executed is determined at run-time, a test only validates that the objects, as constructed in the test, are correct.

It is clear that there is value in being able to call any function or method that matches an interface but does it need to happen at run-time? If one knew, at compile-time, which implementations will be called, the run-time look up is both unnecessary from a performance perspective but also needlessly complicated from an understandability perspective.

As an analogy, think of a system as a floor plan of a building where every method call is a door. In run-time subtyping, every time one opens a door they have to ask an attendant where the door leads and the same door may not go to the same place on each open. In compile-time subtyping, where each door leads is determined when the floor is constructed and stays fixed.

For an example, take the HashiCorp Raft implementation in Go. The implementation abstracts away the state machine, log store, snapshot and stable storage through interfaces. The Raft implementation itself should be reusable under any valid combination of state machine and storage engines but for any given Raft instance, these will not change and will be known at compile-time. By applying the interfaces at compile-time, a particular Raft implementation becomes a static structure at run-time. The structure being static at run-time means that one is aware of which implementations are called just by reading the code.

To make the example concrete, consider a Raft implementation in a pseudo language. In this pseudo language, code is broken up into modules and modules contain types (structs) and functions. The semantics of the pseudo language will change depending on if it is using run-time subtyping or compile-time subtyping.

Below is the initial Raft implementation with run-time subtyping. In this case, the call to state.log.append will involve a look up in a dispatch table. The Raft implementation takes anything that implements the Fsm, Log and Store interfaces (they are not defined here and their definition doesn't matter).

module Raft {
    struct State {
        Fsm fsm;
        Log log;
        Store store;
    };

    State new(Fsm fsm, Log log, Store store) {
        return { fsm = fsm; log = log; store = store }
    }

    void append_entries(State state, Entries entries) {
        state.log.append(entries);
    }
}

module Main {
    void main() {
        Fsm fsm = create_a_fsm();
        Log log = create_a_log();
        Store store = create_a_store();

        Raft.State raft = Raft.new(fsm, log, store);
        Raft.append_entries(raft, entries);
    }
}

What is important about this example is that one does not know what concrete values create_a_fsm, create_a_log and create_a_store instantiates. It happens somewhere else and one needs to read that code in order to understand what it creates. Even reading the code could be difficult if it does interesting run-time decisions. Without making the Raft implementation less reusable, one has no good alternatives to solve this problem.

Translating this code to use compile-time subtyping involves adding a new concept: a functor (this is a functor in the Ocaml and SML definition, not the Haskell, mathematical or C++ definition). A functor is a module which can be applied, like a function, at compile-time. As input it takes other modules. The result of calling a functor is a module.

RaftFunctor will take three modules, just like the run-time subtyping example. FSM, LOG and STORE are interfaces (the compile-time equivalents of Fsm, Log and Store) except they are interface for modules rather than objects. The only other change in this case is the call to append, now it is LOG.append. LOG will be replaced with the exact module that will actually be called. In this example it will be MyLog.

module RaftFunctor(FSM Fsm, LOG Log, STORE Store)  {
    struct State {
        Fsm fsm;
        Log log;
        Store store;
    };

    State new(Fsm fsm, Log log, Store store) {
        return { fsm = fsm; log = log; store = store }
    }

    void append_entries(State state, Entries entries) {
        LOG.append(state.log, entries);
    }
}

module Main {
    module Raft = RaftFunctor(MyFsm, MyLog, MyStore);

    void main() {
        MyFsm fsm = create_a_fsm();
        MyLog log = create_a_log();
        MyStore store = create_a_store();

        Raft.State raft = Raft.new(fsm, log, store);
        Raft.append_entries(raft, entries);
    }
}

The Main module creates a module called Raft which is the functor application of RaftFunctor with the module MyFsm, MyLog and MyStore. The Raft module can now only be used with instances of these modules. The functions create_a_fsm, create_a_log, and create_a_store must return instances of those types.

In using a functor, the reusability of the Raft implementation has not been compromised but it is much more readable. One knows exactly what code will be called just by reading the code. When it comes to debugging, one knows what code to examine. While the abstraction is happening at compile-time, nothing prevents one from implementing run-time subtyping ones self and instantiating a Raft that uses modules which do the run-time subtyping. In that case the fact that the method calls are dynamic at run-time also becomes clear to the reader.

All forms of abstraction are a balance between readability and flexibility. Run-time subtyping allows one to change the behaviour of a system at any time, but at the cost of being able to understand the source code. Compile-time subtyping allows for flexibility and readability at compile-time but limits what one can do at run-time. Whether or not giving up a more flexible run-time is a price worth paying depends on the problem, however many problems are well expressed as a compile-time composition of its dependencies. While the options for which languages support compile-time subtyping as a first-class citizen, they are worth exploring. Ocaml provides a battle-tested functor module system. The value of a compilation step is slowly being re-appreciated and with it there are many opportunities to make code easier and more enjoyable to read.

Monthly Consumption

Books

  • Benjamin Franklin: An American Life by Walter Isaacson (link)
  • Technological Revolutions and Financial Capital (link)
  • Generations: The History of America's Future, 1584 to 2069 (link)

Papers

  • Paxos Made Live - An Engineering Perspective by Chandra et al. (link)
  • Disk failures in the real world: What does an MTTF of 1,000,000 hours mean to you? by Schroeder and Gibson (link)
  • Web Search for a Planet: The Google Cluster Architecture by Barroso et al. (link)
  • Paxos Made Moderately Complex by Renesse (link)

Talks

  • Controlling Complexity in Swift (link)

Corrections

Issue02: Intuiting Availability

The total availability calculated in Table 3 should be 5 Nines rather than 4 Nines. That gives a yearly downtime of around 5 minutes.

RSS