Skip to content

Commit

Permalink
Allow Nodes::Nodes to hold arbitrary values (#85)
Browse files Browse the repository at this point in the history
Allow `Nodes::Node`s to hold arbitrary `value`s! Now, you can have a
terminal node that on top of representing its word, it can hold on to

- Add `value` attribute to `Nodes::Node`
- Add optional, nilable `value` argument to `Container#add`,
`Nodes::Node#add`
- Store given `value` in terminal node via `Nodes::Raw#add` and
`Nodes::Raw#add_to_children_tree`, when present
- Update `Compressor#merge` and `Compressor#compress_children_and_copy`
to store `value` from `Nodes::Raw` in the
  terminal `Nodes::Compressed`, when present
- Add optional `values` array argument to `Container#concat`
corresponding 1:1 to `words`, that passes each word and
  value to `#add` when present
- Add `value` to `Inspectable#inspect` output, when present
- Add generic type `TValue` to `Nodes::Node` type signature
  - Extract `_Nilable` interface to own top-level file
- Make `value` attribute in `Nodes::Node` use the new `TValue` generic
type
  - Allow `TValue` to be `nil` by default with `?`
- Require `TValue` to implement `_Inspect` built-in(!) interface, for
`Inspectable` module
- Change all types that depend on `Nodes::Node` also have the generic
type `TValue < _Inspect`
    including static methods
- Add optional `TValue` argument to `Container#add`, `Nodes::Node#add`,
and `Nodes::Raw#add_to_children_tree`
  - Add optional `Array[TValue?]` argument to `Container#concat`
- Add new `|| raise`s because the inline type conversion to non-nil
doesn't work anymore for `steep` check 🤷🏻‍♂
- Make compatible with `rbs` `v3.7.0` and `steep` `v1.9.0`
  -  Change `ProviderCollection#[]` to return TProvider?
  - Change `Nodes::Node#[]` to return Nodes::Node[TValue]?
  - Change `Container#[]` to return Nodes::Node[TValue]?
  - Add type annotations for `UnannotatedEmptyCollection`s
- Raise `InvalidOperation`s when `compress(child)` return nil value
which is not supposed to be possible
  • Loading branch information
gonzedge authored Dec 8, 2024
1 parent a25d2c9 commit e62bbee
Show file tree
Hide file tree
Showing 36 changed files with 252 additions and 131 deletions.
2 changes: 2 additions & 0 deletions .reek.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ detectors:
# Need these setters for compression
- 'Rambling::Trie::Nodes::Node#children_tree'
- 'Rambling::Trie::Nodes::Node#parent'
- 'Rambling::Trie::Nodes::Node#value'
# All Properties are settable by clients because of config convention
- 'Rambling::Trie::Configuration::Properties'

Expand All @@ -22,6 +23,7 @@ detectors:

FeatureEnvy:
exclude:
- 'Rambling::Trie::Container#concat' # needs to access words array to add all words _and_ values
- 'Rambling::Trie::Compressor' # needs to access node internals to perform compression
- 'Rambling::Trie::Nodes::Compressed#partial_word_chars?' # needs access to child letter to figure out compressed letters

Expand Down
37 changes: 36 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,40 @@
# CHANGELOG

## 2.5.2 [compare][compare_v2_5_1_and_main]
## 2.6.0 (in development) [compare][compare_v2_5_1_and_main]

### Enhancements

#### Major

- Allow `Nodes::Node`s to hold arbitrary values ([#85][github_pull_85]) by [@gonzedge][github_user_gonzedge]
- Add `value` attribute to `Nodes::Node`
- Add optional, nilable `value` argument to `Container#add`, `Nodes::Node#add`
- Store given `value` in terminal node via `Nodes::Raw#add` and `Nodes::Raw#add_to_children_tree`, when present
- Update `Compressor#merge` and `Compressor#compress_children_and_copy` to store `value` from `Nodes::Raw` in the
terminal `Nodes::Compressed`, when present
- Add optional `values` array argument to `Container#concat` corresponding 1:1 to `words`, that passes each word and
value to `#add` when present
- Add `value` to `Inspectable#inspect` output, when present

### Minor

- Types for `Nodes::Node`s arbitrary value (part of [#85][github_pull_85]) by [@gonzedge][github_user_gonzedge]
- Add generic type `TValue` to `Nodes::Node` type signature
- Extract `_Nilable` interface to own top-level file
- Make `value` attribute in `Nodes::Node` use the new `TValue` generic type
- Allow `TValue` to be `nil` by default with `?`
- Require `TValue` to implement `_Inspect` built-in(!) interface, for `Inspectable` module
- Change all types that depend on `Nodes::Node` also have the generic type `TValue < _Inspect`
including static methods
- Add optional `TValue` argument to `Container#add`, `Nodes::Node#add`, and `Nodes::Raw#add_to_children_tree`
- Add optional `Array[TValue?]` argument to `Container#concat`
- Add new `|| raise`s because the inline type conversion to non-nil doesn't work anymore for `steep` check 🤷🏻‍♂️
- Make compatible with `rbs` `v3.7.0` and `steep` `v1.9.0`
- Change `ProviderCollection#[]` to return TProvider?
- Change `Nodes::Node#[]` to return Nodes::Node[TValue]?
- Change `Container#[]` to return Nodes::Node[TValue]?
- Add type annotations for `UnannotatedEmptyCollection`s
- Raise `InvalidOperation`s when `compress(child)` return nil value which is not supposed to be possible

## 2.5.1 [compare][compare_v2_5_0_and_v2_5_1]

Expand Down Expand Up @@ -1224,6 +1258,7 @@ Most of these help with the gem's overall performance.
[github_pull_81]: https://github.com/gonzedge/rambling-trie/pull/81
[github_pull_82]: https://github.com/gonzedge/rambling-trie/pull/82
[github_pull_83]: https://github.com/gonzedge/rambling-trie/pull/83
[github_pull_85]: https://github.com/gonzedge/rambling-trie/pull/85
[github_user_agate]: https://github.com/agate
[github_user_as181920]: https://github.com/as181920
[github_user_godsent]: https://github.com/godsent
Expand Down
2 changes: 1 addition & 1 deletion lib/rambling/trie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def create filepath = nil, reader = nil
# Available formats are `yml`, `marshal`, and `zip` versions of all the
# previous formats. You can also define your own.
# @param [String] filepath the file to load the words from.
# @param [Serializer, nil] serializer the object responsible of loading the trie from disk.
# @param [Serializer, nil] serializer the object responsible for loading the trie from disk.
# @return [Container] the trie just loaded.
# @yield [Container] the trie just loaded.
# @see Rambling::Trie::Serializers Serializers.
Expand Down
20 changes: 16 additions & 4 deletions lib/rambling/trie/compressor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,41 @@ def compress node
# @param [Nodes::Node] node the node to compress.
# @return [Nodes::Compressed] node the compressed version of the node.
def compress_only_child_and_merge node
compressed_child = compress(node.first_child) # : Nodes::Compressed
compressed_child = compress(node.first_child) || raise(InvalidOperation, 'got nil while compressing only child')
merge node, compressed_child
end

def merge node, other
letter = node.letter.to_s << other.letter.to_s

compressed = Rambling::Trie::Nodes::Compressed.new letter.to_sym, node.parent, other.children_tree
compressed.terminal! if other.terminal?
if other.terminal?
compressed.terminal!
value = other.value
compressed.value = value if value
end
compressed
end

def compress_children_and_copy node
children_tree = compress_children(node.children_tree)
compressed = Rambling::Trie::Nodes::Compressed.new node.letter, node.parent, children_tree
compressed.terminal! if node.terminal?
if node.terminal?
compressed.terminal!
value = node.value
compressed.value = value if value
end
compressed
end

def compress_children tree
# @type var new_tree: Hash[Symbol, Nodes::Node]
new_tree = {}

tree.each { |letter, child| new_tree[letter] = compress child }
tree.each do |letter, child|
compressed_child = compress(child) || raise(InvalidOperation, "got nil while compressing #{letter}")
new_tree[letter] = compressed_child
end

new_tree
end
Expand Down
14 changes: 9 additions & 5 deletions lib/rambling/trie/container.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def initialize root, compressor
# @raise [InvalidOperation] if the trie is already compressed.
# @see Nodes::Raw#add
# @see Nodes::Compressed#add
def add word
root.add reversed_char_symbols word
def add word, value = nil
root.add reversed_char_symbols(word), value
end

# Adds all provided words to the trie.
Expand All @@ -37,8 +37,12 @@ def add word
# @raise [InvalidOperation] if the trie is already compressed.
# @see Nodes::Raw#add
# @see Nodes::Compressed#add
def concat words
words.map { |word| add word }
def concat words, values = nil
if values
words.each_with_index.map { |word, index| add(word, values[index]) }
else
words.map { |word| add word }
end
end

# Compresses the existing trie using redundant node elimination.
Expand Down Expand Up @@ -209,7 +213,7 @@ def words_within_root phrase
end

def compress_root
compressor.compress root # : Nodes::Compressed
compressor.compress(root) || raise
end

def reversed_char_symbols word
Expand Down
3 changes: 2 additions & 1 deletion lib/rambling/trie/enumerable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ module Enumerable
include ::Enumerable

# Empty enumerator constant for early each exits.
EMPTY_ENUMERATOR = [].to_enum :each
EMPTY_ENUMERATOR = [] # : Array[String]
.to_enum :each

# Returns number of words contained in the trie
# @see https://ruby-doc.org/3.3.0/Enumerable.html#method-i-count Enumerable#count
Expand Down
7 changes: 6 additions & 1 deletion lib/rambling/trie/inspectable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@ def class_name
def attributes
[
letter_inspect,
value_inspect,
terminal_inspect,
children_inspect,
].join ', '
].compact.join ', '
end

def letter_inspect
"letter: #{letter.inspect}"
end

def value_inspect
value && "value: #{value.inspect}"
end

def terminal_inspect
"terminal: #{terminal.inspect}"
end
Expand Down
4 changes: 2 additions & 2 deletions lib/rambling/trie/nodes/compressed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module Rambling
module Trie
module Nodes
# A representation of a node in an compressed trie data structure.
# A representation of a node in a compressed trie data structure.
# :reek:RepeatedConditional { max_ifs: 4 }
class Compressed < Rambling::Trie::Nodes::Node
# Creates a new compressed node.
Expand All @@ -22,7 +22,7 @@ def initialize letter = nil, parent = nil, children_tree = {}
# @param [String] _ the word to add to the trie.
# @raise [InvalidOperation] if the trie is already compressed.
# @return [void]
def add _
def add _, _ = nil
raise Rambling::Trie::InvalidOperation, 'Cannot add word to compressed trie'
end

Expand Down
4 changes: 4 additions & 0 deletions lib/rambling/trie/nodes/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class Node
# @return [Node, nil] the parent of the current node.
attr_accessor :parent

# Arbitrary value stored in this node
# @return [TValue, nil] the parent of the current node.
attr_accessor :value

# Creates a new node.
# @param [Symbol, nil] letter the Node's letter value.
# @param [Node, nil] parent the parent of the current node.
Expand Down
13 changes: 8 additions & 5 deletions lib/rambling/trie/nodes/raw.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ class Raw < Rambling::Trie::Nodes::Node
# @param [Array<Symbol>] reversed_chars the char array to add to the trie, in reverse order.
# @return [Node] the added/modified node based on the word added.
# @note This method clears the contents of the chars variable.
def add reversed_chars
def add reversed_chars, value = nil
if reversed_chars.empty?
terminal! unless root?
unless root?
self.value = value
terminal!
end
self
else
add_to_children_tree reversed_chars
add_to_children_tree reversed_chars, value
end
end

Expand All @@ -27,10 +30,10 @@ def compressed?

private

def add_to_children_tree chars
def add_to_children_tree chars, value = nil
letter = chars.pop || raise
child = children_tree[letter] || new_node(letter)
child.add chars
child.add chars, value
child
end

Expand Down
3 changes: 3 additions & 0 deletions sig/lib/nilable.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface _Nilable
def nil?: -> bool
end
18 changes: 8 additions & 10 deletions sig/lib/rambling/trie.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,24 @@ module Rambling
module Trie
VERSION: String

@properties: Configuration::Properties
def self.create: [TValue < _Inspect] (?String?, ?Readers::Reader?) ?{ (Container[TValue]) -> void } -> Container[TValue]

def self.create: (?String?, ?Readers::Reader?) ?{ (Container) -> void } -> Container
def self.load: [TValue < _Inspect] (String, ?Serializers::Serializer[Nodes::Node[TValue]]?) ?{ (Container[TValue]) -> void } -> Container[TValue]

def self.load: (String, ?Serializers::Serializer[Nodes::Node]?) ?{ (Container) -> void } -> Container
def self.dump: [TValue < _Inspect] (Container[TValue], String, ?Serializers::Serializer[Nodes::Node[TValue]]?) -> void

def self.dump: (Container, String, ?Serializers::Serializer[Nodes::Node]?) -> void

def self.config: ?{ (Configuration::Properties) -> void } -> Configuration::Properties
def self.config: [TValue < _Inspect] ?{ (Configuration::Properties[TValue]) -> void } -> Configuration::Properties[TValue]

private

def self.properties: -> Configuration::Properties
def self.properties: [TValue < _Inspect] -> Configuration::Properties[TValue]

def self.readers: -> Configuration::ProviderCollection[Readers::Reader]

def self.serializers: -> Configuration::ProviderCollection[Serializers::Serializer[Nodes::Node]]
def self.serializers: [TValue < _Inspect] -> Configuration::ProviderCollection[Serializers::Serializer[Nodes::Node[TValue]]]

def self.compressor: -> Compressor
def self.compressor: [TValue < _Inspect] -> Compressor[TValue]

def self.root_builder: -> ^() -> Nodes::Node
def self.root_builder: [TValue < _Inspect] -> ^() -> Nodes::Node[TValue]
end
end
6 changes: 3 additions & 3 deletions sig/lib/rambling/trie/comparable.rbs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Rambling
module Trie
module Comparable
def ==: (Nodes::Node) -> bool
module Comparable[TValue < _Inspect]
def ==: (Nodes::Node[TValue]) -> bool

private

Expand All @@ -11,7 +11,7 @@ module Rambling

def terminal?: -> bool

def children_tree: -> Hash[Symbol, Nodes::Node]
def children_tree: -> Hash[Symbol, Nodes::Node[TValue]]
end
end
end
4 changes: 2 additions & 2 deletions sig/lib/rambling/trie/compressible.rbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Rambling
module Trie
module Compressible
module Compressible[TValue < _Inspect]
def compressible?: -> bool

private
Expand All @@ -11,7 +11,7 @@ module Rambling

def terminal?: -> bool

def children_tree: -> Hash[Symbol, Nodes::Node]
def children_tree: -> Hash[Symbol, Nodes::Node[TValue]]
end
end
end
12 changes: 6 additions & 6 deletions sig/lib/rambling/trie/compressor.rbs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
module Rambling
module Trie
class Compressor
def compress: (Nodes::Node?) -> Nodes::Compressed?
class Compressor[TValue < _Inspect]
def compress: (Nodes::Node[TValue]?) -> Nodes::Compressed[TValue]?

private

def compress_only_child_and_merge: (Nodes::Node) -> Nodes::Compressed
def compress_only_child_and_merge: (Nodes::Node[TValue]) -> Nodes::Compressed[TValue]

def merge: (Nodes::Node, Nodes::Node) -> Nodes::Compressed
def merge: (Nodes::Node[TValue], Nodes::Node[TValue]) -> Nodes::Compressed[TValue]

def compress_children_and_copy: (Nodes::Node) -> Nodes::Compressed
def compress_children_and_copy: (Nodes::Node[TValue]) -> Nodes::Compressed[TValue]

def compress_children: (Hash[Symbol, Nodes::Node]) -> Hash[Symbol, Nodes::Node]
def compress_children: (Hash[Symbol, Nodes::Node[TValue]]) -> Hash[Symbol, Nodes::Node[TValue]]
end
end
end
10 changes: 5 additions & 5 deletions sig/lib/rambling/trie/configuration/properties.rbs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
module Rambling
module Trie
module Configuration
class Properties
class Properties[TValue < _Inspect]
attr_reader readers: ProviderCollection[Readers::Reader]
attr_reader serializers: ProviderCollection[Serializers::Serializer[Nodes::Node]]
attr_accessor compressor: Compressor
attr_accessor root_builder: ^() -> Nodes::Node
attr_reader serializers: ProviderCollection[Serializers::Serializer[Nodes::Node[TValue]]]
attr_accessor compressor: Compressor[TValue]
attr_accessor root_builder: ^() -> Nodes::Node[TValue]
attr_accessor tmp_path: String

def initialize: -> void
Expand All @@ -15,7 +15,7 @@ module Rambling
private

attr_writer readers: ProviderCollection[Readers::Reader]
attr_writer serializers: ProviderCollection[Serializers::Serializer[Nodes::Node]]
attr_writer serializers: ProviderCollection[Serializers::Serializer[Nodes::Node[TValue]]]

def reset_readers: -> void

Expand Down
6 changes: 1 addition & 5 deletions sig/lib/rambling/trie/configuration/provider_collection.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ module Rambling
module Trie
module Configuration
class ProviderCollection[TProvider < _Nilable]
interface _Nilable
def nil?: -> bool
end

@providers: Hash[Symbol, TProvider]

attr_reader name: Symbol
attr_reader default: TProvider?

def initialize: (Symbol, Hash[Symbol, TProvider], ?TProvider?) -> void

def []: (Symbol) -> TProvider
def []: (Symbol) -> TProvider?

def add: (Symbol, TProvider) -> TProvider

Expand Down
Loading

0 comments on commit e62bbee

Please sign in to comment.