.validatePipelineDef <- function(object){ e <- c() if( !is.list(object@functions) || !all(vapply(object@functions, is.function, logical(1))) ) e <- c("`functions` should be a (named) list of functions!") if(!all(vapply( object@functions, FUN.VALUE=logical(1), FUN=function(x) "x" %in% names(formals(x))))) e <- c(e, "Each function should at least take the argument `x`.") isf <- function(x) is.null(x) || is.function(x) if( !is.list(object@aggregation) || !all(vapply(object@aggregation, isf, logical(1))) ) stop("`aggregation` should be a list of functions and/or NULL slots!") if( !is.list(object@evaluation) || !all(vapply(object@evaluation, isf, logical(1))) ) stop("`evaluation` should be a list of functions and/or NULL slots!") if(!all(names(object@descriptions)==names(object@functions))) e <- c(e, "descriptions do not match functions.") if(!all(names(object@evaluation)==names(object@functions))) e <- c(e, "evaluation do not match functions.") if(!all(names(object@aggregation)==names(object@functions))) e <- c(e, "aggregation do not match functions.") args <- unlist( lapply( object@functions, FUN=function(x){ setdiff(names(formals(x)), "x") }) ) if(any(duplicated(args))) e <- c(e, paste("Some arguments (beside `x`) is", "used in more than one step, which is not currently supported.")) if(length( wa <- setdiff(names(object@defaultArguments),args) )>0) e <- c(e, paste("The following default arguments are not in the pipeline's functions:", paste(wa, collapse=", "))) if(length(e) == 0) TRUE else e } #' @import methods #' @exportClass PipelineDefinition setClass( "PipelineDefinition", slots=representation( functions="list", descriptions="list", evaluation="list", aggregation="list", initiation="function", defaultArguments="list", misc="list" ), prototype=prototype( functions=list(), descriptions=list(), evaluation=list(), aggregation=list(), initiation=identity, defaultArguments=list(), misc=list() ), validity=.validatePipelineDef ) #' PipelineDefinition #' #' Creates on object of class `PipelineDefinition` containing step functions, #' as well as optionally step evaluation and aggregation functions. #' #' @param functions A list of functions for each step #' @param descriptions A list of descriptions for each step #' @param evaluation A list of optional evaluation functions for each step #' @param aggregation A list of optional aggregation functions for each step #' @param initiation A function ran when initiating a dataset #' @param defaultArguments A lsit of optional default arguments #' @param misc A list of whatever. #' @param verbose Whether to output additional warnings (default TRUE). #' #' @return An object of class `PipelineDefinition`, with the slots functions, #' descriptions, evaluation, aggregation, defaultArguments, and misc. #' #' @aliases PipelineDefinition-class #' @seealso \code{\link{PipelineDefinition-methods}}, #' \code{\link{addPipelineStep}}. For an example pipeline, see #' \code{\link{scrna_pipeline}}. #' @export #' @examples #' PipelineDefinition( #' list( step1=function(x, meth1){ get(meth1)(x) }, #' step2=function(x, meth2){ get(meth2)(x) } ) #' ) PipelineDefinition <- function( functions, descriptions=NULL, evaluation=NULL, aggregation=NULL, initiation=identity, defaultArguments=list(), misc=list(), verbose=TRUE ){ if(!is.list(functions) || !all(vapply(functions, is.function, logical(1)))) stop("`functions` should be a (named) list of functions!") n <- names(functions) if(is.null(n)) n <- names(functions) <- paste0("step",seq_len(length(functions))) descriptions <- .checkInputList(descriptions, functions, FALSE) evaluation <- .checkInputList(evaluation, functions) aggregation2 <- .checkInputList(aggregation, functions) names(aggregation2)<-names(evaluation)<-names(descriptions)<-names(functions) for(f in names(aggregation2)){ if(is.null(aggregation2[[f]]) && !is.null(evaluation[[f]]) && !(f %in% names(aggregation))) aggregation2[[f]] <- defaultStepAggregation } if(is.null(misc)) misc <- list() x <- new("PipelineDefinition", functions=functions,descriptions=descriptions, evaluation=evaluation, aggregation=aggregation2, initiation=initiation, defaultArguments=defaultArguments, misc=misc) w <- which( !vapply(x@aggregation, is.null, logical(1)) & vapply(x@evaluation, is.null, logical(1)) ) if(verbose && length(w)>0){ warning(paste("An aggregation is defined for some steps that do not have", "a defined evaluation function: ", paste(names(x@functions)[w], collapse=", "), "It is possible that evaluation is performed by the step's", "function itself.") ) } x } .checkInputList <- function( x, fns, containsFns=TRUE, name=deparse(substitute(x)) ){ name <- paste0("`",name,"`") if(!is.null(x)){ if(length(x)!=length(fns)){ if(is.null(names(x))) stop("If ", name, " does not have the same length as the number of ", "steps, its slots should be named.") if(length(unknown <- setdiff(names(x),names(fns)))>0) stop("Some elements of ",name," (",paste(unknown,collapse=", "),")", "are unknown.") x <- lapply(names(fns), FUN=function(f){ if(is.null(x[[f]])) return(NULL) x[[f]] }) names(x) <- names(fns) } if( !is.null(names(x)) ){ if(!all(names(x)==names(fns)) ) stop("The names of ",name," should match those of `functions`") } }else{ x <- lapply(fns,FUN=function(x) NULL) } if(containsFns && !all(vapply(x, FUN=function(x) is.null(x) || is.function(x), logical(1)))) stop(name," should be a list of functions") x } #' Methods for \code{\link{PipelineDefinition}} class #' @name PipelineDefinition-methods #' @rdname PipelineDefinition-methods #' @aliases PipelineDefinition-method #' @seealso \code{\link{PipelineDefinition}}, \code{\link{addPipelineStep}} #' @param object An object of class \code{\link{PipelineDefinition}} #' @return Depends on the method. #' @examples #' pd <- mockPipeline() #' length(pd) #' names(pd) #' pd$step1 #' pd[2:1] NULL #' @rdname PipelineDefinition-methods #' @importMethodsFrom methods show #' @importFrom knitr opts_current setMethod("show", signature("PipelineDefinition"), function(object){ # colors and bold are going to trigger errors when rendered in a knit, so # we disable them when rendering isKnit <- tryCatch( isTRUE(getOption('knitr.in.progress')) || length(knitr::opts_current$get())>0, error=function(e) FALSE) fns <- unlist(lapply(names(object@functions), FUN=function(x){ x2 <- x if(!isKnit) x2 <- paste0("\033[1m",x,"\033[22m") y <- lapply( names(formals(object@functions[[x]])), FUN=function(n){ if(!is.null(def <- object@defaultArguments[[n]])) n <- paste0(n,"=",deparse(def,100,FALSE)) n }) y <- paste0(" - ", x2, "(", paste(unlist(y), collapse=", "), ")") if(!is.null(object@evaluation[[x]]) || !is.null(object@aggregation[[x]])) y <- paste0(y, ifelse(isKnit, " * ", " \033[34m*\033[39m ")) if(!is.null(object@descriptions[[x]])){ x2 <- object@descriptions[[x]] if(!isKnit) x2 <- paste0("\033[3m",x2,"\033[23m") y <- paste(y, x2, sep="\n") } y })) cat("A PipelineDefinition object with the following steps:\n") cat(paste(fns,collapse="\n")) cat("\n") }) #' get names of PipelineDefinition steps #' @rdname PipelineDefinition-methods #' @param x An object of class \code{\link{PipelineDefinition}} setMethod("names", signature("PipelineDefinition"), function(x){ names(x@functions) }) #' set names of PipelineDefinition steps #' @rdname PipelineDefinition-methods #' @param value Replacement values setMethod("names<-", signature("PipelineDefinition"), function(x, value){ if(any(duplicated(value))) stop("Some step names are duplicated!") names(x@functions) <- value names(x@evaluation) <- value names(x@aggregation) <- value names(x@descriptions) <- value validObject(x) x }) #' @rdname PipelineDefinition-methods #' @param name The step name setMethod("$", signature("PipelineDefinition"), function(x, name){ x@functions[[name]] }) #' @rdname PipelineDefinition-methods setMethod("length", signature("PipelineDefinition"), function(x){ length(x@functions) }) #' @rdname PipelineDefinition-methods #' @param i The index(es) of the steps setMethod("[",signature("PipelineDefinition"), function(x, i){ new("PipelineDefinition", functions=x@functions[i], descriptions=x@descriptions[i], evaluation=x@evaluation[i], aggregation=x@aggregation[i], misc=x@misc) }) #' @rdname PipelineDefinition-methods setMethod("as.list",signature("PipelineDefinition"), function(x){ x@functions }) #' @exportMethod arguments #' @rdname PipelineDefinition-methods setGeneric("arguments", function(object) args(object)) #' @rdname PipelineDefinition-methods setMethod("arguments",signature("PipelineDefinition"), function(object){ lapply(object@functions, FUN=function(x){ setdiff(names(formals(x)), "x") }) }) #' @rdname PipelineDefinition-methods #' @exportMethod defaultArguments setGeneric("defaultArguments", function(object) NULL) #' @exportMethod defaultArguments<- #' @rdname PipelineDefinition-methods setGeneric("defaultArguments<-", function(object, value) NULL) #' @rdname PipelineDefinition-methods setMethod("defaultArguments",signature("PipelineDefinition"), function(object){ object@defaultArguments }) #' @rdname PipelineDefinition-methods setMethod( "defaultArguments<-",signature("PipelineDefinition"), function(object, value){ object@defaultArguments <- value validObject(object) object }) #' @exportMethod stepFn #' @rdname PipelineDefinition-methods setGeneric("stepFn", function(object, step=NULL, type) standardGeneric("stepFn")) #' @param step The name of the step for which to set or get the function #' @param type The type of function to set/get, either `functions`, #' `evaluation`, `aggregation`, `descriptions`, or `initiation` (will parse #' partial matches) #' @rdname PipelineDefinition-methods setMethod("stepFn", signature("PipelineDefinition"), function(object, step=NULL, type){ ft <- c("functions","evaluation","aggregation","descriptions","initiation") type <- match.arg( type, ft ) if(is.null(step)) return(slot(object, type)) step <- match.arg(step, names(object)) slot(object, type)[[step]] }) #' @exportMethod stepFn<- #' @rdname PipelineDefinition-methods setGeneric( "stepFn<-", function(object, step, type, value) standardGeneric("stepFn<-") ) #' @rdname PipelineDefinition-methods setMethod( "stepFn<-", signature("PipelineDefinition"), function(object, step, type, value){ ft <- c("functions","evaluation","aggregation","descriptions","initiation") type <- match.arg(type, ft) if(type!="descriptions" && !is.null(value) && !is.function(value)) stop("Replacement value should be a function.") if(type=="initiation"){ slot(object, type) <- value }else{ step <- match.arg(step, names(object)) slot(object, type)[[step]] <- value } if(type=="evaluation" && !is.null(value)){ # also add the default aggregation: if(is.null(slot(object, "aggregation")[[step]])) slot(object, "aggregation")[[step]] <- defaultStepAggregation } object }) #' addPipelineStep #' #' Add a step to an existing \code{\link{PipelineDefinition}} #' #' @param object A \code{\link{PipelineDefinition}} #' @param name The name of the step to add #' @param after The name of the step after which to add the new step. If NULL, #' will add the step at the beginning of the pipeline. #' @param slots A optional named list with slots to fill for that step (i.e. #' `functions`, `evaluation`, `aggregation`, `descriptions` - will be parsed) #' #' @return A \code{\link{PipelineDefinition}} #' @seealso \code{\link{PipelineDefinition}}, #' \code{\link{PipelineDefinition-methods}} #' @importFrom methods is slot #' @export #' #' @examples #' pd <- mockPipeline() #' pd #' pd <- addPipelineStep(pd, name="newstep", after="step1", #' slots=list(description="Step that does nothing...")) #' pd addPipelineStep <- function(object, name, after=NULL, slots=list()){ if(!is(object, "PipelineDefinition")) stop("object should be a PipelineDefinition") if(name %in% names(object)) stop("There is already a step with that name!") if(!is.null(after) && !(after %in% names(object))) stop("`after` should either be null or the name of a step.") n <- c("functions","evaluation","aggregation","descriptions") if(length(slots)>0) names(slots) <- vapply( names(slots), choices=n, FUN=match.arg, FUN.VALUE=character(1) ) if(!all(names(slots) %in% n)) stop(paste("fns should be a function or a list", "with one or more of the following names:\n", paste(n,collapse=", "))) if(is.null(after)){ i1 <- vector("integer") i2 <- seq_along(names(object)) }else{ w <- which(names(object)==after) i1 <- seq_len(w) i2 <- seq.int(from=w+1, to=length(object)) if(w==length(object)) i2 <- vector("integer") } ll <- list(NULL) names(ll) <- name for(f in n) slot(object,f) <- c(slot(object,f)[i1], ll, slot(object,f)[i2]) for(f in names(slots)) stepFn(object, name, f) <- slots[[f]] if(is.null(stepFn(object, name, "functions"))) stepFn(object, name, "functions") <- identity validObject(object) object } #' mockPipeline #' #' A mock `PipelineDefinition` for use in examples. #' #' @return a `PipelineDefinition` #' @export #' #' @examples #' mockPipeline() mockPipeline <- function(){ PipelineDefinition( list( step1=function(x, meth1){ get(meth1)(x) }, step2=function(x, meth2){ get(meth2)(x) } ), evaluation=list( step2=function(x) c(mean=mean(x), max=max(x)) ), descriptions=list( step1="This steps applies meth1 to x.", step2="This steps applies meth2 to x."), defaultArguments=list(meth1=c("log","sqrt"), meth2="cumsum") ) }