Support custom cache pack/unpack via S3 generics for non-serializable objects - #2340
Conversation
|
maybe preprocess and postprocess are not good names... 🤔 |
This comment was marked as resolved.
This comment was marked as resolved.
259fdd5 to
ccc14d7
Compare
| cache_purge = function(hash) { | ||
| for (h in hash) unlink(paste(cache_path(h), c('rdb', 'rdx', 'RData'), sep = '.')) | ||
| for (h in hash) unlink(paste(cache_path(h), c('rds', 'rdb', 'rdx', 'RData'), sep = '.')) | ||
| } |
There was a problem hiding this comment.
cache_purge() and clean_cache() take into account of limited file types/names.
There may be some cases where knitr should remove more files.
The example in the description saved an extra file as a part of cache, which will not be removed by knitr.
registerS3method("knit_cache_preprocess", "data.frame", function(x) {
write.csv(x, "cache.csv") # NOTE: this file is not removed by `cache$purge()` or `clean_cache()`
structure("cache.csv", class = "knit_cache_csv")
}, envir = asNamespace("knitr"))
| knit_cache_postprocess = function(x, ...) UseMethod('knit_cache_postprocess') | ||
| knit_cache_postprocess.default = function(x, ...) x |
There was a problem hiding this comment.
Postprocess is skipped if a package is not loaded.
|
To solve the above problems, I implemented the |
There was a problem hiding this comment.
I feel this implementation is too complicated, and I'd like to propose a different way: in the latest version of xfun, I added two functions lazy_save() and lazy_load() as a different and simple implementation of base R's lazyLoad() and tools:::makeLazyLoadDB(). By default, xfun::lazy_save()/lazy_load() use the rds format, which should solve the problems #2176 and #2339 (I've only briefly tested #2176).
For backward compatibility, we can first test if *.rdb/*.rdx exist. If they do, we use the old approach (base R), otherwise we switch to xfun's lazy loading.
What do you think?
Correction: I don't remember how I tested #2176 now. I was expecting that this would just work:
```{r}
library(terra)
```
```{r, cache=TRUE}
r = rast(matrix(1:12,3,4))
```
```{r}
r
```but it doesn't (even if we save r to *.rds). The object r will fail to load in a new R session, and we still have to do wrap()/unwrap():
```{r}
library(terra)
```
```{r, cache=TRUE}
r = rast(matrix(1:12,3,4))
p = wrap(r)
```
```{r}
unwrap(p)
```That said, I had this issue in mind when designing the new cache system for litedown. With litedown, it's possible to customize the read/write methods for cache via the chunk option cache.rw, e.g.,
---
title: "Caching terra objects with litedown"
knit: litedown:::knit
---
```{r}
library(terra)
rw_terra = list(
name = 'terra',
save = function(x, file) {
if (inherits(x, 'SpatRaster')) x = wrap(x)
saveRDS(x, file)
},
load = function(...) {
x = readRDS(...)
if (inherits(x, 'PackedSpatRaster')) x = unwrap(x)
x
}
)
```
```{r, cache=TRUE, cache.rw=rw_terra}
r = rast(matrix(1:12,3,4))
```
```{r}
r
```|
Thanks for the comment. I accepted the complexity for following reasons:
With my implementation, user's do not have to care about what is going on under saving/loading caches. For developers, I agree chunk option is a good idea. |
|
Good points, and I agree. Let me think more about it. Thanks! |
… generics Replace tools::makeLazyLoadDB()/lazyLoad() with xfun::lazy_save()/ lazy_load() (rds format). Caches from the former format (.rdb/.rdx) are still detected and loaded, so the change is backward compatible. Add two S3 generics, knit_cache_pack() and knit_cache_unpack(), to customize how objects are written to and restored from the cache. knit_cache_pack() is called on each object before caching (dispatching on the live object) and should return a serializable value; knit_cache_unpack() is called after reading an object back (dispatching on the packed object) and should restore the original. Both default to identity. This supersedes the earlier knit_cache_hook approach and lets users cache objects the default method cannot handle, e.g. external pointers such as terra rasters via wrap()/unwrap() (closes yihui#2176). The pack/unpack happen at xfun's per-object read/write layer, so lazy loading is preserved: an object is only unpacked when actually accessed. Package authors can register methods for their classes in .onLoad(), so users need no configuration. Cache file enumeration (cache_rx, load_cache, cache_exists, cache_purge, clean_cache) is updated for the numbered lazy-load files (<hash>.0.rds, <hash>.1.rds, ...). Co-Authored-By: atusy <30277794+atusy@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Reworked. The cache backend now uses Instead of the
Both default to identity. The pack/unpack happen at xfun's per-object read/write layer, so lazy loading is preserved — an object is only unpacked when it's actually accessed. This closes #2176; packages register methods for their classes in registerS3method("knit_cache_pack", "SpatRaster", function(x, ...) terra::wrap(x), envir = asNamespace("knitr"))
registerS3method("knit_cache_unpack", "PackedSpatRaster", function(x, ...) terra::unwrap(x), envir = asNamespace("knitr"))@atusy What do you think of this approach? |
|
I'll merge this PR first. The next CRAN release won't take place anytime soon (typically it will be several months later), so you still have enough time to review and discuss. I'm open to alternative approaches. Thanks! |
|
Sounds nice! thanks a lot!! |
This PR allows implementing
knit_cache_hookmethods which may preprocess objects (e.g., save to an external file) and define custom loaders.I will add a NEWS item after we agree with the design.
cache_savereplaces rdb/rdx files with rds filecache_load()attemptslazyload()if rdb/rdx files are availablefeat(cache): allow pre/postprocessing cache objects)knit_cache_preprocesspreprocesses objects being savedknit_cache_postprocesspostprocesses objects being loadedknit_cache_hookmethods on saving cache${cache_path(h)}__extradirectory${cache_path(h).rds}With this PR, we can add some hooks on objects to be cached.
For example, we can use writeLines to save character objects.