[This article was first published on rstats on Irregularly Scheduled Programming, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.In this post I explore some differences between R, python, julia, and APL interms of mutability, and try to make something that probably shouldn’t exist.
I watched this code_report video which describesa leetcode problem;
You are given an integer array
nums, an integerk, and an integermultiplier.You need to perform
koperations onnums. In each operation:
- Find the minimum value
xinnums. If there are multiple occurrences of the minimum value, select the one that appears first.- Replace the selected minimum value
xwithx * multiplier.Return an integer array denoting the final state of nums after performing all
koperations.
Conor’s python solution in the video was
def getFinalState(nums, k, m): for \_ in range(k): i = nums.index(min(nums)) nums[i] *= m return numsx = [2, 1, 3, 5, 6]k = 5mult = 2getFinalState(x, k, mult)## [8, 4, 6, 5, 6]
and, as always, I wanted to see how I’d do that in R. I came up with this
getFinalState = function(nums, k, m) { for (i in 1:k) { m <- which.min(nums)[1] nums[m] <- mult * nums[m] } nums}x <- c(2, 1, 3, 5, 6)k <- 5mult <- 2getFinalState(x, k, mult)## [1] 8 4 6 5 6
It’s worth noting that I can’t use a map in this function because iterationsare dependent; the minimum value at any iteration depends on the previousvalues.
I also had a chance to discuss this solution with some APL’ers at a meetup anda J solution was presented, but I don’t think I wrote it down.
My solution is nearly word-for-word the same as the python solution with acouple of notable exceptions arising from the difference between the twolanguages:
First, R has which.min() as a built-in rather than needing to query the indexof the minimum value (and two references to nums). Also, R has no compoundassignment like x *= 2 which modifies in-place - the closest thing I can thinkof is the %<>% operator in {magrittr} (not re-exported in {dplyr} because thisbehaviour is considered bad practice in R, despite not really being “in-place”)
library(magrittr)m <- data.frame(x = 1:6, y = letters[1:6])m## x y## 1 1 a## 2 2 b## 3 3 c## 4 4 d## 5 5 e## 6 6 fm %<>% head(2)m## x y## 1 1 a## 2 2 b
although I can certainly see the case for it - this operator avoids repeatingthe variable being used and assigned, because the alternative using thetraditional pipe is
m <- data.frame(x = 1:6, y = letters[1:6])m## x y## 1 1 a## 2 2 b## 3 3 c## 4 4 d## 5 5 e## 6 6 fm <- m %>% head(2)m## x y## 1 1 a## 2 2 b
One could argue that writing out even a longer variable name twice still makesit clear that shadowing is taking place; the value is being overwritten witha new value, but it does feel a little frustrating to have to type it out twice
important\_variable <- important\_variable * 2
Back to my R solution, the indexing at a specific set of values got me thinkingthat it would be clean if we could pass a function to [ so that we couldwrite
nums[which.min] <- value
(maybe not so much for this example where m is used twice, but it piqued myinterest)
Let’s say I want to set all the even values of a vector to some other value.That’s easy enough to do
x[x %% 2 == 0] <- 0
but I don’t love that it requires two references to x, which may (should?) bea much longer name
important\_variable[important\_variable %% 2 == 0] <- 0
I want something like x[f] <- y to set the values of x where f(x) isTRUE to y. This seemed like it might be possible, maybe with a functionmethod to [<-, but [<- dispatches on the class of x, not what’s inside[, so no dice. In theory (which will never happen) the built-in [<- couldhave some branch logic for dealing with a function passed as the indices to bemodified, but I’m not about to go rebuilding R from source myself just to playwith that idea.
Nonetheless, if I define some functions that do accomplish this
is\_even <- function(z) z %% 2 == 0set\_if <- function(x, f, value) { x[f(x)] <- value x}
then I can try this out on a vector
a <- 1:10a## [1] 1 2 3 4 5 6 7 8 9 10set\_if(a, is\_even, 0)## [1] 1 0 3 0 5 0 7 0 9 0a # unchanged## [1] 1 2 3 4 5 6 7 8 9 10
It works, but I’m back to having to write a <- do_stuff(a) because a isn’tactually modified by this function.
Ideally, my function would operate the same as this does
a <- 1:10a[is\_even(a)] <- 0a## [1] 1 0 3 0 5 0 7 0 9 0
which does modify a in-place; R is not entirely pure, and does occasionallyallow what looks like direct mutation, though under the hood, it’s not - a newobject is actually created
```
``` Note that the memory address has changed.
If I was working with a language which did support (enable?) modify-in-placethen that might look like
def is\_even(x): return x % 2 == 0def set\_if(x, f, value): for i in range(len(x)): if f(x[i]): x[i] = valuea = list(range(10))a## [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]set\_if(a, is\_even, 0)a## [0, 1, 0, 3, 0, 5, 0, 7, 0, 9]
Now, that’s not always a great thing. In such a language with mutable structures(e.g. lists) we can do maddening things like this
x = [3, 4, 5]y = xy is x## Truey[1] = 9x # still 'bound' to y## [3, 9, 5]
Here, is means “are these two things identical in the sense of referring tothe same block of memory”, noting that literals (e.g. single numbers) arereferenced that way, but tuples aren’t
abc = (11, 99)xyz = (11, 99)abc is xyz## Falseabc == xyz## True
The big question is can I hack together some solution that does work in-placein R? Yeah, with some ill-advised calls
set\_if <- function(x, f, value) { # can't use <<- because the value passed in as the x argument isn't # necessarily named 'x' in the parent scope .x <- x .x[f(.x)] <- value e <- parent.env(environment()) assign(deparse(substitute(x)), .x, pos = e) invisible(.x)}a <- 1:10a## [1] 1 2 3 4 5 6 7 8 9 10set\_if(a, is\_even, 0)a## [1] 1 0 3 0 5 0 7 0 9 0
As I note in the comment there, I can’t use the super-assignment arrow <<-inside this function because I don’t know the name of the variable I’m updating;it needs to be deparsed from the incoming argument.
This means that it works regardless of the name of the variable being modified
b <- 10:20b## [1] 10 11 12 13 14 15 16 17 18 19 20set\_if(b, is\_even, 0)b## [1] 0 11 0 13 0 15 0 17 0 19 0
I tried to think of some other languages which might support this sort of in-placeset_if(x, f, value) modification and (Dyalog) APL was worth a thought.
⍝ create a vector from 1 to 10 x←⍳10 x1 2 3 4 5 6 7 8 9 10 ⍝ the function {0=2|⍵} calculates a boolean vector with ⍝ 1 where the value is even {0=2|⍵} x0 1 0 1 0 1 0 1 0 1 ⍝ the `@` operator takes a value (or function) on the left and ⍝ a function (or boolean values) on the right and applies it to the ⍝ other argument on the right 0@{0=2|⍵} x 1 0 3 0 5 0 7 0 9 0 ⍝ alternatively a point-free function defined as the negation (`~`) of a ⍝ binding (`∘`) of the value 2 to modulo (`|`); the negation is needed ⍝ otherwise this returns the result of the modulo, not where it is 0 0@(~2∘|)⍳101 0 3 0 5 0 7 0 9 0 ⍝ x is, however, unchanged as APL is typically immutable x1 2 3 4 5 6 7 8 9 10
So there’s no way to do the in-place modification. it is nice, though, that0@(~2∘|)x only refers to x once.
Julia makes a nice distinction between functions which mutate arguments andthose which don’t; (by convention) the former are named ending with anexclamation mark, e.g.
vec = collect(1:5)## 5-element Vector{Int64}:## 1## 2## 3## 4## 5# non-mutatingreverse(vec)## 5-element Vector{Int64}:## 5## 4## 3## 2## 1vec## 5-element Vector{Int64}:## 1## 2## 3## 4## 5# mutatingreverse!(vec)## 5-element Vector{Int64}:## 5## 4## 3## 2## 1vec## 5-element Vector{Int64}:## 5## 4## 3## 2## 1
In julia, the iseven() function is already built-in, but vectorisation isexplicit via a broadcast operator . and the setting of even values to 0looks like
x = collect(1:10);x[iseven.(x)] .= 0;x## 10-element Vector{Int64}:## 1## 0## 3## 0## 5## 0## 7## 0## 9## 0
which looks very much like the R version with some dots where scalar functionsare vectorised. If I don’t use the last . to perform vectorised assignment,the error tells me that the failure involved the setindex! function which doessound like what I want, but this doesn’t work
setindex!(x, 0, iseven.(x))
because it’s trying to assign the value 0 multiple times and I only provided oneof them. Instead,
x = collect(1:10);setindex!(x, zeros(Int8, 5), iseven.(x));x## 10-element Vector{Int64}:## 1## 0## 3## 0## 5## 0## 7## 0## 9## 0
does work, but I had to manually count how many 0 entries this requires, so the[ approach seems cleaner. Either way, I’ve had to explicitly calculateiseven(x) and pass that result somewhere.
Since Julia allows users to extend methods, I could do that modification myself!
import Base.setindex! function setindex!(A::Vector{Int64}, v::Int64, f::Function) A[f.(A)] .= vend## setindex! (generic function with 240 methods)x = collect(1:10);setindex!(x, 0, iseven);x## 10-element Vector{Int64}:## 1## 0## 3## 0## 5## 0## 7## 0## 9## 0
which I could just as easily call set_if!
set\_if! = setindex!;x = collect(1:10);set\_if!(x, 0, iseven);x## 10-element Vector{Int64}:## 1## 0## 3## 0## 5## 0## 7## 0## 9## 0
Nice! I do wonder if I can “hack” (ahem, extend) Julia’s [ to get my prizedx[f] = 0 solution but I doubt it’s worth it when the above does the rightthing.
I don’t imagine I’ll package up my set_if() anywhere, and I should probablyeven avoid using it myself, but it’s been an interesting journey thinking aboutthis stuff. Maybe there’s a better way to do it? Maybe there’s a language whichbetter supports something like that? If you know, or you have comments orsuggestions, I can be found onMastodon or use the comment section below.
devtools::session_info() ```
``` To leave a comment for the author, please follow the link and comment on their blog: rstats on Irregularly Scheduled Programming.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.Continue reading: In-Place Modifications