Suppress evaluation of knitr chunk options in custom language engine

I am writing a custom knitr language engine to support Targets for Notebooks · Discussion #469 · ropensci/targets · GitHub, and I would like users to be able to write R Markdown code chunks like this:


```{tar_target analysis, pattern = map(data)}
run_analysis(data)
```

I want to avoid evaluating the pattern argument because map(data) is a DSL in tar_target() and not actual R code. But when I run the chunk, I get a "could not find function 'map'" error before control passes to the engine. Is it possible for me as the developer to call substitute() on the necessary options before knitr tries to evaluate them?

Also asked at r - Suppress evaluation of knitr chunk options in custom language engine - Stack Overflow.

I wonder if that would help but custom engine (and mainly external engine) are using the same engine.opts chunk option to pass option specific to an engine as a list. This prevent name conflict in chunk option name for example and allow to share common definition in engine. But that may not apply to your custom engine which is mainly a wrapper on R engine I believe.

Regarding the option transformation before knitr evaluates, there are the options hook. I am not sure where they are evaluated compare to the engine but they allow modifying an option value before doing anything normally. 11.18 Option hooks (*) | R Markdown Cookbook

Did you try those ?
Delayed evaluation is not so common with knitr option, so I feel this is kind of new what you are doing.

Thanks for the tip about engine.opts (see Targets for Notebooks · ropensci/targets · Discussion #469 · GitHub). I tried option hooks, but by the time control reaches the hook it seems that the options are already evaluated. eval.after would have been nice if it weren't for R Markdown chunk option evaluated even though suppressed with the eval.after knit option · Issue #9407 · rstudio/rstudio · GitHub. And the risk of name conflicts is real. tar_target() has an error argument which is different from the error chunk option, and the space of existing chunk options is crowded enough that there may be more for other target factories down the road.

Yes engine.opts is the best knitr way to avoid conflict. Some engine namespace in option name like texPreview with texpreview.path and other, but it is more verbose that using engine.opts with name that user would know from targets functions.

For delayed evaluation, I am not sure what we could do. Maybe something like eval.after but eval.delayed and this would indicate options that the engine itself would evaluate and not knitr ?
Maybe we could think of something to offer this flexibility without changing anything deep in how knitr currently works. (just :thinking: )

For my use case, it would be perfect to have engine.opts.knit as a knit option rather than a chunk option. I would like to write:

knitr::opts_knit$set(engine.opts.knit = list(tar_target = names(formals(targets::tar_target))))

The desired effect:

  1. Quote all the engine-specific chunk options and never evaluate them outside the engine. (Maybe the decision to quote or not quote for a given engine is specified some other way.)
  2. Automatically them in a list apart from the regular chunk options, e.g. options$engine.opts.knit$tar_target, when the engine is called.
  3. Allow the user to write the prespecified engine options just like chunk options.
```{tar_target analysis, pattern = map(data), error = "not_a_logical_type"}
run_analysis(data)
```

Seems like that would break a whole bunch of patterns and design assumptions in knitr, and R Markdown chunk option evaluated even though suppressed with the eval.after knit option · Issue #9407 · rstudio/rstudio · GitHub will take a time, so I still plan to write an engine.opts-based implementation of Targets for Notebooks · Discussion #469 · ropensci/targets · GitHub to start.

Just about this part, are you looking to set the engine.opts by default for the tar_target engine ?
Not sure but ICYMI you can set defaults to engine.opts per engine. It is hidden in example in 15.4 Execute Shell scripts | R Markdown Cookbook and shown in the doc

Something like this should work to set default option to the engine

knitr::opts_chunk$set(engine.opts = list(
  tar_target = list(...)
)

But I guess you can set default at the engine level so this would be for the user only.

About your proposal, a mechanism like that to filter out option would work in knitr maybe, but it would require to handle namespace to avoid conflict.

Generic thoughts about this specific engine: it seems we could generalize what you want to do by see templated chunk

```{function_name, label, optional_aguments}
main argument
```

epoxy offers a glue() engine that seems to follow this pattern

```{glue, .transformer = epoxy_style_bold()}
All cars stopped between {min(cars$dist)} and {max(cars$dist)} feet
from a starting speed of {min(cars$speed)}---{max(cars$speed)}
```

I am just seing this and this makes me thing of this pattern more. Maybe there a generic way to improve :thinking:

However, often the question for this usage is the same: is this easier to write with a specific engine compare to using the function in an R chunk ?

```{tar_target analysis, engine.opts = list(pattern = "map(data)", error = "continue")}
run_analysis(data)
```

or

```{r}
tar_target(
  name = analysis,
  command = run_analysis(data),
  pattern = map(data),
  error = "continue"
)
```

I understand the added value is that the tar_target engine would modify the way the code is evaluated. But you could also use this engine specific engine to change the evaluation of the code (compare to default R engine) without requiring change of the code user already know (prevent setting arguments in chunk option with all the limitation we mentioned)

```{targets}
tar_target(
  name = analysis,
  command = run_analysis(data),
  pattern = map(data),
  error = "continue"
)
```

Is that too simple ?

Other possible interface

```{r, targets = TRUE}
tar_target(
  name = analysis,
  command = run_analysis(data),
  pattern = map(data),
  error = "continue"
)
```

This would be a way to activate some hooks on the R engine only for chunk used for targets. It is not like a custom engine but could also work maybe - just another interface.

You may have look into that also but it was hard to follow in the Github discussion the different design phase you had.

Again I don't really use targets, only saw examples and demo so I am not sure what the aim is to be able to define the pipeline in a Rmd document. What I understand:

  • Use Rmd instead of _targets.R to write the pipeline.
    • Advantage: use prose instead of comments in files ? Interleave target definition with other type of code ?
  • Do not evaluate the chunk as R code but write into the R file _targets.R from the code chunk and do nothing else in knitr
  • Print back in the resulting document another piece of code that the original one or just log of what has been done (not sure to see what the final document look like)

Is that right ?

1 Like

At the very beginning of this discussion, I was resistant to actually calling tar_target() in a code chunk. I thought it seemed contrary to the script-focused mentality people were accustomed to.

```{tar_target analysis}
tar_target(
  name = analysis,
  command = run_analysis(data),
  pattern = map(data),
  error = "continue"
)
```

However, after having extensively explored chunk options with you, this is actually far more appealing. Not only does the interface perfectly align with targets, this approach allows us to do far more in interactive mode. The chunk engine could act exactly like tar_make() with just the targets in the chunk: construct a pipeline with just those targets, run the pipeline, and assign the return values to the user's environment, with all storage redirected to a temporary directory. Targets would stay up to date for that session. I am thinking a new tar_make_interactive() function could handle this.

Related: this approach could easily support interactive dynamic branching and interactive static branching, both of which would be impossible with the chunk option interface.

1 Like

So there will be a tar_target engine for targets and a tar_global engine for user-defined functions and other global objects that the targets require. Target factories in other packages will just work out of the box!

@cderv, in interactive R Markdown, is there a way to run all the tar_global engine chunks in a document when a tar_targetchunk is called? I would like to borrow the behavior of setup chunks.

Also, rethinking something else I said:

Targets would stay up to date for that session.

That actually gets into tricky because different sub-pipelines in different code chunks could fight over each other. I now think the temporary storage should evaporate when the chunk completes, which seems perfectly reasonable for interactive mode.

1 Like

The setup chunk is an IDE feature. So this would require the IDE to have a way to setup this chunk dependency. This type of question has already been asked I think for caching: Would it be possible to interactively run a cached chunk but also all the chunk that it depends on ? I think the idea is related.
This would be a feature request for the IDE and not the open source R package. Anything interactive = IDE support.

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.