# # Class Data provides a convenient way to define simple classes for value-alike # objects. # # The simplest example of usage: # # Measure = Data.define(:amount, :unit) # # # Positional arguments constructor is provided # distance = Measure.new(100, 'km') # #=> # # # # Keyword arguments constructor is provided # weight = Measure.new(amount: 50, unit: 'kg') # #=> # # # # Alternative form to construct an object: # speed = Measure[10, 'mPh'] # #=> # # # # Works with keyword arguments, too: # area = Measure[amount: 1.5, unit: 'm^2'] # #=> # # # # Argument accessors are provided: # distance.amount #=> 100 # distance.unit #=> "km" # # Constructed object also has a reasonable definitions of #== operator, #to_h # hash conversion, and #deconstruct/#deconstruct_keys to be used in pattern # matching. # # ::define method accepts an optional block and evaluates it in the context of # the newly defined class. That allows to define additional methods: # # Measure = Data.define(:amount, :unit) do # def <=>(other) # return unless other.is_a?(self.class) && other.unit == unit # amount <=> other.amount # end # # include Comparable # end # # Measure[3, 'm'] < Measure[5, 'm'] #=> true # Measure[3, 'm'] < Measure[5, 'kg'] # # comparison of Measure with Measure failed (ArgumentError) # # Data provides no member writers, or enumerators: it is meant to be a storage # for immutable atomic values. But note that if some of data members is of a # mutable class, Data does no additional immutability enforcement: # # Event = Data.define(:time, :weekdays) # event = Event.new('18:00', %w[Tue Wed Fri]) # #=> # # # # There is no #time= or #weekdays= accessors, but changes are # # still possible: # event.weekdays << 'Sat' # event # #=> # # # See also Struct, which is a similar concept, but has more container-alike API, # allowing to change contents of the object and enumerate it. # class Data # # Defines a new Data class. If the first argument is a string, the class is # stored in `Data::` constant. # # measure = Data.define(:amount, :unit) # #=> # # measure.new(1, 'km') # #=> # # # # It you store the new class in the constant, it will # # affect #inspect and will be more natural to use: # Measure = Data.define(:amount, :unit) # #=> Measure # Measure.new(1, 'km') # #=> # # # Note that member-less Data is acceptable and might be a useful technique for # defining several homogenous data classes, like # # class HTTPFetcher # Response = Data.define(:body) # NotFound = Data.define # # ... implementation # end # # Now, different kinds of responses from `HTTPFetcher` would have consistent # representation: # # # # # # # And are convenient to use in pattern matching: # # case fetcher.get(url) # in HTTPFetcher::Response(body) # # process body variable # in HTTPFetcher::NotFound # # handle not found case # end # def self.define: [KLASS < _DataClass] (*Symbol) ?{ (KLASS) [self: KLASS] -> void } -> KLASS interface _DataClass # # Constructors for classes defined with ::define accept both positional and # keyword arguments. # # Measure = Data.define(:amount, :unit) # # Measure.new(1, 'km') # #=> # # Measure.new(amount: 1, unit: 'km') # #=> # # # # Alternative shorter intialization with [] # Measure[1, 'km'] # #=> # # Measure[amount: 1, unit: 'km'] # #=> # # # All arguments are mandatory (unlike Struct), and converted to keyword # arguments: # # Measure.new(amount: 1) # # in `initialize': missing keyword: :unit (ArgumentError) # # Measure.new(1) # # in `initialize': missing keyword: :unit (ArgumentError) # # Note that `Measure#initialize` always receives keyword arguments, and that # mandatory arguments are checked in `initialize`, not in `new`. This can be # important for redefining initialize in order to convert arguments or provide # defaults: # # Measure = Data.define(:amount, :unit) do # NONE = Data.define # # def initialize(amount:, unit: NONE.new) # super(amount: Float(amount), unit:) # end # end # # Measure.new('10', 'km') # => # # Measure.new(10_000) # => #> # %a{annotate:rdoc:copy:Data.new} def new: (*untyped) -> Data | (**untyped) -> Data %a{annotate:rdoc:copy:Data.[]} def []: (*untyped) -> Data | (**untyped) -> Data # # Returns an array of member names of the data class: # # Measure = Data.define(:amount, :unit) # Measure.members # => [:amount, :unit] # %a{annotate:rdoc:copy:Data.members} def members: () -> Array[Symbol] end %a{annotate:rdoc:skip} def self.new: () -> bot def self.allocate: () -> bot public # # Returns `true` if `other` is the same class as `self`, and all members are # equal. # # Examples: # # Measure = Data.define(:amount, :unit) # # Measure[1, 'km'] == Measure[1, 'km'] #=> true # Measure[1, 'km'] == Measure[2, 'km'] #=> false # Measure[1, 'km'] == Measure[1, 'm'] #=> false # # Measurement = Data.define(:amount, :unit) # # Even though Measurement and Measure have the same "shape" # # their instances are never equal # Measure[1, 'km'] == Measurement[1, 'km'] #=> false # def ==: (untyped) -> bool # # Returns the values in `self` as an array, to use in pattern matching: # # Measure = Data.define(:amount, :unit) # # distance = Measure[10, 'km'] # distance.deconstruct #=> [10, "km"] # # # usage # case distance # in n, 'km' # calls #deconstruct underneath # puts "It is #{n} kilometers away" # else # puts "Don't know how to handle it" # end # # prints "It is 10 kilometers away" # # Or, with checking the class, too: # # case distance # in Measure(n, 'km') # puts "It is #{n} kilometers away" # # ... # end # def deconstruct: () -> Array[untyped] # # Returns a hash of the name/value pairs, to use in pattern matching. # # Measure = Data.define(:amount, :unit) # # distance = Measure[10, 'km'] # distance.deconstruct_keys(nil) #=> {:amount=>10, :unit=>"km"} # distance.deconstruct_keys([:amount]) #=> {:amount=>10} # # # usage # case distance # in amount:, unit: 'km' # calls #deconstruct_keys underneath # puts "It is #{amount} kilometers away" # else # puts "Don't know how to handle it" # end # # prints "It is 10 kilometers away" # # Or, with checking the class, too: # # case distance # in Measure(amount:, unit: 'km') # puts "It is #{amount} kilometers away" # # ... # end # def deconstruct_keys: (?Array[Symbol]?) -> Hash[Symbol, untyped] # # Equality check that is used when two items of data are keys of a Hash. # # The subtle difference with #== is that members are also compared with their # #eql? method, which might be important in some cases: # # Measure = Data.define(:amount, :unit) # # Measure[1, 'km'] == Measure[1.0, 'km'] #=> true, they are equal as values # # ...but... # Measure[1, 'km'].eql? Measure[1.0, 'km'] #=> false, they represent different hash keys # # See also Object#eql? for further explanations of the method usage. # def eql?: (untyped) -> bool # # Redefines Object#hash (used to distinguish objects as Hash keys) so that data # objects of the same class with same content would have the same `hash` value, # and represented the same Hash key. # # Measure = Data.define(:amount, :unit) # # Measure[1, 'km'].hash == Measure[1, 'km'].hash #=> true # Measure[1, 'km'].hash == Measure[10, 'km'].hash #=> false # Measure[1, 'km'].hash == Measure[1, 'm'].hash #=> false # Measure[1, 'km'].hash == Measure[1.0, 'km'].hash #=> false # # # Structurally similar data class, but shouldn't be considered # # the same hash key # Measurement = Data.define(:amount, :unit) # # Measure[1, 'km'].hash == Measurement[1, 'km'].hash #=> false # def hash: () -> Integer # # Returns a string representation of `self`: # # Measure = Data.define(:amount, :unit) # # distance = Measure[10, 'km'] # # p distance # uses #inspect underneath # # # # puts distance # uses #to_s underneath, same representation # # # def inspect: () -> String # # Returns the member names from `self` as an array: # # Measure = Data.define(:amount, :unit) # distance = Measure[10, 'km'] # # distance.members #=> [:amount, :unit] # def members: () -> Array[Symbol] # # Returns Hash representation of the data object. # # Measure = Data.define(:amount, :unit) # distance = Measure[10, 'km'] # # distance.to_h # #=> {:amount=>10, :unit=>"km"} # # Like Enumerable#to_h, if the block is provided, it is expected to produce # key-value pairs to construct a hash: # # distance.to_h { |name, val| [name.to_s, val.to_s] } # #=> {"amount"=>"10", "unit"=>"km"} # # Note that there is a useful symmetry between #to_h and #initialize: # # distance2 = Measure.new(**distance.to_h) # #=> # # distance2 == distance # #=> true # def to_h: () -> Hash[Symbol, untyped] # # Returns a string representation of `self`: # # Measure = Data.define(:amount, :unit) # # distance = Measure[10, 'km'] # # p distance # uses #inspect underneath # # # # puts distance # uses #to_s underneath, same representation # # # alias to_s inspect # # Returns a shallow copy of `self` --- the instance variables of `self` are # copied, but not the objects they reference. # # If the method is supplied any keyword arguments, the copy will be created with # the respective field values updated to use the supplied keyword argument # values. Note that it is an error to supply a keyword that the Data class does # not have as a member. # # Point = Data.define(:x, :y) # # origin = Point.new(x: 0, y: 0) # # up = origin.with(x: 1) # right = origin.with(y: 1) # up_and_right = up.with(y: 1) # # p origin # # # p up # # # p right # # # p up_and_right # # # # out = origin.with(z: 1) # ArgumentError: unknown keyword: :z # some_point = origin.with(1, 2) # ArgumentError: expected keyword arguments, got positional arguments # def with: (**untyped) -> instance end