Repaying Tom Nook with {S7}

oop
r
r6
s7
videogames
Author
Published

February 26, 2023

Fish-eye lens selfie of the player-character from the game Animal Crossing New Horizons. The character is wearing a knitted black hoodie with bright green letters that say 'S7'. The picture is taken in the Resident Services building. Tom Nook, a raccoon-dog character, is in the background staring ominously at the player.

tl;dr

The R7 S7 object-oriented system is coming to R. I’ve done a little R6-to-S7 translation on an old project to get a very cursory feel for it, featuring Animal Crossing New Horizons.

Note

The S7 system and package are under development and could change at any time, rendering everything in this post useless.1 Heck, last time I checked, the system was called ‘R7’. There’s also a chance that S7 elements may have been integrated into base R itself by the time you read this.

2020 again, oh no

Animal Crossing New Horizons (ACNH) was the perfect pandemic game. And the pandemic was the perfect time to build an ersatz version of the ACNH in-game banking system to solve an exercise in the Advanced R book using the {R6} package for object-oriented programming (OOP) in R.

The exercise helped me fantasize about defeating the game’s main boss, the predatory loanshark (loanraccoon?) Tom Nook, via endless wire transfers of hard-earned in-game currency, called ‘Bells’.

Of course, a lot has changed since 2020. Most importantly, a new OOP system for R is being developed. Conversely, Tom Nook has not changed. He is still a scourge.

Anyway, maybe this is a chance to twitch my OOP muscles with this new system.

OOP they did it again

The R Consortium’s OOP working group has been beavering (raccooning?) away to develop a new OOP system from the ground up: S72 (S3 + S4, geddit?).

The idea is to take the best elements of the existing and in-built S3 and S4 systems, interface with them and improve on them.

You can read various design docs and meeting minutes on their documentation site, which is housed in their ‘OOP-WG’ GitHub repo, and try out the current iteration of the associated package, fittingly called {S7}.

You should refer to their docs in the first instance, or a useful third party review. For example, Jumping Rivers have… jumped the river on this one and produced a handy intro.

A new horizon for OOP

Naturally, I should revisit my post on Repaying Tom Nook with {R6} by replicating it with {S7}. Naturally.

Aha, but actually the {S7} package is more like a development of S3 and S4 objects, and is not a ‘new version’ of {R6}! Ah well. I’m noodling around with {S7} for my own devices and thought I’d post it here so I can refer back to it later.

Basically I’m recycling content from a previous post to get a feel for the new system. But only in the most superficial, basic way. I spent about 15 minutes on this. Look elsewhere for actually-usefully material. You have been warned.

Install

For now, the {S7} package is in the R Consortium’s OOP-WG GitHub repo.

install.packages("remotes")  # if not yet installed
remotes::install_github("RConsortium/OOP-WG")

And for some glamour we’ll also use the quintessential {emoji} package3

install.packages("emoji")  # if not yet installed
library(emoji)

That is class

A new class is constructed with… new_class()

We can give it a name. We can also give it properties: fields that contain data and can be provided a type check and default value. It’s possible to build validators for these as well, which ensure that certain conditions are met when the properties are adjusted. I’ll keep this simple for now: I just want the values to remain equal or greater than zero.

ABD <- new_class(
  name = "ABD",
  properties = list(
    savings = new_property(class_integer, default = 0L),
    loan = new_property(class_integer, default = 2498000L)
  ),
  validator = function(self) {
    if (self@savings < 0L) {
      "@savings must be zero or more"
    } else if (self@loan < 0L) {
      "@loan must be zero or more"
    }
  }
)

For new methods, you can create a new ‘generic’ and define a function for it. For example, the ‘deposit’ method is pretty straightforward: it just adds an amount to the current savings value.

deposit <- new_generic("deposit", "x")

method(deposit, ABD) <- function(x, amount) {
  x@savings <- x@savings + amount
  x
}

I specified some other methods, but I hid them because they’re not much more complicated.

Click for more methods

The ‘withdraw’ method subtracts a specified amount from the savings property. You’re warned if you specify an amount greater than the amount available.

withdraw <- new_generic("withdraw", "x")

method(withdraw, ABD) <- function(x, amount) {
  
  if (x@savings - amount < 0L) {
    warning(
      "Withdrew all savings: ", x@savings, " Bells.\n", 
      call. = FALSE
    )
    x@savings <- 0L
  } else {
    x@savings <- x@savings - amount
  }
  
  x
  
}

The ‘pay’ method moves funds from savings to loan. You’re warned if the loan is already paid, if you specify a greater amount than there are savings, or if you pay a greater amount than the loan remaining. You’ll get a victory message if you pay off the whole loan.

pay <- new_generic("pay", "x")

method(pay, ABD) <- function(x, amount) {
  
  if (x@loan == 0L) {
    stop("You already finished paying your loan!\n", call. = FALSE)
  }
  
  if (x@savings - amount < 0L) {
    warning(
      "Paid total amount from savings instead: ", x@savings, " Bells.\n",
      call. = FALSE
    )
    x@loan <- x@loan - x@savings
    x@savings <- 0L
  } else if (x@loan - amount < 0L) {
    warning(
      "Paid total remaining loan instead: ", x@loan, " Bells.\n",
      call. = FALSE
    )
    x@savings <- x@savings - x@loan 
    x@loan <- 0L
  } else {
    x@savings <- x@savings - amount
    x@loan <- x@loan - amount
  }
  
  if (x@loan == 0L) {
    cat(
      emoji("smiley"),
      "Sweet! I finally finished paying off my very last home loan!",
      emoji("tada"), "\n\n"
    )
  }
  
  x
  
}

The check method is basically a print method. It reports the loan and savings amounts currently stored in the bank.

check <- new_generic("check", "x")

method(check, ABD) <- function(x) {

  loan_formatted <- format(x@loan, big.mark = ",", scientific = FALSE)

  savings_formatted <- format(x@savings, big.mark = ",", scientific = FALSE)

  cat("Automatic Bell Dispenser (ABD)\n\n")
  cat(emoji("bell"), "Loan Balance:", loan_formatted, "Bells\n")
  cat(emoji("pig2"), "Savings Balance:", savings_formatted, "Bells\n\n")
  cat(
    "Please make a selection from the menu below\n\n",
    emoji("house"), "pay()\n",
    emoji("arrow_up"), "deposit()\n",
    emoji("arrow_down"), "withdraw()"
  )

}

You can start a new instance of the ABD class by, y’know, calling it.

bank <- ABD()

When you check the class of this object, you’ll see both the custom class name and a reminder that it has the ‘S7’ class.

class(bank)
[1] "ABD"       "S7_object"

The vanilla print method exposes the properties and their startup values:

bank
<ABD>
 @ savings: int 0
 @ loan   : int 2498000

Note that the properties are prepended with @. This indicates that we can use the ‘at’ symbol to access these ‘slots’ (like S4) from the object, like:

bank@loan
[1] 2498000

While we’re printing stuff, we can use the check() method (that I’ve pre-specified) to see the properties in a manner that more closely resembles the game.

check(bank)
Automatic Bell Dispenser (ABD)

🔔 Loan Balance: 2,498,000 Bells
🐖 Savings Balance: 0 Bells

Please make a selection from the menu below

 🏠 pay()
 ⬆️ deposit()
 ⬇️ withdraw()

You can easily and directly change the properties. To add 10 Bells:

bank@savings <- 9.99
Error: <ABD>@savings must be <integer>, not <double>

Haha, whoops. Remember I specified that the property can only be an integer, so we need to provide an integer value instead of a double value. In other words, we can only provide whole numbers of Bells. Remember that the L suffix is used in R to signify an integer.4

bank@savings <- 10L

Is there an overdraft? Tom Nook would probably love that and would ask for massive overdraft fees, but it’s not programmed into the game. This is where our validator comes in handy. We specified that you can’t have a negative amount of savings, so this causes an error:

bank@savings <- -11L
Error: <ABD> object is invalid:
- @savings must be zero or more

That’s fine, but I have sometimes I have extra logic I want to evaluate when I adjust the properties. That’s why I created new methods earlier on. It means I can use a function to add to the savings property instead, for example.

bank <- deposit(bank, 10L)
bank@savings
[1] 10

We can retrieve Bells in this fashion too:

bank <- withdraw(bank, 10L)
bank@savings
[1] 0

What if we deposit enough Bells to pay the loan?

bank <- deposit(bank, 2500000L)
bank <- pay(bank, 2500000L)
Warning: Paid total remaining loan instead: 2498000 Bells.
😃 Sweet! I finally finished paying off my very last home loan! 🎉 

The method warns us when we try to pay off a value greater than the remaining loan and prints a nice congratulatory message if we’ve cleared the whole debt.

And so we end up with this view:

check(bank)
Automatic Bell Dispenser (ABD)

🔔 Loan Balance: 0 Bells
🐖 Savings Balance: 2,000 Bells

Please make a selection from the menu below

 🏠 pay()
 ⬆️ deposit()
 ⬇️ withdraw()

Huzzah. Get rekt, raccoon dog. More like Tom Crook amirite.

Fish-eye lens selfie of the player-character from the game Animal Crossing New Horizons. The character is wearing a knitted black hoodie with bright green letters that say 'S7'. The picture is taken in the Resident Services building.

Environment

Session info
Last rendered: 2023-08-20 22:31:34 BST
R version 4.3.1 (2023-06-16)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Ventura 13.2.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.11.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] emoji_15.0    S7_0.0.0.9000

loaded via a namespace (and not attached):
 [1] digest_0.6.33     fastmap_1.1.1     xfun_0.39         fontawesome_0.5.1
 [5] magrittr_2.0.3    glue_1.6.2        stringr_1.5.0     knitr_1.43.1     
 [9] htmltools_0.5.5   rmarkdown_2.23    lifecycle_1.0.3   cli_3.6.1        
[13] compiler_4.3.1    rstudioapi_0.15.0 tools_4.3.1       evaluate_0.21    
[17] yaml_2.3.7        rlang_1.1.1       jsonlite_1.8.7    htmlwidgets_1.6.2
[21] stringi_1.7.12   

Footnotes

  1. ‘Useless’ is an extremely relative term with regard to this blog.↩︎

  2. 95% certain that ‘S7’ is pronounced how a snake might say ‘seven’: like ‘sseven’.↩︎

  3. {emo} is dead, long live {emoji}. Haha, joke’s on you, emo will never die. I know this because ‘emo’ was in my top 5 genres on Spotify Wrapped 2022, lololol.↩︎

  4. Why L? Shrug. Just take the L.↩︎

Reuse

CC BY-NC-SA 4.0