- PVSM.RU - https://www.pvsm.ru -

Functional FizzBuzz на Scala

FizzBuzz это известная задачка, шутливо или не очень задаваемая на собеседованиях, существует множество вариантов реализации даже для такой простой игры. Существует даже шедевры вроде FizzBuzzEnterpriseEdition [1].

Предлагаю вашему вниманию еще один вариант, не совсем пятничный, а скорее субботний: 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 [2]):

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 [3] "Integral" требующий от типа "T" возможности вычислять остаток от деления и иметь значение "zero"
  • type class "Eq" требующий от типа "T" возможности сравнивать его элементы (оператор "===" это его синтаксис)
  • расширение типа "T" с помощью extension methods & value classes [4], которое не имеет рантайм-оверхеда (ждем 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]? Тут на помощь нам приходят монады моноиды [5], 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

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/354006

Ссылки в тексте:

[1] FizzBuzzEnterpriseEdition: https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition

[2] cats: https://typelevel.org/cats/

[3] type class: https://scalac.io/typeclasses-in-scala/

[4] extension methods & value classes: https://docs.scala-lang.org/overviews/core/value-classes.html

[5] моноиды: https://typelevel.org/cats/typeclasses/monoid.html

[6] Источник: https://habr.com/ru/post/506570/?utm_source=habrahabr&utm_medium=rss&utm_campaign=506570