数値のFizz、Buzz等への変換を、Smalltalk(とRuby)で通常より直感的に表現したい


FizzBuzzSmalltalk的に書くとして、ある n に対して、

n asFizzString, n asBuzzString ifEmpty: [n printString]


あるいはもっと簡素に、

n fizz, n buzz ifEmpty: [n]


というふうに n にメッセージを送ることでしかるべく変換された文字列の結合―という手続きとして表現するのが落としどころかな…ということは分かっているのですが、その一方で、たとえば関数的に

n fizz buzz


のようにすっきりと書いて(すくなくとも n はひとつ与えるだけで)済ませることはできないかと、常々ひっかかっていたので、みたびブームのこの機にちょっと考えてみました。



残念ながら、リーダーマクロっぽい機能があった Smalltalk-72 のころならともかく、今の Smalltalk にはメッセージに別のメッセージを送るための表現はないため、先の式は一意に、

n に対して fizz というメッセージを送り、その返値に buzz を送る―


と解釈するしかなく、#fizz と #buzz の合成みたいなことをしてから改めて n にその合成したメソッドをコールさせるようなことは、一応、できないことになっています。したがってこの書き方と動きのままでは、通常通り #fizz、#buzz に変換した文字列を返させる実装をしただけでは、たとえば n が3の倍数であるような場合、前半の n fizz が 'Fizz' を返してきてしまうと、その時点で続く buzz のレシーバーとして必要な情報が失なわれてしまい、まずいことになるのは自明です。


目先を変えて、fizz や buzz に変換した文字列を返させるのではなく、トランスクリプトなどへの出力といった副作用を活用するとともに、Smalltalk のカスケード式、つまり、ひとつ定めたレシーバー(この場合 n )に畳みかけるように fizz および buzz というメッセージを送る記法を用いることで、ぱっと見、うまく書けそうな気もするのですが―

n fizz; buzz
Integer >> fizz
   (self isDivisibleBy: 3) ifTrue: [Transcript cr; show: 'Fizz']

Integer >> buzz
   (self isDivisibleBy: 5) ifTrue: [Transcript cr; show: 'Buzz']


Fizz、Buzz の出力部分はすっきり書けても、この後、n をそのまま出力するための処理が非常に書きづらくなってしまっていてあまり嬉しくありません。


なんとか、しかるべき変換を促すメソッド #fizz によって 'Fizz' という情報を付加されつつも、相変わらず n という情報も保ちつつ buzz という次のメッセージに備えることはできないものか…、と考えてこねくり出したのが次の方法です。

Integer >> fizz
   (self isDivisibleBy: 3) ifTrue: [^self->'Fizz'].
   ^self


ここで #fizz には n (#fizz の文脈ではレシーバーである self)が 3 で割り切れるときに、'Fizz' を返す代わりに「キー -> 値」で生成できる Association のインスタンスを返させています。もし 3 で割り切れなければ n (つまり self)をそのまま返させます。なお、Smalltalk では返値を指定しなければ self が自動的に返るので最後の ^self という記述は別になくても構いません。

結果、続く buzz というメッセージは、メソッド #fizz をスルー(^self)してきた n それ自身か、n->'Fizz' という Association のインスタンスのいずれかに送られることになります。当然、その結果としてコールされる #buzz についても通常どおりの Integer のみならず Association のほうにも定義しておく手間が増えてしまうわけですが―

Integer >> buzz
   (self isDivisibleBy: 5) ifTrue: [^self->'Buzz'].
   ^self

Association >> buzz
   (key isDivisibleBy: 5) ifTrue: [^key->(value, 'Buzz')].
   ^self


しかしめでたく当初のもくろみどおり、

n fizz buzz


と、もはや見た目だけではありますが、関数的言語がきっとそうであるように、とてもシンプルに記述できるようになります。もっともこのままでは fizz か buzz のいずれか、あるいは両方に当てはまる場合に Association のインスタンスが返ってきてしまってまずいので、Object>>#value が self を返す(もちろん Association>>#value は key->valuevalue を返す)のを利用し、最後に value を添え、

n fizz buzz value


と書くことで妥協することにしましょう。(妥協と言うより Association のインスタンスへの変換とか value みたいなトリックが必要な時点で正直「負け」なんですが^^;) とはいえ「fizz buzz value」つまり「FizzBuzz値」というのもまあメッセージ的にも読み下し的にも悪くないかな、と今は自分を納得させておくことにします。そのうちメッセージングを介したメソッド(つまり、関数)の合成にもチャレンジしたいところですね。

(1 to: 15) collect: [:n | n fizz buzz value]
=> #(1 2 'Fizz' 4 'Buzz' 'Fizz' 7 8 'Fizz' 'Buzz' 11 'Fizz' 13 14 'FizzBuzz')


とりあえずこれで FizzBuzz に出会って以来のモヤモヤは、いくらかすっきりしました。w 



ただ、互いに似た内容になっている #fizz #buzz を何度も書くのは面倒なので、自動生成させるしくみを書いておくことにします。いくつか方法はあるかと思いますが、ここでは 3 Fizz のように 3 に対して Fizz を送ることで、3 で割り切れるときに 'Fizz' を返す #fizz を Integer と Association の双方に同時に定義させることにしました。

Integer >> doesNotUnderstand: message
   | sel code |
   sel := message selector.
   (sel first isUppercase and: [message arguments isEmpty])
      ifFalse: [^super doesNotUnderstand: message].
   code := '{1} ({4} isDivisibleBy: {2}) ifTrue: [^{4}->({5}''{3}'')]'.
   {Integer. #('self' ''). Association. #('key' 'value,')} pairsDo: [:class :spec |
      class compile: (code format: {sel asLowercase. self. sel}, spec) classified: #'fizz-buzz']


微妙にややこしいコードになってしまい、うれしさ半減…と思いきや、出力のちょっとした改変や割り切る数の追加などの拡張時にその威力は存分に発揮されそうなので、まあ、よしとしましょう。

#(3 Pizz 5 Quzz 7 Razz) pairsDo: [:n :sel | n perform: sel].
(1 to: 3*5*7) collect: [:n | n pizz quzz razz value]
=> #(1 2 'Pizz' 4 'Quzz' 'Pizz' 'Razz' 8 'Pizz' 'Quzz' 11 'Pizz' 13 'Razz'
'PizzQuzz' 16 17 'Pizz' 19 'Quzz' 'PizzRazz' 22 23 'Pizz' 'Quzz' 26 'Pizz'
'Razz' 29 'PizzQuzz' 31 32 'Pizz' 34 'QuzzRazz' 'Pizz' 37 38 'Pizz' 'Quzz'
41 'PizzRazz' 43 44 'PizzQuzz' 46 47 'Pizz' 'Razz' 'Quzz' 'Pizz' 52 53 'Pizz'
'Quzz' 'Razz' 'Pizz' 58 59 'PizzQuzz' 61 62 'PizzRazz' 64 'Quzz' 'Pizz' 67 68
'Pizz' 'QuzzRazz' 71 'Pizz' 73 74 'PizzQuzz' 76 'Razz' 'Pizz' 79 'Quzz' 'Pizz' 82
83 'PizzRazz' 'Quzz' 86 'Pizz' 88 89 'PizzQuzz' 'Razz' 92 'Pizz' 94 'Quzz' 'Pizz'
97 'Razz' 'Pizz' 'Quzz' 101 'Pizz' 103 104 'PizzQuzzRazz')


これくらいなら私の拙い Ruby力でも書けそうなので、以下直訳っぽく。

class Assoc
  attr_accessor :key, :val
   def initialize(k, v); @key=k; @val=v end
end

class Object; def val; self end end

class Integer
  def method_missing(sym, *args)
    sel = sym.to_s
    m = self
    return super unless sel=~/^[A-Z]/ and args.size.zero?
    Integer.class_eval{
      define_method(sel.downcase){ self%m==0 ? Assoc.new(self,sel) : self }
    }
    Assoc.class_eval{
      define_method(sel.downcase){ @key%m==0 ? Assoc.new(@key,@val+sel) : self }
    }
  end
end

3.Pizz; 5.Quzz; 7.Razz
(1..3*5*7).collect{ |n| n.pizz.quzz.razz.val }
=> [1, 2, "Pizz", 4, "Quzz", "Pizz", "Razz", 8, "Pizz", "Quzz", 11, "Pizz", 13, "Razz",
"PizzQuzz", 16, 17, "Pizz", 19, "Quzz", "PizzRazz", 22, 23, "Pizz", "Quzz", 26, "Pizz",
"Razz", 29, "PizzQuzz", 31, 32, "Pizz", 34, "QuzzRazz", "Pizz", 37, 38, "Pizz", "Quzz",
41, "PizzRazz", 43, 44, "PizzQuzz", 46, 47, "Pizz", "Razz", "Quzz", "Pizz", 52, 53, "Pizz",
"Quzz", "Razz", "Pizz", 58, 59, "PizzQuzz", 61, 62, "PizzRazz", 64, "Quzz", "Pizz", 67, 68,
"Pizz", "QuzzRazz", 71, "Pizz", 73, 74, "PizzQuzz", 76, "Razz", "Pizz", 79, "Quzz", "Pizz", 82,
83, "PizzRazz", "Quzz", 86, "Pizz", 88, 89, "PizzQuzz", "Razz", 92, "Pizz", 94, "Quzz", "Pizz",
97, "Razz", "Pizz", "Quzz", 101, "Pizz", 103, 104, "PizzQuzzRazz"]