Chapter 6 Embedding R Scripts into Swift - Part 2

In the previous chapter we dipped our toes into making something a bit more practical and making SEXPs more Swift-friendly. We’ll continue on this path making a small Swift command line wrapper for a utility that takes an input file of inflammation readings for a series of patients and returns the average inflammation for each of them as JSON. This example is shamelessly stolen40 from the awesome Software Carpentry41 folks.

6.1 Arguments and Calls

Duplicate the folder of the previous project and name the directory scriptr-2. There’s no need to rename the project itself this time, but consider doing so to keep that muscle memory you’ve built up.

Then, add the Swift Argument Parser module to the project as we did in an earlier chapter.

We’re going to add a helper (below) to r-utils.swift to let us call an R function (we’ll be making) with a single parameter. This is, essentially, a Swift wrapper for Rf_lang2() + R_tryEvalSilent().

// `call` is to remind us this is for a function call 
// `x` is the SEXP parameter we are passing in
// `env` defaults to the global environment
//
// the funtion throws as this is a potentially dangerous R operation
func Rlang2(_ call: SEXP, x: SEXP, env: SEXP = R_GlobalEnv) throws -> SEXP {
  
  var err: CInt = 0
  
  let call_ƒ = RPROTECT(
    Rf_lang2(
      call.protectedSEXP,
      x.protectedSEXP
    )
  )
  defer { RUNPROTECT(3) }
  
  let res: SEXP = RPROTECT(R_tryEvalSilent(call_ƒ, env, &err)) ?? R_NilValue
  defer { RUNPROTECT(1) }

  if (err != 0) { // populate the RError with what happened
    throw RError.tryEvalError(String(cString: R_curErrorBuf()))
  }
  
  return(res)
  
}

Replace the contents of main.swift with the following (we’ll discuss what’s going on after the code block):

import Foundation
import ArgumentParser

initEmbeddedR()

let calculateMeanPerPatient = safeSilentEvalParseString("""
# https://swcarpentry.github.io/r-novice-inflammation/05-cmdline/index.html
calculate_mean_per_patient <- function(filename) {

  if (file.exists(filename)) {

    dat <- read.csv(file = filename, header = FALSE)
    mean_per_patient <- apply(dat, 1, mean)
    out <- jsonlite::toJSON(mean_per_patient, pretty = TRUE)

    cat(out, "\n", sep = "")

    return(0L)

  } else {
    cat("[]\n")
    return(1L)
  }

}
""")

var ret: CInt = 0

struct MeanR: ParsableCommand {
  
  @Argument(help: "CSV file to analyze")
  var csvFile: String = "~/books/swiftr/repo/data/inflammation-01.csv" 
  // replace ^^ this ^^ with where you have the test file
  
  mutating func run() throws {
    let res = try Rlang2(calculateMeanPerPatient[0], x: csvFile.protectedSEXP)
    defer { RUNPROTECT(1) }
    if (res.isINTEGER) {
      ret = CInt(Int(res)!)
    }
  }
  
}

MeanR.main()

Rf_endEmbeddedR(0)

exit(ret)

The let calculateMeanPerPatient = safeSilentEvalParseString(… section build and R function that will read in a CSV file, perform a statistical operation on it, then return a JSON representation (to stdout) of the results of the statistical operation. Performing this calculation in 100% Swift is really straightforward, so imagine we’re doing some fancier statistical operations than just computing averages.

We then setup the Swift ArgumentParser to take in a file name (with a default just to make it easier to build/run the product without messing with Xcode settings) and then call the R function with that value. The function returns 0 on success and 1 on failure which we use as values to exit() at the very end of the program.

If you build and run it you’ll see a populated JSON array. If you change the file name to something that does not exist, you’ll see an empty JSON array.

The example is deliberately small, but it encapsulates everything you need to do to make an R script available.

6.2 Trusting R to Do (Almost) Everything

If we’re willing to use Swift’s string interpolation42 capabilities and are also willing to throw some caution to the wind when it comes to the sanity of command line arguments, we can do away with the ArgumentParser dependency and pass the expected file name to R via string interpolation and just use the result (if any) as the exit() value:

import Foundation

initEmbeddedR()

var ret: CInt = 0

let res = safeSilentEvalParseString("""
# https://swcarpentry.github.io/r-novice-inflammation/05-cmdline/index.html

filename <- "\(CommandLine.arguments[1])"

if (file.exists(filename)) {

  dat <- read.csv(file = filename, header = FALSE)
  mean_per_patient <- apply(dat, 1, mean)
  out <- jsonlite::toJSON(mean_per_patient, pretty = TRUE)

  cat(out, "\n", sep = "")

  0L

} else {

  cat("[]\n")

  1L 

}
""")

ret = ((res.count == 1) && (res[0].isINTEGER)) ? CInt(Int(res[0])!) : 1

Rf_endEmbeddedR(0)

exit(ret)

6.3 A Shiny Preview

We’ll close with a preview of a bare-bones wrapping of a Shiny app. In later chapters we’ll be starting a Shiny app and using a SwiftUI-wrapped WkWebView to present the web-based application in-app vs in the browser that will get launched when we run this example from the command line.

Either comment out everything we’ve done or duplicate the folder and rename it to scriptr-3 (or, go crazy and give it a “shiny”-ish name and get more practice renaming Xcode projects). We’re replacing the contents of main.swift with the following:

import Foundation

initEmbeddedR()

let _ = safeSilentEvalParseString("""
library(shiny)

runApp(list(
  ui = bootstrapPage(
    numericInput('n', 'Number of obs', 100),
    plotOutput('plot')
  ),
  server = function(input, output) {
    output$plot <- renderPlot({ hist(runif(input$n)) })
  }
))
""")

Rf_endEmbeddedR(0)

6.4 Up Next

It’s time to move away from the command line and make our first R-powered, SwiftUI43 double-clickable macOS application.

Before diving in, make sure you have a solid understanding of what we’ve done so far. There’s a link to Apple’s SwiftUI tutorials in the footnotes and if you’ve never built a SwiftUI macOS application before, you should give some of those a spin since, while there will be exposition on what we’re doing in SwiftUI, there won’t be nearly as much as Apple will provide.

Remember, code examples can be found on GitHub44.