Bug 17457 - as(1L:5L, "double") unexpectedly returns an integer vector
Summary: as(1L:5L, "double") unexpectedly returns an integer vector
Status: CLOSED FIXED
Alias: None
Product: R
Classification: Unclassified
Component: Language (show other bugs)
Version: R 3.5.0
Hardware: x86_64/x64/amd64 (64-bit) Other
: P5 trivial
Assignee: R-core
URL:
Depends on:
Blocks:
 
Reported: 2018-08-30 09:27 UTC by Nicolas Bennett
Modified: 2018-09-03 20:47 UTC (History)
3 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Nicolas Bennett 2018-08-30 09:27:22 UTC
As per the documentation of as(), as(x, "numeric") internally calls the as.numeric() function. Therefore I would expect as(1L:5L, "numeric") to return a double-precision vector.

str(as.numeric(1L:5L))
  num [1:5] 1 2 3 4 5
str(as(1L:5L, "numeric"))
  int [1:5] 1 2 3 4 5

For a function call like as(1L:5L, "double"), the Class argument is internally modified by as() as

if (.identC(Class, "double"))
   Class <- "numeric"

Therefore we have the same issue in this case as above: 

str(as.double(1L:5L))
  num [1:5] 1 2 3 4 5
str(as(1L:5L, "double"))
  int [1:5] 1 2 3 4 5

This unexpected behavior at least deserves to be mentioned in the as() docs. Additionally, to me personally, it seams wrong that a function call like as(1L:5L, "numeric") or as(1L:5L, "double") silently returns an integer vector.

sessionInfo()
R version 3.5.0 (2018-04-23)
Platform: x86_64-apple-darwin15.6.0 (64-bit)
Running under: macOS High Sierra 10.13.6

Matrix products: default
BLAS: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRlapack.dylib

locale:
[1] C/UTF-8/C/C/C/C

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base

other attached packages:
[1] colorout_1.2-0   prettycode_1.0.1 devtools_1.13.5

loaded via a namespace (and not attached):
[1] compiler_3.5.0     fs_1.2.5           withr_2.1.2        glue_1.3.0
[5] Rcpp_0.12.18       crayon_1.3.4       memoise_1.1.0      usethis_1.3.0.9000
[9] digest_0.6.15
Comment 1 Tomas Kalibera 2018-08-30 10:14:58 UTC
For reference this has been discussed before:
https://stat.ethz.ch/pipermail/r-devel/2015-December/072079.html
Comment 2 Benjamin Tyner 2018-08-30 11:03:30 UTC
Yes indeed. And see also,

https://stat.ethz.ch/pipermail/r-devel/2010-April/057098.html
Comment 3 Martin Maechler 2018-08-31 16:49:49 UTC
Thank you Tomas and Benjamin for unearthing previous thorough discussions about this.

As I was quite envolved there too, let me summarize what I got from re-reading the December 2015 discussion: 

- general agreement that this is undesirable and should be changed

- My own preference would have gone to implement (for classes)
  {numeric} := { double, integer }   

- In https://stat.ethz.ch/pipermail/r-devel/2015-December/072146.html , 
  John Chambers said that there are two levels of "fix", the first one

   fix to the complaint in the OP's subject heading is to conform to the default third argument, 
   strict=TRUE: as(1L, "numeric") == 1.0

   This generates some incompatibilities, as for classes that extend "numeric". 
   But still leaves class(1.0) "numeric" and typeof(1.0) "double".

   The workaround for class definitions that really need NOT to coerce integers to double is to define a class union, say
       setClassUnion("Number", c("numeric", "integer")) and use that for the slot.

 and the 2. level of fix would go use a virtual class  {numeric} := { double, integer }  but that this was too late now, as
    ... already in the 1998 S4 book, "numeric" was equated with "double".

 and then

> Fix 1. seems to me an actual bug fix, so my inclination would be to go with that (on r-devel), advertising that it may change the effective definition of some classes.
> But I can sympathize with choosing 1, 2 or neither.

IIRC, I was a somwhat unhappy with this assessment  ... and that may have been the reason I had not worked further on this at the time.

I'm going to have a good look now, following the suggestions John gave (some privately, in addition to those on R-devel).
Comment 4 Martin Maechler 2018-09-03 13:01:20 UTC
... so I have worked quite a bit on this, tried several versions, and came up with a "minimal change" version (not committed anywhere), which fulfills the following regression tests

stopifnot(exprs = {
    identical(as(1L,   "double"), 1.) # new
    ## "double" quite the same as "numeric" :
    c("double", "numeric") %in% extends(Dbl <- getClass("double"))
    "integer" %in% names(Dbl@subclasses)
    ## These all remain as they were :
    identical(as(1L,  "numeric"), 1L) # << may be disputable
    identical(as(TRUE, "double"), 1.)
    identical(as(TRUE,"numeric"), 1.)
    !is(TRUE, "numeric") # "logical" should _not_ be a subclass of "numeric"
    ## We agree these should not change :
    typeof(1.0) == "double"  &  typeof(1L) == "integer"
    class (1.0) == "numeric" &  class (1L) == "integer"
    mode  (1.0) == "numeric" &  mode  (1L) == "numeric"
})

The change is minimal in the sense that only the coercion to "double" newly coerces to double precision whereas the coercion to "numeric" leaves an integer as is.  That's the line I've marked as "disputable" above.

The advantage of this version is that it is more likely to not affect / break much current code out there in package space (e.g., it does not break "Matrix" nor my "Rmpfr" which also use these classes).

A somewhat more radical change -- which John Chambers would have suggested IIUC -- would in addition also entail that  as(1L,  "numeric")  coerce to double precision, too.

What do S4 experts think?
Comment 5 Martin Maechler 2018-09-03 14:31:18 UTC
One further piece of information: If I additionally also have

  as(1L, "numeric")   -->  1.0

then, indeed, existing code breaks: A slot which is declared to be "numeric" and then gets integer data will coerce that to double precision, e.g.,

 myN <- setClass("myN", contains="numeric", slots = c(truly = "numeric"))
 x <- myN(log(1:2), truly = 1:2)
 identical(x@truly, 1:2)

would give FALSE with the additional change mentioned above,
but remain TRUE (as in current and earlier versions of R) if I only implement
the "minimal change" from comment #4.

{Also re-reading what JMC wrote in Dec 2015, hence considering hacking setDataPart(), I don't see a way to achieve what may be desirable:  all(*, "numeric")  to coerce to double for "integer",  but *NOT* call that coercion for "numeric" slots which get class "integer" values assigned.

So, for now I even more strongly think the  "minimal change" to be the one to go with...
Comment 6 Martin Maechler 2018-09-03 20:47:50 UTC
Now committed the "minimal change" bug fix mentioned, as rev 75231 (to R-devel).