🧱 Core logic and data modeling

Write a function, 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 called fromText or toMyType which create values of a given type, for example distributed.lib.base.Text.toBag or Nat.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
📚 How search for functions by type

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.

Data type definition suggestions
Data type definition suggestions

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?

  1. A Target data type which contains enough information to check for the existence of a Char in the target, and the location (an index perhaps? 😉) of a character in the target.
  2. A Guess data type which contains information about the guessed word's characters and their locations.
  3. A Result data type which represents the result of comparing a type Char from the Guess with the Target.

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

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 distributed.lib.base.data.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 |> distributed.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 distributed.lib.base.data.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.

basic.Target.fromText : Text -> basic.Target
basic.Target.fromText input =
  normalized = Text.normalize input
  charList = toCharList normalized
  charsWithIndex =
    charList
      |> distributed.lib.base.data.List.indexed
      |> distributed.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 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) =
      distributed.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 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.

Next steps

👉 Render a colorized result to the console