Подробный разбор методов Ruby

Вы когда-нибудь задумывались о том, что происходит при написании кода на самом деле? Я много думаю об этом. И, будучи разработчиком Ruby, часто использую в работе различные методы, фактически не зная, как они реализованы.

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

В мире Ruby очень трудно обойтись без коллекций и используемых в них методов. Один из таких методов  —  метод find, который возвращает первый элемент, удовлетворяющий заданному в блоке условию. А в случае отсутствия блока он возвращает сам перечислитель.

marks = [40, 60, 34, 70]
marks.find{|a| a < 40}
=> 34

marks_by_subject = { 
  english: 40, maths: 60, physics: 34, chemistry: 70
}
marks_by_subject.find{|key,value| value < 40}
=> [:physics, 34]

divisible_range = 1..5
divisible_range.find{|num| num % 2 == 0 }
=> 2

Кроме метода find, часто используются и другие методы, которые есть в каждой коллекции из модуля Enumerable. Для Ruby характерно помещать общее поведение, используемое различными классами, в модуль и смешивать (с помощью примеси) эти модули с классами.

Примесь Enumerable предоставляет классам коллекций несколько методов обхода и поиска, а также возможность сортировки.

Для использования классами модуля Enumerable должен быть определен метод экземпляра each, который передает каждый элемент в блок. Если метод each не определен внутри класса, то при попытке использовать методы Enumerable возникнет ошибка NoMethodError.

Для передачи каждого элемента в блок нужно задействовать ключевое слово yield. Оно используется в Ruby для вызова блока. Возвращаемое при задействовании yield значение будет значением, возвращаемым из этого блока.

class Week
  include Enumerable
  def each
    yield "Monday"
    yield "Tuesday"
    yield "Wednesday"
    yield "Thursday"
    yield "Friday"
    yield "Saturday"
    yield "Sunday"
  end
end
week = Week.new
=> #<Week:0x0000559fe6b79a40>
week.include?("Friday") 
=> true
week.find {|day| day.start_with?('Sat') }
=> "Saturday"

И хотя мы не определили методы include? и find в классе Week, модуль Enumerable и метод each помогают добиться нашей цели.

Чтобы лучше в этом разобраться, сделаем реализацию методов find и each в Ruby.

Сначала откроем класс array и создадим свою версию метода each под названием my_each. Это простой метод, который с помощью цикла while проходит каждый элемент массива по индексу и передает его в блок.

class Array
  def my_each
    return to_enum(:my_each) unless block_given? 
    i = 0
    while i < size
      yield self[i]
      i += 1
    end
    self
  end
endmarks = [40, 60, 77, 81, 67]
marks.my_each{|mark| p mark} # будут выведены все элементы массива

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

Дальше создадим метод myfind, который будет получать значение из метода each. Это значение затем будет передаваться в блок, указанный в методе my_find. Если блок вернет true, то с помощью break выходим из each и возвращаем найденный элемент.

module MyEnumerable
  def my_find failure = nil
    return to_enum(:my_find) unless block_given? 
    element_found = nil
    is_found = false
    each do |num|
      if yield num
        element_found = num
        is_found = true
        break
      end
    end
    unless (is_found || failure.nil?)
      failure.call
    else
      element_found
    end
  end
endclass Array
  include MyEnumerable
end

В этом коде создан модуль MyEnumerable, имеющий метод myfind и включенный в класс Array. Дополнительным ​аргументом failure в методе myfind будет объект lambda/proc, вызывающийся в случае неуспеха операции поиска.

failure = lambda { "No Distinction" }

marks = [40, 60, 77, 81, 67]

marks.my_find(failure) {|mark| mark > 90 }
=> "No Distinction"

marks.my_find{|mark| mark.equal? 77 }
=> 77

Метод myfind используется не только в массивах, но и во всех классах, в которых есть метод each.

class Hash
  include MyEnumerable
end

marks_by_subject = { 
  english: 40, maths: 60, physics: 34, chemistry: 70
}

marks_by_subject.my_find{|key,value| value < 40}
=> [:physics, 34]

class Week
  include MyEnumerable
end

week.my_find {|day| day.start_with?('Sat') }
=> "Saturday"

Таким же образом в сочетании с методом each создаются все методы Enumerable.

Исходя из опыта создания собственных методов find и each, мы теперь знаем, что при создании всех методов Enumerable используется метод each.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Shan Shaji: Dissecting Ruby

Предыдущая статьяОтладка кода на Python с помощью icecream
Следующая статьяredis-hawk: детализированное отслеживание и контроль развертывания Redis