Skip to contents
library(tinycodet)
#> Run `?tinycodet::tinycodet` to open the introduction help page of 'tinycodet'.

The transform_if function

“Don’t Repeat Yourself”, sometimes abbreviated as “DRY”, is the coding principle that you should try to reduce repeating patterns in your code (within reason).

Consider the following code:

object <- matrix(c(-9:8, NA, NA) , ncol=2)
y <- 0
z <- 1000
ifelse(
  is.na(object>y), -z,
  ifelse(
    object>y,  log(object), object^2
  )
)
#> Warning in log(object): NaNs produced
#>       [,1]          [,2]
#>  [1,]   81     0.0000000
#>  [2,]   64     0.6931472
#>  [3,]   49     1.0986123
#>  [4,]   36     1.3862944
#>  [5,]   25     1.6094379
#>  [6,]   16     1.7917595
#>  [7,]    9     1.9459101
#>  [8,]    4     2.0794415
#>  [9,]    1 -1000.0000000
#> [10,]    0 -1000.0000000

Here a conditional subset of the object object is transformed where the condition is using a function referring to object itself. Consequently, reference to object is written 4 times! This can become cumbersome quickly. Notice also that the above code gives an unnecessary warning, due to ifelse() requiring the entirety of log(object).

The tinycodet package therefore adds the transform_if(x, cond, yes, no, other) function, which will “dry” this up. Here, in argument cond a function must be given that returns a logical vector. For every value where cond(x)==TRUE, function yes(x) is run, for every value where cond(x)==FALSE, function no(x) is run, and for every value where cond(x)==NA, function other is run. Because a function-based approach is used instead of directly supplying vectors, unnecessary warnings and annoying errors are avoided (unlike the above code).

The above code can now be re-written in a less warning/error prone and more compact manner as:

object |> transform_if(\(x)x>y, log, \(x)x^2, \(x) -z)
#>       [,1]          [,2]
#>  [1,]   81     0.0000000
#>  [2,]   64     0.6931472
#>  [3,]   49     1.0986123
#>  [4,]   36     1.3862944
#>  [5,]   25     1.6094379
#>  [6,]   16     1.7917595
#>  [7,]    9     1.9459101
#>  [8,]    4     2.0794415
#>  [9,]    1 -1000.0000000
#> [10,]    0 -1000.0000000

Instead of supplying a function for cond, one can also directly supply a logical vector to argument cond. Moreover, when the transformed value is an atomic scalar, you don’t really need a function; you can just fill in the scalar (vectors are not allowed though, as that will lead the same unnecessary warnings or even annoying errors as occur with ifelse()).

So one can thus also re-write the original code (without warnings/errors and more compact) as:

object |> transform_if(object > y, log, \(x)x^2, -z)
#>       [,1]          [,2]
#>  [1,]   81     0.0000000
#>  [2,]   64     0.6931472
#>  [3,]   49     1.0986123
#>  [4,]   36     1.3862944
#>  [5,]   25     1.6094379
#>  [6,]   16     1.7917595
#>  [7,]    9     1.9459101
#>  [8,]    4     2.0794415
#>  [9,]    1 -1000.0000000
#> [10,]    0 -1000.0000000

 

Atomic type casting without stripping attributes

Atomic type casting in R is generally performed using the functions as.logical(), as.integer(), as.double(), as.character().

These functions have the annoying property that they strip attributes. If you wish to convert a variable x whilst keeping the attributes, one must first safe the attributes of x before conversion, convert x, and then re-assign the attributes. ‘tinycodet’ adds functions that can do this for you, saving repititive code:

All attributes except the “class” attribute are preserved.

Examples:

x <- c(rep(0, 2), seq(0, 2.5, by=0.5)) |> matrix(ncol=2)
colnames(x) <- c("one", "two")
attr(x, "test") <- "test"
print(x)
#>      one two
#> [1,] 0.0 1.0
#> [2,] 0.0 1.5
#> [3,] 0.0 2.0
#> [4,] 0.5 2.5
#> attr(,"test")
#> [1] "test"

as_bool(x)
#>        one  two
#> [1,] FALSE TRUE
#> [2,] FALSE TRUE
#> [3,] FALSE TRUE
#> [4,]  TRUE TRUE
#> attr(,"test")
#> [1] "test"
as_int(x)
#>      one two
#> [1,]   0   1
#> [2,]   0   1
#> [3,]   0   2
#> [4,]   0   2
#> attr(,"test")
#> [1] "test"
as_dbl(x)
#>      one two
#> [1,] 0.0 1.0
#> [2,] 0.0 1.5
#> [3,] 0.0 2.0
#> [4,] 0.5 2.5
#> attr(,"test")
#> [1] "test"
as_chr(x)
#>      one   two  
#> [1,] "0"   "1"  
#> [2,] "0"   "1.5"
#> [3,] "0"   "2"  
#> [4,] "0.5" "2.5"
#> attr(,"test")
#> [1] "test"

 

Subset if and unreal replacement

The tinycodet package adds 2 “subset_if” operators:

  • The x %[if]% cond operator selects elements from vector/matrix/array x, for which the result of cond(x) returns TRUE.

  • The x %[!if]% cond operator selects elements from vector/matrix/array x, for which the result of cond(x) returns FALSE.

For example:

object_with_very_long_name <- matrix(-10:9, ncol=2)
print(object_with_very_long_name)
#>       [,1] [,2]
#>  [1,]  -10    0
#>  [2,]   -9    1
#>  [3,]   -8    2
#>  [4,]   -7    3
#>  [5,]   -6    4
#>  [6,]   -5    5
#>  [7,]   -4    6
#>  [8,]   -3    7
#>  [9,]   -2    8
#> [10,]   -1    9
object_with_very_long_name %[if]% \(x)x %in% 1:10
#> [1] 1 2 3 4 5 6 7 8 9
object_with_very_long_name %[!if]% \(x)x %in% 1:10
#>  [1] -10  -9  -8  -7  -6  -5  -4  -3  -2  -1   0

 

Another operator added by tinycodet is x %unreal =% y, which replaces all NA, NaN, Inf and -Inf in x with the value given in y.

So x %unreal =% y is the same as x[is.na(x)|is.nan(x)|is.infinite(x)] <- y.

 

General in-place modifier

This R package includes a general in-place modifying infix operator.

Consider the following line of code:

mtcars$mpg[mtcars$cyl>6] <- mtcars$mpg[mtcars$cyl>6]^2

The same expression, mtcars$mpg[mtcars$cyl>6], is written twice, making this code rather long and cumbersome, even though we’re just squaring the expression.

This R package solves the above laid-out problem by implementing a general in-place (mathematical) modifier, through the x %:=% f operator.

With tinycodet one can now make this more compact (more “tiny”, if you will) as follows:

mtcars$mpg[mtcars$cyl>6] %:=% \(x)x^2