The Economics of Semantic Coding

The power of text-based editing in coding is that no time is wasted communicating semantics to the editor. Editing is a simple stream of character additions, deletions, and replacements; semantics are not required. This makes efficiency one-to-one with keystrokes. The work of any operation is simply the sum of its keystrokes.

Structured editing offers a different economy. The price of communicating semantics is paid up front, and then a (typically small) number of keystrokes communicates enough to make the semantics manifest.

Expressions involving few characters but complicated semantics typically make these economics unappealing. Consider x+1, an expression with very few characters requiring very few keystrokes in a text-based environment. But the semantics aren’t as simple:

call the + function on the local reference x and the constant value 1

Expressions of this sort do not make a compelling case for structured editing. The cost of communicating semantics seems inordinate compared with the minimal characters necessary to form the expression. In fact, this situation feels viscerally frustrating: “This is the simplest statement I’ll ever code and it feels so cumbersome!”

But it would be incorrect to assume that the economics of structured editing scale proportionally from these sorts of simple expressions. In fact, they move in the opposite direction: as editing moves to more complicated refactoring commands the complexity of the semantics stays constant while the keystrokes and boilerplate become much more tedious.

Imagine you’ve written a simple expression that you later realized should be abstracted into its own function. Perhaps you anticipate this logic being broadly applicable, so your ideal refactoring is to build a new function around the expression and then immediately call it.

For example, you might want to transform something like this:

g(x) ? 0 : h(x, y)

into something like this:

def f(x, y) = { g(x) ? 0 : h(x, y) }
f(x, y)

Despite the numerous keystrokes and tedious boilerplate of this refactoring, the semantics are quite straight-forward:

Make this expression a function called f then refactor expression as call f

It’s commands like these where the upfront cost of communicating intent begins to outweigh the subsequent cost of keystrokes. This is a trade-off commonly leveraged by various editing paradigms.

Autocomplete features can be seen as a step along this economic spectrum. By engaging the autocomplete dialog, you communicate the semantics of your intent (i.e., “I want to reference a defined entity”), and in exchange for this up front cost you spend fewer characters to achieve your edit.

Similarly, modal editing (such as in VIM) offers this same trade-off: use a keystroke to communicate a semantic context and your next keystrokes become more valuable.

Here at the isomorƒ project we see structured editing as a way to push much further along this spectrum of editing economics, and we continue to experiment with this paradigm in our quest to improve the coding experience. Beyond the semantic benefits already reaped by the existing rename, clone, and match commands, we’ve recently rolled out some new semantic commands that demonstrate some of this potential:


Refactoring Expressions as Functions

As discussed above, it is commonplace to write an expression and realize that the logic represented might be useful elsewhere. Creating a function that expresses the logic and refactoring the expression as a call to this new function offers benefits in reuse, maintenance, and adaptability. The refactor as function command transforms any expression into a function (after being supplied the new function’s name):

Here the refactor-as-function command transforms a match expression into a function called “getOrElse” and refactors the match expression into a function application.

Refactoring Expressions as Values

Whether simply for clarity or perhaps to avoid redundant declarations, it is often useful to transfer an expression to a value declaration and then make use of that value (perhaps repeatedly). The refactor as value command refactors any expression (or multiple identical expressions) into a value declaration and then replaces the expression(s) with a reference to the value.

Here the refactor-as-value command takes the two identical expressions “getOrElse(first, b)” and creates a local value “r” that replaces the two expressions.

Refactoring Expressions as Functions Parameters

Another similar pattern for creating value through abstraction is realizing that an expression or reference within a function could be abstracted as an input to the function. The refactor as parameter command takes any expression and adds a parameter to the containing function matching the type of the selected expression. The expression is replaced by the new parameter, and the argument list of any calls to the function is appended with the expression.

Here the refactor-as-parameter command is executed on 0, adding a new function parameter called “else” of type BigDecimal to the enclosing function and padding the relevant application with the 0.

Enumerating Constructor Patterns

Adding a basic match statement in isomorƒ will always enumerate the relevant constructor patterns (when types are known and patterns are enumerable), but this is not performed recursively. To enumerate nested sub-patterns, use the enumerate constructor patterns command and create the desired depth of constructor matching.

Here the enumerate-constructor-patterns command creates new cases for List patterns when called on “head” and then on “tail”.

Branching a Function

Often when applying an existing function, it becomes clear that the behavior of the function isn’t exactly as desired and a slightly altered version will be necessary. Executing the branch command on any function application creates a local copy of the function being called and refactors the application to call the new local version.

Here the branch-function command creates a local copy of the “reverse” function.

De-normalizing a Local Function

As a function evolves the situation can emerge wherein efforts to abstract have gone too far and created indirection where no benefit is conferred. Perhaps a value statement was created with the expectation of multiple usages, but only a single usage materialized. Perhaps a function was envisioned that ended up containing only a trivial transformation not worthy of its own declaration. The refactor-as-expression command takes any local function or value and replaces references to it with the underlying expression, cleanly undoing its declaration.

Here the refactor-as-expression command removes the value “r” and substitutes its body (“0”) for all references to “r”.

We have a live sandbox on isomorf.io where you can play around with these commands. Let us know what you think (and if there are other commands or features you’d like to see). Reach out via email, twitter, or our forum.