A pipe that explicitly replaces a placeholder can be implemented (fairly) straightforwardly since the base R function substitute
implements the bulk of this. The one caveat is that substitute
expects an unquoted expression, so we need to work around that, and we obviously need to evaluate the resulting expression:
`%>%` = function (lhs, rhs) {
subst = call('substitute', substitute(rhs), list(. = lhs))
eval.parent(eval(subst))
}
Here I am reusing the %>%
operator but if you are concerned about conflicts with ‘magrittr’ you may obviously use another operator, e.g. %|%
.
1:5 %>% sum(na.rm = TRUE, .) %>% identity()
# Error in identity() : argument "x" is missing, with no default
1:5 %>% sum(na.rm = TRUE, .) %>% identity(.)
# [1] 15
The above works by replacing each occurrence of .
in the RHS with the value provided by the LHS. That is, during evaluation .
is not a name. This is usually the expected and desired semantic. However, it means that you cannot assign to .
inside the RHS (including calling replacement functions), because .
is not a name.
So something like {names(.) = "foo"; .}
does not work.
We can fix this with a different implementation, which does not replace .
with the LHS but rather injects a definition of .
into the environment where the RHS is evaluated:
`%>%` = function (lhs, rhs) {
eval_env = new.env(parent = parent.frame())
eval_env$. = lhs
eval(substitute(rhs), envir = eval_env)
}
Now we can use .
as a name in assignments:
1 %>% {names(.) = "foo"; .}
# foo
# 1
However, some other things no longer work, because we now evaluate the expression in a different environment:
1:5 %>% assign("x", .)
x
# Error: object 'x' not found
… whereas this did work with the first pipe implementation. We could make it work again by injecting .
directly into the calling environment but this messes with the user environment and I would strongly discourage doing that.1
Instead, if you want to do things like calling assign
within a pipeline expression, be explicit into which environment you want to assign (i.e. pass something like envir = the_environment
to assign
).
1 You could avoid messing with the user environment by carefully preserving the user state and cleaning up afterwards; but this leads to a much more complex (and error-prone) implementation:
`%>%` = function (lhs, rhs) {
caller = parent.frame()
if (exists('.', envir = caller, inherits = FALSE)) {
stored_dot = caller$.
on.exit({caller$. = stored_dot})
} else {
on.exit(rm(., envir = caller))
}
caller$. = lhs
eval.parent(substitute(rhs))
}
(This is a simplified implementation; do not use it! In particular, it fails if .
refers to an active binding in the parent environment. We could add handling for active bindings — with difficulty — but it’s entirely likely that I forgot some other edge case; like I said, this is error-prone.)