Write a function, Guess.score
, with the following signature:
stubs.Guess.score : stubs.Guess -> stubs.Target -> [stubs.Result]
Given a 5 letter target and a 5 letter user guess, 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. The data constructors for these types currently contain a placeholder type, TODO
, which you can replace with what you think the type should enclose.
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!
See 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 a
1. .base.Set.fromList : [k] -> Set k
wordle/main> find : Text -> [Char]
1. .base.Text.toCharList : Text -> [Char]
You 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.
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
Target
data type which contains enough information to check for the existence of aChar
in the target, and the location (an index perhaps? 😉) of a character in the target. - A
Guess
data type which contains information about the guessed word's characters and their locations. - A
Result
data type which represents the result of comparing atype Char
from theGuess
with theTarget
.
Resources
Solutions
One possible way to model the domain:
type basic.Guess
type basic.Guess
= docs.labs.wordle.solutions.basic.Guess.Guess [(Char, Nat)]
type basic.Result
Let'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 -> Text
Text.normalize : Text -> Text
Text.normalize input =
Text.trim input
|> toCharList
|> jsonschema.lib.base.data.List.map ascii.toLower
|> fromCharList
Then 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 |> jsonschema.lib.base.data.List.indexed
basic.Guess.Guess charList
We 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 Nat
countLetters : [Char] -> Map Char Nat
countLetters charList =
charList
|> List.sort
|> groupConsecutive
|> List.map (Nonempty.head &&& List.Nonempty.size)
|> Map.fromList
basic.Target.fromText : Text -> basic.Target
basic.Target.fromText input =
normalized = Text.normalize input
charList = toCharList normalized
charsWithIndex =
charList
|> jsonschema.lib.base.data.List.indexed
|> jsonschema.lib.base.data.Set.fromList
mapChars = countLetters charList
basic.Target.Target mapChars charsWithIndex
Next 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
)
let
(basic.Guess.Guess list) = guess
(_, result) = List.foldLeft scoreHelper (target, []) list
result
We 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.