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 TRUEThe 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
EasyComplete 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)andlog_error(msg), which add[INFO] msgor[ERROR] msgto 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 = "")
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1S3 Basics
Working With FilesLists With a ClassConstructor FunctionsPrint and Format MethodsValidators and HelpersRecap - Temperatures4Encapsulation in R
Closures as ObjectsEnvironments as ObjectsAccessor FunctionsGuarding StateRecap - Parking Meter2S3 Generics and Methods
Generics and UseMethodDefault MethodsMethods for Base GenericsNextMethod BasicsRecap - Shape Areas8Reference Classes
Defining Reference ClassesMethods and Field UpdatesCopy SemanticsInheritance and callSuperRecap - Task Queue11Project: Library Management
Books and MembersBorrowing BooksPractice on your own: Online R compiler