Sinclair Schuller July 21, 2022
When someone talks about “magic” code, the connotation is generally not a positive one. This sort of code can be categorized as “implicit,” meaning that it will do something on the behalf of the developer using it without requiring “explicit” instruction or invocation by that developer. This magical, implicit code is usually less testable. The code hides details behind a veneer thereby reducing detailed control over execution, and could, in some cases, break a basic expectation that a function will produce the same result given the same arguments.
The obvious conclusion is that implicit is bad, right? As with everything else, the answer is not so black and white. The use case, the “customer” for that piece of code, and the aggregate cost of using that code all play into determining whether there is value in the implicit. When all three of these things are true, you have a strong case for implicit code being good:
The Use Case is that the code is part of a platform/systems layer instead of general app logic. This means that the code is itself, a product (e.g. library)
The Customer of that code is a developer consuming that code as a product, and that this customer may be at a lower skill level than the developer of the code in question, or may not care about controlling the details of implementing that part of the stack
The Cost of consuming and maintaining the code goes up or down based on how the code is delivered (implicit/explicit)
The best example of code that fits this profile is nearly any systems code, ranging from the OS libraries and the runtimes we depend on, up to the frameworks we use. It’s why most developers prefer the “new” operator in managed runtimes over explicit allocations like “(char *) malloc(10 * sizeof(char))”, pointer tracking, and having to use “free/realloc”. The “new” operator does a lot of implicit work and we love it. It doesn’t require that you pass in dependencies or anything at all. It just works.
In a more contemporary upstack example, annotations in our code are implicit. They let us turn a simple class into an HTTP-bound API Controller or let us flag a function on that class as a function that should adhere to some sort of authorization scheme. Each of these examples is implicit and indistinguishable from “code magic.” For instance, authentication and authorization are clear cases of invocable code whose behavior changes based not on the arguments passed, but on the ambient context of static-like data, like the identity principal or similar data being present as session variables. Are you a fan of the benefits of “convention over configuration?” If so, there is a chance you rely on (and love) implicitness.
It turns out that implicit is OK sometimes. In fact, we expect it from our platforms and frameworks. Developing a platform, by definition, is to support other developers and their quest to build apps using that platform. Hundreds, thousands, or even millions of developers may use functionality provided by a platform. How productive that platform makes them is directly related to how much work they need to do to extract the functional value they want from the platform. The aggregate cost in a given library is defined by the cost incurred by each developer using that platform library, and not just the cost incurred by the developer building the library.
When worrying about internal code that you need to maintain, the position that implicit code is bad is probably correct. It is less testable, doesn’t display clear execution characteristics, etc. making maintainability and maintenance hygiene difficult. There is very little value in implicitness given the likely usage pattern of that code. But if the code you’re writing is a library or framework that other developers will use in their app, explicitness may be passing the cost along to all dependent developers. It means each and every one of those developers needs to explicitly weave (potentially orthogonal) behavior into their logic by calling lots of code, increasing implementation cost, maintenance costs, and complexity.
Well defined, clean abstractions reduce net complexity rather than increase it. Implicit code is ultimately an abstraction, and one that reduces the burden on your customer, even if it means adding “behind the scenes” complexity for you and your own team. At Nuvalence, one of the things we focus on when we’re brought into a project is to think through design questions like this, and help identify if abstractions are appropriate in a given situation, and implement the right ones.
Poor abstractions can be extraordinarily costly in a digital platform: a bad design decision doesn’t just impact the developers of the platform, but all the developers who depend on it. It’s our job as technologists to make sure we’re leaving things better than we found them, and not multiplying misery or toil.