The Crystal Programming Language Forum

Array view/full wrapper

Hey everyone,

I’ve been struggling to find a way to implement an array view or wrapper or something like that such that it has access to the Array methods but it returns either itself or a wrapped array when appropriate. For instance,

class View(T)
  def initialize(@buffer : Array(T))
  end

  ...
end

ary = (0..4).to_a
view = View.new ary  # => View<#1>{0, 1, 2, 3, 4}
view.size            # => 5 , it returns an unwrappable value
view.map(&.to_s)     # => ["0", "1", "2", "3", "4"] , it returns an unwrapped array as the value type changed
view.select(&.even?) # => View<#2>{2, 4} , it returns a new wrapped value (copy)
view.reverse!        # => View<#1>{4, 3, 2, 1, 0} , it returns itself same as #reverse!
view                 # => View<#1>{4, 3, 2, 1, 0}
ary                  # => [4, 3, 2, 1, 0]

I’ve tested using the method_missing macro but the method information (e.g., return type) is missing to be able to know if it’s mutating the array or whether the result can be wrapped. So I tried different variations of having a hard-coded list of “annotated” methods such that #reverse is marked as wrappable, #reverse! as mutating and returning itself, #first as a non-mutating and non-wrappable method, and #first(n) as non-mutating and wrappable method. But of course, this does not handle methods added to Array.

The idea is to add functionality unique to such type, so methods can be chained easily, otherwise view.reverse.custom_method would fail to compile.

Any help or guidance would be much appreciated.

Does something like this work?

class View(T)
  def initialize(@array : Array(T))
  end

  macro method_missing(call)
    wrap(@array.{{call}})
  end

  private def wrap(array : Array(T))
    if array.same?(@array)
      self
    else
      View.new(array)
    end
  end

  private def wrap(object)
    object
  end
end

view = View.new([1, 2, 3, 4, 5, 6])
p! view

p! view.size

p! view.map(&.to_s)
p! typeof(view.map(&.to_s))

p! view.select(&.even?)
p! typeof(view.select(&.even?))

p! view.reverse!

Also note that because View is just a thin wrapper over an Array, I would personally use a struct instead of a class, and in wrap(array) the same? check is not needed anymore.

Thanks @asterite… it’s a very simple solution to use method overloading.

I neglected to mention that I intended to make the view implement something like copy-on-write such that before calling a mutating method, it makes a copy of the array, e.g.,

class View(T)
  macro method_missing(call)
    {% if mutating?(call.name) %}
      @array = @array.dup
    {% end %}
    wrap(@array.{{call}})
  end
end

However, I didn’t find a way to tell whether a method mutates the array or not so I ended up making a hard-coded list as suggested by someone on the gitter channel.