Extension to an R Package: brew gets a weave

I've had enough of copy and pasting output from my R session into my email editor, blog, etc. I need something like Sweave for plain text files. In particular, I want the result of parsing

<<echo=TRUE>>=
f <- function(x) {
    x + 1
}
f(1)
@

with Sweave, but without the latex markup. For example, the Sweave output of this code chunk looks like this

\begin{Schunk}
\begin{Sinput}
> f <- function(x) {
+     x + 1
+ }
> f(1)
\end{Sinput}
\begin{Soutput}
[1] 2
\end{Soutput}
\end{Schunk}

But I want something like

> f <- function(x) {
+     x + 1
+ }
> f(1)
[1] 2

From my latest reading (today) of the Sweave manual, Sweave does not have a documented option for this. I decided not to explore the code base of Sweave just yet, due to its size (src/library/utils/R/Sweave.R is nearly 1000 lines, and half are related to the RweaveLatex driver). I think it's probably overkill to write an Sweave driver for my purpose.

The brew package, by Jeff Horner is a nice little package that does something similar to Sweave for text files. The source code for brew is still manageable in size, so I decided to start there. I had originally thought that the brew package incorporated the type of functionality I mentioned above, but it does not. However, brew allows the user to provide their own 'template' parsing function. Essentially, when brew encounters the delimiters '<%%' and '%%>' in a text file, the text within the delimiters is passed to the user-supplied template parsing function, which should return a character vector to be printed in place of the delimited text.

In order to duplicate a the desired portion of Sweave functionality in brew , I wrote the following template parsing function.

brew.weave <- function(code) {
    out <- ''
    exr <- try(parse(text=code),TRUE)
    if(inherits(exr, "try-error"))
        return(exr)
    env <- new.env(parent=sys.frame(sys.parent()))
    for(i in 1:length(exr)) {
        dep <- deparse(exr[[i]])
        for(j in 1:length(dep)) {
            pmt <- ifelse(j > 1,
                getOption("continue"),
                getOption("prompt"))
            out <- paste(out, pmt, dep[j], '\n', sep='')
        }
        res <- try(capture.output(eval(exr[i], env)),TRUE)
        if(length(res)>0) {
                res <- paste(res, collapse='\n', sep='')
                if(!grepl("^.*\n$", res))
                        res <- paste(res, '\n')
                out <- paste(out, res, sep='')
        }
    }
    return(out)
}

The first block below is a file (featurefull.brew) I modified from the brew source package that demonstrates all of the brew functionality, and the extended functionality provided by brew.weave. The second block shows the default output of brew for this file. The third block shows the output using the brew.weave template function.

  1. You won't see this R output, but it will run. <% foo <- 'bar' %>
    Now foo is <%=foo%> and today is <%=format(Sys.time(),'%B %d, %Y')%>.
    <%# Comment -- ignored -- useful in testing.
         Also notice the dash-percent-gt.
         It chops off the trailing newline.
         You can add it to any percent-gt. -%>
    How about generating a template from a template?
    <%%
            foo <- 'fee fi fo fum'
            bar <- function(x) {
                     x + 1
             }
             bar(1)
    %%>
    foo is still <%=foo%>.
    
  2. > brew::brew("featurefull.brew")
    You won't see this R output, but it will run.
    Now foo is bar and today is July 26, 2010.
    How about generating a template from a template?
    <%
    	foo <- 'fee fi fo fum'
    	bar <- function(x) {
    		x + 1
    	}
     	bar(1)
    %>
    foo is still bar.
    
  3. > brew::brew("featurefull.brew", tplParser=brew.weave)
    You won't see this R output, but it will run.
    Now foo is bar and today is July 26, 2010.
    How about generating a template from a template?
    > foo <- "fee fi fo fum" 
    > bar <- function(x) {
    +     x + 1
    + }
    > bar(1)
    [1] 2 
    
    foo is still bar.
    

There are (of course) some limitations. This extension applies only to output that can be captured with the capture.output function in the utils package. Also, brew does not provide a mechanism to propagate changes to the R environment made by template (<%% %%>) code. Also, I might like to simultaneously HTML escape the output for easy pasting into hypertext documents. This could be done with additional template parsers in brew. However, you can only use one template parser at a time.

Another interesting possibility would be to use the evaluate function and others in Hadley Wickham's new package evaluate, as the intended purpose of this package seems to be in support of brew and Sweave-like functionality. I have a potential extension/solution to the issues above (and potential package if there is interest) that I'll discuss in a later post.

8 thoughts on “Extension to an R Package: brew gets a weave

  1. You might also want to play with the ascii package? It has Sweave drivers for a bunch of simpler text-based markups.

  2. skyjo, haven't heard of StatWeave. Thanks for the pointer.

    Trevor, ascii looks promising, it appears to implement an Sweave driver for a variety of existing markup languages. I think the most attractive portion of ascii is the ascii function itself, which might, of course, be used within Sweave (noweb) or brew markup.

    Hadley, indeed, this only works for output captured by sink(type="output"). I am eager to explore the code in evaluate for this very reason, as I alluded to in the post. It looks like nice work, and in position to fill some of the major holes in Sweave. In particular, a sore point for me is where Sweave (via parse()) strips '#' comments.

    There are quite a few projects with regard to reproducible research and reporting. A partial list of related packages/functions: Sweave, ascii, brew, relax, weaver (BioC), SWordInstaller, odfWeave, odfWeave.survey, cacheSweave, pgfSweave, SRPM, SweaveListingUtils. Maybe this is enough to begin a Task View, though I'm probably not qualified to maintain it :-).

  3. Dear Matt
    I've just tried out brew.weave() and it looks promising and I love to see the function incorporated in a package, perhaps in brew itself.

    I might have found a bug, though. The following
    <%
    set.seed(1)
    tmp <- rnorm(10)
    res <- list()
    res[["mean"]] <- mean(tmp)
    res[["sd"]]

    parsed via brew.weave yields as expected
    > brew("featurefull.brew", tplParser=brew.weave)

    > res
    $mean
    [1] 0.13220

    $sd
    [1] 0.78059

    However, uncommenting the "mean" line
    <%
    set.seed(1)
    tmp <- rnorm(10)
    res <- list()
    res[["mean"]] <- mean(tmp)
    res[["sd"]]

    yields this
    > brew("featurefull.brew", tplParser=brew.weave)

    > mean(tmp)
    [1] 0.13220
    > res
    [1] "[1] 0.13220 \n"

  4. I'm sorry. Something didn't pass through. This is a retry.
    First code chunk
    <%
    set.seed(1)
    tmp <- rnorm(10)
    res <- list()
    res[["mean"]] <- mean(tmp)
    res[["sd"]]

    and

    For the second, simply uncomment `mean(tmp)'.

  5. Liviu,

    Ahh, I believe you did find a bug. The original brew.weave evaluates the string in the environment of the function. Hence, when you used 'res' in your code, it replaced the 'res' in the function. The solution is to evaluate the code in a new environment. I've fixed this in the code above.

    Thanks!
    Matt Shotwell

Comments are closed.