FizzBuzz это известная задачка, шутливо или не очень задаваемая на собеседованиях, существует множество вариантов реализации даже для такой простой игры. Существует даже шедевры вроде FizzBuzzEnterpriseEdition.
Предлагаю вашему вниманию еще один вариант, не совсем пятничный, а скорее субботний: FizzBuzz на Scala, functional style.
Задача
Для чисел от 1 до 100 нужно выводить на экран
- Fizz, если число делится на 3;
- Buzz, если число делится на 5;
- FizzBuzz, если число делится и на 3 и на 5;
- в противном случае само число.
Решение
Программист должен не столько решать задачу, сколько создавать инструмент для ее решения
Начнем с делимости
def divisibleBy(n: Int, d: Int): Boolean = n % d == 0divisibleBy(10, 5) // => true
Нет, это нас не устроит ведь делимость это свойство не только
чисел типа Int
, опишем делимость в общем виде, а за
одно сделаем ее инфиксным оператором (Тут и далее используются
некоторые возможности библиотеки cats):
import cats.implicits._import cats.Eqimplicit 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 // => trueBigInt(10) divisibleBy BigInt(3) // => falseBigInt(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]
и куда-нибудь вывести результат. Но это уже
совсем другая история...