diff options
| author | s-ol <s-ol@users.noreply.github.com> | 2019-10-14 13:31:30 +0000 |
|---|---|---|
| committer | s-ol <s-ol@users.noreply.github.com> | 2019-10-14 13:31:30 +0000 |
| commit | 75b3bc0b385ea1fb9eda3e29e5b0414b91015356 (patch) | |
| tree | b5b326de40179a517ad93b6bb026cf480bc86b75 | |
| parent | new type/pathfinding algorithm (diff) | |
| download | mmm-75b3bc0b385ea1fb9eda3e29e5b0414b91015356.tar.gz mmm-75b3bc0b385ea1fb9eda3e29e5b0414b91015356.zip | |
add ba_log entry 2019-10-14
| -rw-r--r-- | root/articles/mmmfs/ba_log/2019-10-14/text$markdown.md | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/root/articles/mmmfs/ba_log/2019-10-14/text$markdown.md b/root/articles/mmmfs/ba_log/2019-10-14/text$markdown.md new file mode 100644 index 0000000..e42f8be --- /dev/null +++ b/root/articles/mmmfs/ba_log/2019-10-14/text$markdown.md @@ -0,0 +1,101 @@ +I finally added a better type-conversion-path-finding algorithm \[[`deb21aa`][deb21aa]\]. +The old algorithm only took into consideration how many steps it took to get from type A (stored on disk) to type B (requested). +This could lead to problems, for example in this situation: + +On Disk: `text/moonscript -> fn -> mmm/dom` (a moonscript file that contains a function that returns a bit of UI) +Requested: `mmm/dom` +Known Conversions: +1. `text/moonscript` to `mmm/dom` (displays the source code with highlighting) +2. `text/moonscript -> ???` to `???` (evaluates the moonscript file) +3. `fn -> ???` to `???` (calls the function, providing access to children and other facets) + +Since conversion 1 only takes a single step, it would have been preferred by the old algorithm (although there were workarounds for this). +The new algorithm adds the concept of conversion-cost, that has to be specified for each conversion. +The conversions 2 and 3 now have a cost of `1`, while conversion 1 has a cost of `5`. +The conversions are simply added up and the path with the lowest cost is chosen. +Like in other pathfinding applications like digital games, the cost metric is also used by the algorithm to enhance the search itself, +it prioritises searching further on the path with the least current cost. + +To implement this optimization I implemented a priority queue: + + -- a priority queue with an index + -- only one element with a given key may exist at a time + -- when an element with an existing key is added, + -- the element with lower priority survives. + class Queue + new: => + @values = {} + @index = {} + + -- add a value with a given priority to the queue + -- if no key is specified, assume the element is uniq + add: (value, priority, key) => + entry = { :value, :key, :priority } + + if key + if old_entry = @index[key] + -- already have an entry for this key + -- if it is lower priority, we leave it there and do nothing + if old_entry.priority < priority + return + + -- otherwise we remove the old one and continue as normal + -- find the index of the old entry + local i + for ii, entry in ipairs @values + if entry == old_entry + i = ii + break + + -- remove it + table.remove @values, i + + -- store this entry in the index + @index[key] = entry + + -- store lowest priority last + for i, v in ipairs @values + if v.priority < priority + -- i is the first key that is lower, + -- we want to insert right before it + table.insert @values, i, entry + return + + -- couldn't find a key with a lower priority, + -- so insert at the end + table.insert @values, entry + + peek: => + entry = @values[#@values] + if entry + { :value, :priority, :key } = entry + @index[key] = nil if key + value, priority + + pop: => + entry = table.remove @values + if entry + { :value, :priority, :key } = entry + @index[key] = nil if key + value, priority + + -- iterator, yields (value, priority), low priority first + poll: => @.pop, @ + + { + :Queue + } + +This priority queue behaves mostly as expected, but I added an extra feature to make sure that only the +best conversion path to a specific type is considered for further searching: +When adding a new element to the Queue, an extra `key` can be passed. +If a key is passed, the Queue makes sure that there is only ever one element with that key in the queue. +When a second element would be introduced, the queue discards whichever element has a higher priority value. + +This way I can use the type that a conversion path leads to as the key, +and the queue will automatically discard a worse solution if a better one is found that leads to the same type result. + +To make sure the Queue implementation was solid, I also added unit tests for it: [`spec/queue_spec.moon`][spec] + +[deb21aa]: https://git.s-ol.nu/mmm/commit/deb21aa43fe8bf11eb276803973b272913b7e716/ +[spec]: https://git.s-ol.nu/mmm/blob/deb21aa43fe8bf11eb276803973b272913b7e716/spec/queue_spec.moon |
