Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement convert() and super() #181

Merged
merged 34 commits into from
Mar 15, 2022
Merged

Implement convert() and super() #181

merged 34 commits into from
Mar 15, 2022

Conversation

hadley
Copy link
Member

@hadley hadley commented Feb 21, 2022

Fixes #110. Fixes #136

@hadley
Copy link
Member Author

hadley commented Feb 21, 2022

@lawremi points out that the main feature of up_cast() is not performance, but that it only affects the dispatch of a single method.

@hadley
Copy link
Member Author

hadley commented Feb 21, 2022

@hadley hadley changed the title Implement cast() Implement cast() and up_cast() Feb 22, 2022
@hadley hadley changed the title Implement cast() and up_cast() Implement cast() and cast_next() Feb 22, 2022
@hadley
Copy link
Member Author

hadley commented Feb 22, 2022

Another possible name for cast_next() would be super()

@hadley
Copy link
Member Author

hadley commented Feb 22, 2022

Renamed to super(); I think that makes it more clear that these are related, but distinct, ideas.

@hadley hadley requested a review from DavisVaughan February 22, 2022 17:41
@hadley hadley changed the title Implement cast() and cast_next() Implement cast() and super() Feb 22, 2022
R/cast.R Outdated Show resolved Hide resolved
R/cast.R Outdated
Comment on lines 25 to 26
if (is_base_class(to)) {
R7_data(from)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes both the class and the properties belonging to from, while the documentation implies that only the class will be removed:

#' `cast()` provides an automatic fallback if `from` inherits from `to`. You
 #' can override this if you need some special behavior other than simply
 #' stripping class.

And the other code paths only remove the class

I'm not sure if that was intended, or if the documentation just needs to be more precise.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

foo <- new_class("foo", parent = "character", properties = list(a = "integer"))
bar <- new_class("bar", parent = foo, properties = list(b = "integer"))

x <- bar()
x
#> <bar> chr(0) 
#> @ a:  int(0) 
#> @ b:  int(0)

# lost everything
cast(x, "character")
#> character(0)

# retains `b`
attributes(cast(x, foo))
#> $object_class
#> <R7_class>
#> @ name  :  foo
#> @ parent: <character>
#> @ properties:
#>  $ a: <integer>
#> 
#> $class
#> [1] "foo"       "character" "R7_object"
#> 
#> $a
#> integer(0)
#> 
#> $b
#> integer(0)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me what a cast should do here — should it strip properties or keep them? I think you see this inconsistency in the various paths. I'll standardise to make the minimal changes to the class.

R/cast.R Outdated Show resolved Hide resolved
R/class-spec.R Show resolved Hide resolved
R/super.R Outdated Show resolved Hide resolved
R/super.R Outdated Show resolved Hide resolved
_pkgdown.yml Outdated Show resolved Hide resolved
src/method-dispatch.c Show resolved Hide resolved
tests/testthat/test-cast.R Outdated Show resolved Hide resolved
method(bar, foo1) <- function(x) 1
method(bar, foo3) <- function(x) 3

expect_error(bar(super(foo3())), "Can't find method")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this is inconsistent

foo1 <- new_class("foo1")
foo2 <- new_class("foo2", foo1)
foo3 <- new_class("foo3", foo2)

bar <- new_generic("bar", "x")
method(bar, foo1) <- function(x) 1
method(bar, foo3) <- function(x) 3

# this doesnt work
bar(super(foo3()))
#> Error: Can't find method for generic `bar()` with dispatch classes:
#> - x: foo2

# but this does
bar(foo2())
#> [1] 1

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this is why I previously had mentioned dispatch = class_register(to) in super() might should be dispatch = class_dispatch(to), so match the fact that the C code of method_call_ () typically calls obj_dispatch()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm, I'm not sure. It feels weird to do super(foo3, foo2) and then have it dispatch to foo1. I think I'd rather eliminate the dispatch hierarchy here in favour of forcing the user to be explicit.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we could talk about this one in the next OOP working group meeting?

To me it feels like super(foo3, foo2) says "I return an object of class foo2", meaning that bar(foo2()) is identical to bar(super(foo3(), foo2)) in my head

I am interpreting your argument as: If the user wanted the foo1 behavior, then they should have done super(foo3, foo1), which also feels reasonable (and more explicit). But it still might be nice to discuss this point.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW this is consistent with convert() if you do convert(x, to = foo2), you don't expect to get an instance of foo1 back.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But you do get something back that you can call bar() on, and that will dispatch to the foo1 method?

x <- foo3()

bar(x)
#> [1] 3

bar(convert(x, to = foo2))
#> [1] 1

bar(super(x, to = foo2))
#> Error: Can't find method for generic `bar()` with dispatch classes:
#> - x: foo2

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. It's not directly analogous, but I think it's in the same spirit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's change this so that super(..., to = foo2) behaves exactly like a foo2

@ltierney
Copy link
Collaborator

ltierney commented Feb 22, 2022 via email

@hadley
Copy link
Member Author

hadley commented Feb 23, 2022

@ltierney no problems; the development has been fairly hectic and I've been down a few dead ends. I'd no longer characterise super() as a casting based — it creates a transient wrapper that only affects dispatch for the next generic. I think this resolves your concerns:

library(R7)
A <- new_class("A")
B <- new_class("B", A)
C <- new_class("C", B)

f <- new_generic("f", "x")
# You (now) have to supply the class you want to dispatch
# to, so there is no longer any ambiguity:
method(f, C) <- function(x) c("f-C", f(super(x, B)))
method(f, B) <- function(x) c("f-B", f(super(x, A)))
method(f, A) <- function(x) "f-A"

f(C())
#> [1] "f-C" "f-B" "f-A"

method(f, A) <- function(x) c("f-A", g(x))

# super() only affects dispatch for the next generic, so
# future generics used by the method dispatch on the original class
g <- new_generic("g", "x")
method(g, C) <- function(x) c("g-C", g(super(x, B)))
method(g, B) <- function(x) c("g-B", g(super(x, A)))
method(g, A) <- function(x) "g-A"

f(B())
#> [1] "f-B" "f-A" "g-B" "g-A"

Created on 2022-02-23 by the reprex package (v2.0.1)

@hadley
Copy link
Member Author

hadley commented Feb 23, 2022

We should also consider if cast() is the right name. convert() might be more likely to evoke the right sense.

And probably worth naming the second argument as a role model: convert(x, to = y).

@ltierney
Copy link
Collaborator

ltierney commented Feb 24, 2022 via email

@hadley
Copy link
Member Author

hadley commented Feb 24, 2022

@ltierney Right. Currently super() does create a transient object, but that's just an implementation detail. I'll think about what we can do to make that more obvious: it at least needs a print method, and maybe @ should give an informative error?

@hadley hadley changed the title Implement cast() and super() Implement convert() and super() Feb 24, 2022
@lawremi
Copy link
Collaborator

lawremi commented Mar 8, 2022

I like that super() is an actual function, rather than special syntax that requires special documentation.

R/super.R Outdated
#' # convert() affects every generic:
#' bar2(convert(obj, to = foo1))
#' # super() only affects the _next_ generic:
#' bar2(super(obj, foo1))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name to argument

@hadley hadley merged commit ad6516c into main Mar 15, 2022
@hadley hadley deleted the cast branch March 15, 2022 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

What should coercion look like for R7? Can we eliminate next_method()?
4 participants