DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

【Ruby 3.2 新機能】るりまの Data クラスリファレンス作成 PR をレビューした

こんにちは。にくといいます。github だと niku です。2022 年 12 月に DIGGLE に入社しました。

さて Ruby 3.2 からは Data というクラスが使えるようになりました。 Ruby の日本語リファレンスるりま*1(以下「るりま」)にも class Data (Ruby 3.2 リファレンスマニュアル) というページができていますね。すごい!

「るりま」は GitHub の Pull Request を用いてメンテナンスされています。今回 Data のリファレンスも

github.com

で作られた PR が元になっています。 この PR のレビュアーとして私も参加したので、その時の様子も交えて「るりま」のドキュメントのメンテナンスの雰囲気をお伝えしたいと思います。

Data クラス

この記事を楽しむために、まずは Data クラスというのがどういうものなのかを大まかに説明します。 Data クラスは、Strcut クラスに似たインターフェースを持っています。

~ ()
irb
irb(main):001:0> SMeasure = Struct.new(:amount, :unit)
=> SMeasure
irb(main):002:0> DMeasure = Data.define(:amount, :unit)
=> DMeasure
irb(main):003:0> s_one_km = SMeasure.new(1, :km)
=> #<struct SMeasure amount=1, unit=:km>
irb(main):004:0> d_one_km = DMeasure.new(1, :km)
=> #<data DMeasure amount=1, unit=:km>

一方で Struct クラスは値を書き換えることができるのに対し、Data クラスは値を書き換えることができません。 Struct クラスは構造体クラスと呼ばれる概念を表すものに対し、Data クラスは値オブジェクトと呼ばれる概念を表すもので、値オブジェクトというものの性質の一つに「変わらない(不変)」があるためです。

irb(main):005:0> s_one_km.amount = 2
=> 2
irb(main):006:0> s_one_km
=> #<struct SMeasure amount=2, unit=:km>
irb(main):007:0> d_one_km.amount = 2
(irb):7:in `<main>': undefined method `amount=' for #<data DMeasure amount=1, unit=:km> (NoMethodError)
  from /Users/niku/.asdf/installs/ruby/3.2.1/lib/ruby/gems/3.2.0/gems/irb-1.6.2/exe/irb:11:in `<top (required)>'
    from /Users/niku/.asdf/installs/ruby/3.2.1/bin/irb:25:in `load'
  from /Users/niku/.asdf/installs/ruby/3.2.1/bin/irb:25:in `<main>'

ちょっと横道にそれますが「プログラムでできないことが増える」ことを導入するのに、それが嬉しい理由というのはあるんでしょうかね?私は以下の記事で覚えたことを大事にしていて「できないことが増える」のは便利なことだと感じています。

上が力が強く、下へ行くほど力が弱くなります。力が強いと何でもできてしまうので、コードの意図が不明瞭となり、また間違いが入り込みやすくなります。力が弱いとできることは限られるのでコードの意図は明確となり、間違いが入りにくくなります。 「目的に合う一番力の弱い手段を使う」のがよいプログラムを書くための大原則です。たとえば、草を刈るのにチェーンソーは使うべきではありません。もちろん草も切れますが、大切な植木も傷つけてしまうかもしれませんから。

Haskellの文法(再帰編) - あどけない話

「るりま」 Data クラス PR との出会い

私は Ruby 3.0 から実験的に導入された並列処理機能 Ractor https://techlife.cookpad.com/entry/2020/12/26/131858 に興味を持っていて、Ractor 間のデータのやりとりには値の変わらないオブジェクトがあると便利であることを、Erlang や Elixir といった他のプログラミング言語を通じて知っていました。Data 導入の issue https://bugs.ruby-lang.org/issues/16122 か PR https://github.com/ruby/ruby/pull/6353 をみてから、Ruby に導入できるのを心待ちにしていました。

Ruby の英語リファレンス である ruby-doc には Ruby 3.2 リリース (2022/12/25) 時点から Data クラスのリファレンス https://ruby-doc.org/3.2.0/Data.html がありました。一方で、日本語のリファレンスはその時点では存在していませんでした。kakutani さんが Ruby3.2.0 の新機能のほうの Data のリファレンスマニュアルがあってほしいという issue を rurema に立てた https://github.com/rurema/doctree/issues/2764 のをみて、私は「るりま」でリファレンスを書いたことがなかったので、そのうちやってみようと思っていました。

2023/1/7 に時間がとれたので、Data クラスのリファレンスを作ろうとしました。念のため「るりま」の PR 一覧を眺めてみると、なんと前日の 2023/1/6 に Data クラスを「るりま」に載せるための PR が kyanagi さんによって作られていました。そこで PR を書くのではなく、PR のレビューで貢献することとしました。

そのときの私の喜びのコメントです

欲しいなあと思っていたので PR があって嬉しく思いました✨ぜひマージされてほしい。 通りすがりですが、助けになればと思い一通りの変更をみました。コメントを残させてください。

https://github.com/rurema/doctree/pull/2777#pullrequestreview-1239634811

PR レビューの様子

結局、他の方のレビューも含めて、PR が作られたのは 2023/1/7 マージされたのは 2023/2/9 と Data クラスの記事作成には約一ヶ月かかったようです。私がレビューしたのは 2 回くらい、軽微なものを含めると数回といったところで、PR の出だしから完成度が高かったので、やりとりの回数はそれほど多くなく完成となりました。

「構造体」という言葉は Struct とは強く結びついていますが、Data とはそうでもなさそうに感じます。 別の言い回しだとさらに読者が理解しやすかったりしないでしょうか。

https://github.com/rurema/doctree/pull/2777#discussion_r1063972590

API は Struct に似ているので、Struct のリファレンスを元にしてくださったのかなと想像しています。Struct には構造体という言葉が似合うのですが、私は Data のリファレンスには似合わないと思ったので、コメントで問い掛けていました。

この後に記載されているサンプルは全てエラーになっているようです。また私の手元にある Ruby3.2 で試しても同じ結果になりました

https://github.com/rurema/doctree/pull/2777#discussion_r1063974100

例示されているサンプルを実行してみても、エラーになるので、どういうことかよくわからず相談したコメントです。結果としてこのふるまいは正しく、狙いのある API なのでした。どういうことなのかは完成系の https://docs.ruby-lang.org/ja/3.2/class/Data.html#S_--5B--5D 「new に渡す引数の数がメンバの数より多い場合は new でエラーになります。new に渡す引数の数がメンバの数より少ない場合は new ではエラーにならず、そのまま initialize に渡されます。ユーザが initialize のオーバーライドを通して、少ない引数のときの適切な振舞いを実装可能とするためです。」のあたりをご覧ください。例も含めて kyanagi さんがたいへん伝わりやすくしてくれました。

https://docs.ruby-lang.org/en/3.2/Data.html#method-c-define だと、るりまの定義とは異なり(略)こちらは単に記法の違いによるものになるでしょうか?

https://docs.ruby-lang.org/en/3.2/Data.html#method-c-define にある、 引数を取らずに Data.define() と定義して、パターンマッチングに使う便利なやり方はるりまにも記載があると嬉しそうです。

https://github.com/rurema/doctree/pull/2777#discussion_r1063975629

先行して実装されていた ruby-doc の Data と照らしあわせながらコメントしていました。この質問の回答で知ったのですが kyanagi さんは私が参照していた ruby-doc のリファレンス訂正 PR https://github.com/ruby/ruby/pull/7038 も作られていました。素敵です。

細かいですが、Object#hash の定義をみると、hash が必ず満たさなければいけないのは「メンバの値を eql? で比較して同じなら、hash の値も同じでなければならない」ということで、「メンバの値を eql? で比較して異なるときに hash の値が異ならなければならない」ではなさそうに思います。

https://github.com/rurema/doctree/pull/2777#discussion_r1063978482

Object#hashObject#eql? は異なるメソッドですけれどいい感じに協調するように実装しないと望ましい動作が得られない性質を持っているので、リファレンスの表現も(書くなら)気をつかいますよね。これは Ruby 特有ではなくて Java などでもそういうものですよね。結局この表現は省くことになりました。実装詳細に踏み込まないのは穏当な判断かなと感じました。 参考までに Java の場合のドキュメントも貼っておきます

常に、このオブジェクトに対するequalsの比較で使用される情報が変更されていなければ、hashCodeメソッドは常に同じ整数を返す必要があります

https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/lang/Object.html#hashCode()

「るりま」 PR レビューの感想

現状「るりま」に深くかかわっているわけではない第三者の私がふらっときてレビューさせてもらっても特に困ることはありませんでした。「るりま」をメンテされているみなさん、kyanagi さん、親切にしてくださってありがとうございます。

私はレビューするときに強く確信を持っている場合をのぞくと、だいたい「私には状況がaのように見えていて、その場合私ならxと書くところyと書かれていて違いを感じました。ここの意図を詳しく教えていただけますか?」といった問い掛け形式でレビューしていることを再発見しました。PR の書き手が見ている状況と読み手が認知している状況が異なることはよくあるので、認知している状況を自ら開示するのは、すれ違いによってやりとりがややこしくなるのを防ぐために重要ですね。

DIGGLE は RubyKaigi2023 スポンサーになりました

DIGGLE は今年開催される RubyKaigi2023 の Platinum スポンサー になりました。よろしくおねがいします! RubyKaigi 現地でブースも出展し、私も居ますので、遊びにいらしてください。 この記事のことや、Ractor や、よいレビュー、その他について雑談しましょう〜

*1:たぶん びー ふぁれんす にゅあるの略