Skip to Content

Advent of Code 2021 - Day 2, in Kotlin - Dive!

Kotlin solutions to parts 1 and 2 of Advent of Code 2021, Day 2: 'Dive!'

Posted on

There are a lot of ways to solve today’s puzzle, so I’ve picked a way that I think helps us learn a few functional programming concepts we’ll see in future puzzles.

If you’d rather just view code, the GitHub Repository is here .

Problem Input

We’re given our submarine commands one per line in a file. We’ll use our helper method to read the file from the classpath and turn it into a List<String>. I don’t generally show this code as it doesn’t change from day to day, but you can review it here in GitHub.

Because we’ll need to parse each of the rows further, let’s define a new class to represent that data and the parsing logic. We’ll call it Command and make it private within our Day02 class. This will prevent us from accidentally reusing it in a future puzzle.

class Day02(input: List<String>) {

    private val commands = input.map { Command.of(it) }

    private class Command(val name: String, val amount: Int) {
        companion object {
            fun of(input: String) = input.split(" ").let { Command(it.first(), it.last().toInt()) }
        }
    }
}

I’d like to go over the work being done in Command. We’ll define a function called of in our companion so we can call Command.of(someString) and get a Command object in return. I like this pattern because (to me) it is clear that I’m exchanging data in one format (a String) for data in another format (a Command). Other than that, the Command doesn’t do anything else except hold data for us.

If you are familiar with Kotlin’s data classes, you might be wondering why we didn’t make Command a data class. The answer is that we don’t actually need any of the features of a data class. We don’t care about any of the default functions data classes provide because we won’t be storing them in a Map or printing them out. Additionally, we won’t be making copies of these commands. We just want it to hold data and a regular class will do that just fine.

As for the parsing, we’ll split our input String on a single space. One great aspect of Advent of Code is that we don’t have to worry so much about real-world issues (what if there is more than one space, or a tab?). We can safely assume that Advent of Code is giving us good input. Once we have our String split into a List<String>, we’ll create a Command using the first() and last() elements in the List<String>.

We’ll parse all of our input and store it in a list called commands. I’ve left the type off here to make things easier to read, but commands is a List<Command> if that wasn’t clear.

⭐ Day 2, Part 1

The puzzle text can be found here.

As I said above, there are a lot of different solutions we could go with for this puzzle. Today, we’re going to define a Submarine class that knows how to move itself and return us a new representation of itself. As with Command, we’ll make this private within Day02. Unlike Command, we’ll make this a data class because we do want one of its features - copy. Let’s define our class and then we’ll go over it.

// In Day02

private data class Submarine(
  val depth: Int = 0, 
  val position: Int = 0
) {
    fun answer() = depth * position

    fun movePart1(command: Command): Submarine =
        when(command.name) {
            "forward" -> copy(position = position + command.amount)
            "down" -> copy(depth = depth + command.amount)
            "up" -> copy(depth = depth - command.amount)
            else -> error("Invalid command")
        }
}

When we create our Submarine for part 1, we only care about its depth and position (spoiler: we’ll be modifying this for part 2!). We can use these values to calculate an answer() as well. We’ve defined answer() as a function here rather than a property because we don’t always want it calculated, only at the end when we’ve applied all of the commands. If we had defined it as a property (which would totally work) the Kotlin compiler would have to calculate the answer for every Submarine we create, even for the ones we don’t care about the answer for.

Let’s direct our attention to movePart1. The name might give it away but this handles movement of the Submarine given a Command for part 1 of the puzzle. We’ll have a different movement logic for part 2. When the Submarine gets a Command it is going to use when on the name of the command to figure out what is being asked. For each type of command, we’ll always create a copy of our Submarine. Technically, we’re creating and discarding a lot of objects. However, we don’t have that many commands, and this pattern is fairly common in functional languages. This pattern is nice because it allows us to make Submarine immutable. You’ll notice there’s no way to change the Submarine, we only ever create new ones. I wanted to show you this pattern today, while the puzzles are not as complicated as later in the month, as I suspect we’ll see it again.

For each possible command (forward, up, or down), we will create a new Submarine using the copy function that Kotlin data classes give us for free. We don’t have to write that. The copy function is nice because we can use named parameters to tell Kotlin which parts we want to be different. For example, when handling the “forward” command, we only want to recalculate the position, not the depth. If we don’t tell copy anything about depth, that property will remain the same in the resulting copy.

We could have made Command.name an enumeration but I elected to keep it as a String because the problem is so small today. Turning name into an enum is something you can do yourself, as practice, if you’ve never done it before. If you do make Command.name an enum, you can get rid of the else clause here because Kotlin is smart enough to detect that every case is handled in the when expression.

Now that we have our Submarine and know how to use it, we can use a fold to solve the puzzle.

// In Day02

fun solvePart1(): Int =
    commands.fold(Submarine()) { submarine, command -> submarine.movePart1(command) }.answer()

Let’s go over that. I suspect we’ll see fold again in future puzzles as it is a common tool in both Kotlin and functional languages in general. In this case, fold operates over our commands (which is a List<Command>). For every Command, it is passed into a lambda function along with some state variable that we care about. The state we care about is the Submarine. When we start the fold, we give it the initial state of the Submarine by instantiating one (Submarine()). In the lambda, we are given both the current state (submarine) and the next command in the list. When we have these, we use them to move the Submarine by calling movePart1 and passing the command into it. Because our movePart1 function returns a Submarine, this become the state for the next time the lambda is called.

For those of you familiar with function references, we could have written our solution like this, but I found it easier to explain fold the other way.

// Alternate version, using a reference to a function 

fun solvePart1(): Int =
    commands.fold(Submarine(), Submarine::movePart1).answer()

Once we’ve reached the end of our fold we have a Submarine in the final position (all commands applied), so we call answer() on it to get the solution to part 1!

Star earned! Onward!

⭐ Day 2, Part 2

The puzzle text can be found here.

We only have to learn one more thing in order to solve part 2, and that’s how to add aim to our Submarine. Thankfully, Kotlin has the concept of default values. You’ve already seen and used them when we created our Submarine for part 1. Because of this feature, we can add aim on with a default value and not change our code for part 1 at all.

While we’re in the Submarine class, we can also define a new movement function for part 2. All other aspects of Submarine stay the same.

private data class Submarine(
  val depth: Int = 0, 
  val position: Int = 0, 
  val aim: Int = 0      // NEW!
) {
                                                                
    // answer() and movePart1() are the same

    fun movePart2(command: Command) =
        when (command.name) {
            "forward" -> copy(
                position = position + command.amount,
                depth = depth + (aim * command.amount)
            )
            "down" -> copy(aim = aim + command.amount)
            "up" -> copy(aim = aim - command.amount)
            else -> error("Invalid command")
        }
}

As you can see, movePart2 looks a lot like movePart1 except we have aim to contend with now. Materially, the way we interpret the Command and return a new Submarine has not changed. We can use this to calculation the solution to part 2.

// In Day02

fun solvePart2(): Int =
    commands.fold(Submarine()) { submarine, command -> submarine.movePart2(command) }.answer()

Star earned!

My Challenge To You!

Up for a challenge? Define a single solve function in Day02 and pass either a function reference or a lambda to it. Have solve handle the fold and applying the passsed-in movement logic. Call solve from both solvePart1 and solvePart2.

Further Reading

  1. Index of All Solutions - All posts and solutions for 2021, in Kotlin.
  2. My Github repo - Solutions and tests for each day.
  3. Solution - Full code for day 2
  4. Advent of Code - Come join in and do these challenges yourself!