The Coding Kata: FizzBuzzWhizz in Scala

horance發表於2019-02-16

Functional programming leads to deep insights into the nature of computation. — Martin Odersky

形式化

FizzBuzzWhizz詳細描述請自行查閱相關資料。此處以3, 5, 7為例,形式化地描述一下問題。

r1
- times(3) -> Fizz
- times(5) -> Buzz
- times(7) -> Whizz
r2
- times(3) && times(5) && times(7) -> FizzBuzzWhizz
- times(3) && times(5) -> FizzBuzz
- times(3) && times(7) -> FizzWhizz
- times(5) && times(7) -> BuzzWhizz
r3
- contains(3) -> Fizz
- the priority of contains(3) is highest
rd
- others -> others

接下來我將使用Scala嘗試FizzBuzzWhizz問題的設計和實現。

語義模型

從上面的形式化描述,可以很容易地得到FizzBuzzWhizz問題的語義模型。

Rule: (Int) -> String
Matcher: (Int) -> Boolean
Action: (Int) -> String

其中,Rule存在三種基本的型別:

Rule ::= atom | allof | anyof

三者之間構成了「樹型」結構。

atom: (Matcher, Action) -> String
allof(rule1, rule2, ...): rule1 && rule2 && ... 
anyof(rule1, rule2, ...): rule1 || rule2 || ... 

測試用例

藉助Scala強大的「型別系統」能力,可拋棄掉很多重複的「樣板程式碼」,使得設計更加簡單、漂亮。此外,Scala構造DSL的能力也相當值得稱讚,非常直接,簡單。

import org.scalatest._
import prop._

class RuleSpec extends PropSpec with TableDrivenPropertyChecks with Matchers {
  val spec = {
    val r1_3 = atom(times(3), to("Fizz"))
    val r1_5 = atom(times(5), to("Buzz"))
    val r1_7 = atom(times(7), to("Whizz"))

    val r1 = anyof(r1_3, r1_5, r1_7)

    val r2 = anyof(
      allof(r1_3, r1_5, r1_7),
      allof(r1_3, r1_5),
      allof(r1_3, r1_7),
      allof(r1_5, r1_7))

    val r3 = atom(contains(3), to("Fizz"))
    val rd = atom(always(true), nop);

    anyof(r3, r2, r1, rd)
  }

  val specs = Table(
    ("n", "expect"),
    (3, "Fizz"),
    (5, "Buzz"),
    (7, "Whizz"),
    (3 * 5, "FizzBuzz"),
    (3 * 7, "FizzWhizz"),
    ((5 * 7) * 2, "BuzzWhizz"),
    (3 * 5 * 7,   "FizzBuzzWhizz"),
    (13, "Fizz"),
    (35, "Fizz"),  // 35 > 5*7
    (2,  "2")
  )

  property("fizz buzz whizz") {
    forAll(specs) { spec(_) should be (_) }
  }
}

匹配器:Matcher

Matcher是一個「一元函式」,入參為Int,返回值為Boolean,是一種典型的「謂詞」。從OO的角度看,always是一種典型的Null Object

object Matchers {
  type Matcher = Int => Boolean

  def times(n: Int): Matcher = _ % n == 0
  def contains(n: Int): Matcher = _.toString.contains(n.toString)
  def always(bool: Boolean): Matcher = _ => bool
}

執行器:Action

Action也是一個「一元函式」,入參為Int,返回值為String,其本質就是定製常見的map操作,將定義域對映到值域。

object Actions {
  type Action = Int => String

  def to(str: String): Action = _ => str
  def nop: Action = _.toString
}

規則:Rule

Composition Everywhere

RuleFizzBuzzWhizz最核心的抽象,也是設計的靈魂所在。從語義上Rule分為2種基本型別,並且兩者之間形成了優美的、隱式的「樹型」結構,體現了「組合式設計」的強大威力。

  • Atomic

  • Compositions: anyof, allof

Rule是一個「一元函式」,入參為Int,返回值為String。其中,def atom(matcher: => Matcher, action: => Action)的入參使用Pass By Name的惰性求值的特性。

object Rules {
  type Rule = (Int) => String

  def atom(matcher: => Matcher, action: => Action): Rule =
    n => if (matcher(n)) action(n) else ""

  def anyof(rules: Rule*): Rule =
    n => rules.map(_(n))
      .filterNot(_.isEmpty)
      .headOption
      .getOrElse("")

  def allof(rules: Rule*): Rule =
    n => rules.foldLeft("") { _ + _(n) }
}

原始碼

相關文章