【Ruby】Struct の子クラスにキーワード引数をとってインスタンスを返す特異メソッド create を生やす


何をするのか

タイトルのとおりです。

Ruby3.1 では、Structの子クラス.new(…) の引数が全てキーワード引数である場合、警告が出るようになるそうです。(ここ
それは、Ruby3.2 からは、keyword_init: true していなくても、位置引数に加えてキーワード引数が使えるようになるため、あらかじめ警告しておこうという趣旨のようです。[Feature #16806]

とても有り難いことです。

その絡みで、過去にタイトルと同じような提案([Feature #11925])があったものの、却下されていたことを知りました。

でも、この提案の内容も良さげな感じがします。実装も簡単そうなので、自力で生やしてみようと思いました。

コード

class Struct
  class << self
    alias _new new
    def new(*args, keyword_init: false, &block)
      sub = _new(*args, keyword_init: keyword_init, &block)
      if keyword_init
        sub.instance_eval { alias create new }
      else
        sub.instance_eval(<<~EOC, __FILE__, __LINE__ + 1)
          def create(#{args.map { |arg| "#{arg}: nil" }.join(", ")})
            new(#{args.map(&:to_s).join(", ")})
          end
        EOC
      end
      sub
    end
  end
end

簡単な説明

Original の Struct.new の中身を、エイリアス _new に退避させて、new を自作し、自作の Struct.new(…) で子クラスを作る際に、instance_eval を使って特異メソッド create を作っています。
例えば、

Person = Struct.new(:name, :age)

したとき、併せて

class << Person
  def create(name: nil, age: nil)
    new(name, age)
  end
end

したのと同じ効果があります。

なお、keyword_init: true のときは new 自身がキーワード引数をとるので、createnew のエイリアスにしてあります。

余談

Ruby3.1 からは、

sub = _new(*args, keyword_init: keyword_init, &block)

の部分は、

sub = _new(*args, keyword_init:, &block)

と書けるようです。
大変有り難い。