···62626363but 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.
64646565-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.
6565+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.
66666767``` r
6868library(S7)
69697070Shape <- new_class("Shape", abstract = TRUE)
7171Circle <- new_class("Circle",
7272- Shape,
7272+ parent = Shape,
7373 properties = list(
7474 radius = class_numeric
7575 )
7676)
7777Rect <- new_class("Rect",
7878- Shape,
7878+ parent = Shape,
7979 properties = list(
8080 width = class_numeric,
8181 height = class_numeric
···9999100100``` r
101101Square <- new_class("Square",
102102- Rect,
102102+ parent = Rect,
103103 constructor = function(side) {
104104- new_object(S7_object(), width = side, height = side)
104104+ new_object(Rect, width = side, height = side)
105105 }
106106)
107107square <- Square(5)
···119119)
120120121121Circle <- new_class("Circle",
122122- Shape,
122122+ parent = Shape,
123123 properties = list(
124124 radius = positive_numeric
125125 )
126126)
127127Rect <- new_class("Rect",
128128- Shape,
128128+ parent = Shape,
129129 properties = list
130130 (
131131 width = positive_numeric,
···159159160160Shape <- new_class("Shape", abstract = TRUE)
161161Circle <- new_class("Circle",
162162- Shape,
162162+ parent = Shape,
163163 properties = list(
164164 radius = positive_numeric,
165165 area = new_property(class_numeric, getter = function(self) pi * self@radius^2, setter = NULL)
166166 )
167167)
168168Rect <- new_class("Rect",
169169- Shape,
169169+ parent = Shape,
170170 properties = list(
171171 width = positive_numeric,
172172 height = positive_numeric,
···174174 )
175175)
176176Square <- new_class("Square",
177177- Rect,
177177+ parent = Rect,
178178 constructor = function(side) {
179179 new_object(S7_object(), width = side, height = side)
180180 }
···300300301301 - Comment code
302302303303-- Write tests
304304-305305-- Write a vignette?303303+- Write tests
+227-27
vignettes/BigNum.Rmd
···16161717```{r setup}
1818library(BigNum)
1919+library(S7)
1920```
20212121-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.
2222+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.
2323+<!-- We will primarily be using examples from the package. -->
22242325## Classes, Properties, & Objects
24262527[Website](https://rconsortium.github.io/S7/articles/classes-objects.html)
26282727-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.
2929+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.
28302931```{r}
3030-library(S7)
3131-3232Shape <- new_class("Shape")
3333```
3434···3838Shape # class
3939```
40404141-To get *a* `Shape`, we can call the constructor. Since we haven't given any data to `Shape`, the constructor is an empty function.
4141+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.
42424343```{r}
4444Shape() # object
···5454## Subjective Style Note
5555This note can be ignored as it pertains only to style.
56565757-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.
5757+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.
5858:::
59596060We 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.
···75757676Here 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.
77777878-[^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.
7878+[^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.
79798080```{r}
8181Shape@constructor
···94949595::: {.callout-note collapse="true"}
9696## Subjective Style Note
9797-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.
9797+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.
9898+9999+```{r}
100100+#| eval: false
101101+example <- new_class("example",
102102+ parent = parent_class,
103103+ package = "my_package",
104104+ properties = list(
105105+ prop1 = new_property(class_any,
106106+ getter = function(self) {
107107+ self@prop1
108108+ },
109109+ setter = function(self, value) {
110110+ self
111111+ }
112112+ ),
113113+ prop2 = class_any,
114114+ # ...
115115+ ),
116116+ constructor = function(prop1, prop2 = NULL) {
117117+ force(prop1)
118118+ force(prop2)
119119+ new_object(parent_class, prop1 = prop1, prop2 = prop2)
120120+ },
121121+ validator = function(self) {
122122+ if (self@prop2 == TRUE) {
123123+ "@prop2 must be FALSE"
124124+ } else if (self@prop1 != 42) {
125125+ "@prop1 must be 42"
126126+ }
127127+ }
128128+)
129129+```
98130:::
99131100132Properties have default values. We can observe them by creating an empty `Shape`:
···109141Shape@constructor
110142```
111143112112-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:
144144+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:
113145114146```{r}
115147Circle <- new_class("Circle",
116116- Shape,
148148+ parent = Shape,
117149 properties = list(
118150 radius = class_numeric
119151 )
···130162class(Circle())
131163```
132164133133-Inheritance works as you might expect: Circles now have all the methods of Shapes, as well as `@radius`.
165165+Inheritance works as you might expect: `Circle`s now have all the methods of `Shape`s, as well as `@radius`.
134166135167Think about if the following would return an error, and why. (Hint: check the class definition for `Shape` again.)
136168···200232)
201233202234Circle <- new_class("Circle",
203203- Shape,
235235+ parent = Shape,
204236 properties = list(
205237 radius = positive_numeric
206238 )
···208240```
209241210242211211-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.
243243+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.
212244213245```{r}
214246#| error: true
···232264Shape <- new_class("Shape", abstract = TRUE)
233265234266Circle <- new_class("Circle",
235235- Shape,
267267+ parent = Shape,
236268 properties = list(
237269 radius = positive_numeric
238270 )
239271)
240272```
241273242242-Here we've included what we've written so far, with a small change to `Shape` to disallow instantiation of arbitrary `Shape`s.
274274+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:
275275+ <!-- with a small change to `Shape` to disallow instantiation of arbitrary `Shape`s. -->
243276244277```{r}
245278#| error: true
···250283251284```{r}
252285Circle <- new_class("Circle",
253253- Shape,
286286+ parent = Shape,
254287 properties = list(
255288 radius = positive_numeric,
256289 area = new_property(class_numeric, getter = function(self) pi * self@radius^2, setter = NULL)
···258291)
259292```
260293261261-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.
294294+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.
262295263296```{r}
264297#| error: true
···275308276309```{r}
277310Rect <- new_class("Rect",
278278- Shape,
311311+ parent = Shape,
279312 properties = list(
280313 width = positive_numeric,
281314 height = positive_numeric,
···295328296329```{r}
297330Square <- new_class("Square",
298298- Rect,
331331+ parent = Rect,
299332 constructor = function(side) {
300333 force(side)
301301- new_object(S7_object(), width = side, height = side)
334334+ new_object(Rect, width = side, height = side)
302335 }
303336)
304337```
305338306306-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.
339339+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).
307340308341```{r}
309342Square@constructor
310343Rect@constructor
311344```
312345313313-We didn't set a default value for `@side` in the constructor, meaning that this won't work:
346346+We didn't set a default value for `side` in the constructor, meaning that this won't work:
314347315348```{r}
316349#| error: true
317350Square()
318351```
319352320320-And since `Square`s are `Rect`s, we get all the same nice things.
321321-353353+And since `Square`s are `Rect`s, we get all the same nice things `Rect`s do.
322354323355```{r}
324356#| error: true
···334366335367```{r}
336368Square <- new_class("Square",
337337- Rect,
369369+ parent = Rect,
338370 properties = list(
339371 area = new_property(class_numeric, getter = function(self) self@width^2, setter = NULL)
340372 ),
341373 constructor = function(side) {
342374 force(side)
343343- new_object(S7_object(), width = side, height = side)
375375+ new_object(Rect, width = side, height = side)
344376 }
345377)
346378···349381Square@properties$area$getter
350382```
351383352352-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!
384384+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.
385385+386386+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!
353387354388## Generics and Methods
355389356390[Website](https://rconsortium.github.io/S7/articles/generics-methods.html)
357391392392+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.
393393+394394+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.
395395+396396+[^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)
397397+398398+```{r}
399399+positive_numeric <- new_property(class_numeric,
400400+ validator = function(value) {
401401+ if (value <= 0) "must be greater than 0"
402402+ }
403403+)
404404+405405+Shape <- new_class("Shape",
406406+ properties = list(
407407+ sides = new_property(class_integer,
408408+ validator = function(value) {
409409+ if (value <= 0) "must be greater than 0"
410410+ }
411411+ )
412412+ ),
413413+ abstract = TRUE
414414+)
415415+416416+Circle <- new_class("Circle",
417417+ parent = Shape,
418418+ properties = list(
419419+ radius = positive_numeric
420420+ ),
421421+ constructor = function(radius) {
422422+ force(radius)
423423+ new_object(S7_object(), radius = radius, sides = 1L)
424424+ }
425425+)
426426+427427+Rect <- new_class("Rect",
428428+ parent = Shape,
429429+ properties = list(
430430+ width = positive_numeric,
431431+ height = positive_numeric
432432+ ),
433433+ constructor = function(width, height) {
434434+ force(width)
435435+ force(height)
436436+ new_object(S7_object(), width = width, height = height, sides = 4L)
437437+ }
438438+)
439439+440440+Square <- new_class("Square",
441441+ parent = Rect,
442442+ constructor = function(side) {
443443+ force(side)
444444+ new_object(Rect, width = side, height = side, sides = 4L)
445445+ }
446446+)
447447+```
448448+449449+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.
450450+451451+```{r}
452452+Area <- new_generic("Area", "shape")
453453+method(Area, Circle) <- function(shape) {
454454+ pi * shape@radius^2
455455+}
456456+```
457457+458458+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.
459459+460460+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:
461461+462462+```{r}
463463+#| error: true
464464+method(Area, Circle) <- function(x) {
465465+ pi * x@radius^2
466466+}
467467+```
468468+469469+And now we can calculate the area of `Circle`s!
470470+471471+```{r}
472472+Area(Circle(10))
473473+```
474474+475475+Since we haven't defined methods for any other `Shape`s, this will result in an error.
476476+477477+```{r}
478478+#| error: true
479479+Area(Square(5))
480480+```
481481+482482+With our type hierarchy, we could define a "default" area by creating a method for `Shape`.
483483+484484+```{r}
485485+method(Area, Shape) <- function(shape) {
486486+ "No method defined for this Shape!"
487487+}
488488+489489+Area(Square(5))
490490+method_explain(Area, Square)
491491+492492+method(Area, Rect) <- function(shape) {
493493+ shape@height * shape@width
494494+}
495495+496496+Area(Square(5))
497497+method_explain(Area, Square)
498498+```
499499+500500+Similarly, we could get the number of sides of any `Shape` with minimal effort, due to how we defined `@sides`.
501501+502502+```{r}
503503+number_of_sides <- new_generic("number_of_sides", "x")
504504+method(number_of_sides, Shape) <- function(x) {
505505+ x@sides
506506+}
507507+508508+number_of_sides(Square(5))
509509+number_of_sides(Rect(4, 2))
510510+number_of_sides(Circle(10))
511511+512512+method_explain(number_of_sides, Circle)
513513+```
514514+515515+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.
516516+517517+```{r}
518518+method(length, Circle) <- function(x) {
519519+ 2 * pi * x@radius
520520+}
521521+522522+length(Circle(10))
523523+524524+method(length, Rect) <- function(x) {
525525+ 2 * (x@height + x@width)
526526+}
527527+528528+length(Square(5))
529529+```
530530+531531+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:
532532+533533+```{r}
534534+median_Shapes <- new_external_generic("stats", "median", "x")
535535+method(median_Shapes, Shape) <- function(x) {
536536+ "You can't take the median of a Shape!"
537537+}
538538+539539+median(Rect(4, 2))
540540+```
541541+542542+Since S7 is built atop of S3, you could do something like this:
543543+544544+```{r}
545545+median.Shape <- function(x) {
546546+ "Yay S3"
547547+}
548548+median(Rect(4, 2))
549549+```
550550+551551+but I strongly recommend that you use `method()` instead.
552552+553553+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!
554554+555555+## Conclusion
556556+557557+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!