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

Ruby의 내부 열거

Ruby Magic의 다른 버전에 오신 것을 환영합니다! 1년 전, 우리는 Ruby의 Enumerable에 대해 배웠습니다. 모듈은 배열, 범위 및 해시와 같은 열거 가능한 개체로 작업할 때 사용하는 방법을 제공합니다.

그때 우리는 LinkedLIst를 만들었습니다. #each를 구현하여 개체를 열거 가능하게 만드는 방법을 보여주는 클래스 그것에 대한 방법. Enumerable 포함 모듈에서 #count와 같은 메소드를 호출할 수 있었습니다. , #map#select 직접 구현할 필요 없이 모든 연결 목록에서 사용할 수 있습니다.

열거형을 사용하는 방법을 배웠지만 어떻게 작동합니까? Ruby의 enumerable에서 마법의 일부는 내부 구현에서 비롯되며 모두 단일 #each를 기반으로 합니다. 메서드를 제공하고 열거자를 연결하는 것도 허용합니다.

오늘은 Enumerable 클래스가 구현되고 Enumerator 개체는 열거 메서드를 연결할 수 있습니다.

익숙해지면 Enumerable 자체 버전을 구현하여 자세히 알아볼 것입니다. 모듈 및 Enumerator 수업. 그러니 오버엔지니어링 헬멧을 쓰고 가자!

연결된 목록

시작하기 전에 이전에 작성한 연결 목록 클래스의 새 버전부터 시작하겠습니다.

class LinkedList
  def initialize(head = nil, *rest)
    @head = head
 
    if rest.first.is_a?(LinkedList)
      @tail = rest.first
    elsif rest.any?
      @tail = LinkedList.new(*rest)
    end
  end
 
  def <<(head)
    @head ? LinkedList.new(head, self) : LinkedList.new(head)
  end
 
  def inspect
    [@head, @tail].compact
  end
 
  def each(&block)
    yield @head if @head
    @tail.each(&block) if @tail
  end
end

이전 버전과 달리 이 구현에서는 빈 목록과 항목이 2개 이상 있는 목록을 만들 수 있습니다. 이 버전에서는 다른 것을 초기화할 때 연결 목록을 꼬리로 전달할 수도 있습니다.

irb> LinkedList.new
=> []
irb> LinkedList.new(1)
=> [1]
irb> LinkedList.new(1, 2)
=> [1,[2]]
irb> LinkedList.new(1, 2, 3)
=> [1,[2,[3]]]
irb> LinkedList.new(1, LinkedList.new(2, 3))
=> [1,[2,[3]]]
irb> LinkedList.new(1, 2, LinkedList.new(3))
=> [1,[2,[3]]]

이전에는 LinkedLIst 클래스에 Enumerable이 포함됨 기준 치수. Enumerable 중 하나를 사용하여 개체에 매핑할 때 의 메소드에서 결과는 배열에 저장됩니다. 이번에는 메서드가 대신 새 연결 목록을 반환하도록 자체 버전을 구현합니다.

열거 가능한 메소드

Ruby의 Enumerable 모듈은 #map과 같은 열거 메서드와 함께 제공됩니다. , #count , 및 #select . #each 구현 메소드 및 Enumerable 포함 모듈을 사용하면 연결 목록에서 해당 메서드를 직접 사용할 수 있습니다.

대신 DIYEnumerable을 구현합니다. Ruby 버전 대신 가져옵니다. 이것은 일반적으로 수행하는 작업이 아니지만 열거가 내부적으로 작동하는 방식에 대한 명확한 통찰력을 제공합니다.

#count부터 시작하겠습니다. . Enumerable에서 가져올 수 있는 각 메서드 클래스는 #each를 사용합니다. LinkedLIst에서 구현한 메서드 결과를 계산하기 위해 객체를 반복하는 클래스입니다.

module DIYEnumerable
  def count
    result = 0
    each { |element| result += 1 }
    result
  end
end

이 예에서는 #count를 구현했습니다. 새 DIYEnumerable의 메소드 연결 목록에 포함할 모듈입니다. 0에서 카운터를 시작하고 #each를 호출합니다. 모든 루프에 대해 카운터에 하나를 추가하는 방법입니다. 모든 요소를 ​​반복한 후 이 메서드는 결과 카운터를 반환합니다.

module DIYEnumerable
  # ...
 
  def map
    result = LinkedList.new
    each { |element| result = result << yield(element) }
    result
  end
end

#map 방법은 유사하게 구현됩니다. 카운터를 유지하는 대신 빈 목록으로 시작하는 누산기를 사용합니다. 목록의 모든 요소를 ​​반복하고 각 요소에 대해 전달된 블록을 생성합니다. 각 yield의 결과는 accumulator 목록에 추가됩니다.

이 메서드는 입력 목록의 모든 요소를 ​​반복한 후 누산기를 반환합니다.

class LinkedList
  include DIYEnumerable
 
  #...
end

DIYEnumerable 포함 후 LinkedLIst에서 , 새로 추가된 #count를 테스트할 수 있습니다. 및 #map 방법.

irb> list = LinkedList.new(73, 12, 42)
=> [73, [12, [42]]]
irb> list.count
=> 3
irb> list.map { |element| element * 10 }
=> [420, [120, [730]]]

두 가지 방법 모두 작동합니다! #count 메소드는 목록의 항목을 올바르게 계산하고 #map 메소드는 각 항목에 대해 블록을 실행하고 업데이트된 목록을 반환합니다.

역순 목록

그러나 #map 메소드가 목록을 되돌린 것 같습니다. #<<로 이해할 수 있습니다. 연결 목록 클래스의 메서드는 항목을 추가하는 대신 목록 앞에 추가합니다. 이는 연결 목록의 재귀적 특성입니다.

목록의 순서를 유지해야 하는 상황에서는 목록을 매핑할 때 목록을 뒤집을 방법이 필요합니다. Ruby는 Enumerable#reverse_each를 구현합니다. , 객체를 역순으로 반복합니다. 우리 문제에 대한 훌륭한 해결책처럼 들립니다. 슬프게도 목록이 중첩되어 있기 때문에 그런 접근 방식을 사용할 수 없습니다. 목록을 완전히 반복할 때까지 목록이 얼마나 긴지 모릅니다.

목록에서 블록을 역순으로 실행하는 대신 #reverse_each 버전을 추가합니다. 이 두 단계를 수행합니다. 먼저 목록을 반복하여 새 목록을 만들어 목록을 뒤집습니다. 그 후 역순 목록에 대해 블록을 실행합니다.

module DIYEnumerable
  # ...
 
  def reverse_each(&block)
    list = LinkedList.new
    each { |element| list = list << element }
    list.each(&block)
  end
 
  def map
    result = LinkedList.new
    reverse_each { |element| result = result << yield(element) }
    result
  end
end

이제 #reverse_each를 사용하겠습니다. #map에서 메서드를 사용하여 올바른 순서로 반환되었는지 확인합니다.

irb> list = LinkedList.new(73, 12, 42)
=> [73, [12, [42]]]
irb> list.map { |element| element * 10 }
=> [730, [120, [420]]]

효과가있다! #map을 호출할 때마다 연결 목록에서 메서드를 사용하면 원본과 동일한 순서로 새 목록을 다시 가져옵니다.

열거자로 열거형 연결

#each를 통해 연결 목록 클래스에 구현된 메서드와 포함된 DIYEnumerator , 이제 양방향으로 루프를 돌고 연결 목록을 매핑할 수 있습니다.

irb> list.each { |x| p x }
73
12
42
irb> list.reverse_each { |x| p x }
42
12
73
irb> list.reverse_each.map { |x| x * 10 }
=> [730, [120, [420]]]
=> [420, [120, [730]]]

그러나 매핑해야 하는 경우 목록을 거꾸로? 이제 매핑하기 전에 목록을 뒤집기 때문에 항상 원래 목록과 동일한 순서로 반환됩니다. 우리는 이미 #reverse_each를 모두 구현했습니다. 및 #map , 그래서 우리는 그것들을 서로 연결하여 거꾸로 매핑할 수 있어야 합니다. 다행히 Ruby의 Enumerator 수업이 도움이 될 수 있습니다.

지난번에 Kernel#to_enum을 호출했는지 확인했습니다. LinkedList#each 메서드가 블록 없이 호출되었습니다. Enumerator를 반환하여 열거 가능한 메서드를 연결할 수 있습니다. 물체. Enumerator 클래스가 작동하면 자체 버전을 구현하겠습니다.

class DIYEnumerator
  include DIYEnumerable
 
  def initialize(object, method)
    @object = object
    @method = method
  end
 
  def each(&block)
    @object.send(@method, &block)
  end
end

Ruby의 Enumerator처럼 , 우리의 열거자 클래스는 개체의 메서드를 둘러싼 래퍼입니다. 래핑된 개체까지 버블링하여 열거 메서드를 연결할 수 있습니다.

DIYEnumerator 인스턴스 자체가 열거 가능합니다. #each를 구현합니다. 래핑된 개체를 호출하여 DIYEnumerable을 포함합니다. 모듈에서 모든 열거 가능한 메서드를 호출할 수 있습니다.

DIYEnumerator의 인스턴스를 반환합니다. LinkedList#each에 블록이 전달되지 않은 경우 클래스 방법.

class LinkedList
  # ...
 
  def each(&block)
    if block_given?
      yield @head
      @tail.each(&block) if @tail
    else
      DIYEnumerator.new(self, :each)
    end
  end
end

자체 열거자를 사용하여 이제 빈 블록을 #reverse_each에 전달하지 않고도 원래 순서대로 결과를 얻기 위해 열거를 연결할 수 있습니다. 메소드 호출.

irb> list = LinkedList.new(73, 12, 42)
=> [73, [12, [42]]]
irb> list.map { |element| element * 10 }
=> [420, [120, [730]]]

열망하고 게으른 열거

이것으로 Enumerable 구현에 대한 엿보기를 마칩니다. 모듈 및 Enumerator 지금은 수업. 일부 열거 가능한 메서드가 작동하는 방식과 열거자가 열거 가능한 개체를 래핑하여 열거자가 열거를 연결하는 데 도움이 되는 방법을 배웠습니다.

그러나 우리의 접근 방식에는 몇 가지 문제가 있습니다. 본질적으로 열거는 열심합니다. 즉, 열거 가능한 메서드 중 하나가 호출되는 즉시 목록을 반복합니다. 대부분의 경우 괜찮지만 목록을 반대로 매핑하면 목록이 두 번 반전되므로 불필요합니다.

루프 수를 줄이기 위해 Enumerator::Lazy를 사용할 수 있습니다. 마지막 순간까지 반복을 지연하고 중복 목록 반전이 자체적으로 취소되도록 합니다.

하지만 다음 에피소드를 위해 저장해야 합니다. Ruby의 마법 같은 내부 작동에 대한 추가 탐험을 놓치고 싶지 않으신가요? Ruby Magic 이메일 뉴스레터를 구독하면 새로운 기사가 ​​게시되는 즉시 받은 편지함으로 배달됩니다.