I’m solving Advent of Code (adventofcode.com) 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

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 (www.youtube.com) and my mind was blown.
Meanwhile, I saw a post from Elias Mårtenson (fosstodon.org), 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│
└───────────────────────────────────────────────────────────────┘

Kap (kapdemo.dhsdevelopments.com) 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 (www.uiua.org) 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

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” (⍉
)…

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 (yihui.org) 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
Reading from a file is performed using ⎕NGET
⊃⎕NGET'p07.txt'1
32T3K 765 T55J5 684 KK677 28 KTJJT 220 QQQJA 483
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"┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
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.
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.
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"│
└─────────────┘
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.
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 uses exactly the same approach as APL for this
hands←⊣/p
bids←⍎¨⊢/p
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 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 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┃
┗━━━━━━━━━━━━━━┛
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.
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” (aplwiki.com) 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 (github.com)
Finally, multiplying by the bids themselves, and sum-reducing gives the final answer
+/r{bids×⍋⍋⍵,⍺⍳↑hands}f¨hands
6440
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 (kapdemo.dhsdevelopments.com)).
ranks←"23456789TJQKA"
+/ranks{bids×1+⍋⍋⍵,⍺⍳⊃hands}handrank¨hands
6440
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 (mastodon.social) 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 (fosstodon.org) 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
##
## ──────────────────────────────────────────────────────────────────────────────