├── .Rbuildignore ├── .gitignore ├── .travis.yml ├── DESCRIPTION ├── NAMESPACE ├── R ├── globals.R ├── helpers.R ├── pipe.R └── zz.R ├── README.Rmd ├── README.md ├── benchmark └── benchmarks.R ├── codecov.yml ├── fastpipe.Rproj ├── man ├── is_fastpipe.Rd ├── pipes.Rd └── print.fastpipe.Rd └── tests ├── testthat.R └── testthat ├── Rplots.pdf ├── test-anonymous-functions.r ├── test-compound.R ├── test-fseq.r ├── test-github.R ├── test-multiple-arguments.r ├── test-pipe2.R ├── test-single-argument.r └── test-tee.r /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^fastpipe\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^README\.Rmd$ 4 | ^\.travis\.yml$ 5 | ^codecov\.yml$ 6 | ^benchmark$ 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # R for travis: see documentation at https://docs.travis-ci.com/user/languages/r 2 | 3 | language: R 4 | cache: packages 5 | after_success: 6 | - Rscript -e 'covr::codecov()' 7 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: fastpipe 2 | Title: A fast pipe implementation 3 | Version: 0.0.0.9000 4 | Authors@R: 5 | c(person(given = "Antoine", 6 | family = "Fabri", 7 | role = c("aut", "cre"), 8 | email = "antoine.fabri@gmail.com"), 9 | person(given = "Stefan Milton", 10 | family = "Bache", 11 | role = "ctb", 12 | email = "stefan@stefanbache.dk"), 13 | person(given = "Hadley", 14 | family = "Wickham", 15 | role = "ctb", 16 | email = "hadley@rstudio.com")) 17 | Description: We propose an implementation entirely compatible with magrittr but faster, more consistent, and solving most of its current issues. 18 | License: GPL-3 19 | Encoding: UTF-8 20 | LazyData: true 21 | Suggests: 22 | testthat (>= 2.1.0), 23 | covr, 24 | rlang 25 | RoxygenNote: 6.1.1 26 | Roxygen: list(markdown = TRUE) 27 | URL: https://github.com/moodymudskipper/fastpipe 28 | BugReports: https://github.com/moodymudskipper/fastpipe/issues 29 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,fastpipe) 4 | export("%$%") 5 | export("%$>>%") 6 | export("%<>%") 7 | export("%>%") 8 | export("%>>%") 9 | export("%L>%") 10 | export("%L>>%") 11 | export("%S>%") 12 | export("%S>>%") 13 | export("%T>%") 14 | export("%T>>%") 15 | export(is_fastpipe) 16 | -------------------------------------------------------------------------------- /R/globals.R: -------------------------------------------------------------------------------- 1 | 2 | globals <- new.env() 3 | master_off <- function() globals$master <- FALSE 4 | fs_on <- function() globals$is_fs <- TRUE 5 | compound_on <- function() globals$is_compound <- TRUE 6 | 7 | reset_globals <- function(){ 8 | globals[["master"]] <- TRUE 9 | globals[["is_fs"]] <- FALSE 10 | globals[["is_compound"]] <- FALSE 11 | globals[["compound_lhs"]] <- NULL 12 | } 13 | 14 | set_compound_lhs <- function(lhs){ 15 | globals$compound_lhs <- lhs 16 | } 17 | 18 | reset_globals() 19 | -------------------------------------------------------------------------------- /R/helpers.R: -------------------------------------------------------------------------------- 1 | # to avoid notes on CMD CHECK 2 | `*RETURNED_CALL*` <- NULL 3 | `*RHS_CALL*` <- NULL 4 | 5 | standard_pipe_template <- function(lhs, rhs) { 6 | # mark the entrance in the pipe 7 | if(globals$master) { 8 | sc <- sys.call() 9 | # flips master switches, run back the call, and sort out the output 10 | return(eval_slaves(sc, parent.frame())) 11 | } 12 | 13 | lhs_call <- substitute(lhs) 14 | rhs_call <- `*RHS_CALL*` 15 | 16 | # initiate f_seq 17 | if(lhs_call == quote(.)) { 18 | bare_pipe <- attr(sys.function(), "bare_version") 19 | res <- call(bare_pipe, lhs_call, rhs_call) 20 | fs_on() 21 | return(res) 22 | } 23 | force(lhs) 24 | if(globals$is_fs) { 25 | bare_pipe <- attr(sys.function(), "bare_version") 26 | res <- call(bare_pipe, lhs, rhs_call) 27 | return(res) 28 | } 29 | 30 | `*RETURNED_CALL*` 31 | } 32 | 33 | bare_pipe_template <- function(lhs, rhs) { 34 | rhs_call <- substitute(rhs) 35 | `*RETURNED_CALL*` 36 | } 37 | 38 | 39 | eval_slaves <- function(sc, env){ 40 | master_off() 41 | on.exit(reset_globals()) 42 | res <- eval(sc, env) 43 | if(globals$is_compound) { 44 | 45 | res <- eval(bquote(.(globals$compound_lhs) <- .(res)),env) 46 | return(invisible(res)) 47 | } 48 | if(! globals$is_fs) 49 | return(res) 50 | res <- as.function(c(alist(.=),res),envir = env) 51 | return(res) 52 | } 53 | 54 | insert_dot <- function(expr, special_cases = TRUE) { 55 | if(is.symbol(expr) || expr[[1]] == quote(`(`)) { 56 | # if a symbol or an expression inside parentheses, make it a call with dot arg 57 | expr <- as.call(c(expr, quote(`.`))) 58 | } else if(length(expr) ==1) { 59 | # if a call without arg, give it a dot arg 60 | expr <- as.call(c(expr[[1]], quote(`.`))) 61 | } else if(special_cases && ( 62 | expr[[1]] == quote(`$`) || 63 | expr[[1]] == quote(`::`) || 64 | expr[[1]] == quote(`:::`))) { 65 | # deal with special cases of infix operators 66 | expr <- as.call(c(expr, quote(`.`))) 67 | } else if (expr[[1]] != quote(`{`) && 68 | all(sapply(expr[-1], `!=`, quote(`.`))) && 69 | all(sapply(expr[-1], `!=`, quote(`!!!.`)))) { 70 | # if a call with args but no dot in arg, insert one first 71 | expr <- as.call(c(expr[[1]], quote(`.`), as.list(expr[-1]))) 72 | } 73 | expr 74 | } 75 | 76 | build_pipes <- function (root_name, rhs_call, returned_call) { 77 | # standard pipe 78 | standard_pipe_nm <- paste0("%",root_name,">%") 79 | standard_pipe <- standard_pipe_template 80 | body(standard_pipe) <- do.call(substitute, list(body(standard_pipe), list( 81 | `*RHS_CALL*` = substitute(rhs_call), 82 | `*RETURNED_CALL*` = substitute(returned_call)))) 83 | 84 | # bare pipe 85 | bare_pipe_nm <- paste0("%",root_name,">>%") 86 | bare_pipe <- bare_pipe_template 87 | body(bare_pipe) <- do.call(substitute, list(body(bare_pipe), list( 88 | `*RETURNED_CALL*` = substitute(returned_call)))) 89 | 90 | # add bare pipe as attribute of standard pipe, for construction of functional sequence 91 | attr(standard_pipe, "bare_version") <- bare_pipe_nm 92 | assign(standard_pipe_nm, standard_pipe, envir= parent.frame()) 93 | assign(bare_pipe_nm, bare_pipe, envir= parent.frame()) 94 | } 95 | -------------------------------------------------------------------------------- /R/pipe.R: -------------------------------------------------------------------------------- 1 | 2 | #' Pipes 3 | #' 4 | #' Equivalents to magrittr's pipes, plus some additions : 5 | #' * The `%S>%` pipe is like `%>%` except it supports using `!!!` in any function. 6 | #' * The `%L>%` pipe is like `%>%` except it logs to the console the call and the execution time. 7 | #' * The `%\*>>%` family of pipes contains equivalent that go faster because they 8 | #' don't support functional chains (`. %>% foo() %>% bar()` nor the compound pipe (`%<>%`). 9 | #' 10 | #' @param lhs A value or a dot (`.`). 11 | #' @param rhs A function call using pipe semantics of the relevant pipe. 12 | #' @name pipes 13 | NULL 14 | 15 | #' @export 16 | #' @rdname pipes 17 | #' @inheritParams pipes 18 | #' @export 19 | `%<>%` <- function(lhs, rhs){ 20 | if(substitute(lhs) == quote(.)) 21 | stop("You can't start a functional sequence on a compound operator") 22 | 23 | # check also if we have a pipe further left 24 | # ... 25 | lhs_call <- substitute(lhs) 26 | if(length(lhs_call) == 3 && is_fastpipe(eval(lhs_call[[1]]))) 27 | stop("A compound pipe should only be used at the start of the chain") 28 | 29 | # if it's the main pipe 30 | if(globals$master) 31 | return(invisible(eval.parent(substitute(lhs <- lhs %>% rhs)))) 32 | 33 | res <- eval.parent(substitute(LHS %>>% RHS, list( 34 | LHS = lhs_call, RHS = insert_dot(substitute(rhs))))) 35 | compound_on() 36 | set_compound_lhs(lhs_call) 37 | res 38 | } 39 | 40 | # %>% 41 | build_pipes( 42 | "", 43 | rhs_call = 44 | insert_dot(substitute(rhs)), 45 | returned_call = 46 | eval(rhs_call, envir = list(`.` = lhs), enclos = parent.frame()) 47 | ) 48 | 49 | # %L>% 50 | build_pipes( 51 | "L", 52 | rhs_call = 53 | insert_dot(substitute(rhs)), 54 | returned_call = { 55 | cat(paste(deparse(rhs_call), collapse = "\n"), " ...\n") 56 | cat("~ ", system.time( 57 | res <- eval(rhs_call, envir = list(`.` = lhs), enclos = parent.frame()))[3], 58 | "sec\n") 59 | res 60 | } 61 | ) 62 | 63 | # %T>% 64 | build_pipes( 65 | "T", 66 | rhs_call = 67 | insert_dot(substitute(rhs)), 68 | returned_call = 69 | { 70 | eval(rhs_call, envir = list(`.` = lhs), enclos = parent.frame()) 71 | lhs 72 | } 73 | ) 74 | 75 | # %$>% 76 | build_pipes( 77 | "$", 78 | rhs_call = 79 | substitute(rhs), 80 | returned_call = 81 | eval(bquote(with(.,.(rhs_call))), envir = list(`.` = lhs), enclos = parent.frame()) 82 | ) 83 | 84 | # %S>% 85 | build_pipes( 86 | "S", 87 | rhs_call = 88 | insert_dot(substitute(rhs)), 89 | returned_call = 90 | { 91 | if(!requireNamespace("rlang")) 92 | stop("You need to have the package 'rlang' installed to use `%S>%` or `%S>>%`.") 93 | # splice 94 | rhs_call <- substitute(rlang::expr(rhs),list(rhs=rhs_call)) 95 | rhs_call <- eval(rhs_call, envir = list(`.` = lhs), enclos = parent.frame()) 96 | # eval 97 | eval(rhs_call, envir = list(`.` = lhs), enclos = parent.frame()) 98 | } 99 | ) 100 | 101 | #' @export 102 | #' @rdname pipes 103 | #' @inheritParams pipes 104 | `%>%` <- `%>%` 105 | 106 | #' @export 107 | #' @rdname pipes 108 | #' @inheritParams pipes 109 | `%>>%` <- `%>>%` 110 | 111 | #' @export 112 | #' @rdname pipes 113 | #' @inheritParams pipes 114 | `%T>%` <- `%T>%` 115 | 116 | #' @export 117 | #' @rdname pipes 118 | #' @inheritParams pipes 119 | `%T>>%` <- `%T>>%` 120 | 121 | #' @export 122 | #' @rdname pipes 123 | #' @inheritParams pipes 124 | `%$%` <- `%$>%` 125 | 126 | #' @export 127 | #' @rdname pipes 128 | #' @inheritParams pipes 129 | `%$>>%` <- `%$>>%` 130 | 131 | #' @export 132 | #' @rdname pipes 133 | #' @inheritParams pipes 134 | `%S>%` <- `%S>%` 135 | 136 | #' @export 137 | #' @rdname pipes 138 | #' @inheritParams pipes 139 | `%S>>%` <- `%S>>%` 140 | 141 | #' @export 142 | #' @rdname pipes 143 | #' @inheritParams pipes 144 | `%L>%` <- `%L>%` 145 | 146 | #' @export 147 | #' @rdname pipes 148 | #' @inheritParams pipes 149 | `%L>>%` <- `%L>>%` 150 | 151 | 152 | 153 | class(`%>%`) <- class(`%T>%`) <- class(`%$%`) <- class(`%S>%`) <- class(`%L>%`) <- 154 | class(`%>>%`) <- class(`%T>>%`) <- class(`%$>>%`) <- class(`%S>>%`) <- class(`%L>>%`) <- 155 | "fastpipe" 156 | 157 | #' Print a fastpipe object 158 | #' 159 | #' @param x object to print 160 | #' @param ... Ignored, kept for compatibility with other methods 161 | #' @export 162 | print.fastpipe <- function(x, ...){ 163 | cat("# a fastpipe object\n") 164 | 165 | print(`attributes<-`(x, NULL)) 166 | bv <- attr(x,"bare_version") 167 | if(!is.null(bv)) 168 | cat("# Bare version: `", bv, "`", sep="") 169 | invisible(x) 170 | } 171 | 172 | 173 | #' Test if Object is a fastpipe 174 | #' 175 | #' @param x object to test 176 | #' 177 | #' @return a length one logical 178 | #' @export 179 | is_fastpipe <- function(x) { 180 | inherits(x, "fastpipe") 181 | } 182 | 183 | -------------------------------------------------------------------------------- /R/zz.R: -------------------------------------------------------------------------------- 1 | .onLoad <- function(libname, pkgname){ 2 | pkgs <- c( 3 | "magrittr","dplyr","purrr", "tidyr", "stringr", "forcats", "rvest", 4 | "modelr", "testthat") 5 | for(pkg in pkgs){ 6 | setHook(packageEvent(pkg, "attach"), 7 | function(...) fastpipe_first()) 8 | } 9 | } 10 | 11 | fastpipe_first <- function(){ 12 | detach("package:fastpipe") 13 | library(fastpipe, warn.conflicts = FALSE, quietly = TRUE ) 14 | } 15 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | [![Travis build status](https://travis-ci.org/moodymudskipper/fastpipe.svg?branch=master)](https://travis-ci.org/moodymudskipper/fastpipe) 6 | [![Codecov test coverage](https://codecov.io/gh/moodymudskipper/fastpipe/branch/master/graph/badge.svg)](https://codecov.io/gh/moodymudskipper/fastpipe?branch=master) 7 | 8 | 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | fig.path = "man/figures/README-", 15 | out.width = "100%" 16 | ) 17 | ``` 18 | 19 | # fastpipe 20 | 21 | This package proposes an alternative to the pipe from the *magrittr* package. 22 | 23 | It's named the same and passes all the *magrittr* test so can easily be a drop-in 24 | replacement, its main advantages is that it is faster and solves most of the issues 25 | of *magrittr*. 26 | 27 | 28 | Install with : 29 | 30 | ``` r 31 | remotes::install_github("moodymudskipper/fastpipe") 32 | ``` 33 | 34 | ## Issues solved and special features 35 | 36 | *fastpipe* passes all *magrittr*'s tests, but it doesn't stop there and solves 37 | most of its issues, at least most of the open issues on GitHub. 38 | 39 | ```{r, include = FALSE} 40 | library(fastpipe) 41 | ``` 42 | 43 | 44 | * Lazy evaluation 45 | 46 | ```{r} 47 | # https://github.com/tidyverse/magrittr/issues/195 48 | # Issue with lazy evaluation #195 49 | gen <- function(x) {function() eval(quote(x))} 50 | identical( 51 | {fn <- gen(1); fn()}, 52 | {fn <- 1 %>% gen(); fn()}) 53 | ``` 54 | 55 | ``` {r, eval = FALSE} 56 | # https://github.com/tidyverse/magrittr/issues/159 57 | # Pipes of functions have broken environments 58 | identical( 59 | { 60 | compose <- function(f, g) { function(x) g(f(x)) } 61 | plus1 <- function(x) x + 1 62 | compose(plus1, plus1)(5)}, 63 | { 64 | plus2 <- plus1 %>% compose(plus1) 65 | plus2(5) 66 | }) 67 | #> [1] TRUE 68 | # Note : copy and pasted because didn't work with knitr 69 | ``` 70 | 71 | * It considers `!!!.` as `.` when deciding wether to insert a dot 72 | 73 | This was requested by Lionel and seems fairly reasonable as it's is unlikely that 74 | a user will need to use both `.` and `!!!.` in a call. 75 | 76 | ```{r} 77 | letters[1:3] %>% rlang::list2(!!!.) 78 | ``` 79 | 80 | * `:::`, `::` and `$` get a special treatment to solve a common issue 81 | 82 | ```{r} 83 | iris %>% base::dim 84 | iris %>% base:::dim 85 | x <- list(y = dim) 86 | iris %>% x$y 87 | ``` 88 | 89 | * It fails explicitly in some cases rather than allowing strange behavior silently 90 | 91 | ```{r, error = TRUE} 92 | iris %>% head %<>% dim 93 | . %<>% head 94 | ``` 95 | 96 | 97 | * The new pipe `%S>%` allows the use of *rlang*'s `!!!` operator to splice dots in 98 | any function. 99 | 100 | ```{r} 101 | c(a = 1, b = 2) %S>% data.frame(!!!.) 102 | ``` 103 | 104 | * The new pipe `%L>%` behaves like `%>%` except that it logs to the console the 105 | calls and the time they took to run. 106 | 107 | ```{r} 108 | 1000000 %L>% rnorm() %L>% sapply(cos) %>% max 109 | ``` 110 | 111 | ## Families of pipes and performance 112 | 113 | 114 | * A `%>%` family of pipes reproduce offered by *magrittr* faster and more robustly, 115 | and extends some features. 116 | * A `%>>%` family of pipes, 117 | offer very fast alternatives, provided the dots are provided explicitly on the 118 | rhs. It doesn't support functional sequences (`. %>% foo()`) nor compound assignment (`bar %<>% baz()`). We call them *bare pipes*. 119 | 120 | We provide a benchmark below, the last value is given for context, to remember 121 | that we are discussing small time intervals. 122 | 123 | ```{r} 124 | `%.%` <- fastpipe::`%>%` # (Note that a *fastpipe*, unlike a *magrittr* pipe, can be copied) 125 | `%>%` <- magrittr::`%>%` 126 | bench::mark(check=F, 127 | 'magrittr::`%>%`' = 128 | 1 %>% identity %>% identity() %>% (identity) %>% {identity(.)}, 129 | 'fastpipe::`%>%`' = 130 | 1 %.% identity %.% identity() %.% (identity) %.% {identity(.)}, 131 | 'fastpipe::`%>>%`' = 132 | 1 %>>% identity(.) %>>% identity(.) %>>% identity(.) %>>% identity(.), 133 | base = identity(identity(identity(identity(1)))), 134 | `median(1:3)` = median(1:3) 135 | ) 136 | rm(`%>%`) # reseting `%>%` to fastpipe::`%>%` 137 | ``` 138 | 139 | We see that our `%>%` pipe is twice faster, and that our `%>>%` pipe 140 | is 10 times faster (on a properly formatted call). 141 | 142 | We'd like to stress that a pipe is unlikely to be the performance bottleneck in 143 | a script or a function. If optimal performance is 144 | critical, pipes are best avoided as they will always have an overhead. 145 | 146 | In other cases `%>>%` is fast and robust to program and keep the main benefit of piping. 147 | 148 | ## Implementation 149 | 150 | The implementation is completely different from *magrittr*, we can sum it up as 151 | follow : 152 | 153 | * `%>>%` pipes : evaluate *rhs* in parent environment, overriding `.` with the value 154 | of the *lhs*. Its code is extremely straightforward. 155 | 156 | ```{r} 157 | `%>>%` 158 | ``` 159 | 160 | 161 | * `%>%` pipes : 162 | * Use heuristics to insert dots, so `x %>% head` becomes `x %>% head(.)` 163 | * support functionnal sequences, so pipe chains starting with `.` are functions 164 | * support compound assignment, so pipe chains starting with `%<>%` assign in 165 | place to the lhs 166 | 167 | To support the two latter, we use global variables, stored in `globals`, a child 168 | environment of our package's environment. It contains the values `master`, `is_fs` 169 | and `is_compound`. Outside of pipes master is always `TRUE` while the two latter 170 | are always `FALSE`. The alternative to global parameters was to play with 171 | classes and attributes but was slower and more complex. 172 | 173 | Whenever we enter a pipe we check the value of `globals$master`. 174 | If it is `TRUE` we enter a sequence where : 175 | 176 | * We set `master` to `FALSE`, and use `on.exit()` to setup the restoration of 177 | global parameters to their default value at the end of the call. 178 | * We reexecute the whole call (which now won't enter this sequence) 179 | * If no `.` nor `%<>%` are hit in the call recursion we just evaluate a sequence 180 | of calls as described by `%>>%`'s code, expliciting the implicit dots as we go. 181 | * If the chain starts with `.` 182 | * We switch on `is_fs` 183 | * When `is_fs` is switched on we return the original call, unevaluated, subtituting 184 | the pipe by it's bare version (the name of the bare version is stored as 185 | an attribute of the pipe) and adding explicit dots (so `. %>% head(2)` 186 | becomes `. %>>% head(.,2)`). 187 | * It works recursively and we end up with a quoted pipe chain of bare pipes 188 | starting with a dot 189 | * If the chain starts with a `%<>%` call: 190 | * We switch on `is_compound` 191 | * We define a global variable named `compound_lhs`, which contains 192 | the quoted lhs of the `%<>%` call 193 | * We reevaluate the `%<>%` call, substituting `%<>%` by `%>>%` 194 | * Then consequent pipe calls are recognized as standard 195 | * Once we get the result: 196 | * If `is_fs` was switched on we wrap our quoted pipe in a function and return it 197 | * If `is_compound` was switched on we assign in the calling environment the 198 | result to the expression stored in `global$compoud_lhs` 199 | * If not any of those we just return the result 200 | * Default global values are restored, thanks to our `on.exit()` call 201 | 202 | *fastpipe* operators contain their own code while in *magrittr*'s current 203 | implementation they all have the main code and are recognized in the function 204 | by their name. Moreover the pipe names are not hardcoded in the functions, which 205 | prevents confusion like the fact that `%$%` still sometimes work in *magrittr* 206 | when only `%>%` is reexported. https://github.com/tidyverse/magrittr/issues/194 207 | 208 | The pipes have a class and a printing methods, the functional sequences don't. 209 | 210 | ```{r} 211 | `%T>>%` 212 | . %>% sin %>% cos() %T>% tan(.) 213 | ``` 214 | 215 | ## Benchmarks for functional sequences 216 | 217 | An interesting and little known fact is that using functional sequences in functionals 218 | is much more efficient than using lambda functions calling pipes (though it will 219 | generally be largely offset by the content of the fonction), for instance : 220 | `purrr::map(foo, ~ .x %>% bar %>% baz)` is slower than 221 | `purrr::map(foo, . %>% bar %>% baz)`. I tried to keep this nice feature but 222 | didn't succeed to make it as efficient as in *magrittr*. The difference show only 223 | for large number of iteration and will probably be negligible in any realistic loop. 224 | 225 | ```{r} 226 | `%.%` <- fastpipe::`%>%` 227 | `%>%` <- magrittr::`%>%` 228 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 229 | # functional sequence with 5 iterations, equivalent speed 230 | library(rlang) # to call `as_function()` 231 | bench::mark(check=F, 232 | rlang_lambda = 233 | vapply(1:5, as_function(~.x %>% identity %>% identity() %>% (identity) %>% {identity(.)}), integer(1)), 234 | fastpipe = 235 | vapply(1:5, . %.% identity %.% identity() %.% (identity) %.% {identity(.)}, integer(1)), 236 | magrittr = 237 | vapply(1:5, . %>% identity %>% identity() %>% (identity) %>% {identity(.)}, integer(1)), 238 | base = 239 | vapply(1:5, function(x) identity(identity(identity(identity(x)))) , integer(1)), 240 | `median(1:3)` = median(1:3) 241 | ) 242 | 243 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 244 | # functional sequence with 1000 iterations 245 | bench::mark(check=F, 246 | rlang_lambda = 247 | vapply(1:1000, as_function(~.x %>% identity %>% identity() %>% (identity) %>% {identity(.)}), integer(1)), 248 | fastpipe = 249 | vapply(1:1000, . %.% identity %.% identity() %.% (identity) %.% {identity(.)}, integer(1)), 250 | magrittr = 251 | vapply(1:1000, . %>% identity %>% identity() %>% (identity) %>% {identity(.)}, integer(1)), 252 | base = 253 | vapply(1:1000, function(x) identity(identity(identity(identity(x)))) , integer(1)) 254 | ) 255 | ``` 256 | 257 | ## Breaking changes 258 | 259 | The fact that the tests of *magrittr* are passed by *fastpipe* doesn't mean 260 | that *fastpipe* will necessarily behave as expected by *magrittr*'s users in 261 | all cicumstances. 262 | 263 | Here are the main such cases : 264 | 265 | * In *pastpipe* functional sequences don't have a class and are not subsetable. 266 | This is a feature that Stefan Bache wanted to weed out of his package, but can 267 | still break some code. 268 | 269 | * We said earlier that the instances of using both `!!!.` and `.` are expected 270 | to be rare, but they happen nonetheless. The call `mtcars[8:11] %>% dplyr::count(!!!.)` 271 | works with *magrittr* but with *fastpipe* we need `mtcars[8:11] %>% dplyr::count(., !!!.)`. 272 | 273 | * If `%T>%` or `%$%` are not reexported by a package which reexports `%>%`, 274 | they will not be usable at all, while at the moment after attaching dplyr you can 275 | do `cars %$% speed %>% head(2)` even if `%$%` is nowhere to be found. It's a feature 276 | of *fastpipe* rather than a bug but will still break the code of those who forgot 277 | to attach *magrittr* and had their code work by luck. 278 | 279 | * If reexporting `%>%` from *fastpipe*, one must also reexport `%>>%` as it is 280 | used by functional sequences. 281 | 282 | * If you do strange things like ``cars %>% `$`("spee")`` it won't work 283 | with *fastpipe* because `$`, `::` and `:::` are special cases and *fastpipe* 284 | would basically try to run `'$'("spee",)(cars)` and choke because there'd be 285 | no second argument to `$`. 286 | 287 | ## Notes 288 | 289 | * The package *magrittr* was created by Stefan Milton Bache and Hadley Wickham. 290 | The design of the pipe's interface and most of the testing code of this package 291 | is their work or the work of other *magrittr* contributors, while none of the 292 | remaining code is. 293 | * The package is young and might still change. 294 | * *magrittr*'s pipe is widespread and reexported by prominent *tidyverse* 295 | packages. It could have been annoying if they masked *fastpipe* 's operator(s) 296 | each time so I set a hook so that whenever one of those prominent packages is 297 | attached, *fastpipe* will be detached and reattached at the end of 298 | the search path. This assumes that if you attach *fastpipe* you want to use 299 | its pipes by default, which I think is reasonable. A message makes it explicit, 300 | for instance : 301 | 302 | > Attaching package: ‘dplyr’ 303 | > 304 | > The following object is masked \_by\_ ‘package:fastpipe’: 305 | > 306 | > %>% 307 | 308 | The current list of the packages subjects to this hook is : *magrittr*, *dplyr*, *purrr*, *tidyr*, *stringr*, *forcats*, *rvest*, *modelr*, *testthat*. However many more packages reexport 309 | magrittr's pipe and the safest way to make sure you're using *fastpipe* is to 310 | attach it after all other packages. 311 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Travis build 3 | status](https://travis-ci.org/moodymudskipper/fastpipe.svg?branch=master)](https://travis-ci.org/moodymudskipper/fastpipe) 4 | [![Codecov test 5 | coverage](https://codecov.io/gh/moodymudskipper/fastpipe/branch/master/graph/badge.svg)](https://codecov.io/gh/moodymudskipper/fastpipe?branch=master) 6 | 7 | 8 | # fastpipe 9 | 10 | > DISCLAIMER : There was a big oversight and I'm a bit embarrassed about it, nested pipe calls such as `cars %>% summarize_all(. %>% mean)` fail. I'll see if I can fix this but meanwhile I must admit this is makes the package much less robust that I'd like it to be! 11 | > `%>>%` works fine, nested or not. The moral of the story is there's a reason why one wants to avoid playing with global variables. 12 | 13 | This package proposes an alternative to the pipe from the *magrittr* 14 | package. 15 | 16 | It’s named the same and passes all the *magrittr* test so can easily be 17 | a drop-in replacement, its main advantages is that it is faster and 18 | solves most of the issues of *magrittr*. 19 | 20 | Install with : 21 | 22 | ``` r 23 | remotes::install_github("moodymudskipper/fastpipe") 24 | ``` 25 | 26 | ## Issues solved and special features 27 | 28 | *fastpipe* passes all *magrittr*’s tests, but it doesn’t stop there and 29 | solves most of its issues, at least most of the open issues on GitHub. 30 | 31 | - Lazy evaluation 32 | 33 | 34 | 35 | ``` r 36 | # https://github.com/tidyverse/magrittr/issues/195 37 | # Issue with lazy evaluation #195 38 | gen <- function(x) {function() eval(quote(x))} 39 | identical( 40 | {fn <- gen(1); fn()}, 41 | {fn <- 1 %>% gen(); fn()}) 42 | #> [1] TRUE 43 | ``` 44 | 45 | ``` r 46 | # https://github.com/tidyverse/magrittr/issues/159 47 | # Pipes of functions have broken environments 48 | identical( 49 | { 50 | compose <- function(f, g) { function(x) g(f(x)) } 51 | plus1 <- function(x) x + 1 52 | compose(plus1, plus1)(5)}, 53 | { 54 | plus2 <- plus1 %>% compose(plus1) 55 | plus2(5) 56 | }) 57 | #> [1] TRUE 58 | # Note : copy and pasted because didn't work with knitr 59 | ``` 60 | 61 | - It considers `!!!.` as `.` when deciding wether to insert a dot 62 | 63 | This was requested by Lionel and seems fairly reasonable as it’s is 64 | unlikely that a user will need to use both `.` and `!!!.` in a call. 65 | 66 | ``` r 67 | letters[1:3] %>% rlang::list2(!!!.) 68 | #> [[1]] 69 | #> [1] "a" 70 | #> 71 | #> [[2]] 72 | #> [1] "b" 73 | #> 74 | #> [[3]] 75 | #> [1] "c" 76 | ``` 77 | 78 | - `:::`, `::` and `$` get a special treatment to solve a common issue 79 | 80 | 81 | 82 | ``` r 83 | iris %>% base::dim 84 | #> [1] 150 5 85 | iris %>% base:::dim 86 | #> [1] 150 5 87 | x <- list(y = dim) 88 | iris %>% x$y 89 | #> [1] 150 5 90 | ``` 91 | 92 | - It fails explicitly in some cases rather than allowing strange 93 | behavior silently 94 | 95 | 96 | 97 | ``` r 98 | iris %>% head %<>% dim 99 | #> Error in iris %>% head %<>% dim: A compound pipe should only be used at the start of the chain 100 | . %<>% head 101 | #> Error in . %<>% head: You can't start a functional sequence on a compound operator 102 | ``` 103 | 104 | - The new pipe `%S>%` allows the use of *rlang*’s `!!!` operator to 105 | splice dots in any function. 106 | 107 | 108 | 109 | ``` r 110 | c(a = 1, b = 2) %S>% data.frame(!!!.) 111 | #> a b 112 | #> 1 1 2 113 | ``` 114 | 115 | - The new pipe `%L>%` behaves like `%>%` except that it logs to the 116 | console the calls and the time they took to run. 117 | 118 | 119 | 120 | ``` r 121 | 1000000 %L>% rnorm() %L>% sapply(cos) %>% max 122 | #> rnorm(.) ... 123 | #> ~ 0.26 sec 124 | #> sapply(., cos) ... 125 | #> ~ 2.11 sec 126 | #> [1] 1 127 | ``` 128 | 129 | ## Families of pipes and performance 130 | 131 | - A `%>%` family of pipes reproduce offered by *magrittr* faster and 132 | more robustly, and extends some features. 133 | - A `%>>%` family of pipes, offer very fast alternatives, provided the 134 | dots are provided explicitly on the rhs. It doesn’t support 135 | functional sequences (`. %>% foo()`) nor compound assignment (`bar 136 | %<>% baz()`). We call them *bare pipes*. 137 | 138 | We provide a benchmark below, the last value is given for context, to 139 | remember that we are discussing small time intervals. 140 | 141 | ``` r 142 | `%.%` <- fastpipe::`%>%` # (Note that a *fastpipe*, unlike a *magrittr* pipe, can be copied) 143 | `%>%` <- magrittr::`%>%` 144 | bench::mark(check=F, 145 | 'magrittr::`%>%`' = 146 | 1 %>% identity %>% identity() %>% (identity) %>% {identity(.)}, 147 | 'fastpipe::`%>%`' = 148 | 1 %.% identity %.% identity() %.% (identity) %.% {identity(.)}, 149 | 'fastpipe::`%>>%`' = 150 | 1 %>>% identity(.) %>>% identity(.) %>>% identity(.) %>>% identity(.), 151 | base = identity(identity(identity(identity(1)))), 152 | `median(1:3)` = median(1:3) 153 | ) 154 | #> # A tibble: 5 x 6 155 | #> expression min median `itr/sec` mem_alloc `gc/sec` 156 | #> 157 | #> 1 magrittr::`%>%` 113.5us 250.8us 3104. 88.57KB 6.73 158 | #> 2 fastpipe::`%>%` 44.8us 90.3us 9188. 0B 4.23 159 | #> 3 fastpipe::`%>>%` 8.1us 19us 42288. 4.45KB 8.46 160 | #> 4 base 1.3us 3.1us 297595. 0B 29.8 161 | #> 5 median(1:3) 24.9us 54.4us 14621. 26.61KB 4.32 162 | rm(`%>%`) # reseting `%>%` to fastpipe::`%>%` 163 | ``` 164 | 165 | We see that our `%>%` pipe is twice faster, and that our `%>>%` pipe is 166 | 10 times faster (on a properly formatted call). 167 | 168 | We’d like to stress that a pipe is unlikely to be the performance 169 | bottleneck in a script or a function. If optimal performance is 170 | critical, pipes are best avoided as they will always have an overhead. 171 | 172 | In other cases `%>>%` is fast and robust to program and keep the main 173 | benefit of piping. 174 | 175 | ## Implementation 176 | 177 | The implementation is completely different from *magrittr*, we can sum 178 | it up as follow : 179 | 180 | - `%>>%` pipes : evaluate *rhs* in parent environment, overriding `.` 181 | with the value of the *lhs*. Its code is extremely straightforward. 182 | 183 | 184 | 185 | ``` r 186 | `%>>%` 187 | #> # a fastpipe object 188 | #> function (lhs, rhs) 189 | #> { 190 | #> rhs_call <- substitute(rhs) 191 | #> eval(rhs_call, envir = list(. = lhs), enclos = parent.frame()) 192 | #> } 193 | #> 194 | #> 195 | ``` 196 | 197 | - `%>%` pipes : 198 | - Use heuristics to insert dots, so `x %>% head` becomes `x %>% 199 | head(.)` 200 | - support functionnal sequences, so pipe chains starting with `.` 201 | are functions 202 | - support compound assignment, so pipe chains starting with `%<>%` 203 | assign in place to the lhs 204 | 205 | To support the two latter, we use global variables, stored in `globals`, 206 | a child environment of our package’s environment. It contains the values 207 | `master`, `is_fs` and `is_compound`. Outside of pipes master is always 208 | `TRUE` while the two latter are always `FALSE`. The alternative to 209 | global parameters was to play with classes and attributes but was slower 210 | and more complex. 211 | 212 | Whenever we enter a pipe we check the value of `globals$master`. If it 213 | is `TRUE` we enter a sequence where : 214 | 215 | - We set `master` to `FALSE`, and use `on.exit()` to setup the 216 | restoration of global parameters to their default value at the end 217 | of the call. 218 | - We reexecute the whole call (which now won’t enter this sequence) 219 | - If no `.` nor `%<>%` are hit in the call recursion we just 220 | evaluate a sequence of calls as described by `%>>%`’s code, 221 | expliciting the implicit dots as we go. 222 | - If the chain starts with `.` 223 | - We switch on `is_fs` 224 | - When `is_fs` is switched on we return the original call, 225 | unevaluated, subtituting the pipe by it’s bare version (the 226 | name of the bare version is stored as an attribute of the 227 | pipe) and adding explicit dots (so `. %>% head(2)` becomes 228 | `. %>>% head(.,2)`). 229 | - It works recursively and we end up with a quoted pipe chain 230 | of bare pipes starting with a dot 231 | - If the chain starts with a `%<>%` call: 232 | - We switch on `is_compound` 233 | - We define a global variable named `compound_lhs`, which 234 | contains the quoted lhs of the `%<>%` call 235 | - We reevaluate the `%<>%` call, substituting `%<>%` by `%>>%` 236 | - Then consequent pipe calls are recognized as standard 237 | - Once we get the result: 238 | - If `is_fs` was switched on we wrap our quoted pipe in a function 239 | and return it 240 | - If `is_compound` was switched on we assign in the calling 241 | environment the result to the expression stored in 242 | `global$compoud_lhs` 243 | - If not any of those we just return the result 244 | - Default global values are restored, thanks to our `on.exit()` call 245 | 246 | *fastpipe* operators contain their own code while in *magrittr*’s 247 | current implementation they all have the main code and are recognized in 248 | the function by their name. Moreover the pipe names are not hardcoded in 249 | the functions, which prevents confusion like the fact that `%$%` still 250 | sometimes work in *magrittr* when only `%>%` is reexported. 251 | 252 | 253 | The pipes have a class and a printing methods, the functional sequences 254 | don’t. 255 | 256 | ``` r 257 | `%T>>%` 258 | #> # a fastpipe object 259 | #> function (lhs, rhs) 260 | #> { 261 | #> rhs_call <- substitute(rhs) 262 | #> { 263 | #> eval(rhs_call, envir = list(. = lhs), enclos = parent.frame()) 264 | #> lhs 265 | #> } 266 | #> } 267 | #> 268 | #> 269 | . %>% sin %>% cos() %T>% tan(.) 270 | #> function (.) 271 | #> . %>>% sin(.) %>>% cos(.) %T>>% tan(.) 272 | ``` 273 | 274 | ## Benchmarks for functional sequences 275 | 276 | An interesting and little known fact is that using functional sequences 277 | in functionals is much more efficient than using lambda functions 278 | calling pipes (though it will generally be largely offset by the content 279 | of the fonction), for instance : `purrr::map(foo, ~ .x %>% bar %>% baz)` 280 | is slower than `purrr::map(foo, . %>% bar %>% baz)`. I tried to keep 281 | this nice feature but didn’t succeed to make it as efficient as in 282 | *magrittr*. The difference show only for large number of iteration and 283 | will probably be negligible in any realistic loop. 284 | 285 | ``` r 286 | `%.%` <- fastpipe::`%>%` 287 | `%>%` <- magrittr::`%>%` 288 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 289 | # functional sequence with 5 iterations, equivalent speed 290 | library(rlang) # to call `as_function()` 291 | bench::mark(check=F, 292 | rlang_lambda = 293 | vapply(1:5, as_function(~.x %>% identity %>% identity() %>% (identity) %>% {identity(.)}), integer(1)), 294 | fastpipe = 295 | vapply(1:5, . %.% identity %.% identity() %.% (identity) %.% {identity(.)}, integer(1)), 296 | magrittr = 297 | vapply(1:5, . %>% identity %>% identity() %>% (identity) %>% {identity(.)}, integer(1)), 298 | base = 299 | vapply(1:5, function(x) identity(identity(identity(identity(x)))) , integer(1)), 300 | `median(1:3)` = median(1:3) 301 | ) 302 | #> # A tibble: 5 x 6 303 | #> expression min median `itr/sec` mem_alloc `gc/sec` 304 | #> 305 | #> 1 rlang_lambda 718.9us 1.47ms 589. 63.2KB 6.85 306 | #> 2 fastpipe 120.1us 226.1us 3352. 32.1KB 6.77 307 | #> 3 magrittr 153.9us 370us 2479. 13.8KB 6.56 308 | #> 4 base 12.1us 28.9us 28612. 10.1KB 5.72 309 | #> 5 median(1:3) 28us 58.7us 13592. 0B 4.33 310 | 311 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 312 | # functional sequence with 1000 iterations 313 | bench::mark(check=F, 314 | rlang_lambda = 315 | vapply(1:1000, as_function(~.x %>% identity %>% identity() %>% (identity) %>% {identity(.)}), integer(1)), 316 | fastpipe = 317 | vapply(1:1000, . %.% identity %.% identity() %.% (identity) %.% {identity(.)}, integer(1)), 318 | magrittr = 319 | vapply(1:1000, . %>% identity %>% identity() %>% (identity) %>% {identity(.)}, integer(1)), 320 | base = 321 | vapply(1:1000, function(x) identity(identity(identity(identity(x)))) , integer(1)) 322 | ) 323 | #> Warning: Some expressions had a GC in every iteration; so filtering is 324 | #> disabled. 325 | #> # A tibble: 4 x 6 326 | #> expression min median `itr/sec` mem_alloc `gc/sec` 327 | #> 328 | #> 1 rlang_lambda 322.91ms 358.17ms 2.79 277.39KB 5.58 329 | #> 2 fastpipe 19.64ms 42.77ms 23.2 3.95KB 5.79 330 | #> 3 magrittr 7.96ms 16.16ms 57.8 4.23KB 5.98 331 | #> 4 base 2ms 3.64ms 202. 14.09KB 7.94 332 | ``` 333 | 334 | ## Breaking changes 335 | 336 | The fact that the tests of *magrittr* are passed by *fastpipe* doesn’t 337 | mean that *fastpipe* will necessarily behave as expected by *magrittr*’s 338 | users in all cicumstances. 339 | 340 | Here are the main such cases : 341 | 342 | - In *pastpipe* functional sequences don’t have a class and are not 343 | subsetable. This is a feature that Stefan Bache wanted to weed out 344 | of his package, but can still break some code. 345 | 346 | - We said earlier that the instances of using both `!!!.` and `.` are 347 | expected to be rare, but they happen nonetheless. The call 348 | `mtcars[8:11] %>% dplyr::count(!!!.)` works with *magrittr* but with 349 | *fastpipe* we need `mtcars[8:11] %>% dplyr::count(., !!!.)`. 350 | 351 | - If `%T>%` or `%$%` are not reexported by a package which reexports 352 | `%>%`, they will not be usable at all, while at the moment after 353 | attaching dplyr you can do `cars %$% speed %>% head(2)` even if 354 | `%$%` is nowhere to be found. It’s a feature of *fastpipe* rather 355 | than a bug but will still break the code of those who forgot to 356 | attach *magrittr* and had their code work by luck. 357 | 358 | - If reexporting `%>%` from *fastpipe*, one must also reexport `%>>%` 359 | as it is used by functional sequences. 360 | 361 | - If you do strange things like ``cars %>% `$`("spee")`` it won’t work 362 | with *fastpipe* because `$`, `::` and `:::` are special cases and 363 | *fastpipe* would basically try to run `'$'("spee",)(cars)` and choke 364 | because there’d be no second argument to `$`. 365 | 366 | ## Notes 367 | 368 | - The package *magrittr* was created by Stefan Milton Bache and Hadley 369 | Wickham. The design of the pipe’s interface and most of the testing 370 | code of this package is their work or the work of other *magrittr* 371 | contributors, while none of the remaining code is. 372 | - The package is young and might still change. 373 | - *magrittr*’s pipe is widespread and reexported by prominent 374 | *tidyverse* packages. It could have been annoying if they masked 375 | *fastpipe* ’s operator(s) each time so I set a hook so that whenever 376 | one of those prominent packages is attached, *fastpipe* will be 377 | detached and reattached at the end of the search path. This assumes 378 | that if you attach *fastpipe* you want to use its pipes by default, 379 | which I think is reasonable. A message makes it explicit, for 380 | instance : 381 | 382 | > Attaching package: ‘dplyr’ 383 | > 384 | > The following object is masked \_by\_ ‘package:fastpipe’: 385 | > 386 | > %>% 387 | 388 | The current list of the packages subjects to this hook is : *magrittr*, 389 | *dplyr*, *purrr*, *tidyr*, *stringr*, *forcats*, *rvest*, *modelr*, 390 | *testthat*. However many more packages reexport magrittr’s pipe and the 391 | safest way to make sure you’re using *fastpipe* is to attach it after 392 | all other packages. 393 | -------------------------------------------------------------------------------- /benchmark/benchmarks.R: -------------------------------------------------------------------------------- 1 | 2 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | # simple call 4 | `%.%` <- fastpipe::`%>%` 5 | `%>%` <- magrittr::`%>%` 6 | bench::mark( 7 | 'magrittr::`%>%`' = 8 | 1 %>% identity %>% identity() %>% (identity) %>% {identity(.)}, 9 | 'fastpipe::`%>%`' = 10 | 1 %.% identity %.% identity() %.% (identity) %.% {identity(.)}, 11 | 'fastpipe::`%>>%`' = 12 | 1 %>>% identity %>>% identity() %>>% (identity) %>>% {identity(.)}, 13 | 'fastpipe::`%>>>%`' = 14 | 1 %>>>% identity(.) %>>>% identity(.) %>>>% identity(.) %>>>% identity(.), 15 | base = identity(identity(identity(identity(1)))) 16 | ) 17 | 18 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 19 | # simple functional sequence calls 20 | 21 | bench::mark(iterations = 10000, 22 | magrittr = 23 | (. %>% identity %>% identity() %>% (identity) %>% {identity(.)})(1), 24 | fastpipe = 25 | (. %.% identity %.% identity() %.% (identity) %.% {identity(.)})(1) 26 | ) 27 | 28 | 29 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 30 | # functional sequence with 5 iterations 31 | walk <- purrr::walk 32 | bench::mark( 33 | purrr_lambda = 34 | walk(1:5, ~.x %>% identity %>% identity() %>% (identity) %>% {identity(.)}), 35 | magrittr = 36 | walk(1:5, . %>% identity %>% identity() %>% (identity) %>% {identity(.)}), 37 | fastpipe = 38 | walk(1:5, . %.% identity %.% identity() %.% (identity) %.% {identity(.)}) 39 | ) 40 | 41 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 42 | # functional sequence with 1000 iterations 43 | bench::mark( 44 | purrr_lambda = 45 | walk(1:1000, ~.x %>% identity %>% identity() %>% (identity) %>% {identity(.)}), 46 | magrittr = 47 | walk(1:1000, . %>% identity %>% identity() %>% (identity) %>% {identity(.)}), 48 | fastpipe = 49 | walk(1:1000, . %.% identity %.% identity() %.% (identity) %.% {identity(.)}) 50 | ) 51 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | patch: 10 | default: 11 | target: auto 12 | threshold: 1% 13 | -------------------------------------------------------------------------------- /fastpipe.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /man/is_fastpipe.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/pipe.R 3 | \name{is_fastpipe} 4 | \alias{is_fastpipe} 5 | \title{Test if Object is a fastpipe} 6 | \usage{ 7 | is_fastpipe(x) 8 | } 9 | \arguments{ 10 | \item{x}{object to test} 11 | } 12 | \value{ 13 | a length one logical 14 | } 15 | \description{ 16 | Test if Object is a fastpipe 17 | } 18 | -------------------------------------------------------------------------------- /man/pipes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/pipe.R 3 | \name{pipes} 4 | \alias{pipes} 5 | \alias{\%<>\%} 6 | \alias{\%>\%} 7 | \alias{\%>>\%} 8 | \alias{\%T>\%} 9 | \alias{\%T>>\%} 10 | \alias{\%$\%} 11 | \alias{\%$>>\%} 12 | \alias{\%S>\%} 13 | \alias{\%S>>\%} 14 | \alias{\%L>\%} 15 | \alias{\%L>>\%} 16 | \title{Pipes} 17 | \usage{ 18 | lhs \%<>\% rhs 19 | 20 | lhs \%>\% rhs 21 | 22 | lhs \%>>\% rhs 23 | 24 | lhs \%T>\% rhs 25 | 26 | lhs \%T>>\% rhs 27 | 28 | lhs \%$\% rhs 29 | 30 | lhs \%$>>\% rhs 31 | 32 | lhs \%S>\% rhs 33 | 34 | lhs \%S>>\% rhs 35 | 36 | lhs \%L>\% rhs 37 | 38 | lhs \%L>>\% rhs 39 | } 40 | \arguments{ 41 | \item{lhs}{A value or a dot (\code{.}).} 42 | 43 | \item{rhs}{A function call using pipe semantics of the relevant pipe.} 44 | } 45 | \description{ 46 | Equivalents to magrittr's pipes, plus some additions : 47 | \itemize{ 48 | \item The \code{\%S>\%} pipe is like \code{\%>\%} except it supports using \code{!!!} in any function. 49 | \item The \code{\%L>\%} pipe is like \code{\%>\%} except it logs to the console the call and the execution time. 50 | \item The \code{\%\*>>\%} family of pipes contains equivalent that go faster because they 51 | don't support functional chains (\code{. \%>\% foo() \%>\% bar()} nor the compound pipe (\code{\%<>\%}). 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /man/print.fastpipe.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/pipe.R 3 | \name{print.fastpipe} 4 | \alias{print.fastpipe} 5 | \title{Print a fastpipe object} 6 | \usage{ 7 | \method{print}{fastpipe}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{object to print} 11 | 12 | \item{...}{Ignored, kept for compatibility with other methods} 13 | } 14 | \description{ 15 | Print a fastpipe object 16 | } 17 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(fastpipe) 3 | 4 | test_check("fastpipe") 5 | -------------------------------------------------------------------------------- /tests/testthat/Rplots.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moodymudskipper/fastpipe/512a32981ad3210548467faf15d002b418b1c460/tests/testthat/Rplots.pdf -------------------------------------------------------------------------------- /tests/testthat/test-anonymous-functions.r: -------------------------------------------------------------------------------- 1 | context("%>%: anonymous functions on right-hand side") 2 | 3 | test_that("%>% handles anonymous functions in GlobalEnv", { 4 | 5 | # Simple vectorized function 6 | a <- (function(x) 1 + x^2/2 + x^3/9 + x^4/16)(1:100) 7 | b <- 8 | 1:100 %>% 9 | (function(x) 1 + x^2/2 + x^3/9 + x^4/16) 10 | 11 | # in principle, the dot should also work: 12 | c <- 13 | 1:100 %>% 14 | (function(x) 1 + x^2/2 + x^3/9 + x^4/16)(.) 15 | 16 | expect_that(a, is_identical_to(b)) 17 | expect_that(a, is_identical_to(c)) 18 | 19 | # Same using preferred magrittr syntax 20 | a <- (function(x) 1 + x^2/2 + x^3/9 + x^4/16)(1:100) 21 | 22 | b <- 23 | 1:100 %>% 24 | {1 + .^2/2 + .^3/9 + .^4/16} 25 | 26 | expect_that(a, is_identical_to(b)) 27 | 28 | 29 | # Simple data.frame functions 30 | ht1 <- 31 | iris %>% 32 | (function(x) rbind(head(x), tail(x))) 33 | 34 | ht2 <- rbind(head(iris), tail(iris)) 35 | 36 | expect_that(ht1, is_identical_to(ht2)) 37 | 38 | 39 | df1 <- iris[iris$Species == "setosa", 1:4] 40 | 41 | df2 <- 42 | iris %>% 43 | (function(x) x[x$Species == "setosa", 1:4]) 44 | 45 | expect_that(df1, is_identical_to(df2)) 46 | 47 | 48 | }) 49 | 50 | test_that("%>% handles anonymous functions in other situations.", { 51 | 52 | # Anonymous functions when %>% used in arguments. 53 | df1 <- 54 | transform(iris, test = (function(x) x^2)(Sepal.Length)) 55 | 56 | df2 <- 57 | iris %>% 58 | transform(test = Sepal.Length %>% (function(x) x^2)) 59 | 60 | expect_that(df1, is_identical_to(df2)) 61 | 62 | 63 | a <- sin(abs(1:10)) 64 | b <- sin(1:10 %>% (function(x) abs(x))) 65 | 66 | expect_that(a, is_identical_to(b)) 67 | 68 | # Nested anonymous functions. 69 | a <- iris %>% (function(x) x[, 1] %>% (function(y) max(y))) 70 | b <- max(iris[, 1]) 71 | 72 | expect_that(a, is_identical_to(b)) 73 | }) 74 | 75 | 76 | test_that("%>% throws error with anonymous functions when not parenthesized.", { 77 | 78 | expect_that(iris %>% function(x) { head(x) }, throws_error()) 79 | 80 | }) 81 | 82 | -------------------------------------------------------------------------------- /tests/testthat/test-compound.R: -------------------------------------------------------------------------------- 1 | context("assignment pipe") 2 | 3 | test_that("Assignment pipe works", { 4 | 5 | x <- y <- 1:10 6 | x[1:5] <- sin(cos(x[1:5])) 7 | y[1:5] %<>% cos %>% sin 8 | 9 | expect_that(x, is_identical_to(y)) 10 | 11 | 12 | somedata <- iris 13 | somedata$Sepal.Length %<>% `+`(10) 14 | iris$Sepal.Length <- iris$Sepal.Length + 10 15 | 16 | expect_that(somedata, is_identical_to(iris)) 17 | 18 | z <- 1:10 19 | z %<>% `+`(2) %T>% plot 20 | expect_that(z, is_identical_to(as.numeric(3:12))) 21 | 22 | }) 23 | -------------------------------------------------------------------------------- /tests/testthat/test-fseq.r: -------------------------------------------------------------------------------- 1 | context("functional sequences") 2 | 3 | 4 | test_that("fseq functions work", { 5 | a <- . %>% cos %>% sin %>% tan 6 | 7 | b <- function(x) tan(sin(cos(x))) 8 | 9 | expect_that(a(1:10), is_identical_to(b(1:10))) 10 | }) 11 | -------------------------------------------------------------------------------- /tests/testthat/test-github.R: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # https://github.com/tidyverse/magrittr/issues/200 3 | # compound pipe ignored with functional sequences 4 | 5 | test_that("Trying to combine compound operators and functional sequences fails explicitly",{ 6 | expect_error(. %<>% head(3) %>% dim) 7 | expect_error(. %<>% head(3)) 8 | }) 9 | 10 | ################################################################################ 11 | # https://github.com/tidyverse/magrittr/issues/198 12 | # Allow logging of calls and process time #198 13 | 14 | # we can define an adhoc pipe 15 | 16 | `%L>%` <- function(lhs, rhs) { 17 | origin <- get_origin(match.call()) 18 | if(origin$type == "fs") return(origin$fs) 19 | if(origin$type == "compound") return(eval.parent(origin$modified_call)) 20 | rhs <- insert_dot(substitute(rhs)) 21 | # print the call 22 | cat(paste(deparse(rhs), collapse = "\n"), " ~ ...") 23 | # measure the time 24 | time <- system.time( 25 | res <- eval(rhs, envir = list(`.` = lhs), enclos = parent.frame())) 26 | cat("\n", time[3], "sec\n") 27 | res 28 | } 29 | 30 | ################################################################################ 31 | # https://github.com/tidyverse/magrittr/issues/195 32 | # Issue with lazy evaluation #195 33 | test_that("we can't reproduce issue 195",{ 34 | gen <- function(x) {function() eval(quote(x))} 35 | expect_identical( 36 | {fn <- gen(1); fn()}, 37 | {fn <- 1 %>% gen(); fn()}) 38 | }) 39 | 40 | ################################################################################ 41 | # https://github.com/tidyverse/magrittr/issues/193 42 | # Pipe does not do what is supposed to do when the function is given with library name and no parens 43 | 44 | test_that(":: etc are handled without ()",{ 45 | expect_identical( 46 | cars %>% utils::head, 47 | cars %>% utils::head()) 48 | }) 49 | 50 | ################################################################################ 51 | # https://github.com/tidyverse/magrittr/issues/191 52 | # Splicing the magrittr input 53 | 54 | test_that("`!!!.` is considered as `.` when deciding wether to insert a dot",{ 55 | expect_identical(!!(letters[1:3] %>% rlang::list2(!!!.)), list("a","b","c")) 56 | }) 57 | 58 | 59 | ################################################################################ 60 | # https://github.com/tidyverse/magrittr/issues/186 61 | # if `.` is modified when using `%T>%`, the main chain is affected #186 62 | 63 | test_that("tee pipe can't modify input",{ 64 | expect_identical(mtcars %T>% {. <- iris} %>% head(2), head(mtcars,2)) 65 | }) 66 | 67 | ################################################################################ 68 | # https://github.com/tidyverse/magrittr/issues/159 69 | # Pipes of functions have broken environments 70 | 71 | test_that("we can't reproduce issue 159",{ 72 | expect_identical( 73 | { 74 | compose <- function(f, g) { function(x) g(f(x)) } 75 | plus1 <- function(x) x + 1 76 | compose(plus1, plus1)(5)}, 77 | { 78 | plus2 <- plus1 %>% compose(plus1) 79 | plus2(5)}) 80 | }) 81 | 82 | 83 | ################################################################################ 84 | # https://github.com/tidyverse/magrittr/issues/29 85 | # R CMD check and no visible binding for global variable '.' 86 | 87 | # doesn't happen here 88 | 89 | -------------------------------------------------------------------------------- /tests/testthat/test-multiple-arguments.r: -------------------------------------------------------------------------------- 1 | context("%>%: multi-argument functions on right-hand side") 2 | 3 | test_that("placement of lhs is correct in different situations", { 4 | 5 | # When not to be placed in first position and in the presence of 6 | # non-placeholder dots, e.g. in formulas. 7 | case0a <- 8 | lm(Sepal.Length ~ ., data = iris) %>% coef 9 | 10 | case1a <- 11 | iris %>% lm(Sepal.Length ~ ., .) %>% coef 12 | 13 | case2a <- 14 | iris %>% lm(Sepal.Length ~ ., data = .) %>% coef 15 | 16 | expect_that(case1a, is_equivalent_to(case0a)) 17 | expect_that(case2a, is_equivalent_to(case0a)) 18 | 19 | # In first position and used in arguments 20 | case0b <- 21 | transform(iris, Species = substring(Species, 1, 1)) 22 | 23 | case1b <- 24 | iris %>% transform(Species = Species %>% substr(1, 1)) 25 | 26 | case2b <- 27 | iris %>% transform(., Species = Species %>% substr(., 1, 1)) 28 | 29 | expect_that(case1b, is_equivalent_to(case0b)) 30 | expect_that(case2b, is_equivalent_to(case0b)) 31 | 32 | # LHS function values 33 | case0c <- 34 | aggregate(. ~ Species, iris, function(x) mean(x >= 5)) 35 | 36 | case1c <- 37 | (function(x) mean(x >= 5)) %>% 38 | aggregate(. ~ Species, iris, .) 39 | 40 | expect_that(case1c, is_equivalent_to(case0c)) 41 | 42 | # several placeholder dots 43 | #expect_that(iris %>% identical(., .), is_true()) 44 | expect_true(iris %>% identical(., .)) 45 | 46 | 47 | # "indirect" function expressions 48 | expect_that(1:100 %>% iris[., ], is_identical_to(iris[1:100, ])) 49 | 50 | }) 51 | -------------------------------------------------------------------------------- /tests/testthat/test-pipe2.R: -------------------------------------------------------------------------------- 1 | 2 | test_that("pipe works",{ 3 | expect_equal(cars %>% head %>% dim, c(6,2)) 4 | expect_equal(cars %>% head %T>% print %>% dim, c(6,2)) 5 | expect_equal(cars %>% head(2) %$% dist, c(2,10)) 6 | expect_is(. %>% head %>% dim, "function") 7 | expect_equal("a" %in% letters %>% sum, 1) 8 | expect_error(cars %>% head %>% dim %<>% force) 9 | expect_error(cars %>% head %<>% dim %>% force) 10 | x <- cars 11 | x %<>% head %>% dim 12 | expect_equal(x, c(6,2)) 13 | expect_equal(!!quote(c(a=1,b=1) %S>>% c(!!!.)), c(a=1,b=1)) 14 | 15 | expect_equal(cars %>% head, cars %L>% head) 16 | expect_equal(`%>%`, print(`%>%`)) 17 | 18 | build_pipes( 19 | "test", 20 | rhs_call = 21 | insert_dot(substitute(rhs)), 22 | returned_call = 23 | eval(rhs_call, envir = list(`.` = lhs), enclos = parent.frame()) 24 | ) 25 | 26 | # for coverage 27 | trace(bare_pipe_template, quote(`*RETURNED_CALL*` <- 1)) 28 | bare_pipe_template(1,1) 29 | trace(standard_pipe_template,expression({ 30 | `*RHS_CALL*` <- quote(.); `*RETURNED_CALL*` <- 1})) 31 | attr(standard_pipe_template, "bare_version") <- "bare_pipe_template" 32 | standard_pipe_template(.,force) 33 | standard_pipe_template(1,force) 34 | fs_on() 35 | standard_pipe_template(1,force) 36 | untrace(standard_pipe_template) 37 | untrace(bare_pipe_template) 38 | 39 | }) 40 | -------------------------------------------------------------------------------- /tests/testthat/test-single-argument.r: -------------------------------------------------------------------------------- 1 | context("%>%: one-argument function alternatives.") 2 | 3 | test_that("%>% works as expected with and without parentheses and placeholder", { 4 | 5 | expect_that(1:100 %>% sin %>% abs, is_identical_to(abs(sin(1:100)))) 6 | expect_that(1:100 %>% sin() %>% abs(), is_identical_to(abs(sin(1:100)))) 7 | expect_that(1:100 %>% sin(.) %>% abs(.), is_identical_to(abs(sin(1:100)))) 8 | 9 | expect_that(iris %>% head, is_identical_to(head(iris))) 10 | 11 | dnormsd <- function(sd) function(x) dnorm(x, sd = sd) 12 | some_x <- rnorm(20) 13 | expect_that(some_x %>% dnormsd(5)(.), is_identical_to(dnormsd(5)(some_x))) 14 | expect_that(some_x %>% (dnormsd(5)), is_identical_to(dnormsd(5)(some_x))) 15 | 16 | expect_that(some_x %>% dnormsd(5), throws_error()) 17 | expect_that(some_x %>% function(x) {x} %>% sin, throws_error()) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/testthat/test-tee.r: -------------------------------------------------------------------------------- 1 | context("tee pipe") 2 | 3 | test_that("Tee pipe related functionality works.", { 4 | dim_message <- function(data.) 5 | message(sprintf("Data has dimension %d x %d", NROW(data.), NCOL(data.))) 6 | expect_that(iris %T>% dim_message, shows_message(dim_message(iris))) 7 | expect_that(iris %T>% dim_message, is_identical_to(iris)) 8 | }) 9 | --------------------------------------------------------------------------------