メモ:環境をコピーしたいときはrlang::env_clone()

環境、というかggplot2のレイヤーをコピーすることを考えます。

まず、1つレイヤーをつくります。

library(ggplot2)

d <- data.frame(
  x = 1:4,
  y = 1:4
)

l1 <- geom_point(d = d, aes(x, y))

このレイヤーの中身をいろいろ表示してみましょう。

l1
#> mapping: x = ~x, y = ~y 
#> geom_point: na.rm = FALSE
#> stat_identity: na.rm = FALSE
#> position_identity

names(l1)
#>  [1] "mapping"     "geom_params" "show.legend" "stat_params" "stat"       
#>  [6] "inherit.aes" "geom"        "position"    "super"       "data"       
#> [11] "aes_params"

l1$data
#>   x y
#> 1 1 1
#> 2 2 2
#> 3 3 3
#> 4 4 4

dataという項目に指定したデータが入っているのがわかりました。 これはggplot()と足し合わせれば当然グラフを描けます。

ggplot() + l1

次に、このレイヤーをコピーして、ちょっとずらした位置に赤い点をかぶせることを考えてみましょう。 l2という変数にl1を代入し、このdatax列に2をかけてみます。

l2 <- l1

l2$data
#>   x y
#> 1 1 1
#> 2 2 2
#> 3 3 3
#> 4 4 4

l2$data$x <- l2$data$x * 2

# 詳しい説明は省きますが、こうすると赤くなります
l2$aes_params$colour <- "red"

では、これを足し合わせてみましょう。

ggplot() + l1 + l2

あれ? なぜか赤い点しかなくなってしまっています。 l1の黒い点はどこに消えたのでしょう。

実は、l1のデータはl2のデータとまったく同じになっているので、赤い点の後ろに隠れてしまっているのです。

l1$data
#>   x y
#> 1 2 1
#> 2 4 2
#> 3 6 3
#> 4 8 4

変更を加えたのはl2dataに対してなのに、なぜl1dataまで変更されてしまっているのでしょうか。

これは、ggplot2のレイヤーが環境だからです。 print.default()でむりやり表示してみるとわかりますが、l1l2は同じ環境を指しています。 なので、環境の中のオブジェクトに変更を加えるとどちらにも影響するのです。

print.default(l1)
#> <environment: 0x000000001f924720>
#> attr(,"class")
#> [1] "LayerInstance" "Layer"         "ggproto"       "gg"           

print.default(l2)
#> <environment: 0x000000001f924720>
#> attr(,"class")
#> [1] "LayerInstance" "Layer"         "ggproto"       "gg"

ではどうすればいいかというと、古典的には、as.list()で環境をリストにし、list2env()でそのリストから新しい環境をつくる、という手があるみたいです。

stackoverflow.com

rlang::env_clone()が、そのアイディアのC実装版みたいです(コード)。 これを使ってもう一度やってみると、次のようになります。

l3 <- geom_point(d = d, aes(x, y))
l4 <- rlang::env_clone(l3)

l4$data$x <- l4$data$x * 2
l4$aes_params$colour <- "red"

# このままだとただの環境なので、クラスを合わせる必要がある
l4
#> <environment: 0x000000001e1b7910>

# クラスをあわせる
class(l4) <- class(l3)

ggplot() + l3 + l4

今度はうまくいきました。

ちなみに、ggprotoではなくR6にはちゃんとclone()というメソッドが実装されています。 コードを見るとまあまあ複雑そうでした。

github.com

まあディープコピーをどうする、であるとかを考えるといろいろありそうです。ちゃんとやる必要があるなら、もうちょっとちゃんと考えましょう、ということで。