Skip to content

Commit ff064d8

Browse files
committed
Implement convert_lazy()
Fixes #428
1 parent 4dde531 commit ff064d8

7 files changed

Lines changed: 117 additions & 0 deletions

File tree

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export(class_numeric)
8181
export(class_raw)
8282
export(class_vector)
8383
export(convert)
84+
export(convert_lazy)
8485
export(method)
8586
export(method_explain)
8687
export(methods_register)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* `convert()` now falls back to the corresponding `as.*()` function (e.g. `as.character()`) when converting to a base type like `class_character` and no method or inheritance-based default applies, so `convert(1, class_character)` works out of the box (#472).
1111
* `convert()` accepts a single unnamed list of property overrides when downcasting, as a shortcut for individual name-value pairs (#497).
1212
* `convert()` no longer errors when `from` is a base or S3 object and `to` is an S7 class that inherits from `from`'s class. The base/S3 value is now passed as `.data` to the `to` constructor (#537).
13+
* New `convert_lazy()` is a non-strict variant of `convert()` that returns `from` unchanged if it already inherits from `to`, preserving any extra properties instead of stripping them (#428).
1314
* `method<-` now works for double-dispatch operators (e.g. `+`, `==`, `%*%`) with plain S3 or S4 classes, even when neither operand is an S7 object (#544).
1415
* `method<-` no longer embeds a copy of a generic owned by another package in your package namespace. Instead it returns a sentinel value that the new `S7_on_build()` removes from the namespace at build time; call `S7_on_build()` at the top level of `zzz.R` (see `vignette("packages")`) (#364).
1516
* `method<-` now accepts `NULL` to unregister an existing method, e.g. `method(foo, class_character) <- NULL` (#613).

R/convert.R

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
#' programmatically.
5555
#' @return Either `from` coerced to class `to`, or an error if the coercion
5656
#' is not possible.
57+
#' @seealso [convert_lazy()] for a non-strict variant that leaves `from`
58+
#' unchanged when it already inherits from `to`.
5759
#' @export
5860
#' @examples
5961
#' Foo1 := new_class(properties = list(x = class_integer))
@@ -138,6 +140,45 @@ convert <- function(from, to, ...) {
138140
}
139141
}
140142

143+
#' Non-strict conversion
144+
#'
145+
#' @description
146+
#' `convert_lazy()` is a non-strict variant of [convert()] that guarantees
147+
#' that the result inherits from `to`, without forcing `from` to become an exact
148+
#' instance of `to`, i.e. it never upcasts.
149+
#'
150+
#' @inheritParams convert
151+
#' @return `from`, unchanged, if it already inherits from `to`; otherwise the
152+
#' result of `convert(from, to, ...)`.
153+
#' @seealso [convert()] for the strict variant that always returns an exact
154+
#' instance of `to`.
155+
#' @export
156+
#' @examples
157+
#' Foo1 := new_class(properties = list(x = class_integer))
158+
#' Foo2 := new_class(Foo1, properties = list(y = class_double))
159+
#'
160+
#' # `convert()` upcasts by stripping the extra properties of `from`:
161+
#' convert(Foo2(x = 1L, y = 2), to = Foo1)
162+
#'
163+
#' # `convert_lazy()` never upcasts: because the object already inherits from
164+
#' # Foo1, it's returned unchanged, keeping `y`:
165+
#' convert_lazy(Foo2(x = 1L, y = 2), to = Foo1)
166+
#'
167+
#' # When `from` doesn't inherit from `to`, `convert_lazy()` falls back to
168+
#' # `convert()`, so it can still downcast or coerce to a base type:
169+
#' convert_lazy(Foo1(x = 1L), to = Foo2, y = 2.5)
170+
#' convert_lazy(1.5, to = class_character)
171+
convert_lazy <- function(from, to, ...) {
172+
to <- as_class(to)
173+
check_can_inherit(to)
174+
175+
if (class_inherits(from, to)) {
176+
from
177+
} else {
178+
convert(from, to, ...)
179+
}
180+
}
181+
141182
# Resolve the `convert()` method for converting `from` to `to`, or `NULL` if
142183
# there's no registered method (so `convert()` falls back to its defaults).
143184
# See convert docs for the motivation for this design.

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ reference:
2525
- title: Method dispatch
2626
contents:
2727
- convert
28+
- convert_lazy
2829
- class_missing
2930
- class_any
3031
- super

man/convert.Rd

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/convert_lazy.Rd

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-convert.R

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,24 @@ test_that("base type fallback sits below user methods and inheritance", {
245245
expect_null(attributes(obj))
246246
expect_identical(obj, "hi")
247247
})
248+
249+
test_that("convert_lazy() leaves `from` untouched if it inherits from `to` (#428)", {
250+
foo1 := new_class(properties = list(x = class_double))
251+
foo2 := new_class(foo1, properties = list(y = class_double))
252+
253+
obj <- foo2(x = 1, y = 2)
254+
expect_identical(convert_lazy(obj, to = foo1), obj)
255+
expect_identical(convert_lazy(obj, to = foo2), obj)
256+
})
257+
258+
test_that("convert_lazy() falls back to convert() when not a subtype", {
259+
foo1 := new_class(properties = list(x = class_double))
260+
foo2 := new_class(foo1, properties = list(y = class_double))
261+
262+
# Downcasting still works
263+
obj <- convert_lazy(foo1(x = 1), to = foo2, y = 2.5)
264+
expect_equal(obj, foo2(x = 1, y = 2.5))
265+
266+
# As does casting to a different class
267+
expect_identical(convert_lazy(1.5, class_character), "1.5")
268+
})

0 commit comments

Comments
 (0)