cynkra / constructive Goto Github PK
View Code? Open in Web Editor NEWDisplay Idiomatic Code to Construct Most R Objects
Home Page: https://cynkra.github.io/constructive
License: Other
Display Idiomatic Code to Construct Most R Objects
Home Page: https://cynkra.github.io/constructive
License: Other
``` r
library(tidyverse)
library(readxl)
devtools::install_github("cynkra/constructive")
#> Using github PAT from envvar GITHUB_PAT
#> Skipping install of 'constructive' from a github remote, the SHA1 (41f955e1) has not changed since last install.
#> Use `force = TRUE` to force installation
sessionInfo()
#> R version 4.2.0 (2022-04-22)
#> Platform: x86_64-apple-darwin17.0 (64-bit)
#> Running under: macOS Big Sur/Monterey 10.16
#>
#> Matrix products: default
#> BLAS: /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRblas.0.dylib
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRlapack.dylib
#>
#> locale:
#> [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] readxl_1.4.1 forcats_0.5.2 stringr_1.4.1 dplyr_1.0.9
#> [5] purrr_0.3.4 readr_2.1.2 tidyr_1.2.0 tibble_3.1.8
#> [9] ggplot2_3.3.6 tidyverse_1.3.2
#>
#> loaded via a namespace (and not attached):
#> [1] fs_1.5.2 usethis_2.1.6 lubridate_1.8.0
#> [4] devtools_2.4.4 httr_1.4.4 R.cache_0.16.0
#> [7] tools_4.2.0 profvis_0.3.7 backports_1.4.1
#> [10] utf8_1.2.2 R6_2.5.1 DBI_1.1.3
#> [13] colorspace_2.0-3 urlchecker_1.0.1 withr_2.5.0
#> [16] tidyselect_1.1.2 prettyunits_1.1.1 processx_3.7.0
#> [19] curl_4.3.2 compiler_4.2.0 cli_3.3.0
#> [22] rvest_1.0.3 xml2_1.3.3 scales_1.2.1
#> [25] callr_3.7.2 digest_0.6.29 rmarkdown_2.16
#> [28] R.utils_2.12.0 pkgconfig_2.0.3 htmltools_0.5.3
#> [31] sessioninfo_1.2.2 styler_1.7.0 dbplyr_2.2.1
#> [34] fastmap_1.1.0 highr_0.9 htmlwidgets_1.5.4
#> [37] rlang_1.0.4 rstudioapi_0.14 shiny_1.7.2
#> [40] generics_0.1.3 jsonlite_1.8.0 R.oo_1.25.0
#> [43] googlesheets4_1.0.1 magrittr_2.0.3 Rcpp_1.0.9
#> [46] munsell_0.5.0 fansi_1.0.3 lifecycle_1.0.1
#> [49] R.methodsS3_1.8.2 stringi_1.7.8 yaml_2.3.5
#> [52] pkgbuild_1.3.1 grid_4.2.0 promises_1.2.0.1
#> [55] crayon_1.5.1 miniUI_0.1.1.1 haven_2.5.1
#> [58] hms_1.1.2 knitr_1.40 ps_1.7.1
#> [61] pillar_1.8.1 pkgload_1.3.0 reprex_2.0.2
#> [64] glue_1.6.2 evaluate_0.16 remotes_2.4.2
#> [67] modelr_0.1.9 vctrs_0.4.1 tzdb_0.3.0
#> [70] httpuv_1.6.5 cellranger_1.1.0 gtable_0.3.0
#> [73] assertthat_0.2.1 cachem_1.0.6 xfun_0.32
#> [76] mime_0.12 xtable_1.8-4 broom_1.0.0
#> [79] later_1.3.0 googledrive_2.0.0 gargle_1.2.0
#> [82] memoise_2.0.1 ellipsis_0.3.2
gwrg <- read_xlsx(file.path(Sys.getenv("FBEA_DATADIR"), "GWRG/GWRG_2020.xlsx"), sheet = 2, skip = 1)
#> New names:
#> • `EKZ Heizen` -> `EKZ Heizen...66`
#> • `EKZ Warmwasser` -> `EKZ Warmwasser...67`
#> • `EKZ Heizen` -> `EKZ Heizen...68`
#> • `EKZ Warmwasser` -> `EKZ Warmwasser...69`
constructive::construct(gwrg, max_atomic = 0)
#> Error in `constructive::construct()`:
#> ! {constructive} could not build the requested code.
#> Caused by error in `x_chr[dec_lgl] <- paste0(x_chr[dec_lgl], sub("^0", "", format(split_s[dec_lgl],
#> digits = 5)))`:
#> ! NAs are not allowed in subscripted assignments
#> Backtrace:
#> ▆
#> 1. ├─constructive::construct(gwrg, max_atomic = 0)
#> 2. │ └─constructive:::try_construct(...)
#> 3. │ ├─rlang::try_fetch(...)
#> 4. │ │ ├─base::tryCatch(...)
#> 5. │ │ │ └─base (local) tryCatchList(expr, classes, parentenv, handlers)
#> 6. │ │ │ └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
#> 7. │ │ │ └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#> 8. │ │ └─base::withCallingHandlers(...)
#> 9. │ └─constructive:::construct_raw(...)
#> 10. │ ├─data_match(x, data) %||% ...
#> 11. │ ├─constructive:::construct_idiomatic(x, data = data, ...)
#> 12. │ └─constructive:::construct_idiomatic.tbl_df(x, data = data, ...)
#> 13. │ ├─construct_tribble(x, tribble, ...) %||% ...
#> 14. │ └─constructive:::construct_apply(...)
#> 15. │ └─base::lapply(args, construct_raw, ...)
#> 16. │ └─constructive (local) FUN(X[[i]], ...)
#> 17. │ ├─data_match(x, data) %||% ...
#> 18. │ ├─constructive:::construct_idiomatic(x, data = data, ...)
#> 19. │ └─constructive:::construct_idiomatic.POSIXct(x, data = data, ...)
#> 20. └─base::.handleSimpleError(...)
#> 21. └─rlang (local) h(simpleError(msg, call))
#> 22. └─handlers[[1L]](cnd)
#> 23. └─rlang::abort(...)
constructive::construct(gwrg, max_atomic = 1)
#> Error in `constructive::construct()`:
#> ! {constructive} could not build the requested code.
#> Caused by error in `x_chr[dec_lgl] <- paste0(x_chr[dec_lgl], sub("^0", "", format(split_s[dec_lgl],
#> digits = 5)))`:
#> ! NAs are not allowed in subscripted assignments
#> Backtrace:
#> ▆
#> 1. ├─constructive::construct(gwrg, max_atomic = 1)
#> 2. │ └─constructive:::try_construct(...)
#> 3. │ ├─rlang::try_fetch(...)
#> 4. │ │ ├─base::tryCatch(...)
#> 5. │ │ │ └─base (local) tryCatchList(expr, classes, parentenv, handlers)
#> 6. │ │ │ └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
#> 7. │ │ │ └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#> 8. │ │ └─base::withCallingHandlers(...)
#> 9. │ └─constructive:::construct_raw(...)
#> 10. │ ├─data_match(x, data) %||% ...
#> 11. │ ├─constructive:::construct_idiomatic(x, data = data, ...)
#> 12. │ └─constructive:::construct_idiomatic.tbl_df(x, data = data, ...)
#> 13. │ ├─construct_tribble(x, tribble, ...) %||% ...
#> 14. │ └─constructive:::construct_apply(...)
#> 15. │ └─base::lapply(args, construct_raw, ...)
#> 16. │ └─constructive (local) FUN(X[[i]], ...)
#> 17. │ ├─data_match(x, data) %||% ...
#> 18. │ ├─constructive:::construct_idiomatic(x, data = data, ...)
#> 19. │ └─constructive:::construct_idiomatic.POSIXct(x, data = data, ...)
#> 20. └─base::.handleSimpleError(...)
#> 21. └─rlang (local) h(simpleError(msg, call))
#> 22. └─handlers[[1L]](cnd)
#> 23. └─rlang::abort(...)
constructive::construct(gwrg, max_atomic = 2)
#> Error in `constructive::construct()`:
#> ! {constructive} could not build the requested code.
#> Caused by error in `x_chr[dec_lgl] <- paste0(x_chr[dec_lgl], sub("^0", "", format(split_s[dec_lgl],
#> digits = 5)))`:
#> ! NAs are not allowed in subscripted assignments
#> Backtrace:
#> ▆
#> 1. ├─constructive::construct(gwrg, max_atomic = 2)
#> 2. │ └─constructive:::try_construct(...)
#> 3. │ ├─rlang::try_fetch(...)
#> 4. │ │ ├─base::tryCatch(...)
#> 5. │ │ │ └─base (local) tryCatchList(expr, classes, parentenv, handlers)
#> 6. │ │ │ └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
#> 7. │ │ │ └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#> 8. │ │ └─base::withCallingHandlers(...)
#> 9. │ └─constructive:::construct_raw(...)
#> 10. │ ├─data_match(x, data) %||% ...
#> 11. │ ├─constructive:::construct_idiomatic(x, data = data, ...)
#> 12. │ └─constructive:::construct_idiomatic.tbl_df(x, data = data, ...)
#> 13. │ ├─construct_tribble(x, tribble, ...) %||% ...
#> 14. │ └─constructive:::construct_apply(...)
#> 15. │ └─base::lapply(args, construct_raw, ...)
#> 16. │ └─constructive (local) FUN(X[[i]], ...)
#> 17. │ ├─data_match(x, data) %||% ...
#> 18. │ ├─constructive:::construct_idiomatic(x, data = data, ...)
#> 19. │ └─constructive:::construct_idiomatic.POSIXct(x, data = data, ...)
#> 20. └─base::.handleSimpleError(...)
#> 21. └─rlang (local) h(simpleError(msg, call))
#> 22. └─handlers[[1L]](cnd)
#> 23. └─rlang::abort(...)
Created on 2022-08-30 with reprex v2.0.2
since for now dm() is not deterministic
in the same spirit as construct_reprex in #20
construct_workspace()
construct all objects in the global env, and also print library calls and use these packages as data.construct_dump(x, path, ...)
takes a named list of objects and prints it to a R script.The fun continues, with most recent #56, d8ab33b.
fun1 <- function() {
foo::bar()
}
fun2 <- function() {
foo::bar()
callr::r(function() {})
}
fun3 <- function() {
foo::bar()
fun1()
}
constructive::construct(fun1)
#> function() {
#> foo::bar()
#> }
constructive::construct(fun2)
#> function() {
#> `::`(foo, bar)()
#> `::`(callr, r)(function() `{`())
#> }
constructive::construct(fun3)
#> function() {
#> foo::bar()
#> fun1()
#> }
Created on 2022-10-15 with reprex v2.0.2
If we use the syntax in #64 we might not need to define a new method, we'd call construct(x, my_new_class = my_constructor)
.
If we want to "properly" extend, so we might create a method for construct_raw()
(if we follow current vision in #65), and optionally an opts_*()
function.
At the present time a user might extend the package by providing a construct_idiomatic()
and repair_attributes()
method but they'll need to use unexported functions, so that's a bit too messy to advertise.
Input
df <- structure(
list(
date = structure(
c(18745, 18746, 18747), class = "Date"),
`bla` =
c(0.069, 0.044, 0.049),
`bla bla` =
c(0.046, 0.005, 0.003)
),
row.names = c(NA, 3L),
class = "data.frame")
construct(df)
Output:
> construct(df)
data.frame(
date = as.Date(c("2021-04-28", "2021-04-29", "2021-04-30")),
bla = c(0.06900000000000001, 0.044, 0.049),
`bla bla` = c(0.046, 0.005, 0.003)
)
Error in `construct()`:
! {constructive} couldn't create code that reproduces perfectly the output
`names(original)`: "date" "bla" "bla bla"
`names(recreated)`: "date" "bla" "bla.bla"
`original$bla bla` is a double vector (0.046, 0.005, 0.003)
`recreated$bla bla` is absent
`original$bla.bla` is absent
`recreated$bla.bla` is a double vector (0.046, 0.005, 0.003)
i use `check = FALSE` to ignore this error
Run `rlang::last_error()` to see where the error occurred.
Warning message:
Could not use colored = TRUE, as the package prettycode is not installed. Please install it if you want to see colored output or see `?print.vertical` for more information.
> rlang::last_trace()
<error/rlang_error>
Error in `construct()`:
! {constructive} couldn't create code that reproduces perfectly the output
`names(original)`: "date" "bla" "bla bla"
`names(recreated)`: "date" "bla" "bla.bla"
`original$bla bla` is a double vector (0.046, 0.005, 0.003)
`recreated$bla bla` is absent
`original$bla.bla` is absent
`recreated$bla.bla` is a double vector (0.046, 0.005, 0.003)
i use `check = FALSE` to ignore this error
---
Backtrace:
x
1. \-constructive::construct(df)
2. \-constructive:::check_round_trip(x, evaled, styled_code, ignore_srcref = ignore_srcref)
3. \-rlang::abort(...)
Input
nadf <- structure(
list(
date = structure(
c(18745, 18746, 18747), class = "Date"),
`bla` =
c(0.069, 0.044, 0.049),
`bla bla` =
c(0.046, 0.005, 0.003),
`bla bla bla` =
c(NA_real_, NA_real_, NA_real_)
),
row.names = c(NA, 3L),
class = "data.frame")
construct(nadf)
Output
> construct(nadf)
Error in `construct()`:
! {constructive} could not build the requested code.
Caused by error in `if (as.numeric(formatted) == x) ...`:
! missing value where TRUE/FALSE needed
Run `rlang::last_error()` to see where the error occurred.
Warning message:
In FUN(X[[i]], ...) : NAs introduced by coercion
> rlang::last_trace()
<error/rlang_error>
Error in `construct()`:
! {constructive} could not build the requested code.
Caused by error in `if (as.numeric(formatted) == x) ...`:
! missing value where TRUE/FALSE needed
---
Backtrace:
x
1. +-constructive::construct(nadf)
2. | \-constructive:::try_construct(...)
3. | +-rlang::try_fetch(...)
4. | | +-base::tryCatch(...)
5. | | | \-base (local) tryCatchList(expr, classes, parentenv, handlers)
6. | | | \-base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
7. | | | \-base (local) doTryCatch(return(expr), name, parentenv, handler)
8. | | \-base::withCallingHandlers(...)
9. | \-constructive:::construct_raw(...)
10. | +-data_match(x, data) %||% ...
11. | +-constructive:::construct_idiomatic(x, data = data, ...)
12. | \-constructive:::construct_idiomatic.data.frame(...)
13. | \-constructive:::construct_apply(...)
14. | \-base::lapply(args, construct_raw, ...)
15. | \-constructive (local) FUN(X[[i]], ...)
16. | +-data_match(x, data) %||% ...
17. | +-constructive:::construct_idiomatic(x, data = data, ...)
18. | \-constructive:::construct_idiomatic.double(x, data = data, ...)
19. | +-constructive:::construct_apply(...)
20. | \-base::vapply(x, format_flex, character(1))
21. | \-constructive (local) FUN(X[[i]], ...)
22. \-base::.handleSimpleError(...)
23. \-rlang (local) h(simpleError(msg, call))
24. \-handlers[[1L]](cnd)
25. \-rlang::abort(...)
Just learnt about those functions.
They wouldn't be all that great in the general case, but if some components are constant they are recycled so we might provide a compact representation.
Not crucial
copied from #1 :
The main challenges come from :
srcrefs have all those, the package might implement its own compact constructor for those since I see no compact solution with base R or mainstream packages.
This arg would allow us :
construct(quote({
1
2
}, language.semi_colon = TRUE)
#> quote({1; 2})
This is cool because there's currently no way that I know about that allows to deparse a call into a unique character string. the closest thing is deparse1()
which is just a space separated deparse()
that fails really bad on the above.
We use rlang::expr_deparse()
on language objects and regular deparse on the body of functions because of some limitations of rlang::expression_deparse()
.
The way that would offer more flexibility would be to build our own deparser with more flexibility. Alternatives are :
We might:
rlang::expr_deparse()
flexible : I don't see itexpr_deparse()
and deparse()
, we need to parse quotes, backquotes, % and { and bind lines with closed everything except {, new lines in : That's hackynote: rlang::expr_deparse
has a fair amount of problematic corner cases, so if we want to be able to deal with pathological cases we might really need our own deparser:
rlang::expr_deparse(call("@", 1, 2))
#> [1] "1@2"
constructive::construct(call("@", 1, 2))
#> Error in `constructive::construct()`:
#> ! The code built by {constructive} could not be parsed.
#> Caused by error in `parse_safely()`:
#> ! <text>:1:9: unexpected numeric constant
#> 1: quote(1@2
#> ^
For now we rely on rlang::express_deparse()
, which is better than base::deparse()
but insufficient nevertheless.
These seem like artificial cases but :
# Problem 1 --------------------------------------------------------------------
# OK
x <- quote(a(1)(2))
attr(x, "foo") <- "bar"
constructive::construct(x)
#> quote(a(1)(2)) |>
#> structure(foo = "bar")
# not OK
x <- quote(a(1)(2))
attr(x[[1]], "foo") <- "bar"
constructive::construct(x)
#> quote(a(1)(2))
#> Error in `constructive::construct()`:
#> ! {constructive} couldn't create code that reproduces perfectly the output
#> `attr(original[[1]], 'foo')` is a character vector ('bar')
#> `attr(recreated[[1]], 'foo')` is absent
#> ℹ use `check = FALSE` to ignore this error
# due to
rlang::expr_deparse(x)
#> [1] "a(1)(2)"
# valid generation -------------------------------------------------------------
x <- quote(a(1)(2))
attr(x[[1]], "foo") <- "bar"
# works but we'll have problems with recursion
y <- bquote(
.(structure(quote(a(1)), foo = "bar"))(2)
)
identical(x, y)
#> [1] TRUE
# a bit less pretty but no problem with recursion
y <- as.call(c(
structure(quote(a(1)), foo = "bar"),
2
))
identical(x, y)
#> [1] TRUE
# Problem 2 --------------------------------------------------------------------
# OK
x <- quote(a(1))
x[[1]] <- quote(mean)
constructive::construct(x)
#> quote(mean(1))
# not OK
x <- quote(a(1))
x[[1]] <- mean
constructive::construct(x)
#> Error in `constructive::construct()`:
#> ! The code built by {constructive} could not be parsed.
#> Caused by error in `parse_safely()`:
#> ! <text>:1:7: unexpected '<'
#> 1: quote(<
#> ^
# due to
rlang::expr_deparse(x)
#> [1] "<function(x, ...) UseMethod(\"mean\")>(1)"
# valid generation -------------------------------------------------------------
x <- quote(a(1))
x[[1]] <- mean
# here maybe we can use bquote because no possible recursion
# still if the original code use `.()` this won't work, and only `as.call()` can save us there I think
y <- bquote(
.(mean)(1)
)
identical(x, y)
#> [1] TRUE
Created on 2022-09-01 by the reprex package (v2.0.1)
construct_repair <- function(x, ref, ...) {...}
#> ref |>
#> purrr::modify_in(...) |>
#> purrr::modify_in(...) |>
#> ...
This is useful to :
{waldo} enumerates differences but doesn't provide a way to reconcile easily
We would assume a similar nested structure between x
and ref
, and would output code that uses purrr::modify_in()
(which allow us to use "pluck locations").
As a first iteration, replacing/adding/removing completely any nested element is good enough. Implementation while we recurse in parallel, whenever a nested element is not identical we dig in.
Once we have a working implementation we can see if it's useful to go further and look for renamed items for instance, or look at data frame by rows etc. This could all be very much customisable.
We use construct_raw()
on everything we add and replace, and style the final code as we do in construct()
Use case :
See discussion in #58
functions are for first CRAN version, I think srcrefs for language objects can wait a bit more
@krlmlr made an interesting suggestion
Current implementation and ideas in #34 pose a few challenges :
The suggestion, is to use additional functions that return "option" objects. These objects are fed unnamed to the ...
and are recognised by the relevant method (we need a helper for this too).
This would additionally provide a way to isolate the argument checking/processing from the logic.
The downside is :
constructive::
several times)The upside seems bigger though.
From #56
I now think the ... should just be for the user facing interface. We might just pass around an opts object.
As an early step we resolve the the "constructive_options" object, the named args, and the template into an opts object and we forward it. dots don't need to be used. It's more efficient too, we don't fetch_opts() several times.
This is internal/not breaking, so not crucial for 1st CRAN release
This would violate a principle of constructive, which is that we construct an object with a single piped call, but it might be worth it.
We add a new new arg to opts_environment(), e.g. predefine = FALSE, when predefine is TRUE, any environment that is not directly accessible through asNamespace() etc is defined once on top.
..env.1.. <- new.env() ..env.1..$a <- ... ..env.1..$b <- ...
This would make the code look better and be easier to trim in case some environment can be simplified or removed manually.
It also allow us to refer to the same environment in multiple places, with the current way we would create a different environment.
Finally this would allow us to deal with circularity, whenever we find an environment we've already defined we just reference it.
construct_idiomatic()
methods on a single page, even though they (and their generic) are not exported.This means that :
tribble
becomes tbl_df.tribble
read.table
becomes data.frame.read.table
max_atomic
leaves top level and becomes atomic.max
, possibly overriden by character.max
etcmax_list
leaves top level and becomes list.max
max_body
leaves top level and becomes function.max
max
argument that they all inherit fromenv_as_list
leaves top level and becomes environment.as_list
(and we can have independent args environment.use_namespace
etc)In particular I'd like an arg for the formatting of functions, using as.function()
is really ugly and unnecessary in many cases where the environment is not important.
Since this makes room at the top level we can add more ignore_
args to forward to Waldo.
I'd like a zap_srcref
arg at the top level, that function.zap_srcref
etc inherit from, and let's make it FALSE
by default, these are rarely useful.
deparse_call(quote(a()$b))
#> `$`(a(), b)
I first though we could parse Waldo's messages and summarise but it's brittle, we can do better.
Whenever we do a reconstruction that we know will be inexact, e.g. reproducing an environment, or reproducing objects linked to env like functions, formulas, srcrefs, we update global variables, and we'll get a summary like :
- 2 functions couldn't be reliably reproduced because of their environment and their srcref
- 3 formulas couldn't be reproduced because of their environment
- 1 other environment couldn't be reproduced
The object will have an additional issues
element next to code
and compare
construct_issues()
would reiterate those and add suggestions since some constructors might do the job better
Installing r-dbi/DBItest@477ae1d with --with-keep.source
, then:
DBItest:::spec_result_roundtrip$data_timestamp_current
#> function(ctx, con) {
#> #' (also applies to the return value of the SQL function `current_timestamp`)
#> coercible_to_timestamp <- function(x) {
#> x_timestamp <- try_silent(as.POSIXct(x))
#> !is.null(x_timestamp) && all(is.na(x) == is.na(x_timestamp))
#> }
#>
#> is_roughly_current_timestamp <- function(x) {
#> coercible_to_timestamp(x) && (Sys.time() - as.POSIXct(x, tz = "UTC") <= hms::hms(2))
#> }
#>
#> test_select_with_null(
#> .ctx = ctx, con,
#> "current_timestamp" ~ is_roughly_current_timestamp
#> )
#> }
#> <environment: namespace:DBItest>
constructive::construct(DBItest:::spec_result_roundtrip$data_timestamp_current, ignore_function_env = TRUE)
#> function(ctx, con) {
#> coercible_to_timestamp <- function(x) {
#> x_timestamp <- try_silent(as.POSIXct(x))
#> !is.null(x_timestamp) && all(is.na(x) == is.na(x_timestamp))
#> }
#> is_roughly_current_timestamp <- function(x) {
#> coercible_to_timestamp(x) && (Sys.time() - as.POSIXct(x, tz = "UTC") <= `::`(hms, hms)(2))
#> }
#> test_select_with_null(.ctx = ctx, con, "current_timestamp" ~ is_roughly_current_timestamp)
#> }
Created on 2022-10-15 with reprex v2.0.2
Having good idiomatic constructors deprives us of inspecting the structure of an object at a lower level.
We might add arguments to option constructors to turn them of. For instance opts_tbl_df(skip = TRUE)
would describe a tibble with a list constructor, like list(a = 1:2, b = 3:4) |> structure(class = c("tbl_df", "tbl", "data.frame"), row.names = c(NA, -2L))
This would work a bit like NextMethod()
in the spirit and maybe we can even use it.
It's probably not a trivial change so -> 0.2
The check
arg is useful but the failures are distracting and not always helpful.
Let's make check = NULL
the default.
It works like check = TRUE if the check is satisfied
If the check is not satisfied we just mention it or summarise it in a couple discrete lines at the bottom, and inform the user they can get details on the last issues with construct_issues()
check = NULL
doesn't fail, it returns the rich object described in #36 but the user still sees if the object was not recreated perfectly.check = TRUE
does fail, and returns the rich object if success.check = FALSE
returns the rich object with an empty waldo_compare
letdput has a file
arg, maybe it's better to have a different function though ?
I'm thinking construct_to_file()
with similar signature, but we provide a named list as a min input.
construct_to_file(list(foo = ..., bar = ...), path = ..., ...)
will build a file containing :
foo <- ...
bar <- ...
We need construct_issues()
to build something fancier that a waldo output, so that would come after that.
we don't have to name the inputs.
foo <- source(path)
(assuming source() return latest statement ?)constructive::construct(list(pryr::partial, purrr::partial), data = c("pryr", "purrr"))
#> {constructive} couldn't create code that reproduces perfectly the input
#> ℹ Call `construct_issues()` to inspect the last issues
#> list(partial, partial)
constructive::construct(list(1, 2), data = list(list(x=1), list(x=2)))
#> {constructive} couldn't create code that reproduces perfectly the input
#> ℹ Call `construct_issues()` to inspect the last issues
#> list(x, x)
Created on 2022-11-04 with reprex v2.0.2
I think values detected in namespaces should be constructed with namespaced notation, this would solve the first case.
Second case should probably fail, taking the first or last silently, or even with a warning, would probably be confusing and I suspect the situation will be rare.
Now we have :
This would be nice but I don't think we can do it:
UseMethod("construct")
We'd need to find a way to deal with the preprocessing of data
(can't change args before UseMethod()
, and we'd not be able anymore to call the generic from the code, for efficiency reasons, unless we provide args to construct to skip all check and process
A simpler way would be to keep these steps :
construct_raw
be the generic containing UseMethod("construct")
Have to think about this some more -> 0.2
We might need a bit more experience with the API, but as I see it now :
construct_apply()
, wrap()
, pipe()
. These needs to be documented and renamed, with prefix that separate them well from user facing functions, maybe cnstr_*
. We might also move those to a {constructor.utils} package that {construct} imports so {constructor} doesn't attach too much, or is it over-engineering ?The opts_*
functions need to be namespaced if the package is not attached, they're useful because of autocomplete but we could provide shorthands, these would be equivalent
construct(x, opts_my_class(my_constructor, foo = bar))
construct(x, my_class = list(my_constructor, foo = bar)
construct(x, opts_my_class(my_constructor))
construct(x, my_class = list(my_constructor)
construct(x, my_class = my_constructor)
Where my_constructor
can be a string (only option at time of writing), or a function when we have the feature.
r-dbi/RSQLite#437 is generated by r-dbi/DBItest#267 which uses constructive. In DBItest I'm making extensive use of inline comments. Is there a way to keep them with construct()
?
This one seems fun, for some definition of fun. With most recent #56, 94bb159:
fun <- parse(keep.source = FALSE, srcfile = "test.R", text = "function() df[1, 1, drop = FALSE]")[[1]]
fun
#> function() df[1, 1, drop = FALSE]
constructive::construct(fun, check = TRUE)
#> quote(function() df[1, 1, FALSE])
#> Error in `check_round_trip()` at constructive/R/constructive.R:63:2:
#> ! {constructive} couldn't create code that reproduces perfectly the input
#> `original`: `function() df[1, 1, drop = FALSE]`
#> `recreated`: `function() df[1, 1, FALSE]`
#> Backtrace:
#> ▆
#> 1. └─constructive::construct(fun, check = TRUE)
#> 2. └─constructive:::check_round_trip(...) at constructive/R/constructive.R:63:2
#> 3. └─rlang::abort(c(msg)) at constructive/R/constructive.R:159:4
Created on 2022-10-15 with reprex v2.0.2
We want to return a list with a class "constructive"
It prints as it does now.
It contains :
The print method uses styler but users can fetch the code element and do what they want.
The waldo_compare element is used by construct_issues
and to display a summary of the issues. If we have environment issues, secret issues, attribute issues, I'd like the summary just to mention it and no other detail.
construct()
will update the globals$last_construct
variable, which will be used by construct_issues()
by default.
current behaviour :
foo <- function(x) {
x
}
constructive::construct(foo)
#> {constructive} couldn't create code that reproduces perfectly the input
#> ℹ Call `construct_issues()` to inspect the last issues
#> function(x) {
#> x
#> }
constructive::construct_issues()
#> `environment(original)` is <env:global>
#> `environment(recreated)` is <env:0x1230c2768>
via usethis::git_default_branch_rename()
. For consistency with other repositories in this organization.
df <- tibble::tibble(a = 1:2,
b = 2:3,
c = rep(as.POSIXct(Sys.time(), tz = "UTC"), 2))
df
#> # A tibble: 2 × 3
#> a b c
#> <int> <int> <dttm>
#> 1 1 2 2022-07-14 15:24:08
#> 2 2 3 2022-07-14 15:24:08
constructive::construct(df, max_atomic = 0)
#> tibble::tibble(
#> a = integer(0),
#> b = integer(0),
#> c = as.POSIXct(c("2022-07-14 15:24:08.66721", "2022-07-14 15:24:08.66721")) |>
#> structure(tzone = NULL),
#> )
Created on 2022-07-14 by the reprex package (v2.0.1)
Can I try fix this and fill a PR?
We delegate it to styler because I didn't want to bother pass indentions recursively but it would probably be more efficient. We would just need {prettycode} for colors. {styler} might still be used optionally.
Low priority
activeBindingFunction()
and then explore environments, {constructive} might helpIn practice we care about those very little but putting it out there for reference/discussion.
Error occurs (no exact reproduction) when using Inf
in date values:
testdf <- structure(
list(
security = c(
"a", "b",
"c", "d", "e", "f",
"g", "h", "x", "y",
"z", "m", "n"),
Benchmark = c(
"bla",
"blabla", "bla", "blubb", "bla", "blubb",
"bla", "blubbblubb", "bla", "blubbblubb", "bla",
"blubbblubb", "blabla"),
Level = c(
1L, 2L, 1L, 2L, 1L,
2L, 1L, 2L, 1L, 2L, 1L, 2L, 2L),
start.date = structure(
c(Inf,
Inf, 18839, 18839, 18839, 18839, 18900, 18900, Inf, Inf, 18839,
18900, 18839),
class = "Date"),
end.date = structure(
c(-Inf,
-Inf, 18961, 18870, 18961, 18961, 18961, 18961, -Inf, -Inf, 18961,
18961, 18870),
class = "Date")),
class = "data.frame",
row.names = c(NA, -13L))
constructive::construct(
testdf
)
Output
data.frame(
security = c(
"a", "b", "c", "d", "e", "f", "g", "h", "x", "y", "z", "m",
"n"
),
Benchmark = c(
"bla", "blabla", "bla", "blubb", "bla", "blubb", "bla", "blubbblubb",
"bla", "blubbblubb", "bla", "blubbblubb", "blabla"
),
Level = c(1L, 2L, 1L, 2L, 1L, 2L, 1L, 2L, 1L, 2L, 1L, 2L, 2L),
start.date = as.Date(c(
NA, NA, "2021-07-31", "2021-07-31", "2021-07-31", "2021-07-31",
"2021-09-30", "2021-09-30", NA, NA, "2021-07-31", "2021-09-30",
"2021-07-31"
)),
end.date = as.Date(c(
NA, NA, "2021-11-30", "2021-08-31", "2021-11-30", "2021-11-30",
"2021-11-30", "2021-11-30", NA, NA, "2021-11-30", "2021-11-30",
"2021-08-31"
))
)
Error in `constructive::construct()`:
! {constructive} couldn't create code that reproduces perfectly the output
`unclass(original$start.date)[1:5]`: Inf Inf 18839 18839 18839
`unclass(recreated$start.date)[1:5]`: NA NA 18839 18839 18839
`unclass(original$start.date)[6:13]`: 18839 18900 18900 Inf Inf 18839 18900 18839
`unclass(recreated$start.date)[6:13]`: 18839 18900 18900 NA NA 18839 18900 18839
`unclass(original$end.date)[1:5]`: -Inf -Inf 18961 18870 18961
`unclass(recreated$end.date)[1:5]`: NA NA 18961 18870 18961
`unclass(original$end.date)[6:13]`: 18961 18961 18961 -Inf -Inf 18961 18961 18870
`unclass(recreated$end.date)[6:13]`: 18961 18961 18961 NA NA 18961 18961 18870
i use `check = FALSE` to ignore this error
Run `rlang::last_error()` to see where the error occurred.
We could have (for max = 2):
letters
becomes c("a", "b", character(24))
as.list(letters)
becomes list("a", "b") |> c(vector("list", 24)
We'd have also a fill = TRUE
arg, when it's set to FALSE
we just trim the vectors/lists.
So to get the prototype we need both max = 0 and fill = FALSE. And we might have a wrapper construct_ptype() which might be the same with different defaults (with fill = TRUE we get the "skeleton").
I wonder if it makes sense to offer a way to fill with NA
s of relevant type rather than default atomic (character(1)
is ""), we'd use rep(NA, n)
for those, or mention the right NA type if max_atomic is 0.
we could also have a set of match.arg'ed values for fill, including "+" (current) and "..." (previous)
We might also have fill_atomic = fill
, fill_list = fill
, max_atomic = max
, max_list = max
for finer control and nice shortcuts.
This will run forever, though it doesn't have to :
env <- new.env()
env$e <- env
construct(env)
We should keep a table of visited environments, and fall back to new.env() when we find circularity.
Maybe this should be done for first release since it's a bad look when nothing happens.
I'll fix this with usethis::use_package("pixarfilms")
.
I am not entirely sure this is correct in this case since it is only used to run tests smoothly. So it might not be the case to add it as a dependency to the package.
It's a common enough object type.
They already "work" but not idiomatically :
constructive::construct(rlang::quo(a))
#> match.fun("environment<-")(~a, .GlobalEnv) |>
#> structure(class = c("quosure", "formula"))
library(tidyverse)
library(readxl)
library(rstudioapi)
sessionInfo()
#> R version 4.2.0 (2022-04-22)
#> Platform: x86_64-apple-darwin17.0 (64-bit)
#> Running under: macOS Big Sur/Monterey 10.16
#>
#> Matrix products: default
#> BLAS: /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRblas.0.dylib
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.2/Resources/lib/libRlapack.dylib
#>
#> locale:
#> [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] rstudioapi_0.14 readxl_1.4.1 forcats_0.5.2 stringr_1.4.1
#> [5] dplyr_1.0.9 purrr_0.3.4 readr_2.1.2 tidyr_1.2.0
#> [9] tibble_3.1.8 ggplot2_3.3.6 tidyverse_1.3.2
#>
#> loaded via a namespace (and not attached):
#> [1] lubridate_1.8.0 assertthat_0.2.1 digest_0.6.29
#> [4] utf8_1.2.2 R6_2.5.1 cellranger_1.1.0
#> [7] backports_1.4.1 reprex_2.0.2 evaluate_0.16
#> [10] httr_1.4.4 highr_0.9 pillar_1.8.1
#> [13] rlang_1.0.4 googlesheets4_1.0.1 R.utils_2.12.0
#> [16] R.oo_1.25.0 rmarkdown_2.16 styler_1.7.0
#> [19] googledrive_2.0.0 munsell_0.5.0 broom_1.0.0
#> [22] compiler_4.2.0 modelr_0.1.9 xfun_0.32
#> [25] pkgconfig_2.0.3 htmltools_0.5.3 tidyselect_1.1.2
#> [28] fansi_1.0.3 crayon_1.5.1 tzdb_0.3.0
#> [31] dbplyr_2.2.1 withr_2.5.0 R.methodsS3_1.8.2
#> [34] grid_4.2.0 jsonlite_1.8.0 gtable_0.3.0
#> [37] lifecycle_1.0.1 DBI_1.1.3 magrittr_2.0.3
#> [40] scales_1.2.1 cli_3.3.0 stringi_1.7.8
#> [43] fs_1.5.2 xml2_1.3.3 ellipsis_0.3.2
#> [46] generics_0.1.3 vctrs_0.4.1 tools_4.2.0
#> [49] R.cache_0.16.0 glue_1.6.2 hms_1.1.2
#> [52] fastmap_1.1.0 yaml_2.3.5 colorspace_2.0-3
#> [55] gargle_1.2.0 rvest_1.0.3 knitr_1.40
#> [58] haven_2.5.1
devtools::install_github("cynkra/constructive")
#> Using github PAT from envvar GITHUB_PAT
#> Skipping install of 'constructive' from a github remote, the SHA1 (753b2da6) has not changed since last install.
#> Use `force = TRUE` to force installation
sanl_path <- file.path(Sys.getenv("FBEA_DATADIR"), "SANL/SANL_2020.xlsx")
raw <- suppressMessages(suppressWarnings(read_xlsx(sanl_path)))
one_dttm_of_sanl <- slice(raw, 1) %>%
select(11) %>%
rename_with(.fn = ~ "a_dttm")
ptype_sanl <- constructive::construct(one_dttm_of_sanl, max_atomic = 0)
sendToConsole(ptype_sanl)
#> Error in if (nzchar(tz)) attr(res, "tzone") <- tz :
#> argument is of length zero
Created on 2022-08-31 with reprex v2.0.2
We have this but it is not pretty :
library(constructive)
construct(ave, opts_function(environment = TRUE))
#> (function(x, ..., FUN = mean) {
#> if (missing(...)) x[] <- FUN(x) else {
#> g <- interaction(...)
#> split(x, g) <- lapply(split(x, g), FUN)
#> }
#> x
#> }) |>
#> match.fun("environment<-")(asNamespace("stats"))
This seems better
library(constructive)
construct(ave, opts_function(environment = TRUE))
#> (function(x, ..., FUN = mean) {
#> if (missing(...)) x[] <- FUN(x) else {
#> g <- interaction(...)
#> split(x, g) <- lapply(split(x, g), FUN)
#> }
#> x
#> }) |>
#> local(asNamespace("stats"))
This won't work with {magrittr} and maybe that's a bit surprising to those that don't know about the laziness of the base pipe, so we might have the following without pipe:
library(constructive)
construct(ave, opts_function(environment = TRUE))
#> local(
#> envir = asNamespace("stats"),
#> function(x, ..., FUN = mean) {
#> if (missing(...)) x[] <- FUN(x) else {
#> g <- interaction(...)
#> split(x, g) <- lapply(split(x, g), FUN)
#> }
#> x
#> }
#> )
Using options(error=)
we can automate the inspection that we might do with options(error = recover)
for each frame we check the inputs of the call and construct()
them. Then reproduce the error, the output is a knitted md report automatically open.
NSE is tricky but we can probably deal with most use case with some effort, we can use delayAssign and try to eval what we can.
Just like construct() does we can check that we reproduce the same error, first with SE and implementing heuristics if it doesn't do it.
Maybe better in another package since it is a bit different and needs new deps. Also since we can reproduce the inputs we can also use flow_run()
and {boomer} at each step and have a very detailed report of the error through different angles.
constructive::construct(data.frame(a = I(list(2))))
#> as.data.frame(tibble::tibble(a = I(list(2))))
in that case thanks to I()
we can use the data.frame()
constructor directly
constructive::construct(data.frame(
a = character()
))
#> data.frame(a = character(0)) |>
#> structure(row.names = integer(0))
Now we assume everything to be S3
con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")
x <- glue::glue_sql("a", .con = con)
constructive::construct(x)
#> "a" |>
#> structure(
#> class = "SQL" |>
#> structure(package = "DBI")
#> )
#> Error in `constructive::construct()`:
#> ! {constructive} couldn't create code that reproduces perfectly the output
#> `original` is an S4 object of class <SQL>
#> `recreated` is an S3 object of class <SQL>, a character vector
#> `original` is an S4 object of class <character>
#> `recreated` is an S3 object of class <SQL>, a character vector
#> `original` is an S4 object of class <vector>
#> `recreated` is an S3 object of class <SQL>, a character vector
#> `original` is an S4 object of class <data.frameRowLabels>
#> `recreated` is an S3 object of class <SQL>, a character vector
#> `original` is an S4 object of class <SuperClassMethod>
#> `recreated` is an S3 object of class <SQL>, a character vector
#> ℹ use `check = FALSE` to ignore this error
dput(x)
#> new("SQL", .Data = "a")
identical(x, new("SQL", .Data = "a"))
#> [1] TRUE
We cannot just fall back on dput though since it might be brittle/verbose on recursive elements.
Then of course, it would be nice to have constructors for main S4 classes, such as DBI::SQL()
(in other issues/PRs)
There are 2 ways to extend constructive :
(1) is already possible but we don't advertise it for v 0.1 because:
(2) is currently impossible without forking the repo or overriding the S3 method altogether, but with our options system we might be able to set something up with a new arg custom_constructor
for instance, which will take a function that will be used on the objects.
The author will need to understand the internals so we need doc, stability etc
-> 0.2
they're the parent from the namespace. We'd need parent.env(as.Namespace("graphics"))
to reproduce "imports::graphics"
We can test and confirm, in artificial cases when they woudldn't be we fall back on standard way.
obj <- attributes(data.table::data.table(a=1))
obj
#> $names
#> [1] "a"
#>
#> $row.names
#> [1] 1
#>
#> $class
#> [1] "data.table" "data.frame"
#>
#> $.internal.selfref
#> <pointer: 0x12380d4e0>
constructive::construct(obj)
#> Error in `constructive::construct()`:
#> ! The code built by {constructive} could not be parsed.
#> Caused by error in `parse_safely()`:
#> ! <text>:5:21: unexpected '<'
#> 4: class = c("data.table", "data.frame"),
#> 5: .internal.selfref = <
#> ^
Created on 2022-08-30 by the reprex package (v2.0.1)
I don't believe there's a base constructor (or even packaged ?) to create a "prototype" of a pointer so we cannot use the same type of tricks we used for environments.
I think the best we can do is ignore the pointer objects (replace by NULL
) and display a message that pointers have been ignored. It should still fail by default (check = TRUE
).
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.