Search code examples
rfunctionfunctional-programmingnamespacesellipsis

Passing elipsis argument to be evaluated within function call within Reduce


Background

I have a fun function that compares vectors within a list using all.equal. As I'm making use of the all.equal I would like to pass relevant all.equal arguments via ellipsis. Without the need to pass anything to all.equal the function works as desired.

Function goals

  • The function aims to provide modified version of all.equal call working on any number of vectors
  • Each of the vectors can have any number of elements; however, if all vectors are not of equal lengths the function will return false.
  • The function should be able to take advantage of arguments available in all.equal. For instance vectors c(1.1, 2), c(1, 2) and c(1.3, 2) will be considered equal if tolerance argument with the right value is provided. The question is concerned with getting this to work.

Example

Comparing 1,000 vectors, each consisting of three integers.

    compare_multiple_vectors(x = lapply(
              X = vector(mode = "list", length = 1e3),
              FUN = function(...) {
                  c(1, 2, 3)
              }
          ))

    # [1] TRUE

Problem / desired results

all.equal called with tolerance = 1 on the following list of vectors will return expected TRUE

all.equal(c(1,2), c(1,1), tolerance = 1)
# [1] TRUE

The tolerance = 1 argument fails to filter down to inside Reduce.

compare_multiple_vectors(x = list(c(1,2), c(1,1)), tolerance = 1)
# [1] FALSE

The desired result should be TRUE.


Code

#' @title Compare Values of Multiple Vectors
#'
#' @description The function compares values across multiple vectors using
#'   \code{\link[base]{all.equal}}.
#'
#' @param x Alist of vectors to compare
#' @param ... as in  \code{\link[base]{all.equal}}
#'
#' @return A logical
#'
#' @export
#'
#' @importFrom checkmate assert_atomic_vector
#'
#' @examples
#' # Returns TRUE
#' compare_multiple_vectors(c(1,1,1), c(1,1,1))
#' # Returns FALSE
#' compare_multiple_vectors(c(1,1,1), c(1,1,1), c(1,2,1))
#' # Returns FALSE
#' compare_multiple_vectors(c(1,2,3), c(3,2,1))
compare_multiple_vectors <- function(x, ...) {
    # Check if all elements of x are atomic vectors
    Vectorize(FUN = checkmate::assert_atomic_vector,
              vectorize.args = "x")(x)

    # Compare list elements
    Reduce(
        f = function(a, b, ...) {
            if (isTRUE(all.equal(target = a, current = b, ...))) {
                a
            } else {
                FALSE
            }
        },
        x = x
    ) -> res_red

    # Return results
    if (isFALSE(res_red)) {
        return(FALSE)
    } else {
        return(TRUE)
    }
}

Notes

  • I'm interested in making use of ellipsis and leaving the initial call the way it is with

    compare_multiple_vectors(x = list_of_vectors_to_compare, 
                             ... # all.equal arguments
                             )
    

Solution

  • I think just need a little change:

    compare_multiple_vectors <- function(x, ...) {
        # Check if all elements of x are atomic vectors
        Vectorize(FUN = checkmate::assert_atomic_vector,
                  vectorize.args = "x")(x)
    
        # Compare list elements
        Reduce(
            f = function(a, b) {  # <===================== Remove *...*
    
                if (isTRUE(all.equal(target = a, current = b, ...))) {
                    a
                } else {
                    FALSE
                }
            },
            x = x
        ) -> res_red
    
        # Return results
        if (isFALSE(res_red)) {
            return(FALSE)
        } else {
            return(TRUE)
        }
    }
    

    The arguments f of Reduce seems to have a signature like function(x, y). So Reduce will ignore ... in f. If remove the ellipsis of f, ... will reference from the outer space, and will have the right result you want.