exercism

Exercism - Food Chain

This post shows you how to get Food Chain exercise of Exercism.

Stevinator Stevinator
14 min read
SHARE
exercism dart flutter food-chain

Preparation

Before we click on our next exercise, let’s see what concepts of DART we need to consider

Food Chain Exercise

So we need to use the following concepts.

Enums with Fields

Enums can have fields that store data for each enum value. They’re perfect for representing animals with their names and reactions.

enum AnimalType {
  fly('fly', ''),
  spider('spider', 'It wriggled and jiggled and tickled inside her.'),
  bird('bird', 'How absurd to swallow a bird!');
  
  final String name;
  final String reaction;
  
  const AnimalType(this.name, this.reaction);
}

void main() {
  AnimalType animal = AnimalType.spider;
  print(animal.name); // "spider"
  print(animal.reaction); // "It wriggled and jiggled and tickled inside her."
}

Enum Values

Enum values can be accessed using .values, which returns a list of all enum values in declaration order. You can index into this list to get specific values.

enum AnimalType {
  fly, spider, bird, cat, dog, goat, cow, horse;
}

void main() {
  // Get all enum values
  List<AnimalType> allAnimals = AnimalType.values;
  print(allAnimals); // [AnimalType.fly, AnimalType.spider, ...]
  
  // Access by index
  AnimalType first = AnimalType.values[0]; // fly
  AnimalType second = AnimalType.values[1]; // spider
  
  // Use in loops
  for (int i = 0; i < AnimalType.values.length; i++) {
    AnimalType animal = AnimalType.values[i];
    print(animal);
  }
}

Const Constructors

Const constructors allow enum values to have constant fields. The const keyword ensures the values are compile-time constants.

enum AnimalType {
  fly('fly', ''),
  spider('spider', 'It wriggled and jiggled and tickled inside her.');
  
  final String name;
  final String reaction;
  
  // Const constructor
  const AnimalType(this.name, this.reaction);
}

void main() {
  // Values are compile-time constants
  AnimalType animal = AnimalType.fly;
  print(animal.name); // "fly"
}

Enum Getters

Enums can have getter properties that compute values based on the enum’s fields or identity. They’re perfect for checking conditions.

enum AnimalType {
  fly('fly', ''),
  spider('spider', 'It wriggled and jiggled and tickled inside her.'),
  horse('horse', "She's dead, of course!");
  
  final String name;
  final String reaction;
  
  const AnimalType(this.name, this.reaction);
  
  // Getters
  bool get hasReaction => reaction.isNotEmpty;
  bool get isFinal => this == AnimalType.horse;
  bool get needsSuffix => this == AnimalType.spider;
}

void main() {
  AnimalType spider = AnimalType.spider;
  print(spider.hasReaction); // true
  print(spider.needsSuffix); // true
  
  AnimalType fly = AnimalType.fly;
  print(fly.hasReaction); // false
}

List addAll() Method

The addAll() method adds all elements from an iterable to a list. It’s perfect for adding multiple lines to a verse.

void main() {
  List<String> lines = [];
  
  // Add single element
  lines.add('Line 1');
  
  // Add multiple elements
  lines.addAll(['Line 2', 'Line 3', 'Line 4']);
  print(lines); // [Line 1, Line 2, Line 3, Line 4]
  
  // Add from another list
  List<String> moreLines = ['Line 5', 'Line 6'];
  lines.addAll(moreLines);
  print(lines); // [Line 1, Line 2, Line 3, Line 4, Line 5, Line 6]
}

List isNotEmpty Property

The isNotEmpty property checks if a list has at least one element. It’s the opposite of isEmpty.

void main() {
  List<String> lines = [];
  
  // Check if not empty
  if (lines.isNotEmpty) {
    lines.add(''); // Add separator
  }
  
  lines.add('New line');
  
  // Use for adding separators
  List<String> result = [];
  for (int i = 0; i < 3; i++) {
    if (result.isNotEmpty) result.add(''); // Add separator between verses
    result.addAll(['Line 1', 'Line 2']);
  }
}

List sublist() Method

The sublist() method creates a new list containing elements from a specified range. It’s perfect for getting animals up to a certain verse.

void main() {
  List<String> animals = ['fly', 'spider', 'bird', 'cat', 'dog'];
  
  // Get sublist from start to index
  List<String> firstThree = animals.sublist(0, 3);
  print(firstThree); // ['fly', 'spider', 'bird']
  
  // Get sublist from start to end
  List<String> all = animals.sublist(0);
  print(all); // ['fly', 'spider', 'bird', 'cat', 'dog']
  
  // Use with enum values
  List<AnimalType> allAnimals = AnimalType.values;
  List<AnimalType> upToVerse3 = allAnimals.sublist(0, 3);
  print(upToVerse3); // [fly, spider, bird]
}

Reverse Iteration

Reverse iteration processes elements from the end to the beginning. It’s achieved by starting from the last index and decrementing.

void main() {
  List<String> animals = ['fly', 'spider', 'bird', 'cat'];
  
  // Iterate backwards
  for (int i = animals.length - 1; i > 0; i--) {
    String current = animals[i];
    String previous = animals[i - 1];
    print('$current catches $previous');
    // cat catches bird
    // bird catches spider
    // spider catches fly
  }
  
  // Use for building chains
  List<String> chain = [];
  for (int i = animals.length - 1; i > 0; i--) {
    chain.add('${animals[i]} catches ${animals[i - 1]}');
  }
}

String Interpolation

String interpolation allows you to embed expressions and variables directly within strings using ${expression} or $variable.

void main() {
  String animal = 'spider';
  String previous = 'fly';
  
  // Basic interpolation
  String line = "She swallowed the $animal to catch the $previous.";
  print(line); // "She swallowed the spider to catch the fly."
  
  // Expression interpolation
  int verse = 3;
  String intro = "I know an old lady who swallowed a ${AnimalType.values[verse - 1].name}.";
  print(intro); // "I know an old lady who swallowed a bird."
  
  // Conditional in interpolation
  bool needsSuffix = true;
  String suffix = needsSuffix ? ' that wriggled and jiggled and tickled inside her' : '';
  String line2 = "She swallowed the spider to catch the fly$suffix.";
  print(line2); // "She swallowed the spider to catch the fly that wriggled and jiggled and tickled inside her."
}

Helper Methods

Helper methods (often private methods starting with _) encapsulate reusable logic, making code more organized and maintainable.

class FoodChain {
  // Public method
  List<String> recite(int startVerse, int endVerse) {
    List<String> result = [];
    for (int verse = startVerse; verse <= endVerse; verse++) {
      if (result.isNotEmpty) result.add('');
      result.addAll(_buildVerse(verse)); // Use helper
    }
    return result;
  }
  
  // Private helper method
  List<String> _buildVerse(int verseNumber) {
    // Build single verse
    return ['Line 1', 'Line 2'];
  }
}

For Loops with Range

For loops can iterate through a range of numbers. They’re perfect for generating multiple verses.

void main() {
  List<String> result = [];
  int startVerse = 1;
  int endVerse = 3;
  
  // Loop through verse range
  for (int verse = startVerse; verse <= endVerse; verse++) {
    if (result.isNotEmpty) result.add(''); // Add separator
    result.addAll(buildVerse(verse));
  }
  
  // Example: verses 1, 2, 3
  // verse 1: add verse 1 lines
  // verse 2: add '', then verse 2 lines
  // verse 3: add '', then verse 3 lines
}

Conditional Logic

Conditional statements allow you to execute different code based on conditions. They’re essential for handling special cases like the final verse or animals with reactions.

void main() {
  AnimalType animal = AnimalType.horse;
  
  // Check if final verse
  if (animal.isFinal) {
    print(animal.reaction); // "She's dead, of course!"
    return; // No chain for final verse
  }
  
  // Check if has reaction
  if (animal.hasReaction) {
    print(animal.reaction);
  }
  
  // Check if needs special suffix
  AnimalType previous = AnimalType.spider;
  String suffix = previous.needsSuffix
      ? ' that wriggled and jiggled and tickled inside her'
      : '';
  print("She swallowed the bird to catch the spider$suffix.");
}

Introduction

Generate the lyrics of the song ‘I Know an Old Lady Who Swallowed a Fly’.

While you could copy/paste the lyrics, or read them from a file, this problem is much more interesting if you approach it algorithmically.

This is a cumulative song of unknown origin.

This is one of many common variants.

Example Verses

Verse 1:

I know an old lady who swallowed a fly.
I don't know why she swallowed the fly. Perhaps she'll die.

Verse 2:

I know an old lady who swallowed a spider.
It wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.

Verse 3:

I know an old lady who swallowed a bird.
How absurd to swallow a bird!
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.

Verse 8 (Final):

I know an old lady who swallowed a horse.
She's dead, of course!

Instructions

Your task is to generate the lyrics algorithmically. The song follows a pattern:

  • Each verse introduces a new animal
  • Most verses include a reaction line
  • Verses build a chain backwards (newest animal catches previous)
  • Spider needs special suffix when mentioned in chain
  • Final verse (horse) has no chain, just reaction

How do we generate the food chain lyrics?

To generate the lyrics:

  1. Define animals: Create enum with all animals, their names, and reactions
  2. Build verses: For each verse number:
    • Get the animal for that verse
    • Add intro line (“I know an old lady who swallowed a [animal]”)
    • If final verse (horse): add reaction and return
    • If has reaction: add reaction line
    • Build chain backwards: iterate from current animal down to fly
    • Add special suffix for spider when it’s the previous animal
    • Add final line (“I don’t know why she swallowed the fly…”)
  3. Combine verses: Add empty line between verses, combine all lines

The key insight is using reverse iteration to build the chain backwards, and using enum getters to handle special cases (reactions, final verse, spider suffix).

Solution

enum AnimalType {
  fly('fly', ''),
  spider('spider', 'It wriggled and jiggled and tickled inside her.'),
  bird('bird', 'How absurd to swallow a bird!'),
  cat('cat', 'Imagine that, to swallow a cat!'),
  dog('dog', 'What a hog, to swallow a dog!'),
  goat('goat', 'Just opened her throat and swallowed a goat!'),
  cow('cow', "I don't know how she swallowed a cow!"),
  horse('horse', "She's dead, of course!");

  final String name;
  final String reaction;

  const AnimalType(this.name, this.reaction);

  bool get hasReaction => reaction.isNotEmpty;
  bool get isFinal => this == AnimalType.horse;
  bool get needsSuffix => this == AnimalType.spider;
}

class FoodChain {
  List<String> recite(int startVerse, int endVerse) {
    final result = <String>[];

    for (int verse = startVerse; verse <= endVerse; verse++) {
      if (result.isNotEmpty) result.add('');

      result.addAll(_buildVerse(verse));
    }

    return result;
  }

  List<String> _buildVerse(int verseNumber) {
    final animal = AnimalType.values[verseNumber - 1];
    final lines = <String>[];

    lines.add("I know an old lady who swallowed a ${animal.name}.");

    if (animal.isFinal) {
      lines.add(animal.reaction);
      return lines;
    }

    if (animal.hasReaction) {
      lines.add(animal.reaction);
    }

    final animals = AnimalType.values.sublist(0, verseNumber);
    for (int i = animals.length - 1; i > 0; i--) {
      final current = animals[i];
      final previous = animals[i - 1];
      
      final suffix = previous.needsSuffix
          ? ' that wriggled and jiggled and tickled inside her'
          : '';
      
      lines.add("She swallowed the ${current.name} to catch the ${previous.name}$suffix.");
    }

    lines.add("I don't know why she swallowed the fly. Perhaps she'll die.");

    return lines;
  }
}

Let’s break down the solution:

  1. enum AnimalType - Animal enum:

    • Defines all 8 animals in order
    • Each has a name and reaction string
    • Uses const constructor for compile-time constants
  2. fly('fly', '') - Animal definitions:

    • First parameter: animal name
    • Second parameter: reaction line (empty for fly)
    • Example: spider('spider', 'It wriggled...')
  3. final String name and final String reaction - Enum fields:

    • Store data for each enum value
    • Accessible on each enum instance
  4. const AnimalType(this.name, this.reaction) - Const constructor:

    • Makes enum values compile-time constants
    • Initializes name and reaction fields
  5. bool get hasReaction => reaction.isNotEmpty - Reaction getter:

    • Checks if animal has a reaction line
    • Returns true if reaction is not empty
    • Example: fly → false, spider → true
  6. bool get isFinal => this == AnimalType.horse - Final verse getter:

    • Checks if this is the final animal (horse)
    • Used to handle special case (no chain)
  7. bool get needsSuffix => this == AnimalType.spider - Suffix getter:

    • Checks if animal needs special suffix
    • Only spider needs ” that wriggled and jiggled and tickled inside her”
    • Used when spider is the previous animal in chain
  8. class FoodChain - Main class:

    • Encapsulates lyric generation logic
    • Contains public recite() and private _buildVerse() methods
  9. List<String> recite(int startVerse, int endVerse) - Public method:

    • Takes verse range (start to end, inclusive)
    • Returns list of all lines for those verses
    • Adds empty lines between verses
  10. final result = <String>[] - Result list:

    • Creates empty list to collect all lines
    • Will contain lines from all requested verses
  11. for (int verse = startVerse; verse <= endVerse; verse++) - Verse loop:

    • Iterates through each verse in range
    • Inclusive range (includes both start and end)
  12. if (result.isNotEmpty) result.add('') - Add separator:

    • Adds empty line between verses
    • Only adds if result already has content
    • Prevents leading empty line
  13. result.addAll(_buildVerse(verse)) - Add verse lines:

    • Calls helper method to build single verse
    • Adds all lines from that verse to result
    • addAll() adds multiple elements at once
  14. List<String> _buildVerse(int verseNumber) - Verse builder:

    • Private helper method
    • Builds lines for a single verse
    • Returns list of lines for that verse
  15. final animal = AnimalType.values[verseNumber - 1] - Get animal:

    • Gets animal for this verse number
    • Verse 1 → index 0 (fly)
    • Verse 2 → index 1 (spider)
    • Uses - 1 because verses are 1-indexed, arrays are 0-indexed
  16. final lines = <String>[] - Verse lines:

    • Creates list to collect lines for this verse
    • Will be returned at end of method
  17. lines.add("I know an old lady who swallowed a ${animal.name}.") - Intro line:

    • Adds the opening line for the verse
    • Uses string interpolation to include animal name
    • Example: “I know an old lady who swallowed a spider.”
  18. if (animal.isFinal) - Check final verse:

    • If this is the horse (final verse)
    • Special handling: no chain, just reaction
  19. lines.add(animal.reaction) and return lines - Final verse handling:

    • Adds reaction line (“She’s dead, of course!”)
    • Returns immediately (no chain for final verse)
  20. if (animal.hasReaction) - Add reaction:

    • If animal has a reaction line, add it
    • Example: spider → “It wriggled and jiggled and tickled inside her.”
  21. final animals = AnimalType.values.sublist(0, verseNumber) - Get animals up to verse:

    • Gets all animals from start to current verse
    • Example: verse 3 → [fly, spider, bird]
    • Used to build the chain
  22. for (int i = animals.length - 1; i > 0; i--) - Reverse iteration:

    • Iterates backwards through animals
    • Starts from last (current animal)
    • Goes down to index 1 (stops before fly)
    • Example: [fly, spider, bird] → i=2, then i=1
  23. final current = animals[i] and final previous = animals[i - 1] - Get pair:

    • current: animal at position i (being swallowed)
    • previous: animal at position i-1 (being caught)
    • Example: i=2 → current=bird, previous=spider
  24. final suffix = previous.needsSuffix ? '...' : '' - Spider suffix:

    • Checks if previous animal is spider
    • If yes, adds special suffix
    • If no, empty string (no suffix)
  25. lines.add("She swallowed the ${current.name} to catch the ${previous.name}$suffix.") - Chain line:

    • Builds chain line using string interpolation
    • Includes current and previous animal names
    • Adds suffix if previous is spider
    • Example: “She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.”
  26. lines.add("I don't know why she swallowed the fly. Perhaps she'll die.") - Final line:

    • Adds the closing line for non-final verses
    • Always the same for all verses (except final)
  27. return lines - Return verse:

    • Returns all lines for this verse
    • Will be added to result in recite() method

The solution elegantly generates the cumulative song lyrics using enums to represent animals, reverse iteration to build chains backwards, and conditional logic to handle special cases (reactions, final verse, spider suffix). The structure makes it easy to add or modify animals.


A video tutorial for this exercise is coming soon! In the meantime, check out my YouTube channel for more Dart and Flutter tutorials. 😉

Visit My YouTube Channel
Stevinator

Stevinator

Stevinator is a software engineer passionate about clean code and best practices. Loves sharing knowledge with the developer community.