The Coding Kata: FizzBuzzWhizz in Ruby

Code Kata · horance · Created at · Last by chenge Replied at · 1334 hits
3

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

接下来我将使用Ruby尝试FizzBuzzWhizz问题的设计和实现。

语义模型

从上面的形式化描述,可以很容易地得到FizzBuzzWhizz问题的语义模型。

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

其中,Rule存在三种基本的类型:

Rule ::= atom | allof | anyof

三者之间构成了「树型」结构。

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

测试用例

借助Scala强大的「类型系统」能力,可抛弃掉很多重复的「样板代码」,使得设计更加简单、漂亮。此外,Scala构造DSL的能力也相当值得称赞,非常直接,简单。

describe "fizz buzz whizz" do
  include Matcher
  include Action
  include Rule

  def spec
    r1_3 = atom(times(3), to("Fizz"))
    r1_5 = atom(times(5), to("Buzz"))
    r1_7 = atom(times(7), to("Whizz"))

    r1 = anyof([r1_3, r1_5, r1_7])

    r2 = anyof([
      allof([r1_3, r1_5, r1_7]),
      allof([r1_3, r1_5]),
      allof([r1_3, r1_7]),
      allof([r1_5, r1_7])])

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

    anyof([r3, r2, r1, rd])
  end

  [ 
    [3, 'Fizz' ],
    [5, 'Buzz' ],
    [7, 'Whizz' ],
    [3*5, 'FizzBuzz' ],
    [3*7, 'FizzWhizz' ],
    [5*7*2, 'BuzzWhizz' ],
    [3*5*7, 'FizzBuzzWhizz' ],
    [13, 'Fizz' ],
    [35, 'Fizz' ],
    [2,  '2' ]
  ].each do |n, expect|
    it "#{n} -> #{expect}" do
      expect(spec.call(n)).to eq expect
    end
  end
end

匹配器:Matcher

Matcher是一个「一元函数」,入参为Int,返回值为Boolean,是一种典型的「谓词」。从OO的角度看,always是一种典型的Null Object

module Matcher
  def times(n)
    ->(x) { x % n == 0 }
  end

  def contains(n)
    ->(x) { x.to_s.include?(n.to_s) }
  end

  def always(b)
    ->(x) { b }
  end
end

执行器:Action

Action也是一个「一元函数」,入参为Int,返回值为String,其本质就是定制常见的map操作,将定义域映射到值域。

module Action
  def to(str)
    ->(n) { str }
  end

  def nop 
    ->(x) { x.to_s }
  end
end

规则:Rule

Composition Everywhere

RuleFizzBuzzWhizz最核心的抽象,也是设计的灵魂所在。从语义上Rule分为2种基本类型,并且两者之间形成了优美的、隐式的「树型」结构,体现了「组合式设计」的强大威力。

  • Atomic
  • Compositions: anyof, allof

Rule是一个「一元函数」,入参为Int,返回值为String

module Rule
  def atom(matcher, action)
    ->(n) { matcher.call(n) ? action.call(n) : '' } 
  end

  def allof(rules)
    ->(n) { strings(rules, n).join }
  end

  def anyof(rules)
    lambda do |n|
      result = strings(rules, n).find { |s| !s.empty? }
      result != nil ? result : ''
    end
  end

  def strings(rules, n)
    rules.map { |rule| rule.call(n) }
  end

  private :strings
end

源代码


「软件匠艺社区」旨在传播匠艺精神,通过分享好的「工作方式」和「习惯」以帮助程序员更加快乐高效地编程。
共收到 3 条回复
46
chenge · #1 ·

大系统需要重构支持,ruby,clojure似乎没看到支持改名这样的重构工具。加上scala,erlang,哪个语言最值得深造呢?

3
horance · #2 ·

#1楼 @chenge 动态语言都很难重构,IDE相对于静态语言也相对不太成熟。但重构是一种习惯,没有重构工具,也是能做重构,只是速度慢些,工作效率不高,也容易出错。

我在重构Ruby, Erlang时,也会出现一些低级错误,但总结还是我自己没有遵循良好的重构原则,例如步子太大扯了蛋,同时干两件事情等等不良习惯,不经意间引入了低级错误。更重要的是,由于是动态语言,缺乏测试覆盖的情况,我在运行时不经意地埋了好几个雷。所以,对于动态语言,我的经验是:TDD和重构“双剑合璧”的软件设计。静态语言,重构相对会好一些,至少不会犯低级的错误。

语言的选择,我也很谨慎。因为要精通一门语言,至少要修炼3年时间,这是一个很大的机会成本。我的原则:精通主流语言,尝试不同思维。

为此,我选择C++, Java作为我最精通的语言,胜任大部分的工作;脚本语言我要精通:Ruby, Shell,提高工作效率;函数式我选择了Scala,目前也在为此付出较大的精力。其他语言,我只是尝试,多听多练多学多做足以,帮助我在精通的这几门语言上更多的思考,帮助提高设计的水准,除非工作将我逼上了绝路(_)

46
chenge · #3 ·

#2楼 @horance TDD全覆盖的话似乎不现实,我一般会测试部分上层代码。我感觉重构如果没工具支持的话,就比较麻烦了。

所以感觉这两项只能部分采用。

需要 Sign In 后回复方可回复, 如果你还没有账号你可以 Sign Up 一个帐号。