🧱 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 exampleBag.fromTextorNat.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:

unique type basic.Guess
unique type basic.Guess = Guess [(Char, Nat)]
unique type basic.Target
unique type basic.Target = Target (Set Char) (Set (Char, Nat))
unique type basic.Result
unique type basic.Result = NotFound Char | Exists Char | InPlace Char

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 |> List.map ascii.toLower |> fromCharList

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

We do the same sequence of transformations for thebasic.Targettype, with the additional step of callingSet.fromListto create twoSetsβ€”onewhich includes the indices of the characters, and one without.

basic.Target.fromText : Text -> basic.Target
basic.Target.fromText input =
  use Set fromList
  normalized = Text.normalize input
  charList = toCharList normalized
  charsWithIndex = charList |> indexed |> fromList
  setChars = fromList charList
  basic.Target.Target setChars 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 =
  (basic.Guess.Guess list) = guess
  List.map (t -> basic.characterResult t target) list

For each character and index tuple in the user'sbasic.Guess,we call thebasic.characterResultfunction. It checks whether the tuple isbasic.inLocation,or if it merelybasic.existsin the target.

basic.characterResult : (Char, Nat) -> basic.Target -> basic.Result
basic.characterResult tuple target =
  match tuple with
    tup 
      | basic.inLocation tup target -> basic.Result.InPlace (at1 tup)
      | basic.exists tup target     -> basic.Result.Exists (at1 tup)
      | otherwise                   ->
        basic.Result.NotFound (at1 tup)

The two boolean functionsbasic.inLocationandbasic.existsboth useSet.containsin their implementations.

basic.inLocation : (Char, Nat) -> basic.Target -> Boolean
basic.inLocation tup = cases
  basic.Target.Target _ indexedChars -> Set.contains tup indexedChars
basic.exists : (Char, Nat) -> basic.Target -> Boolean
basic.exists tup target =
  (char, index) = tup
  (basic.Target.Target set _) = target
  Set.contains char set

Next steps

πŸ‘‰Render a colorized result to the console