Learning (better) Dart with Advent of Code (and ChatGPT)

13Dec22

TL;DR

I’ve been using Advent of Code as a way to practice Dart, try out ChatGPT, and learn from how other people approach the problems. ChatGPT quickly disappointed, but there’s still been plenty to learn, and I’ve found some things I’ll definitely take into my future coding.

Also (value judgement here) I’m finding it much easier to reason about what other people’s Dart is doing versus what other people’s Rust is doing.

Inspiration

Tim Sneath (the Director of Product and UX for Flutter & Dart at Google) tweeted that he was running a leaderboard for people doing Advent of Code with Dart.

Let’s do this! If you want to participate in Advent of Code 2022 with Dart, join our private leaderboard: https://adventofcode.com/2022/leaderboard

Use code 1671198-6c003f39. Who knows, we might even find some swag for the most committed! #AdventOfCode2022
@dart_lang #Flutter

Tim Sneath

Then I came across Simon Willison’s post about Learning Rust with ChatGPT, Copilot and Advent of Code, and I thought ‘maybe I should try that with Dart’.

It didn’t take long for ChatGPT to go astray

I probably wasn’t as thorough as Simon in documenting my ChatGPT interactions, but like him I kept them in GitHub Issues.

Things started off pretty well in Day 1, with a decent enough example of reading files.

import 'dart:io';

void main() {
  // Open the file for reading
  var file = File('my_file.txt');

  // Read the file line by line
  var lines = file.readAsLinesSync();

  // Print each line to the console
  for (var line in lines) {
    print(line);
  }
}

The first signs of trouble came on Day 3, with ChatGPT offering a sample that looked OK, but resulted in a type mismatch.

Then on Day 5 I found ChatGPT making stuff up that doesn’t exist. Perhaps it would be nice if dart:collection had a Stack class, but it doesn’t. Maybe more worrying was how authentic looking the documentation was for the fictional class.

Since then I’ve occasionally tried to get help from ChatGPT on error messages etc., but service has become pretty unreliable under the huge load that’s been put onto it.

Learning from Tim’s code

Tim has been posting his solutions on GitHub, and I’ve been taking a look at how I might sharpen up my own use of Dart.

Directory structure

The first thing that hit me is the directory structure. Dart (and Flutter) projects tend to be a certain shape, and this keeps to that form. I should probably find (or make myself) a Dart boilerplate repo to start things off properly.

Reading the input file

Tim uses a standard approach to reading the input:

void main(List<String> args) {
  final path = args.isNotEmpty ? args[0] : 'data/day01.txt';
  final data = File(path).readAsLinesSync();

This defaults to reading the day’s input.txt from its appropriate file (in the directory structure already mentioned), but allows easy overriding to test against example input or something else. He also doesn’t dive headlong into tearing the file into lines, but passes that object around whole to other classes that iterate across it.

Final everywhere

I see final all over the place in Tim’s code, where I’m accustomed to seeing various variable declarations.

In some cases this use obviously fits into the pattern of “A final variable can be set only once”, with the path and data example above illustrating that. But there are other times I found myself scratching my head a little, like final in an iterator:

for (final row in data) {
  if (row.isNotEmpty) {
    currentCalories += int.parse(row);

For that block to work it’s quite obvious that the value of row is changing on each iteration, but what is ‘final’ is the amount of memory that needs to be reserved for each row.

Final and lists can also be counterintuitive, but then I find this example in the docs for the Dart type system:

void printInts(List<int> a) => print(a);

void main() {
  final list = <int>[];
  list.add(1);
  list.add(2);
  printInts(list);
}

It doesn’t mean that an immutable list is being created (that wouldn’t be very useful), but rather that each element of the list is immutable, which is a useful cue to the runtime in terms of memory allocation. Of course the use of final in lists is related to using final with lists (or iterables) in an iterator.

Deeply nested operators

There’s a lot going on in something like:

int countContainedIntervals(Iterable<String> rows) =>
    rows.map(convertRaw).map(isIntervalContained).where((e) => e).length;

But each chained operation gets us towards the desired outcome.

Conclusion

After some initial success ChatGPT has been a disappointment (and I suspect it has better training material coming from stuff relating to Rust’s learning cliff), but it’s always worth spending some time reading other people’s code to see what and how they do things differently, and why that might be better.