Advent of Array Elegance (AoC2023 Day 7)

I’m solving Advent of Code this year using a relaxed criteria compared to last year in that I’m allowing myself to use packages where they’re helpful, rather than strictly base R. Last year I re-solved half of the exercises using Rust which helped me learn a lot about Rust. This year I’m enamored with APL, and I wanted to share a particularly beautiful solution.

⚠⚠⚠⚠⚠

Spoilers ahead for Day 7, in case you haven’t yet completed it yourself.

⚠⚠⚠⚠⚠

I solved Day 7 of Advent of Code using base R by testing whether or not a given hand was of each type with an individual function, either returning 0 (if it was not of that type) or N + a score, where N was sufficiently different between each type that they would sort nicely. For the score, I initially tried offsetting each card in a poor-man’s base-15 as 15^(4:0)*card_score but later improved on that by using hex digits (which automatically sort nicer). The large N values ensured that ‘type’ would be sorted before the first/second/etc.. card.

That was sufficient to do an apply(strength, hands), calculate the order of those, and multiply by the relevant bids. Aside from a bug not caught by the test case (the difference between bid*order(x) and bid[order(x)]*seq_along(x)) it was an okay solution to the problem, and it worked.

After solving each day, I’ve been trying to re-solve using APL; in particular Dyalog APL. For those who don’t know, APL is an old language (circa 1960s) borne from a mathematical notation in which a single glyph (symbol) represents some operation or application of a function. This makes it look very different to more modern languages, partly because of the glyphs, but also because it requires no boilerplate whatsoever. As an array language, it deals with vectors and matrices without needing to “loop over columns” or “for i in values”. It looks scary at first, but it’s really not - once you’re familiar with the glyphs it’s actually beautiful!

Let’s say you have a matrix m which contains the values 1 through 9

    m
1 2 3
4 5 6
7 8 9

and you want to sum the columns. Chances are, the language you normally use will require you to first calculate the size of the matrix, maybe even perform a loop. In APL, it’s

    +⌿m
12 15 18

is the glyph for “reduce along first axis”, or perform some operation (supplied as its left argument) to its right argument. +⌿ is therefore “sum columns”. No boilerplate, just a direct explanation (the glyphs themselves are better names than any word you could attach to them) of what needs to be done.

Sure, you need to learn the glyphs, and potentially even how to enter them; one option being a prefix key then a corresponding key. How committed am I to learning those, you ask? Well, here’s my laptop

My laptop with APL stickers on the keys
My laptop with APL stickers on the keys

I considered using APL for my Day 7 solution, but it was so many functions defined, and fiddly if/else logic, I figured it was just ill-suited to APL. Then I saw a video recap of an APL solution for Day 7 and my mind was blown.

Meanwhile, I saw a post from Elias Mårtenson, creator of the Kap language, promoting some examples of Kap and was even more interested given that it can do some things that (Dyalog) APL can’t, like produce graphics.

Can your APL do this?

    chart:line mtcars 
┌→──────────────────────────────────────────────────────────────┐
↓1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1│
│4 4 4 3 3 3 3 4 4 4 4 3 3 3 3 3 3 4 4 4 3 3 3 3 3 4 5 5 5 5 5 4│
│4 4 1 1 2 1 4 2 2 4 4 3 3 3 4 4 4 1 2 1 1 2 2 4 2 1 2 2 4 6 8 2│
└───────────────────────────────────────────────────────────────┘
chart:line mtcars in Kap
chart:line mtcars in Kap

Kap is a fairly new APL-based language (written in Kotlin) that supports most of Dyalog APL, but adds some cool extensions and alterations like lazy evaluation and parallel execution.

Uiua is another new language on the scene (written in Rust) which also supports graphics; the Uiua logo itself is written in Uiua

Xy ← °⍉⊞⊟. ÷÷2: -÷2,⇡.200
Rgb ← [:°⊟×.Xy ↯△⊢Xy0.5]
u ← ↥<0.2:>0.7.+×2 ×.:°⊟Xy
c ← <:⍜°√/+ Xy
⍉⊂:-¬u c1 +0.1 ↧¤c0.95Rgb
Uiua logo, coded in Uiua
Uiua logo, coded in Uiua

The online editor for Uiua uses colours to distinguish different functions/operators, and the author has the flexibility to do what they want with that, so it’s awesome to see what they’ve used for “all” () and “transpose” ()…

Uiua coloured glyphs for ‘all’ and ‘transpose’
Uiua coloured glyphs for ‘all’ and ‘transpose’

I figured I’d try to reproduce the APL solution in Kap as a way to learn more about that language. The APL/Kap solution is so elegant! Additionally, I tried writing equivalent R code. I’ll interleave all three in this post (a nice excuse to get tabsets working!).

Reading Input

To start with, get the data into the workspace - this reads in a vector with each element representing a row of input

  • APL

    Reading from a file is performed using ⎕NGET

        ⊃⎕NGET'p07.txt'1
     32T3K 765  T55J5 684  KK677 28  KTJJT 220  QQQJA 483
  • Kap

    Kap uses some namespaces, which makes reading in a bit nicer, and the output is boxed, with explicit quotes for strings

    p ← io:read "p07.txt"
    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
    ┃"32T3K 765" "T55J5 684" "KK677 28" "KTJJT 220" "QQQJA 483"┃
    ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
  • R

    readLines reads in each line as an element of a vector

    p <- readLines("example07.txt")
    p
    ## [1] "32T3K 765" "T55J5 684" "KK677 28"  "KTJJT 220" "QQQJA 483"

Preprocessing

The input consists of hands of cards juxtaposed with a bid value, separated by a space. The approach here is not to treat them individually, but to create a matrix containing columns of hands and columns of bids.

  • APL

    Partition ((≠⊆⊢)) on spaces (' ') for each (¨) row

        ' '(≠⊆⊢)¨⊃⎕NGET'p07.txt'1
     32T3K  765    T55J5  684    KK677  28    KTJJT  220    QQQJA  483

    It’s not entirely clear from this layout, but this is a vector of length-2 vectors.

    These are “mixed” (stacked; ), and the result assigned () to p

    p←↑' '(≠⊆⊢)¨⊃⎕NGET'p07.txt'1
     32T3K  765 
     T55J5  684 
     KK677  28  
     KTJJT  220 
     QQQJA  483 

    This is now a matrix, where the first column contains the hands, the second (last) column contains the bids.

  • Kap

    Rather than the partition idiom, Kap has regex support, so splitting the components involes regex:split for each (¨) element of input

    p←⊃{" " regex:split ⍵}¨p
    ┌→────────────┐
    ↓"32T3K" "765"│
    │"T55J5" "684"│
    │"KK677"  "28"│
    │"KTJJT" "220"│
    │"QQQJA" "483"│
    └─────────────┘
  • R

    The boilerplate of R’s matrix construction takes a toll after using APL/Kap…

    p <- matrix(unlist(strsplit(p, " ")), ncol = 2, byrow = TRUE)
    p
    ##      [,1]    [,2] 
    ## [1,] "32T3K" "765"
    ## [2,] "T55J5" "684"
    ## [3,] "KK677" "28" 
    ## [4,] "KTJJT" "220"
    ## [5,] "QQQJA" "483"

Extraction

The hands and bids can be extracted into their own variables.

  • APL

    This can be achieved several ways, but a clean way is by reducing (/) with either the ‘leftmost’ () or ‘rightmost’ () operator, and evaluating (executing ) each (¨) of the bids to convert from strings to numbers

    hands←⊣/p
    bids←⍎¨⊢/p
  • Kap

    Kap uses exactly the same approach as APL for this

    hands←⊣/p
    bids←⍎¨⊢/p
  • R

    R’s ‘subset by index’ works just fine, but if this was generalised I’d use something like p[, ncol(p)] to get to the last column

    hands <- p[,1]
    hands
    ## [1] "32T3K" "T55J5" "KK677" "KTJJT" "QQQJA"
    bids <- as.integer(p[,2])
    bids
    ## [1] 765 684  28 220 483

Tabulate

Now comes the interesting part! Rather than deal with the types separately, one approach is to identify them by their relative counts; a five-of-a-kind has 5 of one card and nothing elese; a four-of-a-kind has four of one and one of another.

  • APL

    APL has a “key” () which takes a function as a left argument, which can be to count the occurrences of each element with “tally” ()

          {⍺,≢⍵}⌸'TGGATAACTTGAAC'
    T 4
    G 3
    A 5
    C 2

    In this case, we can get just the tallied count of each card in the hand with

        {⊢∘≢⍵}⌸¨hands
    2 1 1 1  1 3 1  2 1 2  1 2 2  3 1 1

    We can then sort (⍵[⍒⍵]) these, take just the first two values (2↑), and decode () using base 10 to a single number. A nice feature of APL is that trying to take the “first N” elements of a single element pads to the full N with zeroes.

        f←{10⊥2↑{⍵[⍒⍵]}⊢∘≢⌸⍵}
        f¨hands
    21 31 22 22 31
  • Kap

    Kap doesn’t have the equivalent Key, but after some discussion with the creator, it’s entirely possible to get something that does the same

      key⇐(⍪+⌿≡⌻)∘∪ ⍝ using outer product - see the R solution
      key2⇐{u←∪⍵ ⋄ c←⍸˝∧u⍳⍵} ⍝ using inverse 'where' and 'index of'
    
      key2¨hands
    ┌→────────────────────────────────────────┐
    │┌→──────┐ ┌→────┐ ┌→────┐ ┌→────┐ ┌→────┐│
    ││2 1 1 1│ │1 3 1│ │2 1 2│ │1 2 2│ │3 1 1││
    │└───────┘ └─────┘ └─────┘ └─────┘ └─────┘│
    └─────────────────────────────────────────┘

    The rest is the same as APL, except Kap uses a dedicated sort ()

        handrank⇐{10⊥2↑∨⊢/key ⍵}
        handrank¨hands
    ┏━━━━━━━━━━━━━━┓
    ┃21 31 22 22 31┃
    ┗━━━━━━━━━━━━━━┛
  • R

    I wanted to recreate the above approach in R, so this will take the long way ’round.

    First, we need a ‘key’ function

    key <- function(x) {
      l <- strsplit(x, "")[[1]]
      setNames(colSums(outer(l, unique(l), "==")), unique(l))
    }
    
    sapply(hands, key)
    ## $`32T3K`
    ## 3 2 T K 
    ## 2 1 1 1 
    ## 
    ## $T55J5
    ## T 5 J 
    ## 1 3 1 
    ## 
    ## $KK677
    ## K 6 7 
    ## 2 1 2 
    ## 
    ## $KTJJT
    ## K T J 
    ## 1 2 2 
    ## 
    ## $QQQJA
    ## Q J A 
    ## 3 1 1

    The idea of this is to create an outer product between the set of unique letters in the string, and the individual letters, performing an == check on each combination

    y <- strsplit(hands[2], "")[[1]]
    outer(y, unique(y), "==")
    ##       [,1]  [,2]  [,3]
    ## [1,]  TRUE FALSE FALSE
    ## [2,] FALSE  TRUE FALSE
    ## [3,] FALSE  TRUE FALSE
    ## [4,] FALSE FALSE  TRUE
    ## [5,] FALSE  TRUE FALSE

    This is, of course, unnecessary as R has a way to do this

    table(y)
    ## y
    ## 5 J T 
    ## 3 1 1

    but I wanted to see how to do it from scratch.

    Applying this over the hands, we can sort each of the counts again, but now taking the first two values fails for the five-of-a-kind which only has a 5, so in that case I add the missing 0. Decoding as base 10 can be done a couple of ways, but pasting and converting seems to work fine.

    handrank <- function(x) {
      rank <- sort(sapply(x, key), decreasing = TRUE)
      if (length(rank) == 1) rank <- c(rank, 0)
      as.integer(paste(rank[1:2], collapse = ""))
    }
    
    sapply(hands, handrank)
    ## 32T3K T55J5 KK677 KTJJT QQQJA 
    ##    21    31    22    22    31

Subsequent Rankings and Answer

Finally, the part where the ‘array’ approach shines! Rather than constructing some sortable number for each hand, we can just score each card and use an array.

  • APL

    Creating a vector of all the cards is aided by the ‘numbers as a string’ helper ⎕D. Drop the first two of these (2↓) then append the ‘face’ cards

        r←'TJQKA',⍨2↓⎕D
        r
    23456789TJQKA

    Stacking the hands into a matrix of cards

        ↑hands
    32T3K
    T55J5
    KK677
    KTJJT
    QQQJA

    we can ask for the index of matches to the individual cards with

        r⍳↑hands
     2  1  9  2 12
     9  4  4 10  4
    12 12  5  6  6
    12  9 10 10  9
    11 11 11 10 13

    Prepending (,) each column with the tabulated type of each hand

        r{⍵,⍺⍳↑hands}f¨hands
    21  2  1  9  2 12
    31  9  4  4 10  4
    22 12 12  5  6  6
    22 12  9 10 10  9
    31 11 11 11 10 13

    Now, some real magic… APL support “total array ordering” which means we can just sort the entire thing - it will sort by the first column, using the second and subsequent columns for ties. Given that the first column is the ‘type’ of hand, and subsequent columns are values of each card in order, that’s precisely the sorting we need!

        r{⍋⍋⍵,⍺⍳↑hands}f¨hands
    1 4 3 2 5

    There’s a nice discussion about why the double grading from BQN

    Finally, multiplying by the bids themselves, and sum-reducing gives the final answer

      +/r{bids×⍋⍋⍵,⍺⍳↑hands}f¨hands
    6440
  • Kap

    This is mostly the same solution as APL, except I couldn’t find the ‘numbers as string’ so i just typed it out. Kap also uses ‘disclose’ () in place of mix () (ref).

        ranks←"23456789TJQKA"
        +/ranks{bids×1+⍋⍋⍵,⍺⍳⊃hands}handrank¨hands
    6440
  • R

    R doesn’t support Total Array Ordering, but it does seem to have a way to do it, so say the documentation examples for order

    ## or along 1st column, ties along 2nd, ... *arbitrary* no.{columns}:
    dd[ do.call(order, dd), ]

    That only works for a data.frame, which is a list (per do.call’s requirement). We can still work with that. First, smoosh together all the hands and convert the individual cards to a matrix - again, a long line of commands for what is reasonably straightforward in APL… 3 3⍴'abcdefghi' reshapes those 9 letters into a 3x3 matrix.

    m <- matrix(strsplit(paste0(hands, collapse = ""), "")[[1]], ncol = 5, byrow = TRUE)
    m
    ##      [,1] [,2] [,3] [,4] [,5]
    ## [1,] "3"  "2"  "T"  "3"  "K" 
    ## [2,] "T"  "5"  "5"  "J"  "5" 
    ## [3,] "K"  "K"  "6"  "7"  "7" 
    ## [4,] "K"  "T"  "J"  "J"  "T" 
    ## [5,] "Q"  "Q"  "Q"  "J"  "A"

    The individual cards vector benefits from coercing the digits to characters

    ranks <- c(2:9, "T", "J", "Q", "K", "A")

    The index mapping does actually work nicely with match, except it returns a single vector, not a matrix, so we need to reshape yet again. Plus, this time, the matches went down columns not along rows, so we need to use byrow = FALSE

    mm <- matrix(match(m, ranks), ncol = 5, byrow = FALSE)
    mm
    ##      [,1] [,2] [,3] [,4] [,5]
    ## [1,]    2    1    9    2   12
    ## [2,]    9    4    4   10    4
    ## [3,]   12   12    5    6    6
    ## [4,]   12    9   10   10    9
    ## [5,]   11   11   11   10   13

    Prepending with the type rankings does work nicely via cbind

    g <- cbind(sapply(hands, handrank), mm)
    g
    ##       [,1] [,2] [,3] [,4] [,5] [,6]
    ## 32T3K   21    2    1    9    2   12
    ## T55J5   31    9    4    4   10    4
    ## KK677   22   12   12    5    6    6
    ## KTJJT   22   12    9   10   10    9
    ## QQQJA   31   11   11   11   10   13

    Then ordering with the do.call idiom

    gdf <- as.data.frame(g)
    gdf[do.call(order, gdf), ]
    ##       V1 V2 V3 V4 V5 V6
    ## 32T3K 21  2  1  9  2 12
    ## KTJJT 22 12  9 10 10  9
    ## KK677 22 12 12  5  6  6
    ## T55J5 31  9  4  4 10  4
    ## QQQJA 31 11 11 11 10 13

    Putting this all together into a function

    sortrank <- function(x, y) {
      m <- matrix(strsplit(paste0(y, collapse = ""), "")[[1]], ncol = 5, byrow = TRUE)
      mm <- matrix(match(m, x), ncol = 5, byrow = FALSE)
      g <- cbind(sapply(y, handrank), mm)
      do.call(order, as.data.frame(g))
    }
    
    sortrank(ranks, hands)
    ## [1] 1 4 3 2 5

    This isn’t the double sorting that APL and Kap used, and that little difference is what held me up for all too long trying to figure out why my solution passed tests but gave the wrong answer. Annoyingly, this mistake doesn’t show up in the test case because the ranks only differ by a switched place. The true input was not so kind.

    This result is the order in which we need to place the bids, so doing that, then multiplying by the position (since it’s sorted, this is just a vector from 1 to the number of elements) we get the answer

    sum(bids[sortrank(ranks, hands)]*seq_along(bids))
    ## [1] 6440

Summary

So, how do these solutions all look? I’ll stop with the tabsets for a side-by-side comparison.

Compacting the APL solution (which does involve some duplication) it’s as simple as

p←↑' '(≠⊆⊢)¨⊃⎕NGET'p07.txt'1
+/('TJQKA',⍨2↓⎕D){(⍎¨⊢/p)×⍋⍋⍵,⍺⍳↑⊣/p}{10⊥2↑{⍵[⍒⍵]}⊢∘≢⌸⍵}¨⊣/p

which, admittedly, requires a fair amount of unpacking to read. In full form, it’s

p←↑' '(≠⊆⊢)¨⊃⎕NGET'p07.txt'1
hands←⊣/p
bids←⍎¨⊢/p
f←{10⊥2↑{⍵[⍒⍵]}⊢∘≢⌸⍵}
r←'TJQKA',⍨2↓⎕D
+/r{bids×⍋⍋⍵,⍺⍳↑hands}f¨hands

which is still pretty nice, considering what it’s doing.

The R solution, somewhat minimally, and leveraging table, is

handrank <- function(x) {
  rank <- sort(sapply(strsplit(x, ""), table), decreasing = TRUE)
  if (length(rank) == 1) rank <- c(rank, 0)
  as.integer(paste(rank[1:2], collapse = ""))
}

sortrank <- function(x, y) {
  m <- matrix(strsplit(paste0(y, collapse = ""), "")[[1]], ncol = 5, byrow = TRUE)
  mm <- matrix(match(m, x), ncol = 5, byrow = FALSE)
  g <- cbind(sapply(y, handrank), mm)
  do.call(order, as.data.frame(g))
}

solve <- function(x) {
  p <- matrix(unlist(strsplit(x, " ")), ncol = 2, byrow = TRUE)
  hands <- p[,1]
  bids <- as.integer(p[,2])
  ranks <- c(2:9, "T", "J", "Q", "K", "A")
  sum(bids[sortrank(ranks, hands)]*seq_along(bids))
}

solve(readLines("example07.txt"))

Certainly more typing, but still a much shorter solution than the one I originally came up with.

Takeaways

Both APL and Kap (and so many other languages) benefit greatly from treating a string as an array of characters. This always hurts in R, where strsplit(x, "") is needed.

The array approach here highlights how one can think differently about a problem, provided the tools are at hand.

Kap has a lot to offer - it’s (vastly) newer, which comes with both advantages (can do new things) and disadvantages (things need to be implemented, and they won’t necessarily carry over 1:1).

Advent of Code once again proves to be a useful exercise.

One more thing

I saw a solution in Uiua on Mastodon and had to give it a go, too…

Input ← ⊜(⊜□≠@ .)≠@\n.&fras"p07.txt"
Label ← ⇌"AKQJT98765432"
Bids ← ⋕⊢↘1⍉
Cards ← ⊐≡(⊗:Label)⊢⍉
Types ← 0_1_2_4_8_5_10_9_3_6_12_11_13_7_14_15⊚1_4_3_3_2_2_1
TypeStr ← ⊏⊗⊙Types≡(°⋯≡/=◫2⊏⍏.)
/+×+1⍏⍏/+×ⁿ⇌⇡6⧻Label⊂⊃TypeStr⍉⊃Cards Bids Input

I think this is taking the same approach, though unpacking this is even trickier.


Comments and improvements most welcome. I can be found on Mastodon or use the comments below.


devtools::session_info()
## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.3.2 (2023-10-31)
##  os       Pop!_OS 22.04 LTS
##  system   x86_64, linux-gnu
##  ui       X11
##  language (EN)
##  collate  en_AU.UTF-8
##  ctype    en_AU.UTF-8
##  tz       Australia/Adelaide
##  date     2023-12-10
##  pandoc   3.1.1 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/ (via rmarkdown)
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package     * version date (UTC) lib source
##  blogdown      1.18    2023-06-19 [1] CRAN (R 4.3.2)
##  bookdown      0.36    2023-10-16 [1] CRAN (R 4.3.2)
##  bslib         0.5.1   2023-08-11 [3] CRAN (R 4.3.1)
##  cachem        1.0.8   2023-05-01 [3] CRAN (R 4.3.0)
##  callr         3.7.3   2022-11-02 [3] CRAN (R 4.2.2)
##  cli           3.6.1   2023-03-23 [3] CRAN (R 4.2.3)
##  crayon        1.5.2   2022-09-29 [3] CRAN (R 4.2.1)
##  devtools      2.4.5   2022-10-11 [1] CRAN (R 4.3.2)
##  digest        0.6.33  2023-07-07 [3] CRAN (R 4.3.1)
##  ellipsis      0.3.2   2021-04-29 [3] CRAN (R 4.1.1)
##  evaluate      0.22    2023-09-29 [3] CRAN (R 4.3.1)
##  fastmap       1.1.1   2023-02-24 [3] CRAN (R 4.2.2)
##  fs            1.6.3   2023-07-20 [3] CRAN (R 4.3.1)
##  glue          1.6.2   2022-02-24 [3] CRAN (R 4.2.0)
##  htmltools     0.5.6.1 2023-10-06 [3] CRAN (R 4.3.1)
##  htmlwidgets   1.6.2   2023-03-17 [1] CRAN (R 4.3.2)
##  httpuv        1.6.12  2023-10-23 [1] CRAN (R 4.3.2)
##  icecream      0.2.1   2023-09-27 [1] CRAN (R 4.3.2)
##  jquerylib     0.1.4   2021-04-26 [3] CRAN (R 4.1.2)
##  jsonlite      1.8.7   2023-06-29 [3] CRAN (R 4.3.1)
##  knitr         1.44    2023-09-11 [3] CRAN (R 4.3.1)
##  later         1.3.1   2023-05-02 [1] CRAN (R 4.3.2)
##  lifecycle     1.0.3   2022-10-07 [3] CRAN (R 4.2.1)
##  magrittr      2.0.3   2022-03-30 [3] CRAN (R 4.2.0)
##  memoise       2.0.1   2021-11-26 [3] CRAN (R 4.2.0)
##  mime          0.12    2021-09-28 [3] CRAN (R 4.2.0)
##  miniUI        0.1.1.1 2018-05-18 [1] CRAN (R 4.3.2)
##  pkgbuild      1.4.2   2023-06-26 [1] CRAN (R 4.3.2)
##  pkgload       1.3.3   2023-09-22 [1] CRAN (R 4.3.2)
##  prettyunits   1.2.0   2023-09-24 [3] CRAN (R 4.3.1)
##  processx      3.8.2   2023-06-30 [3] CRAN (R 4.3.1)
##  profvis       0.3.8   2023-05-02 [1] CRAN (R 4.3.2)
##  promises      1.2.1   2023-08-10 [1] CRAN (R 4.3.2)
##  ps            1.7.5   2023-04-18 [3] CRAN (R 4.3.0)
##  purrr         1.0.2   2023-08-10 [3] CRAN (R 4.3.1)
##  R6            2.5.1   2021-08-19 [3] CRAN (R 4.2.0)
##  Rcpp          1.0.11  2023-07-06 [1] CRAN (R 4.3.2)
##  remotes       2.4.2.1 2023-07-18 [1] CRAN (R 4.3.2)
##  rlang         1.1.1   2023-04-28 [3] CRAN (R 4.3.0)
##  rmarkdown     2.25    2023-09-18 [3] CRAN (R 4.3.1)
##  rstudioapi    0.15.0  2023-07-07 [3] CRAN (R 4.3.1)
##  sass          0.4.7   2023-07-15 [3] CRAN (R 4.3.1)
##  sessioninfo   1.2.2   2021-12-06 [1] CRAN (R 4.3.2)
##  shiny         1.7.5.1 2023-10-14 [1] CRAN (R 4.3.2)
##  stringi       1.7.12  2023-01-11 [3] CRAN (R 4.2.2)
##  stringr       1.5.0   2022-12-02 [3] CRAN (R 4.3.0)
##  urlchecker    1.0.1   2021-11-30 [1] CRAN (R 4.3.2)
##  usethis       2.2.2   2023-07-06 [1] CRAN (R 4.3.2)
##  vctrs         0.6.4   2023-10-12 [3] CRAN (R 4.3.1)
##  xfun          0.40    2023-08-09 [3] CRAN (R 4.3.1)
##  xtable        1.8-4   2019-04-21 [1] CRAN (R 4.3.2)
##  yaml          2.3.7   2023-01-23 [3] CRAN (R 4.2.2)
## 
##  [1] /home/jono/R/x86_64-pc-linux-gnu-library/4.3
##  [2] /usr/local/lib/R/site-library
##  [3] /usr/lib/R/site-library
##  [4] /usr/lib/R/library
## 
## ──────────────────────────────────────────────────────────────────────────────


APL  Kap  rstats  Uiua 

See also