Menu
Coddy logo textTech

Singleton Pattern

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

A design pattern is a proven solution to a design problem that comes up again and again. The singleton pattern makes sure there is exactly one instance of something, reachable from anywhere. In R, an accessor function creates the instance on its first call, keeps it in a variable made with local(), and returns the same one every time:

get_config <- local({
  instance <- NULL
  function() {
    if (is.null(instance)) {
      instance <<- new.env()
      instance$theme <- "light"
    }
    instance
  }
})
a <- get_config()
a$theme <- "dark"
b <- get_config()
cat(b$theme, identical(a, b))

Output:

dark TRUE

The instance is an environment, so a change made through one call is seen by every other part of the program. Two functions in different places write to the same log:

get_log <- local({
  instance <- NULL
  function() {
    if (is.null(instance)) {
      instance <<- new.env()
      instance$lines <- character(0)
    }
    instance
  }
})
page_a <- function() {
  log <- get_log()
  log$lines <- c(log$lines, "a visited")
}
page_b <- function() {
  log <- get_log()
  log$lines <- c(log$lines, "b visited")
}
page_a()
page_b()
page_a()
print(get_log()$lines)

Output:

[1] "a visited" "b visited" "a visited"

The instance is created only when it is first needed. Counting the creations shows it happens once, however often the accessor is called:

created <- 0
get_db <- local({
  instance <- NULL
  function() {
    if (is.null(instance)) {
      created <<- created + 1
      instance <<- new.env()
    }
    instance
  }
})
for (i in 1:5) db <- get_db()
cat("created", created, "time(s)")

Output:

created 1 time(s)

Use singletons sparingly. A singleton is global state: any code can change it, and a test cannot start from a clean copy without resetting it. It fits things that are truly one per program, such as a logger or the settings; passing an object as an argument is often clearer:

# clearer to test: the log is passed in
process <- function(order, log) {
  log$lines <- c(log$lines, paste("processing", order$id))
}
challenge icon

Challenge

Easy

Complete the singleton logger in Logger.R. get_logger() returns one environment, created on the first call, with the field entries (character, empty at first). Then write:

  • log_info(msg) and log_error(msg), which add [INFO] msg or [ERROR] msg to the entries of the shared logger;
  • error_count(), which counts the entries that start with [ERROR].

The supplied code reads lines info,started or error,disk full, logs each, then prints every entry, the number of errors, and whether two calls of get_logger() return the same object.

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

Try it yourself

source("Logger.R")

# Supplied input/output code: keep it as it is
input <- suppressWarnings(readLines(file("stdin")))
for (line in input) {
  p <- strsplit(line, ",")[[1]]
  if (p[1] == "error") log_error(p[2]) else log_info(p[2])
}
for (e in get_logger()$entries) cat(e, "\n", sep = "")
cat("errors: ", error_count(), "\n", sep = "")
cat("one logger: ", identical(get_logger(), get_logger()), "\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