In my last article I wrote about integrated development environments. Lately, I have been thinking about how regular applications would benefit from being easy to modify. Imagine every application also included a development environment tailored to the application.
Stop Writing Dead Programs by Jack Rusher captures a lot of where my head is at here. There is a huge amount of value both for the user and the developer in pursuing this.
Trivial and natural modifiability
Emacs is the only program where I truly feel that I can naturally modify it. The code for any function in the system can be quickly and easily navigated to. Modifying the code is as easy as making my changes to the text, then highlighting it and "evaluating" it, all while the application is still running. This level of ease puts most modern software toolchains to shame.
The amount of friction to this sort of modifiability is crucial to the amount of value it can add. In Interface ergonomics I argue for lowering friction because it often changes what things people are willing to do. The same applies here–if I know it is easy to add a feature or fix a bug, I am much more likely to do it.
A natural path to programming
If more programs were easy to inspect and modify, the chances of more people getting into programming will naturally increase.
The web is a good example of this. When I was a kid I would "View source" of a web page. This allowed me to glean some information about what went into the creation of that page. The web developer tools built into Firefox and Chrome are an example of a development environment shipped to every user. These give people the chance to play with the markup, styles, and code that created the website, and might eventually inspire them to write their own pages from scratch.
Compare this to a native desktop application. There is absolutely no exposure of the internals, and no clear way to learn about how it is made.1
A great middle-ground application example is Blender. Blender's Info Editor will report the Python code for each operation the user performs. For example, if I use the Add Mesh -> Cube from the user interface, I see the following line appear in Info:
=2, enter_editmode=False, location=(0, 0, 0))bpy.ops.mesh.primitive_cube_add(size
I can then copy-paste that line into the built-in Console or Text Editor. I can tweak it and modify it, and combine it with other commands in order to create whole chains of commands. This provides me a way to ease into the scripting system, because I can perform the operation by hand first to learn the necessary commands to execute in my scripts. I can start to share my scripts with others, and read scripts others share with me. Eventually, I gain enough experience that I can read straight from the API documents, which can be more efficient.
Blender also provides two options, Python Tooltips and Developer Extras, which show me information about the application's workings when I e.g. hover over a button. I can hover over e.g. Render Engine and see what Python objects are associated with that field, which directly informs me as to how I can read or modify that field's value from my scripts.
I can also open the context menu by right clicking on the Render Engine field and select Edit Source. This pops open the actual source code that caused that field to be rendered in the UI, on the exact line which added it. Even if I didn't plan on having my users utilize these features, I want this kind of functionality for creating the program!
A positive influence on fighting complexity
If you ship as part of your application the ability for the application to essentially rebuild itself, you must ensure that this works reliably on many different machines and in many different situations.
This helps to resist the natural urge to have a single build machine or build farm with a complex configuration that builds all of your releases. This kind of organization might be convenient for the developer, but it means the user has very little chance of building their own version, and the setup has very little pressure to be streamlined or simplified.
Fundamentally, a software development toolchain's purpose is to translate a human-readable language into executable machine code (or bytecode run by an interpreter). Any additional complications beyond a small pile of machine code that does this transformation should be candidates for elimination in the name of performance and simplicity. I'm talking about containerization, and virtualization, and GNU GCC + binutils, and LLVM, and all these giant complex things.2
Additionally, building software which is intended to be understood and modified by the user encourages you to build that software better. Your APIs will take more thought and have more pressure to be well-documented (or self-documenting), which will end up making your life easier as well.
Modifiability helps free software
I am a believer in free-as-in-freedom software, and license all my code under the GNU General Public License in support of this belief.
Whether or not you believe in this license, modifiability as a feature can provide a huge amount of value to your users. If you do not want to give your users freedom with your software, you can still support modifiability by using restrictive licenses on your source. An example of this is Epic's EULA for Unreal Engine, where they feel comfortable allowing anyone to view their code, but restrict you on various other aspects, and charge royalties. This may be known as a "source-available" license, because is still allows room for users to modify the software extensively when compared to completely closed-source arrangements.
While I would encourage you to release your software under a free (libre) software license, I would still prefer at least letting your users modify the source to their benefit, even if they don't have the freedom to share those modifications with others.
If you do support free software, if you make your applications easy to modify, you demonstrate the value of free software immediately. Users will know the software is free because they can easily see and might directly modify the code. Without self-modification, it can be hard to tell that an application's source code is free because users must go out of their way to find it.
Many software toolchains are fragile and complex, which scares away many people from joining software development. By simplifying and integrating the software build toolchain, you ensure users have a lower barrier to entry to modifying the software, which increase chances of more people developing free software (and contributing back to your application).
Cakelisp and self-modifiability
I had two main goals with Cakelisp:
- Support compile-time code generation and code modification with little to no runtime performance drawbacks.
- Support runtime code hot-loading, similar to Naughty Dog's GOAL. This would be more in line with what Jack Rusher argues for in that talk.
I was able to achieve the first goal, but I have not been satisfied with my attempts on the second goal. I started writing a linker/loader in order to address this, but have since pivoted to using Tiny C Compiler as a base for code reloading instead. I have a modified version of Tiny C Compiler which allows me to re-use data symbols from past compilations. This accomplishes what I set out to do with my Cakelisp hot loading code, but is much more robust because it re-uses all data symbols, not just Cakelisp-defined data symbols. This means third-party code can be hot-reloaded without any modification.
I am continuing on this path of using Tiny C Compiler to find the advantages and challenges with the approach. I think there is a huge amount of value in existing C libraries, so leveraging those while still advancing in program dynamism and toolchain simplicity seems like a win to me.
Note that there is no inherent reason why a compiled language cannot be as dynamic as an interpreted language. The simple fact is that the compiled languages were not designed with interactive development in mind. This is not a technical limitation, which means that with some changes you can add dynamism to compiled languages without the serious performance impacts introduced from going to an interpreted/virtual machine language.
Self-modifiability is the exception, not the rule in modern applications. Most software is built with the assumption that there is a "developer" and a "user", and the users do not often become developers. Software developers and designers assume that they know best, and that it is their job to support each and every use case that users might want. This should change.
By building tightly integrated, unlimited, and convenient modifiability into your applications, you give your users the chance to solve their own problems. You will of course still need to provide value by improving the application according to your vision for it, and in order to gain users you must build base functionality which the majority of users desire. But after that, you can provide a huge amount of value to users by empowering them to modify your application.
We should leverage the fact that tailoring a software application to your individual needs is easier than any other product in history. As an industry, we need to admit that one size does not fit all.
Despite this, I still much prefer writing desktop applications over writing web pages. There aren't any technical reasons why desktop applications cannot be as easy to inspect and modify as web pages. Web pages simply must be this way because it is how the web works–you visit a web page by downloading its data and scripts and executing them locally. If it was required for applications to work this way (for example, in Smalltalk, Lisp machines, etc., but not in Microsoft Windows, GNU/Linux, macOS, etc.), they would as well, but it isn't required, and so it isn't done.↩︎
For inspiration, watch Jonathan Blow's Preventing the Collapse of Civilization.↩︎