Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Ruby의 4가지 간단한 메모이제이션 패턴(및 하나의 보석)

메모이제이션은 접근자 메서드의 속도를 높이는 데 사용할 수 있는 기술입니다. 시간이 많이 걸리는 작업, 한 번만 수행하면 되는 작업을 수행하는 메서드의 결과를 캐시합니다. Rails에서는 메모이제이션이 너무 자주 사용되어 메서드를 메모하는 모듈도 포함되어 있는 것을 볼 수 있습니다.

나중에, 이것은 내가 먼저 이야기할 정말 일반적인 메모이제이션 패턴을 사용하기 위해 논쟁적으로 제거되었습니다. 하지만 보시겠지만 이 기본 패턴이 제대로 작동하지 않는 곳이 있습니다. 그래서 우리는 또한 더 발전된 메모이제이션 패턴을 살펴보고 그 과정에서 Ruby에 대한 몇 가지 멋진 것들을 배울 것입니다!

초기초 메모이제이션

Ruby에서 항상 다음과 같은 메모이제이션 패턴을 볼 수 있습니다.

앱/모델/주문.rb
class User < ActiveRecord::Base
  def twitter_followers
    # assuming twitter_user.followers makes a network call
    @twitter_followers ||= twitter_user.followers
  end
end

||= @twitter_followers = @twitter_followers || twitter_user.followers . 즉, 처음 twitter_followers에 전화를 걸 때만 네트워크에 전화를 걸 수 있습니다. , 그리고 미래의 호출은 인스턴스 변수 @twitter_followers의 값을 반환합니다. .

여러 줄 메모

때때로 느린 코드는 끔찍한 일을 하지 않으면 한 줄에 맞지 않습니다. 여러 줄의 코드로 작업하도록 기본 패턴을 확장하는 몇 가지 방법이 있지만 가장 좋아하는 방법은 다음과 같습니다.

앱/모델/주문.rb
class User < ActiveRecord::Base
  def main_address
    @main_address ||= begin
      maybe_main_address = home_address if prefers_home_address?
      maybe_main_address = work_address unless maybe_main_address
      maybe_main_address = addresses.first unless maybe_main_address
    end
  end
end

begin...end {...}와 같은 단일 항목으로 처리할 수 있는 코드 블록을 Ruby에 생성합니다. C 스타일 언어로. 그래서 ||= 이전과 마찬가지로 여기에서도 잘 작동합니다.

무는 어떻습니까?

그러나 이러한 메모이제이션 패턴에는 불쾌하고 숨겨진 문제가 있습니다. 첫 번째 예에서 사용자에게 트위터 계정이 없고 트위터 팔로어 API가 nil을 반환했다면 어떻게 될까요? ? 두 번째에서 사용자에게 주소가 없고 블록이 nil을 반환하면 어떻게 될까요? ?

메소드를 호출할 때마다 인스턴스 변수는 nil이 됩니다. , 그리고 우리는 값비싼 가져오기를 다시 수행할 것입니다.

따라서 ||= 아마도 올바른 방법이 아닐 것입니다. 대신 nil을 구별해야 합니다. 및 undefined :

앱/모델/주문.rb
class User < ActiveRecord::Base
  def twitter_followers
    return @twitter_followers if defined? @twitter_followers
    @twitter_followers = twitter_user.followers
  end
end
앱/모델/주문.rb
class User < ActiveRecord::Base
  def main_address
    return @main_address if defined? @main_address
    @main_address = begin
      main_address = home_address if prefers_home_address?
      main_address ||= work_address
      main_address ||= addresses.first # some semi-sensible default
    end
  end
end

불행히도 이것은 약간 못생겼지만 nil과 함께 작동합니다. , false , 그리고 다른 모든 것. (nil을 처리하려면 이 경우 Null 개체와 빈 배열을 사용하여 이 문제를 방지할 수도 있습니다. nil을 피해야 하는 또 다른 이유 !)

매개변수는 어떻습니까?

간단한 접근자에게 잘 작동하는 메모이제이션 패턴이 있습니다. 하지만 이와 같이 매개변수를 사용하는 메소드를 메모화하려면 어떻게 해야 할까요?

앱/모델/city.rb
class City < ActiveRecord::Base
  def self.top_cities(order_by)
    where(top_city: true).order(order_by).to_a
  end
end

Ruby의 Hash 완벽하게 작동하는 초기화 장치가 있습니다. 이 상황을 위해. Hash.new를 호출할 수 있습니다. 블록:

Hash.new {|h, key| h[key] = some_calculated_value }

그런 다음 설정되지 않은 해시의 키에 액세스하려고 할 때마다 블록이 실행됩니다. 그리고 해시 자체를 블록에 액세스하려고 시도한 키와 함께 전달합니다.

따라서 이 방법을 메모화하려면 다음과 같이 하면 됩니다.

앱/모델/city.rb
class City < ActiveRecord::Base
  def self.top_cities(order_by)
    @top_cities ||= Hash.new do |h, key|
      h[key] = where(top_city: true).order(key).to_a
    end
    @top_cities[order_by]
  end
end

그리고 order_by에 무엇을 전달하든 , 올바른 결과가 메모화됩니다. 블록은 키가 존재하지 않을 때만 호출되기 때문에 블록의 결과가 nil 또는 false인지 걱정할 필요가 없습니다.

놀랍게도 Hash 실제로 배열인 키에서 잘 작동합니다.

h = {}
h[["a", "b"]] = "c"
h[["a", "b"]] # => "c"

따라서 매개변수가 많은 메소드에서 이 패턴을 사용할 수 있습니다!

이 모든 문제를 겪고 있는 이유는 무엇입니까?

물론 이러한 메모이제이션 패턴을 많은 메서드에 추가하기 시작하면 코드를 꽤 빨리 읽을 수 없게 됩니다. 귀하의 방법은 모든 의식이 될 것이며 실체가 없을 것입니다.

따라서 많은 메모이제이션이 필요한 앱에서 작업하고 있다면 멋지고 친숙한 API로 메모이제이션을 처리하는 gem을 사용하는 것이 좋습니다. Memoist는 좋은 것으로 보이며 Rails가 사용하던 것과 매우 유사합니다. (또는 새로 발견한 메모이제이션 지식으로 직접 작성해 볼 수도 있습니다.)

그러나 이와 같은 패턴을 조사하고, 어떻게 조합되고, 작동하며, 날카로운 모서리가 있는지 확인하는 것은 항상 흥미롭습니다. 탐색하는 동안 덜 알려진 Ruby 기능에 대해 몇 가지 유용한 정보를 얻을 수 있습니다.