Scalaの奇妙なFizzBuzz - PartialFunctionとimplicit conversionを添えて
前回書いたPartialFunctionの利用例として、奇妙なFizzBuzzを書いてみましたよ。
// PartialFunction[A,B]を "A --> B"のように書けるようにしておく type -->[A,B] = PartialFunction[A,B] // FizzBuzz用のPartailFunctionを生成するユーティリティ def toPF[A]( r:String )( f: A => Boolean): A --> String = { case v if f(v) => r } // FizzBuzzできるSeq class FizzBuzzSeq[A]( theSeq:Seq[A] ) extends Seq[A] { def apply(idx: Int):A = theSeq( idx ) def length = theSeq.length def iterator = theSeq.iterator val defaultPF:A --> String = { case v => v.toString } def fizzBuzz( pfSeq:A --> String * ) = theSeq.collect( ( pfSeq :+ defaultPF ) reduceLeft { (a,b) => a orElse b } ) } // implicit conversionでSeq[A]をFizzBuzzSeq[A]にする implicit def seqToFizzBuzz[A]( theSeq:Seq[A] ) = new FizzBuzzSeq( theSeq ) // PatrialFunctionを渡してFizzBuzz // implicit conversionで(1 to 100)はFizzBuzzSeq[Int]になる (1 to 100 ).fizzBuzz( toPF( "FizzBuzz") { v => v % 15 == 0 }, toPF( "Fizz") { v => v % 3 == 0 }, toPF( "Buzz") { v => v % 5 == 0 } ).foreach{ println } // こう書いても同じ (1 to 100 ).fizzBuzz( { case v if v % 15 == 0 => "FizzBuzz" }, { case v if v % 3 == 0 => "Fizz" }, { case v if v % 5 == 0 => "Buzz" } ).foreach{ println }
解説など
まず、type aliasでPartialFunction[A,B]を "A --> B"のように書けるようにしておきます。Intを受け取ってStringを返すPFは"Int --> String" と書けて関数リテラルっぽくてカコイイですよね?
// PartialFunction[A,B]を "A --> B"のように書けるようにしておく type -->[A,B] = PartialFunction[A,B]
次に、返すべき結果をStringで受け取り、caseのifのパターンガードに適用する"A => Boolean"型の関数オブジェクトを受け取ってPFを生成するユーティリティを用意しますよ。
// FizzBuzz用のPartailFunctionを生成するユーティリティ def toPF[A]( r:String )( f: A => Boolean): A --> String = { case v if f(v) => r }
これで"val pf3 = toPF("Fizz"){ v => v % 3 == 0 }"のようにPFを生成できると。
そいでもって、FizzBuzzできるクラスであるFizzBuzzSeqを定義します。コイツはFizzBuzz対象のSeq[A]を持っていて、fizzBuzzメソッドで引数にもらったPFをorElseで連結して、対象のSeq[A]のcollectにPFを渡してFizzBuzzさせます。
// FizzBuzzできるSeq class FizzBuzzSeq[A]( theSeq:Seq[A] ) extends Seq[A] { def apply(idx: Int):A = theSeq( idx ) def length = theSeq.length def iterator = theSeq.iterator val defaultPF:A --> String = { case v => v.toString } def fizzBuzz( pfSeq:A --> String * ) = theSeq.collect( ( pfSeq :+ defaultPF ) reduceLeft { (a,b) => a orElse b } ) }
ポイントは、fizzBuzzメソッドの引数PFに一致しない場合のデフォルトを"val defaultPF:A --> String = { case v => v.toString }"で定義してあって、引数のpfSeq(可変長引数なのでArray[ A --> String]型)にくっつけた後、reduceLeftですべてのPFをorElseで連結した新しいPFを作っているとこですね。
foldLeftとかreduceLeftとか大好きすぎて生きてるのがツライ。
そして、対象のSeq[A]に前回紹介したcollect[B, That](pf: PartialFunction[A, B]): Thatを連結したPFを引数に呼び出すと、FizzBuzzされたSeq[String]ができるって寸法さぁ!
で、implicit conversionを利用してSeq[A]をFizzBuzzできるFizzBuzzSeq[A]型に変換するための関数を定義しておきますよ。これで"(1 to 100).fizzbuzz(...)"でPF渡してFizzBuzzできるようになりますね。
// implicit conversionでSeq[A]をFizzBuzzSeq[A]にする implicit def seqToFizzBuzz[A]( theSeq:Seq[A] ) = new FizzBuzzSeq( theSeq )
実際にFizzBuzzするには、FizzBuzzするものを作ってそいつに対してfizzBuzzメソッドをPFを引数に呼び出せばOK。implicit conversionでFizzBuzzSeqに変換されたあとにfizzBuzzメソッドが呼ばれます。PFを作るには、さっき定義したtoPF[A]( r:String)(f:A => Boolean)でやってます。
// implicit conversionで(1 to 100)はFizzBuzzSeq[Int]になる (1 to 100 ).fizzBuzz( toPF( "FizzBuzz") { v => v % 15 == 0 }, toPF( "Fizz") { v => v % 3 == 0 }, toPF( "Buzz") { v => v % 5 == 0 } ).foreach{ println } // こう書いても同じ (1 to 100 ).fizzBuzz( { case v if v % 15 == 0 => "FizzBuzz" }, { case v if v % 3 == 0 => "Fizz" }, { case v if v % 5 == 0 => "Buzz" } ).foreach{ println }
ほんとは、implicit parameterやextractorも使おうかと思ったんだけどいい適用例がおもいつかなったので、今度別なネタで紹介します。