A toy package designed to showcase and serve as a reference/tutorial for S7 development. To be used in conjunction with the S7 package webs
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Finished first draft of vignette

+238 -40
+11 -13
README.md
··· 62 62 63 63 but I think this is harder to reason about. The idea of just giving a garden variety vector `1:10` a whole class without really defining what that class is, is strange to me. I find it hard sometimes to understand what S3 objects are and how they interact with methods. S7 makes it very clear what things are; I find this easier to reason about. This is something I noted firsthand when swapping to S7 in some [tidyverse](https://www.tidyverse.org/) packages. Even with well documented code, it can be hard to understand how things work. 64 64 65 - S7 may be formal, but it is also elegant–there are nice design patterns that can be implemented effortlessly. Take, for example, this implementation of some `Shape`s. 65 + S7 may be formal, but it is also elegant–there are nice design patterns that can be implemented effortlessly. Take, for example, this implementation of some `Shape`s. This example is revisited and built from the ground up in a vignette; here it is presented without much explanation. 66 66 67 67 ``` r 68 68 library(S7) 69 69 70 70 Shape <- new_class("Shape", abstract = TRUE) 71 71 Circle <- new_class("Circle", 72 - Shape, 72 + parent = Shape, 73 73 properties = list( 74 74 radius = class_numeric 75 75 ) 76 76 ) 77 77 Rect <- new_class("Rect", 78 - Shape, 78 + parent = Shape, 79 79 properties = list( 80 80 width = class_numeric, 81 81 height = class_numeric ··· 99 99 100 100 ``` r 101 101 Square <- new_class("Square", 102 - Rect, 102 + parent = Rect, 103 103 constructor = function(side) { 104 - new_object(S7_object(), width = side, height = side) 104 + new_object(Rect, width = side, height = side) 105 105 } 106 106 ) 107 107 square <- Square(5) ··· 119 119 ) 120 120 121 121 Circle <- new_class("Circle", 122 - Shape, 122 + parent = Shape, 123 123 properties = list( 124 124 radius = positive_numeric 125 125 ) 126 126 ) 127 127 Rect <- new_class("Rect", 128 - Shape, 128 + parent = Shape, 129 129 properties = list 130 130 ( 131 131 width = positive_numeric, ··· 159 159 160 160 Shape <- new_class("Shape", abstract = TRUE) 161 161 Circle <- new_class("Circle", 162 - Shape, 162 + parent = Shape, 163 163 properties = list( 164 164 radius = positive_numeric, 165 165 area = new_property(class_numeric, getter = function(self) pi * self@radius^2, setter = NULL) 166 166 ) 167 167 ) 168 168 Rect <- new_class("Rect", 169 - Shape, 169 + parent = Shape, 170 170 properties = list( 171 171 width = positive_numeric, 172 172 height = positive_numeric, ··· 174 174 ) 175 175 ) 176 176 Square <- new_class("Square", 177 - Rect, 177 + parent = Rect, 178 178 constructor = function(side) { 179 179 new_object(S7_object(), width = side, height = side) 180 180 } ··· 300 300 301 301 - Comment code 302 302 303 - - Write tests 304 - 305 - - Write a vignette? 303 + - Write tests
+227 -27
vignettes/BigNum.Rmd
··· 16 16 17 17 ```{r setup} 18 18 library(BigNum) 19 + library(S7) 19 20 ``` 20 21 21 - This vignette is written to get you acquainted with `{BigNum}` and `{S7}`, and assumes no knowledge of `{S7}` but does assume basic general knowledge of objected oriented principles and terminology. We highly recommend that you use this package in conjunction with the [S7 website](https://rconsortium.github.io/S7/), but you should also be able to gain a basic understanding of S7 solely through this vignette. We will primarily be using examples from the package. 22 + This vignette is written to get you acquainted with `{BigNum}` and S7, and assumes no knowledge of the latter; however, this does assume basic general knowledge of objected oriented principles and terminology. I highly recommend that you use this package in conjunction with the [S7 website](https://rconsortium.github.io/S7/), but ideally you should be able to gain a basic understanding of S7 solely through this vignette. If you are completely new to S7, start with this vignette, check the website as you go (especially if I don't explain something to your liking), and once you've read a section or understood a concept, check the source code of `BigNum` to see some (ideally less trivial) examples. 23 + <!-- We will primarily be using examples from the package. --> 22 24 23 25 ## Classes, Properties, & Objects 24 26 25 27 [Website](https://rconsortium.github.io/S7/articles/classes-objects.html) 26 28 27 - We will start with the example from the readme--`Shape`s. We will be constructing a type hierarchy with Squares, Rectangles, Circles, and Shapes, with the typical relationships (e.g. all Squares are Rectangles, all Rectangles and Circles are Shapes). Let us start with `Shape`s though. 29 + We will start with the example from the readme--`Shape`s. We will be constructing a type hierarchy with squares, rectangles, circles, and shapes, with the typical relationships (e.g. all squares are rectangles, all rectangles and circles are shapes). Let us start with `Shape`s though. 28 30 29 31 ```{r} 30 - library(S7) 31 - 32 32 Shape <- new_class("Shape") 33 33 ``` 34 34 ··· 38 38 Shape # class 39 39 ``` 40 40 41 - To get *a* `Shape`, we can call the constructor. Since we haven't given any data to `Shape`, the constructor is an empty function. 41 + To create *a* `Shape`, we can call the constructor. Since we haven't given any data to `Shape`, the default constructor doesn't take any arguments. 42 42 43 43 ```{r} 44 44 Shape() # object ··· 54 54 ## Subjective Style Note 55 55 This note can be ignored as it pertains only to style. 56 56 57 - We think that class definitions in general shouldn't be inlined like they are above--however, since it is so short, we think this is an exception. Later we will see longer class definitions and will comment on style again there. 57 + I think that class definitions in general shouldn't be inlined like they are above--however, since it is so short, I think this is an ok exception. Later we will see longer class definitions and will comment on style again there. 58 58 ::: 59 59 60 60 We use `properties` in `new_class()` to define what data the class holds. S7 objects have *properties* (similar-ish to S4 `slots`), and are defined through a list. We provide the name of the property: `name`, and the type of data it can hold: `class_character`. `class_character` is part of a special set of properties exported by `{S7}`; other common ones include `class_numeric`, `class_logical`, `class_list`, etc. You can find an exhaustive list [on the website](https://rconsortium.github.io/S7/reference/index.html#compatibility). Defining the class of properties upfront allows for some validation through type safety. ··· 75 75 76 76 Here we attempt to make a `Shape` with a `@name`[^1] of `10`, but since `10` isn't a character, we cannot do this. Note that there is no automatic type coercion. By default, we pass properties to the constructor by position, and the order is determined by the order in `properties`. We can observe properties of the class `Shape` itself the same way we look at properties of S7 objects. 77 77 78 - [^1]: We use the `@` symbol (`@property`) to signify that we are talking about a property of some object, just like how you would access it. This is another style thing, and certainly not standard. 78 + [^1]: I use the `@` symbol (`@property`) to signify that I am talking about a property of some object, just like how you would access it. This is another style thing, and certainly not standard. 79 79 80 80 ```{r} 81 81 Shape@constructor ··· 94 94 95 95 ::: {.callout-note collapse="true"} 96 96 ## Subjective Style Note 97 - Note that this class definition is no longer inlined. In general, we think that (excepting the `name` argument of `new_class()`) one should space arguments out with linebreaks. 97 + Note that this class definition is no longer inlined. In general, I think that (excepting the `name` argument of `new_class()`) one should space arguments out with linebreaks. Here is a full class with the styling I think one should use. 98 + 99 + ```{r} 100 + #| eval: false 101 + example <- new_class("example", 102 + parent = parent_class, 103 + package = "my_package", 104 + properties = list( 105 + prop1 = new_property(class_any, 106 + getter = function(self) { 107 + self@prop1 108 + }, 109 + setter = function(self, value) { 110 + self 111 + } 112 + ), 113 + prop2 = class_any, 114 + # ... 115 + ), 116 + constructor = function(prop1, prop2 = NULL) { 117 + force(prop1) 118 + force(prop2) 119 + new_object(parent_class, prop1 = prop1, prop2 = prop2) 120 + }, 121 + validator = function(self) { 122 + if (self@prop2 == TRUE) { 123 + "@prop2 must be FALSE" 124 + } else if (self@prop1 != 42) { 125 + "@prop1 must be 42" 126 + } 127 + } 128 + ) 129 + ``` 98 130 ::: 99 131 100 132 Properties have default values. We can observe them by creating an empty `Shape`: ··· 109 141 Shape@constructor 110 142 ``` 111 143 112 - Let's add some more `Shape`s, starting with a `Circle`. All Circles are Shapes, so we wish to reflect that in our class definition. We can do that using inheritance, which in S7 is expressed as follows: 144 + Let's add some more `Shape`s, starting with a `Circle`. All circles are shapes, so we wish to reflect that in our class definition. We can do that using inheritance, which in S7 is expressed as follows: 113 145 114 146 ```{r} 115 147 Circle <- new_class("Circle", 116 - Shape, 148 + parent = Shape, 117 149 properties = list( 118 150 radius = class_numeric 119 151 ) ··· 130 162 class(Circle()) 131 163 ``` 132 164 133 - Inheritance works as you might expect: Circles now have all the methods of Shapes, as well as `@radius`. 165 + Inheritance works as you might expect: `Circle`s now have all the methods of `Shape`s, as well as `@radius`. 134 166 135 167 Think about if the following would return an error, and why. (Hint: check the class definition for `Shape` again.) 136 168 ··· 200 232 ) 201 233 202 234 Circle <- new_class("Circle", 203 - Shape, 235 + parent = Shape, 204 236 properties = list( 205 237 radius = positive_numeric 206 238 ) ··· 208 240 ``` 209 241 210 242 211 - Before we see how our new classes have appropriate and intuitive behavior, note that we inlined a definition of a custom property analogous to `positive_numeric` but restricted to `integer`s, for `@sides`. We suggest only doing this when you don't intend to reuse the property--it adds some clutter but defining `positive_integer` and never using it seems more wasteful. 243 + Before we see how our new classes have appropriate and intuitive behavior, note that I inlined a definition of a custom property analogous to `positive_numeric` but restricted to `integer`s, for `@sides`. I suggest only doing this when you don't intend to reuse the property--it adds some clutter but defining `positive_integer` and never using it seems more wasteful. 212 244 213 245 ```{r} 214 246 #| error: true ··· 232 264 Shape <- new_class("Shape", abstract = TRUE) 233 265 234 266 Circle <- new_class("Circle", 235 - Shape, 267 + parent = Shape, 236 268 properties = list( 237 269 radius = positive_numeric 238 270 ) 239 271 ) 240 272 ``` 241 273 242 - Here we've included what we've written so far, with a small change to `Shape` to disallow instantiation of arbitrary `Shape`s. 274 + Here we've included what we've written so far. I used `abstract = TRUE` to tell S7 that we don't ever mean to make a `Shape` object outright. This means the following is now not disallowed: 275 + <!-- with a small change to `Shape` to disallow instantiation of arbitrary `Shape`s. --> 243 276 244 277 ```{r} 245 278 #| error: true ··· 250 283 251 284 ```{r} 252 285 Circle <- new_class("Circle", 253 - Shape, 286 + parent = Shape, 254 287 properties = list( 255 288 radius = positive_numeric, 256 289 area = new_property(class_numeric, getter = function(self) pi * self@radius^2, setter = NULL) ··· 258 291 ) 259 292 ``` 260 293 261 - Here, we are defining the area of a `Circle` through a property, but we aren't setting that property explicitly at construction, like we are with `@radius`. Instead, we are creating a [computed property](https://rconsortium.github.io/S7/articles/classes-objects.html#computed-properties), which also happens to be [frozen](https://rconsortium.github.io/S7/articles/classes-objects.html#frozen-properties). What that means is that `@area` is calculated when you call it, and the value itself cannot be set. The `getter` is a function in a property that defines what happens when you call `object@property`. It must always take only one argument, self, and returns the value of the property. Here, we can see that `@area`'s getter is defined to grab `@radius` and use that to calculate the area of a circle. The setter defines what happens when you do `obj@property <- value`. In this case, you technically don't need to set setter to `NULL` as that is its default value and would make `@area` a read only property. We advocate for explicitly setting the setter to `NULL` to make it very clear that `@area` is a read only property. 294 + Here, we are defining the area of a `Circle` through a property, but we aren't setting that property explicitly at construction, like we are with `@radius`. Instead, we are creating a [computed property](https://rconsortium.github.io/S7/articles/classes-objects.html#computed-properties), which also happens to be [frozen](https://rconsortium.github.io/S7/articles/classes-objects.html#frozen-properties). What that means is that `@area` is calculated when you call it, and the value itself cannot be set. The `getter` is a function in a property that defines what happens when you call `object@property`. It must always take only one argument, self, and returns the value of the property. Here, we can see that `@area`'s getter is defined to grab `@radius` and use that to calculate the area of a circle. The setter defines what happens when you do `obj@property <- value`. In this case, you technically don't need to set setter to `NULL` as that is its default value and would make `@area` a read only property. I advocate for explicitly setting the setter to `NULL` to make it very clear that `@area` is a read only property. 262 295 263 296 ```{r} 264 297 #| error: true ··· 275 308 276 309 ```{r} 277 310 Rect <- new_class("Rect", 278 - Shape, 311 + parent = Shape, 279 312 properties = list( 280 313 width = positive_numeric, 281 314 height = positive_numeric, ··· 295 328 296 329 ```{r} 297 330 Square <- new_class("Square", 298 - Rect, 331 + parent = Rect, 299 332 constructor = function(side) { 300 333 force(side) 301 - new_object(S7_object(), width = side, height = side) 334 + new_object(Rect, width = side, height = side) 302 335 } 303 336 ) 304 337 ``` 305 338 306 - This is strange though--this class definition is far shorter than `Circle` or `Rect`--what's going on? Here, we are leveraging the idea that all Squares are Rectangles to offload most of the heavy work to `Rect`. We are essentially providing `Square` as an edge case `Rect` with well defined behavior. All we need is to define the parent of `Square` as `Rect` (whose parent is `Shape` whose parent is `S7_object`), and define a short custom constructor. As you can see, the constructor is almost a regular R function--it just ["must always end with a call to `new_object()`"](https://rconsortium.github.io/S7/articles/classes-objects.html#constructors). We can set default values of the object in the constructor itself, or could pass a default value in the property definition. 339 + Here, we are leveraging the idea that all squares are rectangles to offload most of the heavy work to `Rect`. We are essentially presenting `Square` as an special case `Rect` with well defined behavior. All we need is to define the parent of `Square` as `Rect` (whose parent is `Shape` whose parent is `S7_object`), and define a short custom constructor. vWe can set default values of the object in the constructor itself, or could pass a default value in the property definition. As you can see, the constructor is almost a regular R function--it just ["must always end with a call to `new_object()`"](https://rconsortium.github.io/S7/articles/classes-objects.html#constructors). Another important thing to note is that if you define a custom constructor, ["any subclass will also require a custom constructor"](https://rconsortium.github.io/S7/articles/classes-objects.html#constructors). 307 340 308 341 ```{r} 309 342 Square@constructor 310 343 Rect@constructor 311 344 ``` 312 345 313 - We didn't set a default value for `@side` in the constructor, meaning that this won't work: 346 + We didn't set a default value for `side` in the constructor, meaning that this won't work: 314 347 315 348 ```{r} 316 349 #| error: true 317 350 Square() 318 351 ``` 319 352 320 - And since `Square`s are `Rect`s, we get all the same nice things. 321 - 353 + And since `Square`s are `Rect`s, we get all the same nice things `Rect`s do. 322 354 323 355 ```{r} 324 356 #| error: true ··· 334 366 335 367 ```{r} 336 368 Square <- new_class("Square", 337 - Rect, 369 + parent = Rect, 338 370 properties = list( 339 371 area = new_property(class_numeric, getter = function(self) self@width^2, setter = NULL) 340 372 ), 341 373 constructor = function(side) { 342 374 force(side) 343 - new_object(S7_object(), width = side, height = side) 375 + new_object(Rect, width = side, height = side) 344 376 } 345 377 ) 346 378 ··· 349 381 Square@properties$area$getter 350 382 ``` 351 383 352 - There is certainly a lot more to learn about properties and classes, but this should be enough to make you dangerous. The website (and of course `BigNum`) are great places to see and learn more! 384 + Note that we can't use `side` in the definition for `@area` since `side` isn't a property--it's just the name we gave to the only argument we take when creating `Square`s. 385 + 386 + There is certainly a lot more to learn about properties and classes, but hopefully this has helped you build enough of an understanding of how these constructs work to be able to reason about them. The website (and of course `BigNum`) are great places to see examples and learn more! 353 387 354 388 ## Generics and Methods 355 389 356 390 [Website](https://rconsortium.github.io/S7/articles/generics-methods.html) 357 391 392 + This section is going to be shorter since I think this is best explained on the website and is closer to S3 (which I assume is the OOP most are familiar with) than Properties and Classes. 393 + 394 + We have objects, and we can give our objects some data--but we can't do anything with them yet. That's where Generics and Methods step in. A generic defines an idea of a behavior while a method defines an implementation of that behavior for a particular class. [^3] Let's revisit the idea of `Shape`s' area. We initially defined area as a property, but there's no reason why we couldn't make it a function--this may not always be the case. 395 + 396 + [^3]: S7 supports multiple dispatch so this is technically not correct--Methods define implementations for an arbitrary set of classes, which could be just one class, but doesn't have to be. Multiple dispatch means that S7 can select a method based on the classes of multiple arguments, not just the first one; i.e. `method(obj1, obj2)` could have different behavior from `method(obj1, obj3)`. S3 only has single dispatch, except for [double dispatch](https://adv-r.hadley.nz/s3.html#double-dispatch) 397 + 398 + ```{r} 399 + positive_numeric <- new_property(class_numeric, 400 + validator = function(value) { 401 + if (value <= 0) "must be greater than 0" 402 + } 403 + ) 404 + 405 + Shape <- new_class("Shape", 406 + properties = list( 407 + sides = new_property(class_integer, 408 + validator = function(value) { 409 + if (value <= 0) "must be greater than 0" 410 + } 411 + ) 412 + ), 413 + abstract = TRUE 414 + ) 415 + 416 + Circle <- new_class("Circle", 417 + parent = Shape, 418 + properties = list( 419 + radius = positive_numeric 420 + ), 421 + constructor = function(radius) { 422 + force(radius) 423 + new_object(S7_object(), radius = radius, sides = 1L) 424 + } 425 + ) 426 + 427 + Rect <- new_class("Rect", 428 + parent = Shape, 429 + properties = list( 430 + width = positive_numeric, 431 + height = positive_numeric 432 + ), 433 + constructor = function(width, height) { 434 + force(width) 435 + force(height) 436 + new_object(S7_object(), width = width, height = height, sides = 4L) 437 + } 438 + ) 439 + 440 + Square <- new_class("Square", 441 + parent = Rect, 442 + constructor = function(side) { 443 + force(side) 444 + new_object(Rect, width = side, height = side, sides = 4L) 445 + } 446 + ) 447 + ``` 448 + 449 + Let's review what we've changed. First, `Shape`s have recovered `@sides`, with an identical definition to what we saw earlier. To accommodate this, I've written custom constructors for all `Shape`s, and defined `@sides` for each shape there. Additionally, we didn't specify any default values for any shape now, so all arguments are mandatory. Now we wish to get the area of a `Shape`, so we will write a generic, then a method for `Circle`s. 450 + 451 + ```{r} 452 + Area <- new_generic("Area", "shape") 453 + method(Area, Circle) <- function(shape) { 454 + pi * shape@radius^2 455 + } 456 + ``` 457 + 458 + The arguments to `new_generic()` are relatively straightforward. We start with the name of the method, which should always be the same as the name you assign the value of `new_generic()` to, just like with Classes. The second argument are the dispatch arguments. S7 uses multiple dispatch, which means that multiple objects can be used to determine the correct method for a given generic. `BigNum` uses multiple dispatch (see definition of operators), but for this example we will rely on single dispatch. We pass the title of the argument(s) we wish to dispatch on. We aren't passing the dots here, and [read the website](https://rconsortium.github.io/S7/articles/generics-methods.html#generic-method-compatibility) to see what that implies. 459 + 460 + After that comes a method implementation. `method()` is very flexible, and allows you to register methods for a variety of generics: viz. for [an S7 generic, an external generic, an S3 generic, or an S4 generic.](https://rconsortium.github.io/S7/reference/method.html) We specify the generic we are defining a method for, and then the class we are adding behavior to. We implement the actually functionality in a garden variety R function--except it must have the named argument as defined in the generic: 461 + 462 + ```{r} 463 + #| error: true 464 + method(Area, Circle) <- function(x) { 465 + pi * x@radius^2 466 + } 467 + ``` 468 + 469 + And now we can calculate the area of `Circle`s! 470 + 471 + ```{r} 472 + Area(Circle(10)) 473 + ``` 474 + 475 + Since we haven't defined methods for any other `Shape`s, this will result in an error. 476 + 477 + ```{r} 478 + #| error: true 479 + Area(Square(5)) 480 + ``` 481 + 482 + With our type hierarchy, we could define a "default" area by creating a method for `Shape`. 483 + 484 + ```{r} 485 + method(Area, Shape) <- function(shape) { 486 + "No method defined for this Shape!" 487 + } 488 + 489 + Area(Square(5)) 490 + method_explain(Area, Square) 491 + 492 + method(Area, Rect) <- function(shape) { 493 + shape@height * shape@width 494 + } 495 + 496 + Area(Square(5)) 497 + method_explain(Area, Square) 498 + ``` 499 + 500 + Similarly, we could get the number of sides of any `Shape` with minimal effort, due to how we defined `@sides`. 501 + 502 + ```{r} 503 + number_of_sides <- new_generic("number_of_sides", "x") 504 + method(number_of_sides, Shape) <- function(x) { 505 + x@sides 506 + } 507 + 508 + number_of_sides(Square(5)) 509 + number_of_sides(Rect(4, 2)) 510 + number_of_sides(Circle(10)) 511 + 512 + method_explain(number_of_sides, Circle) 513 + ``` 514 + 515 + Let's say that we want to be able to get the perimeter of our `Shape`s--but we don't want to make our own generic. Instead, we want to use `length()` to get the perimeter of a Shape. Doing so is trivial; just remember to use the correct arguments in the method definition. 516 + 517 + ```{r} 518 + method(length, Circle) <- function(x) { 519 + 2 * pi * x@radius 520 + } 521 + 522 + length(Circle(10)) 523 + 524 + method(length, Rect) <- function(x) { 525 + 2 * (x@height + x@width) 526 + } 527 + 528 + length(Square(5)) 529 + ``` 530 + 531 + If you need to define behavior for a generic defined in another package, you can use [`new_external_generic()`](https://rconsortium.github.io/S7/reference/new_external_generic.html). Example from that site: 532 + 533 + ```{r} 534 + median_Shapes <- new_external_generic("stats", "median", "x") 535 + method(median_Shapes, Shape) <- function(x) { 536 + "You can't take the median of a Shape!" 537 + } 538 + 539 + median(Rect(4, 2)) 540 + ``` 541 + 542 + Since S7 is built atop of S3, you could do something like this: 543 + 544 + ```{r} 545 + median.Shape <- function(x) { 546 + "Yay S3" 547 + } 548 + median(Rect(4, 2)) 549 + ``` 550 + 551 + but I strongly recommend that you use `method()` instead. 552 + 553 + There are far more details on the website's [Generics and Methods page](https://rconsortium.github.io/S7/articles/generics-methods.html), give it a read! 554 + 555 + ## Conclusion 556 + 557 + Equipped with this knowledge of S7, you should hopefully be able to understand how the package and S7 works--or at the very least, know enough to know what to search or learn. You should probably be able to understand how the package works, which I hope is a more interesting (and certainly more fleshed out) example than the `Shape`s we looked at!