FizzBuzz fun in Scala: Combining functions

Every implementation of FizzBuzz in this series, at its core, has relied on an infinitely counting lazy list. This modelling is logical, as the game can theoretically be played indefinitely. In this post, we will explore the possibility of defining a single function operating on this running number and, as a bonus, utilize Scala 3's extension methods to enhance code clarity.

LazyList
  .from(1)
  ... // multiple statements
  .take(n)
  .toList

What we are looking for is:

LazyList
  .from(1)
  .map(i: Int => ???)
  .take(n)
  .toList

Let's define a method and its signature:

LazyList
  .from(1)
  .map(fizzbuzzAt)
  .take(n)
  .toList

def fizzbuzzAt(n: Int): String = ???

Note from the first article in this series an implementation of fizzbuzzAt using if statements could be:

// Commenting the Scala 3 version / Scala CLI directive
//> using scala 3.3.1

@main def fizzbuzz(): Unit =

  // function to re-implement
  def fizzbuzzAt(n: Int): String =
    if n % 15 == 0 then "FizzBuzz"
    else if n % 5 == 0 then "Buzz"
    else if n % 3 == 0 then "Fizz"
    else
      n.toString
  end fizzbuzzAt

  def fizzbuzz(n: Int): List[String] =
    LazyList
      .from(1)
      .map(fizzbuzzAt)
      .take(n)
      .toList
  end fizzbuzz

  fizzbuzz(20).foreach(fb => print(fb + ","))
end fizzbuzz

Now we want to simplify the if expression and make it easier to implement e.g. other 'shouts', say Bazz every 2 times. For this article, we want to be looking into a solution using functions.

A quite natural function to decide whether Fizz should be shouted is one taking an integer as input and returning maybe a String ("Fizz"):

val fizz: Int => Option[String] = ???

Our fizzbuzzAt could then look like:

  def fizzbuzzAt(n: Int): String =
    // fizz and buzz are similar and can be implemented:
    val fizz: Int => Option[String] = i =>
      if i % 3 == 0 then Some("Fizz") else None
    val buzz: Int => Option[String] =
      i => if i % 5 == 0 then Some("Buzz") else None
    // Both need to be combined into a function with same signature
    val combined: Int => Option[String] = ???
    // The final function which should return a String
    val fizzbuzz: Int => String = ???
    fizzbuzz(n)
  end fizzbuzzAt

So we need to combine 2 functions of the same signature Int => Option[String]:

  def combine[A](f1: A => Option[String], f2: A => Option[String]): A => Option[String] =
    a =>
      (f1(a), f2(a)) match
        case (Some(s1), Some(s2)) => Some(s1 + s2)
        case (None, Some(s2))     => Some(s2)
        case (Some(s1), None)     => Some(s1)
        case (None, None)         => None

We generalized a little in the argument, the return type we keep at String as we need to operate on it. (In a next blog post we will also generalize this more but we leave it as this for now.)

fizzbuzz needs to turn the combined function Int => Option[String] into a function Int => String. For the FizzBuzz game, turning Option[String] into a String given an integer can be done like:

val fizzbuzz: Int => String = i =>
  combined(i).getOrElse(i.toString)

fizzbuzzAt then becomes:

def fizzbuzzAt(n: Int): String =
  val fizz: Int => Option[String] = i =>
    if i % 3 == 0 then Some("Fizz") else None
  val buzz: Int => Option[String] = i =>
    if i % 5 == 0 then Some("Buzz") else None
  val combined: Int => Option[String] =
    combine(fizz, buzz)
  val fizzbuzz: Int => String = i =>
    combined(i).getOrElse(i.toString)
  fizzbuzz(n)
end fizzbuzzAt

Let's transform fizzbuzzAt into a function as well:

val fizzbuzzAt: Int => String =
  val fizz: Int => Option[String] = i =>
    if i % 3 == 0 then Some("Fizz") else None
  val buzz: Int => Option[String] = i =>
    if i % 5 == 0 then Some("Buzz") else None
  val combined: Int => Option[String] =
    combine(fizz, buzz)
  val fizzbuzz: Int => String = i =>
    combined(i).getOrElse(i.toString)
  fizzbuzz // return a function from Int to String
end fizzbuzzAt

Let's eliminate the duplication in the fizz and buzz functions:

  val fizzbuzzAt: Int => String =
    extension (word: String)
      def every(n: Int): Int => Option[String] = i => 
        if i % n == 0 then Some(word) else None

    val fizz = "Fizz".every(3)
    val buzz = "Buzz".every(5)
    val combined: Int => Option[String] =
      combine(fizz, buzz)
    val fizzbuzz: Int => String = i =>
      combined(i).getOrElse(i.toString)
    fizzbuzz
  end fizzbuzzAt

Here we make use of a Scala 3 extension method to make the function definition clearer and, I would argue, more easily reusable with less chance of errors.

Putting all word shouts into a list allows for easier extension in a single place:

val fizzbuzzAt: Int => String =
  extension (word: String)
    def every(n: Int): Int => Option[String] = i => if i % n == 0 then Some(word) else None

  val wordShouts = List(
    "Fizz".every(3),
    "Buzz".every(5),
    // Easy to extend:
    "Bazz".every(2)
  )
  val combined = wordShouts.reduce(combine) // will fail on empty wordShouts
  val fizzbuzz: Int => String = i =>
    combined(i).getOrElse(i.toString)
  fizzbuzz
end fizzbuzzAt

If we want to account for empty wordShouts we can instead resort to a fold:

val combined = words.fold(_ => None)(combine)

We defined the 'zero' function to accept any integer and return None. I will expand on this in a next blog post and leave the reduce function in the final version of this article:

// Commenting the Scala 3 version / Scala CLI directive
//> using scala 3.3.1

@main def fizzbuzz(): Unit =

  def combine[A](f1: A => Option[String], f2: A => Option[String]): A => Option[String] =
    a =>
      (f1(a), f2(a)) match
        case (Some(s1), Some(s2)) => Some(s1 + s2)
        case (None, Some(s2))     => Some(s2)
        case (Some(s1), None)     => Some(s1)
        case (None, None)         => None

  val fizzbuzzAt: Int => String =
    extension (word: String)
      def every(n: Int): Int => Option[String] = i =>
        if i % n == 0 then Some(word) else None

    val wordShouts = List(
      "Fizz".every(3),
      "Buzz".every(5)
    )
    val combined = wordShouts.reduce(combine) // will fail on empty wordShouts
    val fizzbuzz: Int => String = i =>
      combined(i).getOrElse(i.toString)
    fizzbuzz
  end fizzbuzzAt

  def fizzbuzz(n: Int): List[String] =
    LazyList
      .from(1)
      .map(fizzbuzzAt)
      .take(n)
      .toList
  end fizzbuzz

  fizzbuzz(20).foreach(fb => print(fb + ","))
end fizzbuzz

This prints:

1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,16,17,Fizz,19,Buzz,

In conclusion, this article demonstrated how to create a single function for the FizzBuzz game using Scala, combining functions and making use of Scala 3's extension methods for clarity and reduced potential for errors. This approach allows for easier testing and extension of the game with additional word shouts, showcasing the power and flexibility of functional programming and Scala.