🧱 Core logic and data modeling

Write a function,stubs.Guess.score,with the following signature:

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 calledfromTextortoMyTypewhich create values of a given type, for examplelib.base.Text.toBagorNat.toInt.We should have those for our Wordle guess and target types too!

Choose your data representations wisely!

🎥How to define a function
🎥How to define a function

See how to write a simple Unison function together:

📚How search for functions by type and with wildcards
📚How search for functions by type and with wildcards

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-basedfindcommand.

When you want to search by type, thefindcommand is followed by a colon:and then a type signature.

.> find : List a -> Set a

  1. .base.Set.fromList : [k] -> Set k
.> find : Text -> [Char]

  1. .base.Text.toCharList : Text -> [Char]

You might also try employing wildcard operators to help scope your search further. Say I wanted to find all the functions under theListnamespace inbase.If I typefind Listin the UCM, I'll receive a bunch of results that include the wordListin their name, but aren't scoped to the namespace. Instead, we can use?to represent any results following the given prefix.

.> find .base.List.?

  1.   structural type .base.List.Nonempty a
  2.   .base.List.++ : [a] -> [a] -> [a]
  3.   .base.List.+: : a -> [a] -> [a]
  4.   .base.List.:+ : [a] -> a -> [a]
  [...]
.> find .base.List.delete?

  1. .base.List.deleteAt : Nat -> [a] -> [a]
  2. .base.List.deleteAt.doc : Doc.Deprecated
  3. .base.List.deleteAt.test : [Test.Result]
  4. .base.List.deleteFirst : (a ->{g} Boolean) -> [a] ->{g} [a]
  [...]
Data type definition suggestions
Data type definition suggestions

The user and target word will initially be entered as typeTextvalues, but dealing with the rawTextvalues might not be optimal for making the kinds of comparisons needed to produce a result.

Why not create three types that contain variouscollection typesand types frombaseto model this domain?

  1. ATargetdata 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.
  2. AGuessdata type which contains information about the guessed word's characters and their locations.
  3. AResultdata type which represents the result of comparing a
                                                          type
                                                                   Char
    from theGuesswith theTarget.

Resources

Solutions

🔑An implementation walk through
🔑An implementation walk through

One possible way to model the domain:

type basic.Guess
type basic.Target

Let's look how we might transform a raw text value into our respectivebasic.Guessandbasic.Targettypes.

First, we'll write aText.normalizefunction to lowercase and strip whitespace from the incoming text. We don't want thebasic.Guessto fail to match thebasic.Targetbecause of casing or whitespace issues.

Text.normalize : Text -> Text
Text.normalize input =
  trim input
    |> toCharList
    |> lib.base.data.List.map ascii.toLower
    |> fromCharList

Then we break down theTextbody into aListofCharwith thetoCharListfunction frombase.Working in theListtype allows us to calllib.base.data.List.indexedso 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 |> lib.base.data.List.indexed
  basic.Guess.Guess charList

We do the same sequence of transformations for thebasic.Targettype, with the additional step of callinglib.base.data.Set.fromListto create aSetwhich 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.

basic.Target.fromText : Text -> basic.Target
basic.Target.fromText input =
  normalized = Text.normalize input
  charList = toCharList normalized
  charsWithIndex =
    charList
      |> lib.base.data.List.indexed
      |> lib.base.data.Set.fromList
  mapChars = countLetters charList
  basic.Target.Target mapChars charsWithIndex

Next we'll break down the overallbasic.Guess.scorefunction implementation.

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
          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) =
      lib.base.data.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 newbasic.Result.InPlaceto 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 isbasic.Result.NotFound.If there are still occurrences left, then the new result isbasic.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.

Next steps

👉Render a colorized result to the console