123456789_123456789_123456789_123456789_123456789_

Using Data and Struct

Data and Struct are commonly used utilities to define simple value objects. The objects have attributes, and the equality between the two objects are defined by equality of the attributes. (Note that we can define additional methods and overwrite the equality definitions when we want.)

# Defines {Measure} class with `#amount` and `#unit` attributes
Measure = Data.define(:amount, :unit)

Unfortunately, supporting Data and Struct in RBS is not straightforward. You have to write down the attribute definitions and initializers in RBS.

class Measure
  # `attr_accessor amount: Integer` in the case of Struct
  attr_reader amount: Integer

  # `attr_accessor unit: String` in the case of Struct
  attr_reader unit: String

  def initialize: (Integer amount, String unit) -> void
                | (amount: Integer, unit: String) -> void             
end

This is simplified definition of the Measure class, for the case you only use the attributes and initializers. You can add more method definitions or inherit from Data class to make the definition more complete.

However, it's common that you don't need all of the Data and Struct methods, like .members and .[]. When you are using those utility classes just for the attributes methods, you can simply ignore other methods or skip specifying a super class.

You may want to implement a generator that understands Data.define and Struct.new. But even with the generator, you need to edit the generated RBS files so that the attribute definitions have correct types.

Type checking class definitions using Data and Struct

If you use Steep, you may need additional annotation in Ruby implementation.

# Type error because return type of {Data.define(...)} is not `singleton(Measure)`
Measure = Data.define(:amount, :unit)

You can please the type checker by adding a cast (_) or define the class inheriting from Data.define(...).

# Skip type checking by assigning to `_`
Measure = _ = Data.define(:amount, :unit)

# Super class is not type checked by Steep
class Measure < Data.define(:amount, :unit)
end

@soutaro has prefered inheriting from Data.define, but you may find an extra annonymous class in .ancestors [^1].

Measure.ancestors #=> [Measure, #<Class:0xOOF>, Data, ...]

[^1]: Shannon Skipper told me it in Discord

Generate prototype for Data and Struct

RBS prototypes for classes using Data and Struct can be generated by rbs prototype runtime.

# t.rb
class Measure < Data.define(:amount, :unit)
end
$ bundle exec rbs prototype runtime -R t.rb Measure
class Measure < ::Data
  def self.new: (untyped amount, untyped unit) -> instance
              | (amount: untyped, unit: untyped) -> instance

  def self.[]: (untyped amount, untyped unit) -> instance
             | (amount: untyped, unit: untyped) -> instance

  def self.members: () -> [ :amount, :unit ]

  def members: () -> [ :amount, :unit ]

  attr_reader amount: untyped

  attr_reader unit: untyped
end