R/RJuliaConnect.R

#' An Interface to Julia
#'
#' The JuliaInterface class provides an evaluator for computations in Julia, following the structure
#' in the XR  package.  Proxy functions and classes allow use of the interface with no explicit
#' reference to the evaluator.  The function \code{RJulia()} returns an evaluator object.
#'
#' @field host The remote host, as a character string.  By default this will be the local host, and initializing the evalutor
#' will set the field to "localhost".
#' @field port The port number for commuicating to Julia from this evalutor.
#' By default, the port is set by adding the evaluator number-1 to a base port number.
#' By default the base port is randomly chosen at package load time (this strategy may change).
#'
#' The port may be controlled in two ways.  If you know a good range or set of ports, it will
#' be preferrable to supply unique port values (integer) in the initialization call.
#' A less direct way is set the R option "JuliaBasePort", which will then be used as the base port.
#' Since evaluator numbers are used to increment the port, the call to \code{\link{options}} should normally come before 
#' initializing the first Julia evaluator.
#' @field julia_bin The location for an executable version of the Julia interpreter.  By default, this assumes there is a file named \code{"julia"}
#' on the command-line  search path.  If Julia is not usable from the command line or if you want to run with a different version, supply
#' the executable file name as this argument.  It is also possible to set the location for all evaluators by setting the shell variable
#' \code{JULIA_BIN} to this location \emph{before} starting R.
#' @field connection The connection object through which commands are sent to Julia.  Normally will be created by the initialization
#' of the evaluator.  Should only be supplied as a currently
#' open socket on which to communicate with the Julia interpreter.
#' @field serverWrapup a vector of actions for the ServerEval to take after evaluation.  Used to clean up after special operations,
#' such as sending large objects to Julia.
#' @field largeObject Vectors with length bigger than this will be handled specially.  See \link{largeVectors}.  Default currently 1000.  To change this, call \code{\link{juliaOptions}()}
#' to set option \code{largeObject}.
#' @field fileBase a pattern for file names that the evaluator will use in Julia for various data transfer
#' and other purposes.  The evaluator appends "_1", "_2", etc.  To change this, call \code{\link{juliaOptions}()}
#' to set option \code{fileBase}.  It is initiaized to an R tempfile with pattern \code{"Julia"}.
JuliaInterface <- setRefClass("JuliaInterface",
                      fields = c( port = "integer", host = "character",
                          julia_bin = "character",
                          connection = "ANY",
                          serverWrapup = "character",
                          contains = "Interface",
                          largeObject = "integer",
                          fileBase = "character"),
                      contains = "Interface")

JuliaInterface$methods(
                   initialize = function(..., startJulia, verbose = FALSE)
                   {
                       'The initialize method attempts to open a socket unless the "connection" field in the evaluator is an open socket.  Else, if the host is the local host an attempt is made to start a Julia process.  See the documentation of the interface class for details.'
                       languageName <<- "Julia"
                       prototypeObject <<- JuliaObject()
                       prototypeObject$.ev <<- .self
                       largeObject <<- 1000L
                       fileBase <<- tempfile(pattern = "Julia")
                       callSuper(...) # initialize object & register it
                       if(is(connection, "connection")) {
                           if(is(connection, "sockconn") && isOpen(connection))
                               return()
                           else {
                               if(!is(connection, "sockconn"))
                                   msg <- gettextf("got class %s",dQuote(class(connection)))
                               else
                                   msg <- "Socket not open"
                               stop(gettextf(
                                   "Argument connection= should be an open socket connection: %s",
                                   msg))
                           }
                       }
                       if(missing(startJulia))
                           startJulia <- length(host) == 0L || identical(host, "localhost")
                       ## start a julia proc.
                       if(startJulia && (length(port) == 0L || is.na(port))) {# uninitialized
                           base <- getOption("JuliaBasePort")
                           if(is.null(base) || is.na(base))
                               base <- basePort # value set at package load time
                           port <<- as.integer(base + XR::evaluatorNumber(.self)) - 1L
                       }
                       if(startJulia) {
                           Sys.setenv(JULIA_JUNK = "This is a silly environment variable\n")  ## just for testing
                           ## the equivalent of AddToPath(), done in Julia
                           julia_lib <- system.file("julia",package = "XRJulia")
                           Sys.setenv(RJULIA_LIB = julia_lib)
                           if(!(julia_lib %in% serverPath))
                               serverPath <<- c(serverPath, julia_lib)
                       }
                       if(length(host) == 0)  ## never set
                           host <<- "localhost"
                       if(identical(host, "localhost") && length(julia_bin) == 0L)
                               julia_bin <<- findJulia()
                       if(verbose)
                           Sys.setenv(JuliaVerbose = 1)
                       else
                           Sys.unsetenv("JuliaVerbose")
                       if(startJulia) {
                           juliaFolder <- system.file("julia", package = .packageName)
                           juliaStart <-  system.file("julia","RJuliaJSON.jl", package = .packageName)
                           Sys.setenv(RJuliaPort=port, RJuliaHost = host, RJuliaSource=juliaFolder)
                           if(identical(host, "localhost")) {
                               testJSON(julia_bin)
                               base::system(juliaCMD(julia_bin, juliaStart), wait = FALSE)
                           }
                       }
                       ## else, the Julia process should have been started and have called accept()
                       ## for the chosen port
                       if(verbose)
                           cat(gettextf(
                               "Starting connection to %s on port %d\n", host, port))
                       for(i in 1:300) {
                           ## waiting for Julia proc. to establish connection
                           Sys.sleep(.2)
                           sc <- tryCatch(socketConnection(host = host, port = port, block = TRUE), error = function(e) e,
                                          warning = function(w)w)
                           if(is(sc, "connection"))
                               break
                           if(verbose)
                               cat(if(i==1) "Waiting." else ".")
                       }
                       cat("\n")
                       if(!is(sc, "connection"))
                           stop(gettextf("Unable to start Julia connection on port %s: got the error/warning message \"%s\"",
                                         port, sc$message))
                       connection <<- sc
                       ## now, actions such as setting the path can be performed
                       startupActions()
                       ## set some julia options--either defaults or specified in the constructor call.
                       juliaOptions(largeObject = largeObject, fileBase = fileBase, .ev = .self)
                   })

## The definition of Julia-dependent methods
JuliaInterface$methods(
    ServerEval = function(expr,key = "", get = NA) {
        value <- ServerTask("eval", expr, key, get)
        on.exit(serverWrapup <<- character())
        for(action in serverWrapup)
            base::eval(base::parse(text = action))
        XR::valueFromServer(value, key, get, .self)
    },
    ServerRemove = function(what)
        XR::valueFromServer(ServerTask("remove", what), "", NA, .self),
    ServerClassDef = function(Class, module = "", ...) {
        value <- juliaClassDef(Class, module, ..., .ev = .self)
        value$methods <- character() # don't make any proxy methods
        value
       },
    ServerTask = function(task, expr,  key = "" , get = NA) {
        'Call the task operation in the Julia code for the interface; the arguments must
be the simple strings or logical value expected.'
        cmd <- jsonlite::toJSON(c(task, expr, key , get))
        writeLines(cmd, connection)
        for(try in 1:10) {
            value <- readLines(connection, 1)
            if(length(value) == 0)  # But shouldn't happen?
                Sys.sleep(1)
            else
                break
        }
        value
    },
    ServerQuit = function(...) {
        cmd <- jsonlite::toJSON("quit") # NB: this is the *task* quit in XRJulia, not the Julia quit() function
        writeLines(cmd, connection) # don't expect an answer; the process will exit
        close(connection)
    },
    ProxyClassName = function(serverClass) {
        'Return the name of a proxy class associated with this server class, or NA.
The proxy class name is the serverClass_Julia.  If the serverClass is parametrized,
the unparamaterized version will be a secondary match.'
        pclass <- serverClass
        if(grepl("{",pclass,fixed = TRUE))
            pclass <- c(pclass, gsub("[{].*","", pclass))
        pclass <- paste(pclass, languageName, sep = "_")
        for(pcl in pclass)
            if(!is.null(getClassDef(pcl)))
                return(pcl)
        NA # will be assigned in table to avoid future search
    }
                       )

## Programming methods
JuliaInterface$methods(
    ServerAddToPath = function(serverDirectory, serverPos = NA) {
        'Julia version of the AddToPath method.'
        ## Julia may try to interpret anything with attributes, so strip
        serverDirectory <- as(serverDirectory, "character")
        if(is.na(serverPos))
            Call("push!", as.name("LOAD_PATH"), serverDirectory)
        else
            Call("insert!", as.name("LOAD_PATH"), serverPos, serverDirectory)
    },
    Import = function (module = "", ..., .command = if(identical(members, "*")) "using" else "import") {
        'import the specified Julia module, optionally for the names provided in the "..."  arguments.  May also be used to invoke the "using" command by giving "*" as the names. In this case multiple modules can be imported by supplying a vector of length > 1 for module. All the exported objects in those modules will be available unqualified.'
        members <- unlist(c(...))
        switch(.command,
               using = {
                   if(length(module) > 1)
                       module <- paste(module, collapse = ", ") # allow multiple modules for using
               },
               import = {
                   if(length(members))
                       module <- paste(module, members, sep=".")
               })
        Command(paste(.command, module))
    },
ProxyClassName = function(serverClass) {
    'Find the proxy class name for a Julia composite type.  Uses the templated version if that has been
defined, otherwise the general untemplated one.'
    pclass <- callSuper(serverClass)
    if(is.na(pclass) && grepl("{",serverClass, fixed = TRUE)) { # a templated class
        baseClass <- gsub("[{].*","", serverClass)
        callSuper(baseClass) # check for untemplated version
    }
    else
        pclass
},
Source = function(filename) {
    'Julia version of the $Source() method, using the include command in Julia'
    Call("include", filename)
},
    Using = function(...) {
        'The Julia "using" form of importing.  Arguments are module names.  All the exported
members of these modules will then be available, without prefix.'
        Import(c(...), .command = "using")
    }
)

#' Function Versions of Methods for Julia Interface evaluators.
#'
#' @name functions
#' @param ... arguments to the corresponding method for an evaluator object.
#' @param evaluator The evaluator object to use.  By default, and usually, the current evaluator
#' is used, and one is started if none has been.  But see the note under \code{juliaImport} for the load actions created in special cases.
NULL

#' @describeIn functions
#' evaluate the file of Julia source.
juliaSource <- function(..., evaluator = RJulia())
    evaluator$Source(...)

#' @describeIn functions
#' adds the directory specified to the search path for Julia modules.
#' If called from the source directory of a package during installation, sets up
#' a load action for that package.  If you want to add the path to all
#' evaluators in \emph{this} session, call the function before creating an evaluator.
#' Otherwise, the action applies only to the specified evaluator or,
#' by default, to the current evaluator.
#' @param directory the directory to add, defaults to "julia"
#' @param package,pos arguments to the method, usually omitted.
#' @param where for the load action, omitted if called from a package source file.
#' Otherwise, must be the environment in which a load action can take place.
juliaAddToPath <- function(directory = "julia", package = utils::packageName(topenv(parent.frame())), pos = NA,  evaluator = RJulia(.makeNew = FALSE),
                           where = topenv(parent.frame())) {
    if(is.null(evaluator))
        XR::serverAddToPath("JuliaInterface", directory, package, pos, where = where)
    else
        evaluator$AddToPath(directory, package, pos)
}

#' @describeIn functions
#' the "using" form of Julia imports:  the module is imported with all exports exposed.
#' @param module String identifying a Julia module.
juliaUsing <- function(module, evaluator) {
    if(missing(evaluator))
        juliaImport(module, "*")
    else
        juliaImport(module, "*", evaluator = evaluator)
}

#' @describeIn functions
#' adds the module information specified to the modules imported for future Julia evaluator objects.
#'
#' Add the module to the table of imports for Julia evaluators, and import it to the current evaluator
#' if there is one.
#' If called from the source directory of a package during installation, both \code{juliaImport}
#' and \code{juliaAddToPath()} set up
#' a load action for that package.  The functional versions, not the methods themselves, should
#' be called from package source files to ensure that the load actions are created.
#' Note that calling either function before any evaluator has been generated
#' will install that call as a setup action for all XRJulia evaluators.
juliaImport <- function(...,  evaluator = RJulia(.makeNew = FALSE)) {
    if(is.null(evaluator))
        XR::serverImport("JuliaInterface", ...)
    else
        evaluator$Import(...)
}



.JuliaInterfaceClass <- getClass("JuliaInterface")

#' An Evaluator for the Julia Interface.
#'
#' Returns an evaluator for the Julia interface.  Starts one on the first call, or if arguments are provided;
#' providing argument \code{.makeNew = TRUE} will force a new evaluator.  Otherwise, the current evaluator is
#' returned.
#'
#' @param ... Arguments passed to \code{\link{getInterface}()} but none usually required.
#' See \code{\link{JuliaInterface}} for details of the evaluator.
RJulia <- function(...)
    XR::getInterface(.JuliaInterfaceClass, ...)

#' Find a Julia Executable
#'
#' This function looks for an executable Julia application in the local operating system.  The location can be prespecified by
#' setting environment variable \code{JULIA_BIN}; otherwise, the function looks in various conventional locations
#' and if that doesn't work, runs a shell command to look for \code{julia}.
#' @return The location as a character string, unless \code{test} is \code{TRUE}, in which case success or failure
#' is returned, and the location found (or the empty string) is saved as the environment variable.
#' Note that in this case, \code{FALSE} is returned if the Julia package \code{JSON} has not been added.
#' 
#' If \code{test} is \code{FALSE}, failure to find a Julia
#' in the current system is an error.
#' @param test Should the function test for the existence of the application.  Default \code{FALSE}. Calling with
#' \code{TRUE} is useful to bullet-proof examples or tests for the absence of Julia. If the test search succeeds,
#' the location is saved in environment variable \code{JULIA_BIN}.
#' @section On Mac OS X:
#' Installing Julia in the usual way does not put the command line version in a
#' standard location, but instead in a folder under \code{/Applications}.
#' Assuming one wants to have Julia available from the command line,
#' creating a symbolic link to it in \code{/usr/local/bin} is a standard approach.
#' If the current version of Julia is \code{0.6}:
#'
#' \code{sudo ln -s /Applications/Julia-0.6.app/Contents/Resources/julia/bin/julia /usr/local/bin/julia}
#'
#' If for some reason you did not want this to be available,  set the shell variable
#' \code{JULIA_BIN} to the first file in the command, the one in \code{/Applications}.
findJulia <- function(test = FALSE) {
    ## See if a location for the Julia executable has been specified
    ## environment variables JULIA_BIN or JULIA_SRC
    envvar <- Sys.getenv("JULIA_BIN")
    if(!nzchar(envvar)) {
        src <- Sys.getenv("JULIA_SRC")
        if(nzchar(src)) {
            ## try either the standard Julia source, or the directory above bin, etc.
            trybin <- paste(src, "usr","bin","julia", sep=.Platform$file.sep)
            if(file.exists(trybin))
                envvar <- trybin
            else {
                trybin <- paste(src, "julia", sep=.Platform$file.sep)
                if(file.exists(trybin))
                    envvar <- trybin
            }
        }
    } # if none of these succeeds, `which julia` used to find an executable version
    if(!nzchar(envvar)) {
        command <-if (.Platform$OS.type == "windows") "where" else "which"
        envvar <- tryCatch(system2(command, "julia", stdout = TRUE), warning = function(e) "")
        if(test)
            Sys.setenv(JULIA_BIN = envvar) # so next call finds this immediately
        else if(!nzchar(envvar))
            stop("No julia executable in search path and JULIA_BIN environment variable not set")
    }
    if(test)
        nzchar(envvar)
    else
        envvar
}

## command to run a julia file.
## Needs to allow for a blank in the Windows location ("Program Files")
juliaCMD <- function(julia_bin, testFile)
    if (.Platform$OS.type == "windows") paste0('"',julia_bin,'" ', testFile) else paste(julia_bin, testFile)

testJSON <- function(julia_bin) {
    testFile <- system.file("julia", "testJSON.jl", package = "XRJulia")
    cmd <-  juliaCMD(julia_bin, testFile)
    if(base::system(cmd)) # error exit from command
        stop("Unable to continue:  failed to get JSON.  Try Pkg.add() from julia directly")
    TRUE
}
    

## the base for the port used by JuliaInterface objects
## This value should be overwritten by a random choice in the load action
basePort <- 1233L

.setPort <- function(ns) {
    assign("basePort", as.integer(1000+sample(999,1)), envir = ns)
}
setLoadAction(.setPort, "setJuliaPort")


## Julia will have some methods for data conversion via asServerObject
## The evaluator's prototypeObject field will be from class JuliaObject

#' Proxy Objects in R for Julia Objects
#'
#' @name proxyJuliaObjects
NULL

#' Proxy Objects in R for Julia Objects
#'
#' This is a class for all proxy objects from a Julia class with an R proxy class definition.
#' Objects will normally be from a subclass of this class, for the specific Julia class.
#'
#' Proxy objects returned from the Julia interface will be promoted to objects
#' from a specific R proxy class for their Julia class, if such a class has been defined.
JuliaObject <- setRefClass("JuliaObject",
                           contains = "ProxyClassObject")


## NOT Roxygen  Array method for asServerObject()
## NOT Roxygen
## NOT Roxygen use the reshape() function in Julia to create a suitable multi-way array
## NOT Roxygen @param object The \R object.
## NOT Roxygen @param prototype The proxy for a prototype of the Julia object, supplied by the evaluator.
setMethod("asServerObject", c("array", "JuliaObject"),
          function(object, prototype) {
              ## the data part will be a JSON list, forced by the asS4() for length 1
              data <- asServerObject(asS4(as.vector(object)), prototype)
              dims <- paste(dim(object), collapse = ",")
              value <- gettextf("reshape(%s, %s)", data, dims)
              value
          })

.asServerList <- function(object, prototype) {
    if(length(object) == 0)
        return("{ }")
    onames <- names(object)
    vals <- sapply(object, function(x) asServerObject(x, prototype))
    if(is.null(onames))
        paste("[", paste(vals, collapse = ", "), "]")
    else {
        onames <- XR::nameQuote(XR::fillNames(onames))
        paste("Dict{String, Any}(", paste(onames, vals, sep = " => ", collapse = ", "),
              ")")
    }
}

## NOT Roxygen List method for asServerObject()
## NOT Roxygen
## NOT Roxygen For "plain" lists, produces the Julia expression for a list or a dictionary;
## NOT Roxygen with other attributes, uses the .RClass form.
## NOT Roxygen @param object The \R object.
## NOT Roxygen @param prototype The proxy for a prototype of the Julia object, supplied by the evaluator.
setMethod("asServerObject", c("list", "JuliaObject"),
          function(object, prototype){
              attrs <- attributes(object)
              if(is.null(attrs) || identical(names(attrs), "names"))
                  .asServerList(object, prototype)
              else
                  .asServerList(XR::objectDictionary(object), prototype)
          }
          )

typeToJulia <- function(object, prototype) {
    switch(typeof(object),
           ## For complex, Julia requires an explicitly typed array for the real, imaginary parts
           complex = paste0("complex(Array{Float64,1}(", typeToJSON(Re(object), prototype), ") , Array{Float64,1}(",
                            typeToJSON(Im(object), prototype), "))"),
           ## typeToJSON() will create an RData object if there are NA's:  won't parse in Julia
           ## Instead two work arounds but with warning
           integer = , logical = if(any(is.na(object))) {
               warning(gettextf("Julia cannot represent missing values in type %s; converting to double", typeof(object)))
               typeToJSON(as.numeric(object), prototype)
           } else typeToJSON(object, prototype),
           character = { if(any(is.na(object))) {
               warning("Julia cannot represent missing values in strings; converting to \"<NA>\"")
               object[is.na(object)] <- "<NA>"
                         }
           typeToJSON(object, prototype)},
           raw = rawToJulia(object, prototype),
           typeToJSON(object, prototype))
}

notSimpleVector <- function(object)
    (isS4(object) && !is.null(attr(object, "class"))) ||
              ## exclude the S4 case where the bit is on to force a JSON array
                  is.list(attributes(object)) || # S3 class or structure
                  is.recursive(object)

rawToJulia <- function(object, prototype) {
    if(length(object) > 1 || isS4(object))
        paste0("convert(Array{UInt8,1}, ", typeToJSON(as.integer(object), prototype), ")")
    else
        paste0("convert(UInt8, ", as.integer(object), ")")
}

## NOT Roxygen Default Julia method for asServerObject()
## NOT Roxygen
## NOT Roxygen The default method for JuliaObject is modelled on the overall default method in XR.
## NOT Roxygen @param object The \R object.
## NOT Roxygen @param prototype The proxy for a prototype of the Julia object, supplied by the evaluator.
setMethod("asServerObject", c("ANY", "JuliaObject"),
           function(object, prototype) {
               if(notSimpleVector(object))
                  .asServerList(XR::objectDictionary(object), prototype)
              else #<FIXME> There are some edge cases and NA is not handled well
                  typeToJulia(object, prototype)
           })

#' Internal Computations for Large Vectors
#'
#' @name largeVectors
#'
#' @section Sending Large Vectors between R and Julia:
#' Large vectors will be slow to transfer as JSON, and may fail in Julia.  Internal computations have
#' been added to transfer vectors of types real, integer, logical and character by more direct
#' computations when they are large.  The computations and their implementation are
#' described here.
#'
#'
#' R and Julia both have the concept of numeric (floating point) and integer arrays whose elements have a consistent type and both implement
#' these (following Fortran) as contiguous blocks in memory, augmented by length or dimension information.
#' They also both have a mechanism for arrays of character strings, class \code{"character"} in R and array type
#' \code{Array{String, 1}} in Julia.
#' Julia has arrays for boolean data; R stores the corresponding \code{logical} as integers.
#' 
#' JSON has no such concepts, so interface evaluators using the standard JSON form provided by 'XR' must send such data as a JSON list.  This will
#' become inefficient for very large data from these classes.  Users have reported failure by Julia to
#' parse the corresponding JSON.
#'
#' The 'XRJulia' package (as of version 0.7.9) implements special code to send vectors to Julia, by
#' writing an intermediate file that Julia reads.  The actual text sent to Julia is a call to the
#' relevant Julia function.  The code is triggered within the methods for the \code{asServerObject}
#' function, so vectors should be transferred this way whether on their own or as part of a larger structure,
#' such as an array or the column of a data frame.
#'
#' Similarly, large arrays to be retrieved in R by the \code{Get()} method or the optional argument \code{.get = TRUE}
#' will be written to an intermediate file by Julia and read by R.
#'
#' As vectors become large, direct transfer becomes \emph{much} faster.  On a not-very-powerful laptop,
#' vectors of length \code{10^7} transfer in an elapsed time of a few seconds.  Character vectors are slightly
#' slower than numeric, as explained below, but in all cases it would be hard to do much computation with
#' the data that did not swamp the cost of transfer.  That said, as always it's more sensible to transfer
#' data once and then use the corresponding proxy object in later calls.
#' 
#' @section Details:
#' For all vectors, the method uses binary writes and reads, which are defined
#' in both R and Julia.  No special computationss are needed for numeric, integer, complex and raw.
#' For these, the R binary representation corresponds to array types in Julia.
#' The special pseudo-value \code{NA} is defined for vectors in R, but no corresponding concept exists
#' in Julia.  For numeric and complex vectors, the floating-point pattern \code{NaN} is used.
#' For all other vectors, a warning is issued and either a numeric object or a special character string is used instead.
#' 
#' For logicals, the internal representation in R uses integers.
#' The Julia code when data is sent from R casts the integer array to a boolean array.
#' On the return side, the Julia boolean array is converted to integer before writing.
#'
#' Character vectors take a little more work, partly because of a weirdness in binary writes
#' for string arrays in Julia.  Where R character vectors can be written in binary form and then read
#' back in, writing a \code{String} array in Julia omits the end-of-string character,
#' effectively writing a single string, from which the array cannot
#' be recovered.  Communicating the entire vector to Julia requires
#' that the Julia side uses this information to split the single string resulting from the R binary write
#' by matching the end-of-string character explicitly
#' For sending back to R, the Julia code
#' appends an end-of-string character to each string before writing the array to a file.  This produces the
#' R format for a binary read of a character vector.
#'
#' Two fields in the evaluator object control details.
#' A large object is defined as a vector of length greater than the integer field \code{largeObject}.
#' Julia creates intermediate files for sending large arrays to R by appending sequenctial numbers to a
#' character field \code{fileBase}.  By default, \code{largeObject} and  \code{fileBase} is obtained from
#' \code{\link{tempfile}()} with pattern \code{"Julia"}. Note that all the files are removed at the end of
#' the evaluation of the expression sending or getting the relevant objects.
#'
#' Since these fields must be known to the Julia evaluator, they should \emph{not} be set directly---this will
#' have no effect.  Instead call the function \code{\link{juliaOptions}()} with these parameter names.
#'

setClassUnion("simpleVectorJulia", c("numeric", "integer", "logical", "character", "complex", "raw"))

setMethod("asServerObject", c("simpleVectorJulia", "JuliaObject"),
          function(object, prototype) {
              useTypeToJulia <- notSimpleVector(object) # the general rule
              ## the length test:  KLUDGE alert:  Julia v 0.6 has a bug that prints an erroneous warning the first time on complex
              useTypeToJulia <- useTypeToJulia || (if(is.complex(object)) length(object) < 2 else 
                  length(object) < prototype$.ev$largeObject)
               if(useTypeToJulia)
                  callNextMethod()
              else {
                  file <- tempfile("toJulia")
                  on.exit(addServerWrapup(gettextf('base::system2("rm","%s")', file)))
                  writeBin(as.vector(object), file)
                  gettextf('binaryRVector("%s", "%s", %d)', file, typeof(object), length(object))
              }
          })

# no longer used
## setMethod("asServerObject", c("character", "JuliaObject"),
##           function(object, prototype) {
##               n <- length(object)
##               if(n < prototype$.ev$largeObject || notSimpleVector(object) )
##                  return(callNextMethod())
##               file1 <- tempfile("toJulia")
##               file2 <- tempfile("toJulia")
##               on.exit(addServerWrapup(gettextf('base::system2("rm","%s")', paste(file1, file2))))
##               writeChar(as.vector(object), file1, eos = NULL) # write with no trailing \0 bytes
##               writeBin(nchar(object), file2)
##               gettextf("readRStrings(\"%s\", \"%s\", %d)", file1, file2, length(object))
##           })

addServerWrapup <- function(command, .ev = RJulia())
    .ev$serverWrapup <- c(.ev$serverWrapup,command)



## these methods are "copied" from XR only to avoid ambiguity when selecting
## Otherwise they score the same as the c("ANY", "JuliaObject") method
## which would be wrong if selected (and either way, a message is printed).
## TODO:  copy man/asServerObject.Rd documentation from XR ??
.copyFromXR <- c("AssignedProxy","ProxyClassObject","ProxyFunction", "name")
for(Class in .copyFromXR)
    setMethod("asServerObject", c(Class, "JuliaObject"),
              selectMethod("asServerObject", Class))

#' @describeIn functions
#' sends the \code{object} to Julia, converting it via methods for
#' \code{\link[XR]{asServerObject}} and returns a proxy for the converted object.
juliaSend <- function(object, evaluator = XR::getInterface(.JuliaInterfaceClass))
                          evaluator$Send(object)
#' @describeIn functions
#' converts the proxy object that is its argument to an \R{} object.
juliaGet <- function(object, evaluator = XR::getInterface(.JuliaInterfaceClass))
    evaluator$Get(object)

#' @describeIn functions
#' Print an object in Julia.  Either one object or several arguments as would
#' be given to the Eval() method.
juliaPrint <- function(object, ..., evaluator = XRJulia::RJulia()) {
    if(length(list(...)))
       object <- evaluator$Eval(object, ...)
    evaluator$Command("print(%s)",object)
    cat("\n")
}

#' @describeIn functions
#' evaluates the \code{expr} string subsituting the arguments.  See the corresponding evaluator
#' method for details.
#' @param expr A string that should be legal when parsed and evaluated in Julia.
juliaEval <- function(expr, ..., evaluator = XR::getInterface(.JuliaInterfaceClass))
    evaluator$Eval(expr, ...)

#' @describeIn functions
#' evaluates the \code{expr} string subsituting the arguments; used for a command that is not
#' an expression.
juliaCommand <- function(expr, ..., evaluator = XR::getInterface(.JuliaInterfaceClass))
    evaluator$Command(expr, ...)

#' @describeIn functions
#' call the function in Julia, with arguments given; expr is the string name of the function
juliaCall <- function(expr, ..., evaluator = XR::getInterface(.JuliaInterfaceClass))
    evaluator$Call(expr, ...)

#' @describeIn functions
#' serialize the \code{object} in Julia
#' @param file,append,all Arguments to the evalutor's serialize and unserialize methods. See the reference,
#' Chapter 10.
#' @param object A proxy in R for a Julia object.
juliaSerialize <- function(object,  file, append = FALSE, evaluator = XR::getInterface(.JuliaInterfaceClass))
    evaluator$Serialize(object, file, append)

#' @describeIn functions
#' unserialize the file in Julia
juliaUnserialize <- function(file, all = FALSE, evaluator = XR::getInterface(.JuliaInterfaceClass))
    evaluator$Unserialize(file, all)

#' @describeIn functions
#' return the name by which this proxy object was assigned in Julia
juliaName <- function(object)
    XR::proxyName(object)

#' @describeIn functions
#' Import a Julia module or add a directory to the Julia Search Path
#' If called from the source directory of a package during installation, both \code{juliaImport}
#' and \code{juliaAddToPath()} also set up
#' a load action for that package.  The functional versions, not the methods themselves, should
#' be called from package source files to ensure that the load actions are created.
juliaImport <- function( ...,  evaluator) {
    if(missing(evaluator))
        XR::serverImport("JuliaInterface", ...)
    else
        evaluator$Import(...)
}

#' Get and/or Set Internal Option Parameters in the Julia Evaluator
#'
#' The Julia code for an evaluator maintains a dictionary, \code{RJuliaParams}, of named parameters used to control
#' various evaluation details.  These and any other desired options can be queried and/or set by calls to \code{juliaOptions}.
#' 
#' The function behaves essentially like the \code{\link{options}()} function in R itself, returning a list of the current entries
#' corresponding to unnamed character arguments and setting the parameters named to the value in the corresponding named argument
#' to \code{juliaOptions}.
#' If no parameter corresponding to a name has been set, requesting the corresponding returned value is \code{nothing}, \code{NULL} in R.
#' @param ... arguments to the corresponding method for an evaluator object.
#' @param .ev The evaluator object to use.  By default, and usually, the current evaluator.
#' @return A named list of those parameters requested (as unnamed character string arguments).  If none, an empty list.
#' Note that options are always returned converted to R, not as proxyies.
juliaOptions <- function(..., .ev = XRJulia::RJulia()) {
    args <- list(...)
    toSet <- nzchar(allNames(args))
    value <- list()
    if(any(toSet)) {
        setArgs <- args[toSet]
        ## check for some special options and set the corresponding field in the evaluator.
        ## This also has the effect of validating the object's class.
        for(special in c("largeObject", "fileBase"))
            if(!is.null(setArgs[[special]]))
               .ev$field(special, setArgs[[special]])
        .ev$Call("RJuliaSetParams", setArgs)
    }
    if(any(!toSet))
        value <- .ev$Call("RJuliaGetParams", unlist(args[!toSet]), .get = TRUE)
    value
}


## proxy Julia functions
JuliaFunction <- setClass("JuliaFunction",
                          contains = "ProxyFunction")

setMethod("initialize", "JuliaFunction",
     function (.Object, name, module = "", evaluator = RJulia(), ...)
    {
        if(nzchar(module) && !grepl(".", name, fixed = TRUE))
            name <- paste(module, name, sep=".")
        else if(!nzchar(module) && grepl(".", name, fixed = TRUE))
            module <- gsub("[.][^.]*$", "", name)
        value <- callNextMethod(.Object, name, evaluator = evaluator, module = module, ...)
        if(nzchar(module)) {
            ## ensure module is imported
            fn <- value@.Data
            bdy <- body(fn)
            body(fn) <- substitute({.evaluator$Import(MODULE); BDY},
                                   list(MODULE = module, BDY = bdy))
            value@.Data <- fn
        }
        value
    }
)

#' Information about a Julia Class
#'
#' The Julia class definition information is computed, and converted to R.
#' @return the Julia definition of the specified class, optionally from the module.
#' @param Class,module Strings identifying the Julia composite type and optionally, the module containing it.
#' @param ...,.ev Don't supply these, \code{.ev} defaults to the current Julia interface evaluator.
juliaClassDef <- function(Class, module = "", ..., .ev = RJulia()) {
    if(nzchar(module))
        .ev$Import(module, Class)
    .ev$Eval(gettextf("classInfo(%s)", Class), .get = TRUE)
}

#' Define a Proxy Julia Class (Composite Type)
#'
#' Given the name and optionally the module for a Julia composite type, defines an R proxy
#' class with the same fields as the Julia type.  By default, uses metadata from Julia to
#' find the fields.  If the call supplies the desired field names explicitly, metadata is
#' not used.
#' @param juliaType,module Strings identifying the composite type and optionally the module containing it. In normal use,
#' metadata from Julia is used to find the definition of the type.
#' @param fields,where,proxyObjectClass,... Overriding arguments that should not be used by direct calls from package source code.
setJuliaClass <- function (juliaType, module = "", fields = character(),
    where = topenv(parent.frame()),
    proxyObjectClass = "JuliaObject", ...)
{
    XR::setProxyClass(juliaType, module, fields,
        methods = if(length(fields)) character() else NULL, ServerClass = juliaType,
        where = where, evaluatorClass = "JuliaInterface",
        proxyObjectClass = proxyObjectClass, language = "Julia", ...)
}


## a table to convert Julia arrays of non-standard types, not handled by the signature = c("vectorR", "ANY")
## method for asRObject
doSpecialVector <- list( raw = function(object) as.raw(unlist(object@data))) # should check, but these came from UInt8

setMethod("asRObject", c("vector_R","JuliaInterface"),
          function (object, evaluator) {
              if(is.function(doSpecialVector[[object@type]]))
                  doSpecialVector[[object@type]](object)
              else
                  callNextMethod() # signature c("vectorR", "ANY")
          })

#' Class for General Julia Composite Type Objects
#'
#' The Julia side of the interface will return a general object from a composite type as an R
#' object of class "from_Julia.  its Julia fields (converted to R objects) can be accessed by the \code{$}
#' operator.
#'
#' @slot serverClass the Julia type.
#' @slot module the Julia module, or ""
#' @slot fields the converted versioin of the Julia fields; these are accessed by the \code{$} operator.
setClass("from_Julia", contains = "from_Server")

setMethod("show", "from_Julia",
          function(object) {
              cat(gettextf("R conversion of Julia object of composite type %s\n\nJulia fields:\n",
                           dQuote(object@serverClass)))
              methods::show(as.list(object@fields))
          })

setMethod("initialize", "from_Julia",
    function (.Object, ...)
    {
        .Object@language <- "Julia"
        callNextMethod(.Object, ..., referenceClass = TRUE)
    }
)

## a class for returning large vectors in simple form
setClass("vector_R_direct", slots = c(file = "character", type = "character", length = "integer"))

setMethod("asRObject", c("vector_R_direct", "JuliaInterface"),
          function(object, evaluator) {
              on.exit(base::system(paste("rm ",object@file)))
              readBin(object@file, object@type, object@length)
          })

## ## a class for returning long character vectors
## setClass("character_R_direct", slots = c(stringFile = "character", nbytes = "integer", ncharsFile = "character", length = "integer"))

## setMethod("asRObject", c("character_R_direct", "JuliaInterface"),
##           function(object, evaluator) {
##               on.exit(system(paste("rm ",object@stringFile, object@ncharsFile)))
##               string <- readChar(object@stringFile, object@nbytes, TRUE)
##               n <- object@length
##               lens <- readBin(object@ncharsFile, "integer", n)
##               last <- cumsum(lens)
##               first <- c(0L, last[-n])+1
##               substring(string,first,last)
##           })
johnmchambers/XRJulia documentation built on May 19, 2019, 5:16 p.m.