Advent of Code 2021 - Day 2, in Kotlin - Dive!
Kotlin solutions to parts 1 and 2 of Advent of Code 2021, Day 2: 'Dive!'
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 madeCommand.name
an enumeration but I elected to keep it as aString
because the problem is so small today. Turningname
into anenum
is something you can do yourself, as practice, if you’ve never done it before. If you do makeCommand.name
anenum
, you can get rid of theelse
clause here because Kotlin is smart enough to detect that every case is handled in thewhen
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
- Index of All Solutions - All posts and solutions for 2021, in Kotlin.
- My Github repo - Solutions and tests for each day.
- Solution - Full code for day 2
- Advent of Code - Come join in and do these challenges yourself!