Class: R::Object

Inherits:
BasicObject
Includes:
BinaryOperators, ExecBinOp, ExecUniOp, IndexedObject, UnaryOperators
Defined in:
lib/R_interface/robject.rb

Constant Summary collapse

EXPLICIT_RUBY_SURFACE =

Methods we explicitly define (Phase A / BasicObject). respond_to?(sym) is true for these and for any symbol we forward to R.

[
  :r_interop, :expression, :expression=,
  :[], :[]=, :>>, :unboxed_get, :to_ruby, :to_i, :to_ary, :length, :size,
  :class, :to_s, :rclass, :typeof, :inspect, :object_id, :__id__,
  :==, :equal?, :call, :nil?, :pretty_print, :send,
  :instance_variable_set, :instance_variable_get,
  :is_a?, :kind_of?, :instance_of?, :respond_to?,
  :method_missing
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ExecUniOp

#exec_uni_oper

Methods included from UnaryOperators

#!, #+@, #-@

Methods included from ExecBinOp

#coerce, #exec_bin_oper

Methods included from BinaryOperators

#!=, #%, #*, #**, #+, #-, #/, #<, #<=, #>, #>=, #_, #eq, #int_div, #til

Methods included from IndexedObject

#[], #[]=, #size

Constructor Details

#initialize(r_interop, expression = nil) ⇒ Object

Returns a new instance of Object.



62
63
64
65
# File 'lib/R_interface/robject.rb', line 62

def initialize(r_interop, expression = nil)
  @r_interop = r_interop
  @expression = expression
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(symbol, *args) ⇒ Object

Forward unknown methods to R: process_missing(symbol, self, *args) builds and runs the R call.



405
406
407
# File 'lib/R_interface/robject.rb', line 405

def method_missing(symbol, *args)
  ::R::Support.process_missing(symbol, self, *args)
end

Instance Attribute Details

#expressionObject

Returns the value of attribute expression.



34
35
36
# File 'lib/R_interface/robject.rb', line 34

def expression
  @expression
end

#r_interopObject (readonly)

Returns the value of attribute r_interop.



33
34
35
# File 'lib/R_interface/robject.rb', line 33

def r_interop
  @r_interop
end

Class Method Details

.build(r_interop, expression = nil, r_class: nil, wrapper_tag: nil) ⇒ Object

Factory: build the right Ruby wrapper for an R value. Returns a subclass (DataFrame, Vector, etc.), or the unwrapped Ruby value for rb_obj_* handles and for R NULL. Pass r_class to avoid an R class() call. Pass wrapper_tag from the new-bridge envelope to classify without string-matching r_class and to allow probe-free builds when r_class is absent but tag is present (non-+other+).



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/R_interface/robject.rb', line 85

def self.build(r_interop, expression = nil, r_class: nil, wrapper_tag: nil)
  # Ruby object handles stored in R: unwrap to the original Ruby object.
  if r_interop.is_a?(::String) && r_interop.start_with?("rb_obj_")
    return ::R::Support.get_ruby_object(r_interop)
  end

  # If we got a basic Ruby type, it's already unboxed
  return r_interop if r_interop.is_a?(::Numeric) || r_interop.is_a?(::TrueClass) ||
                     r_interop.is_a?(::FalseClass) || r_interop.nil? || r_interop.is_a?(::Symbol)

  if r_interop.is_a?(::String) && r_interop.start_with?("g2_v")
    return new(r_interop, expression) unless ::R.bridge.ready?

    begin
      rc_in = r_class
      rc = r_class.to_s.strip
      rc = nil if rc.nil? || rc.empty?
      tag = wrapper_tag.to_s.strip
      tag = nil if tag.nil? || tag.empty?

      # Probe only when we have no class string and no definitive wrapper tag.
      if rc.nil? && (!tag || tag == "other")
        rc = class_probe_fetch_r_class(r_interop)
      elsif rc.nil? && tag && tag != "other"
        # Envelope-style path: tag is enough; avoid class() round-trip.
      end

      return nil if rc && rc.strip == "NULL"

      if ::ENV['GALAAZ_DEBUG']
        ::Kernel.puts "DEBUG: Object.build r_interop=#{r_interop.inspect} r_class_arg=#{rc_in.inspect} r_class=#{rc.inspect} wrapper_tag=#{tag.inspect}"
      end

      if tag && tag != "other"
        obj = build_from_wrapper_tag(r_interop, expression, tag, r_class_hint: rc)
        if obj
          ::Kernel.puts "DEBUG: Object.build returning #{obj.class}" if ::ENV['GALAAZ_DEBUG']
          return obj
        end
      end

      if rc.nil?
        rc = class_probe_fetch_r_class(r_interop)
        return nil if rc.strip == "NULL"
      end

      obj = build_from_r_class_string(r_interop, expression, rc)
      ::Kernel.puts "DEBUG: Object.build returning #{obj.class}" if ::ENV['GALAAZ_DEBUG']
      obj
    rescue => e
      ::Kernel.puts "DEBUG: Object.build rescue: #{e.message}" if ::ENV['GALAAZ_DEBUG']
      new(r_interop, expression)
    end
  else
    # Fallback for already unboxed or unexpected strings
    r_interop
  end
end

.build_class_probe_countObject

Number of R class() probes issued from Object.build (missing r_class and no usable wrapper_tag). Reset with reset_build_counters! before measuring. See docs/performance_plan.md Phase 2.



69
70
71
# File 'lib/R_interface/robject.rb', line 69

def self.build_class_probe_count
  @build_class_probe_count ||= 0
end

.build_from_r_class_string(r_interop, expression, r_class) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/R_interface/robject.rb', line 179

def self.build_from_r_class_string(r_interop, expression, r_class)
  case
  when r_class.include?("data.frame") || r_class.include?("tbl_df")
    ::R::DataFrame.new(r_interop, expression)
  when r_class.include?("matrix") || r_class.include?("array")
    ::R::Matrix.new(r_interop, expression)
  when r_class.include?("numeric") || r_class.include?("integer") ||
       r_class.include?("logical") || r_class.include?("character")
    v = ::R::Vector.new(r_interop)
    v.expression = expression if expression
    v
  when r_class.include?("list")
    ::R::List.new(r_interop, expression)
  when r_class.include?("environment")
    ::R::Environment.new(r_interop, expression)
  when r_class.include?("function")
    ::R::Closure.new(r_interop, expression)
  when r_class.include?("language") || r_class == "call"
    ::R::Language.new(r_interop, expression)
  when r_class == "name" || r_class == "symbol"
    ::R::RSymbol.new(r_interop, expression)
  when r_class == "expression"
    ::R::RExpression.new(r_interop, expression)
  else
    ::Kernel.puts "DEBUG: Object.build fell through to else (r_class=#{r_class.inspect})" if ::ENV['GALAAZ_DEBUG']
    new(r_interop, expression)
  end
end

.build_from_wrapper_tag(r_interop, expression, tag, r_class_hint: nil) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/R_interface/robject.rb', line 149

def self.build_from_wrapper_tag(r_interop, expression, tag, r_class_hint: nil)
  # typeof(formula) is language in R, but class is "formula"; keep plain R::Object (legacy semantics).
  if tag == "language" && r_class_hint && r_class_hint.include?("formula")
    return nil
  end

  case tag
  when "data_frame"
    ::R::DataFrame.new(r_interop, expression)
  when "matrix"
    ::R::Matrix.new(r_interop, expression)
  when "vector"
    v = ::R::Vector.new(r_interop)
    v.expression = expression if expression
    v
  when "list"
    ::R::List.new(r_interop, expression)
  when "environment"
    ::R::Environment.new(r_interop, expression)
  when "closure"
    ::R::Closure.new(r_interop, expression)
  when "language"
    ::R::Language.new(r_interop, expression)
  when "symbol"
    ::R::RSymbol.new(r_interop, expression)
  else
    nil
  end
end

.class_probe_fetch_r_class(r_interop) ⇒ Object



144
145
146
147
# File 'lib/R_interface/robject.rb', line 144

def self.class_probe_fetch_r_class(r_interop)
  increment_build_class_probe_count!
  ::R.bridge.eval_r("paste(class(#{r_interop}), collapse=' ')").gsub(/^\[1\] /, "").gsub(/"/, "").strip
end

.increment_build_class_probe_count!Object



77
78
79
# File 'lib/R_interface/robject.rb', line 77

def self.increment_build_class_probe_count!
  @build_class_probe_count = build_class_probe_count + 1
end

.reset_build_counters!Object



73
74
75
# File 'lib/R_interface/robject.rb', line 73

def self.reset_build_counters!
  @build_class_probe_count = 0
end

Instance Method Details

#==(other) ⇒ Object

Equality: R::Object vs R::Object uses all.equal; vs Ruby scalar uses unboxed value(s).



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/R_interface/robject.rb', line 379

def ==(other)
  return true if self.equal?(other)
  if other.is_a?(::R::Object)
    res = ::R.bridge.eval_r("isTRUE(all.equal(#{@r_interop}, #{other.r_interop}))")
    return res == "[1] TRUE"
  elsif other.is_a?(::Numeric) || other.is_a?(::String) || other.is_a?(::TrueClass) ||
        other.is_a?(::FalseClass) || other.nil? || other.is_a?(::Symbol)
    # DataFrame/Matrix etc. are not equal to a scalar even if they contain that value (e.g. row['mpg'] != 21.0).
    return false if self.class == ::R::DataFrame || self.class == ::R::Matrix
    val = (self >> nil)
    if val.is_a?(::Array)
      return val[0] == other if val.size == 1
      return val.include?(other) if other.is_a?(::String)
    end
    return val == other
  else
    super
  end
end

#call(*args) ⇒ Object

Call this object as an R function with the given args (e.g. obj.call(1, 2)).



400
401
402
# File 'lib/R_interface/robject.rb', line 400

def call(*args)
  ::R::Support.exec_function(self, *args)
end

#classObject

Phase B: BasicObject has no class; we must define it.



215
216
217
# File 'lib/R_interface/robject.rb', line 215

def class
  ::R::Object
end

#inspectObject

Debug string: class, object id, and @r_interop handle.



240
241
242
# File 'lib/R_interface/robject.rb', line 240

def inspect
  "#<#{self.class}:#{__id__.to_s(16)} @r_interop=#{@r_interop.inspect}>"
end

#instance_of?(mod) ⇒ Boolean

Exact class check (e.g. obj.instance_of?(R::Object)).

Returns:

  • (Boolean)


234
235
236
237
# File 'lib/R_interface/robject.rb', line 234

def instance_of?(mod)
  return false unless mod.is_a?(::Module)
  self.class == mod
end

#instance_variable_get(name) ⇒ Object

Delegate to Kernel#instance_variable_get so that instance vars work despite BasicObject.



42
43
44
# File 'lib/R_interface/robject.rb', line 42

def instance_variable_get(name)
  ::Object.instance_method(:instance_variable_get).bind(self).call(name)
end

#instance_variable_set(name, value) ⇒ Object

Delegate to Kernel#instance_variable_set so that instance vars work despite BasicObject.



37
38
39
# File 'lib/R_interface/robject.rb', line 37

def instance_variable_set(name, value)
  ::Object.instance_method(:instance_variable_set).bind(self).call(name, value)
end

#is_a?(mod) ⇒ Boolean Also known as: kind_of?

Type check against Ruby modules/classes (e.g. obj.is_a?(R::DataFrame)).

Returns:

  • (Boolean)


227
228
229
230
# File 'lib/R_interface/robject.rb', line 227

def is_a?(mod)
  return false unless mod.is_a?(::Module)
  !!(self.class <= mod)
end

#nil?Boolean

R objects are never nil; R NULL is converted to Ruby nil in Object.build.

Returns:

  • (Boolean)


53
54
55
# File 'lib/R_interface/robject.rb', line 53

def nil?
  false
end

#pretty_print(pp) ⇒ Object

Used by pp/inspect-style pretty printing.



58
59
60
# File 'lib/R_interface/robject.rb', line 58

def pretty_print(pp)
  pp.text(inspect)
end

#rclassObject

R's class() of this object; unwrapped to a single string when length 1.



247
248
249
250
251
# File 'lib/R_interface/robject.rb', line 247

def rclass
  res = ::R::Support.exec_function("class", self)
  val = res.respond_to?(:>>) ? (res >> nil) : res
  val.is_a?(::Array) ? val[0] : val
end

#respond_to?(sym, include_private = false) ⇒ Boolean

true for EXPLICIT_RUBY_SURFACE and for any other symbol (forwarded to R via method_missing).

Returns:

  • (Boolean)


220
221
222
223
224
# File 'lib/R_interface/robject.rb', line 220

def respond_to?(sym, include_private = false)
  sym = sym.to_sym
  return true if EXPLICIT_RUBY_SURFACE.include?(sym)
  true
end

#send(method_name, *args, &block) ⇒ Object

BasicObject lacks Kernel#send; provide Ruby dispatch explicitly so calls like obj.send(:mpg) do not get forwarded to R as send(obj, mpg).



48
49
50
# File 'lib/R_interface/robject.rb', line 48

def send(method_name, *args, &block)
  ::Object.instance_method(:send).bind(self).call(method_name, *args, &block)
end

#to_aryObject

Return nil so RSpec/eq and array conversion don't forward to_ary to R (plain Object is not array-like).



374
375
376
# File 'lib/R_interface/robject.rb', line 374

def to_ary
  nil
end

#to_iObject

Unbox length-1 numeric (or string digits) to Integer without forwarding to R (R has no to_i). Callback procs often use x.to_i when R passes scalars as R::Object.



361
362
363
364
365
366
367
368
369
370
371
# File 'lib/R_interface/robject.rb', line 361

def to_i
  v = to_ruby
  v = v.first if v.is_a?(::Array) && v.size == 1
  case v
  when ::Integer then v
  when ::Float then v.to_i
  when ::String then Integer(v)
  else
    Integer(v)
  end
end

#to_rubyObject

Simple way to get a Ruby value: (self >> nil). For length-1 vectors returns the scalar; for longer returns array.



355
356
357
# File 'lib/R_interface/robject.rb', line 355

def to_ruby
  self >> nil
end

#to_sObject

Printed representation of the R object (as R would print it).



209
210
211
212
# File 'lib/R_interface/robject.rb', line 209

def to_s
  return super unless ::R.bridge.ready?
  ::R.bridge.print_r(@r_interop)
end

#typeofObject

R's typeof() of this object; unwrapped to a single string when length 1.



254
255
256
257
258
# File 'lib/R_interface/robject.rb', line 254

def typeof
  res = ::R::Support.exec_function("typeof", self)
  val = res.respond_to?(:>>) ? (res >> nil) : res
  val.is_a?(::Array) ? val[0] : val
end

#unboxed_get(index = nil, depth = 0) ⇒ Object Also known as: >>, <<

Unbox this R object to a Ruby value. Recurses until only Ruby values (no R::Object). Plain R::Object: re-evaluates handle; if list (typeof=="list") iterates [[i]] and recurses; if atomic uses [[index+1]] and result protocol (never uses $ on atomic). Raises UnboxDepthError when depth exceeded.



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/R_interface/robject.rb', line 263

def unboxed_get(index = nil, depth = 0)
  if depth >= ::R::Support::MAX_UNBOX_DEPTH
    ::Kernel.raise(::R::UnboxDepthError, "unbox: list too deep (max depth #{::R::Support::MAX_UNBOX_DEPTH} exceeded)")
  end
  val = ::R::Support.eval(@r_interop)
  return val unless val.is_a?(::R::Object)
  return val.unboxed_get(index, depth + 1) if val.class.instance_method(:unboxed_get).owner != ::R::Object

  handle = @r_interop.to_s
  safe_handle = handle =~ /\Ag2_v\d+\z/

  # Whole object (index.nil?): plain Object may be a list in R — use typeof/length, never $
  if index.nil?
    type_str = self.typeof.to_s.strip
    len_obj = self.length
    n = len_obj.respond_to?(:unboxed_get) ? len_obj.unboxed_get(0) : len_obj
    n = n.to_i if n.respond_to?(:to_i)
    if type_str == "list" && n.is_a?(::Integer) && n >= 1
      list_handle = handle
      unless safe_handle
        list_handle = ::R::Support.generate_var_name
        ::R.bridge.eval_r("#{list_handle} <- #{::R::Support.parse_arg(self)}")
      end
      arr = []
      (1..n).each do |i|
        elt = ::R::Support.eval("#{list_handle}[[#{i}]]")
        if elt.nil?
          arr << nil
        elsif !elt.is_a?(::R::Object)
          arr << elt
        else
          arr << (elt.is__null.unboxed_get(0) ? nil : elt.unboxed_get(nil, depth + 1))
        end
      end
      return arr
    end
    # Atomic (or length 0): treat as single element for consistency
    index = 0
  end

  idx = index + 1
  r_code = safe_handle ? "#{handle}[[#{idx}]]" : nil
  unless r_code
    var = ::R::Support.generate_var_name
    ::R.bridge.eval_r("#{var} <- #{::R::Support.parse_arg(self)}")
    r_code = "#{var}[[#{idx}]]"
  end
  val2 = ::R::Support.eval(r_code)
  return val2 unless val2.is_a?(::R::Object)
  return val2.unboxed_get(nil, depth + 1) if val2.class.instance_method(:unboxed_get).owner != ::R::Object

  # Plain R::Object single element: use result protocol until scalar or Vector/List
  r_code = "#{val2.r_interop}[[1]]"
  max_plain = ::R::Support::MAX_UNBOX_DEPTH - depth
  loop do
    var = ::R::Support.generate_var_name
    assignment = "#{var} <- #{r_code}"
    envelope = ::R.bridge.eval_r_with_result(assignment)
    raise "Result protocol: no envelope (buffer missing or invalid)" unless envelope
    case envelope[:type]
    when :scalar_double, :scalar_integer, :scalar_logical, :scalar_symbol
      return envelope[:value]
    when :scalar_character
      v = envelope[:value]
      return (v.is_a?(::String) && v =~ /^rb_obj_\d+$/) ? ::R::Support.get_ruby_object(v) : v
    when :handle
      obj = ::R::Object.build(envelope[:handle], nil, r_class: envelope[:r_class], wrapper_tag: envelope[:wrapper_tag])
      unless obj.instance_of?(::R::Object)
        return obj.unboxed_get(nil, depth + 1)
      end
      r_code = "#{envelope[:handle]}[[1]]"
      max_plain -= 1
      if max_plain <= 0
        ::Kernel.raise(::R::UnboxDepthError, "unbox: list too deep (max depth #{::R::Support::MAX_UNBOX_DEPTH} exceeded)")
      end
    else
      raise "Result protocol: unknown envelope type #{envelope[:type].inspect}"
    end
  end
end