ボウリングスコア集計の条件分岐記述にひとひねり(あるいはバッドノウハウ)を入れる


最後のこれは記述の簡潔さと読み下しのしやすさを兼ね備えていてなかなかよさげなのですが、条件式のネストがちょっといや〜んな感じです。

| score |
score := [:pins |
   | restPins framePoints |
   restPins := pins asOrderedCollection.
   framePoints := (1 to: 9) collect: [:idx |
      restPins first = 10
         ifTrue: [(restPins removeFirst: 1), (restPins first: 2)]
         ifFalse: [restPins first + restPins second = 10
            ifTrue: [(restPins removeFirst: 2), (restPins first: 1)]
            ifFalse: [restPins removeFirst: 2]]].
   framePoints := framePoints, {restPins}.
   (framePoints collect: #sum) sum
].

score value: #(10 10 10 10 10 10 10 10 10 10 10 10).  "=> 300 "
score value: #(10 10 10 10 10 10 10 10 10 3 3).       "=> 255 "
score value: #(0 0 10 8 2 10 10 10 5 3 8 2 10 2 3).   "=> 161 "
score value: #(5 3 7 2 8 2 10 7 1 9 0 6 2 10 6 4 8 0).  "=> 126 "


Ruby のように else if が使えればすこしマシになりますが、Smalltalk にはそんな便利なものはありません(作れば別ですが)。

def score(pins)
  framePoints = (1 .. 9).collect{ | idx |
    if pins.first == 10 then
      pins.shift(1) + pins.first(2)
    elsif pins[0] + pins[1] == 10
      pins.shift(2) + pins.first(1)
    else
      pins.shift(2)
    end
  }
  framePoints << pins
  framePoints.collect{ |ea| ea.inject(&:+) }.inject(&:+)
end


さすがに Haskell のパターンマッチのようにすっきり書くのは無理としても、組み込みのメソッドでもうすこしマシにできる何かいい手はないかと考えていたところ、Smalltalk の標準的組み込みメソッドではないが Squeak Smalltalk には何故か存在する #caseOf:otherwise: をひとひねりして使ってみてはどうかというアイデアが浮かびました。


通常、#caseOf:otherwise: メソッドはこのように使うのですが、

#(one two three four) atRandom caseOf: {
   [#one]->[1].
   [#two]->[2]} 
   otherwise: [0]   "=> 0,1,2 のいずれか "


条件と処理のキーと値の組([#one]->[1] など)は値だけでなくキーもブロックなので、ここには式を書くことができます。そこで、ボウリングスコアで倒したピン数の列の最初が 10 の場合(ストライク)と最初の二つの数の和が 10 の場合(スペア)の条件式自体をそれぞれのキーにし、一方で、caseOf: assocs otherwise: block のレシーバーを true にすれば、こんなふうに書くことが可能だと気がつきました。

| score |
score := [:pins |
   | restPins framePoints |
   restPins := pins asOrderedCollection.
   framePoints := OrderedCollection new.
   9 timesRepeat: [
      framePoints add: (true caseOf: {
         [restPins first = 10] -> [(restPins removeFirst: 1), (restPins first: 2)].
         [restPins first + restPins second = 10] -> [(restPins removeFirst: 2), (restPins first: 1)]}
         otherwise: [restPins removeFirst: 2])].
   framePoints add: restPins.
   (framePoints collect: #sum) sum
].

score value: #(10 10 10 10 10 10 10 10 10 10 10 10).  "=> 300 "
score value: #(10 10 10 10 10 10 10 10 10 3 3).       "=> 255 "
score value: #(0 0 10 8 2 10 10 10 5 3 8 2 10 2 3).   "=> 161 "
score value: #(5 3 7 2 8 2 10 7 1 9 0 6 2 10 6 4 8 0).  "=> 126 "


もとよりパターンマッチの記述力には遠く及びませんが、条件を列挙できるので、少なくとも条件のネストよりはコードの記述とメンタルモデルが一致していい感じに思えて気に入っています。


今まで #caseOf:otherwise: に対しては、もうちょっと柔軟な条件分岐ができれば使い勝手があがっていいのになぁと少し不満に感じていたところなので、真偽値をレシーバーにするこのイディオム(バッドノウハウとも)は今後もいろいろと応用が利きそうです。