Ruby3.2で追加されるDataクラスのお話

## はじめに
この記事は [SmartHR Advent Calendar 2022](https://qiita.com/advent-calendar/2022/smarthr) の6日目の記事です。

この記事で説明するDataクラスは、**Ruby2.xまで存在し、Ruby3.0でなくなったDataクラスではなく、Ruby3.2で新たに導入される予定のDataクラスです。**
~~Ruby3.2は（おそらく）今月のクリスマス頃にリリースされます。~~
Ruby 3.2 がリリースされました！ :tada: 
https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/

この記事では以下のバージョンのRubyを使用しています。
`ruby 3.2.0 (2022-12-25 revision a528908271) [arm64-darwin21]`

## 参考リンク
なるべく一次ソースに近い情報を見たい！という方は以下のリンクをご覧ください。

リリースノート日本語版
https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/
リファレンスマニュアル最新版
https://docs.ruby-lang.org/en/3.2/
Ruby自体の開発におけるDataクラスのFeatureチケット
https://bugs.ruby-lang.org/issues/16122
Github上のPR
https://github.com/ruby/ruby/pull/6353
Ruby3.2.0.preview3のDataクラスドキュメント
https://ruby-doc.org/3.2.0.preview3/Data.html

RubyKaigi2022内でDataクラスに言及された部分
Contribute to Ruby
https://youtu.be/m_LW5WIYJ9Q?t=2494
Ruby Committers vs The World
https://youtu.be/ajm3lr6Y9yE?t=3408

## Dataクラスについて
Ruby3.2で追加されるDataクラスは、わかりやすく言うと「Immutable（不変）なStructクラス」です。
Structクラスについては以前記事を書きましたので、もしよろしければ以下をご覧ください。
[めっちゃ便利なRubyのStructクラスのお話](https://qiita.com/ex_SOUL/items/696b3ac2869a2f71f3c0)

## 使い方

```ruby
# 4つの要素を持つ BreakwaterClub クラスを生成
BreakwaterClub = Data.define(:id, :name, :grade, :age)
#=> BreakwaterClub
```

Structクラスと異なり、 `Data.define` でクラスを定義できます。
参照リンクに記載したRubyKaigi2022内で `Struct.define` も定義して `Struct.new` と同じことをできるようにする予定、とのことでしたが `ruby 3.2.0` 時点では `Struct.define` は定義されていませんでした。

```ruby
# 生成した BreakwaterClub クラスのインスタンスを作成
bucho = BreakwaterClub.new(1, 'kuroiwa', 3, 18)
#=> #<data BreakwaterClub id=1, name="kuroiwa", grade=3, age=18>

# キーワード引数も使用可能
hina = BreakwaterClub.new(name: 'tsurugi', grade: 1, id: 2, age: 16)
#=> #<data BreakwaterClub id=2, name="tsurugi", grade=1, age=16>

# 作成したインスタンスは定義したメンバ変数のアクセサメソッドを持っている
p hina.name
#=> "tsurugi"
```
基本的にははStructクラスと同じように使えます。

```ruby
bucho = BreakwaterClub.new(1, 'kuroiwa')
#=>  `initialize': missing keyword: :grade, :age  (ArgumentError)

# 初期化時に定義したメンバ変数以外も指定できない。この点はStructクラスと同じ
hina = BreakwaterClub.new(name: 'tsurugi', grade: 1, id: 2, age: 16, hobby:  'handicraft')
#=> `initialize': unknown keyword: :hobby (ArgumentError)

# メンバ変数の再定義はできない
hina.age = 17
#=> undefined method `age=' for #<data BreakwaterClub id=2, name="tsurugi", grade=1, age=16> (NoMethodError)
```
Structクラスと異なり、クラス生成時に定義したメンバ変数定義は必須です。
また、初期化時に定義したメンバ変数は再定義できません。この点がStructクラスと大きく異なります。
StructクラスはHashのように振る舞うメソッド(`to_a`, `values`, `[]`)を持っていましたが、Dataクラスはそれを持っていません。

```ruby
hina.deconstruct
#=> [2, "tsurugi", 1, 16]

hina.deconstruct_keys(nil)
#=> {:id=>2, :name=>"tsurugi", :grade=>1, :age=>16}

hina.deconstruct_keys([:age])
#=> {:age=>16}
```
パターンマッチを扱えるよう、 `Data#deconstruct`, `Data#deconstruct_keys` も用意されています。

### `with`メソッドの追加
本記事を投稿した2022/12/06 からリリースまでに、`with` メソッドが追加されていたのでそれの紹介です。
ドキュメントは以下リンクからご覧ください。
https://docs.ruby-lang.org/en/master/Data.html#method-i-with
チケットは(多分)こちらです
https://bugs.ruby-lang.org/issues/19000

```ruby
# 4つの要素を持つ BreakwaterClub クラスを生成
BreakwaterClub = Data.define(:name, :grade, :age)
#=> BreakwaterClub

hina = BreakwaterClub.new(name: 'tsurugi', grade: 1, age: 16)
#=> #<data BreakwaterClub name="tsurugi", grade=1, age=16>

natsumi = hina.with(name: 'hodaka')
#=> #<data BreakwaterClub name="hodaka", grade=1, age=16>
```

`with` は作成したインスタンスをコピーするメソッドです。引数としてメンバ変数名を指定するとそのメンバ変数のみを変更することができます。
なぜ `with` という名前なのかは、上記に記載したチケットのコメントで議論されていますので気になる方はどうぞ。

## まとめ
基本的にはStructクラスと同じように、テスト時のモック用途や、シンプルな要素を持つ、クラスを作成するほどでもないオブジェクトの生成などで使用できると思います。

Structクラスとの感覚的な違いとしては、 `Foo = Struct.new(:foo)` の後に `Foo.new('foo')` としていたものを、 `Foo = Data.define(:foo)` の後に `Foo.new('foo')` とできるようになり、より直感的でわかりやすいものになったと感じます。

## おまけ
今回ご紹介したDataクラス以外にも、Ruby3.2には多くの機能が追加されています。
Ruby3.2がリリースされるであろう12/25まで毎日Ruby3.2の記事が追加される[Ruby 3.2 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/ruby32) というとてつもないアドベントカレンダーがありますので勝手ながら紹介しておきます。