S7クラス

S7クラス(Vaughan et al. 2026)はRのほぼ最後発のオブジェクト指向プログラミングのためのクラスで、S3とS4をまとめて整理し、いずれは置き換えることを目標として開発されています。S7は2022年から開発されていて、今のところS7はライブラリとして提供されています。ライブラリとして提供されているため、利用するためにはライブラリをインストールする必要があります。

ライブラリのインストール・ロード
# ggplot2で使われているので、ggplot2をロードすると自動的にロードされる
pacman::p_load(S7)

S7は使い勝手としてはS4によく似ており、S4の過剰に複雑な部分を排除し、しっかりとしたバリデーションの方法を追加したものとなっています。

S7はR6とは異なり、R的な使い方を想定しており、メソッドは提供されず、ジェネリック関数をクラスの定義とは別に定義します。ですので、RubyやPythonのようなオブジェクト指向プログラミングの言語を用いている人からするとやや不自然に感じるかもしれません。

ココでは、簡単にS7の使い方について説明してみます。ただし、S7は開発中のクラスであり、仕様が変更となる可能性があります。仕様の変更はなるべくフォローするようにしますが、正確な使い方についてはS7のドキュメントを参照してください。

クラスの定義

S7では、クラスの定義にnew_class関数を用います。このnew_class関数は第一引数にクラス名、第二引数にproperties(S4のSlot、R6のメンバーと同じくクラスの要素)、そしてvalidatorを引数に取ります。基本的にvalidatorを設定して使うことが想定されており、propertiesが変更を受ける場合にはvalidatorに指定した条件が常にチェックされます。したがって、S7ではpropertiesの型安全性が高いことが大きな特徴となっています。

S7:クラスの定義
# クラス名と同名の変数に代入して用いる
MyDog <-
  new_class(
    "MyDog",
    properties = 
      list(
        name = class_character,
        age = class_numeric,
        trick = class_character
      ),
    
    validator = function(self){
      # NULLが返ってくるとエラーなし,返り値があるとエラーが出る
      if(length(self@name) != 1){
        "2つ以上の名前を付けることはできません"
      } else if (length(self@age) != 1){
        "2つ以上の年齢を付けることはできません"
      } else if (self@age < 0 | self@age > 20){
        "年齢は0歳以上20歳未満として下さい"
      }
    }
  )

S7のクラス名はアッパーキャメルケースとし、同名の変数に代入して用います。以下の例では、"MyDog"クラスをMyDogという変数に代入しています。

propertiesはリストで指定します。リストでは、名前側がpropertiesの名前、リストの値側はそのpropertiesのデータ型を指定するようにします。このデータ型の指定には、class_characterclass_numericclass_logicalのようなデータ型を固定するためのS7のクラスを指定します。

型固定用のS7クラス
# S7でbaseやS3のクラスを囲っている(ラッパー)
class_character
<S7_base_class>: <character>
class_Date
<S7_S3_class>: S3<Date>

この指定したクラス以外の値をpropertiesは取ることができません。したがって、S3やS4、R6よりも型安全性が高いpropertiesを設定することができます。

クラスの代入の演算子::=

クラスの作成時には、クラス名と同じ名前の変数に代入します。この代入は:=という演算子で置き換えることができます。

:=演算子
# 最新のdevelopmental versionで登録されたため、:=を使えるようにするとggplot2が使えなくなる
A := new_class()

後ほどcalloutで説明するように、S7ではクラス名と変数名が異なっていてもクラスを作れてしまいますし、ジェネリック関数を作成するときには関数名と関数を代入する変数名が違っても関数を作れてしまいます。S7を使う場合には基本的にこの:=演算子を使った方がよいでしょう。

バリデータ

validatorselfを引数とした関数として設定します。関数内ではpropertiesに問題が無ければNULLが返り、問題があればエラーメッセージが返るようなif文を指定します。

上に示したMyDogクラスでは、以下のように設定することで名前や年齢のベクターの長さが1で、年齢が0歳以上20歳未満でないとメッセージが返るように指定しています。

バリデータの設定
validator = function(self){
  # NULLが返ってくるとエラーなし,返り値があるとエラーが出る
  if(length(self@name) != 1){
    "2つ以上の名前を付けることはできません"
  } else if (length(self@age) != 1){
    "2つ以上の年齢を付けることはできません"
  } else if (self@age < 0 | self@age > 20){
    "年齢は0歳以上20歳未満として下さい"
  }
}

クラスの表示

作成したクラスを表示すると、以下のようになります。

クラスの内容を表示する
MyDog
<MyDog> class
@ parent     : <S7_object>
@ constructor: function(name, age, trick) {...}
@ validator  : function(self) {...}
@ properties :
 $ name : <character>          
 $ age  : <integer> or <double>
 $ trick: <character>          

クラスの個々の要素は@$で確認できます。

クラスの要素を表示する
MyDog@constructor
function (name = character(0), age = integer(0), trick = character(0)) 
{
    name
    age
    trick
    S7::new_object(S7::S7_object(), name = name, age = age, trick = trick)
}
MyDog@validator
function (self) 
{
    if (length(self@name) != 1) {
        "2つ以上の名前を付けることはできません"
    }
    else if (length(self@age) != 1) {
        "2つ以上の年齢を付けることはできません"
    }
    else if (self@age < 0 | self@age > 20) {
        "年齢は0歳以上20歳未満として下さい"
    }
}
MyDog@properties$name
<S7_property> 
 $ name     : chr "name"
 $ class    : <S7_base_class>: <character>
 $ getter   : NULL
 $ setter   : NULL
 $ validator: NULL
 $ default  : NULL

クラスオブジェクト(インスタンス)の作成

クラスのオブジェクトは、クラスの定義を代入した変数(上のMyDogクラスではMyDog)を関数とし、propertiesを引数として作成します。propertiesのクラス設定と異なるデータ型や、バリデーションでNULL以外が返ってくるような値をpropertiesに指定すると、エラーが出ます。

オブジェクトの作成
MyDog("Pochi", 10, "sit")
<MyDog>
 @ name : chr "Pochi"
 @ age  : num 10
 @ trick: chr "sit"
MyDog("Pochi", 100, "sit") # バリデーションに適合しないのでエラー
Error:
! <MyDog> object is invalid:
- 年齢は0歳以上20歳未満として下さい

propertiesの呼び出し・変更

propertiesは@を用いて呼び出すことができます。また、propertiesに代入する形でpropertiesの値を変更することもできます。この際にもpropertiesはバリデーションの設定と合っているかどうかを評価されます。

propertiesの呼び出しと変更
Pochi <- MyDog("Pochi", 10, "sit")

Pochi@age
[1] 10
Pochi@age <- 15
Pochi
<MyDog>
 @ name : chr "Pochi"
 @ age  : num 15
 @ trick: chr "sit"

実際にバリデーションの設定に合っていない値をpropertiesに代入すると、エラーが出ます。

propertiesに対するバリデーション
Pochi@age <- 25
Error:
! <MyDog> object is invalid:
- 年齢は0歳以上20歳未満として下さい
Pochi@name <- c("Pochi", "Kuro")
Error:
! <MyDog> object is invalid:
- 2つ以上の名前を付けることはできません
Pochi@age <- "10"
Error:
! <MyDog>@age must be <integer> or <double>, not <character>

propertiesのデフォルト値の設定

propertiesにデフォルト値を設定する際には、new_property関数で設定します。new_property関数はproperties引数のリスト内で呼び出し、第一引数にデータ型を指定するS7クラス、default引数にデフォルト値を指定します。

new_property関数でデフォルト値を設定する
A := new_class(
  properties = 
    list(
      a = 
        # リスト内で呼び出してpropertiesの設定を追加する
        new_property(
          class_character,
          default = "default character"
        )
    )
)

new_property関数にはsettergettervalidatorという引数も設定可能です。いずれも関数を指定して用いる引数で、それぞれセッター、ゲッター、バリデータを個別のpropertiesに対して設定することができます。

関数の設定

S7クラスを引数に取る関数はmethod関数で作成します。ただし、このmethod関数をいきなり使おうとするとエラーが出ます。これはS4クラスのsetGeneric、setMethod関数と同じく、先にジェネリック関数の名前を設定しておかないと関数を設定できないためです。

関数名がジェネリックとして登録されていないとエラー
method(add_trick, MyDog) <- function(x, trick){
  x@trick <- c(x@trick, trick)
  x
}
Error:
! object 'add_trick' not found

ジェネリック関数の名前を設定するための関数はnew_generic関数です。new_generic関数は関数名とクラスを指定する引数を取り、関数名と同じ名前の変数に代入して用います。このように指定すると、add_trickはmethodの登録がない(with 0 methods)S7のジェネリック関数として設定されます。

ジェネリック関数名の登録:new_generic関数
# UseMethodと同じようなものが必要
add_trick <- new_generic("add_trick", "x")

add_trick
<S7_generic> add_trick(x, ...) with 0 methods:

また、new_generic関数の設定でも:=を用いることができます。S7では関数名と変数名が一致しなくてもジェネリック関数を設定できてしまいますので、できれば:=を用いてnew_generic関数を用いた方がよいでしょう。

:=演算子でnew_generic関数を用いる
add_trick := new_generic("x")

ジェネリック関数として登録出来たら、method関数で関数を作成することができます。method関数は引数に関数名とS7のクラス名を取り、method関数に関数(function)を代入して用います。この代入する関数の引数ではnew_generic関数で指定した引数(add_trickの場合はx)を引数に取る必要があります。

method関数でS7クラスを引数とする関数を作成する
method(add_trick, MyDog) <- function(x, trick){
  x@trick <- c(x@trick, trick)
  x
}

上記のmethod関数で作成したadd_trick関数を用いると、MyDogクラスのオブジェクトにtrickを追加できます。S7は値渡し(copy on modify)でオブジェクトを取り扱うため、R6のように元のオブジェクト(Pochi)が関数によって変更されることはありません。

作成した関数を用いる
add_trick(Pochi, "hand")
<MyDog>
 @ name : chr "Pochi"
 @ age  : num 15
 @ trick: chr [1:2] "sit" "hand"

関数を確認すると、add_trick関数にmethodが1つ追加されていることがわかります。add_trick関数の詳細を調べたい場合には、method(add_trick, MyDog)を実行します。

関数の内容を確認する
add_trick
<S7_generic> add_trick(x, ...) with 1 methods:
1: method(add_trick, MyDog)
method(add_trick, MyDog)
<S7_method> method(add_trick, MyDog)
function (x, trick) 
{
    x@trick <- c(x@trick, trick)
    x
}

new_generic関数では、第一引数の関数名と代入する変数名を一致させるよう、new_generic関数のヘルプには記載されています。しかし、別の変数名に代入してもなぜか成立します。

new_generic関数を間違った変数名に代入する
add_tric <- new_generic("add_trick", "x")
add_tric
<S7_generic> add_trick(x, ...) with 0 methods:
method(add_tric, MyDog) <- function(x, trick){
  x@trick <- c(x@trick, trick)
  x
}

add_tric(Pochi, "hand")
<MyDog>
 @ name : chr "Pochi"
 @ age  : num 15
 @ trick: chr [1:2] "sit" "hand"

別の変数名に入れてしまうと、S7_genericとしてはadd_trickという関数名で設定されていても、add_tricで関数を作れてしまいます。ややっこしいのでこういうことはしない方がよいです。

同様にクラスも別名の変数に代入できてしまいます。

クラス名と変数が異なる場合
B <- new_class("A")
B()
<A>

このような複雑な事態を避けるために、上で説明した:=演算子を用いるのがよいでしょう。

:=演算子でジェネリック関数を指定する
B := new_class()
B

add_tric := new_generic("x")
add_tric

2つ以上のpropertiesを変更する関数の場合

2つ以上のpropertiesを一つの関数で変更したい場合には、バリデーションの問題でpropertiesがうまく変更できなくなる場合があります。propertiesを変更する関数を作成する場合には、propertiesを設定するための関数であるprops関数を用いた方がよいでしょう。

props関数を用いてpropertiesを変更する
change_all_properties := new_generic("x")

method(change_all_properties, MyDog) <- function(x, name, age, trick){
  # propertiesを変更する際にはpropsを用いた方がよい場合もある
  props(x) <- 
    list(
      name = name,
      age = age,
      trick = trick
    )
  x
}

継承

S7でもS4と同様に親クラス(スーパークラス)の継承を行うことができます。継承する場合には、parent引数に親クラスを指定します。

継承
BDMyDog <- 
  new_class(
    "BDMyDog",
    parent = MyDog,
    properties = list(birthday = class_Date)
  )

継承したクラス(子クラス)では、親クラスのpropertiesとそのクラスのpropertiesをそれぞれ設定します。また、親クラスの関数を用いることもできます。

子クラス / 親クラスの関数の適用
Pochi <- BDMyDog("Pochi", 10, "sit", as.Date("2016/6/10"))
Pochi
<BDMyDog>
 @ name    : chr "Pochi"
 @ age     : num 10
 @ trick   : chr "sit"
 @ birthday: Date[1:1], format: "2016-06-10"
add_trick(Pochi, "lefthand")
<BDMyDog>
 @ name    : chr "Pochi"
 @ age     : num 10
 @ trick   : chr [1:2] "sit" "lefthand"
 @ birthday: Date[1:1], format: "2016-06-10"

ただし、S4とは違い、多重継承(multiple inheritance)はできなくなっています。一方で多重ディスパッチ(multiple dispatch)は利用できるため、関数には多重ディスパッチを用いた仕組みを取り入れることができます。

super

親クラスと子クラスで共に同名のジェネリック関数を設定していて、小クラスを取り扱うときに親クラスの関数を用いたい場合にはsuperを用います。S7のドキュメントに記載されている以下の例のように、Dateの平均を求める場合、date(mean(super(x, to = class_double)))のようにsuperを用いることで、Dateを数値として平均の計算を行い、その後Dateに戻すような処理を行うことができます。

superで親クラスの関数を呼び出す
mean := new_generic("x")

method(mean, date) <- function(x) {
  date(mean(super(x, to = class_double)))
}

Union

S4のClass Unionに当たるものがnew_union関数で作成できるUnionです。ほぼS4のClass Unionと同じような機能を持ち、以下の例では文字列か論理型を取ることができるS7クラスであるchar_logiを作成しています。

Unionの設定
char_logi <- new_union(class_character, class_logical)
char_logi
<S7_union>: <character> or <logical>
Back to top