In my previous post, I talked about how using a CSS library cluttered the semantics of what a web component does, and after another week of living in this codebase I realized that there's a conversation to be had about the difference between abstraction and legibility. Because most of the time when we're talking about abstraction? We're actually talking about legibility. For example, I recently read the introduction to an OCaml textbook that said, "You will improve at abstraction, which is the practice of avoiding repetition by factoring out commonality."

That is absolutely not abstraction. That is legibility.

To quote James Koppel, an abstraction is an encapsulation of an idea, such as "sanitized" or "authorized" or "connected to a server," and locking the details behind an abstraction barrier so than not only don't you have to worry about the details, you shouldn't. That's not what my previous example did. Instead, it made the code's purpose and function legible.

So let's talk about legibility with another example.

I am absolutely delighted to get paid to work on Authentik, an open-source SSO (Single Sign-On)solution. My job is to impose some consistency and order on a LitElement-based suite of web apps, now that we've got a little funding they can hire someone like me, someone with opinions, to modernize the application further.

The application is smartly built; there are all sorts of Scripts that can be run during signing in and signing on. Users and administrators can be authenticated, authorized, enrolled, unenrolled, passwords reset, everything you can imagine needs to happen with users trying to access a suite of different applications from Google Mail to Github to Calendly to Jira to whatever. The Scripts have a strong uniform API for identifying and accessing them, although further details for each kind of script are very different, and there is accompanying JavaScript on the client side for executing an LDAP Authorization Script versus a Radius Authentication Script versus a Username PasswordReset Script.

There are 25 locations in the JavaScript codebase where an administrator can say, "To access this application, use this Authentication script" or "To access that application, use this Authentication Script and that Authorization script." Whatever is necessary to associate a specific application your user wants to access with a collection of scripts to access it.

Every single one of those locations has a "search for scripts" function, and every single one of them looks like this copy-pasted blob:

runSearch({
    fetcher: async (query?: string): Promise<Script[]> => {
        const args: ScriptRequest = {
            sortOrder: "name",
            designation: ScriptType.Authentication,
        };
        if (query !== undefined) {
            args.search = query;
        }
        const scripts = await new ScriptApi().scriptList(args);
        return scripts;
    },
    renderer: (script: Script): string => 
        RenderScriptOption(script),
    descriptionRenderer: (script: Script): HTMLTemplate =>
        html`${script.name}`,
    deriveValue: (script: Script | undefined): string | undefined => 
        script?.id,
    defaultValue: (script: Script): boolean => {
        if (this.instance.authorizationScript === script.id) {
            return true;
        }
        else {
            return this.tenant?.authenticationScript == script.id
        }
    },
});

(There is, by the way, a serious bug in this code.)

In this code, we are passing into the runSearch function a series of functions to retrieve the data, render the data, render a description of the data, get the value to send back to the server, and to determine what, if any, the default value should be. This code block is, again, copied and pasted twenty-five times throughout the code base.

I extracted this code and put it into a single place, and wrote a wrapper for it, and now all 25 of those calls look like this:

searchForScript({ 
    scriptType: ScriptType.Authentication,
    tenantId: this.tenant?.authenticationScript,
    instanceId: this.instance.authorizationScript
});

Notice how the bug just leaps out at you: if we're working with the Authentication system we shouldn't be comparing our values to an Authorization token, should we?

Now before anyone leaps up and shouts, "But that's abstraction!" ... Okay, yes, in a very limited sense I have extracted "search for a Script" into a function (it's literally in the name!). But this extraction isn't a conceptual barrier of any kind, it doesn't preserve a quality of the data that we would label using a type system.

Instead, what I've done is figure out what here is essential to the process of searching for a script, and what is just clutter. Because everything that's repetitious is clutter. In a function where you're going to render multiple searches to fill out a form, seeing the renderer, descriptionRenderer, and deriveValue functions, which are all the same, just makes your eyes glaze over. The code is not legible.

When programmers talk about "abstraction" in the context of de-duplicating code, almost always they're talking about that other thing, legibility. We're talking about what makes the code look repetitious and obscure, and removing what obscures the purpose and function of the code.