42

著書「Software for Data Analysis: Programming with R」の中で、John Chambers は、関数は通常、副作用のために書かれるべきではないと強調しています。むしろ、関数は呼び出し環境で変数を変更せずに値を返す必要があります。逆に、data.table オブジェクトを使用して適切なスクリプトを作成する場合は<-、通常は関数の結果を格納するために使用される でのオブジェクト割り当ての使用を明確に避ける必要があります。

まず、技術的な質問です。引数としてオブジェクトproc1を受け入れるR 関数が呼び出されると想像してください (さらに、おそらく他のパラメーターも)。NULL を返しますが、を使用して変更します。私が理解していることから、呼び出しは、約束が機能する方法のためにコピーを作成します。ただし、以下に示すように、元のオブジェクトはまだ によって変更されています。なぜ/どうですか?data.tablexproc1x:=proc1proc1(x=x1)x1x1proc1

> require(data.table)
> x1 <- CJ(1:2, 2:3)
> x1
   V1 V2
1:  1  2
2:  1  3
3:  2  2
4:  2  3
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> proc1(x1)
NULL
> x1
   V1 V2 y
1:  1  2 2
2:  1  3 3
3:  2  2 4
4:  2  3 6
> 

さらに、使用proc1(x=x1)は x で直接プロシージャを実行するよりも遅くはないようです。これは、promise に関する私の漠然とした理解が間違っており、それらが参照渡しのような方法で機能することを示しています。

> x1 <- CJ(1:2000, 1:500)
> x1[, paste0("V",3:300) := rnorm(1:nrow(x1))]
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> system.time(proc1(x1))
   user  system elapsed 
   0.00    0.02    0.02 
> x1 <- CJ(1:2000, 1:500)
> system.time(x1[,y:= V1*V2])
   user  system elapsed 
   0.03    0.00    0.03 

したがって、data.table 引数を関数に渡しても時間がかからないことを考えると、data.table の速度と関数の一般化可能性の両方を組み込んで、data.table オブジェクトのプロシージャを作成できます。しかし、関数に副作用があってはならないという John Chambers の発言を考えると、この種の手続き型プログラミングを R で書くことは本当に「OK」なのだろうか? なぜ彼は副作用が「悪い」と主張したのですか? 彼のアドバイスを無視する場合、どのような落とし穴に注意すればよいですか? 「良い」data.table プロシージャを作成するにはどうすればよいですか?

4

2 に答える 2

30

はい、data.tables の列の追加、変更、削除は によって行われreferenceます。は通常大量のデータを保持し、変更が行われるたびにすべてを再割り当てするのは非常にメモリと時間がかかるため、ある意味では良いことです。data.table一方で、R がデフォルトで使用して促進しようとしている関数型プログラミングのアプローチに反するため、悪いことでもあります。副作用のないプログラミングでは、関数を呼び出すときに心配することはほとんどありません。入力や環境が影響を受けないことを確信でき、関数の出力に集中できます。シンプルだから着心地がいい。no-side-effectpass-by-value

もちろん、自分が何をしているのかわかっているのであれば、John Chambers のアドバイスを無視してもかまいません。「適切な」data.tables プロシージャの記述について、複雑さと副作用の数を制限する方法として、私があなただった場合に検討するいくつかのルールを次に示します。

  • 関数は複数のテーブルを変更してはなりません。つまり、そのテーブルの変更が唯一の副作用であるべきです。
  • 関数がテーブルを変更する場合、そのテーブルを関数の出力にします。もちろん、再割り当てはしたくdo.something.to(table)ありませんtable <- do.something.to(table)。代わりに、関数が別の (「実際の」) 出力を持っていた場合、 を呼び出すときresult <- do.something.to(table)に、出力に注意を集中し、関数の呼び出しがテーブルに副作用をもたらしたことを忘れてしまうことは容易に想像できます。

R では「1 つの出力/副作用なし」関数が標準ですが、上記の規則では「1 つの出力または副作用」が許可されます。副作用がなんらかの出力形式であることに同意する場合は、R の 1 出力の関数型プログラミング スタイルに緩く固執することで、ルールを曲げすぎないことに同意するでしょう。関数に複数の副作用を持たせることは、少し無理が​​あります。できないわけではありませんが、できれば避けたいと思います。

于 2012-12-07T03:53:15.573 に答える
17

ドキュメントは改善される可能性がありますが(提案は大歓迎です)、現時点での内容は次のとおりです。おそらく、「関数内でも」と言うべきでしょうか?

?":="

data.tablesは、:=、setkey、またはその他のset*関数によって変更時にコピーされません。コピーを参照してください。

DTは参照によって変更され、新しい値が返されます。コピーが必要な場合は、最初にコピーを取ります(DT2 = copy(DT)を使用)。このパッケージは、テーブル全体をコピーするよりも参照による更新が何桁も速くなる可能性がある(複数列のキーを持つ混合列タイプの)大きなデータ用であることを思い出してください。

そしてで?copy(しかし私はこれがsetkeyで混乱していることに気づきます):

入力は参照によって変更され、(目に見えないように)返されるため、複合ステートメントで使用できます。例:setkey(DT、a)[J( "foo")]。コピーが必要な場合は、最初にコピーを取ります(DT2 = copy(DT)を使用)。copy()は、参照によって列にサブ割り当てするために:=が使用される前に役立つ場合もあります。?copyを参照してください。setattrもパッケージビットに含まれていることに注意してください。どちらのパッケージも、Rの内部setAttrib関数をCレベルで公開するだけですが、戻り値が異なります。bit :: setattrはNULLを(目に見えないように)返し、関数がその副作用のために使用されていることを通知します。data.table :: setattrは、複合ステートメントで使用するために、変更されたオブジェクトを(目に見えない形で)返します。

ここで、最後の2つの文はbit::setattr、興味深いことに、flodelのポイント2に関連しています。

これらの関連する質問も参照してください。

data.tableが別のdata.tableへの(のコピーに対する)参照である場合を正確に理解する参照による受け渡し
:data.tableパッケージdata.tableの:=演算子
1.8.1:「DT1=DT2」はそうではありませんDT1 = copy(DT2)と同じですか?

私はあなたの質問のこの部分がとても好きです:

これにより、data.tableの速度と関数の一般化可能性の両方を組み込んだ、data.tableオブジェクトのプロシージャを記述できます。

はい、これは間違いなく意図の1つです。データベースがどのように機能するかを考えてみましょう。データベース内の1つ以上の(大きな)テーブルを参照(挿入/更新/削除)することで、多くの異なるユーザー/プログラムが変更されます。これはデータベースの世界ではうまく機能し、data.tableの考え方に似ています。したがって、ホームページ上のsvSocketビデオ、およびとの欲求insertdelete参照により、動詞のみ、副作用機能)。

于 2012-12-07T11:18:16.830 に答える