Created
August 21, 2022 22:15
-
-
Save JoelQ/02f3ef9f61bebc7c8e5ea67d10ed92c6 to your computer and use it in GitHub Desktop.
Demonstration of how using `Enumerator` makes it nicer to work with a data structure that has multiple valid traversals such as a binary tree
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Tree | |
attr_reader :val, :left, :right | |
def self.empty | |
EmptyTree.new | |
end | |
def self.leaf(val) | |
new(val, empty, empty) | |
end | |
def initialize(val, left, right) | |
@val = val | |
@left = left | |
@right = right | |
end | |
# ==== Traversals ==== | |
# | |
# These methods provide different ways of traversing a tree. See | |
# https://en.wikipedia.org/wiki/Tree_traversal#Depth-first_search for more on | |
# the different ways a tree can be traversed. | |
# | |
# The methods can be called with a block and each item will be yielded in that | |
# given order: | |
# | |
# tree.preorder { |n| puts n } | |
# | |
# When called without a block they return a `Enumerator`. This allows calling | |
# any of the `Enumerable` methods for that given traversal such as: | |
# | |
# tree.inorder.map { |n| n * 2 } | |
# tree.postorder.find(&:even?) | |
# Depth-first search, pre-order. Given this tree: | |
# | |
# 1 | |
# / \ | |
# 2 5 | |
# / \ / \ | |
# 3 4 6 7 | |
# | |
# A pre-order traversal will go: | |
# 1, 2, 3, 4, 5, 6, 7 | |
def preorder(&block) | |
return to_enum(:preorder) unless block_given? | |
block.call(val) | |
left.preorder(&block) | |
right.preorder(&block) | |
end | |
# Depth-first search, in-order. Given this tree: | |
# | |
# 1 | |
# / \ | |
# 2 5 | |
# / \ / \ | |
# 3 4 6 7 | |
# | |
# An in-order traversal will go: | |
# 3, 2, 4, 1, 6, 5, 7 | |
def inorder(&block) | |
return to_enum(:inorder) unless block_given? | |
left.inorder(&block) | |
block.call(val) | |
right.inorder(&block) | |
end | |
# Depth-first search, post-order. Given this tree: | |
# | |
# 1 | |
# / \ | |
# 2 5 | |
# / \ / \ | |
# 3 4 6 7 | |
# | |
# A post-order traversal will go: | |
# 3, 4, 2, 6, 7, 5, 1 | |
def postorder(&block) | |
return to_enum(:postorder) unless block_given? | |
left.postorder(&block) | |
right.postorder(&block) | |
block.call(val) | |
end | |
end | |
class EmptyTree | |
def preorder | |
end | |
def postorder | |
end | |
def inorder | |
end | |
end | |
# 1 | |
# / \ | |
# 2 5 | |
# / \ / \ | |
# 3 4 6 7 | |
tree = | |
Tree.new( | |
1, | |
Tree.new( | |
2, | |
Tree.leaf(3), | |
Tree.leaf(4), | |
), | |
Tree.new( | |
5, | |
Tree.leaf(6), | |
Tree.leaf(7), | |
), | |
) | |
# (2 * 3) + (12 / 6) | |
expression = | |
Tree.new( | |
:+, | |
Tree.new(:*, Tree.leaf(2), Tree.leaf(3)), | |
Tree.new(:/, Tree.leaf(12), Tree.leaf(6)), | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is awesome, thanks for sharing! A humble suggestion: I often use the
__method__
local when turning a method into an enum, eg:Renaming the method is easier now, since I only have to change it in one place. It's especially handy if you're calling
to_enum
farther down in the method body so it's less visually obvious that you have to change the method name in multiple places. I've been bitten by this in the past so I thought I'd mention it 😄