Write a function, Guess.score, with the following signature:
stubs.Guess.score : stubs.Guess -> stubs.Target -> [stubs.Result]Given a 5 letter user guess and the 5 letter target, it should return the guess with each character annotated as one of the following:
- in the correct place
- in the target word but not in the correct place
- not in the target word
We've given this function a signature which suggests we've created our own data types for this step. See the hint below for some example types to use.
In Unison it's common to provide functions called fromText or toMyType which create values of a given type, for example Text.toBag or Nat.toInt. We should have those for our Wordle guess and target types too!
Choose your data representations wisely!
The user and target word will initially be entered as type Text values, but dealing with the raw Text values might not be optimal for making the kinds of comparisons needed to produce a result.
Why not create three types that contain various collection types and types from base to model this domain?
- A
Targetdata type which contains enough information to check for the existence of aCharin the target, and the location (an index perhaps? 😉) of a character in the target.
- A
Guessdata type which contains information about the guessed word's characters and their locations.
type basic.Guesstype basic.Guess
= docs.labs.wordle.solutions.basic.Guess.Guess [(Char, Nat)]Result data type which represents the result of comparing a type Char from the Guess with the Target.type basic.ResultSee how to write a simple Unison function together:
If you ever find yourself wondering "🤔 What's the Unison name for that function which turns a List into a Set?" or "🧐 How do I break apart Text into its constituent characters?" you may want to use the type-based find command.
When you want to search by type, the find command is followed by a colon : and then a type signature.
wordle/main> find : List a -> Set aYou might also try using Unison's view command in combination with fzf to quickly scan through approximate matches to function names. Just enter view in the UCM with no arguments and then start typing the name of a type whose functions you want to view.
Resources
Solutions
One possible way to model the domain:
type basic.Guesstype basic.Guess
= docs.labs.wordle.solutions.basic.Guess.Guess [(Char, Nat)]type basic.ResultLet's look how we might transform a raw text value into our respective basic.Guess and basic.Target types.
First, we'll write a Text.normalize function to lowercase and strip whitespace from the incoming text. We don't want the basic.Guess to fail to match the basic.Target because of casing or whitespace issues.
Text.normalize : Text -> TextText.normalize : Text -> Text
Text.normalize input = Text.trim input |> Text.toLowercaseThen we break down the Text body into a List of Char with the toCharList function from base. Working in the List type allows us to call List.indexed so we can pair the character with its index for position lookup later.
basic.Guess.fromText : Text -> basic.Guess
basic.Guess.fromText input =
normalized = Text.normalize input
charList = toCharList normalized |> List.indexed
basic.Guess.Guess charListWe do the same sequence of transformations for the basic.Target type, with the additional step of calling Set.fromList to create a Set which includes the indices of the characters, and we can define a function to create a mapping of characters to their number of occurrences in the target word.
countLetters : [Char] -> Map Char NatcountLetters : [Char] -> Map Char Nat
countLetters charList =
charList
|> List.sort
|> groupConsecutive
|> List.map (Nonempty.head &&& List.Nonempty.size)
|> Map.fromListbasic.Target.fromText : Text -> basic.Target
basic.Target.fromText input =
normalized = Text.normalize input
charList = toCharList normalized
charsWithIndex = charList |> List.indexed |> Set.fromList
mapChars = countLetters charList
basic.Target.Target mapChars charsWithIndexNext we'll break down the overall basic.Guess.score function implementation.
basic.Guess.score : basic.Guess -> basic.Target -> [basic.Result]basic.Guess.score : basic.Guess -> basic.Target -> [basic.Result]
basic.Guess.score guess target =
use List :+
use Map adjust
use Nat -
use basic Result Target.Target
use basic.Result NotFound
scoreHelper :
(basic.Target, [Result])
-> (Char, Nat)
-> (basic.Target, [Result])
scoreHelper targetAccs = cases
(c, i) ->
(target', acc) = targetAccs
(Target.Target m ls) = target'
if basic.inLocation (c, i) target' then
( Target.Target (adjust (x -> x - 1) c m) ls
, acc :+ basic.Result.InPlace c
)
else
match Map.get c m with
Optional.None -> (target', acc :+ NotFound c)
Some 0 -> (target', acc :+ NotFound c)
Some _ ->
( Target.Target (adjust (x -> x - 1) c m) ls
, acc :+ basic.Result.Exists c
)
(basic.Guess.Guess list) = guess
(_, result) = List.foldLeft scoreHelper (target, []) list
resultWe first define a helper function that can take a single character and its position in the guess, as well as a tuple that represents both the current target and the results of each letter comparison so far. The function then decides which of the result types the current character fits into.
Firstly, it can simply check if the character, position tuple is in the target set, which results in a new target with one less occurrence of the character and a new basic.Result.InPlace to append to the results.
If the character, position tuple is not in the target set, it must then check how many occurrences of the character are in the current target. If there never were any, or there are no more left, then the new result is basic.Result.NotFound. If there are still occurrences left, then the new result is basic.Result.Exists, and we must update the target to reduce the occurrences by 1.
The final score function then becomes simply a foldLeft call over the guess, with the helper function as the folding function. The starting state is the target and an empty list of results.