-
Notifications
You must be signed in to change notification settings - Fork 272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Friendlier compose() #366
Comments
yes if you're going to modify a call from the stack you should always |
I see, thanks for the tip! If I've used compose <- function(...) {
fs <- lapply(dots_list(...), rlang::as_function)
n <- length(fs)
last <- as_closure(fs[[n]])
`__call_last` <- function() {
call <- duplicate(sys.call(-1))
eval_bare(mut_node_car(call, last), parent.frame(2))
}
`__rest` <- rev(fs[-n])
set_attrs(
`formals<-`(
value = formals(last),
function() {
out <- `__call_last`()
for (f in `__rest`)
out <- f(out)
out
}
),
class = "composite_function"
)
} (Not 100% sure there isn't some "pathologically" impure function that could spoil this ... ) |
The proposed |
That's a general assumptions of function operators, not sure what is the right place to document this. Also introspective and even reflexive functions might still work, there's just no guarantees. By the way could you use more intermediary results in your code? They help the reader, especially with well chosen names. Also don't call |
I was trying to keep the formals adjacent to the function declaration, but I do agree that it's unconventional. Consider it gone. ;) Does this read better? Any further suggestions? compose <- function(...) {
fs <- lapply(dots_list(...), rlang::as_function)
n <- length(fs)
last <- as_closure(fs[[n]])
`__call_last` <- function() {
call <- duplicate(sys.call(-1))
eval_bare(mut_node_car(call, last), parent.frame(2))
}
`__rest` <- rev(fs[-n])
composite <- function() {
out <- `__call_last`()
for (f in `__rest`)
out <- f(out)
out
}
formals(composite) <- formals(last)
class(composite) <- "composite_function"
composite
} |
You could use the mut_node_car(call, last)
eval_bare(call, parent.frame(2)) to make it clearer that some side effects are going on. I wonder if there's a better way to forward the arguments. The capture of the function call + evaluation in the parent frame seems brittle to me. |
Thanks for those suggestions. Makes sense. I'll incorporate them. This pass-arguments-then-call-in-parent-frame pattern is not uncommon for these kinds of functional operators. It does feel to me, too, like a bit of a contortion for something so simple. If there's a more robust, or succinct, way to do it, I would certainly be interested in that. I trust that you are right when you say that the current method is potentially brittle, but I don't quite understand how ... :/ |
By the way, another change—improvement?—to Say, something like this: instead of fs <- lapply(dots_list(...), rlang::as_function) one could try unpacking any nested compositions fs <- flatten_fns(...) where flatten_fns <- function(...) {
fns <- lapply(dots_list(...), as_fn_decomposed)
flatten(fns)
}
as_fn_decomposed <- function(x) {
if (inherits(x, "composite_function"))
decompose(x)
else
rlang::as_function(x)
} This won't unpack compositions beyond the first level, however. (Would need to decompose recursively.) |
I like the forwarding of formals but I think there's not much gain from a better print method here. If we really needed it it would probably be better to use compose(log, ~abs(.) + 1)
compose(log, ~abs(.) + 1)
#> function(x, base = exp(1))
#> {
#> x <- .Primitive("log")(x = x, base = base)
#> x <- (function (..., .x = ..1, .y = ..2, . = ..1)
#> abs(.) + 1)(x)
#>
#> x
#> } A better source could be produced by specifying names to functions and storing them in the enclosure of the composite function. |
One more thought, there is no need to duplicate the whole call so you can use You only need to duplicate the parents of the node you're mutating, and here |
I wasn't aware of Incorporating your recommendations, compose <- function(...) {
fns <- flatten_fn_list(...)
n <- length(fns)
fn_last <- as_closure(fns[[n]])
`__call_fn_last` <- function() {
call <- new_language(fn_last, node_cdr(sys.call(-1)))
eval_bare(call, parent.frame(2))
}
`__fns_rest` <- rev(fns[-n])
fn_comp <- function() {
out <- `__call_fn_last`()
for (f in `__fns_rest`)
out <- f(out)
out
}
formals(fn_comp) <- formals(fn_last)
class(fn_comp) <- "composite_function"
fn_comp
} with the small addition of flattening of nested compositions (same as above, hopefully with better names): flatten_fn_list <- function(...) {
fns <- lapply(dots_list(...), as_decomposed_function)
unlist(fns)
}
as_decomposed_function <- function(x) {
if (inherits(x, "composite_function"))
decompose(x)
else
rlang::as_function(x)
} (Contrary to my previous claim, the flattening happens all the way down.) |
With flattening, the following all yield the same function. compose(log, abs, sin, `+`)
compose(log, abs, sin, compose(`+`))
compose(log, abs, compose(sin, compose(`+`)))
compose(log, compose(abs, compose(sin, compose(`+`)))) In particular, the call modifier |
Your suggested print method is an interesting idea; it certainly more accurately reflects the underlying action than just listing the functions. Another thought: If there are a lot of functions, or if their bodies are long, some kind of output truncation could be useful. In any case, I think some kind of specialized print method is helpful for compositions, because otherwise you'd just see this: print.default(compose(log, abs, sin, `+`))
#> function (.x, .y)
#> {
#> out <- `__call_fn_last`()
#> for (f in `__fns_rest`) out <- f(out)
#> out
#> }
#> <environment: 0x7fccae013e98>
#> attr(,"class")
#> [1] "composite_function" |
I think we should inline the function literal so you would see the source. And optionally a symbol if the function was supplied with a name. This way we don't need a print method and s3 class. |
I also don't think we need a complicated decomposition / recomposition logic just for printing, I'd rather just have the user manipulate a list of functions beforehand. |
If the decomposition logic were just for printing, then I agree, that'd be overkill. But what about situations where you'd want to reuse certain parts of a composition/pipeline? Wouldn't For example (probably not the best): serialize_as_df <- compose(as.data.frame, some_operation)
serialize_as_json <- compose(to_json, decompose(serialize_as_df)[-1]) The point is, the user might not have (direct) access to |
Currently the S3 class is being used for printing and for decomposing (morally, |
You could store the list of functions in a sentinel value in the closure env, e.g. The S3 class would allow handling of magrittr functional sequences. |
Ah, now I think I understand what you're getting at. If I understood correctly, we should now have something like this (ignoring the printing issue): compose <- function(...) {
`__purrr_composed_fns` <- flatten_fn_list(...)
n <- length(`__purrr_composed_fns`)
fn_last <- as_closure(`__purrr_composed_fns`[[n]])
`__call_fn_last` <- function() {
call <- new_language(fn_last, node_cdr(sys.call(-1)))
eval_bare(call, parent.frame(2))
}
`__fns_rest` <- rev(`__purrr_composed_fns`[-n])
fn_comp <- function() {
out <- `__call_fn_last`()
for (f in `__fns_rest`)
out <- f(out)
out
}
formals(fn_comp) <- formals(fn_last)
fn_comp
}
flatten_fn_list <- function(...) {
fns <- lapply(dots_list(...), as_fn_decomposition)
unlist(fns)
}
as_fn_decomposition <- function(x) {
decompose(x) %||% rlang::as_function(x)
}
decompose <- function(x) {
environment(x)$`__purrr_composed_fns`
} |
Initially, I had defined an ad hoc Now that rlang has an |
I don't think you should use srcref attributes for printing, they are for step-debugging. Edit: I now think srcref attrs are for printing as well. The |
It would be nice to have a "friendlier" version of
compose()
that:To satisfy these desiderata (the second of which I presume is planned), I have something like the following in mind, which is a minor tweaking of the current implementation:
(The method for formals setting is the same as in #349.)
Example:
The text was updated successfully, but these errors were encountered: