Monday, December 27, 2021

Making a turn-based game: A game server on DreamHost

The next step in setting up our turn-based game is prepping up our server to do double-duty: it needs to both vend static HTML content and handle dynamic client-server traffic to run the game logic. To that end, we need to extend the initial configuration to support both.

Koa server

Boardgame.io integrates with the koa framework to serve HTML. It's a relatively straightforward web server that supports a static server (koa-static); the server is even instrumented with the npm debug library and can give debug logging output if the DEBUG=koa-static env var is set.

Armed with this knowledge, I can improve the server.js file to vend static content.

const frontendJsPath = path.resolve(__dirname, '..');
const frontendPublicPath = path.resolve(__dirname, '../../public');
server.app.use(async (ctx, next) => {
    console.log(`requesting ${ctx.request.path}`);
    await next();
});
server.app.use(serve((frontendJsPath)));
server.app.use(serve((frontendPublicPath)));

Now we're making progress, but the client code doesn't work because the browser can't import npm modules. It's time to bundle up the client-side code into something a regular JS browser can handle.

Webpack

Being already familiar with webpack, I chose to grab that as my weapon of choice for generating browser-compatible code. First step is to get it into the project:

npm install --save-dev webpack
npm install --save-dev ts-loader

I set up the webpack config based on the guide and changed my build rule to tsc && webpack. My output is now public/bundle.js instead of build.

At this point, I realized I'd need two different configs for client and server code. I can do this with two tsconfig.json files and the tsc -p build option to select which one to build. With a little reorganizing of my code (client-specific in "client", server-specific in "server", and common in "game"), I can now generate two build artifacts: a server that can run in Node, and a client that the server vends to a browser that connects to it.

After uploading all that content (the build artifacts and the node_modules directory) and giving it a run, I had a successful instance of the game running from my server.

The Swatch user interface, showing a color to name


Now to set up a lobby.

Supporting a lobby

Boardgame.io provides a rudimentary lobby server that makes it easy to create and connect to games (just replace the top-level game object in your code with a Lobby client). Not quite documented is that lobby also assumes that HTML components that are class hidden will be hidden from user view (display: none css rule works fine). At this point, I also discovered that the default config rules for caching on my Dreamhost server didn't work well; every piece of GET content was served with a Cache-Control: max-age=172800. I added some cache-breaking to the .htaccess file:

Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires 0

Hiding colors from end-users

One last bit of housekeeping at this stage: the color database is a JSON object that gets read into memory, but it should live only in the server memory; clients should not get a whole copy of it. To that end, I stored the colors in private/colors.json and used Node to fetch them (require('fs'), require('path'), dynamically load the file at boot time). I also had to get those files out of webpack's field of view so it didn't try (and fail) to bind fs and path into browser code. The following got added to webpack.config.js:

resolve: {
  fallback: {
    'fs': false,
    'path': false,
  }
}

So in the common code, I can check if fs.readFileSync is defined, and if it is not I know I'm in the browser and can avoid trying to load colors.

Give it a try!

The game is up and available for 2 or more players at http://swatch.fixermark.com; feel free to try it and let me know what you think!

Next post, I'll talk a bit about re-architecting the game to support different kinds of rounds.

Monday, December 13, 2021

A Whirligig for a play

Taking a short break from talking about Boardgame.io for a project I took on this weekend.

My wife is in a play this holiday season, and one of the plot points is a kid struggling with a science fair project getting a little help from the main character. I went ahead and did some prop work for the before-and-after on the project, and I'm pretty happy with how it turned out.

Preparation

Amanda sourced several STEM kits from Sntieecr for raw materials so we could make the prop dynamic. They were pretty good, but they don't include any structural elements. I had a few yard stakes in the basement that I figured had enough bulk to serve. When it comes to woodworking, I'm absolutely amateur; I don't even have a jigsaw or a workbench. But I was able to jury-rig a bench with random stuff in the basement, and with some grunting with a saw I was able to segment a few yard stakes into 8" and 10" lengths.
A jury-rigged workbench: two folding chairs supporting a single shelf from an old Borders bookshelf. On the shelf, my drill kit, saw, the wood, and some clamps.
Literal basement-tier work

I figured I could make a sufficiently-sturdy shape with a 3-4-5 right triangle and a cross span to keep the whole thing from tipping over. Yes, my understanding of mechanical stability pretty much tops out at this.

Against the backdrop of a stack of three cut lawn spikes, "The plan": a rough sketch of a windmill.


The work

One thing I was concerned about was that without a jigsaw, cutting an angle into the top of the diagonal was going to be a real chore. Then I remembered that since I was using yard stakes, the original manufacturer had tapered the end for me. Thanks, industrial manufacturing!

Two trusses, one including a diagonal brace, the second with the diagonal brace pulled away.



With a "before" and "after" truss finished, I was ready to put the STEM kit on them.

On a large table, the two trusses and an open bag of small parts: motors, switches, plastic fan blades that fit to the motors, battery cases, and a large assortment of wires.


Here I hit a small snag: after affixing one of the light bulb sockets, it came apart when I screwed the light bulb in! It turns out the whole affair (socket threading, anode terminal, cathode terminal, and a paper insulator between) was held together with a single screw, and it had shipped loose; screwing the lightbulb in unscrewed te screw and dropped all the parts out of the socket. Once I figured out what had gone wrong, it was pretty straightforward to put together.

The kit worked great once assembled, and I was excited to see it in action.


The last step was to bond the gadgets to a base board for easy transport, I was worried about a screw sunk into the stakes on the thin side splitting the stakes. So I used hot glue. A lot of hot glue.

Close-up on the joint showing hot glue
There are problems hot glue cannot solve.
Someday I'll meet one.

The result

Overall, I think they turned out okay! I hope they're robust enough for handling during the show, but I think they'll be fun!

The "before" whirligig, with several small parts laying around it indicating it's half-done.


The "after" whirligig, notably Christmas-themed.

Monday, December 6, 2021

Making a turn-based game: Getting started with boardgame.io

For my game, I went looking for a framework that would handle some of the boring parts and free me up to focus on the game mechanics and logic. It didn't actually take much hunting to stumble upon boardgame.io, which I was surprised to discover handled just about everything I wanted.

In this post, I'll talk a bit about my choice of engine and the steps I took to get started.

Architecture

The boardgame.io framework has several features I appreciate:

Language and libraries

  • Designed to run on NodeJS, which I'm already familiar with
  • Includes React components
  • Includes TypeScript type bindings

Features

  • State synchronization between server and clients
  • Client-side prediction of next move on public state, to speed up the experience
  • Automated client information hiding
  • Game state machine library
  • Debugging UI
  • Integration with a couple of backing stores
  • Rudimentary multi-game server and client lobby

The framework was designed with Heroku in mind, but I was able to get it working on a shared DreamHost server. For the remainder of this post, I'll go into that process (which involved a lot of dead-ends, but eventually worked!).

Getting started with a boardgame.io project

For this project, I set up a pretty standard Node project:

  • installed nvm using the wget directions
  • nvm install 12.22.7 to sync with version required by DreamHost for compatibility with Passenger Phusion
  • configured the project to use TypeScript
  • Started setting up the boardgame.io tic-tac-toe tutorial

By the end of this stage, I had the following files set up:

tsconfig.json ``` { "compilerOptions": { /* Language and Environment / "target": "es6", / Set the JavaScript language version for emitted JavaScript and include compatible library declarations. / "lib": ["es2015"], / Specify a set of bundled library declaration files that describe the target runtime environment. */

/* Modules */
"module": "commonjs",                                /* Specify what module code is generated. */
"rootDir": "src",                                    /* Specify the root folder within your source files. */
"moduleResolution": "node",                          /* Specify how TypeScript looks up a file from a given module specifier. */

/* Emit */
"outDir": "build",                                   /* Specify an output folder for all emitted files. */

/* Interop Constraints */
"esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"forceConsistentCasingInFileNames": false,           /* Ensure that casing is correct in imports. */

/* Type Checking */
"strict": true,                                      /* Enable all strict type-checking options. */

/* Completeness */
 "skipLibCheck": true                                /* Skip type checking all .d.ts files. */

} } ```

tslint.json


{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {
        "no-console": false
    },
    "rulesDirectory": []
}

One note about the boardgame.io tutorials: they'll mention using parcel-bundler, but it's deprecated for parcel.

Next on the list was to set up React.

  • npm install react (which gave me v17.0.2)
  • npm i --save-dev @types/react
  • add "jsx": "react" to compilerOptions in tsconfig.json
  • npm install react-dom
  • add dom to lib in compilerOptions
  • add "include": ["./src/*"] as a peer to compilerOptions (note: src/* is not the same and will result in failure to find modules)

This gets us halfway to a working solution, but to serve from DreamHost, we'll need to have a JavaScript program that Node.JS can run and a program (likely one JS file) that can be vended to the client. Configuring that is the next step.

Monday, November 29, 2021

Making a turn-based game: setting up DreamHost for NodeJS

Recently, I've taken an idea I started prototyping on boardgamearena and moved it to my personal server. it's been an exciting project so far, and I'm getting close to showing it off widely.

I'm going to do a few posts talking through the process of getting the game working. This one will go into getting DreamHost to run a Node server.

Using boardgame.io with DreamHost

DreamHost uses a shared Phusion Passenger instance to host web apps, which is compatible with NodeJS apps. Details of configuration are helpfully provided in their documentation and an associated example. To start, I set up a new subdomain and made sure to enable Passenger on it.

DreamHost expects the NodeJS app you want to serve to exist as an app.js file at the root of the domain directory. My first attempt was to set up one of the example scripts, but it immediately crashed. Hand-running on port :8888 by executing node app.js in an SSH terminal succeeded. I followed some of the troubleshooting advice (chmod 755 app.js, then mkdir tmp; touch tmp/restart.txt to force Passenger to restart its instance of my server). No luck, so I modified the .htaccess file at the domain root to offer some more details:

PassengerNodejs /home/myusername/.nvm/versions/node/v12.22.7/bin/node PassengerFriendlyErrorPages on

Adding PassengerFriendlyErrorPages helped here; turns out I was seeing

*** ERROR ***: Cannot execute /usr/bin/node: No such file or directory (2)]

At this point, I got a little stuck and filed a support ticket to DreamHost. It didn't take them more than an hour to respond, and the solution was straightforward: turns out I was the first person trying to use Node on this shared server, and the Passenger Phusion server itself needed a restart. Heh.

After the restart, I got a successful Hello World message. next step was to begin setting up my boardgame.io project on my local machine.

We'll go into that next week.

Monday, November 22, 2021

When not to use classes in JavaScript

... all the time.

Do you want to be more specific?

Okay, yes, I probably should.

JavaScript is secretly a very simple language with some ill-thought-out features layered on top of it. The most famous one is probably the class architecture, which started as "duck-typed" (objects can have arbitrary fields assigned to them), prototype-based (an object is only a specific kind of object because it happens to have a previously-very-visible-but-now-hidden-the-visible-field-is-deprecated-forget-I-mentioned-it-__proto__-what-is-that-even "Prototype" property that adds fields beyond the ones living directly in the object), and mutable (an object can become a different type of object by calling Object.setPrototypeOf(instance, newPrototype), which on modern browsers will stab your performance directly in the jibblies but will also change an object to another type of object). Instances of classes are very convenient—Who doesn't love getting an object and just calling myObject.doSomeStuff(args)?—and they can really help you organize your code.

Here's why you should use them less.

They require special serialization and deserialization

Since you're using JavaScript, you're probably in a web browser or talking to a web browser (note: if you are using JavaScript and not in those circumstances, I'm sending you a digital hug and I highly encourage you to explore the wide, beautiful world of programming languages that aren't a hacked-together shambling mass built atop a Netscape demo from 1995 with name and syntax specifically chosen to capitalize on the popularity of a totally-unrelated language to the Scheme that birthed it). This means that sooner or later, you're shipping data over the network to or from the server. If your data can be represented as 'POJOs' (plain ol' JavaScript objects), the entire process of converting the data to or from network format is a single pair of library calls in modern JavaScript runtimes, JSON.stringify and JSON.parse. You'll likely still need to validate the parsed data is the right format, since the Web is a hellworld full of active attackers (such as your own server code compiled to the wrong version), but you're 90% of the way there.

But if your objects are class instances? Oh dear. I'm sorry you did that to yourself. Don't forget to define a toJSON method to explicitly select what fields you want serialized, and keep it up-to-date as the class changes. If you don't, you'll get the object's "enumerable properties" (what those are is left as an exercise for the reader). And on the deserialization side, don't forget to specify a reviver function that takes pieces of your parsed JSON, pattern-matches them against the original instances, and uses the class constructor to change the object to an instance. Be careful: reviver is looking at sub-trees and you're at risk if some sub-trees with the same properties should be different classes; I recommend synchronizing the toJSON for those classes to add a 'tag' field that can be inspected to pop the POJO back into an object instance. And don't forget to synchronize all that serialization and deserialization logic with the server's representation of the data, or uh-oh!

... or you could do none of that, and just have the in-memory object representation track more closely to the on-wire model by not using classes.

They add overhead

Every instance of an object is carrying around some indication of what its prototype is. That's not a lot of data on a large object, but on a small object (such as an RGB "Color" specifier or a three-number 3D coordinate), the "what kind of thing am I" field is over 10% of the data stored in the object. And since JavaScript is so dynamic, there are very few optimizations the runtime can practically do to strip out that data. Unless your code frequently has to pass colors or 3D coordinates in the same information channel and doesn't know which (which we might consider a code smell), all those extra "Hey I'm a color here are my methods" tags are wasted space.

They're a pain in the ass to unit test

What you want to do in unit tests is confirm your functions manipulate state correctly. Because class use encourages state hiding, it makes it trickier to write unit tests... Do you want to test your private or protected methods? "No," says the purist, "you should test your public API." Okay, but the public API is relying internally on some couple dozen private functions, so now I'm writing big, jangly dependency-heavy tests to get around the fact that I can't just call myInstance.privateMethod directly and test its output. And if I'm using a mocking library, I'm now mocking stuff up in MyClass.prototype, and sometimes I'm working around private methods by adding a makeTestable public method of some kind... It's all a bit of a mess. JavaScript's class model has no escape hatches for letting test logic sneak a peek into the permission space of a class, and it's frustrating.

Inheritance and instanceof are traps

One reason people sometimes turn to classes is inheritance. The "Shape" that could be a "Circle" or "Rectangle" is the most classic example. Tricky thing is, it's also one of the more corner-case examples (are you doing GUI programming?); many inheritance hierarchies aren't nearly so clean, and JavaScript handles the classic "diamond of death" problem (where one class inherits from two other classes that inherit from the same base class) by... Not allowing multiple inheritance. Inheritance also somewhat clashes with the mutable-class "feature" of the language... You can change the type of an object by editing its prototype, but you can't edit the prototype chain to, for example, swap one instance's grandparent with some other class (if you try to do so by editing prototype relationships, congratulations... You've now mutated every class in your runtime).

When you have to dynamically determine if an unknown object is an instance of some class, you can use the built-in instanceof operator. This walks the prototype chain for the object to see if the specified class shows up anywhere as a parent. This works great until you get into anything complicated involving libraries and modules. Suddenly, you discover that your ThreeDCoordinate isn't a ThreeDCoordinate because it was built with ThreeDLib version 2.7, but you're using npm and the code you're running right now is in a library you added which is depending on version 2.9 of ThreeDLib, and no, the 2.9 and 2.7 ThreeDCoordinate classes aren't the same class even though they are 100% the same code.

So what should we do instead?

JavaScript is actually perfectly capable of supporting a functional pattern riding atop POJO data. In this model, we rarely use classes; we just build objects by calling functions that instantiate them and manipulate those objects as "bags of data referenced by field." In fact, the language's "duck-typed" nature makes this simpler than in other languages: we don't have to be overly-cautious about type. I don't need my inputs to getDistance(coord1, coord2) to really be ThreeDCoordinate objects; if they have x,y, and z fields that are numbers, I can act on them.

With POJOs manipulated by functions, I don't need special handling for serialization, my objects are much smaller (and I can't modify a prototype chain so I can't incur the expense of doing a very slow operation in a modern browser), and I can get inheritance by either extending objects (taking one object and adding fields to it... Not great, because this also incurs browser overhead) or composing objects (making a new object that has a field containing the "parent" object).

There are some possible downsides to this approach. One is the lack of enforced discipline in only having some methods available on some objects means you'll have to be more careful with your code to keep your inputs straight (it's easier to pass the wrong object to the wrong handler function if you're not referencing the methods via myObject.method()). Another is functions divorced from the data they care about can tend to end up wordy; it's no longer myCoordinate.translateX(value), it's ThreeDCoordTranslateX(coord, value). The latter is actually a namespace problem, not a class problem; modern JavaScript offers some good tooling around namespacing functions (either via modules or the "poor man's module," a class full of static functions (one weird tip; Google hates it)).

One additional downside is the lack of private members. To be honest, while I find these conceptually useful I don't find I need the language itself enforcing discipline around them these days. My experience is that the question "how private is private" is wuzzier than I want it to be. The object model enforces it as "data only visible inside the methods of the class," and I find myself needing to "jail-break" that abstraction (for testing or "friend-class" reasons) too often. For functions, I can get privacy by scoping them to the module level. For data, if the API is sufficiently complex that private data matters, I put creation and maintenance of the object behind constructor and mutator functions and only change the data through those functions.

A TypeScript plug

It's definitely worth noting that I don't program much in plain JavaScript these days. The language enables a lot of shoot-your-own-leg-off opportunities regarding its near-total lack of static type enforcement.

To implement the approach I'm describing here, what I really do these days is build my types of objects as interfaces and use interface inheritance to indicate when one object can be treated as a subset of another object. I construct objects in functions declared to return a particular interface-conforming object and write functions that take in a particular interface-conforming object. The compiler will do the work at compile time to let me know if I'm trying to pass the wrong type to the wrong function. In the relatively rare cases that I'm handling multiple types of object on the same channel, I can use tag fields (or the structure of the data) and type guards to turn mystery-typed objects into an understood type.

The zeroth rule is there are no rules

I've been hard on classes, but this walk has been a bit tongue-and-cheek. In reality, classes are an integral part of JavaScript, they aren't going anywhere, and it's okay to use them. I wanted to get us out of the mindset that they're the only way to solve problems, so I've asked you to imagine a world where we should never be using them.

I actually use them often, but I have some specific rules of thumb on when to use and when to avoid them:

1. If you have a big type family and inheritance is cheaper than special-casing

If you're dealing with a family of a dozen or more related types, where most of them share implementation but a few do have special handling needs (i.e. the traditional "shapes are circles and rectangles" problem), you may very well be better off using classes than an elaborate family of handler functions and special-case logic for switching on particular instances of the type family. In my experience, big bags of things mapping to tangible objects will fit this description.

2. Don't use them to describe data on the wire

It's hardly ever worth it to do the heavyweight serialization / deserialization of mapping data on the wire to class instances. If your data is going on the wire, keep it simple. Note that point 1 and point 2 come into conflict sometimes. There is no universal answer here; you'll be making tradeoffs one way or the other if you class-up your big type family that also serializes onto the wire. At least if you go that road, you can make implement toJSON on every class and a reviver that understands the whole class family part of the process.

3. Don't use instanceof

At this point in my career, I consider instanceof harmful; it actively conflicts with the ability to use different versions of a library in the same codebase, and fails in silent and confusing ways. It also bakes knowledge of the class hierarchy into possibly-unrelated code. Try not to do it.

Monday, November 15, 2021

Setting up Xmonad on a Lenovo ThinkPad X1 Carbon: Bluetooth and Screenshots

Configuring Bluetooth

Controlling Bluetooth is relatively straightforward; we just need another systray icon. A little Googling suggested the easiest tool to use for this purpose would be blueman, so I got that set up.

sudo apt-get install blueman

Then, I added to the ~/.xsessionrc file:

blueman-applet &

This put a Bluetooth icon in my system tray, which has relatively straightforward configuration options.

Systray, showing the Bluetooth icon

The Bluetooth systray icon dropdown


Connecting headphones works fine.


Screenshots

Screenshotting could probably be made simpler, but I've decided to just use ImageMagick.

sudo apt-get install imagemagick

Once installed, the import command captures from the screen. To select the area I'm capturing:

import filename.png

To capture the whole screen:

import -window root filename.png

I can even delay a few seconds before capturing:

sleep 3; import -window root filename.png

I could probably find a GUI tool for this, but honestly, it doesn't get much simpler than this ImageMagick solution.

Monday, November 8, 2021

Setting up Xmonad on a Lenovo ThinkPad X1 Carbon: Monitor configuration

I know I shouldn't be surprised that monitor configuration is deeply entwined with a window manager, but switching out to Xmonad caused montor config to fly right out the window. It took a bit of work, but I was able to get something configured that isn't too painful. This is not ideal, and I think I will revisit this topic in the future when I have more time. But what I have for now works.

The challenge

I regularly switch between two monitor configurations on this laptop: standalone and connected to a large monitor on my desk. By default, Xmonad appears to do exactly nothing in response to connecting a monitor. To address this, I installed arandr, which provides a GUI for writing configuration for the X11 "RandR" plugin. RandR stands for "Resize and Rotate," and is an extension on the X11 protocol which allows you to move the monitors around.

The UI for the arandr configuration tool, showing two rectangles representing the two monitors plugged in (HDMI-1 and eDP-1).
The arandr configuration UI: spartan and functional

The arandr tool will allow you to save configuration scripts which can be fed to the xrandr command-line tool to tell X11 to configure the screen. I saved those scripts to a .screenlayout directory. By manually running the scripts from the command line, I can set my current monitor configuration.

Automating monitor configuration

Now that I have some configurations set up, I just need to get to them automatically.

There is an autorandr tool, by Stefan Tomanek, which will detect configuration change and intelligently select a layout based on what is plugged in. After installing it (sudo apt-get install autorandr), I simply had to train it with my configurations. To start, with no monitors plugged in,

autorandr --save mobile

Then, I plugged in the second monitor and ran the script I'd built previously

.screenlayout/xmonad-two-screens.sh

That configured my monitors, and the remaining step was just to save that config

autorandr --save home-docked

Running autorandr with no options, I see my configs are set:

home-docked (detected) (current)
mobile

And for good measure, I declared to autorandr which of these configs it should default to if it has no idea how to talk to a monitor:

autorandr --default mobile

Now, I'm all configured for one or two monitors. Good stuff! Next on the agenda is Bluetooth and taking screenshots.

Monday, November 1, 2021

Setting up Xmonad on a Lenovo ThinkPad X1 Carbon: Power Management and Sound (and Dead Ends)

This is my second post on the topic of setting up Xmonad on a Lenovo ThinkPad X1 Carbon. Previous post is here. This time around, we'll talk power management and screen geometry.

Power Management

Power management is handled by the xfce4-power-manager applet. I launch the applet in the .xsessionrc file, but when I initially launched it, it was invisible. It turns out, there were two issues:

  1. By default, the tray icon doesn't show up. I had to enable it in the xfce-power-manager-settings app, which can be run from the command line.
  2. The default icon in my desktop was black and I had it on a black background. That was easy enough to change out by adjusting the GNOME theme. lxappearance is the tool I used to do this; you can install it with apt-get and it then just runs on the command line. It allows me to tweak various configs, including the theming for icons; "Ubuntu-Mono-Dark" gave me a fine visible icon.


My icon bar, showing two Chrome icons, wifi, power, and sound control
Not fancy, but it works


Once the power manager is in the tray, I can right-click on it and select "Power manager settings..." to configure the manager options. In particular, I make sure "When laptop lid is closed" is set to "Suspend" for both battery and plugged in modes. Note: this didn't work on the first try, and needed a restart to make it work. I can also use systemctl suspend to pause the machine, if needed.

Sound

Sound is notoriously complicated in Linux ecosystems. I tried a couple different options here and didn't find anything I'm completely thrilled with, but I did find some things I hated least. I ended up installing pasystray, which operates at the PulseAudio level. As a result, it's a bit options-heavy; PulseAudio lets you somewhat arbitrarily pipe audio sources to audio sinks, so I have the capability to, for example, send Zoom to my headphones while Chrome goes to my PC speaker (I don't want that, but I have it). My streams are also uhelpfully named "sof-hda-dsp HDMI1/DP1" etc. instead of, I don't know. "Computer speakers" or "Monitor output" or, uh, something sane. But it does work, and I can probably figure out (eventually) how to configure the human-readable device names.

Sound menu, showing way too many options for where to route audio to, none of which are named in a human-readable fashion
"Sound is a solved problem in Linux"


Volume can be increased and decreased (on the default sink) via two-finger scrolling on the speaker icon (on the Carbon X1, Mod-volume-up and Mod-volume-down also work, since I configured them in Xmonad's keybindings themselves).

Final thoughts

I give this part of the configuration three out of five stars. Making the audio layer do anything that isn't space-alien is a real challenge when I step off the beaten path of Canonical's pre-configured tooling. But it is working, and I'm not unhappy with that.

Epilogue: Dead ends

Later update:I should mention some dead-ends I encountered while trying to configure everything. As always, one of the challenges of free software is there are so many options to choose from, and many aren't intended to work together.

Trying to install xmonad-extras from cabal

Directions here led me to try and configure volume control by installing xmonad-extras from cabal, the Haskell package manager. This was a detour; I later discovered the package was available in apt, but was named ligbhc-xmonad-extras-dev. Cabal isn't integrated with the apt packages in several ways (for one, its version of xmonad-extras installs in a different location that the apt install of Xmonad won't find them in; for another, xmonad-extras depends on a pile of C-written libraries that cabal can't install because it's not a universal package manager, it's only intended for Haskell code). Not worth the trouble

fdpowermon

For a brief period, I thought the power manager for xfce4-power-manager was buggy and didn't show an icon. The icon was there, but it was showing as black-on-black because of my icon theme. To work around this, I installed fdpowermon. fdpowermon has a couple of annoying issues: for one, it doesn't have a click-to-configure feature. But perhaps more annoying, the icon disappears when the laptop is plugged in and fully charged. This is a preference by the plugin's author; it can be overridden, but that's the default, to hide the whole thing! More trouble than it's worth; I uninstalled it.

volumeicon-alsa

Before settling on pasystray, I gave volumeicon-alsa a quick tour. This worked okay, but it didn't seem to want to let me switch my audio output devices. Also, I'm still a bit unclear on whether I want to be operating at the PulseAudio or ALSA layer of the audio stack. Rather than debug why internal laptop speakers, HDMI-accessible audio, and headphones weren't all showing up in volumeicon-alsa, I gave up and switched to pasystray... Even though the control it gives is overkill, it gives enough control that I can do what I want.

Saturday, October 23, 2021

Setting up Xmonad on a Lenovo ThinkPad X1 Carbon: Getting Started

From time to time, I have profoundly bad ideas. One of those ideas is to change out the window manager on a Linux PC.

For many tasks (such as maintaining this blog), I keep a Lenovo ThinkPad X1 Carbon running Linux. It’s been a good little workhorse, running Ubuntu 20.04. I decided to switch my window manager to Xmonad, because I enjoy fast manipulation of my windows without needing to tell the window manager where they should live.

In principle, this should be very straightforward. In practice, it turns out that switching out your window manager causes a wide variety of seemingly-unrelated things to break. As best I understand it, this is because several basic “housekeeping” tasks of a desktop Linux environment actually route through the window manager, and when the WM goes away, the configurations that launch those tools goes with it. Here’s a short list of things that no longer “just work” when you switch off (I keep discovering more as I go):

The rest of this post will go into how to get a basic Xmonad setup done. Over the next while, I’ll fill in more information on how to get the remaining functionality back without Canonical’s default window manager in place.

First steps

I drew heavily from a beginner’s guide to Xmonad, which generally steered me in the right direction. I was able to install the relevant components with apt-get. This got me a configuration running with Xmonad, Xmobar (to display content at the top), and Stalonetray (an icon tray, which is necessary for some X11 apps to have a place to show system status).

After finishing this config, some things worked but some took some banging on. In particular,

  • Xmonad has been updated since this guide was released, and the way “docks” attach has changed. My xmonad.sh factors this change in.
  • xfce4-power-manager icon is black-on-black, so I thought it was missing. It was simply invisible in the default all-black color scheme of the top-bar. Switching to a dull grey helped the missing icons appear. Once I knew what the issue was, I was able to install lxappearance and run it to change the icon theme configuration; switching to “Ubuntu-Mono-Dark” provided an icon that was visible.
  • To attempt to integrate with the sound control buttons on the ThinkPad, I pulled in “xmonad-extras,” which integrates against “Third party extensions for xmonad with wacky dependencies” (I’m leaving it as an exercise for the reader whether ‘controlling volume via keyboard’ should be considered “wacky,” but you do you). This proved more challenging than I expected because apt-get actually knows this package as libghc-xmonad-extras-dev, but once I found it it worked fine (though I will probably extract it for reasons I’ll get into when I drill open audio).

Put it all together and the end result so far isn’t terrible!

Screenshot of my desktop with xmonad running: status bar at top, three windows open
I don't expect it to win any awards, but it's pretty functional


The Configs

Below are the relevant config files I have so far.

Monday, October 18, 2021

Moving big chunks of text: how I use emacs

(This post is a followup to a previous post on the topic of using emacs for blobs of text) 

As a programmer, sometimes your job is to move blobs of text.

This is unavoidable for many reasons: while we strive for approaches that maximize brevity and we do our best to avoid repeating ourselves, there are tradeoffs. .Configuration files, in particular, tend to be "flat;" Logic that isn't repeated can be harder to comprehend (seeking information in two or more files to understand a concept) and if there is a mechanism for expanding a meta-configuration language into a "flat" configuration, someone has to build and maintain that mechanism.

As an example of a task I have to do often, let's assume I have the configuration for a dashboard program. The dashboard has multiple configurations in which it runs (monitoring several environments, perhaps).

A list of directories displayed in the emacs *dired* mode

Each directory might look a little different, but they all have a 'config' subdirectory.

An environment subdirectory in emacs *dired* mode

The config subdir has several files, one of which is an environment config:

A config subdirectory ein emacs *dired* mode, showing the env.conf file

Finally, env might have several key-value pairs, which vary from file to file.

The contents of an env.conf file

Let's say that I need to update where we get our data from, and for all of these environments, I now need to pull from the copernicus datasource instead of the tycho datasource. I could go through, one by one, and make the edit, but that takes time (and repeatedly re-typing the same small set of keystrokes is error-prone). When I have to manage nearly the same configuration for multiple parallel processes, I infrequently find myself needing to copy the same config change across many files. When I need to do that, I turn to emacs keyboard macros.

Because emacs is built as a text editor running in a virtual LISP machine (as discussed in my previous post), it is capable of recording and replaying every input. We can start a macro at any time with Ctrl-X, then (, (for brevity, I'm using emacs abbreviations from here forward, where the previous macro is C-x (). C-x e can then be used to replay a macro. At the end of macro replay, emacs is in a special "mini-mode" where hitting e again will play the macro again, over and over. I like this approach because it lets me break down the task into smaller steps and spot-check the work; doing these kinds of edits as a shell script is some people's preference, but I feel the shell solution is usually a bit like hammering a few nails by renting a steamroller to drive over them: quite a bit of setup, and if you mess up, you really mess up.

So here's how I approach this task:

  • Navigate to the top-level directory
  • Do the task by hand one time to check for sharp edges
  • Before doing it the second time, Ctrl-X ( to start a macro
  • Now, record the macro. As we go, we're going to be sensitive to how individual configs could vary and lean towards using commands, not the arrow keys to navigate. Arrow navigation will fail us if a subdirectory has too many files or the env.conf file has too many parameters.
    • Enter to descend to the dashboard subdir (dev, in my case)
    • Ctrl-X [, move cursor to beginning of buffer
    • Ctrl-s, interactive search forward. Type config and hit enter. Tricky bit: I have to be careful typing, if the search comes up empty here, my macro will "Bell." More on that in a bit, but if I "bell," I usually just C-x ) to stop editing the macro, finish this one out by hand, and start a macro on the next one.
    • Enter to resolve search, then enter again to descend directory
    • Same plan: beginning of buffer, C-s, env.conf, enter-enterNow that we're in the config file, C-x [ to beginning of buffer, then C-S and search for DATASOURCE=. Enter to confirm search, which moves the edit point to after the equals sign
    • C-k, kill the whole line
    • type copernicus
    • C-x C-s to save the buffer, updating the file
    • C-x k enterC-x k enterC-x k enter to get to the dashboards directory again (closing up all the subdirectory buffers and the env.conf file as we go)
    • (This is key) Hit down arrow one time to move the directory cursor to the next directory
    • C-x ) to close macro


Now that we've done it one time, I can just hit C-x e, then e e e e to update prod, remote, staging, and test.

Checking your work and the zen of emacs: buffers are state machines

So why bother with this instead of a shell script? What I like about this approach is that if something goes wrong, it's much easier to recover than a shell script. If an error occurs while a shell script is running and the script bails, I'm now in a not-great state: running the script again will try to re-edit the files that are already edited, which is rarely what I want. There's no way for the script to know; it has no context on previous runs. But emacs keeps context in the form of the point (cursor)  position in the top-level directory buffer, which doubles as a progress-tracker. This is a valuable piece of the zen of using emacs, which is worth highlighting:

Emacs buffers are stateful. The point doubles as a progres tracker.

This gives emacs a nice tradeoff between fully-manual and fully-automatic edits for repetitive tasks. The command-line is a sword; a shell script is a machine gun nest. Emacs keyboard macros are a semi-automatic weapon: every push of e is a pull of the trigger. If something unexpected happens (i.e. a search fails because the env.conf file or the DATASOURCE row is missing), emacs will take a "bell" event and the macro will interrupt, which allows me to correct the state of the world and then run the macro on the next line instead of starting over.

Using the buffer point as state opens up a couple clever tricks that I find significantly harder to do in shell. Say, for example, that instead of switching everything from tycho to copernicus, I needed to set each file to its own DATASOURCE. In shell, this'd be a little tricky; I'd have to do a lookup file of some kind. With emacs, I just create a new temporary buffer, *datasources*, in which I put a sequence of environment-name / datasource name pairs ("admin: newton","dev: einstein", etc.). Then, I'd change the procedure I described previously as follows:


  • Open the temporary buffer in a second window
  • At the beginning of the macro: before opening the directory, select the directory name and use M-w (metakey-w, usually "alt") to save the directory name
  • At the step where I'd insert copernicus, instead do C-x o to switch to the second buffer
  • C-x [ to go to the beginning of the buffer, then C-s C-y enter to search the buffer for the name of the directory
  • rightarrow to the beginning of the value next to the directory name, then C-space, C-e, M-w to select the value and copy it to the "kill ring" (emacs' concept of a paste buffer)
  • C-x o to go back to the env.conf buffer and do the copernicus replacement, but use C-y to paste ("yank") the value copied from the other buffer

Here's what it looks like.

There are many ways to solve this problem, and different coders will have different favorite approaches. I'll definitely find people who swear by using a shell script for all this. But I think the important thing is to talk about it; something I've noticed in my career is how rarely developers talk about the way they do their craft. If you're a professional developer, I definitely recommend taking some time to look into other people's favorite approaches and find what works best for you.

Friday, October 15, 2021

Moving big chunks of text: why I use emacs

This one ran a little long, so I'm turning it into a two-parter. This part is about emacs and the theory of choosing your tools in general. In the next one, I'll show off a demo of how I use emacs to automate a task that is annoying toil but I don't do quite frequently enough to justify fully automating it.

In a previous post, I mentioned that half my job is moving big blobs of text around, and how important it is to automate that process. I'm going to provide a concrete example using my multi-tool of choice, emacs. Before I continue, of course, I should address the elephant in the room.

Why not vi?

I never really learned it.

That's not very satisfying

Yeah, sorry. Twenty years on from when I first encountered both tools in college, My use of emacs is pretty much just luck. I started learning it first, continued learning it, and now I have accreted a 103MB configuration directory that follows me around from computer to computer. If you tell me vi is great, I'll agree. If you tell me vi is better than emacs at something, I'll believe you.

But if you tell me there's something vi can do that emacs definitely cannot, I'll be modestly surprised and want to know more. Because that's the thing: these tools are more similar than different. And passionate arguments about which of two things that are 90% the same are better is a young person's game. In fact, on topics like this in general, as a word of advice:

For software of the same category, choose one and go deep on it. Know enough about its neighbors to learn more if you need to.

I find I cover more ground when I do this.

So why emacs?

emacs logo


What emacs offers that matters is the following:

Ancient

Emacs was written in 1976. It's older than MS-DOS and only three years younger than the XEROX Parc Alto, the first personal computer to offer a graphical user interface (at an affordable $32,000). Nothing in software is forever, but anything that has stuck around for 25 years in this industry has crocodile DNA. The reason this matters is that community matters for the longevity of software tools; if there's no community, you're building and maintaining it yourself, which is fine if that's your goal but a massive cost otherwise. If you're going to invest heavily in something, it should be something that probably won't fall out of fashion next week.

Degradable

Because of its age (and a community mentality of keeping it maximally compatible), emacs runs on UIs ranging from a full GUI to a decent-quality TTY emulator (i.e. command line). As a result, I rarely find a computing environment I can't bring up emacs on (with just a little configuration). It's comfortable to be able to quickly spin up a familiar development and editing environment on every machine I sit down at, and decreases friction from having to learn something novel.

Network-aware

I don't merely mean "can do TCP/IP" here. Emacs is constructed as a distributable tool; the editor engine and the client can be running in different programs and even on different machines. Furthermore, emacs is aware of SSH and can remote-control the filesystems of other machines, meaning I can edit remote files without having to spin up a shell on them or copy files back and forth. That proves to be a big deal pretty often in my modern ecosystem of remote controlling computers.

Deeply, deeply reconfigurable

This is the important property of the tool, the one that justifies keeping a 100MB config directory around and learning all the esoteric space-alien-language keyboard shortcuts and interface metaphors that predate such exotic topics as "mouse" and "desktop" (to say nothing of "cut", "copy" and "paste"). Emacs stands for Editor MACroS. There's a joke that emacs is really a virtual LISP machine that someone wrote a text editor in.

It's not a joke. It's the law and the whole of the law and it's magic.

Emacs lets you reconfigure basically everything. Every command, every menu option, every human activity is running a command that can be overridden. Even individual keystrokes are just bound to default "insert this character at the current point in the current buffer" functions. That massive extensibility means emacs is something more than a text editor: it's a development environment in which you can develop it. I've seen a full implementation of Tetris running in it. I'm in the process of writing a shell wrapper in it (more on that... Possibly never, I make no promises. ;) ).

The text of the "self-insert-command" help documentation
emacs is built on self-insert fictions


And really, that's the most important thing. In fact, second word of advice:

Seek out the most powerful tools. The most powerful tools can modify themselves.

The tradeoff of doing such a thing is that you eventually end up with an emacs with one user. I don't expect anyone to understand or help me with my emacs configuration; there's 100MB of custom behavior on top of it. Hence, the need to go deep: to maintain a well-customized tool, you have to become an expert in it. if you really drill down on a tool, you may find that in the end, you're the only one who can fully understand what you've done to it.

A screenshot of my emacs editor. On the top is a view of the emacs_config directory I've built. On the bottom is some of the text of this blog post.
This is my editor, this is my GNU. I built it for me and not for you.

What other tools do you use?

In my day-to-day, I have a handful of other things I develop with.

Chrome

Chrome browser split into two panels. On the left is a pair of wheels. On the right is the inspector.
This screenshot is a lie; I'm not debugging anything here. But the wheels are real

This is a bit domain-specific, but: I use Chrome for my web development. Chrome has some overlap with emacs in terms of its extensibility, and the developer tooling is pretty solid. I've even built a (shudder) debuggable build of Chromium. Once. For a very special reason.

... and yes, working at Google is a huge part of this choice. I have no strong opinion on Firefox vs. Chrome, other than the ugly market truth that if you're trying to provide a service to the most people, debugging your website on Chrome and Safari first will cover more people's usage environment faster than Firefox.

VSCode

A VSCode editing screen, showing navigation bar, some text, and some hints

It's weird that I use a second text editor, I know. But I've been pretty impressed with VSCode; for the kind of development I do (mostly TypeScript, JavaScript, and Java), it's a very effective IDE. The formatting, autocompletion, and static checking integration out of the box is simpler to configure than emacs (the one downside of a hyper-flexible editor: many ways to do a thing means many ways to do it wrong). And it shares emacs's comfort with operating in a networked environment, being able to connect to a remote instance of itself over SSH. It's possible that in 25 years, it'd be the kind of thing I'd recommend over emacs if it stands the test of time. I keep emacs launched in a second window for the few tasks VSCode makes harder (it can be very opinionated about auto-formatting files that already have a format set up that is incompatible with the VSCode defaults, and rather than either turning off the formatter or resetting its rules to match the ones in the target file, it's easier to just pop the file open in emacs and let its lack of auto-formatting help me out).

In my next post, I'll demonstrate a task I have to do not-infrequently and how I speed it up a bit with emacs.


Tuesday, October 5, 2021

Things a computer science degree didn't teach me about software engineering

Agent K and James Edwards III, soon to become Agent J, in an elevator. Scene from movie: Men in Black
"Whatever you say, Slick, but I need to tell you something about all your skillz..."

While I'm very happy with / grateful for my computer science degree, I eventually became a software engineer. And the thing about software engineering is that it's related to, but quite distinct from, computer science. Following up on my previous post about that topic, here's the topics and skills I wasn't taught in my computer science undergrad that proved important in my career so far. Along with each one, I'll try and provide the closest thing I remember from undergrad.

Reading other people's code

Almost no professional software engineering is done alone, which means reading other people's code is basically a necessity. And other people's code can be atrocious. While I learned to write well enough to be understood, the skill of fitting my mind around someone else's mental model to understand their code was rarely exercised.

Incidentally, one study has suggested that aptitude to learning other programming languages is related to aptitude in learning natural languages significantly more than aptitude in the mathematics that makes up the core of computer science.

Closest I got: We did some debugging exercises which touched on this space, but the biggest match was becoming a teaching assistant (TA) for one of our courses involving a functional programming language gave me a lot of practical experience in understanding other people's code. I recommend it highly.

Choosing what problems to solve

It turns out, there are infinite problems to be solved with computers, and succeeding in a software engineering career involves a certain "nose" for which ones are the most valuable. Growing this skill can involve both introspection and retrospection, and is generally not a challenge that comes with undergrad, where problems will be handed to you until you complete coursework. Any software engineering career track will eventually expect you, the engineer, to generate the problems that should be solved; those are the engineers that a company needs, because they underwrite the bottom line for the next five years.

Closest I got: I undertook an independent study, but unfortunately it put me off of grad school and PhD pursuit. The gaping maw of the unknown was too much to stare at. But over time, I've been able to learn this skill better on-the-job.

Deciding on a solution, designing it, and defending it

With coursework, there is generally one metric by which your solution is evaluated: passing grade. But engineered software will be maintained by other people and often interlocks with a huge edifice of already-existing technology operating closer to or further away from the end-user. The right way to solve these problems is your responsibility to create, and your design will often require explanation or even defense of its fit vs. alternatives. These aren't software skills; they're people skills and they involve understanding not just the system you're building but the people interested in seeing it built, as well as time and cost tradeoffs.

Closest I got: As a TA, I had an opportunity to design new lessons and put them in front of the professor. This was an excellent opportunity to refine my ability to explain and defend something novel I'd created.

Huge systems

Corporate software accretes features. Once something is built, it is incredibly rare for the company to decide to stop doing whatever the thing is that the software supports.

As a result, becoming a member of a large team at a mature company will often mean struggling with software of absolutely enormous complexity. This isn't useful to do to undergrads; pedagogy is better served by approaching the elephant one piece at a time, and any support systems needed for a class tend to be slim and focused. But big-world software (especially web software) is not designed to be understood by one person; that's why companies hire teams, and it's why reading other people's code becomes so important. A huge system requires some limited expertise in a broad skillset, because eventually an abstraction breaks and, for example, you have to care about the network layer to fix the UI, or the 3D rendering layer to build the schema for a new dataset, or the package management layer to write any code at all. And industry software often has to make practical tradeoffs that more abstract, academic software can skip... Does your system have a time limit in which it can operate, because you're building banking software and your peers turn off on weekends? Is your website breaking because you've hit the limit for max URL size (which is implementation-dependent)? Are you building on a system that lacks a floating-point coprocessor (or, for that matter, a "divide" operator)?

Closest I got: Reading the source code and design notes of big systems. The Linux kernel is open-source. So is Docker. So are multiple implementations of MapReduce. Reading and really trying to understand all parts of one of those elephants can give a person perspective on how wide the problem space can be.

Handling user feedback and debugging user bug reports

Real-world systems have real-world users, and users are the greatest way of finding faults that have ever been devised. Software of sufficient quality to pass academic exercises is often not nearly well-tested enough to be easily maintained or improved in the future, or grow to handle new requirements; it's write-and-forget software. Related to this: academic projects that require code often provide relatively straightforward feedback on problems. User feedback is anything but straightforward (when it isn't outright abusive, because users are emotionally angry that your tool got them 99% of the way to where they wanted to be and then ate their data).

Closest I got: While I actually had a few classes that taught expecting the unexpected, nothing was as educational as building real software that real users use. Whether it's maintaining a system relied upon by other students or building a library used by people on GitHub, getting early experience with the bug-debug loop with strangers in that loop is useful.

Instrumentation

More challenging for a software engineer than vague or angry bug reports is the bug report that never comes, where the user just gets frustrated and stops using your software. Software operating at huge scale can't rely on users to have the time or patience to file reports and must police itself. The practice of instrumenting systems (encompassing both robust error capture and reporting and observation of users' interactions with the tools, what pieces they care about vs. what goes unused, whether they often get halfway through a designed flow and stop, etc.) is as much art as science, but it demands its own attention to design and implementation.

Closest I got: This is a big piece that was basically never touched on in undergrad. Debugging exercises were close in that they could involve temporary instrumentation to understand code flow, but this is a skill that I think may be under-served (unless pedagogy has changed in fifteen-odd years).

Automating toil and transforming big blobs of text

While code isn't text, much code is represented as text and mechanically, a software engineer spends a lot of time moving text around. A significant difference between a stagnant software engineer and a growing one is whether they get better at that process. That can involve both practice and recognizing when a particular task is a "hotspot" that is repetitive drudgery enough that it could itself be automated. There's no rule against writing code to write code; half of software is probably exactly that. I like to rely on tools that make this kind of growth easier, and I'll touch on that in a later blog post.

Closest I got: Computer science exercises tend to try and avoid being the sort of thing that can be solved with moving big blobs of text, trying to instead focus on problems solvable with the right (relatively) few lines in the right place. One exception is an operating system design course I took, and that one gave the most incentive to learn how to quickly replace phrases globally, or move an entire data structure from one form to another (when we the developers didn't expect that change so it wasn't cleanly structured to do it).

Growing your tools

Related to the previous topic: the industry will change over time, and the tools available to you will change with it. Like any good practicing laborer in a technical field, be it medicine, agriculture, or manufacturing, learning of new tools and improving the ones you already have is a continual exercise. The ride doesn't end until you get off.

Closest I got: Everything and nothing. Improving and seeking to improve one's tools has to become like breathing air to a software engineer; this is the "piano, not physics" part of the practice of programming itself. Better IDEs, better languages, best practices, new frameworks, new ideas... Seek them out, and may they stay exciting to you your entire life.

Tuesday, September 28, 2021

Things I wish I'd Known Before Getting A Computer Science Degree

 Hi, my name is Mark, and I struggled with impostor syndrome for decades.

("Hi, Mark").

In my case at least, impostor syndrome came from the discontinuity between being good at things in high school and far more mediocre in college. I came from a place where I thought I had things figured out to a far more challenging environment in a different city, with peers who knew way more than I did about a subject I thought I was passionate about. For awhile, I seriously considered changing disciplines.

These feelings don't really chase me anymore, but I've thought for some time about why they came up and what I wish I'd known—in the high-level sense, not the day-to-day sense—about the path I'd started out on.

So, dear Mark in 2001, here's some things Mark in 2021 gets to know.

Computer science is more math than engineering

You're into computers because of what they can do. That's cool. Computer science is about why they do it. And the why brushes up against the limits of what they can do. You've never thought too much about that aspect of things. You'll be thinking about it a lot these next four years. Some of this will be hard, because you have a mind that thrives on learning things you can apply right now, not on abstraction and theory. 

You'll adapt. 

It'll hurt like hell. 

One thing that helps adaptation: tie the whys to the whats. Learn the history of how we came to know these things (oftentimes, we learned them because someone was chasing a what, got stuck, and had to chase a why to understand the reasons "what" wasn't working). Computer science as a pedagogy isn't interested in its history, so it won't hand those pieces to you... But you are. Seek them out.

Software engineering isn't computer science


You're gonna be real discouraged in freshman year because a full half of your classes are deep math you haven't seen before. That's okay. It gets good junior and senior year when you start applying the theory on larger projects. But understand: that theory is computer science. Your peers that go on to grad school or to pursue a PhD, they aren't likely to be designing the next generation of developer tooling—they might, but they can also make a career out of never publishing any mainstream-known software. This discipline is about understanding the pure-math space of computation as much as the machines, "meat and bones" of how to make a computer go.

Computer science isn't software engineering


On the flip side, there are things you will absolutely need to know to be a successful writer of software people use and maintain that a computer science undergraduate degree doesn't need to teach you. I've read some brilliant code by some very smart researchers that is incredibly bad for communicating the concepts behind the code to other people—single-letter variable names, zero comments, no encapsulation, and so forth. It's not necessary to do those things to publish novel research, so those things aren't done. I'll write another article on this topic in the future.

Computer science relies on mathematics not always taught in high school

Palpatine talking to Anakin Skywalker from Star Wars. "Have you heard the tale of the Travelling Salesman? It is not a story the calculus teachers would tell you."
Professor Palpatine introducing a freshman to discrete mathematics

Computer science is the science of algorithmic solution of problems: what problems can be solved by algorithm, how hard it is, and what algorithms are the same thing in disguise. Of the mathematics an average US high school student will know, the least likely in this set is discrete mathematics. Discrete has few practical applications relative to algebra or calculus, so it often falls by the wayside. But it's fundamental to computer science, because it deals with the mathematics of processes that progress in orderly, distinct steps.

You don't really know the term "discrete math" right now. You'll be comfortable enough with it to make jokes about it soon.

If there were a way to bootstrap yourself into this before getting to college—summer class, elective in high school—I can't recommend it enough. They'll teach it to you in college, but if you get there knowing it, it'll be one fewer thing to worry about while you're trying to stuff everything else into your brain.

The industry is young, and pedagogy varies wildly


Your father made a career out of writing software on the back of an English undergraduate degree. As a disciplined field, computer science is young; pedagogy varies wildly between institutions that offer computer science degrees. You'll take an internship in Philadelphia where you'll meet several students from Drexel; they'll run circles around you in terms of prolific code authorship, because they've been using the tools the company uses since freshman year. But Carnegie Mellon intends to graduate potential grad students and PhD candidates from their undergrad program; they start computer science on fundamentals of theory and mathematics, and most code can be written in a plaintext editor. Drexel, to my observation, biases towards getting students productive earlier; they have classes intent on teaching more intricate and esoteric parts of tools like Visual Studio (whereas CMU's opinion on such things is that Visual Studio didn't exist 25 years ago and may not be in use 25 years from now). So you'll be slogging through "How to set up a project" while they're scripting the debugger. Tuck in.

The field is moving so fast, it's impossible to predict what will be relevant

When you start, you'll meet some grad students who are very excited about computer simulation of electronic circuits. The idea they're chasing is that instead of building a circuit by exhaustively laying out logic gates on a chip, you instead use a high-level language to describe what the chip has to do, and an algorithm could "grow" a chip's physical design by heuristically selecting logic gate configurations to satisfy the requested input. This process will become standard in huge swaths of hardware design for new custom circuit fabrication. You'll also watch sloppy prototypes of voice recognition and image recognition explode into bog-standard technologies that come on every smartphone as a pack-in deal.

Faced with this rapid pace, the only option is to tuck in and prepare to be learning something new continuously, your whole career, forever. As one of your mentors freshman year will say, "Programming is piano; it's not physics." This is a field where what is possible is expanding at a rate we can barely keep up with. Enjoy the ride; don't get lazy.

Set aside time to get good at the tools

Corollary to the previous: a lot of classes will focus on teaching theory and rely on the students to learn the mechanics of their tools on their own. If it can be read out of a manual, it's not something the college is focused on expositing (the manual is, after all, right there).

Set aside time to get good at using your tools of choice and exploring their nooks and crannies. The biggest favor you'll do yourself in your undergrad is taking the time one summer to learn the esoteric language used in the class you'll be taking next semester. It will still be a challenge, but you won't be struggling over syntax and theory at the same time.

Put thought into where you want to be in four years

College can give you tools to succeed but won't tell you who to be. It can be easy to get so heads-down on the tasks and classes that you wake up one day and realize you don't have a life path beyond "student." That can be your life path, but if you want that, seek out grad students and PhD candidates and talk to them. You might find what they do with their time is what you want to do; you might not! Research and deep theory isn't for everyone.

If, instead, you choose industry, you will never be freer in your life to choose which one you pursue than graduation year. You will have an incredibly valuable talent to share with the world; it's up to you who benefits from that. Make sure it's someone who deserves it.


Whatever you're interested in doing, computer science will get you closer to it

Software is eating the world. There are very few things humans do now—in industry, in recreation, in travel, in finance—that don't (or can't) involve a computer somehow. And the addition of computers opens entirely new ways of doing things that weren't even considerable previously. The skills you will acquire to understand the fundamentals underpinning all of these new approaches and technologies will be invaluable navigating the world that is evolving in front of us.

This is going to be a hell of a good time. Enjoy it.