···20202121This 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.
22222323-<!-- ## Getting Started -->
2323+## Classes, Properties, & Objects
24242525-We will start with the example from the readme--`Shape`s. We will be constructing a type hierarchy with Squares, Rectangles, Circles, & Shapes, with the typical relationships (e.g. all Squares are Rectangles, all Rectangles and Circles are Shapes). Let us start with `Shape`s though.
2525+[Website](https://rconsortium.github.io/S7/articles/classes-objects.html)
2626+2727+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.
26282729```{r}
2830library(S7)
···120122121123`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]
122124125125+[^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)`).
126126+123127```{r}
124128class(Shape())
129129+125130class(Circle())
126131```
127132128133Inheritance works as you might expect: Circles now have all the methods of Shapes, as well as `@radius`.
129134130130-Think about if the following would return an error, and why. (Hint: check the class definition again.)
135135+Think about if the following would return an error, and why. (Hint: check the class definition for `Shape` again.)
131136132137```{r}
133138#| eval: false
139139+Shape <- new_class("Shape",
140140+ properties = list(
141141+ name = class_character,
142142+ sides = class_integer
143143+ )
144144+)
145145+134146Circle("circle", 1, 5)
135147```
136148···173185)
174186```
175187176176-`validator()` is
188188+`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.
189189+190190+```{r}
191191+Shape <- new_class("Shape",
192192+ properties = list(
193193+ name = class_character,
194194+ sides = new_property(class_integer,
195195+ validator = function(value) {
196196+ if (value <= 0) "must be greater than 0"
197197+ }
198198+ )
199199+ )
200200+)
201201+202202+Circle <- new_class("Circle",
203203+ Shape,
204204+ properties = list(
205205+ radius = positive_numeric
206206+ )
207207+)
208208+```
209209+210210+211211+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.
212212+213213+```{r}
214214+#| error: true
215215+216216+Shape("myshape", 0L)
217217+218218+Circle("circle", -1L, -10)
219219+220220+Circle("circle", 1L, -10)
221221+```
222222+223223+Note that the property name is automatically added to the validator's error message. Let us further explore properties.
224224+225225+```{r}
226226+positive_numeric <- new_property(class_numeric,
227227+ validator = function(value) {
228228+ if (value <= 0) "must be greater than 0"
229229+ }
230230+)
231231+232232+Shape <- new_class("Shape", abstract = TRUE)
233233+234234+Circle <- new_class("Circle",
235235+ Shape,
236236+ properties = list(
237237+ radius = positive_numeric
238238+ )
239239+)
240240+```
241241+242242+Here we've included what we've written so far, with a small change to `Shape` to disallow instantiation of arbitrary `Shape`s.
243243+244244+```{r}
245245+#| error: true
246246+Shape()
247247+```
248248+249249+Let's look at some more interesting things we can do with properties.
250250+251251+```{r}
252252+Circle <- new_class("Circle",
253253+ Shape,
254254+ properties = list(
255255+ radius = positive_numeric,
256256+ area = new_property(class_numeric, getter = function(self) pi * self@radius^2, setter = NULL)
257257+ )
258258+)
259259+```
260260+261261+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.
262262+263263+```{r}
264264+#| error: true
265265+circ <- Circle(10)
266266+267267+circ@area
268268+circ@radius <- 20
269269+circ@area
270270+271271+circ@area <- 0
272272+```
273273+274274+Circles are getting boring; let's add Rectangles and Squares!
275275+276276+```{r}
277277+Rect <- new_class("Rect",
278278+ Shape,
279279+ properties = list(
280280+ width = positive_numeric,
281281+ height = positive_numeric,
282282+ area = new_property(class_numeric, getter = function(self) self@width * self@height, setter = NULL)
283283+ )
284284+)
285285+```
286286+287287+I hope by now the class definition should be pedestrian. The only real difference between `Rect` and `Circle` is that `Rect` has an extra property and a different means to compute area.
288288+289289+```{r}
290290+Rect(10, 6)
291291+Rect(10, 10)
292292+```
293293+294294+We can easily extend our shapes to further include `Square`s.
295295+296296+```{r}
297297+Square <- new_class("Square",
298298+ Rect,
299299+ constructor = function(side) {
300300+ force(side)
301301+ new_object(S7_object(), width = side, height = side)
302302+ }
303303+)
304304+```
177305178178-<!-- https://rconsortium.github.io/S7/articles/classes-objects.html#validation-1 -->
306306+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.
179307180180-[^2]: S7 is [built atop of S3](https://rconsortium.github.io/S7/articles/compatibility.html), so S3 methods still work like `{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)`).308308+```{r}
309309+Square@constructor
310310+Rect@constructor
311311+```
312312+313313+We didn't set a default value for `@side` in the constructor, meaning that this won't work:
314314+315315+```{r}
316316+#| error: true
317317+Square()
318318+```
319319+320320+And since `Square`s are `Rect`s, we get all the same nice things.
321321+322322+323323+```{r}
324324+#| error: true
325325+326326+s <- Square(10)
327327+s
328328+s@area <- 0
329329+330330+Square(-9)
331331+```
332332+333333+We could've overwritten `Rect`'s `@area` to exploit our knowledge of `Square`s:
334334+335335+```{r}
336336+Square <- new_class("Square",
337337+ Rect,
338338+ properties = list(
339339+ area = new_property(class_numeric, getter = function(self) self@width^2, setter = NULL)
340340+ ),
341341+ constructor = function(side) {
342342+ force(side)
343343+ new_object(S7_object(), width = side, height = side)
344344+ }
345345+)
346346+347347+Square(5)
348348+349349+Square@properties$area$getter
350350+```
351351+352352+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!
353353+354354+## Generics and Methods
355355+356356+[Website](https://rconsortium.github.io/S7/articles/generics-methods.html)
357357+