Squeak Smalltalk で、カスケードと thisContext を使って FizzBuzz

Ruby勉強会@札幌-26 - Ruby札幌 | DoorkeeperSmalltalk(特に Squeak Smalltalk)の thisContext を含めた変態さん機能をご紹介した流れで、今まで書いたことのないタイプの FizzBuzz 実装を思いついたのでメモ。

Integer >> fizz
   ^self fizzBuzz: ((self isDivisibleBy: 3) ifTrue: ['Fizz'] ifFalse: [''])

Integer >> buzz
   ^self fizzBuzz: ((self isDivisibleBy: 5) ifTrue: ['Buzz'] ifFalse: [''])

Integer >> fizzBuzz: fizzBuzzString
   | caller result |
   caller := thisContext sender sender.
   result := ((caller stackPtr > 0 and: [(caller at: 1) isKindOf: String])
      ifTrue: [caller at: 1] ifFalse: ['']), fizzBuzzString.
   caller stackPtr > 0 ifTrue: [caller at: 1 put: result].
   caller stackPtr = 1 ifTrue: [caller push: self].
   ^result ifEmpty: [self]
(1 to: 3*5) collect: [:n | n fizz; buzz]
=>  #(1 2 'Fizz' 4 'Buzz' 'Fizz' 7 8 'Fizz' 'Buzz' 11 'Fizz' 13 14 'FizzBuzz')


3あるいは5の倍数のときにそれぞれ 'Fizz' もしくは 'Buzz' を生成するメソッド #fizz および #buzz のコールをカスケードで追記するだけなのが今回のウリです。7の倍数の場合も容易に追加できます。

Integer >> pezz
   ^self fizzBuzz: ((self isDivisibleBy: 7) ifTrue: ['Pezz'] ifFalse: [''])
(1 to: 3*5*7) collect: [:n | n fizz; buzz; pezz]
=> #(1 2 'Fizz' 4 'Buzz' 'Fizz' 'Pezz' 8 'Fizz' 'Buzz' 11 'Fizz' 13 'Pezz' 'FizzBuzz' 
16 17 'Fizz' 19 'Buzz' 'FizzPezz' 22 23 'Fizz' 'Buzz' 26 'Fizz' 'Pezz' 29 'FizzBuzz' 
31 32 'Fizz' 34 'BuzzPezz' 'Fizz' 37 38 'Fizz' 'Buzz' 41 'FizzPezz' 43 44 'FizzBuzz' 
46 47 'Fizz' 'Pezz' 'Buzz' 'Fizz' 52 53 'Fizz' 'Buzz' 'Pezz' 'Fizz' 58 59 'FizzBuzz' 
61 62 'FizzPezz' 64 'Buzz' 'Fizz' 67 68 'Fizz' 'BuzzPezz' 71 'Fizz' 73 74 'FizzBuzz' 
76 'Pezz' 'Fizz' 79 'Buzz' 'Fizz' 82 83 'FizzPezz' 'Buzz' 86 'Fizz' 88 89 'FizzBuzz' 
'Pezz' 92 'Fizz' 94 'Buzz' 'Fizz' 97 'Pezz' 'Fizz' 'Buzz' 101 'Fizz' 103 104 'FizzBuzzPezz')


コンテキストを実行中にいじるとかいう通常の言語では考えられない作業以前に、そもそも副作用だけ期待して結果を捨てる「カスケード」という特殊な言語機能(しかし実体は単なる構文糖)が Smalltalk ならではなので、そういう切り口でも他言語ではちょっと実現が難しい FizzBuzz 実装のひとつかと思います。


追記
スタック操作の手抜きがひどかったのでもうちょっと丁寧に書いてみました。どれだけ意味があるかわかりませんが、これで 3 buzz + 4 fizz "=> 7 " とか 3 fizz, 4 buzz "=> 'Fizz4' " とかも誤動作しなくなります。

Integer >> fizzBuzz: fizzBuzzString
   | caller ptr result |
   caller := thisContext sender sender.
   ptr := (1 to: caller stackPtr) findLast: [:idx | (caller at: idx) value = #fizzBuzz].
   result := (ptr > 0 ifTrue: [(caller at: ptr) key] ifFalse: ['']), fizzBuzzString.
   ptr > 0 ifTrue: [(caller at: ptr) key: result] ifFalse: [
      (caller stackPtr > 0 and: [caller top = self]) ifTrue: [
         caller pop; push: result -> #fizzBuzz; push: self]].
   ^result ifEmpty: [self]