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
ordoCompile
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 aBinaryExpressionNode
with an invalid operator. That would probably be an improvement over the first option. - Perhaps
doEvaluate
anddoCompile
have some default behavior for an unknown operator, such as returning0
. 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.
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.
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?