Cheating at Wordle (AKA, intro to array filters in Javascript)

A new category! The Programming category is a place for talking about specific code. Tutorials and help requests welcome, in the language of your choice. In this post we’ll look at a simple but fundamental aspect of Javascript programming: filtering arrays.

:warning: :mega: This post uses today’s Wordle as an example and is thus a spoiler! If you care about that fact, go do it before reading the rest of this post! :sweat_smile:

Filtering arrays is one of the most fundamental programming tasks. It shows up in all kinds of contexts. In this short tutorial, I’ll introduce some basics of array filtering in Javascript, taking as an example key life skill: how to cheat at Wordle.

One way to think about solving Wordle is as a process of whittling down an array to all the words that could possibly fit, given what you know
from previous guesses.

We have to assume a starting list of possible solution words, which of course must have five letters.

The list I have (from before Wordle hit the big time was just a viral website) contains 2315 words.

You can access a copy of the data by going to this page and opening your console:

https://docling.land/data/wordle.html

Let’s call that array words. It starts like this:

let words = [ "rural", "judge", "artsy", "guppy", "dopey", … ]

I don’t know about you but I like to start with ADIEU. Here’s the result I got:

First guess: ADIEU

So, what does this tell us? The green A means “right letter, right slot”: so we know that the first letter is A. There are a few ways we could “say” that in Javascript. The first thing that popped into my head was to use the [.startsWith](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith) string method, which returns true if a word starts with the string passed in as an argument. Like this:

"artsy".startsWith("A")

That’s true, of course, whereas:

"rural".startsWith("A")

…is false. So our goal is simply to do this for all our possible solutions — which words start with an A? Obviously, we don’t want to write this 2315 times!! Instead, we want to “save” this procedure as something that we can do over and over. The way we do that in programming is to define a function, which is simply a procedure that we give a name to.

You can think of a function like a recipe which has three key parts:

  1. The ingredients
  2. The steps
  3. The dish

To make a chocolate chip cookie :cookie:, you need flour, chocolate :chocolate_bar:, eggs :egg:, etc. So if we reimagine our recipe as a Javascript function, it would look something like this:

let makeCookie = ingredients => {
   // do steps with ingredients
   // call the result cookie, and “return” it
   return cookie
}
More about this function

There are two ways to write functions in Javascript, I’m using the more modern version called an “arrow function”, named for the => “arrow”.

I’m afraid the docs on MDN are not very helpful for a beginner, as they start with obscure details instead of basics.

This just the recipe, we haven’t made any cookies yet! To do that, we have to call the function. Our recipe metaphor is going to break down pretty quickly but “calling” our function would look something like this:

let ourNewCookie = makeCookie("flour", "chocolate"…)

Suffice it to say that we call or execute or run or invoke the function by simply typing its name and passing in the expected ingredients. Functions can return a value (although they don’t have to), and that value is like the dish. Note that in this case we’re “catching” the result by declaring a variable ourNewCookie and assigning the resulting value to it.

Finding words that start with A

Alas, enough with cookies. Back to Wordle. Recall that we want to define a procedure which will tell us if a word starts with A. We can call that, verbosely enough, starstWithA. Here’s how we can write that function:

let startsWithA = word => {
  return word.startsWith("a")
}

Very simple! Now, we can cook up a result by calling the function on with a particular word string as our ingredient:

let notAnAWord = startsWithA("rural")
let anAWord = startsWithA("artsy")

Now notAnAWord contains false and anAWord contains true.

So we have a named procedure which can return true or false based on our defined condition. What remains is to “filter out” only those words in our words array which meet that condition. How do we do that? Why, we use the .filter method! Like this:

words.filter(startsWithA)

And Javascript tells us that the resulting array has just 141 words! Wow, this is some good cheating.

Functions as arguments

But wait a second, I hear you say. startsWithA is a function, it’s not an “ingredient”! When we call startsWithA, we “pass in” a string as the argument. String as an ingredient, okay, that seems intuitive. But how can the procedure itself be an argument?

Welp, functions can be arguments! This is called a first-class function, which is one of the nice features of Javascript. In programming languages with first-class functions, we can pass functions around as variables, just like we pass around strings or arrays or whatever else.

Remember, the words variable is assigned to an array, in this case, a array of strings. In the same way that a string can “do” startsWith, more or less like this:

<your string here>.startsWith(<your string here>)

…an array can “do” filtering, roughly:

<your array here>.filter(<your filtering function here>)

The proper nomenclature here is to say that .startsWith() is a method of strings, and .filter() is a method of arrays. This is kind of deep topic, but for now you can just think of methods as being functions that are “attached” to another data type, which you can call “on” them.

(The best way to absorb this stuff is not to try to memorize nomenclature! It’s to practice with examples!)

So anyway, what actually happens is this. Let’s try a simpler list of words, just some fruit:

let fruits = ["apple", "avocado", "banana", "orange"]

We can run our startsWithA on this array, and when we do, our filter function is called in turn on each item in the array.

Here’s the “call” again:

fruits.filter(startsWithA)

Imagine you are the fruits array. Your programmer tells you to use your filter method to go through your items, and filter out the items that return true when they are passed into that function.

So you have this internal dialog with yourself… First off, you know that the job of your filter method is to create a new array. And you’re going to progressively fill with your items which pass the test. Yes, the return value of your filter method is a new array!

startsWithA("apple")  // Yep, keep it.
startsWithA("avocado")   // Yep, keep it.
startsWithA("banana")   // Nope, not the droid I’m looking for.
startsWithA("orange") // Nope again.

Or more schematically:

input string startsWithA?
:apple: "apple" true
:avocado: "avocado" true
:banana: "banana" false
:tangerine: "orange" false

The emoji are for decoration. :wink:

You keep running the test the programmer passed in as an argument on your items. The items that return a true value when run through the test function added to the new array, and those that don’t are not. Simple as that.

In the end, you will return a new array which contains two strings:

["apple", "avocado"]

Back to cheating at Wordle…

But wait, we have also learned more from our first guess than that fact that our word starts with a. We have learned that it doesn’t contain any of d, i, e, or u.

We’ll introduce a few more details of Javascript syntax to represent this knowledge.

First, there is a string method .includes() which does what you probably expect: it returns true or false depending on whether the string includes the argument as a substring. So:

"apple".includes("a") // true
"apple".includes("p") // true
"apple".includes("x") // false

Logical NOT

But we want to know which of the words in the words array do not include certain letters (d, i, e, or u) now. To get that idea, we can use the logical NOT, which is written in Javascript and many other languages as an exclamation point. It will just reverse our previous results:

!"apple".includes("a") // false
!"apple".includes("p") // false
!"apple".includes("x") // true

So we could write a new function, doesntIncludeWithD, like this:

let doesntIncludeD = word => {
  return !word.includes("d")
}

Now we test our array with that:

words.filter(doesntIncludeD)

…and that gives us an array with 1945 words. Hmm. Not as effective as our previous startsWithA query… but of course, we want to do both of those filters, right?

We want to filter with startsWithA, and then filter the result of that with doesntIncludeD.

We could do this:

let wordThatDontStartWithA = words.filter(startsWithA)
let wordThatDontStartWithAOrIncludeD = wordThatDontStartWithA.filter(doesntIncludeD)

And now we’re down to 115 words. Meh. Are we really going to go along naming all these interim arrays like this? That’s a lot of typing for not much progress. Can we do better?

Anonymous functions

We have been using named functions, because we have assigned our arrow functions to variables. But Javascript also allows us to use “anonymous” functions, which aren’t assigned to a variable name. It’s easiest to just see what I mean:

words
  .filter(word => {
    return word.startsWith("a")
  })

That’s exactly equivalent to:

let startsWithA = word => {
  return word.startsWith("a")
}
words
  .filter(startsWithA)

Compact arrow functions

I can’t resist squeezing one more bit of syntax in here, the “compact arrow function”, because it’s so dang pretty. If your function only contains a single line, you can dispense with the brackets and the return keyword, like this:

let startsWithA = word => word.startsWith("a")

So tidy! And, such a function can also be used anonymously, which cleans up our filter considerably:

words
  .filter(word => word.startsWith("a"))

:heart_eyes:

You’ll notice that I’ve indented the filter call here on its own line. That’s because it’s a nice way to start method chaining, which will look like this:

words
  .filter(word => word.startsWith("a"))
  .filter(word => !word.includes("d"))

And of course, we can keep going:

words
  .filter(word => word.startsWith("a"))
  .filter(word => !word.includes("d"))
  .filter(word => !word.includes("i"))
  .filter(word => !word.includes("e"))
  .filter(word => !word.includes("u"))

We’re down to 43 words!

Here’s where I share my embarrassingly bad second guess (hey, I hadn’t had :coffee: yet…):

Second guess: ARDOR

That was bad because I knew d was bad but I used a word containing it anyway. Sheesh. But at least we learned that r was no good. Let’s add that to our chain:

words
  .filter(word => word.startsWith("a"))
  .filter(word => !word.includes("d"))
  .filter(word => !word.includes("i"))
  .filter(word => !word.includes("e"))
  .filter(word => !word.includes("u"))
  .filter(word => !word.includes("r"))

25 words!

Next guess was somewhat better:

Third guess: ANGST

So we want to remove n, g, and s, and we know that t can’t be the last letter, but that there must be a t…. Let’s add those:

words
  .filter(word => word.startsWith("a"))
  .filter(word => !word.includes("d"))
  .filter(word => !word.includes("i"))
  .filter(word => !word.includes("e"))
  .filter(word => !word.includes("u"))
  .filter(word => !word.includes("r"))
  .filter(word => !word.includes("n"))
  .filter(word => !word.includes("g"))
  .filter(word => !word.includes("s"))
  .filter(word => word.includes("t"))
  .filter(word => !word.endsWith("t"))

Okay y’all, we’re down to just two words:

[
  "aptly",
  "atoll"
]

Dang, that is some high-quality cheating, but you can do the last step yourselves. :wink:

What’s next?

Well, this code could be improved, but it does what it says on the tin. The notion of searching an array with a chain of filters is a very powerful one, and has many applications in language documentation.

2 Likes

Yay, functional programming!

1 Like