{"id":5832,"date":"2021-04-07T07:00:34","date_gmt":"2021-04-07T14:00:34","guid":{"rendered":"http:\/\/writeasync.net\/?p=5832"},"modified":"2021-04-07T09:16:38","modified_gmt":"2021-04-07T16:16:38","slug":"letter-boxed-rust-impl-part-4-complete-solver","status":"publish","type":"post","link":"http:\/\/writeasync.net\/?p=5832","title":{"rendered":"Letter Boxed: Rust impl, part 4 (complete solver)"},"content":{"rendered":"<p>After some <a href=\"http:\/\/writeasync.net\/?p=5828\">incremental progress in our Letter Boxed solver<\/a>, we&#8217;re now ready to complete the app.<\/p>\n<p>Our hard-won &#8220;expertise&#8221; in Rust now makes the last several steps largely mechanical. The first set of changes produces <code>LetterBox<\/code> and <code>Vertices<\/code> structs, so that we can <a href=\"https:\/\/github.com\/brian-dot-net\/games\/commit\/ac0ee9a30cb89d3d291afbf828a54e7104b0a439\">parse the actual Letter Boxed puzzle<\/a>:<\/p>\n<pre class=\"brush: rust; title: ; notranslate\" title=\"\">\r\npub struct Vertices(u16);\r\n\r\nimpl Index&lt;u8&gt; for Vertices {\r\n    type Output = bool;\r\n\r\n    fn index(&amp;self, index: u8) -&gt; &amp;Self::Output {\r\n        match (self.0 &gt;&gt; index) &amp; 0x1 {\r\n            0 =&gt; &amp;false,\r\n            _ =&gt; &amp;true,\r\n        }\r\n    }\r\n}\r\n\r\nimpl Add for Vertices {\r\n    type Output = Vertices;\r\n\r\n    fn add(self, rhs: Self) -&gt; Self::Output {\r\n        Vertices(self.0 | rhs.0)\r\n    }\r\n}\r\n\r\nimpl Display for Vertices {\r\n    fn fmt(&amp;self, f: &amp;mut Formatter) -&gt; fmt::Result {\r\n        write!(f, &quot;{:012b}&quot;, self.0)\r\n    }\r\n}\r\n\r\npub struct LetterBox(St);\r\n\r\nimpl LetterBox {\r\n    pub fn new(b: St) -&gt; LetterBox {\r\n        if b.len() != 12 {\r\n            panic!(&quot;Out of range&quot;);\r\n        }\r\n\r\n        LetterBox(b)\r\n    }\r\n\r\n    fn next(&amp;self, index: u8) -&gt; Vertices {\r\n        match index {\r\n            0 | 1 | 2 =&gt; Vertices(0b111111111000),\r\n            3 | 4 | 5 =&gt; Vertices(0b111111000111),\r\n            6 | 7 | 8 =&gt; Vertices(0b111000111111),\r\n            9 | 10 | 11 =&gt; Vertices(0b000111111111),\r\n            _ =&gt; panic!(&quot;Out of range&quot;),\r\n        }\r\n    }\r\n}\r\n\r\nimpl Display for LetterBox {\r\n    fn fmt(&amp;self, f: &amp;mut Formatter) -&gt; fmt::Result {\r\n        self.0.fmt(f)\r\n    }\r\n}\r\n\r\nimpl Index&lt;u8&gt; for LetterBox {\r\n    type Output = Ch;\r\n\r\n    fn index(&amp;self, index: u8) -&gt; &amp;Self::Output {\r\n        &amp;self.0&#x5B;index]\r\n    }\r\n}\r\n<\/pre>\n<p>Yes, it is true that Rust has a <a href=\"https:\/\/crates.io\/crates\/bitvec\">crate for supporting bit vectors<\/a>, which would perhaps render <code>Vertices<\/code> redundant. But we&#8217;ll go with a hand-rolled version for now, to match the C# solver implementation.<\/p>\n<p>Next, we need the data and algorithms for searching for words, and thus, solving the puzzle. We&#8217;ll start with the <code>LetterBoxWords<\/code> collection, and its private implementation detail <code>Word<\/code>:<\/p>\n<pre class=\"brush: rust; title: ; notranslate\" title=\"\">\r\n#&#x5B;derive(Eq, PartialEq)]\r\nstruct Word {\r\n    word: St,\r\n    verts: Vertices,\r\n}\r\n\r\nimpl Word {\r\n    fn last(&amp;self) -&gt; Ch {\r\n        self.word&#x5B;self.word.len() - 1]\r\n    }\r\n\r\n    fn is_solution_with(&amp;self, other: &amp;Word) -&gt; bool {\r\n        let v = self.verts + other.verts;\r\n        v.is_complete()\r\n    }\r\n}\r\n\r\nimpl Hash for Word {\r\n    fn hash&lt;H: std::hash::Hasher&gt;(&amp;self, state: &amp;mut H) {\r\n        self.word.hash(state)\r\n    }\r\n}\r\n\r\npub struct LetterBoxWords {\r\n    words: HashMap&lt;Ch, HashSet&lt;Word&gt;&gt;,\r\n    count: usize,\r\n}\r\n\r\nimpl LetterBoxWords {\r\n    fn new() -&gt; LetterBoxWords {\r\n        let words = HashMap::new();\r\n        let count = 0;\r\n        LetterBoxWords { words, count }\r\n    }\r\n\r\n    fn insert(&amp;mut self, word: St, verts: Vertices) {\r\n        let k = word&#x5B;0];\r\n        let w = Word { word, verts };\r\n        if let Some(v) = self.words.get_mut(&amp;k) {\r\n            if v.insert(w) {\r\n                self.count += 1;\r\n            }\r\n        } else {\r\n            let mut v = HashSet::new();\r\n            v.insert(w);\r\n            self.words.insert(k, v);\r\n            self.count += 1;\r\n        }\r\n    }\r\n\r\n    fn find&lt;F&gt;(&amp;self, mut found: F)\r\n    where\r\n        F: FnMut(St, St),\r\n    {\r\n        for w1 in self.words.values().flat_map(|v| v) {\r\n            if let Some(v) = self.words.get(&amp;w1.last()) {\r\n                for w2 in v {\r\n                    if w1.is_solution_with(&amp;w2) {\r\n                        found(w1.word, w2.word);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    fn len(&amp;self) -&gt; usize {\r\n        self.count\r\n    }\r\n}\r\n<\/pre>\n<p>Let&#8217;s pause for a few observations. Only <code>LetterBoxWords<\/code> is public here which makes <code>Word<\/code> akin to a <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/csharp\/programming-guide\/classes-and-structs\/nested-types\">private inner type<\/a> in the C# version. We also see usage of <a href=\"https:\/\/doc.rust-lang.org\/std\/iter\/trait.Iterator.html#method.flat_map\">flat_map<\/a> as the equivalent of <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/system.linq.enumerable.selectmany?view=net-5.0\">LINQ SelectMany<\/a>.<\/p>\n<p>At last, we are almost done. Since Rust allows <a href=\"https:\/\/www.reddit.com\/r\/rust\/comments\/2xfsre\/why_arent_there_more_nonmember_functions\/\">non-member functions<\/a> (AKA &#8220;<a href=\"https:\/\/stackoverflow.com\/questions\/4861914\/what-is-the-meaning-of-the-term-free-function-in-c\">free functions<\/a>&#8220;) directly at the module level, we can finish off the <code>search<\/code> module with a <code>search(...)<\/code> function. No ceremony or artificial class wrappers here:<\/p>\n<pre class=\"brush: rust; title: ; notranslate\" title=\"\">\r\npub fn search&lt;F&gt;(trie: &amp;StTrie, b: LetterBox, mut found: F)\r\nwhere\r\n    F: FnMut(St, Vertices),\r\n{\r\n    for v1 in 0..12 {\r\n        let c = b&#x5B;v1];\r\n        let verts = Vertices::new(1 &lt;&lt; v1);\r\n        search_next(St::empty() + c, v1, verts, &amp;trie, &amp;b, &amp;mut found);\r\n    }\r\n}\r\n\r\nfn search_next&lt;F&gt;(str: St, v1: u8, verts: Vertices, trie: &amp;StTrie, b: &amp;LetterBox, found: &amp;mut F)\r\nwhere\r\n    F: FnMut(St, Vertices),\r\n{\r\n    let kind = trie.find(str);\r\n    if kind == NodeKind::None {\r\n        return;\r\n    }\r\n\r\n    if kind == NodeKind::Terminal {\r\n        found(str, verts);\r\n    }\r\n\r\n    if str.len() == 12 {\r\n        return;\r\n    }\r\n\r\n    let next = b.next(v1);\r\n    for v2 in 0..12 {\r\n        if next&#x5B;v2] {\r\n            let c = b&#x5B;v2];\r\n            let next_verts = verts + Vertices::new(1 &lt;&lt; v2);\r\n            search_next(str + c, v2, next_verts, &amp;trie, b, found);\r\n        }\r\n    }\r\n}\r\n<\/pre>\n<p>In both code snippets there is an <a href=\"https:\/\/doc.rust-lang.org\/std\/ops\/trait.FnMut.html\"><code>FnMut<\/code> type constraint<\/a>, essentially allowing us to pass any closure we want with <a href=\"https:\/\/zhauniarovich.com\/post\/2020\/2020-12-closures-in-rust\/#variable-capture-modes\">any capture mode<\/a>. In practice, we&#8217;ll <a href=\"https:\/\/github.com\/brian-dot-net\/games\/commit\/5e525a5899a78a66e243424efc285cee89db69db\">use <code>LetterBoxWords<\/code> in the <code>search<\/code> closure<\/a>, and <a href=\"https:\/\/github.com\/brian-dot-net\/games\/commit\/3df62fcf803263e68ff926b524920d95618ac56e\">a simple <code>println!<\/code> macro in the <code>LetterBoxWords::find<\/code> closure<\/a>. Here is the final Rust solver app:<\/p>\n<pre class=\"brush: rust; title: ; notranslate\" title=\"\">\r\nuse letter_boxed_solver::{\r\n    core::{LetterBox, St},\r\n    search::{LetterBoxWords, search},\r\n    trie::StTrie\r\n};\r\nuse std::{env::args, fs::File, time::Instant};\r\n\r\nfn main() {\r\n    let start = Instant::now();\r\n    let args: Vec&lt;String&gt; = args().skip(1).collect();\r\n    if args.len() != 2 {\r\n        println!(&quot;Please specify a Letter Boxed puzzle and a word list file.&quot;);\r\n        return;\r\n    }\r\n\r\n    let b = LetterBox::new(args&#x5B;0].parse::&lt;St&gt;().unwrap());\r\n\r\n    log(start, &quot;Loading trie...&quot;);\r\n    let mut stream = File::open(&amp;args&#x5B;1]).unwrap();\r\n    let trie = StTrie::load(&amp;mut stream);\r\n    log(start, format!(&quot;Loaded {} words.&quot;, trie.len()).as_str());\r\n\r\n    let mut words = LetterBoxWords::new();\r\n\r\n    log(start, &quot;Finding valid words...&quot;);\r\n    search(&amp;trie, b, |w, v| words.insert(w, v));\r\n    log(start, format!(&quot;Found {} valid words.&quot;, words.len()).as_str());\r\n\r\n    log(start, &quot;Finding solutions...&quot;);\r\n    words.find(|w1, w2| println!(&quot;{}-{}&quot;, w1, w2));\r\n\r\n    log(start, &quot;Done.&quot;);\r\n}\r\n\r\nfn log(start: Instant, message: &amp;str) {\r\n    println!(&quot;&#x5B;{:07.3}] {}&quot;, start.elapsed().as_secs_f64(), message);\r\n}\r\n<\/pre>\n<p>Was it all worth it? Let&#8217;s check the timing results for our two benchmark puzzles:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n&gt; letter_boxed_solver.exe RMEWCLTGKAPI words.txt\r\n&#x5B;000.000] Loading trie...\r\n&#x5B;000.039] Loaded 162309 words.\r\n&#x5B;000.039] Finding valid words...\r\n&#x5B;000.042] Found 754 valid words.\r\n&#x5B;000.042] Finding solutions...\r\nMARKETPLACE-EARWIG\r\nWIGWAM-MARKETPLACE\r\nPRAGMATIC-CAKEWALK\r\n&#x5B;000.044] Done.\r\n\r\n&gt; letter_boxed_solver.exe ERGBIDNCFTAO words.txt\r\n&#x5B;000.000] Loading trie...\r\n&#x5B;000.039] Loaded 162309 words.\r\n&#x5B;000.039] Finding valid words...\r\n&#x5B;000.043] Found 1436 valid words.\r\n&#x5B;000.043] Finding solutions...\r\nDOGCART-TENEBRIFIC\r\n -- snipped 72 other solutions --\r\nNONBRAND-DEFECTING\r\n&#x5B;000.072] Done.\r\n<\/pre>\n<p>That&#8217;s right, the Rust implementation is at least 10% faster than <a href=\"http:\/\/writeasync.net\/?p=5802\">the best C++ benchmark<\/a>. Thanks, Rust &#8212; you&#8217;ve more than earned that oh-so-rare <strong>unqualified success<\/strong> badge!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>After some incremental progress in our Letter Boxed solver, we&#8217;re now ready to complete the app. Our hard-won &#8220;expertise&#8221; in Rust now makes the last several steps largely mechanical. The first set of changes produces LetterBox and Vertices structs, so&hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[91,101,41],"tags":[],"class_list":["post-5832","post","type-post","status-publish","format-standard","hentry","category-design","category-native","category-tdd"],"_links":{"self":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5832","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5832"}],"version-history":[{"count":2,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5832\/revisions"}],"predecessor-version":[{"id":5834,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5832\/revisions\/5834"}],"wp:attachment":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5832"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5832"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5832"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}