О ключевых словах Ruby Args, структурах, операторах Splat и Double Splat (о, Боже!)


Разбор подхода к решению проблемы кодирования, с которой я столкнулся в дикой природе

Ниже приведена задача по кодированию, с которой я недавно столкнулся. Во время решения задачи я использовал некоторую «магию» Ruby, чтобы сделать то, что я считаю элегантным решением. Далее следует пошаговое выполнение итерационных задач задачи кодирования.
Для тех, кому не терпится, перейдите к задаче 3.

Задача 1

Напишите функцию, которая, получив целое число, возвращает следующее:

  • Для чисел, кратных 5 или включающих цифру 5, вывести «cats».
  • Для чисел, кратных 7 или включающих цифру 7, вывести «ботинки».
  • Для чисел, которые являются обоими вышеперечисленными, выведите «сапоги и кошки».

Примечание: Если вы занимаетесь кодированием, освоите арифметику по модулю; она часто встречается.
Странно, но я редко использую ее в своей повседневной работе. Поэтому мне интересно, почему она постоянно появляется в задачах по кодированию на интервью?

Задание 2

Начиная с 1, составьте список первых чисел, которые соответствуют каждой из перечисленных выше перестановок. (например, A: содержит 5, но не делится на 5, ни на 7, ни содержит 7, B: содержит 5, делится на 5, но не 7 и не содержит 7).

Чтобы это было выполнимо, необходимо знать, сколько комбинаций возможно.
Обратите внимание, что на данном этапе мы не беспокоимся о печати «котов» и «ботинок». Вместо этого мы генерируем список чисел.

Задача 3

Обобщите вышесказанное так, чтобы пользователи могли предоставлять произвольные правила для задачи 2.

Решение

class FirstMatchGenerator
  def initialize(**rules)
    @rules = rules
    @number_of_permutations = 2 ** rules.length
    @tester = Struct.new(*rules.keys, keyword_init: true)
  end

  def call
    results = {}
    integer = 0
    continue = true
    while continue
      integer += 1
      candidate = candidate_for(integer: integer)
      next if results.include?(candidate)

      results[candidate] = integer
      continue = results.size < @number_of_permutations
    end
    results.values
  end

  private

  def candidate_for(integer:)
    conditions = @rules.each_with_object({}) do |(name, func), cond|
      cond[name] = func.call(integer)
    end
    candidate = @tester.new(**conditions)
  end
end

puts FirstMatchGenerator.new(
  by_5: ->(i) { i % 5 == 0 },
  by_7: ->(i) { i % 7 == 0 }
).call.inspect
# => [1, 5, 7, 35]

Войти в полноэкранный режим Выйти из полноэкранного режима

Разбор решения

Приведенное выше решение использует ключевое слово args, оператор splat и double splat, а также Struct для проверки равенства.

Давайте рассмотрим инстанцирование класса:

FirstMatchGenerator.new(
  by_5: ->(i) { i % 5 == 0 },
  by_7: ->(i) { i % 7 == 0 }
)

Вход в полноэкранный режим Выход из полноэкранного режима

В приведенном выше примере мы передаем ключевые слова-ключи :by_5 и :by_7. Значениями этих ключей являются lambdas (например, ->(i) { i % 5 == 0 }, и ->(i) { i % 7 == 0 }). Если это поможет, представьте, что в качестве параметра мы передаем хэш Hash из Ruby.

Теперь приведем пример метода FirstMatchGenerator#initialize:

def initialize(**rules)
  @rules = rules
  @number_of_permutations = 2 ** rules.length
  @tester = Struct.new(*rules.keys, keyword_init: true)
end

Вход в полноэкранный режим Выход из полноэкранного режима

В приведенном выше примере **rules означает, что мы принимаем произвольно названные ключевые слова args (например, :by_5 и :by_7). Фактически, это рассматривается как объект Hash в методе initialize.

Число перестановок равно двум, возведенным в степень числа правил. В примере это будет 22 или 4.

И наконец, @tester — это динамически создаваемый объект Struct с атрибутами, которые являются заданными именованными args через оператор splat (например, *rules.keys). А с помощью keyword_init: true мы можем инстанцировать эту Struct с ключевыми словами args.

Причина использования Struct в том, что он реализует оператор равенства. Для двух объектов Struct, если все их атрибуты идентичны, то два объекта Struct считаются «равными» (например, a == b).

Теперь перейдем к методу FirstMatchGenerator#call:

def call
  results = {}
  integer = 0
  continue = true
  while continue
    integer += 1
    candidate = candidate_for(integer: integer)
    next if results.include?(candidate)

    results[candidate] = integer
    continue = results.size < @number_of_permutations
  end
  results.values
end

Войдите в полноэкранный режим Выход из полноэкранного режима

Здесь происходит довольно много всего. Перед циклом while находится набор переменных. Внутри цикла while мы:

  • создаем кандидата (подробнее об этом чуть позже)
  • проверяем, встречался ли нам уже этот кандидат (метод Hash#include? проверяет равенство кандидатов)
  • запомнить нового кандидата
  • прервать, если мы встретили всех кандидатов.

После цикла while мы возвращаем список целых чисел.

В моей первоначальной реализации у меня не было приватного метода, а вместо этого я вставлял результаты.

И, наконец, метод FirstMatchGenerator#candidate_for:

def candidate_for(integer:)
  conditions = @rules.each_with_object({}) do |(name, func), cond|
    cond[name] = func.call(integer)
  end
  candidate = @tester.new(**conditions)
end

Вход в полноэкранный режим Выход из полноэкранного режима

Правила @rules представляют собой Hash, ключами которого являются символы, а значениями — lambdas (например, { by_5: ->(i) { i % 5 == 0 }, by_7: ->(i) { i % 7 == 0 } }).

Используя эти правила, приведенный выше код вызывает каждую из lambdas для определения результата.

  • Учитывая integer из 5, мы получим conditions Hash из { by_5: true, by_7: false }.
  • Учитывая integer из 7, мы получим conditions Hash из { by_5: false, by_7: true }.
  • Учитывая integer из 35, мы получим conditions Hash из { by_5: true, by_7: true }.

Затем мы используем этот conditions Hash для создания нашей @tester Struct, которая предоставляет нам замечательный тест на равенство.

Заключение

Нужно ли мне было динамическое присвоение Struct? Нет. Я мог бы использовать тесты равенства Hash.

Но, по моему опыту, чрезмерное использование объекта Hash создает проблемы в дальнейшем. Почему? Потому что Hash — это хранилище данных без схемы. В то время как Struct имеет схему. И может предоставить более надежную отладочную информацию.

Я надеюсь, что в этом обзоре я расскажу о некоторых особенностях взаимодействия идиом Ruby.

Для себя я давно являюсь поклонником ключевых слов args и все больше поклонником скромного объекта Struct.

Оцените статью
devanswers.ru
Добавить комментарий