dart

Extension Methods in Dart: a Tale of Two Kitties

by Steve Marx on

My family is fostering two adorable kittens / Instagram influencers named Zack and Cody. As responsible cat parents, my wife and I enforce strict bedtimes and limit the kittens’ screen time.

To that end, I wrote a Flutter app to play every cat’s favorite movie, The AristoCats, with a sleep timer. Each night, we launch the app for the kittens and start the timer. Ten minutes later, the movie pauses, and the kittens should be asleep. (The next night we pick up where they left off.)

A mystery

Initially, this system seemed to be working fine. But after a few nights, we noticed that the kittens seemed exhausted in the morning. They also kept quoting lines from parts of the movie they shouldn’t have seen yet—remember that they only watch ten minutes a night, and we’d only had the kittens for a few nights.

I began to suspect something about the sleep timer wasn’t working.1

Debugging

I knew that the kittens, particularly Zack, liked to type on my laptop, as evidenced here:

“Zack” rhymes with “hack”

My first thought was that the kittens had modified the app somehow to disable the sleep timer, but the video was always paused at the right timestamp in the morning. I also checked the git commit history and didn’t see any recent changes.

With no obvious reason for the change in sight, I settled in to read the code carefully, line by line, paying particular attention to the sleep timer code.

Sabotage!

Going through the code carefully, I noticed something curious:

floatingActionButton: FloatingActionButton(
  onPressed: () {
    Timer(Duration(minutes: 10), () {
      _controller.paws();
    });

Did you catch it? It’s subtle, but the line that should read _controller.pause() actually says _controller.paws().

It turns out the code had been modified, but I’d underestimated the kittens! They apparently knew how to use git rebase and git push -f to rewrite commit history so I wouldn’t see their changes.

Extension methods

I was making progress, but now I was confused. VideoPlayerController has no method called paws(), so how was this code even compiling?

It turns out Dart supports something called “extension methods”, which you can find in many other languages, such as C# and Java. Ruby has a particularly interesting mechanism for extension methods, where you can just reopen an existing class. I knew about this because one of our grownup cats, Kit, had written this rather sloppy2 extension years earlier:

require 'active_support/core_ext'

class DateTime
    def self.meow
        # all times should be in Central Africa Time (CAT)
        Time.current.in_time_zone("Africa/Cairo")
    end
end

puts DateTime.meow()

In Python, you can just reassign things directly. Here’s a Python example from our other grownup cat, Shadow:

import humanize

orig = humanize.apnumber
humanize.apnumber = lambda n: 'fur' if n == 4 else orig(n)

for i in range(10):
    print(humanize.apnumber(i))

# Output:
#   zero
#   one
#   two
#   three
#   fur
#   five
#   six
#   seven
#   eight
#   nine

As you can see, this kind of runtime modification makes it hard to know exactly what code is going to run when you call a function. This technique is called “monkey patching” or “duck punching”, unpleasant terms that hopefully dissuade new programmers from abusing this dangerous language feature.

So where is paws() defined?

Dart extension methods only work with static types and are resolved at compile time. That means you can’t do ugly runtime monkey patching, and a good IDE can tell you exactly where an extension method is implemented:

Visual Studio Code to the rescue!

This explains how I had initially missed the code. It was in a file the kittens had cleverly called thumbsAndOtherHumanStuff.dart, which they knew I, as a human, would find perfectly normal.

Below are the contents of that file:

import 'dart:async';

import 'package:video_player/video_player.dart';

extension TheStupidKindOfMouse on VideoPlayerController {
  paws() {
    this.setLooping(true);
    this.setVolume(0.1); // humans have inferior ears

    Timer(this.value.duration * 2, () {
      this.setVolume(1);
      this.pause();
    });
  }
}

The kittens were cleverer than I had given them credit for! Instead of pausing the movie, this code loops the video and reduces the volume to 10%. After two full repeats of the video, the volume is restored to 100% and actually paused. This explains why everything looked right in the morning.

Teaching those kittens a lesson

My first thought was to revert the code to the way it was: _controller.pause(). But the next time the kittens were using my laptop, they would surely just change it to _controller.paws() again.

With my newfound knowledge of extension methods, I thought of a subtler technique. What if I added my own extension method? If I rewrote the git history, the kittens might not notice the change.

My first attempt was to just add another extension method to thumbsAndOtherHumanStuff.dart3:

extension HeyGoToSleep on VideoPlayerController {
  paws() {
    this.pause();
  }
}

But the compiler told me right away that this was a problem:

A member named 'paws' is defined in extensions 'TheStupidKindOfMouse' and
'HeyGoToSleep' and neither is more specific.

Try using an extension override to specify the extension you want to to be
chosen.

I needed to resolve this conflict in a way that was subtle enough the kittens wouldn’t notice. The part of the error message saying “neither is more specific” was intriguing, but it didn’t end up being useful. In the case of inheritance, the “more specific” extension is the one on the more derived class. But both extensions are on VideoPlayerController, so this isn’t helpful.

The Dart extension method documentation has a section about conflicts that described two ways to resolve them. First, you can wrap the object being extended, e.g. HeyGoToSleep(_controller).paws(). I thought this would be too much of a giveaway.

The other option is to make sure only one extension method is visible. I modified the import line like so, to hide the kittens’ extension method:

import 'thumbsAndOtherHumanStuff.dart' hide TheStupidKindOfMouse;

It’s easy to skim past import lines, so I was betting the kittens wouldn’t notice the change.

Success… for now?

The kittens now seem better rested, but they’re showing increased interest in getting time on my laptop, so I’m sure it’s only a matter of time before they come up with some other clever hack. I’ll be sure to post here if it happens.

Summary

  • Dart allows you to extend classes with static methods.
  • These extension methods are fully resolved at compile time, so they don’t have the drawbacks of more dynamic modification in languages like Ruby and Python.
  • If there’s ambiguity, it’s up to the developer to resolve the API conflict explicitly.
  • Kittens are cute, but they’re also devious and not to be trusted.

  1. To address all the hate mail, yes, of course we made sure the app was running on a device outside of the cage so the kittens couldn’t restart the movie themselves. ↩︎

  2. Sloppy because you’d expect DateTime.meow() to return a DateTime, but it actually doesn’t! Kit’s never been a particularly disciplined developer, and it takes a lot of discipline to write good extension methods. ↩︎

  3. I added lots of blank lines so they wouldn’t see. Kittens are notoriously bad at scrolling. ↩︎

Me. In your inbox?

Admit it. You're intrigued.

Subscribe