Linus's stream

Software design today usually lacks the kind of detail that's pervasive in reality. A part of me thinks what people really liked about 2000-2010 skeuomorphism in software design isn't actually skeuomorphism per se, but the richness of detail. Perhaps we should be less minimal in design?

From The Ongoing Computer Revolution, Butler Lampson (emphasis mine):

Xerox asked us to invent the electronic office, even though no one knew what that meant. We did, and everyone’s using it today. That makes it hard to remember what the world was like in 1972. Most people thought it was crazy to devote a whole computer to the needs of one person—after all, machines are fast and people are slow. But that’s true only if the person has to play on the machine’s terms. If the machine has to make things comfortable for the person, it’s the other way around. No machine, even today, can yet keep up with a person’s speech and vision.

I've been thinking about how we might integrate better machine intelligence into our thinking-writing tools, and one thesis I'm developing is that it's important that machines and humans can collaborate on the same document. Writing is how we think. If we want to think together with computers rather than using computers, we need to write together, not simply with the computer as a blunt tool for recording our own words.

Popular approaches often stick software-driven suggestions or connections in a sidebar or a context menu or squiggly red lines under our own text. But I really want my software to write alongside me, underneath my bullet points and in my margins, as if I'm editing and thinking together with a colleague. I want my eyes to slide seamlessly between my words and the machine's, and trust its voice.

I may sound pedantic, but I think there's a huge qualitative difference between a machine as a thought partner correcting my writing and being asked for help, versus the machine working with me and contributing proactively at a level equal to my own creative power.

I want to focus some slice of my research time on the question: how can we make collaborative authoring and thinking with computers more seamless?.

While implementing the Levenshtein edit distance algorithm in Oak, I came up with a handy quick benchmark helper function:

fn bench(name, f) {
	start := time()
	elapsed := time() - start
	'[bench] {{0}}: {{1}}ms' |> fmt.printf(
		math.round(elapsed * 1000, 3) |> string()

Use it as with bench('...') fn { ... }, like

[3, 4, 5, 6, 7] |> int(pow(10, n))) |>
	with std.each() fn(max) {
		with bench(string(max)) fn {

for output like

[bench] 1000: 1.737ms
[bench] 10000: 13.052ms
[bench] 100000: 131.559ms
[bench] 1000000: 1316.693ms
[bench] 10000000: 12747.569ms

Naming Oak's std.uniq

I was thinking about adding a couple of functions for de-duplicating lists of values to Oak's standard library. Here, I ran into a naming problem. Names in the language standard library are really important! They have to be short and memorable, but accurately represent what they do with minimal room for confusion.

This was my plan: One function would take [a, b, b, a] and return [a, b, a]; and the other function will return [a, b]. In other words, one returns a list that de-duplicates consecutive occurrences of a thing into just one, and the other sorts before de-duplicating so that elements occur at most once in the whole list.

It seems like different languages and environments use the name uniq to mean either of these operations. Some languages also use dedup for the other. What should Oak do?

For now, I think I'll just implement the first of the two functions, and call it uniq to be memorable. The other is a simple list |> sort() |> uniq() if it's needed, and having one name and one function reduces room for confusion. It also mirrors the UNIX command line idiom sort | uniq nicely, which feels right. This approach also means std.uniq won't need to depend on sort.sort, which depends on std; so this avoids a circular dependency.

An epiphany I had while preparing for a Metamuse podcast recording and reading through an old Hacker News thread on building my own software ecosystem -- none of this is really about productivity. It's pretty difficult to make the case, even if I can build these things quickly, that my time is not better spent elsewhere.

I think more importantly, building your own tools and software is about changing your relationship with the software that runs your life. Maybe I don't get more work output per hour of time invested, but I trust my tools more, it feels more ergonomic, and there's an intangible benefit to a deeper, more durable relationship I can have with the tools that I have my hands on for so many hours of the day.

As of this week, Oak is at a stage where what I consider "basic" features of the language and surrounding tools are done. These include

  1. The oak interpreter (obviously)
  2. Syntax highlighting in my editor of choice (Vim)
  3. Automatic code formatting (oak fmt)
  4. Compilation and bundling to single-file programs, especially to JavaScript/the web platform
  5. Basic standard libraries, including a Markdown renderer and date/time utilities

It puts Oak at toolchain feature parity with Ink where I left off with it, and makes me very comfortable to finally build on Oak, rather than simply work on Oak the language and toolchain.

Since I've gotten to this point, I've found myself keeping a terminal tab with an Oak program and a repl open, and tinkering and playing with it from time to time. Mostly writing programs that don't do anything special, like:

std := import('std')

Message := 'Hello, Mac!'
len(Message) |> std.range() |> with std.each() fn(n) {
	std.println(Message |> std.slice(0, n + 1))

Nonetheless, I enjoy it and it occasionally leads to interesting hacks. It's making me think about whether being able to play with a tool is a vital aspect of a good tool. Play is where a lot of discovery and divergent thinking happens, and where a tool can really come to feel right in your hands.

I've spent a bunch of the last weekend and some of this week working on oak build --web -- the Oak language toolchain feature that lets an entire Oak program (across multiple files) be cross-compiled into JavaScript, to run in browsers or on Node.js/Deno. I've done this once before for Ink with September, but there are a few improvements in the way I'm doing oak build.

  1. Most obviously, oak build is a command built entirely into the interpreter binary. Even though it's written in Oak (and therefore self-hosted), the whole thing is baked into the oak executable. This means no need to clone a separate repository / project like September. It also means it gets tested with the language's standard library tests, and that I can assume every language user has it.
  2. Speaking of tests... oak build outputs are continuously integrated against the entire Oak standard library test suite, which is something that wasn't possible with September because...
  3. oak build can take a single entry point program file and recursively follow top-level static imports to figure out which other files need to be included in the compiled bundle (including standard libraries for the JS output). No more passing multiple files to september translate.

As with many other parts of Oak, I'm really appreciating the opportunity to make architectural decisions with experience "from the field" to design with much more foresight than my first attempt.

Specifically, I'm pretty proud of the fact that oak build's current architecture lets all of the tokenizer, parser, static analyzer, bundler, and some of the code generator share code between compilation targets (Oak and JS), yielding a much more maintainable codebase.

Knowledge tooling is mostly reified notation, and everything else in service of that notation.

Spent a good half hour reading, reading over, and thinking about The Immortal by Jorge Borges. As with other works of Borges I've read, I'm taken by his eloquence and depth and the layers and the connections in the text. Absolutely one I'll be coming back to.