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.

Updated vignette.

+34 -28
+1 -1
DESCRIPTION
··· 22 22 testthat (>= 3.0.0) 23 23 Config/testthat/edition: 3 24 24 Config/testthat/parallel: true 25 - VignetteBuilder: quarto 25 + VignetteBuilder: knitr
+2 -2
R/big_num.R
··· 167 167 append_to_start(node(x), ll) 168 168 } 169 169 170 - method(print, linked_list) <- function(x) { 170 + method(print, linked_list) <- function(x, ...) { 171 171 current <- x@head 172 172 while (S7_inherits(current)) { 173 173 cat(current@VALUE, "-> ") ··· 177 177 178 178 invisible(x) 179 179 } 180 - method(print, big_num) <- function(x) { 180 + method(print, big_num) <- function(x, ...) { 181 181 len <- x@length 182 182 183 183 if (len == 0) {
+1 -1
README.md
··· 26 26 27 27 ### Why S7? 28 28 29 - This package was created for a number of reasons. Chief among those is to provide a worked example of S7 in a package context, but it was also written to purvey the benefits of S7, and posit it as the next R OOP paradigm you should reach for. I highly recommend listening to [Hadley's talk on S7 (then called R7) at posit::conf(2022)](https://www.youtube.com/watch?v=P3FxCvSueag) (thank you [Dr Bodwin](https://www.kelly-bodwin.com/) for showing this to me!), and reading the S7 webpage to get arguments for S7 from very bright people. This section can be skipped--or at least skimmed--if you (like me) are convinced that S7 is the way to go. 29 + This package was created for a number of reasons. Chief among those is to provide a worked example of S7 in a package context, but it was also written to purvey the benefits of S7, and posit it as the next R OOP paradigm you should reach for. I highly recommend listening to [Hadley's talk on S7 (then called R7) at posit::conf(2022)](https://www.youtube.com/watch?v=P3FxCvSueag) (thank you [Dr Bodwin](https://www.kelly-bodwin.com/) for showing this to me!), and reading the S7 webpage to get arguments for S7 from very bright people. This section can be skipped--or at least skimmed--if you (like me) are convinced that S7 is the way to go. Read the vignette for a gentle introduction to S7. 30 30 31 31 Here's an unordered list of some nice things about S7. I won't directly address these ideas, but you can observe them throughout the code. 32 32
+1 -1
man/BigNum-package.Rd
··· 9 9 A toy package designed to showcase and serve as a reference/tutorial for S7 development. To be used in conjunction with the S7 package website. 10 10 } 11 11 \author{ 12 - \strong{Maintainer}: Visruth Srimath Kandali \email{visruth@gmail.com} (\href{https://orcid.org/0009-0005-9097-0688}{ORCID}) 12 + \strong{Maintainer}: Visruth Srimath Kandali \email{public@visruth.com} (\href{https://orcid.org/0009-0005-9097-0688}{ORCID}) 13 13 14 14 } 15 15 \keyword{internal}
+29 -23
vignettes/BigNum.Rmd
··· 1 1 --- 2 - title: "BigNum" 2 + title: "A (Hopefully Gentle) Introduction to S7" 3 3 output: rmarkdown::html_vignette 4 4 vignette: > 5 5 %\VignetteIndexEntry{BigNum} ··· 15 15 ``` 16 16 17 17 ```{r setup} 18 - library(BigNum) 19 18 library(S7) 20 19 ``` 21 20 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. 21 + 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. 22 + 23 + It may be paced too slowly, in which case feel free to skip portions you find trivial. This package and the S7 website tend to have more fleshed out examples and more details. 23 24 <!-- We will primarily be using examples from the package. --> 24 25 25 26 ## Classes, Properties, & Objects ··· 75 76 76 77 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 78 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 + [^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 is not standard. 79 80 80 81 ```{r} 81 82 Shape@constructor ··· 152 153 ) 153 154 ``` 154 155 155 - `new_class()` has a `parent` argument which defaults to `S7_object` (think `Object` in Java), which is the parent class of all S7 objects. We can see this using `class()`[^2] 156 + `new_class()` has a `parent` argument which defaults to `S7_object` which is the parent class of all S7 objects (think `Object` in Java.) We can see this using `class()`[^2] 156 157 157 158 [^2]: S7 is [built atop of S3](https://rconsortium.github.io/S7/articles/compatibility.html), so methods for S3 OOP will still work on S7 (e.g. `{sloop}` and `class()`). `S7` sometimes exposes some functions which you should use instead, though. Check if there is a `S7` function before defaulting to a S3 one. (e.g. use `S7_class()` instead of checking if `"S7_object"` is in `class(your_object)`). 158 159 ··· 195 196 Observe that the default constructor places the properties of the parent class before those of the children. 196 197 197 198 ```{r} 198 - Circle 199 199 Circle@constructor 200 200 ``` 201 201 202 - You can change this easily using a custom constructor. 202 + You can change this easily using a custom constructor, which we will see later. 203 203 204 - Whilst we have some validation to ensure our properties are sensible since `@radius` and `@sides` have to be integers and numerics respectively, we can pass negative values. 204 + Whilst we have some validation to ensure our properties are sensible since `@radius` and `@sides` have to be integers and numerics respectively, we can still pass negative values. 205 205 206 206 ```{r} 207 207 Circle("circle", -1L, -10) ··· 217 217 ) 218 218 ``` 219 219 220 - `validator()` is how a property's values get validated. The first argument we are passing to `new_property()` is `class_numeric`, telling us that data defined by this property can only be of the class `numeric`. Additionally, our validator checks values, and returns a string if there is an issue. Read more about how validation works [here](https://rconsortium.github.io/S7/articles/classes-objects.html#validation-1) but for our purposes, we can just think of a validator as accepting or rejecting values. `positive_numeric` is sensibly defined to only accept positive numerics, and provides an appropriate error message if faced with negative numbers. Let use it. 220 + `validator()` is how a property's values get validated. The first argument we are passing to `new_property()` is `class_numeric`, telling us that data defined by this property can only be of the class `numeric`. Additionally, our validator checks values, and returns a string if there is an issue. Read more about how validation works [here](https://rconsortium.github.io/S7/articles/classes-objects.html#validation-1) but for our purposes, we can just think of a validator as accepting or rejecting values; returning a string rejects a value. `positive_numeric` is defined to only accept positive numerics, and provides an appropriate error message if faced with negative numbers. 221 221 222 222 ```{r} 223 223 Shape <- new_class("Shape", ··· 240 240 ``` 241 241 242 242 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. 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. Maybe that is better still, though--I'm torn. 244 244 245 245 ```{r} 246 246 #| error: true ··· 252 252 Circle("circle", 1L, -10) 253 253 ``` 254 254 255 - Note that the property name is automatically added to the validator's error message. Let us further explore properties. 255 + Note that the property name (e.g. `@radiues` or `@sides`) is automatically added to the validator's error message. Don't hardcode that yourself. 256 + 257 + Let us further explore properties. 256 258 257 259 ```{r} 258 260 positive_numeric <- new_property(class_numeric, ··· 271 273 ) 272 274 ``` 273 275 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: 276 + 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 disallowed: 275 277 <!-- with a small change to `Shape` to disallow instantiation of arbitrary `Shape`s. --> 276 278 277 279 ```{r} ··· 291 293 ) 292 294 ``` 293 295 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. 296 + 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` an immutable property. I advocate for explicitly setting the setter to `NULL` to make it very clear that `@area` is a read only property. 295 297 296 298 ```{r} 297 299 #| error: true ··· 304 306 circ@area <- 0 305 307 ``` 306 308 307 - Circles are getting boring; let's add Rectangles and Squares! 309 + Circles are getting boring; let's add Rectangles and Squares. 308 310 309 311 ```{r} 310 312 Rect <- new_class("Rect", ··· 324 326 Rect(10, 10) 325 327 ``` 326 328 327 - We can easily extend our shapes to further include `Square`s. 329 + And now we can easily extend our shapes to further include `Square`s. 328 330 329 331 ```{r} 330 332 Square <- new_class("Square", ··· 336 338 ) 337 339 ``` 338 340 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). 341 + 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. We can set default values of the object in the constructor itself, or could pass a default value in the property definition. 342 + 343 + 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). 340 344 341 345 ```{r} 342 346 Square@constructor ··· 350 354 Square() 351 355 ``` 352 356 353 - And since `Square`s are `Rect`s, we get all the same nice things `Rect`s do. 357 + Since `Square`s are `Rect`s, we get all the same nice things `Rect`s do. 354 358 355 359 ```{r} 356 360 #| error: true ··· 383 387 384 388 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 389 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! 390 + 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. 387 391 388 392 ## Generics and Methods 389 393 ··· 455 459 } 456 460 ``` 457 461 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. 462 + 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 463 460 464 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 465 ··· 466 470 } 467 471 ``` 468 472 469 - And now we can calculate the area of `Circle`s! 473 + And now we can calculate the area of `Circle`s. 470 474 471 475 ```{r} 472 476 Area(Circle(10)) ··· 528 532 length(Square(5)) 529 533 ``` 530 534 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: 535 + 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 the site: 532 536 533 537 ```{r} 534 538 median_Shapes <- new_external_generic("stats", "median", "x") ··· 539 543 median(Rect(4, 2)) 540 544 ``` 541 545 546 + Note that this isn't necessary for `base`, `stats`, etc. functions, but would be if you wish to register a method for a generic defined in another package--i.e. an external generic. This example is just to let you see the syntax and usage. 547 + 542 548 Since S7 is built atop of S3, you could do something like this: 543 549 544 550 ```{r} ··· 550 556 551 557 but I strongly recommend that you use `method()` instead. 552 558 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! 559 + There are far more details on the website's [Generics and Methods page](https://rconsortium.github.io/S7/articles/generics-methods.html). 554 560 555 561 ## Conclusion 556 562 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! 563 + Equipped with this knowledge of S7, you should hopefully be able to understand how this package and S7 works--or at the very least, know enough to know what to search or learn. I'd suggest reading through the source code of the package next, which I hope is a more interesting (and certainly more fleshed out) example than the `Shape`s we looked at. Additionally, the code in the package is slightly different as it is in a package, unlike the examples shown.