読者です 読者をやめる 読者になる 読者になる

( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

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も使おうかと思ったんだけどいい適用例がおもいつかなったので、今度別なネタで紹介します。