Handmade Seattle

November 16 - 18. In person and online.
Catch up

We are a community of programmers producing quality software through deeper understanding.

Originally inspired by Casey Muratori's Handmade Hero, we have grown into a thriving community focused on building truly high-quality software. We're not low-level in the typical sense. Instead we realize that to write great software, you need to understand things on a deeper level.

Modern software is a mess. The status quo needs to change. But we're optimistic that we can change it.

Around the Network

sqdrck
Roman
New forum thread: RemedyBG 0.3.8.7
x13pixels
Christoffer Lernö

Recently, I did some work on the math libraries for C3. This involved working on vector, matrix and complex types. In the process I added some conveniences to the built in (simd) vector types. One result of this was that rather than having a Vector2 and Vector3 user defined type, I would simply add methods to float[<2>] and float[<3>] (+ double versions). This works especially well since + - / * are all defined on vectors.

In other words, even without operator overloading this works:

float[<2>] a = get_a();
float[<2>] b = get_b();
return a + b;

Seeing as a complex number being nothing other than a vector of two elements, it seemed interesting to implement the complex type that way, and get much arithmetics for free.

Just making a complex type a typedef of float[<2>] has problems though. Any method defined on the complex type would be defined on float[<2>]!

define Complex = float[<2>]; // Type alias
// Define multiply
fn Complex Complex.mul(Complex a, Complex b) 
{
    return {
        a[0] * b[0] - a[1] * b[1], 
        a[1] * b[0] + b[1] * a[0] 
    };
}
...
float[<2>] f = get_f();
f = f.mul(f); // Accidentally get the Complex version!

Distinct types

Now C3 has the concept of "distinct" types. That is, a type which is in practice identical to some type, but has a different name and will not be implicitly cast into the other. For example, I can write define Id = distinct int and have the compiler complain if an int rather than an Id is used.

This is similar to the C trick of wrapping a type in a struct, but without the inconvenience of that method.

This solves our problem form from before:

define Complex = distinct float[<2>]; // Distinct type
fn Complex Complex.mul(Complex a, Complex b) 
{
    return {
        a[0] * b[0] - a[1] * b[1], 
        a[1] * b[0] + b[1] * a[0] 
    };
}
float[<2>] f = get_f();
Complex c = get_c();
c = c.mul(c); // Works.
f = f.mul(f); // Compile time error!

At first glance this looks promising. Unfortunately the advantages of distinct types becomes a disadvantage: the distinct type retains the functions of the original type. For the c + d case this is what we want, but c * d is not what we expect:

Complex c = { 1, 3 };
Complex d = { 2, 7 };
e = c.mul(d); // Correct! e is { -19, 13 }
e = c * d; // e is { 2, 21 }

While we can try to remember to use the right thing, it's far from ideal. Especially if this is baked into the standard library: you can't have a type that mostly behaves incorrectly for regular operators!

Possible solutions

Since we're able to change the language semantics to try to "fix" this while still using "distinct", there are a few obvious solutions:

  1. Being able to "override" an operator for a distinct type. In this case we would override * and / and leave the other operators. This would be a limited form of overloading.
  2. Being able to "turn off" operators. So in this case we turn off * and / forcing the programmer to use methods, like mul and div instead.
  3. Always require to explicitly inherit operators and methods.

These could work, but we need to recognize the added complexity needed for these solutions. And on top of that, some functionality can't quite be described this way, such as conversion of floats to complex numbers.

Operator overloading with structs

The common solution in C++ would be a struct with operator overloading to get + - * /. C3 doesn't have operator overloading for numbers, but maybe we could add it?

However, operator overloading is not sufficient to get us conversion from floats to complex numbers. For that we need user-defined conversion operators, which interacts with the type system in various ways. This leaves the whole problem with custom constructor and custom conversions: is float -> Complex a conversion function on float or a construction function on Complex? All of this interacts in subtle ways with other implicit conversions.

Built-in types

Another possibility is of course to make the types built-in. After all this is how C does complex types. But then the problem is how to limit it: ok, complex types built-in but then what about quaternions? Matrices? Matrices with complex numbers? Matrices with quaternions?!

Drawing a line here means some types have better support than others, and trying to go beyond (simd) vectors, it's hard to figure out where that line should be drawn.

Worth it?

Ultimately each feature needs to be balanced against utility. Are the benefits sufficiently big to motivate the cost. Comparing what is already in C3 against what would be necessary to add, it seems that the cost would be fairly high.

Even if vectors work for complex numbers, matrices are more likely to require operator overloading with structs which is a bigger feature than overriding operators on distinct types. This means that the idea fixing so that Complex can be a vector is a feature with very limited use.

General overloading and user-defined conversion functions can be applied to a wider set of types, but has a much higher cost with the primary use restricted to numeric types and string handling.

So even if it's more useful, it's also costs a whole lot more in terms of language complexity, making it ultimately a net negative for the language as a whole.

So unless I have come up with some other solution, user-defined numeric types will have to stick with methods and explicit conversions.

x13pixels
x13pixels
kvosdev

There was recently a question on r/ProgrammingLanguages about error handling strategies in a compiler.

The more correct errors a compiler can produce, the better for a language where compile times are long. On the other hand false positives are not helping anyone.

In my case, the language compiles fast enough, so my focus has been to avoid false positives. I use the following rules:

  1. Lexing errors: these are handed over to the parser creating parser errors.
  2. Parsing errors: skip forward until there is some token it is possible to safely sync on.
  3. Parser sync: some tokens will always be the start of a top level statement in my language: struct, import, module. Those are safe to use. For some other token types indentation can help: for example in C3 fn is a good sync token if it appears at zero indentation, but if it's found further in, it's likely part of a function type declaration: define Foo = fn void();. Only sync on tokens you are really sure of.
  4. No semantic analysis for code that doesn't parse: code that doesn't parse are very unlikely to semantically analyse. Pessimistic parser sync means lots of valid code may get skipped, making semantic analysis fail even though the code might pass.
  5. Use poisoning during semantic analysis. I saw this first described by Walter Bright, the creator of the D-Language. It is simple and incredibly effective in avoiding incorrect error reporting during semantic analysis. The algorithm is simply this: if an AST-node has an error, report it and mark it as poisoned. Then proceed to mark the parent of this AST-node poisoned as well, stopping any further analysis of the node (but without reporting any further errors).

I don't do anything particularly clever in regards to error reporting, but I found that these rules are sufficient to give very robust and correct error handling.

Wxzuir
Christoffer Lernö

Structural casts are now gone from C3. This was the ability to do this:

struct Foo { int a; int b; }
struct Bar { int x; int y; }

fn void test(Foo f)
{
    // Actual layout of Foo is the same as Bar
    Bar b = (Bar)f;
    // This also ok:
    int[2] x = (int[2])f;
}

Although I think that in some ways this is a good feature, it is too permissive to be good: it's not always clear that the structural cast is even intended, and yet it suddenly allows a wide range of (explicit) casts. While doing a pointer case like (Bar*)&f would usually raise all sorts of warning flags, one would typically assume a value cast to be fairly safe and intentional. Structural casting breaks that.

The intention was a check that essentially confirms that bitcasting from one type to the other will retain match the internal data. This could then be combined with an @autocast attribute allowing something like this:

fn void foo(@autocast Foo f) { ... }

fn void test()
{
    Bar b = { 1, 2 };
    foo(b); // implicitly foo((Foo)b) due to the @autocast
}

The canonical use for this was when an external API could be used with a structurally equivalent internal type: For example you use a library which takes a Vector2 everywhere, and maybe there is another library in use that has it's Vector2D. And finally there is a Vec2 used internally in the application. With structural casts, these could be used interchangeably as long as they were structurally equivalent.

However, there are other solutions: transparent unions (see the GCC feature), macro forwarding wrappers and user definable conversions.

Then there is the question of use cases: while this vector case is common enough, I can't think of many other uses. (We might also note that when people want operator overloading, it's collections and user defined vector types they will take as examples).

So if the standard library is defining some vector types, or the use of real vector types becomes dominant then the interoperability use case might completely go away.

In any case, this is one more feature that seemed really cool to have, but ended up being less useful than expected.

(It might be somewhat useful to have compile time function that determines if two types are structurally identical though, as this allows you to build macros that work for a set of structurally equivalent types, should you ever want to)

Christoffer Lernö
New forum thread: Unruly question
kaiserschmarrn_
Max
OSKevin

This post is mirrored on my blog.

I started my morning off with Alan Kay's talk Programming and Scaling. There are several things I agree with and disagree with in that talk. It got me thinking about software as a whole, and my part to play in it.

Background

I have long been a fan of Jonathan Blow and Casey Muratori. Two talks in particular have been especially influential for me:

My other programming hero, John Carmack, has a different opinion of software, where things are generally pretty good. From his Twitter:

Just last night I finished a book (How to Take Over the World: Practical Schemes and Scientific Solutions for the Aspiring Supervillain) with a related data point that made me a bit sad. I remain a strident optimist, and there is a ton of objective data to back that position, but I do feel that broad pessimism is an unfortunate brake on progress.

Fiery indignation about a flaw, when directly engaged in building a solution is a fine path to progress, but "pessimism cheerleading" and self-flagellation have few upsides. Being an optimist, I feel that progress is going to win anyway, but I would rather win faster!

I can't find a direct quote about software, but I believe he would agree that he is optimistic about software as well. I have a hard time reconciling this, because he has created really amazing things and knows way more than I do about programming, but I still strongly feel we are not on the right track as an industry.

Conway's law

Casey Muratori's talk The Only Unbreakable Law is a must-watch. He talks about Conway's law, which is basically that organizations can only create things which replicate the organizational structure.

This is something I've had personal experience fighting in the game industry. Game studios frequently organize based on roles, which means there is a design team, a gameplay programming team, an art team, etc. Communication is naturally more expensive between teams, which leads to team power struggles, out-of-sync feature development, poor product coherency, and other issues.

For example, Design requests a feature which doesn't get implemented for weeks until Gameplay gets around to it. If the designer instead sat next to the gameplay engineer and brainstormed with them, from the very inception of the idea the engineer has been involved. This gives the engineer opportunities to suggest designs which uniquely leverage the technology, and gives the designer opportunities to have a more realistic picture of the feasibility of there ideas. (I have heard of design throwing out ideas thinking that they would be too hard to implement, but are actually technically simple, for example.) With the two people sitting together, the chances are high that the feature will in by the end of the day.

Methodologies like Scrum try to combat siloization by having cross-disciplinary teams--e.g. a designer, engineer, and artist all working closely together. Note, however, that you are still splitting a project into separate teams. If you have a Quests team and a Combat team, chances are lower that quests and combat will be as coherent with each other than if you had simply a "gameplay" team with no artificial divisions.

Open source development

The story gets even more complicated when you enter the free software world. Now, there aren't even teams, but frequently individual contributors, who both

  • don't have as close an understanding as the project's original developer
  • likely have a lower-bandwidth communication with the project's owners.

The FOSS world still naturally creates divisions here: there's the trusted core development team, and everyone else. This is a natural consequence of there being differing levels of quality, experience, and understanding between contributors.

At a more macro scale, you have teams of contributors working on single libraries or programs. There is rarely ever coherence between two programs, for example GIMP and Emacs. There's no one leading the overall system's direction. This has an advantage in the freedom it provides, but the sacrifice is increased system complexity, redundant work, and overall lower quality.

If you have N developers working on M different photo editors, you likely will end with M photo editors at 80% completion; if you have them all work on one editor, it seems you would end up with one 100% photo editor. However, even as I say this I know it's a fallacy, because having more people work on something does not imply higher quality--see both Brooks' Mythical Man Month and Conway's law again. I think this shows how natural that line of thinking is.

One could instead imagine the M different editors being different attempts to make something great, and then let natural selection take over. This seems like a reasonable away to approach innovation, but has a few problems:

  • People will make things without understanding the state of the art and the previous attempts, causing them to waste time retracing steps.
  • People can copy-cat programs and make things with only trivial innovations, causing programs to get stuck in local maxima. (Though evolutionarily, this would be the equivalent to mutation, which can end up a winning strategy in the very long term. Ideally, we would be able to do things faster thanks to our ability to think and react, but it's possible at the macro scale we can't beat out evolution. I'm out of my depth here.)
  • End-users likely don't care about there being ten different innovative attempts at editing photos. They just want one program that does everything they want. I don't believe this is a good counter-argument, however. People have different ways of thinking about and doing things, which necessitates different programs--there will never be a program that can please everybody. In fact, the more people you try to please, you either have to simplify the program (to appeal to beginner users; this is largely the strategy SaaS seems to take) or bloat the program with every feature you can think of.

A system created by ten people sitting in a room together is guaranteed to be more conceptually sound than one created by thousands. This doesn't necessarily mean the ten-person program will end up better, but the chances of all 10 people being on the same page are vastly higher than all those thousands.

I do not want my argument to be used as one against free software; I only want to point out that small, tight teams are likely to produce more coherent programs. I don't think the proprietary vs. free applies here, except that it seems easier to convince ten people to sit in the same room for a few years if you're paying them.

Local maxima

Systems can grow to the point where making a necessary change takes so long and is so costly that the change cannot happen without large rework. In order to escape local maxima, we must be willing to start over.

Over time, software seems to increase the amount of dependencies it has. For example, an abstraction library like SDL will add code for X11, then Wayland, then Win32, then Apple's Metal, etc. New technology is invented, which is a good thing, but the question becomes, well, what do we do with all the old stuff?

How Microsoft Lost the API War discusses exactly this with regard to Microsoft. He reaches the conclusion that the Web is the ultimate solution, which I refuse to accept, and which I believe end-users also dislike. We still install "apps", and we still care about latency, even if we don't know it; things are so bad it's easy to forget what good software feels like.

Maybe the existence of successful app markets is evidence that web developers have done a really terrible job, that no UI standardization severely limits app quality, or that the foundational web technology can never result in as good of experiences for users. Hey, maybe all these are true.

While I'm on Joel on Software, I have to reference his argument against ever re-writing code. I disagree with this essay when applied to the industry at large. It might make sense if you are a startup with a limited runway or a for-profit company, but this attitude in regard to overall industry health and technological innovation is extremely harmful. We must not only be willing to rewrite things, but encourage it! We should be telling every new graduate, "study the mistakes of the past, and build the new future!", not "don't ever start from scratch, just keep piling on".[^1] We need new ideas and we need to try new approaches if we are ever going to evolve.

Software engineering vs. engineering engineering

In that Alan Kay talk he references the construction of the Empire State Building. This got me thinking how programming is fundamentally different from construction engineering. At the core it's about the difficulty of changing the system: a building is immensely difficult to change after it is finished, whereas software can be relatively trivial to modify.

This is something we should embrace. It is miraculous that we have a technology that can so easily be molded into different shapes. However, it comes with some interesting side-effects at many levels.

For example, organizations feel more comfortable taking risks with software. If they can fix the software after it has shipped, they are more likely to take a "more, buggy features" approach than a "fewer, higher quality features" approach and fix the bugs after. This is one reason why technical debt grows even before a piece of software is released--you're already operating under the assumption that you'll fix it "later", because it's relatively easy to fix it then.

The lean startup methodology brought continuous deployment, which meant that software gets shipped without even being tested, because even if the end-users have a bad experience, the change can come so fast that it will be fixed the very next day. I'm not endorsing this as a good practice, just indicating it's something unique to software.

Ideally, we embrace the ease of changing to allow users to customize the software and to allow software to be ported easily to new platforms. We should also try to be aware of the negative impact frequent change has on user experiences when we ship software that might frustrate them, ruin their data, or put them in danger.

Burn it all to the ground

Project Oberon is "a design for a complete desktop computer system from scratch. Its simplicity and clarity enables a single person to know and implement the whole system, while still providing enough power to make it useful and usable in a production environment."

Alan Kay and several others formed Viewpoints Research Institute partially to answer a similar question: If we were to start from scratch, how much code would it take to get us what we have?

Another great paper to read is Rob Pike's "polemic" Systems Software Research is Irrelevant.

Many programmers thinking of these issues fantasize about doing these kinds of rewrite projects. It takes both confidence and, pessimistically, arrogance to believe that you can re-invent the software world better than those who came before. However, we should not discourage people from doing this, for all the reasons I have stated previously:

  • The system could be much stronger with fewer minds designing it (Conway's law)
  • The system could escape the local maxima the previous system was trapped in (Accretion of complexity)

Problems with starting over

The hardest pill to swallow is the upfront cost of catching up. This cost is largely levied by the use of existing technology. Complexity seeps in from every level:

  • Users are frequently quirky and understand things in different ways. This is the domain of user interface/user experience design.
  • People disagree, but must come together and create standards of communication to utilize the physical infrastructure optimally. These standards bring with them significant complexity. They get revised every so often, thereby obsoleting existing software and requiring new work be done to support the new standards. Backwards compatibility can be a way to reduce this impact, but it can also reduce innovation and impose a large cost on implementing a standard.
  • Programmers want to save time by layering on abstractions, each of which adds complexity and possibility of bugs.
  • Computer hardware is complicated, frequently in order to offer higher performance (which I believe is frequently worth it because it enables new possibilities).
  • The physics of the universe itself make things difficult. Computers get hot, bits must be transferred between computers via some medium, there's EM noise and physical vibration, etc.

At a nitty-gritty software level, one might try to mitigate some of these complexities by re-using software. This is made more difficult by the explosion of dependencies between software. In the worst case I've seen, a single JavaScript file to rasterize a font to an image resulted in downloading code from three different languages (Python, C, and JavaScript), took over 3 minutes to download on my 20 Mebibyte/second connection, and failed to run due to missing pip dependencies! Software is a disaster.

Please, don't let this discourage you from trying to start something from scratch! We need people who are willing to do the hard work to advance the field.

Virtualize everything

A common head-in-the-sand approach to attacking complexity is to create your own little virtual machine, then program against it.

This brings along with it numerous problems:

  • You have to implement that virtual machine, which means you still have to deal with that complexity; it's not a way to actually eliminate the complexity at its source.
  • Performance always degrades. Always. At the core of the counter-argument to this is the "sufficiently smart compiler" that will magically make the cost of virtualization/high abstraction go away. It's not here yet, and I wouldn't bank on it being here for a while. JIT compilers bring significant complexity and runtime cost.
  • Hardware matters. The industry has largely been profiting off of orders-of-magnitude improvements in hardware. Virtualizing away what makes one machine special or faster than another is counter-productive.
  • It's another layer which has overhead, has to be learned, can break, people must agree on, etc. It's a liability. It's more. We want less, not more.

A similar approach is to have strict abstraction layers which separate your code from the "other stuff". Interestingly, this approach suffers the same ills predicted by Conway's law. If I'm writing a game in C using SDL as my abstraction layer, I am less likely to do things that the operating system would allow, but SDL does not. It's possible for me to modify SDL to expose the functionality, but there's more friction in doing so, which means I'm less likely to do so.

I'm not trying to rag on using libraries, because it seems like a much more sustainable approach than using gigantic game engines or frameworks. It's a danger we still need to be aware of in all of these approaches.

What's the solution?

The only solution I can think of to these problems is strong culture, community, and education.

We cannot focus on a single piece of technology that will somehow save us--we have to focus on the people. Smart, aligned people can make new technology and tackle new problems.

Culture

People need to care about these problems. If no one believes we even have problems[^2], then we of course will never efficiently solve them.

We need to encourage innovation. When someone starts writing a new operating system, programming language, game engine, etc., we should be cheering them on.

If someone isn't yet experienced enough to practically undertake the project, we can advise them as much while still encouraging them by saying e.g. "I don't think you are ready to undertake that scale of project, but you will get there in time if you continue learning, especially by studying X, Y, and Z."

In comparison, if you ever say "it's stupid to create an X, we already have Y", think really hard about what positive influence you actually have. Think about your Y, and what came before it. Would the creators of that Y have been told the same thing, for their era? Wouldn't you rather have a new, potentially better X in ten years, rather than aborting it at its inception?

Similarly, we need to encourage digging deeper. We should encourage web developers to learn about cache lines and GPU architectures. Game devs should know how the internet works. Don't encourage people to only learn the minimum amount necessary to complete their next task. Learning as little as possible is not a virtue. Take joy in exploring all the fascinating innovations. Take note of all the cruft you find on the way so you can learn what not to do.

Community

You need people willing to do work to fix these problems. Those people likely need to talk to other people to get advice or increase their understanding.

This is one of the shining traits of free software. Knowledge gained from studying proprietary software is restricted to that company and the people who bring knowledge from other companies. Clearly an open industry is superior; artificial restriction of knowledge can be nothing but harmful to innovation.

As a small PSA, please don't start your community on a closed platform like Discord. These platforms are not good for long-term archival and search. The data aren't available to the public. You pour more fuel on a closed, proprietary future rather than supporting an open and free discourse, of which we have multiple free and open protocols to choose from.

Education

New people are constantly being born, growing up, and entering the field. We need systems to educate people on how to combat these problems.

If you think universities instill any good software development practice, well, you haven't interviewed many new grads. This isn't necessarily the fault of universities. They rarely have a stated goal to create good programmers, and instead focus on the science of the field. There's definitely value to that; the danger is assuming that someone with a CS degree knows anything about writing good software. Don't believe me? Next time you read a CS paper, actually check out the code in their GitHub link. Again, writing good code is not really a goal of papers, nor the university.

Technical schools are similarly suspicious. Coding bootcamps frequently hire their own graduates to be instructors, meaning the instructors never gain real industry experience or write significant programs before parroting what they were taught. It's effectively playing the game of Telephone with programming knowledge.

Education in the game industry

The game industry has GDC, a conference where developers gather to share insights gained in the field. However, talks frequently cover the "what we did" rather than the "how we arrived here" nor "what was tried but ended up failing". Most GDC talks are pay-walled in GDC Vault. Talks frequently are thinly veiled recruiting advertisements rather than honest attempts at sharing knowledge. They are often mile-high views without revealing any deep technical details.

The game industry does not have an open source software practice. id Software, while under John Carmack, is one of the only AAA game studios to release the entirety of their game code under free software licenses. Since Carmack left, they no longer follow this practice. Indie developers are no better on this front, despite the drastically lower legal oversight that might cause a corporation more friction in releasing their code.

University courses in games are becoming more common, but courses which teach game tech fundamentals (as opposed to specifically Unity or Unreal) are becoming increasingly rare. People are graduating with degrees that certify they know Unity, not how game engines actually work. These practices do not bode well for innovation in engine technology in the long term.

Conclusion

I am consistently frustrated with my experience writing software. What is a fundamentally simple task is often made incredibly hard to implement due to accidental complexity and cruft. I feel powerless to fix everything, because I have limited time and limited willpower.

However, I continue to learn and grow, and hope that I can positively impact the field by contributing to and participating in these solutions I've proposed. At the end of the day, the magic of software keeps me going, and I hope I can somehow spread that magic and amplify it with my work.

If you have thoughts, I encourage you to share them with me at [email protected].

[^1]: For those feeling motivated to jump right in, I will temper this advice by saying that you should really know the actual problems and state of the art before trying to solve them. This can take a few years of study and experience before you will actually be able to know where the problems and the frontiers are.

[^2]: I have raged against the Unity- and Unreal-ification of the game industry time and time again. It seems the majority opinion at both the indie and AAA level is that moving to the big U engines is a good, perfect thing with no drawbacks. The short-sightedness of the engine commoditization is the second biggest problem in the game industry in my opinion, the first being the rising the cost per byte (Raph Koster - Industry Lifecycles). These two are intimately related--the verdict on whether moving to big-U's is better rests on the answer to the following question: are our games getting better (higher quality, and not just graphically), easier to make, and faster to deliver? If the answer is not "yes" to all of these, there's something lost here.

Community Showcase

This is a selection of recent work done by community members. Want to participate? Join us on Discord.