Classes and Methods
Auto is an object-oriented language -- that is, it provides features that support object-oriented programming, which has these defining characteristics:
Most of the computation is expressed in terms of operations on objects.
Objects often represent things in the real world, and methods often correspond to the ways things in the real world interact.
Programs include type and method definitions.
For example, in the previous chapter we defined a Time type that corresponds to the way people record the time of day, and we defined functions that correspond to the kinds of things people do with times. But there was no explicit connection between the definition of the Time type and the function definitions that follow.
We can make the connection explicit by rewriting a function as a method, which is defined inside a type definition.
Defining methods
In the previous chapter we defined a type named Time and wrote a function named print_time that displays a time of day.
type Time {
hour: int,
minute: int,
second: int,
}
fn print_time(time: Time) {
let s = f"{time.hour:02d}:{time.minute:02d}:{time.second:02d}"
print(s)
}To make print_time a method, we define it with &self as the first parameter. The &self parameter refers to the object the method is called on -- it is the receiver.
type Time {
hour: int,
minute: int,
second: int,
fn print_time(&self) {
let s = f"{self.hour:02d}:{self.minute:02d}:{self.second:02d}"
print(s)
}
}To call this method, you invoke it on a Time instance using dot notation:
let start = make_time(9, 45, 0)
start.print_time() // 09:45:00In this version, start is the object the method is invoked on, which is called the receiver. Inside the method, self refers to the same object as start.
Another method
Here's the time_to_int function from the previous chapter, rewritten as a method:
fn time_to_int(&self) -> int {
let minutes = self.hour * 60 + self.minute
let seconds = minutes * 60 + self.second
return seconds
}As in the previous example, the method uses &self as the first parameter and accesses fields through self. Other than that, the method is identical to the function.
let start = make_time(9, 45, 0)
let secs = start.time_to_int()
print("Seconds:", secs) // 35100It is common to say that we "call" a function and "invoke" a method, but they mean the same thing.
Static methods
As another example, let's consider the int_to_time function. This function takes seconds as a parameter and returns a new Time object. If we transform it into a method, we have to invoke it on a Time object. But if we're trying to create a new Time object, what are we supposed to invoke it on?
We can solve this problem using a static method, which is a method that does not have &self as a parameter and is invoked on the type itself rather than on an instance.
fn int_to_time(seconds: int) -> Time {
let (minute, second) = divmod(seconds, 60)
let (hour, minute) = divmod(minute, 60)
return make_time(hour, minute, second)
}Because it is a static method, it does not have &self as a parameter. To invoke it, we use Time, which is the type name:
let start = Time.int_to_time(34800)
start.print_time() // 09:40:00The result is a new object that represents 9:40.
How It Works
Instance methods (with &self) are called on an object: start.print_time(). Inside the method, self refers to the receiving object. Static methods (without &self) are called on the type itself: Time.int_to_time(3661). They don't operate on a specific instance and are useful for utility functions that create objects or perform computations related to the type.
Comparing Time objects
As one more example, let's write is_after as a method. Because we're comparing two objects, and the first parameter is self, we'll call the second parameter other:
fn is_after(&self, other: Time) -> bool {
return self.time_to_int() > other.time_to_int()
}To use this method, we invoke it on one object and pass the other as an argument:
let end = make_time(11, 17, 0)
print(end.is_after(start)) // true
print(start.is_after(end)) // falseOne nice thing about this syntax is that it almost reads like a question: "end is after start?"
The init method
The most important special method is init, so-called because it initializes the fields of a new object. An init method for the Time type might look like this:
fn init(&self, hour: int = 0, minute: int = 0, second: int = 0) {
self.hour = hour
self.minute = minute
self.second = second
}Now when we instantiate a Time object, Auto invokes init and passes along the arguments. So we can create an object and initialize the fields at the same time:
let time = Time(9, 40, 0)
print(time) // 09:40:00In this example, the parameters have default values, so if you call Time with no arguments, you get the defaults:
let time = Time()
print(time) // 00:00:00If you provide one argument, it overrides hour:
let time = Time(9)
print(time) // 09:00:00And if you provide three arguments, they override all three default values.
When writing a new type, it is good practice to start by writing init, which makes it easier to create objects, and to_string, which is useful for debugging.
How It Works
The init method in Auto corresponds to __init__ in Python. It is called automatically when you create a new object using Time(...). The parameters can have default values, allowing you to create objects with varying levels of specificity -- from Time() (all defaults) to Time(9, 40, 0) (all specified). Named arguments let you override specific fields without providing preceding ones.
Note for Python Programmers:
Auto uses
fn init(&self, ...)instead of Python'sdef __init__(self, ...). Thea2ptranspiler convertsinitto__init__automatically.
The to_string method
When you write a method, you can choose almost any name you want. However, some names have special meanings. If an object has a method named to_string, Auto uses that method to convert the object to a string when needed -- for example, when printing or using in an f-string.
This is Auto's equivalent of Python's __str__ method.
fn to_string(&self) -> str {
return f"{self.hour:02d}:{self.minute:02d}:{self.second:02d}"
}You can invoke this method in the usual way:
let s = end.to_string()
print(s) // 11:17:00But Auto can also invoke it for you. If you print a Time object, Auto uses the to_string method:
print(end) // 11:17:00And it does the same if you use the object in an f-string:
print(f"The time is {end}") // The time is 11:17:00How It Works
The to_string method in Auto corresponds to __str__ in Python. It returns a string representation of the object. When you print an object or use it in an f-string, Auto (via the a2p transpiler) automatically calls this method. The format specifiers :02d ensure that minutes and seconds are always displayed with two digits.
Note for Python Programmers:
Auto uses
fn to_string(&self)instead of Python'sdef __str__(self). Thea2ptranspiler convertsto_stringto__str__automatically. When reading Auto code, any method namedto_stringis the string representation method.
Operator overloading
By defining special methods, you can specify the behavior of operators on programmer-defined types. For example, if you define a method named __add__ for the Time type, you can use the + operator on Time objects.
Here is an __add__ method:
fn __add__(&self, other: Time) -> Time {
let seconds = self.time_to_int() + other.time_to_int()
return Time.int_to_time(seconds)
}We can use it like this:
let start = Time(9, 45, 0)
let duration = Time(1, 32, 0)
let end = start + duration
print(end) // 11:17:00There is a lot happening when we run these three lines of code:
- When we instantiate a
Timeobject, theinitmethod is invoked. - When we use the
+operator with aTimeobject, its__add__method is invoked. - And when we print a
Timeobject, itsto_stringmethod is invoked.
Changing the behavior of an operator so that it works with programmer-defined types is called operator overloading. For every operator, like +, there is a corresponding special method, like __add__.
We can also overload the == operator for equality comparison:
fn __eq__(&self, other: Time) -> bool {
return self.time_to_int() == other.time_to_int()
}print(start == Time(9, 45, 0)) // true
print(start == duration) // falseHow It Works
Operator overloading lets you define how operators like + and == work with your custom types. The __add__ method is called when you use +, and __eq__ is called when you use ==. Both are instance methods that take &self and other as parameters.
The is_after method is not an operator overload -- it's a regular method with a descriptive name. But it demonstrates the same pattern of comparing objects by converting them to a simpler representation (integers) first.
Debugging
A Time object is valid if the values of minute and second are between 0 and 60 -- including 0 but not 60 -- and if hour is positive. Also, hour and minute should be integer values. Requirements like these are called invariants because they should always be true.
Writing code to check invariants can help detect errors and find their causes. For example, you might have a method like is_valid that returns false if an invariant is violated:
fn is_valid(&self) -> bool {
if self.hour < 0 or self.minute < 0 or self.second < 0 {
return false
}
if self.minute >= 60 or self.second >= 60 {
return false
}
return true
}Then, at the beginning of each method you can check the arguments to make sure they are valid:
fn is_after(&self, other: Time) -> bool {
assert self.is_valid(), "self is not a valid Time"
assert other.is_valid(), "other is not a valid Time"
return self.time_to_int() > other.time_to_int()
}The assert statement evaluates the expression that follows. If the result is true, it does nothing; if the result is false, it causes an error.
Glossary
object-oriented language: A language that provides features to support object-oriented programming, notably user-defined types.
method: A function that is defined inside a type definition and is invoked on instances of that type.
receiver: The object a method is invoked on.
static method: A method that can be invoked without an object as receiver. In Auto, this is a method without &self as a parameter.
instance method: A method that must be invoked with an object as receiver. In Auto, this is a method with &self as a parameter.
special method: A method that changes the way operators and some functions work with an object. In Auto, these include init, to_string, __add__, __eq__, etc.
operator overloading: The process of using special methods to change the way operators work with user-defined types.
invariant: A condition that should always be true during the execution of a program.
Exercises
Exercise
In the previous chapter, a series of exercises asked you to write a Date type and several functions that work with Date objects. Now let's practice rewriting those functions as methods.
Write a definition for a
Datetype that represents a date -- that is, a year, month, and day of the month. Include aninitmethod that takesyear,month, anddayas parameters.Write a
to_stringmethod that uses an f-string to format the fields and returns the result. If you test it with theDateyou created, the result should be1933-06-22.Write a method called
is_afterthat takes twoDateobjects and returnstrueif the first comes after the second. Create a second object that represents September 17, 1933, and check whether it comes after the first object.
Hint: You might find it useful to write a method called to_tuple that returns a tuple that contains the fields of a Date object in year-month-day order.