Values and Operations
In English, we can pick any noun and mix it with any verb to form a sentence. For example: Blue melts, Trees vote, and Fish shatter. The sentences may not make sense, but they are grammatically correct. Programming language grammars similarly allow us to mix data and operations in ways that don't make sense, as in these Ruby expressions:
puts "abcdefghijklmnopqrtuvwxyz" / 7
puts 62.length
Changing the grammar to prevent the formation of nonsense is too difficult. Instead, we add a notion of types to our data and our operations. A type describes two things:
- a set of values
- a set of operations that may be applied to the values
After the compiler or interpreter parses a program, it walks the tree representation and ensures that the values and operations are legal and compatible. Any errors that arise at this point are not syntax errors, but type errors.
Programming without types is like eating without taste buds. If you were to somehow turn off your taste buds, you could eat anything, including spoiled or poisonous food, and you wouldn't know the food was bad until it was already inside your system. Your taste buds send early signals to your brain about the safety of your food. So does a language's typechecker send signals to the compiler or interpreter that data and operations are being used incorrectly. A momentary bad taste is unpleasant, but it is better than a trip to the hospital. Likewise, errors in code are frustrating, but they are better than failures at runtime.
The set of values for a type might be fully and explicitly enumerated by the programmer, as in an enum
. This C++ enum
defines a new type that consists of only three values:
enum Category {
Animal,
Vegetable,
Mineral
};
For other types, the set of values is a property of the language or the computer. A boolean admits only true and false values. An unsigned byte admits values from 0 to 255. A string admits a sequence of zero or more Unicode characters.
The operations that a type supports may be builtin. For example, many languages provide arithmetic, relational, and bitwise operations for integers. In other cases, the developer is the one deciding which operations are supported. This is especially true in object-oriented programming. When you define a new class, you are effectively creating a brand new type. Each method you define is an operation supported by that type. This Ruby program defines a brand new type for points in 2D space with operations for shifting them and converting them to a string representation:
class Point2
def initialize(x, y)
@x = x
@y = y
end
def shift(dx, dy)
@x += dx
@y += dy
end
def to_s
"(#{@x}, #{@y})"
end
end
point = Point2.new(-1, 0)
point.shift(2, 3)
puts point.to_s
In this chapter, we explore the many different ways that types influence how we write and run code. By the end, you'll be able to answer the following questions:
- What types are commonly predefined in a programming language?
- What are the ways in which a value is made to have a certain type?
- When and how do programmers convert values of one type to another?
- When and how are values and operations checked for compatibility?
The answers to these questions vary widely across our programming languages.