Skip to content

Instantly share code, notes, and snippets.

@pcantrell
Last active December 8, 2023 20:35
Show Gist options
  • Save pcantrell/66ce7f0a3186c044173a5dca8fec3049 to your computer and use it in GitHub Desktop.
Save pcantrell/66ce7f0a3186c044173a5dca8fec3049 to your computer and use it in GitHub Desktop.

Turning programmer errors into type errors

An example problem

In Wordy, suppose that BinaryExpressionNode were designed like this, with the operator modeled as a string:

public class BinaryExpressionNode extends ExpressionNode {
    // Valid operator types include "addition", "subtraction", "multiplication",
    // "division", and "exponentiation". Other strings are not valid operators.
    private final String operator;

    private final ExpressionNode lhs, rhs;

    public BinaryExpressionNode(String operator, ExpressionNode lhs, ExpressionNode rhs) { ... }
    // ...etc...

With this model, one could make the following mistake:

new BinaryExpressionNode("multipladdition", someExpr, otherExpr)

That code would compile, but would presumably fail at runtime at some point:

  • Perhaps it would not fail until doEvaluate or doCompile encounters the nonsensical "multipladdition" operator, and throws an “unknown operator” exception of some kind.
  • Perhaps the constructor could check the operator parameter first, using a “fail fast” strategy to raise the error immediately and not even allow the creation of a BinaryExpressionNode with an invalid operator. That would probably be an improvement over the first option.
  • Perhaps doEvaluate and doCompile have some default behavior for an unknown operator, such as returning 0. That would be the worst of all! Now our code doesn’t report any error at all; it just gives a bad answer. Yikes!

In any of these scenarios, one way or another, trying to use the "multipladdition" operator would fail at runtime.

One could also make the following far more subtle mistake:

public ASTNode createTwinVariableNode(String varName, String operator) {
    return new BinaryExpressionNode(
        operator,
        new VariableNode(varName),
        new VariableNode(varName));
}

public ASTNode createVariableSquaredNode(String varName) {
    return createTwinVariableNode("multiplication", varName);
}

Do you see it? Inspect the code (without running it, just using your eyes!) and try to find the mistake.

When either you’re stumped or you think you’ve got it…

Click to show the answer!

createVariableSquaredNode is passing the arguments to createTwinVariableNode in the wrong order, so that "multiplication" is the variable name and varName is the operator.

If, for example, one said createVariableSquaredNode("width"), the result would be equivalent to:

new BinaryExpressionNode(
    "width",  // operator
    new VariableNode("multiplication"),
    new VariableNode("multiplication"))

In other words, it would be like saying multiplication width multiplication where width is an operator.

Oops!

People sometimes refer to this kind of design as “stringly typed” code. This is a pun on “strongly typed:” instead of having strong types that help prevent errors, values that represent fundamentally different things are all strings.

An example solution

To prevent this problem, the Wordy implementation I gave you does not model operator as a String. Instead, it uses a Java enum type, which lists a limited set of specific values:

public class BinaryExpressionNode extends ExpressionNode {
    public enum Operator {
        ADDITION, SUBTRACTION, MULTIPLICATION, DIVISION, EXPONENTIATION
    }

    private final Operator operator;   // No longer a string!

    private final ExpressionNode lhs, rhs;

    public BinaryExpressionNode(Operator operator, ExpressionNode lhs, ExpressionNode rhs) { ... }
    // ...etc...

Now Operator.MULTIPLADDITION would show an immediate error in the IDE, because there is no value in Operator named MULTIPLADDITION. The operator name is not a string anymore; it is an identifier that Java insists on resolving to a declaration at compile time. Operator.MULTIPLADDITION won’t compile.

What about that second, more subtle mistake?

// Note: the two parameters now have different types
public ASTNode createTwinVariableNode(String varName, Operator operator) {
    return new BinaryExpressionNode(
        operator,
        new VariableNode(varName),
        new VariableNode(varName));
}

public ASTNode createVariableSquaredNode(String varName) {
    return createTwinVariableNode(Operator.MULTIPLICATION, varName);
}

We can't use the string "multiplication" for the operator anymore, because operators aren’t strings anymore. We have to say Operator.MULTIPLICATION, which is a value of type Operator.

But aha! Now when createVariableSquaredNode calls createTwinVariableNode with the parameters in the wrong order, we get a compile error! Why? Because the expected arguments to createTwinVariableNode are (String, Operator), but we are calling it with (Operator, String). The types don’t match, so the code doesn’t compile.

⭐️⭐️⭐️ CENTRAL IDEA HERE ⭐️⭐️⭐️

We have successfully turned a programmer error into a type error: we created a new type, Operator, to express the fact that variable names and operator names have fundamentally different meanings, and are not interchangeable. We structured our code so that data with a different meaning has a different type.

And because Java has static type checking, we have thus turned a runtime error into a compile-time error. Hooray! Java’s type checker can catch our mistakes at compile time, before we ever even run the code.

The ability to do this is one of the benefits, arguably the most important benefit, of having static types in a programming language.

⭐️⭐️⭐️⭐️⭐️⭐️

This is the heart of this unit of the course. Does every word of the previous three paragraphs make sense to you? Study them, and post your questions.

A puzzler for you

Here is another “stringly typed” API for an imaginary game that stores its image assets on the web:

public String addFileExtension(String fileName, String extension) { ... }

public String createPath(String directory, String file) { ... }

public String createURL(String urlBase, String path) { ... }

public String getAssetURLBase() { ... }  // returns host and path prefix for game images, sounds, etc.

public Image loadImage(String url) { ... }

Here is an example of proper usage:

Image loadGameCharacterImage(String characterName, String direction) {
    return loadImage(
        createURL(
            configuration.getAssetURLBase(),
            createPath(
                "images",
                addFileExtension(
                    characterName + "_" + direction,
                    "png"))));
}

loadGameCharacterImage("mario", "left")
// → returns an image loaded from a URL like "https://assets.mygame.com/v2/images/mario_left.png"

And here are examples of a several mistakes a developer might make with that API that all result in generating an incorrect URL for the image:

// Oops, wrong argument order: "https://assets.mygame.com/v2/images/left_mario.png"
loadGameCharacterImage("left", "mario")

// Oops, swapped file name and file extension: "https://assets.mygame.com/v2/images/png.mario_left"
return loadImage(
    createURL(
        configuration.getAssetURLBase(),
        createPath(
            "images",
            addFileExtension(
                "png",
                characterName + "_" + direction))));

// Oops, attempting to load a path, not a full URL: "images/mario_left.png"
return loadImage(
    createPath(
        "images",
        addFileExtension(
            characterName + "_" + direction,
            "png")));

// Oops, URL and subpath reversed: "images/mario_left.png/https://assets.mygame.com/v2"
return loadImage(
    createURL(
        createPath(
            "images",
            addFileExtension(
                characterName + "_" + direction,
                "png"))),
        configuration.getAssetURLBase());

// Oops, forgot to add the "images" subpath: "https://assets.mygame.com/v2/mario_left.png"
return loadImage(
    createURL(
        configuration.getAssetURLBase(),
        addFileExtension(
            characterName + "_" + direction,
            "png")));

Consider these questions, and bring your thoughts to class:

  • What are some types other than String that you could introduce to prevent some of the errors above?
  • Would introducing these types prevent other valid usages of this API?
  • Would introducing these types be worth the complexity they cause?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment