You can use purrr::reduce (or just Reduce) to assemble the pieces, and rlang to munge the ingredients. I made one change to opts, storing the functions as expressions instead of raw functions, as otherwise the name of lag isn't stored anywhere, so there is no way to know which parameters go with which function. You could use quosures instead of expressions, but since the data is determined by the pipeline structure, not any references, it is easier to use expressions so you can ignore environments.
Assembling the pipeline is not too bad; it's just reduceing the calls, with .init set, splicing each call into the resulting expression. Adding the parameters is a little harder, but the heavy lifting can be done with rlang::call_modify. The parameters have to be subset out of opts$args, which means altering the input expr into a string with which to subset. This can be done with expr_name(.y[[1]]), where the [[1]] is to drop the parentheses from the call. The parameters thus subset need to be unquote-spliced into call_modify so they are passed raw, not as a list.
The resulting expression of a pipeline can be evaluated with purrr::eval_tidy, or because it an ordinary expression, plain old eval.
library(purrr)
library(rlang)
opts = list(x = 1:10,
chain = list(expr(cumsum()), expr(diff())),
args = list(diff = list(lag = 2)))
reduce(opts$chain, ~expr(!!.x %>% !!.y), .init = opts$x)
#> 1:10 %>% cumsum() %>% diff()
chain <- reduce(opts$chain,
~expr(!!.x %>% !!call_modify(.y, !!!opts$args[[expr_name(.y[[1]])]])),
.init = opts$x)
chain
#> 1:10 %>% cumsum() %>% diff(lag = 2)
eval(chain) # or eval_tidy(chain)
#> [1] 5 7 9 11 13 15 17 19
If you'd rather store the data as symbols instead of expressions of calls (i.e. without the parentheses), you can drop the call subsetting in the expr_name call, but will need to call call2 on the symbol to turn it into a call (i.e. add parentheses). call2 can be used to add parameters instead of call_modify, too:
opts = list(x = 1:10,
chain = list(expr(cumsum), expr(diff)),
args = list(diff = list(lag = 2)))
reduce(opts$chain, ~expr(!!.x %>% !!.y), .init = opts$x)
#> 1:10 %>% cumsum %>% diff
reduce(opts$chain, ~expr(!!.x %>% !!call2(.y)), .init = opts$x)
#> 1:10 %>% cumsum() %>% diff()
chain <- reduce(opts$chain,
~expr(!!.x %>% !!call2(.y, !!!opts$args[[expr_name(.y)]])),
.init = opts$x)
chain
#> 1:10 %>% cumsum() %>% diff(lag = 2)
eval(chain) # or eval_tidy(chain)
#> [1] 5 7 9 11 13 15 17 19
call2 will accept a variety of inputs to specify the function, so the above will actually work fine if opts$chain is just a character vector of function names c("cumsum", "diff") without any modification to the code (though expr_name would be superfluous). If there were a way to figure out which args to get, it would work on the original data, too (though the intermediary code would look a bit uglier).