Squeak Smalltalk で FizzBuzz
コメント欄が「キミならどう書く? 〜いろんな言語で FizzBuzz 〜」状態…になっている Raganwald: Don't Overthink FizzBuzz より。当該エントリー中に提示されている Ruby で書かれたちょっと変わった実装の動きがよく分からなかったので、例によって Squeak Smalltalk で直訳気味に変換してから動きを調べてみることに。
…と、その前に。リンク先にある Ruby 版のように、ある種、難読化を施したものを考えたり読み解いたりするのもそれなりに愉しいことだけれども、やっぱり Smalltalk の真骨頂はコードの読み下しやすさ(誰です!? そこで首をかしげているのは? ムッキー!w)。なので、まずは読んで内容がすぐ分かるオーソドックスなものから。
よくあるように(あるいはコードゴルフ的に…)出力までを一気にまとめて書いてしまってもよいのですが、メッセージング命の Smalltalk らしさというようなものを意識して、任意の整数に対して必要なら 'Fizz'、'Buzz'、'FizzBuzz' のいずれか、そうでないなら自身を printString して答えるよう、Integer に #asFizzBuzzString として定義してみます。
Integer >> asFizzBuzzString "converting" | stream | stream := String new writeStream. (self isDivisibleBy: 3) ifTrue: ['Fizz' putOn: stream]. (self isDivisibleBy: 5) ifTrue: ['Buzz' putOn: stream]. stream isEmpty ifTrue: [self printString putOn: stream]. ^ stream contents
3 asFizzBuzzString "=> 'Fizz' "
うーむ。美しい!(←自己満足)
ただ念のため、ケント・ベック的にはこういうのはNGです。その理由など詳しくは SBPP の「Converter Method」の項をご覧ください。
ストリームはちょっと使い方がよく分からない…という初学者なかたにも、次のように書きなおせば、やっていることはお分かりいただけるかと。
Integer >> asFizzBuzzString "converting" | result | result := String new. (self isDivisibleBy: 3) ifTrue: [result := result, 'Fizz']. (self isDivisibleBy: 5) ifTrue: [result := result, 'Buzz']. ^ result isEmpty ifTrue: [self printString] ifFalse: [result]
あとは、これを逐次数値に起動させて #collect: するのもよし、
(1 to: 15) collect: [:each | each asFizzBuzzString]
=> #('1' '2' 'Fizz' '4' 'Buzz' 'Fizz' '7' '8' 'Fizz' 'Buzz' '11' 'Fizz' '13' '14' 'FizzBuzz')
ループで回して、適宜トランスクリプトに出力するのもよし。
World findATranscript: nil. (1 to: 100) do: [:n | Transcript cr; show: n asFizzBuzzString]
さて、本題。元のへんてこな Ruby 版を、Squeak Smalltalk で書き下すとこんな感じ。(改行位置もちょっとだけ意識してヘンテコにしてあります…(^_^;))
| compose carbonation | compose := nil. compose := [:lambdas | lambdas isEmpty ifTrue: [ [nil] ] ifFalse: [lambdas size = 1 ifTrue: [ lambdas first ] ifFalse: [ [:n| lambdas first value: ( (compose copy fixTemps value: lambdas allButFirst) value: n)] ]] ]. carbonation := [:modulus :printableForm | | i | i := 0. [:n | (i := i+1) \\ modulus = 0 ifTrue: [printableForm] ifFalse: [n]] copy fixTemps ]. World findATranscript: nil. ((1 to: 100) collect: ( compose value: { carbonation value: 15 value: 'FizzBuzz'. carbonation value: 5 value: 'Buzz'. carbonation value: 3 value: 'Fizz'})) do: [:each | Transcript cr; show: each]
LISP や Ruby と違って、クロージャ(ただし、Squeak Smalltalk のそれは“もどき”。後述)を作るのにいちいち lambda(あるいは proc、Proc.new …)と書く必要がないこと、そして、ブロックがオブジェクトではない Ruby(引数の受け渡しの際に“&”を使ってクロージャとブロックとの変換を明示的にする必要がある…)と違って、compose の返り値をそのまま #collect:(Ruby 版では #map を使用…)の引数にできるところが Smalltalk での特色です。
一方で、Ruby や他の一般的な Smalltalk 処理系とは異なり、古典的な Smalltalk-80 の名残りを未だに残している Squeak Smalltalk では、ブロックが“クローズ”していない(ちゃんとしたクロージャではない)ので、再入の際には copy fixTemps してごまかす必要があります。
| fact | fact := nil. fact := [:n | n < 2 ifTrue: [1] ifFalse: [n * (fact value: n-1)]]. ^ fact value: 10 "=> Error: Attempt to evaluate a block that is already being evaluated."
| fact | fact := nil. fact := [:n | n < 2 ifTrue: [1] ifFalse: [n * (fact copy fixTemps value: n-1)]]. ^ fact copy fixTemps value: 10 "=> 3628800 "
よく見てみると、compose でやっていることは、与えられたブロック群について、#inject:into: みたいな作業を通じて #collect: 用のブロックを自動生成しているだけのようです。ならばと、素直にそう書き直してみたのが次のスクリプト。なお、カウンタである i と与えられた n とが(場合によっては…)一致して、それの残余をいちいち算出するのは何だか負けた気がするので、サイクリックなカウンタにもしてみました。
| lambdas collectBlock | lambdas := #( 3 'Fizz' 5 'Buzz' 15 'FizzBuzz') pairsCollect: [:modulus :printableForm | | i | i := 0. [:n | (i := i+1) = modulus ifTrue: [i := 0. printableForm] ifFalse: [n]] copy fixTemps]. collectBlock := lambdas inject: [:n | n] into: [:result :each | [:n | each value: (result value: n)] copy fixTemps]. World findATranscript: nil. ((1 to: 100) collect: collectBlock) do: [:each | Transcript cr; show: each]
Ruby に戻すと、こんな感じ。
lambdas = [3, 5, 15].zip(["Fizz", "Buzz", "FizzBuzz"]).collect do |m,f| i = 0 proc { |n| if (i += 1) == m then i = 0; f else n end } end collectBlock = lambdas.inject(proc { |n| n }) do |r,e| proc { |n| e.call(r.call(n)) } end puts (1..100).collect(&collectBlock)