FizzBuzz это известная задачка, шутливо или не очень задаваемая на собеседованиях, существует множество вариантов реализации даже для такой простой игры. Существует даже шедевры вроде FizzBuzzEnterpriseEdition.
Предлагаю вашему вниманию еще один вариант, не совсем пятничный, а скорее субботний: FizzBuzz на Scala, functional style.
Задача
Для чисел от 1 до 100 нужно выводить на экран
- Fizz, если число делится на 3;
- Buzz, если число делится на 5;
- FizzBuzz, если число делится и на 3 и на 5;
- в противном случае само число.
Решение
Программист должен не столько решать задачу, сколько создавать инструмент для ее решения
Начнем с делимости
def divisibleBy(n: Int, d: Int): Boolean = n % d == 0
divisibleBy(10, 5) // => true
Нет, это нас не устроит — ведь делимость это свойство не только чисел типа Int
, опишем делимость в общем виде, а за одно сделаем ее инфиксным оператором (Тут и далее используются некоторые возможности библиотеки cats):
import cats.implicits._
import cats.Eq
implicit class DivisionSyntax[T](val value: T) extends AnyVal {
def divisibleBy(n: T)(implicit I: Integral[T], ev: Eq[T]): Boolean = {
import I._
(value % n) === zero
}
def divisibleByInt(n: Int)(implicit I: Integral[T], ev: Eq[T]): Boolean =
divisibleBy(I.fromInt(n))
}
10 divisibleBy 5 // => true
BigInt(10) divisibleBy BigInt(3) // => false
BigInt(10) divisibleByInt 3 // => false
Тут используются:
- type class "
Integral
" требующий от типа "T
" возможности вычислять остаток от деления и иметь значение "zero
" - type class "
Eq
" требующий от типа "T
" возможности сравнивать его элементы (оператор "===
" это его синтаксис) - расширение типа "
T
" с помощью extension methods & value classes, которое не имеет рантайм-оверхеда (ждем dotty, который принесет нам нормальный синтаксис экстеншен методов)
Строго говоря метод divisibleByInt
не совсем тут нужен, но он пригодится нам позже, если мы захотим использовать литералы целочисленного типа 3 и 5.
FizzBuzz
Отлично! Перейдем к вычислению того, что нужно вывести на экран, напомню, что это может быть "Fizz", "Buzz", "FizzBuzz" либо само число. Тут есть общий паттерн — некоторое значение участвует в результате, только если выполняется определенное условие. Для этого подойдет Option
, который будет определять используется значение или нет:
def useIf[T](value: T, condition: Boolean) = if (condition) Some(value) else None
Как и в случае с "divisibleBy(10, 5)
" и "10 divisibleBy 5
" задача решается, но как-то некрасиво. Мы ведь хотим не только решить задачу, но и создать инструмент для ее решения, DSL! По-сути, большая часть работы программиста и есть создание DSL разного рода, когда мы отделяем "как сделать" от "что сделать", "10 % 5 == 0
" от "10 divisibleBy 5
".
implicit class WhenSyntax[T](val value: T) extends AnyVal {
def when(condition: Boolean): Option[T] = if (condition) Some(value) else None
}
"Fizz" when (6 divisibleBy 3) // => Some("Fizz")
"Buzz" when (6 divisibleBy 5) // => None
Осталось собрать все вместе! Мы могли бы использовать orElse
и получили бы 3 правильных ответа из 4, но когда мы должны вывести "FizzBuzz" это не сработает, нам нужно получить Some("Fizz") ? Some("Buzz") => Some("FizzBuzz")
. Просто строки можно складывать, но как сложить Option[String]
? Тут на помощь нам приходят монады моноиды, cats предоставляет нам все нужные инстансы и даже удобный синтаксис:
def fizzBuzz[T: Integral: Eq: Show](number: T): String =
("Fizz" when (number divisibleByInt 3)) |+|
("Buzz" when (number divisibleByInt 5)) getOrElse
number.show
Тут type class Show
дает типу T
возможность превращения в строку, |+|
синтаксис моноида для сложения и getOrElse
задает значение по-умолчанию. Все в общем виде и для любых типов, мы могли бы и от строк "Fizz" & "Buzz" абстрагироваться, но это лишнее на мой взгляд.
Конец
Все, что нам осталось сделать это (1 to 100) map fizzBuzz[Int]
и куда-нибудь вывести результат. Но это уже совсем другая история...
Автор: Alex