Giter VIP home page Giter VIP logo

constructive's People

Contributors

andreranza avatar aviator-app[bot] avatar github-actions[bot] avatar krlmlr avatar maelle avatar moodymudskipper avatar olivroy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

constructive's Issues

! {constructive} could not build the requested code.

``` 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

construct_workspace

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.

:: sometimes not used as operator

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

Extension to new classes

Related to #65 and #64

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.

Crash when using NAs

Output but error

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(...)

Crash

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(...)

ISOdate and ISOdatetime constructors for dates ?

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

srcrefs

copied from #1 :

The main challenges come from :

  • objects that don't have idiomatic constructors (or have some constructors that shuffle arguments compared to how they're found when unseating complex objects)
  • Environments
  • Recursive environments

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.

language.semi_colon arg

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:

  • convince {rlang} to make rlang::expr_deparse() flexible : I don't see it
  • design our own deparser : I might have something in {doubt}, inspired from rlang, but that might be a rabbit hole (though we don't need to deal with inlined values).
  • build a hack around the output of expr_deparse() and deparse(), we need to parse quotes, backquotes, % and { and bind lines with closed everything except {, new lines in : That's hacky

note: 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
#>             ^

complex language object

For now we rely on rlang::express_deparse(), which is better than base::deparse() but insufficient nevertheless.

These seem like artificial cases but :

  • They're necessary for #31
  • The package might be used to debug complicated cases where dput or deparse are misleading, so extreme cases are not necessarily out of scope
# 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)

diff as repairing code

construct_repair <- function(x, ref, ...) {...}
#>  ref |>
#>    purrr::modify_in(...) |>
#>    purrr::modify_in(...) |>
#>  ...

This is useful to :

  • debug, especially for big objects that might be corrupted by small changed.
  • build reproducible data for tests
  • reproduce bugs

{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.

  • If nested element doesn't exist compare to ref, create
  • If nested element doesn't exist in ref, remove
  • If nested element is different and is a atomic, replace
  • If nested element is different and is a list
    • if length < n , recurse
    • else replace

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 :

  • I am building a test in the Zurich project
  • I put a browser call in the function to test so I can grab a realistic object for the test
  • The object is huge however (quote as nt), constructing it would take 100s of lines, but is a variation of an object I can easily build
  • building this object from known input using waldo would be tedious
  • This object as a repaired version of my input quote might be written in maybe 10 lines and would tell me a lot about the history of this object

Alternative way of passing arguments to methods

@krlmlr made an interesting suggestion

Current implementation and ideas in #34 pose a few challenges :

  • Documentation is awkward
  • We sometimes have a lot of arguments to pass to the next ellipsis, it looks bad, lengthen the code and is error prone
  • Argument names are prefixed, it's not super pretty
  • no autocomplete
  • no easy way to store a constructive "template"

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 :

  • nested function calls (means if package not attached needs constructive:: several times)
  • we need to find cute names for these functions
  • We'd need extra work for NSE (but we don't use it/need it here)

The upside seems bigger though.

pass `opts` rather than dots

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

environments constructed first

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.

argument documentation and conventions

  • We document all construct_idiomatic() methods on a single page, even though they (and their generic) are not exported.
  • All arguments specific to these methods start with the method's class name
  • The latter can inherit from some top level args
  • This will give us a neat long list of arguments in alphabetical order
  • We can afford to offer much more customisation without having bloated function signatures
  • The downside is no autocomplete, but anyway its utility is mitigated when many args

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 etc
  • max_list leaves top level and becomes list.max
  • max_body leaves top level and becomes function.max
  • We can have a max argument that they all inherit from
  • env_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.

Better information on reconstruction issues

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

srcref not always used

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

fallback system

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

check = NULL

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 let

Construct to file

dput 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.

  • sourcing a file with unnamed input is like reading a RDS file : foo <- source(path) (assuming source() return latest statement ?)
  • sourcing a file with named inputs is like reading a rda file

handle conflicts in data

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.

Better code structure

Now we have :

  • construct() calls try_construct()
  • try_construct() is a small wrapper around construct_raw()
  • construct_raw() calls generics construct_idiomatic() and repair_attributes()

This would be nice but I don't think we can do it:

  • inline the try_construct() wrapper
  • move repair code inside constructors
  • rename construct_idiomatic.* to construct.*
  • replace calls to construct_raw with 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 :

  • move repair code inside constructors
  • rename construct_idiomatic.* to construct.*
  • and have construct_raw be the generic containing UseMethod("construct")

Have to think about this some more -> 0.2

Custom constructors

We might need a bit more experience with the API, but as I see it now :

  • custom constructors are functions that can be passed instead of the strings used for predefined options, it works well with #64
  • construct_raw becomes a generic, and we have construct_raw methods that do idiomatic construction AND repair step
  • constructors for a given class are implemented in separate functions, for clarity of code but also so users can have good templates to start from. Then the construct_raw method is mainly a switch call, or a call to the custom constructor if provided constructor is a function and not a string.
  • We need to export helper for custom constructor design, such as 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 ?

shorthands for options

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.

Losing argument names in `[` call

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

Return rich object + print method

We want to return a list with a class "constructive"
It prints as it does now.
It contains :

  • a code item (character vector)
  • a waldo_compare item that contains the result of the comparison

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.

Evaluate constructed code in correct env

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>

Rename main branch

via usethis::git_default_branch_rename() . For consistency with other repositories in this organization.

The output of `construct(a_tibble, max_atomic = 0)` is not entirely correct for columns of class "POSIXct"

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?

Should we not rely on styler for indention ?

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.

Active bindings

Low priority

  • Active bindings themselves are a bit hard to explore, we have to know about activeBindingFunction() and then explore environments, {constructive} might help
  • The way we reproduce environments is by converting them to lists, this destroys active bindings

In practice we care about those very little but putting it out there for reference/discussion.

Error when `is.infinite()`

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.

improved behaviour for max_atomic and max_list ?

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 NAs 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.

detect circularity when constructing an environment

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.

Fix test regarding dm

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.

constructor for quo

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"))

The `max_atomic` arg might generate failing code

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

Consider using `eval()` or `local()` for "function" constructor

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
#>   }
#> )

error reprex

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.

  • default path to temp file
  • default format to md but Cmd and html possible
  • open defaults to TRUE

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.

S4 objects

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)

Providing custom constructors for existing classes

There are 2 ways to extend constructive :

  • (1) Providing methods for new classes
  • (2) Providing new constructors for existing classes

(1) is already possible but we don't advertise it for v 0.1 because:

  • takes time to document it
  • we need to be more sure about internal API stability, which might be simplified

(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

support imports::pkg envs

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.

deal with pointer objects

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).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.