Memoize техники в Ruby и Ruby on Rails

декабря 24, 2010  |  Published in Ruby, Ruby on Rails, Основы  |  2 Comments

ruby brainЗаголовок не опечатка. Если вы не слышали о «memoizе», то я вам кратко расскажу. Memoiz’инг — это кэширование результата метода таким образом что когда вы вызываете метод в будущем, то он не выполняет заново все операции. Я покажу вам несколько различных техник для подобного кэширования, с комментариями за и против использования каждой техники.

Подготовка

Давайте создадим простое приложение Ruby on Rails на котором мы будем испытывать техники memoiz’инга. Нам понадобится лишь одна простая модель и таблица, которая вкоторой будут храниться имена и фамилии пользователей.

memoize$ rails g model user first_name:string last_name:string
invoke active_record
create db/migrate/20101204003605_create_users.rb
create app/models/user.rb
invoke test_unit
create test/unit/user_test.rb
create test/fixtures/users.yml
memoize$ rake db:migrate
(in /Users/bellmyer/Desktop/bellmyer/blog/memoize)
== CreateUsers: migrating ====================================================
— create_table(:users)
-> 0.0013s
== CreateUsers: migrated (0.0014s) ===========================================

Стандартный стиль использования

Вы вероятно случайно или намеренно сталкивались с простейшей формой memoize. Наша модель User имеет поля first_name и last_name. Давайте представим, что нам необходимо сделать метод который будет возвращать полное имя пользователя объединяя first_name с last_name:

class User < ActiveRecord::Base
  def full_name
    "#{first_name} #{last_name}"
  end
end

Давайте проверим в консоли Ruby on Rails, что модель работает правильно:

memoize$ rails c
Loading development environment (Rails 3.0.1)
uby-1.9.2-p0 > user = User.new :first_name => ‘Bob’, :last_name => ‘Smith’
=> #
ruby-1.9.2-p0 > user.full_name
=> «Bob Smith»

Отлично! Теперь давайте воспользуемся простой формой memoiz’инга:

class User < ActiveRecord::Base

  def full_name
    @full_name ||= "#{first_name} #{last_name}"
  end
end

В первый раз, когда этот метод был вызван, @full_name еще не существовало, поэтому будет выполнен код справа от ||=. В следующий раз когда вы вызовите этот метод, результат метода уже будет храниться в @full_name, поэтому методу нет необходимости заново создавать полное имя пользователя.

Способ получше

Это быстрый и грязный способ работает отлично для множества вещей, но работает он не во всех случаях. Давайте добавим к модели другой метод с memoize, и попытаемся найти в нем недостаток в логическом условии:

def has_full_name?
  @has_full_name ||= (!first_name.blank? && !last_name.blank?)
end

Этот метод проверяет есть ли у пользователя и имя и фамилия. На первый взгляд все выглядит хорошо и нам кажется, что он будет отлично работать, но что если результат будет false? @has_full_name будет иметь значение false,а это значит что правая сторона уравнения будет запущена с метки каждый раз. Вместо проверки того, содержит @has_full_name значение true или false, нам необходимо проверить объявлена ли переменная @has_full_name следующим образом:

def has_full_name?
  return @has_full_name if defined?(@has_full_name)
  @has_full_name = (!first_name.blank? && !last_name.blank?)
end

Теперь мы возвращаем значение переменной @has_full_name если она существует и в противном случае производим оценку. Теперь больше нет больше глюков с true/false. Этот метод более читабелен, но он не так краток и хорош, чтобы я его использовал.

Memoize для методов с аргументами

Что если у нас есть метод с аргументами? Мы обычно хотим чтобы при передаче одинаковых значений аргументов метод возвращал одинаковый результат. Таким образом почему бы не добавить нашей меморизации возможность кэширование для методов с аргументами?

def formal_name(salutation='Mr.', suffix=nil)
  @formal_name ||= {}
  return @formal_name[[salutation, suffix]] if @formal_name.has_key?([salutation, suffix])
  @formal_name[[salutation, suffix]] = "#{salutation} #{full_name} #{suffix}"
end

Эта реализация memoize немного сложнее. Так как этот метод может быть вызван с различными аргументами мы создаем хэш для хранения наших кэшируемых результатов. Мы используем метод has_key? для проверки того имеем ли мы уже значение для переданных аргументов.

Давайте испытаем наш новый метод:

memoize$ rails c user

Loading development environment (Rails 3.0.1) ruby-1.9.2-p0 >

user = User.new :first_name => ‘Bob’, :last_name => ‘Smith’
=> #
ruby-1.9.2-p0 > user.formal_name(‘Mr.’, ‘Jr.’)
=> «Mr. Bob Smith Jr.»
ruby-1.9.2-p0 > user.first_name = ‘John’
=> «John»
ruby-1.9.2-p0 > user.formal_name(‘Mr.’, ‘Jr.’)
=> «Mr. Bob Smith Jr.»

Теперь вы знаете, что memoize работает, потому, что несмотря на то, что я изменил первое имя метод formal_name продолжил возвращать нам тот же результат.

Используем модуль Memoizable из Ruby on Rails

Наш «лучший способ» замечательно работает и мы даже добавили возможность кеширования результатов для методов с аргументами. Но требуется много сил и времени чтобы добавить memoize для каждого метода, кроме того это увеличивает размер кода и уменьшает его читабельность.

Если вы используете Ruby on Rails версии 2.2 и выше, то вы можете поиметь хорошую выгоду от использования модуля Memoizable, который возьмет на себя все работу по добавлению кэширования в методы. Давайте добавим к нашей модели метод, который использует модуль Memoizable:

class User < ActiveRecord::Base
  extend ActiveSupport::Memoizable

# здесь всякие-разные методы...

  def initials(middle_initial)
    first_name[0] + middle_initial + last_name[0]
  end

  memoize :initials
end

Очень просто! Метод memoize берет заботу о добавлении кэширования результатов методов. Все, что вам необходимо — это расширить ваш класс с помощью модуля Memoizable и передать методу memoize имена методов, для которых необходимо добавить memoiz’ацию. В отличии от других способов memoize’инга обсуждаемых выше, этот использует модуль из ActiveSupport, поэтому этот способ больше подходит для проектов разрабатываемых на Ruby on Rails.

Хорошая новость состоит в том, что если вы используете Ruby on Rails в своем проекте, то вы можете использовать Memoizable везде, где вам того хочется. Давайте добавим клас который не наследуется от Active Record в папку lib.

# lib/my_number.rb
class MyNumber
  extend ActiveSupport::Memoizable
  attr_accessor :x
  def initialize(x)
    @x = x
  end
  def plus(y)
    @x + y
  end

  memoize :plus
end

А вот и доказательство того, что это работает:

memoize$ rails c
require ‘myLoading development environment (Rails 3.0.1)
ruby-1.9.2-p0 > require ‘my_number’
=> ["MyNumber"]
ruby-1.9.2-p0 > num = MyNumber.new(5)
=> #
ruby-1.9.2-p0 > num.plus 2
=> 7
ruby-1.9.2-p0 > num.x = 0
=> 0
ruby-1.9.2-p0 > num.plus 2
=> 7

Это самое лучшее решение для memoiz’инга если вы используете Ruby on Rails и я очень рекомендую его вам. Чем меньше кода вы вводите сами — тем меньше в вашем коде ошибок.

Кое-что, что вы еще должны знать

Во-первых, как упоминалось выше, модуль Memoizable доступен только в части ActiveSupport Ruby on Rails. Однако предыдущие примеры являются чистым Ruby и могут быть использованы в любом проекте на Ruby.

Во-вторых, вам не нужна mamoiz’ация абсолютно везде. Привожу ситуации где кэширование результатов недопустимо:

— Метод простой и memoiz’ация не сыграет особой роли

— Результат метода нуждается в изменении во время жизни своего объекта.

— Метод вряд ли будет вызван снова с теми же параметрами.

— Есть огромное количество комбинаций параметров которые будут вызываться и это съест слишком много памяти для хранения всех результатов.

Оригинал статьи на английском:

Ваши комментарии — лучшая благодарность автору блога.
Нашли ошибку — сообщите о ней.

Tags:

Responses

  1. says:

    января 5, 2011 at 00:29 (#)

    Великолепно!
    Спасибо за перевод, не знал об этой технике раньше.
    Переписал часть кода своего проекта, избавившись от неуклюжего кэширования в угоду этой memoiz’ации. Работает просто идеально. :)

  2. admin says:

    января 5, 2011 at 07:46 (#)

    И вам спасибо за спасибо=)

    Кстати, у вас замечательная преветствующая страница на сайте. Мне понравилась.

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.