Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญโมดูล Enumerable ของ Ruby: การสร้างคลาส Enumerable

สวัสดีครับทุกท่าน กลับมาพบกันอีกครั้งกับ Ruby Magic! ปีที่แล้วเราได้เรียนรู้เกี่ยวกับโมดูล Enumerable ของ Ruby ซึ่งให้เมธอดต่างๆ ที่ใช้เมื่อทำงานกับออบเจ็กต์ที่วนรอบได้ เช่น อาร์เรย์ รีจ และแฮช

ครั้งนั้นเราสร้างคลาส LinkedList เพื่อแสดงวิธีการทำให้ออบเจ็กต์วนรอบได้ โดยการนำเมธอด #each ไปใช้งาน ด้วยการ include โมดูล Enumerable เราจึงสามารถเรียกเมธอดอย่าง #count, #map และ #select บน linked list ใดๆ ได้โดยไม่ต้องนำเมธอดเหล่านั้นไป implement เอง

เราได้เรียนรู้วิธีใช้ enumerables แล้ว แต่พวกมันทำงานอย่างไร? ส่วนหนึ่งของ "เวทมนตร์" ใน enumerables ของ Ruby มาจากการ implement แบบภายใน ซึ่งทั้งหมดอาศัยเมธอด #each เพียงเมธอดเดียว และยังอนุญาตให้ chain enumerators ได้อีกด้วย

วันนี้เราจะเรียนรู้ว่าเมธอดต่างๆ ในคลาส Enumerable ถูก implement อย่างไร และว่า物件 Enumerator ช่วยให้ chain เมธอดการวนรอบได้อย่างไร

ตามที่คุณคุ้นเคยแล้ว เราจะลงลึกเข้าไปโดยการ implement โมดูล Enumerable และคลาส Enumerator ของตัวเอง ดังนั้น ใส่หมวก "over-engineering" ไว้แล้ว เราเริ่มกันเลย!

Linked Lists

ก่อนเริ่ม เรามาดูเวอร์ชันใหม่ของคลาส linked list ที่เราเขียนไว้ก่อนหน้านี้

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

แตกต่างจากเวอร์ชันก่อนหน้า implementation นี้อนุญาตให้สร้างรายการว่างได้ รวมถึงรายการที่มีมากกว่าสองรายการ เวอร์ชันนี้ยังอนุญาตให้ส่ง linked list ไปเป็น tail เมื่อ initialize อีก linked list หนึ่งได้อีกด้วย

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 ของเรา include โมดูล Enumerable เมื่อ mapping บนออบเจ็กต์ด้วยเมธอดของ Enumerable ผลลัพธ์จะถูกเก็บในอาร์เรย์ ครั้งนี้เราจะ implement เวอร์ชันของตัวเองเพื่อให้เมธอดต่างๆ ส่งคืน linked list ใหม่แทน

เมธอด Enumerable

โมดูล Enumerable ของ Ruby มาพร้อมกับเมธอดการวนรอบอย่าง #map, #count, และ #select โดยการ implement เมธอด #each และ include โมดูล Enumerable ในคลาสของเรา เราจะสามารถใช้เมธอดเหล่านั้นบน linked lists ได้โดยตรง

แทนที่จะทำเช่นนั้น เราจะ implement DIYEnumerable และ import มันแทนเวอร์ชันของ Ruby แม้ว่าจะไม่ได้ทำแบบนี้ในทางปฏิบัติ แต่จะให้เราเห็นภาพชัดเจนว่า enumeration ทำงานภายในอย่างไร

มาเริ่มกับ #count กัน เมธอดที่ import ได้แต่ละเมธอดในคลาส Enumerable ใช้เมธอด #each ที่เรา implement ในคลาส LinkedList เพื่อวนลูปผ่านออบเจ็กต์เพื่อคำนวณผลลัพธ์

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

ในตัวอย่างนี้ เรา implement เมธอด #count บนโมดูล DIYEnumerable ใหม่ที่เราจะ include ใน linked list มันเริ่มนับจากศูนย์ และเรียกเมธอด #each เพื่อบวกหนึ่งเข้าไปในตัวนับสำหรับทุกรอบ หลังจากวนลูปผ่านทุก element เมธอดจะส่งคืนตัวนับที่ได้

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

เมธอด #map ถูก implement คล้ายกัน แทนที่จะเก็บตัวนับ มันใช้ accumulator ที่เริ่มต้นเป็นรายการว่าง เราจะวนลูปผ่านทุก element ในรายการ และ yield บล็อกที่ส่งมาให้แต่ละ element ผลลัพธ์ของแต่ละ yield จะถูก append เข้าไปใน accumulator list

เมธอดจะส่งคืน accumulator หลังจากวนลูปผ่านทุก element ใน input list แล้ว

class LinkedList
  include DIYEnumerable
  #...
end

หลังจาก include 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 ดูเหมือนจะกลับด้านลิสต์ นั่นเข้าใจได้ เพราะเมธอด #<< บนคลาส linked list ของเรา prepend รายการเข้าไปในลิสต์แทนที่จะ append ซึ่งเป็นคุณสมบัติของลักษณะ recursive ของ linked lists

สำหรับสถานการณ์ที่สำคัญที่ลำดับของลิสต์ต้องคงอยู่ เราต้องการวิธีการ reverse ลิสต์เมื่อ mapping ผ่านมัน Ruby implement Enumerable#reverse_each ซึ่งวนลูปผ่านออบเจ็กต์ในทิศทางตรงข้าม 聽起來像是解決方案很棒 但我们不能使用这种方法,因为我们的列表是嵌套的。我们直到完全遍历列表才知道列表有多长。

แทนที่จะรันบล็อกผ่านลิสต์ในทิศทางตรงข้าม เราจะเพิ่มเวอร์ชันของ #reverse_each ที่ทำในสองขั้นตอน มันวนลูปผ่านลิสต์ก่อนเพื่อ reverse โดยการสร้างลิสต์ใหม่ หลังจากนั้นจึงรันบล็อกผ่านลิสต์ที่ reverse แล้ว

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 บน linked list เราจะได้ลิสต์ใหม่กลับมาในลำดับเดียวกับลิสต์เดิม

การ Chain Enumeration ด้วย Enumerators

ผ่านเมธอด #each ที่ implement บนคลาส linked list และ DIYEnumerable ที่ include ไป เราสามารถวนลูปได้ทั้งสองทิศทางและ map ผ่าน linked lists

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]]]

อย่างไรก็ตาม ถ้าเราต้องการ map ผ่านลิสต์ในทิศทางตรงข้าม? เนื่องจากตอนนี้เรา reverse ลิสต์ก่อน mapping มันจึงส่งคืนในลำดับเดียวกับลิสต์เดิมเสมอ เราได้ implement #reverse_each และ #map ไปแล้ว ดังนั้นเราควรจะ chain พวกมันเข้าด้วยกันเพื่อ map ย้อนกลับได้ โชคดีที่คลาส Enumerator ของ Ruby ช่วยได้

ครั้งก่อน เราทำให้แน่ใจว่าเรียก Kernel#to_enum ถ้าเมธอด LinkedList#each ถูกเรียกโดยไม่มีบล็อก ซึ่งทำให้สามารถ chain เมธอด enumerable ได้โดยการส่งคืนออบเจ็กต์ Enumerator เพื่อดูว่าคลาส Enumerator ทำงานอย่างไร เราจะ implement เวอร์ชันของตัวเอง

class DIYEnumerator
  include DIYEnumerable

  def initialize(object, method)
    @object = object
    @method = method
  end

  def each(&block)
    @object.send(@method, &block)
  end
end

เช่นเดียวกับ Enumerator ของ Ruby คลาส enumerator ของเราเป็น wrapper รอบเมธอดบนออบเจ็กต์ ด้วยการ bubble up ไปยังออบเจ็กต์ที่ wrapped เราสามารถ chain เมธอด enumeration ได้

สิ่งนี้ทำงานได้เพราะ instance ของ DIYEnumerator เป็น enumerable เอง มัน implement #each โดยการเรียกออบเจ็กต์ที่ wrapped และ include โมดูล DIYEnumerable ดังนั้นเมธอด enumerable ทั้งหมดจึงสามารถเรียกบนมันได้

เราจะส่งคืน instance ของคลาส 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

ด้วย enumerator ของตัวเอง เราสามารถ chain enumeration เพื่อได้ผลลัพธ์ในลำดับเดิมโดยไม่ต้องส่งบล็อกว่างให้การเรียกเมธอด #reverse_each

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

Enumeration แบบ Eager และ Lazy

นี่คือการจบของการสำรวจการ implement ของโมดูล Enumerable และคลาส Enumerator สำหรับตอนนี้ เราได้เรียนรู้ว่าเมธอด enumerable บางส่วนทำงานอย่างไร และว่า enumerator ช่วย chain enumeration ได้อย่างไรโดยการ wrap ออบเจ็กต์ enumerable

แต่ยังมีปัญหาบ้างกับแนวทางของเรา โดยธรรมชาติ enumeration เป็น eager หมายถึงมันวนลูปผ่านลิสต์ทันทีที่หนึ่งในเมธอด enumerable ถูกเรียกบนมัน แม้จะโอเคในกรณีส่วนใหญ่ แต่ mapping ผ่านลิสต์ในทิศทางตรงข้ามจะ reverse ลิสต์สองครั้ง ซึ่งควรจะไม่จำเป็น

เพื่อลดจำนวนลูป เราสามารถใช้ Enumerator::Lazy เพื่อเลื่อนการวนลูปไปตอนสุดท้าย และให้การ reverse ลิสต์ซ้ำกัน cancel ตัวเองออกไป

เราจะเก็บไว้สำหรับตอนหน้า อย่าพลาดตอนต่อไปและการสำรวจเพิ่มเติมเกี่ยวกับการทำงานภายในที่神奇ของ Ruby! สมัครรับจดหมายข่าว Ruby Magic เพื่อรับบทความใหม่ส่งเข้าอีเมลทันทีที่เผยแพร่