После непродолжительного отпуска сначала
в горах, потом
на острове решил выложить небольшую статью о том, с чем я сталкиваюсь каждый день - о мемоизации.
И так, мемоизация - запоминание (кэширование) результата исполнения какого нибудь куска кода, в нашем случае - метода. Если применять ее умело, то выгода велика, как впрочем и опасность возможных веселых багов. И так, ближе к делу, давайте рассмотрим следующий код.
class Dog
def bar
sleep 3
'bar!'
end
end
Метод #bar у нас будет представлять медленный метод, предположим что в нем происходит сложное вычисление, результат которого в принципе будет постоянным для данного объекта, т.е. не поменяется за время его жизни. Каждый раз при вызове #bar мы будем ждать 3 секунды, а в реальности еще будем тратить ресурсы машины. Хотелось бы как нибудь сохранить результат исполнения метода, и при следующем вызове возвращать его, а не исполнять все заново. И вот к нам на помощь приходят инстанс переменные, они имеют значение nil по умолчанию.
# тут и дальше представьте что мы пишем внутри класса Bar
def bar
unless @bar
@bar = begin
sleep 3
'bar!' #мы запишем в @bar строку, и в следующий раз руби не пойдет в unless
end
end
@bar
end
Этот некрасивый участок кода уже будет работать, но хотелось бы то нибудь покороче. Упростим:
def bar
@bar || @bar = begin
sleep 3
'bar!'
end
end
Вот это уже куда ближе. Когда в @bar ничего нет - выполнится вторая часть, когда там будет строка - метод вернет эту строка и остановится.
В руби есть еще более сокращенная запись - выражение ||=. Используем его, и вместо блока с begin - end вынесем медленное вычисление в отдельный приватный метод
def bar
@bar ||= calculate_bar # ||= следует понимать как “верни мне значение переменной или,
# если там nil или false,- вычисли правую часть, запиши результат в
# переменную, и верни его
end
private
def calculate_bar
sleep 3
'bar!'
end
Поговорим теперь о подводных камнях, которые тут легко упустить
1 Приватный метод исполниться только один раз. Не используйте мемоизацию если не уверены что результат исполнения будет постоянным.
2 И менее очевидная вещь - если результатом исполнение медленной части является булево значение ( True или False ) то такая мемоизация сработает только если метод возращает True. Мой прошлый проект жил с такими методами где то с год, и все удивлялись, почему же так медленно)
def fast_part
@result ||= slow_part
end
private
def slow_part
sleep 3
false
end
И так, вызываем первый раз #fast_part и записываем в @result false. Вызываем следующий раз и фактически будет исполнено следующее:
false ||= slow_part # и как результат руби всегда будет исполнять slow_part
# и записывать результат в result
Для мемоизации булевых методов нужно делать так def fast_part
@result = slow_part unless defined? @result # defined? уже просто так не обдурить))
@result
end
А что делать если метод принимает аргументы и его результат будет зависеть от их? Воспользуемся хэшами, ведь они тоже возвращают nil для несуществующего ключа (по умолчанию). Так же ключами хэшей могут быть почти все объекты, в том числе и массивы.
def fast_part(a, b)
@results ||= {} #инициализируем @results пустым хэшем, если нужно
@results[[a, b]] ||= slow_part(a, b) # записываем результат в хэш с ключем - массивом из аргументов
end
private
def slow_part(a, b)
sleep 3
"#{a} #{b}"
end
Вариант fast_part для булевых методов будет таким
def fast_part(a, b)
@results ||= {}
@results[[a, b]] = slow_part(a, b) unless @results.has_key?([a, b])
@results[[a, b]]
end
Вот и все, используйте мемоизацию с умом и да ускорятся скрипты ваши.