The purpose of this tutorial is to give a very brief introduction to R6, limited to the functionality needed to go through the basic tutorials that are in this website. This includes: dollar sign notation, construction, methods & chaining, and cloning.
We won’t go into a full history of R here but we will give a very quick overview. R is a functional programming language that contains some object-oriented paradigms, namely S3, S4, R5 and R6. This means that whilst the majority of your time in R will be spent calling functions on variables with different arguments, it is also possible to create ‘objects’ that store data and are editable via methods. A method is essentially a function that is stored and acts on the object.
An object is an instance of a class, an instance is created by construction. In simpler and more practical terms: probability distributions in distr6 are stored as ‘classes’ and to use these distributions they have to be ‘constructed’. We will cover the construction of distributions in distr6 in the next tutorial, so for now we just focus on the R6 specifics.
Every distribution in distr6 is stored as a class, we will take the
Normal distribution as an example. Note the R class type is an
R6ClassGenerator
this tells us that this is a class to be
constructed (as opposed to an object that can be manipulated).
class(Normal)
#> [1] "R6ClassGenerator"
Constructing a class in R6 is always done using the same syntax
$new(...)
but the arguments passed to this function depend
on the class. Just like functions in R these arguments can be named or
un-named and a default may or may not be provided. In this case there is
a default so the following does not produce an error
Normal$new()
#> Norm(mean = 0, var = 1)
But we can also pass arguments in the usual way
Normal$new(var = 2)
#> Norm(mean = 0, var = 2)
See the help documentation for the specific arguments to pass to the constructor.
Methods work in the exact same way as functions except that again the
dollar sign notation is used, e.g. object$method(...)
.
Notice that the constructor $new()
is used for
classes whereas methods are called on objects. For
example let’s construct a Normal distribution object and save this
N <- Normal$new()
# Notice the R class is now 'Normal'
class(N)
#> [1] "Normal" "SDistribution" "Distribution" "R6"
To call the method mean
:
N$mean()
#> [1] 0
Or a method with arguments:
N$kurtosis(excess = FALSE)
#> [1] 3
A final point about methods is that they act on the object, this
means that whilst some will produce an output such as the ones above,
others will actually change the object. Whereas to update a variable in
R you may run something like y = y + 1
in R6 the change is
made in the method:
N$getParameterValue("var")
#> [1] 1
N$setParameterValue(var = 5)
N$getParameterValue("var")
#> [1] 5
Don’t worry about the particulars of these methods just yet, we will
return to them in later tutorials. For now just remember that
setParameterValue()
will update the object N
without us having to manually save this.
Method chaining is a slightly more advanced topic and therefore we will only very briefly discuss it here. It is essentially the act of combining multiple methods into one line (or chain), for example:
N$setParameterValue(var = 6)$getParameterValue("var")
#> [1] 6
See how we simply added one method to the end of the other using the same dollar sign syntax. Not all methods can be chained and do not worry if you don’t feel comfortable doing this, we’re just highlighting it for users more familiar with object-oriented programming.
Active bindings are similar to public variables, they are accessed
using $
, and make the R6 object look as if its a list. In
distr6
all active bindings are read-only, and exist for
object properties and traits. They differ from public methods as no
()
is required.
b <- Binomial$new()
# public variable
b$name
#> [1] "Binomial"
# active bindings
b$kurtosisType
#> Deprecated. Use $properties$kurtosis instead.
#> [1] "platykurtic"
b$inf
#> [1] 0
# public method
b$kurtosis()
#> [1] -0.2
b$mean()
#> [1] 5
In distr6, all mathematical functions are public methods (e.g. mean, variance, kurtosis), whereas all properties and traits are active bindings (e.g. kurtosisType, symmetry, inf, sup).
The final thing we discuss is an advanced topic but is very important in R6. The best way to demonstrate cloning is by example but don’t worry we won’t go into any of the technical details.
First we create a class called “adder”, again ignore the technicals here. Adder is a class with one method, which adds numbers to itself.
adder <- R6::R6Class("adder",public = list(add = function(y){
self$x = self$x + y
invisible(self)}, x = 0))
a = adder$new()
Here’s an example of it in action, see how the internal value is automatically updated without the object having to be re-saved.
a$x
#> [1] 0
a$add(2)
a$x
#> [1] 2
Now comes the tricky part, watch what happens when we copy the object
a
via b <- a
(equivalently
b = a
)
a <- adder$new()
b <- a
a$x
#> [1] 0
b$x
#> [1] 0
b$add(4)
b$x
#> [1] 4
a$x
#> [1] 4
See what happened? Even though we only called the method on
b
, the results were copied back to a
.
Sometimes this can be very useful but often it can cause confusion if
used by mistake. To get around this problem and create a completely
separate copy, use the $clone()
method:
a <- adder$new()
b <- a$clone()
a$x
#> [1] 0
b$x
#> [1] 0
b$add(4)
b$x
#> [1] 4
a$x
#> [1] 0
This can be a tricky concept to get your head around but try not to
worry too much about it and just remember that copying using
=
or <-
will lead to the values of both
variables being updated to always be the same; whereas copying using
$clone()
creates a completely separate object that is
identical at the time of cloning but then remains independent.