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 이메일 뉴스레터를 구독하면 새로운 기사가 게시되는 즉시 받은 편지함으로 배달됩니다.