プロトタイプ と サイズ

vctrs は class()length() を使う代わりに、 prototype (vec_ptype_show()) や size (vec_size()) という概念を持っています。 この vignette では、なぜこれらの代替概念が必要なのかという動機を説明し、それらの定義を型強制とリサイクルルールに結びつけます。

size と prototype は、c()rbind()の最適な動作を考えたときに、特に行列やデータフレームの列を持つデータフレームに触発されて生まれました。

library(vctrs)

Prototype

プロトタイプのアイデアは,データをキャプチャすることなく,ベクトルに関連するメタデータをキャプチャすることです.残念ながら、オブジェクトのclass()はこの目的には不十分です。

代わりに、vctrs は R のベクトル化された性質を利用して、 prototype というベクトルの 0 観測のスライスを使用します (これは基本的には x[0] ですが、後で説明するような微妙な点があります)。 これはベクトルのミニチュア版で、すべての属性を含みますが、データは含みません。

便利なことに,既存の Base 関数(例えば,double()factor(levels = c("a", "b"))など)を用いて,多くのプロトタイプを作成することができます。 vctrsは,RのBase 関数では同等のものがない場合に,いくつかのヘルパー(例えば,new_date(), new_datetime(), new_duration())を提供します。

Base prototypes

vec_ptype() は既存のオブジェクトからプロトタイプを作成します。しかし,多くのBase ベクトルは,0長の部分集合のための情報のない印刷方法を持っているので,vctrs は,フレンドリーな方法でプロトタイプを印刷する (そして何も返さない) vec_ptype_show() も提供しています。

vec_ptype_show()`を使うと,Rの基本クラスのプロトタイプを見ることができます.

共通タイプへの強制

vctrs は、vec_ptype_common() を通じて、強制のための一貫したルールを提供しています。vec_ptype_common() は以下の不変量を持ちます。

つまり、 vec_ptype_common() は(クラスに関して)可換性と連想性の両方を持ち、同一の要素であるNULLを持つ、つまり 可換性モノイド なのです。つまり、基本的な実装は非常にシンプルで、オブジェクトのペアの共通の型を順に見つけることで、任意の数のオブジェクトの共通の型を見つけることができます。

vec_ptype() と同様に、 vec_ptype_common() を探索する最も簡単な方法は、 vec_ptype_show() です: 複数の入力が与えられると、それらの共通のプロトタイプを表示します。(言い換えれば、 vec_ptype_common() でプログラムして、 vec_ptype_show() で遊ぼう、ということです。)

特定の型へのキャスト

vec_ptype_common()は,ベクトルの集合の共通型を求めるものです。しかし、一般的に欲しいのは、その共通型に強制されたベクトルのセットです。これが vec_cast_common() の仕事です。

また、vec_cast()を使って特定のプロトタイプにキャストすることもできます。

一般的なキャストは可能だが(例:double -> integer)、特定の入力に対して情報が失われる場合(例:1.5 -> 1)、エラーが発生します。

allow_lossy_cast()で、情報が失われるキャストのエラーを抑制することができます。

これにより、すべての情報が失われるキャストのエラーが抑制されます。許容される情報が失われるキャストのタイプを特定したい場合は、prototype を指定します。

キャストのセットは、強制セットよりも寛容であってはなりません。これは強制されるものではありませんが、クラスにはこのルールに従い、強制エコシステムを健全に保つことが求められます。

サイズ

vec_size()は,データ構造中の 「オブザベーション」 の数を記述する不変量を持つ必要性から生まれたものです。 これはデータフレームでは特に重要で、f(data.frame(x))f(x)が等しくなるような関数があると便利なのです。この特性を持つベース関数はありません。

vec_size()を以下のように定義します。

vec_size()が与えられれば,データフレームの正確な定義を与えることができます:データフレームは,すべてのベクトルが同じサイズを持つベクトルのリストです。これは,行列とデータフレームの列を自明にサポートするという望ましい特性を持っています。

スライス

vec_slice()vec_size() に対する [ に対する length() のようなものです; つまり,基礎となるオブジェクトの次元に関わらず,オブザベーションを選択することができます.vec_slice(x, i) は次のものと同じです。

vec_slice(data.frame(x), i)data.frame(vec_slice(x, i))に等しい(変数名と行名はモジュロ)。

プロトタイプは vec_slice(x, 0L) で生成されます。プロトタイプが与えられれば、vec_init() で、与えられたサイズのベクトル(NA で満たされる)を初期化することができます。

一般的なサイズ:リサイクルルール

サイズの定義と密接な関係にあるのが、__リサイクルルール__です。リサイクルルールは、異なるサイズの2つのベクトルを結合したときの出力のサイズを決定します。vctrs では、リサイクルルールは vec_size_common() でエンコードされており、これはベクトルの集合の共通サイズを与えます。

vctrs はbase R よりも厳しいリサイクルルールに従います。サイズ1のベクトルは他のサイズにリサイクルされます。他のサイズの組み合わせではエラーが発生します。この厳格さにより,dest == c("IAH", "HOU")) のようなよくあるミスを防ぐことができますが,その代償として,rep() の明示的な呼び出しが必要になることがあります。

Summary of vctrs recycling rules. X indicates n error

Summary of vctrs recycling rules. X indicates n error

リサイクルルールを適用するには2つの方法があります。

付録:Base Rでのリサイクル

Base R のリサイクルルールはThe R Language Definitionに記載されていますが、単一の関数で実装されていないため、一貫して適用されていません。ここでは、その最も一般的な実現方法を簡単に説明し、いくつかの例外も示します。

一般的に Base Rでは、ベクトルのペアが同じ長さでない場合、短い方のベクトルは長い方と同じ長さにリサイクルされます。

rep(1, 6) + 1
#> [1] 2 2 2 2 2 2
rep(1, 6) + 1:2
#> [1] 2 3 2 3 2 3
rep(1, 6) + 1:3
#> [1] 2 3 4 2 3 4

長い方のベクトルの長さが、短い方のベクトルの長さの整数倍でない場合、通常は警告が表示されます。:

invisible(pmax(1:2, 1:3))
#> Warning in pmax(1:2, 1:3): 引数は部分的に再利用されます
invisible(1:2 + 1:3)
#> Warning in 1:2 + 1:3: 長いオブジェクトの長さが短いオブジェクトの長さの倍数になっ
#> ていません
invisible(cbind(1:2, 1:3))
#> Warning in cbind(1:2, 1:3): number of rows of result is not a multiple of vector
#> length (arg 1)

しかし、機能によっては静かにリサイクルするものもあります。:

length(atan2(1:3, 1:2))
#> [1] 3
length(paste(1:3, 1:2))
#> [1] 3
length(ifelse(1:3, 1:2, 1:2))
#> [1] 3

また、data.frame()はエラーになります。:

data.frame(1:2, 1:3)
#> Error in data.frame(1:2, 1:3): arguments imply differing number of rows: 2, 3

R言語の定義では、「ゼロ長のベクトルを含む算術演算は、ゼロ長の結果を持つ」とされています。しかし、算術演算以外では、このルールは一貫して守られていません。

# length-0 output
1:2 + integer()
#> integer(0)
atan2(1:2, integer())
#> numeric(0)
pmax(1:2, integer())
#> integer(0)

# dropped
cbind(1:2, integer())
#>      [,1]
#> [1,]    1
#> [2,]    2

# recycled to length of first
ifelse(rep(TRUE, 4), integer(), character())
#> [1] NA NA NA NA

# preserved-ish
paste(1:2, integer())
#> [1] "1 " "2 "

# Errors
data.frame(1:2, integer())
#> Error in data.frame(1:2, integer()): arguments imply differing number of rows: 2, 0