blob: dfb41bba1c131933fab2eb4023c77443fbca78f1 [file] [log] [blame]
#' Keeping results in a file of one's own
#'
#' The `cacheAs` parameter of the query functions is a different thing from the
#' `cache` parameter of [KorAPConnection()]. The latter is a transparent
#' speed-up: it stores server responses where the package finds them again, and
#' throwing it away costs nothing but time. The former stores a finished result
#' in a file the caller names and keeps, which is what makes an analysis
#' reproducible: KorAP corpora grow, so the same query returns different numbers
#' next year, and the scores computed from them may change with the package.
#'
#' A cache file therefore records what produced it, and is not reused when that
#' no longer matches what is being asked for.
#'
#' @name cacheAs
#' @keywords internal
NULL
#' Attribute under which cache files record what produced them
#' @noRd
cacheAsAttribute <- "RKorAPClient.cacheAs"
#' First version whose association scores are still computed the same way today
#'
#' `logDice()` and `ll()` were corrected in 1.4.0, so a file written by an
#' earlier version holds numbers that would not be arrived at again, which no
#' comparison of parameters can notice.
#'
#' Deliberately a constant of its own rather than the package version: what a
#' cache file has to be checked against is the generation of the scores, which
#' changes far more rarely than the version does, and reading it out of
#' DESCRIPTION would make the check depend on a release having been prepared.
#' Raise it whenever a score changes.
#' @noRd
cacheAsScoreVersion <- "1.4.0"
#' Append .rds to a cache file name that does not end in it
#' @noRd
cacheAsFileName <- function(cacheAs) {
if (grepl("\\.rds$", cacheAs, ignore.case = TRUE)) cacheAs else paste0(cacheAs, ".rds")
}
#' What a cached result was computed with
#'
#' Collected from the calling function's frame, so that parameters added in the
#' future are taken into account automatically. `kco` and `cacheAs` are
#' excluded: the former is not a parameter of the analysis, the latter only says
#' where to store it.
#'
#' @param frame environment of the calling query function
#' @param dots its `...` arguments, or `NULL` where it has none
#' @param kco [KorAPConnection()] object
#' @return list to store with, and compare against, a cache file
#' @noRd
cacheAsRecord <- function(frame, dots, kco) {
parameterNames <- setdiff(
names(formals(sys.function(sys.parent()))),
# verbose says how loud the computation is, not what it computes
c("kco", "cacheAs", "verbose", "...")
)
list(
# what has to match on the next call
scoreVersion = cacheAsScoreVersion,
# recorded so that a file can say where its numbers come from
packageVersion = as.character(utils::packageVersion("RKorAPClient")),
parameters = mget(parameterNames, envir = frame),
dots = dots,
# reusing one cache file for two KorAP instances is a mistake worth catching
apiUrl = kco@apiUrl,
# recorded for reference only, deliberately not compared: corpus updates
# should not invalidate a deliberately kept result
indexRevision = kco@indexRevision
)
}
#' Why a cache file cannot be used for the current call
#'
#' @param stored record read from the cache file, `NULL` if it has none
#' @param current record of the call at hand
#' @return a sentence naming the reason, or `NULL` if the file can be used
#' @noRd
cacheAsRejectionReason <- function(stored, current) {
generation <- if (is.null(stored)) NULL else stored$scoreVersion
if (is.null(generation) || package_version(generation) < package_version(cacheAsScoreVersion)) {
writtenBy <- if (is.null(stored)) NULL else stored$packageVersion
return(if (is.null(writtenBy)) {
sprintf(
"was written before RKorAPClient %s, which corrected logDice and ll",
cacheAsScoreVersion
)
} else {
sprintf(
"was written by RKorAPClient %s, whose logDice and ll differ from those of %s",
writtenBy, cacheAsScoreVersion
)
})
}
differing <- character(0)
for (name in union(names(stored$parameters), names(current$parameters))) {
if (!identical(stored$parameters[[name]], current$parameters[[name]])) {
differing <- c(differing, name)
}
}
if (!identical(stored$dots, current$dots)) {
differing <- c(differing, "...")
}
if (!identical(stored$apiUrl, current$apiUrl)) {
differing <- c(differing, "KorAP instance")
}
if (length(differing) == 0) {
NULL
} else {
sprintf("was created with different parameters (%s)", paste(differing, collapse = ", "))
}
}
#' Read back a result stored in a cache file, if it is the one being asked for
#'
#' Warns and returns `NULL` where the file exists but does not match, so that
#' the caller recomputes and [writeCacheAs()] overwrites it.
#'
#' @param cacheAs cache file name, already passed through [cacheAsFileName()]
#' @param kco [KorAPConnection()] object, for its `verbose` flag
#' @param record what the call at hand computes, from [cacheAsRecord()]
#' @param what name of the result, for the log and warning messages
#' @return the cached result, or `NULL` if there is none to use
#' @noRd
readCacheAs <- function(cacheAs, kco, record, what) {
if (!file.exists(cacheAs)) {
return(NULL)
}
cached <- readRDS(cacheAs)
stored <- attr(cached, cacheAsAttribute)
attr(cached, cacheAsAttribute) <- NULL
reason <- cacheAsRejectionReason(stored, record)
if (is.null(reason)) {
log_info(kco@verbose, sprintf("Loading %s from cache: %s\n", what, cacheAs))
return(cached)
}
warning(
sprintf(
paste0(
"Cache file '%s' %s.\n",
"It is recomputed and overwritten; pass a different cacheAs file name to keep it."
),
cacheAs, reason
),
call. = FALSE
)
NULL
}
#' Store a result in a cache file, together with what produced it
#'
#' @param cacheAs cache file name, already passed through [cacheAsFileName()]
#' @param kco [KorAPConnection()] object, for its `verbose` flag
#' @param record what produced the result, from [cacheAsRecord()]
#' @param what name of the result, for the log message
#' @param result the result to store
#' @return `result`, invisibly and unchanged
#' @noRd
writeCacheAs <- function(cacheAs, kco, record, what, result) {
log_info(kco@verbose, sprintf("Saving %s to cache: %s\n", what, cacheAs))
# only the stored copy carries the record, so that the returned value is the
# same whether it was cached or not
cachedResult <- result
attr(cachedResult, cacheAsAttribute) <- record
saveRDS(cachedResult, cacheAs)
invisible(result)
}