Упражнение в классах, методах и рекурсии

Как человек, который впервые начал изучать программирование на Javascript, циклы for занимают особое место в моем сердце. Итерация по коллекции всегда включает или использует вспомогательный метод с некоторым длинным, чрезвычайно явным циклом «for». Поэтому, когда я начал изучать Ruby, я сразу же скептически отнесся к его огромному выбору встроенных методов, итераторов и перечислимых элементов. Как .each узнал, с чего начать и прекратить итерацию, если я не хочу давать ему счетчик? Где .map или .collect хранила свою рабочую информацию?

На мое понимание того, как работают итераторы Ruby, ответили упражнения, в которых я воссоздал методы .each и .map, используя yield, но я не собираюсь вдаваться в подробности. Вместо этого я собираюсь подробно рассказать вам о своем решении для подсказки, с которой я столкнулся при подаче заявки на учебный курс по программированию: воссоздание метода Flatten в Ruby для массивов.

Ruby’s Flatten

Допустим, у вас есть массив вложенных или многомерных массивов, то есть массив, в котором есть элементы, которые также являются массивами:

array = [1, [2, 3, [4, 5]]]

Метод flatten вернет одномерный массив, массив, в котором все значения находятся на одном уровне:

array = [1, [2, 3, [4, 5]]]
array.flatten #=> [1, 2, 3, 4, 5]

Кроме того, вы можете вызвать метод flatten с аргументом, который сгладит этот массив на такое количество уровней:

array = [1, [2, 3, [4, 5]]]
array.flatten(1) #=> [1, 2, 3, [4, 5]]

Объектно-ориентированный рубин

Ruby - объектно-ориентированный язык. Почти все в Ruby является объектом, и каждый объект имеет класс и является экземпляром этого класса. Ruby включает в себя набор методов по умолчанию для объектов, которые различаются для разных классов, поэтому мы можем использовать .flatten для получателей, которые являются массивами или хешами (хотя .flatten работает с хешами несколько иначе), а не с теми, которые числа или строки. Но как узнать класс объекта? Конечно, для этого есть способ: .class!

arr = [1, [2, 3, [4, 5]]]
arr.class #=> Array
[1, [2, 3, [4, 5]]].class #=> Array

Мы будем использовать этот метод в нашем пользовательском методе сглаживания, чтобы определить, являются ли элементы массива также массивами.

Методы класса

Начнем с определения метода my flatten для класса Array:

class Array
  def my_flatten
    # code goes here
  end
end 

Таким образом, метод my_flatten доступен для любого объекта, который является экземпляром класса Array.

Аргументы по умолчанию

Мы хотим, чтобы наш метод сгладил массив по заданному количеству измерений, если передан аргумент, но полностью сгладил массив, если аргумент не передан. Следовательно, наш метод должен иметь возможность принимать необязательный аргумент. Устанавливая для параметра значение n = nil, мы сообщаем Ruby, что если этот аргумент не определен при вызове метода, метод передаст значение по умолчанию, которое для наших целей будет ноль.

Наш метод должен делать одно, если n имеет значение, и другое, если n его нет. Я решил использовать тернарный оператор, который проверяет истинность n, как если бы n имеет значение, он вернет true, а если n равно nil, он вернет false:

def my_flatten(n = nil)
  n ? method_if_n(self, n) : method_if_no_n(self)
end

Поскольку мы хотим, чтобы наш метод работал с получателем, мы используем self для ссылки на экземпляр, на котором вызывается метод.

Теперь напишем наши методы сглаживания!

Выравнивание массива по одному измерению

Мы собираемся начать с определения вспомогательного метода, который выравнивает массив по одному измерению:

def single_flatten(array)
  results = []
  array.each do |element|
    if element.class == Array
      element.each {|value| results << value}
    else
      results << element
    end
  end
  results
end

Этот метод принимает массив и устанавливает пустой массив результатов. Затем он просматривает каждый элемент рассматриваемого массива. Если элемент является массивом, он будет смотреть на каждый элемент в этом массиве и добавлять значение вложенного элемента в массив результатов с помощью оператора лопаты. Если элемент не является массивом, он добавит значение этого элемента в массив результатов. После того, как каждый элемент в массиве либо сам, либо его компоненты были добавлены в результаты, метод возвращает массив результатов, который эквивалентен исходному массиву, сглаженному по одному измерению:

my_array = [1, [2, 3, [4, 5]]]
single_flatten(my_array) #=> [1, 2, 3, [4, 5]]

Сглаживание массива n раз

Но что, если мы хотим сгладить массив заданное количество раз? Мы можем вызвать single_flatten в цикле while со счетчиком:

def multiple_flatten(array, n)
  count = 0
  arr = array
  while count < n do
    arr = single_flatten(arr)
    count += 1
  end
  arr
end

Сначала мы устанавливаем счетчик равным нулю, а переменную arr равной нашему начальному массиву. Если счетчик не достиг того количества измерений, которое мы указали, чтобы уменьшить наш массив на, arr будет передан в single_flatten. Каждый раз, когда выполняется блок в цикле while, arr переназначается результату single_flatten, а счетчик увеличивается на единицу. Как только счетчик равен количеству измерений, на которое мы хотим уменьшить, окончательное возвращаемое значение метода - arr, последний результат single_flatten.

my_array = [1, [2, 3, [4, [5, 6]]]]
multiple_flatten(my_array, 2) #=> [1, 2, 3, 4, [5, 6]]

Сглаживание массива с помощью рекурсии

Наш последний вспомогательный метод - это рекурсивное сглаживание:

def recursive_flatten(array, results = [])
  array.each do |element|
    if element.class == Array
      recursive_flatten(element, results)
    else
      results << element
    end
  end
  results
end

Как и single flatten, он проверяет, является ли элемент массивом, и, если нет, добавляет значение элемента в массив результатов. Однако, если элемент является массивом, метод вызывает сам себя, вытягивая этот элемент и существующий массив результатов. Метод будет продолжать вызывать себя для элементов элементов, пока все элементы не станут массивами и не будут помещены в массив результатов.

Поскольку мы хотим, чтобы метод был полностью рекурсивным, мы устанавливаем для результатов необязательный аргумент, который по умолчанию равен пустому массиву. Если мы установим в методе пустой массив результатов, он будет сбрасывать массив результатов каждый раз, когда вызывается функция. В первый раз, когда мы вызываем recursive_flatten, мы вызываем его без аргумента результатов. Таким образом, он создаст наш изначально пустой массив результатов, который затем будет заполнен, передан другим методам recursive_flatten и никогда не будет сброшен обратно в пустой!

Теперь мы можем уменьшить многомерный массив до одного измерения, независимо от того, со скольких измерений он начинается:

my_array = [1, [2, 3, [4, 5]]]
recursive_flatten(my_array) #=> [1, 2, 3, 4, 5]

Собираем все вместе

Вызывая методы multiple_flatten и recursive_flatten в тернарном операторе и вставляя вспомогательные методы в определение класса, мы получаем:

class Array
  def my_flatten(n = nil)
    n ? multiple_flatten(self, n) : recursive_flatten(self)
  end
  private
  def recursive_flatten(array, results = [])
    array.each do |element|
      if element.class == Array
        recursive_flatten(element, results)
      else
        results << element
      end
    end
    results
  end
  def multiple_flatten(array, n)
    count = 0
    arr = array
    while count < n do
      arr = single_flatten(arr)
      count += 1
    end
    arr
  end
  def single_flatten(array)
    results = []
    array.each do |element|
      if element.class == Array
        element.each {|value| results << value}
      else
        results << element
      end
    end
    results
  end
end

Поскольку вспомогательные методы никогда не должны вызываться с явным получателем, я решил сделать вспомогательные методы закрытыми .

И теперь, после запуска этого кода в своей среде, вы сможете сглаживать объекты, являющиеся массивами, с помощью .my_flatten!

Счастливое сглаживание ~