Tuesday, March 24, 2026

Programming languages and paradigms

Steps for creating a programming language:

  1. Think about who will want to use your new language and for what.
  2. Conjecture a unified mental model of how a programmer should think about your language. Make it fit how your users think and their expected use cases.
  3. Translate your mental model into machine code.
  4. Announce your new language to the world, making sure to report how users should think about your language.

Steps for creating a modern general-purpose programming language:

  1. Define your target audience as "everyone" and their expected use cases as "anything ever".
  2. Make a unified mental model of how everyone should approach doing anything ever. Model it after how everyone currently does everything.
  3. ???
  4. Be a large corporation. Make everyone use your language to interact with your products. Eventually, people will start using it for other tasks out of familiarity.

That was probably the fastest I have yet derailed a blog post. I want to talk about how to make a proper mental model.

Switch-statements

C# requires a control flow statement at the end of every case in a switch-statement. I'm sure they wished to add an implicit `break` after every (non-empty) case, but that would have shocked developers coming from C, where cases fall through by default. So they resigned themselves to making the user choose control-flow behavior explicitly, presumably reasoning that any default would surprise someone.

I know why C chose default fall-through and C# didn't.

In my oversimplified understanding, control flow constructs compile to jump-instructions--the equivalent of using `goto` everywhere. Switch-statements compile to jump tables, which figure out how far forward in the code to jump before you start executing again.

Notably, a jump table says nothing about where to jump after that. It doesn't know what a switch case is. So a `break` statement at the end of the switch case is another jump instruction--extra effort for the runtime. Fall-through is the default in C because it is the default for the implementation. C probably expects its programmers to know already basically what the assembler will do and to write programs based on that knowledge.

C# doesn't do that. C# would consider that an implementation detail. Why would you need to know assembly concepts to program in C#? In C#, fall-through requires the use of `goto`, the most damning marker of unidiomaticity that I can imagine. The creators of the C# language did not want people using switch-statements with fall-through. (Stephen Toub does, though, if I remember correctly.)

(Actually, switch-statements don't always compile to jump tables in C#. The probably do if you're switching on int (or char or bool), but they can also do pattern matching using the same syntax. In C#, the code you write may have a completely different implementation based on context.)

The point

Why does every modern language try to be opaque?

Richard Gabriel says that simplicity of implementation is at least as important as simplicity of interface. What sense does this make unless the user is supposed to understand the implementation? And Joel Spolsky argues that the user will need a correct mental model of the implementation anyway. These are big figures in software, worthy of our attention.

We all agree that programs should be simple. Is there any plausible measurement of software simplicity other than how easy it is for the computer to run? ("Quick for the programmer to write" is a separate question; a throwaway script may rely on brute force.)

To write good code, you must first have the right mental model. The right mental model is that which most correctly describes the problem. In programming, every problem is flipping some bits somewhere into the proper state. So should we not be thinking in those terms? Should we not use technologies that we are allowed to understand?

Friday, March 20, 2026

Managing vim plugins

I wish to draw attention to this brilliant Reddit comment:

vim-plug, minpac or just git clone to your ~/.vim/pack/my-plugins/start/ directory.

My update script now sports this crudely elegant addition:


while read -r dir; do
    cd "$dir"
    git pull
done < <(find ~/.vim/pack -maxdepth 3 -mindepth 3 -type d)

And I need not remember which plugin manager I installed. Everything is taken care of.

I love simple technologies.

Thursday, February 12, 2026

Review: The new Outlook for Windows

Outlook is asking for my feedback. I went to the bother to type it up, so I'm posting it here as well to get as much mileage out of my effort as possible. I don't trust Microsoft in general to read feedback, but I think that the Outlook team does.

Likelihood to recommend: 1/5

Why?


The feedback

I am a programmer, not a suit. Outlook's strength is in coordinating meetings. My job doesn't require many meetings.

I have to use Outlook because my company does. The features I would look for in an email client:
  • Plain-text email
  • Markdown support
  • HTML editing
  • Keyboard-based navigation
  • Scripting capabilities
I do appreciate that Outlook now allows me to check email headers.

I find Microsoft 365's Ctrl+Backspace behavior idiosyncratic and unhelpful--except that it works properly in this feedback box, which is nice.

I actively disable any sort of smart tooling, even spelling autocorrection, and definitely including Copilot. I don't use Copilot to draft emails because it would be simpler just to send the prompt. I don't use Copilot to summarize emails because I don't read emails that need summarized.

Outlook and Teams have all these buttons I will never use and care about, but I still get a reminder for a recurring meeting every day at the default time even though I changed the reminder time. The notification comes up fifteen minutes before the meeting. The little drop-down box says "10 minutes before", which is what I selected. It is most amusing.

The "Upload to OneDrive" button

  1. I go to attach a file to an email.
  2. The file is too big, so I drop it onto the button for "Upload to OneDrive and send link".
  3. I send the email.
  4. The recipient emails me back to tell me that he can't access the file at the link I sent.
What good is a link that the recipient can't access? If I am attempting to email a file, can we not assume that I wish the recipient to be able to read it? Why can't the "Upload to OneDrive" button make the file accessible by the recipient of the email? Or at least provide a quick way to do that?

I feel as though the assumption is that I will mostly send files to others within the company. But I do nearly all my internal communication through Teams. So if I send a file by email, then I am almost certainly sending it outside the organization.

(Unless I am sending a script that isn't in the Git repo. If I send new versions by Teams, then Teams will increment the file name of each version. This increment will include a space character, forcing the recipient either to rename the file after downloading it or to quote the file name when he types it into the terminal. So I might send the script by email to avoid having the file automatically renamed. But I digress.)

Thursday, February 5, 2026

Generate more, parse less

Be liberal in what you accept, and conservative in what you send.

--Postel's Prescription. (I think that this popular quote is relevant to this post, but I can't decide how.)

I wrote a code generator to take a C enum type and make a string parsing function for it. While writing this parser, I ran into parsing difficulties.

Code has to be generated from some other information. To get that information, I had to read it from another file. So I had to parse it myself before I could write my parser.

On my first try, I started by reading the header file containing the enum type. I immediately ran into apparently necessary feature creep. What if the user put other code before the enum in the file? What if the user defined values for the cases? These cases would require me to expend a lot more work to implement, but a user would be shocked to find that his code suddenly didn't work because he did these perfectly normal things. I implemented more features than I needed for my use case, but still felt that my approach was far too naive and constrained. Eventually I gave up.

The next time, I made a text file listing the cases I wanted. Then I iterated over that and generated both the enum and the parser function. Easy as pi.

For the understanding of non-programmers, which looks easier to deal with?

gardening-tools.h:


  #ifndef GARDENINGTOOLS_H
  #define GARDENINGTOOLS_H
  
  typedef enum GardeningTools {
    Trowel,
    Hose,
    Dynamite
  }
  
  #endif // GARDENINGTOOLS_H

Or gardening-tools.enum:


  Trowel
  Hose
  Dynamite

But they contain the same information! I can generate the second from the first!

If I have to read data, why would I encode it in a format that has more features than I need to use right now?

Hooray for flat, simple data layouts! My three lines of data doesn't need the power of C. Here is the entire spec for my layout: Put a line break after each case name. I get the enum type name from the input file name.

I can add more features if I wish. Since my format is simple, modifying it is easy. Values for the enum cases? Put a space between the case name and value. File name different from the enum name? One extra parameter. Documentation comments? Now lines starting with "//" pass through to the output unmodified. But I probably won't need any of that, so why bother now?

Code generation: Accomplished

Number of library references added: 0

Tuesday, January 20, 2026

Rethinking my past dislike of C#

In response to my previous post.


We don't use "goto", of course, because we want our iteration to be structured.

But "for" is only for collection iteration with indexing. For everything else, we use "while", even though "for" gives the reader the accumulator variable, means of accumulation, and end condition upfront. And then we scatter in a few "break"s to confuse the hurried reader further.

Still annoyed at the limited idiom of "for".


We use "for" instead of "foreach" for performance

On lists that could have been arrays. We always call "ToList", never "ToArray".

ToArray often builds a list first, then converts it to an array, which takes an extra copy operation. ToArray could be worth it if you're just using Select or other length-preserving transformations.

Also, most codebases don't use "for" instead of "foreach" for performance. That's not idiomatic.


We don't need discriminated unions. Classes and interfaces are enough for any situation.

And then we "switch" on them and have to repair bugs having to do with missing cases that were created later.

Now that I am older and wiser, I realize that my greatest mistake was when I thought that inheritance might have been worth using to get exhaustive matching. If you are programming in C#, learn to accept that you don't get exhaustive matching. If you use inheritance, you will end up with so many layers of indirection that you never quite know where to find the code that does the thing. Don't do polymorphism. Kill it on sight. You are allowed to use "if" and to switch on enums. That is all.

C# is supposedly getting DUs one of these days. I wonder whether that will change my opinion.


We don't use static fields and singleton classes. Instead, we use dependency injection frameworks to ensure that only one instance of each class is generated and that all other classes have access to that instance. Which is different.

I'm withholding judgment on DI until I have been bitten by global objects. To figure this one out, I'll need to use more global objects.


We don't have an order of compilation below the project level. Every class in a project, unless it is private to another class, is accessible from every other class. We deal with circular class dependencies by not worrying about them. Code goes wherever happens to feel best at the moment.

I haven't thought about this in a while. F#'s file ordering was quite nice.

I think that this problem probably mostly goes away if you understand what your program does. Of course, this requires writing a program that a human can understand.

The non-F# solution is to split the program into small units with narrow interfaces. Maybe that is why the early C programmers encouraged us to split functionality into separate executables and communicate between them with plain text. Your interface can't be very complex if you have to parse all input and output.


"IConvertsFooToBar" is supposed to be prettier than "Func<Foo, Bar>" for some reason.

Granted, "Func" is an ugly word. But I guess we had already used up all the punctuation elsewhere.

And we need to have separate "Func" and "Action" delegates since you can't have a value of type "void".

Same answer as for polymorphism: C# wasn't designed for passing functions. Don't do that.

Don't pass interfaces, either, if you can help it. Refer to the previous comment on avoiding polymorphism. Flat code is easy to read.


We use static typing so that we can be sure that all representable cases are valid.

Unless they're null. Anything could be null. (Except structs, but we don't use structs.)

A defined value representing "no value" is highly useful. This is a sub-problem of understanding your program's flow so that you know the range of possible values at any point. This, again, requires writing an understandable program.


"default". Without "default", you have to make two versions of every generic method, one for classes and one for structs. With "default", we still have to make two versions of every generic method since the two have completely different behaviors. For classes, it returns "null", which might blow up your program if you're not on guard. For structs, it returns an instance in which every field is set to its own "default", which is significantly worse since it's very difficult to detect.

Now that I have written a little C, "default" makes sense. Now it is the distinction between structs and classes that I don't understand. Why can I only allocate structs on the stack and classes on the heap? Isn't allocation strategy more related to data access and lifetime than to data type? I have yet to explore functional programming in C, but at least I can pass a reference to stack-allocated data.


Exceptions are unexceptional. We wrap deep calls in try/catch because something in there will probably throw an exception at some point, and just because there was an exception doesn't mean that we need to crash now.

Catching all exceptions from a function call is evil; you don't know exactly where the exception was thrown or how the program state has changed. You must at least take some effort to recover rather than continuing blithely on as though no error had occurred.

That's more a pitfall than a language problem, though F#'s native Either type provides a much nicer alternative to exceptions than C# does.


Everything is a "manager". "Manager" is pretty close to synonymous with "class" when you think about it.

This is an anti-pattern, but also C#'s fault for making us wrap all code in classes while telling us that each class should do one thing. How do you name a class and method that do one thing? WidgetFrobber.FrobWidget? WidgetFrobber.Frob? WidgetFrobber.Do? FrobWidget.Execute? I usually go with WidgetFrobation.Do, but it still feels wrong. Organizing code by verbs is often better than organizing it by nouns.


I don't mind C# as much as I did then; I have since spent much time learning to simplify code to avoid the features that make C# painful to use. C# has records now, which take most of the headache out of classes. If you avoid polymorphism of any sort--interfaces, inheritance, or passing delegates--and structure your code as a straightforward imperative program, then modern C# isn't so bad.

Monday, January 19, 2026

Do you have left-pad syndrome?

The npm left-pad incident

How to parse CLI args:

  1. Iterate over the array of CLI args.
  2. Inside the loop, switch on each item.

Just make sure, if any of your options take parameters, to use a for-loop instead of a foreach-loop so that you can just increment the counter in the switch case.

If you install a library to parse CLI args, you might have left-pad syndrome.

How to parse CSV:

  1. Call String.Split.
If you install a library to parse CSV, you definitely have left-pad syndrome.

Monday, January 5, 2026

My device.

My device. MINE. You can't have it.

You may not install games or "suggested apps" on it without my permission.

You may not preinstall apps and prevent me from uninstalling them.

You may not show me anything I did not ask for, whether advertisements or pop-ups that helpfully inform me that no malware has been detected and thus no action is required.

You may not send my information somewhere else without my permission, whether my credit card numbers or my browsing habits.

You may not throw new icons on my clean desktop every time I install updates.

You may not redirect my Internet searches to your own search engine.

You may not use my accounts to send phishing messages to my contacts.

You may not use my device to spam anyone else's device with information until it crashes.

You may not add toolbars to my browser.

You may not beg me to make this program my default at any point after I first run it.

You may not throw away files I have downloaded because they are not from an organization you recognize.

You may not install half an operating system with every program and then take fifteen minutes to install updates for them.

You may not do anything involving "analytics". I might make an exception if I specifically trust you as an organization or individual.

You may not add cookies to my browser that will not in any way benefit me. "Targeted advertising" does not benefit me; you know what I want right now by which web page I am visiting.

You may not pop up a survey request that won't go away if I ignore it.

You may not read my emails.

You may not disable my working camera because some sensor has gone bad.

You may not refuse to use my working camera because my other camera is broken.

You may not sneak yet another AI onto my device through program updates.

You may not refuse to do anything whatsoever, including close, unless I reach for my mouse and click you.

You may not turn my Number Lock off when I did not press the Number Lock button.

You may not make my programs to freeze when I tell them to open.

You may not countermand any decision I make because you think that you know what I want better than I do.

My device shall do what I tell it to do. My device shall not do what I do not wish it to do, except if I tell it to do so.

Very little in this post is true. "My" devices mostly obey the orders of various corporations with blithe disregard for me. They might do anything; I would never know. I can foresee no happy ending.

Programming languages and paradigms

Steps for creating a programming language: Think about who will want to use your new language and for what. Conjecture a unified mental mode...