[ Partly continued from the thread earlier this month https://stat.ethz.ch/pipermail/r-devel/2025-July/084096.html in turn continued from discussions in 2016 https://stat.ethz.ch/pipermail/r-devel/2016-August/072970.html https://stat.ethz.ch/pipermail/r-devel/2016-November/073385.html ... ]
As I seem to have converged on a (tested, timed, ...) 'ifelse' analogue, I'd like to point again to the repository housing it: https://github.com/jaganmn/ifelse/ The home page has a "matrix" comparing the behaviours of all of the 'ifelse' analogues that I know about, including mine but at the moment excluding ones collected by Martin in 2016 ... https://gist.github.com/mmaechler/9cfc3219c4b89649313bfe6853d87894 ... which I'd not seen until recently. Besides handling type, class, length, attributes, etc. in a predictable way, a nice feature of ifelse::ifelse1 is that it is "generic" in the following sense: as long as there are suitable methods for generic functions '[', '[<-', 'c', and 'length', ifelse::ifelse1 will work for abstract classes of vectors, not limited to the "basic" classes raw, logical, integer, etc. Indeed, generic behaviour was a main point of discussion in 2016, and at the time Martin even encapsulated some very nice tests in a function chkIfelse(); see his GH gist above. After I tweak the body of chkIfelse() (to reflect changes since 2016 in 'base' and 'Matrix'), chkIfelse(ifelse::ifelse1) passes with a few exceptions related either to a bug in R itself https://bugs.r-project.org/show_bug.cgi?id=18919 or to a case where I have thought that a test *should* fail [*]. See the output of $ diff -u tests/chkIfelse.R.orig tests/chkIfelse.R for my changes to chkIfelse() and $ R --vanilla -f tests/chkIfelse.R for an indication of which tests fail. (You will want to have installed the suggested packages Matrix, Rmpfr, and zoo.) ifelse::ifelse1 is also fast, comparable to single-threaded data.table::fifelse in the most common usage where none of the arguments has a class attribute and there is no need for method dispatch. This common usage is handled by a lower level function, ifelse::.ifelse1 (with dot), also exported. Of course, we lose this speed if we remove if (!(is.object(yes) || is.object(no) || is.object(na))) return(.Call(R_ifelse_ifelse1, ltest, yes, no, na)) from body(ifelse::ifelse1), which we might do because, e.g., it may not be desirable for R-core to maintain additional C code. My tests/timings.Rout suggests that the function without .Call would still be nontrivially (though not orders of magnitude) faster than base::ifelse in the most common usage. This RFC asks: 1. Are there behaviour or API changes that I should consider before possibly submitting the package to CRAN? 2. Are there comments from R-core about the suitability for 'base'? I am personally a bit agnostic about it, but I could file a "wish" at Bugzilla on behalf of people who feel strongly. Grateful for feedback or further testing, comparison, ... Mikael [*] chkIfelse(FUN) asserts that identical(x, FUN(x != 0, x, x)) is TRUE for 'x' inheriting from virtual class Matrix (from package 'Matrix'). It is not TRUE when FUN=ifelse::ifelse1, because the class of the return value of ifelse::ifelse1(x != 0, x, x) is the class of c(x[0], x[0]), and x[0] is a traditional vector if 'x' is a Matrix. Martin's proposal (the function named 'ifelse2' in his GH gist) adds logic to handle this special case whereas ifelse::ifelse1 does not, on purpose. The logic involves a test of identical(class(yes), class(no)), but that is a bit unsatisfactory to me: in particular, should the behaviour of 'FUN' really differ if class(no) is a simple *subclass* of (hence compatible but not identical to) class(yes)? > loadNamespace("Matrix") > x <- new("dsyMatrix", Dim = c(1L, 1L), x = 1) > y <- new("dpoMatrix", Dim = c(1L, 1L), x = 1) > identical(class(x), class(y)) [1] FALSE > extends(class(x), class(y)) [1] FALSE > extends(class(y), class(x)) [1] TRUE > MMgist::ifelse2(x != 0, x, x) # MMgist::<name> is pseudocode 1 x 1 Matrix of class "dgeMatrix" [,1] [1,] 1 > MMgist::ifelse2(x != 0, x, y) [1] 1 > ifelse::ifelse1(x != 0, x, x) [,1] [1,] 1 > ifelse::ifelse1(x != 0, x, y) [,1] [1,] 1 I claim that the consistent behaviour of ifelse::ifelse1 is nicer even if the return value does not preserve inheritance from virtual class Matrix. After all, the *user* can arrange to preserve inheritance *if desired* with: > z <- x > z[] <- ifelse::ifelse1(x != 0, x, x) > z 1 x 1 Matrix of class "dgeMatrix" [,1] [1,] 1 For an S3 example, consider time series objects: > x <- ts(1) > y <- structure(x, class = c("zzz", class(x))) > identical(class(x), class(y)) [1] FALSE > isa(x, class(y)) [1] TRUE > isa(y, class(x)) [1] FALSE > ifelse2(x != 0, x, x) Time Series: Start = 1 End = 1 Frequency = 1 [1] 1 > ifelse2(x != 0, x, y) [1] 1 > ifelse::ifelse1(x != 0, x, x) [1] 1 > ifelse::ifelse1(x != 0, x, y) [1] 1 Then: > z <- x > z[] <- ifelse::ifelse1(x != 0, x, x) > z Time Series: Start = 1 End = 1 Frequency = 1 [1] 1 Of course, new logic involving 'extends' and 'isa' could be incorporated. But it seems cleaner to me to leave out such special case logic, which is liable to introduce unintended "discontinuities" in behaviour and make the source code less transparent to non-experts. Indeed, my preference is to clearly document examples like the above, which are of no concern to 99% of users) and advertise work-arounds like { z <- yes OR no OR na; z[] <- ifelse::ifelse1(test, yes, no, na); z}, which gives the remaining 1% a clue as well as a bit more control ... ______________________________________________ R-devel@r-project.org mailing list https://stat.ethz.ch/mailman/listinfo/r-devel