Beer Song in Cairo: Complete Solution & Deep Dive Guide

macbook pro on brown wooden table

Mastering Cairo Loops & Strings: The Complete Beer Song Tutorial from Zero to Hero

This guide demonstrates how to solve the classic "99 Bottles of Beer" problem using the Cairo programming language. You will learn to implement loops, handle string formatting with arrays, manage conditional logic for pluralization, and structure a complete Cairo program for this recursive song.

You’re staring at the screen, fresh into your Cairo journey. The basic syntax makes sense, but then you hit a wall. How do you actually build something? How do you handle something as seemingly simple as generating repetitive text with slight variations? It’s a common frustration, the gap between knowing the keywords and knowing how to combine them into a working program.

This is where classic coding challenges shine. The "99 Bottles of Beer" problem isn't just a nostalgic song; it's a perfect crucible for forging your core programming skills. In this deep dive, we'll guide you through solving this challenge from the exclusive kodikra.com curriculum, transforming you from a Cairo novice into a confident developer who understands control flow, string manipulation, and function composition.


What is the "Beer Song" Problem?

The "Beer Song" is a programming exercise based on the classic repetitive song, "99 Bottles of Beer on the Wall." The goal is to generate the complete lyrics programmatically. The song starts at 99 bottles and counts down to zero, with a specific verse structure for each number.

The core logic involves a countdown loop, but the complexity lies in handling the grammatical changes and special cases:

  • The General Case (99 down to 3): The verse follows a standard pattern: "X bottles of beer on the wall, X bottles of beer. Take one down and pass it around, X-1 bottles of beer on the wall."
  • The "2 Bottles" Case: When you have 2 bottles, the next line must say "1 bottle" (singular).
  • The "1 Bottle" Case: This verse is unique: "1 bottle of beer on the wall, 1 bottle of beer. Take it down and pass it around, no more bottles of beer on the wall."
  • The "0 Bottles" Case: The final verse breaks the pattern entirely: "No more bottles of beer on the wall, no more bottles of beer. Go to the store and buy some more, 99 bottles of beer on the wall."

Solving this requires careful attention to conditional logic (if/else statements) and string formatting to handle these variations correctly.


Why Use Cairo for This Challenge?

At first glance, using a cutting-edge smart contract language like Cairo for a simple song might seem like overkill. However, it's an incredibly effective learning tool for several reasons specific to the Cairo ecosystem.

First, it forces you to confront Cairo's unique approach to data types and strings. Unlike languages like Python or JavaScript with built-in, flexible string types, Cairo uses felt252 (a field element) as its base. Strings are often represented as short strings fitting into a single felt252 or as arrays of them. This exercise provides practical experience in building and manipulating text in a way that is fundamental to writing Cairo contracts.

Second, it solidifies your understanding of control flow. You'll implement loops and conditional branches, which are the building blocks of any complex logic, whether in a simple script or a secure DeFi protocol. Mastering how to structure if-else if-else blocks is non-negotiable.

Finally, it encourages modular design. The most elegant solution involves breaking the problem down into smaller, manageable functions—one to handle a single verse (verse) and another to orchestrate the entire song (song). This principle of separation of concerns is paramount in writing clean, maintainable, and auditable smart contracts.

This module from the kodikra Cairo learning path is designed to be a practical stepping stone from basic syntax to real-world application logic.


How to Implement the Beer Song in Cairo: A Step-by-Step Guide

We will build our solution logically, starting with the smallest piece—a single verse—and then assembling the full song. This bottom-up approach makes the code easier to reason about and test.

The Core Logic: The `verse` Function

The heart of our program is a function that can generate the correct verse for any given number of bottles. This function, which we'll call verse, will take a number n as input and return the corresponding verse as a string. It needs to contain all the conditional logic for the special cases (2, 1, and 0).

Here is a conceptual flow for our verse function:

    ● Start (Input: n)
    │
    ▼
  ┌───────────────────┐
  │ Check value of 'n'│
  └─────────┬─────────┘
            │
            ▼
    ◆ n == 0 ?
   ╱          ╲
  Yes          No
  │            │
  ▼            ▼
[Generate     ◆ n == 1 ?
"No more"    ╱          ╲
 verse]     Yes          No
             │            │
             ▼            ▼
           [Generate   ◆ n == 2 ?
           "1 bottle"  ╱          ╲
            verse]    Yes          No
                       │            │
                       ▼            ▼
                     [Generate   [Generate
                     "2 bottles"  Default 'n'
                      verse]       verse]
                       │            │
           └───────────┼────────────┘
                       │
           ┌───────────┴────────────┐
           │ Combine & Return Verse │
           └───────────┬────────────┘
                       ▼
                   ● End

The Orchestrator: The `song` Function

Once we have a reliable verse function, creating the full song is straightforward. The song function will take a starting number and an ending number, loop downwards, call `verse` for each number, and concatenate the results with a newline character in between.

This illustrates the overall program flow:

    ● Start (Input: start, end)
    │
    ▼
  ┌───────────────────────┐
  │ Initialize empty song │
  │ array & counter = start│
  └──────────┬────────────┘
             │
             ▼
    ◆ counter >= end ?
   ╱          ╲
  Yes          No
  │            │
  │            └───────────────────► ● Return Final Song
  ▼
┌───────────────────────┐
│ Call verse(counter)   │
└──────────┬────────────┘
           │
           ▼
┌───────────────────────┐
│ Append verse to song  │
└──────────┬────────────┘
           │
           ▼
┌─────────────────────────┐
│ Append newline if not   │
│ the last verse          │
└──────────┬──────────────┘
           │
           ▼
┌───────────────────────┐
│ Decrement counter     │
└──────────┬────────────┘
           │
           └─────────────────────────► ◆ (Loop back)

The Complete Cairo Solution

Now, let's translate this logic into Cairo code. We'll create a module and define our two primary functions. Notice how we use an Array<felt252> to build our strings and then join them.


// This solution is part of the exclusive kodikra.com curriculum.
// It demonstrates core Cairo concepts like loops, conditionals, and string handling.

use array::ArrayTrait;
use option::OptionTrait;
use integer::u32_to_felt252;

fn verse(n: u32) -> Array<felt252> {
    let mut result: Array<felt252> = ArrayTrait::new();

    if n == 0 {
        result.append('No more bottles of beer on the wall, ');
        result.append('no more bottles of beer.\n');
        result.append('Go to the store and buy some more, ');
        result.append('99 bottles of beer on the wall.\n');
    } else if n == 1 {
        result.append('1 bottle of beer on the wall, ');
        result.append('1 bottle of beer.\n');
        result.append('Take it down and pass it around, ');
        result.append('no more bottles of beer on the wall.\n');
    } else if n == 2 {
        result.append('2 bottles of beer on the wall, ');
        result.append('2 bottles of beer.\n');
        result.append('Take one down and pass it around, ');
        result.append('1 bottle of beer on the wall.\n');
    } else {
        // General case for n > 2
        let n_felt = u32_to_felt252(n);
        let n_minus_1_felt = u32_to_felt252(n - 1);

        result.append(n_felt);
        result.append(' bottles of beer on the wall, ');
        result.append(n_felt);
        result.append(' bottles of beer.\n');
        result.append('Take one down and pass it around, ');
        result.append(n_minus_1_felt);
        result.append(' bottles of beer on the wall.\n');
    }

    result
}

fn song(start: u32, end: u32) -> Array<felt252> {
    let mut result: Array<felt252> = ArrayTrait::new();
    let mut current = start;

    loop {
        if current < end {
            break;
        }

        let mut current_verse = verse(current);
        result.append_array(ref current_verse);
        
        // Add a newline between verses, but not after the last one
        if current != end {
            result.append('\n');
        }

        current -= 1;
    };

    result
}

// Example of how you might test this in a test file
#[cfg(test)]
mod tests {
    use super::{verse, song};

    #[test]
    #[available_gas(2000000)]
    fn test_verse_1() {
        let expected = "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n";
        // Note: In a real test, you'd need a helper to compare Array with a string literal.
        // This is a conceptual representation.
    }
}

Detailed Code Walkthrough

  1. Imports: We start by importing necessary traits. ArrayTrait is essential for creating and manipulating arrays (our strings), OptionTrait is often used with array methods, and u32_to_felt252 is a helper for converting numbers to a type that can be appended to our string array.
  2. verse(n: u32) Function:
    • It accepts an unsigned 32-bit integer n.
    • We initialize an empty mutable array result of type Array<felt252>. This array will hold the parts of our verse.
    • An if-else if-else chain handles the logic. The conditions are checked from most specific (n == 0) to most general.
    • For n == 0, n == 1, and n == 2, we hardcode the specific lyrics, appending each part of the verse to the result array. Note the use of \n for newlines.
    • In the final else block (the general case for n > 2), we convert the numbers n and n - 1 to felt252 so they can be appended to the array alongside the string literals.
    • Finally, the function returns the populated result array.
  3. song(start: u32, end: u32) Function:
    • It takes a start and end number for the song range.
    • It initializes its own empty result array to hold the entire song.
    • A mutable variable current is set to start to act as our loop counter.
    • We use a loop construct, which creates an infinite loop that we must manually break out of.
    • The first line inside the loop is our exit condition: if current < end { break; }. This stops the loop once we've processed the `end` verse.
    • Inside the loop, we call verse(current) to get the lyrics for the current number.
    • result.append_array(ref current_verse) efficiently appends the entire verse array to our main song array.
    • A crucial detail: we add a newline separator \n between verses, but only if it's not the very last verse (if current != end). This prevents a trailing newline at the end of the song.
    • current -= 1; decrements the counter, moving us closer to the end condition.
    • The function returns the complete song array.

Running and Testing the Code

To compile and test your Cairo project, you'll use the Scarb toolchain. Scarb is the official package manager and build tool for Cairo.

In your terminal, you would navigate to your project's root directory and run:


# To check for compilation errors
scarb build

# To run the tests defined in your project
scarb test

These commands are fundamental to the Cairo development workflow, ensuring your code is valid and behaves as expected before deploying it or integrating it into a larger application.


Alternative Approaches and Considerations

While the if-else if-else chain is clear and effective, it's worth considering other patterns you might see or use in different contexts.

Using `match` Expressions

Cairo supports match expressions, which can be a more expressive way to handle multiple distinct cases. For the Beer Song, a match statement could replace the if-else chain, which some developers find more readable.


// Conceptual example of using match
fn verse_with_match(n: u32) -> Array<felt252> {
    match n {
        0 => {
            // return verse for 0
        },
        1 => {
            // return verse for 1
        },
        2 => {
            // return verse for 2
        },
        _ => {
            // return general verse for n > 2
        }
    }
}

Recursive Approach

One could also solve the `song` function recursively. A recursive function would call itself with a decremented number until it reaches the base case (the `end` number). While elegant, this is often less efficient in the context of blockchains like Starknet due to gas costs associated with function calls and potential stack depth limitations.

Pros and Cons of Iterative vs. Recursive `song`

Approach Pros Cons
Iterative (loop) - More gas-efficient on-chain.
- Avoids stack depth limits.
- Generally easier to reason about for simple countdowns.
- Can be slightly more verbose with manual counter management.
Recursive - Can be more elegant and concise for problems with recursive structures.
- Follows a functional programming paradigm.
- Higher gas cost due to repeated function call overhead.
- Risk of hitting the call stack limit with large ranges (e.g., 9999 bottles).

For on-chain Starknet development, the iterative approach using a loop is almost always the preferred method for its predictability and efficiency.


Frequently Asked Questions (FAQ)

Why is string manipulation in Cairo so different from other languages?

Cairo's primary data type is the felt252, a 252-bit field element designed for cryptographic operations, not text. Short strings (up to 31 characters) can be packed into a single felt252. For longer strings, Cairo uses arrays of felt252. This design prioritizes computational efficiency for zero-knowledge proofs over the ergonomic string handling found in general-purpose languages. Mastering this array-based approach is a key part of learning Cairo.

What is felt252 and why is it used everywhere?

felt252 stands for "field element 252 bits." It's the native integer type of the Starknet Virtual Machine. Nearly every operation in Cairo ultimately works with felts. They are the foundation of the STARK proofs that secure the network. Even concepts like strings, booleans, and custom structs are abstractions built on top of one or more felt252 values.

Can I use a `for` loop instead of a `while` or `loop`?

Cairo does not have a traditional C-style for loop. The primary looping construct is the loop {} block, which requires a manual break condition. For iterating over data structures, you would typically use other patterns, sometimes involving iterators, but the fundamental countdown logic in this problem is best served by a loop with a decrementing counter.

How do I properly test my Cairo code?

Testing is done using the Scarb framework. You create a module annotated with #[cfg(test)] and write functions annotated with #[test]. Inside these functions, you call your main logic and use assertions (like assert()) to verify the output is correct. Running scarb test will execute all such functions and report any failures. This is a critical practice for ensuring smart contract correctness.

What is the purpose of separating `verse` and `song` functions?

This is a core software design principle called "Separation of Concerns." The verse function has one job: correctly format a single verse. The song function has a different job: assemble multiple verses into a whole. This separation makes the code easier to read, test, and debug. You can test the logic for `verse(1)` independently of the looping logic in `song`.

Is recursion a good way to solve this in Cairo?

While technically possible, recursion is generally discouraged for tasks that can be done iteratively in Cairo, especially for on-chain logic. Every function call consumes gas, and deep recursion can become very expensive. Furthermore, there's a hard limit on the call stack depth to prevent denial-of-service attacks, which a recursive function with a large input could hit. The iterative loop is safer and more efficient.

Where can I learn more about Cairo's fundamentals?

This exercise is a great start. To continue building a strong foundation, we highly recommend exploring the complete Cairo guide on kodikra.com, which covers everything from basic syntax to advanced smart contract patterns.


Conclusion: Your Next Steps in Cairo

Congratulations! By working through the "Beer Song" challenge, you've done more than just print lyrics. You've grappled with fundamental Cairo concepts: control flow with loop and if-else, data manipulation with Array<felt252>, function modularity, and the importance of handling edge cases. These are the skills that separate a passive learner from an active builder.

This problem perfectly encapsulates the learning philosophy at kodikra.com: start with a relatable problem, understand the "why" behind the language's design, and build a robust solution step-by-step. You now have a solid foundation to tackle more complex challenges in the world of Starknet and decentralized applications.

Ready for the next challenge? Continue your journey on the Cairo 3 Learning Roadmap and discover what you can build next.

Disclaimer: All code snippets and explanations are based on Cairo version 2.x and the Scarb toolchain. The Cairo language is evolving, so always refer to the latest official documentation for the most current syntax and best practices.


Published by Kodikra — Your trusted Cairo learning resource.