トレイトを使った FizzBuzz を Squeak Smalltalk で
2011-10-08 に触発されて、いつもどおり何周目かの FizzBuzz に今回は Traits というのを使って乗っかってみます。
追記:前置きがやたら長くなってしまったのでまずコードをここで示します。下のほうに載せてあるワークスペース(Squeak4.2などの―)で直接実行できるコードと実質同じ内容ですが、いくらか読み下しやすくする目的で簡略書式に整形してあります。何となく内容を把握するにはこちらが断然読みやすいですが、実際に動かすとなると逆で、省略情報を適宜補ったり、メソッドごとにクラスブラウザにコピペ&コンパイルするといった Smalltalk環境の知識がある程度必要になりますのでご注意ください。
Trait named: #TFizzBuzz TFizzBuzz >> fizz (self asInteger isDivisibleBy: 3) ifTrue: [^self asString, 'Fizz'] TFizzBuzz >> buzz (self asInteger isDivisibleBy: 5) ifTrue: [^self asString, 'Buzz'] TFizzBuzz >> value ^self asString splitInteger last
Integer uses: TFizzBuzz. String uses: TFizzBuzz. (1 to: 15) collect: [:m | m fizz buzz value]
=> #(1 2 'Fizz' 4 'Buzz' 'Fizz' 7 8 'Fizz' 'Buzz' 11 'Fizz' 13 14 'FizzBuzz')
で、以下解説。
Traits というのは、クラス(あるいはインスタンスを生成できないなどクラス本来の機能を制限した類似のエンティティ)とその継承機構を流用した従来のミックスイン(多重継承)で頻繁に取りざたされる問題(代表格はダイヤモンド継承)を解決すべく、Schärliらが考えたトレイト(trait)というエンティティを用いた新しい多重継承の枠組みです。
2002 年頃に Squeak Smalltalk で実装が試され、組み込みの Collection クラス群の再構成で一定の成果が示された後、Squeak3.9 から組み込みになりました。他の言語処理系でも、Perl では Role という名前で、また次の PHP5.4 でも採用が決まっています。
- Applying Traits to the Smalltalk Collection Hierarchy - SCG: SCG Bibliography
- http://damako.net/perl6specs/S14-roles-and-parametric-types.html
- PHP.next: Traits
なお、Traits というと最近はやりの Scala ではじめてその存在を知った人も多いかと思います。しかしその名前に反して Scala を含む静的型言語での Traits の実装は一般に、 Java などのインターフェイス様のエンティティに対して実装も書けるという意味での目新しさはあるものの、本質的なところは旧来のミックスインと変わらない機構であることが多いようなので「ミックスインと何が違うの?」と振り回されないよう注意が必要なように感じます。
ちなみにトレイトであるための要件はというと、メソッドの集合でクラスの一部として機能できること―などいくつかありそうですが、たぶんこれは欠かせないだろうな…と考えるのは、あるクラスに複数のトレイトを適用した場合、コンパイル時に合成(フラット化)されることを通じメソッド名の衝突を検出できる、という点かと思います。この線引きであれば、クラス様エンティティのリニアライズ(どのエンティティにあるメソッドを優先するか決めること)のみの旧来のミックスインとかなり明確な区別か可能ではないでしょうか。
とまあ、長々と Traits について書きましたが今回はこの Traits の要件を満たしている必要はまったくありません。そんなわけで、従来のミックスイン機構、つまり、複数のクラスでのメソッドの共有と(おまけとして)オープンクラス、またはそれに準ずる機構があれば実装は容易なはずですので、Smalltalk アレルギーのない方は、Scala のトレイトや Ruby のモジュールを使った実装にもぜひチャレンジしてみてください。
さて本題です。
前回同様、今回も基本的な立ち位置は、与えられた n に対して 'Fizz' あるいは 'Buzz' あるいは 'FizzBuzz'(いずれでもなければ n 自身)を返させるのに n fizz buzz value (可能であれば n fizz buzz)と DSL っぽく書きたいが、そうするにはどうしたらよいか、というあたりです。
本題とは関係ないですが参考まで、Squeak Smalltalk で普通に書いた FizzBuzz をまず示しておきます。
| fizzbuzz | fizzbuzz := [:n | ((n isDivisibleBy: 3) ifTrue: ['Fizz'] ifFalse: ['']), ((n isDivisibleBy: 5) ifTrue: ['Buzz'] ifFalse: ['']) ifEmpty: [n]]. (1 to: 15) collect: [:m | fizzbuzz value: m]
=> #(1 2 'Fizz' 4 'Buzz' 'Fizz' 7 8 'Fizz' 'Buzz' 11 'Fizz' 13 14 'FizzBuzz')
Smalltalk 風に書くとこの処理の骨格は、
n asFizzString, n asBuzzString ifEmpty: [n]
つまり、n が 3 の倍数なら 'Fizz'(そうでなければ空文字列)と、n が 5 の倍数なら 'Buzz'(同)で得られた文字列を結合(#,)して結果が空文字でなければそれを。空文字列なら(#ifEmpty:) n を返す―というパターンですね。
もちろん、3、5 に加えて 15 をチェックしてその都度に 'Fizz'、'Buzz'、'FizzBuzz' を出力するもっとオーソドックスな書き方もできますが、個人的には 15 のときの条件を書くのは「負け」だと思っているので、このパターンが基本解かなと考えています。
話を戻して、n fizz buzz と書きたいと思うときの問題は、前半の n fizz の返値が 'Fizz' であったときに、その後にコールされる #buzz で 'Buzz' を付加すべきか否かの判断がつかないことにありました。前回はこの問題を解決するために n->fizzBuzz文字列 のようにキー値の組を返させてそのオブジェクトにも #buzz というメソッドを多態させようというアイデアを用いました。
結果、複数クラスに同名メソッドを定義する必要が生じたわけですが、その際、メタプログラミングっぽい方向に暴走してしまった結果コードが複雑になるという本末転倒なことになってしまってとても悲しかったので、似たようなことをトレイトを使ったメソッドの共有によりできないか、というのが今回の流れです。
あと前回と違い、わざわざキー値オブジェクトを新たに生成したりせず、同様の情報を単純に文字列として持たせることで泥くさく解決している点も異なります。
n は fizz あるいは buzz を受け取ると、必要なら自身を文字列化し、3 で割り切れれば 'Fizz' を、5 で割り切れれば 'Buzz' を付け足して返します。結果、'3Fizz' や '10Buzz' といった文字列が返されるので、しあげの value で #splitInteger を使い、数値部分と 、それに付加された文字列部分('Fizz'、'Buzz'、'FizzBuzz')を分離して必要な方だけを返させています。
参考まで、文字列は splitInteger を送られると次のように振る舞います。
'3' splitInteger "=> #('' 3) " '3abc' splitInteger "=> #(3 'abc') " 'abc3' splitInteger "=> #('abc' 3) "
'2' あるいは '3Fizz' に対して splitInteger を送ると、たまたま今回必要になる 2 あるいは 'Fizz' は配列の二番目にくるので、self asString splitInteger last とシンプルに書けました。おそらく今回はこの #splitInteger が主役ですね。きっと。
以上をまとめたものを、ワークスペースに貼り付けて print it(評価と結果の出力の操作。alt/cmd + p)できるコードで示します。
Trait named: #TFizzBuzz uses: {} category: 'Traits-FizzBuzz'
TFizzBuzz compile: 'fizz (self asInteger isDivisibleBy: 3) ifTrue: [^self asString, ''Fizz'']'; compile: 'buzz (self asInteger isDivisibleBy: 5) ifTrue: [^self asString, ''Buzz'']'; compile: 'value ^self asString splitInteger last'. Integer uses: TFizzBuzz. String uses: TFizzBuzz.
(1 to: 15) collect: [:m | m fizz buzz value]
=> #(1 2 'Fizz' 4 'Buzz' 'Fizz' 7 8 'Fizz' 'Buzz' 11 'Fizz' 13 14 'FizzBuzz')
「15 の場合をコードに書いたら負け」の根拠でもありますが、やはり仕様の追加に柔軟かつ最小限必要なコードの追加のみで対応できることは「もしかしてYAGNI?」と思いつつもついつい意識してしまいますね。たとえば 7 で割り切れたときの pezz を追加したければ、#pezz を次のように追加するだけでシンプルに済ますことができる点が今回の実装のミソでしょうか。
TFizzBuzz compile: 'pezz (self asInteger isDivisibleBy: 7) ifTrue: [^self asString, ''Pezz'']'. (1 to: 105) collect: [:m | m fizz buzz pezz value]
=> #(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')
参考: Ruby版。こんなふうに書いてみました。
module FizzBuzz def fizz; to_i % 3 == 0 ? to_s+"Fizz" : self end def buzz; to_i % 5 == 0 ? to_s+"Buzz" : self end def val; to_s.split(/(-?[0-9]+)/).last end end class Integer; include FizzBuzz end class String; include FizzBuzz end (1..15).collect{ |m| m.fizz.buzz.val }
追記: Scala版もググりまくりながら書いてみました。添削あるいはもっとエレガントな解への書き直しをお願いいたします。
object FizzBuzzTraitTest extends Application { trait FizzBuzz { def fizz: FizzBuzz = if (toInt % 3 == 0) toString + "Fizz" else this def buzz: FizzBuzz = if (toInt % 5 == 0) toString + "Buzz" else this def value: String = "[0-9]+|[^0-9]+".r.findAllIn(toString).toList.last override def toString: String def toInt: Int } class FizzBuzzInt(i: Int) extends FizzBuzz { override def toString = i.toString def toInt = i } class FizzBuzzString(s: String) extends FizzBuzz { override def toString = s def toInt = try { new Integer(s.takeWhile(Character.isDigit(_))) } catch { case e:Exception => 0 } } for (n <- 1 to 15) println( n.fizz.buzz.value ) implicit def intToFizzBuzzInt(i: Int) : FizzBuzzInt = new FizzBuzzInt(i) implicit def strToFizzBuzzStr(s: String) : FizzBuzzString = new FizzBuzzString(s) }