Menu
Coddy logo textTech

Constructor Functions

Part of the Object Oriented Programming section of Coddy's R journey. Lesson 3 of 57.

Because R never checks an S3 object's fields, every object of a class should be built by one function, its constructor. By convention the low-level constructor is called new_ plus the class name. It takes each field as an argument, checks the types with stopifnot() and returns the object:

new_money <- function(amount = double(), currency = character()) {
  stopifnot(is.double(amount), is.character(currency))
  structure(list(amount = amount, currency = currency), class = "money")
}
m <- new_money(9.5, "EUR")
cat(m$amount, m$currency)

Output:

9.5 EUR

stopifnot() stops the program when one of its conditions is not TRUE, and the message names the failing check. A wrong type is caught at the moment the object is built, not later when some other function uses it:

new_money <- function(amount = double(), currency = character()) {
  stopifnot(is.double(amount), is.character(currency))
  structure(list(amount = amount, currency = currency), class = "money")
}
m <- new_money("9.5", "EUR")

Output:

Error in new_money("9.5", "EUR") : is.double(amount) is not TRUE
Calls: new_money -> stopifnot
Execution halted

The constructor is the only place that calls structure() for the class, so every object has the same fields in the same order. Code that needs many objects calls it in a loop or with lapply():

new_point <- function(x = 0, y = 0) {
  stopifnot(is.numeric(x), is.numeric(y))
  structure(list(x = x, y = y), class = "point")
}
pts <- lapply(1:3, function(i) new_point(i, i * i))
for (p in pts) cat(p$x, p$y, "\n")

Output:

1 1 
2 4 
3 9 

An S3 object does not have to be a list. A constructor can label a plain vector and store extra facts as attributes, which is how R's own factors and dates work. attr(x, "unit") reads an attribute:

new_distance <- function(value = double(), unit = "m") {
  stopifnot(is.double(value), is.character(unit))
  structure(value, unit = unit, class = "distance")
}
d <- new_distance(c(1.5, 2), "km")
print(attr(d, "unit"))
print(unclass(d)[2] * 1000)

Output:

[1] "km"
[1] 2000
challenge icon

Challenge

Easy

Complete the constructor new_book(title, author, pages) in Book.R. It checks with stopifnot() that title and author are character and pages is numeric, and returns a list with those three fields and the class book. Then complete book_label(b), which returns Dune by Frank Herbert, 412 pages.

The supplied code reads lines title;author;pages. It passes pages as a number when it is made of digits and as the original text otherwise, prints book: and the label for each object, and prints rejected: and the title when the constructor stops.

Your code goes in Book.R. main.R holds the supplied input/output code and cannot be edited.

Try it yourself

source("Book.R")

# Supplied input/output code: keep it as it is
input <- suppressWarnings(readLines(file("stdin")))
for (line in input) {
  p <- strsplit(line, ";")[[1]]
  pages <- if (grepl("^[0-9]+$", p[3])) as.numeric(p[3]) else p[3]
  b <- tryCatch(new_book(p[1], p[2], pages), error = function(e) NULL)
  if (is.null(b)) {
    cat("rejected: ", p[1], "\n", sep = "")
  } else {
    cat(class(b), ": ", book_label(b), "\n", sep = "")
  }
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Online R compiler