Listを扱うプログラムを書く時に、match式で::を使ってheadとtailを取り出すことが良くあると思う。
例えば、「二つのList a, bがあり、bがaに前方一致するならaからbを取り除いた残りを戻す」というような処理をmatchを使って書くと*1、こんな風になるかと思う。
def dropWith[A](a: List[A], b: List[A]): Option[List[A]] = { (a, b) match { case (_, Nil) => Some(a) case (Nil, _) => None case (h1 :: _, h2 :: _) if h1 != h2 => None case (h1 :: t1, h2 :: t2) => dropWith(t1, t2) } }
scala> dropWith(List(1, 2, 3, 4, 5), List(1, 2)) res1: Option[List[Int]] = Some(List(3, 4, 5)) scala> dropWith(List(1, 2, 3, 4, 5), List(1, 3)) res2: Option[List[Int]] = None
これを拡張して、一つ目の引数が無限リスト(というかStream)でもOKにしたい。それで一つ目の引数の型をSeqにしてみたんだけど、List以外を指定すると「::」でマッチさせようとしてもうまく動かない。「::」に加えて「#::」も書いておくとStreamの場合にも対応できるけど、表現が冗長だし、結局ListとStream以外(例えばRange)が来た場合に動かない。*2
// scalaVersion := "2.9.1" def dropWith[A](a: Seq[A], b: List[A]): Option[Seq[A]] = { (a, b) match { case (_, Nil) => Some(a) case (Nil, _) => None case (h1 :: _, h2 :: _) if h1 != h2 => None case (h1 :: t1, h2 :: t2) => dropWith(t1, t2) case (h1 #:: _, h2 :: _) if h1 != h2 => None case (h1 #:: t1, h2 :: t2) => dropWith(t1, t2) } }
scala> dropWith(Stream.from(1), List(1, 2)) res1: Option[Seq[Int]] = Some(Stream(3, ?)) scala> dropWith(1 to 5, List(1, 2)) scala.MatchError: (Range(1, 2, 3, 4, 5),List(1, 2)) (of class scala.Tuple2) ...
どうしたらいいかなーと思って検索してると、extractorを自分で定義している例を見つけた。
scala> val fff = new { def unapply[A, T](x : { def head : A; def tail : T;
def isEmpty : Boolean }) = if(x.isEmpty) None else Some(x.head, x.tail) }
fff: java.lang.Object{def unapply[A,T](AnyRef{def head: A; def tail: T;
def isEmpty: Boolean}): Option[(A, T)]} = $anon$1@32b90a0scala> fff.unapply(List(1,2))
res112: Option[(Int, List[Int])] = Some((1,List(2)))scala> List(1,2,3) match { case x fff xs => (x, xs
)} res113: (Int, List[Int]) = (1,List(2, 3))
scala> Stream(1,2,3) match { case x fff xs => (x, xs)}
Using the foldLeft operator /: | The Scala Programming Language
res114: (Int, Stream[Int]) = (1,Stream(2, ?))
上の例では構造的部分型を使っているけど、ドキュメントを見るとtailは共通の親traitであるSeqLikeに定義されているので、SeqLikeに対してextractorを用意してやれば良いと思う。
import collection.SeqLike def dropWith[A, S <% SeqLike[A, S]](a:S, b:Seq[A]): Option[S] = { object ::#:: { def unapply[T <% SeqLike[A, T]](x: T) = if (x.isEmpty) None else Some(x.head, x.tail) } (a, b) match { case (_, Nil) => Some(a) case (Nil, _) => None case (h1 ::#:: _, h2 ::#:: _) if h1 != h2 => None case (h1 ::#:: t1, h2 ::#:: t2) => dropWith(t1, t2) } }
scala> dropWith(List(1, 2, 3, 4, 5), List(1, 2)) res0: Option[List[Int]] = Some(List(3, 4, 5)) scala> dropWith(Stream.from(1), List(1, 2)) res1: Option[scala.collection.immutable.Stream[Int]] = Some(Stream(3, ?)) scala> dropWith("pineapple", "pine") res2: Option[java.lang.String] = Some(apple) scala> dropWith(1 to 5, 1 to 3) <console>:11: error: No implicit view available from scala.collection.immutable.Range.Inclusive => scala.collection.SeqLike[Int,scala.collection.immutable.Range.Inclusive]. dropWith(1 to 5, 1 to 3) ^
うーん、Rangeでは結局上手くいかない。RnageはSeqLike[Int, Range]ではなくSeqLike[Int, IndexedSeq[Int] ]を継承しているせいみたい。
ちなみにStreamが::をサポートしていない理由は「Scala by Example」に書いてあって、::がtailをListとして事前に評価してしまうから(遅延評価を旨とするStreamとは合わない)らしい。
追記: やっぱり構造的部分型で受けてみた
object :#: { def unapply[A, T <% {def head: A; def tail: T; def isEmpty: Boolean}](x: T): Option[(A, T)] = if (x.isEmpty) None else Some(x.head, x.tail) } @tailrec def dropWith[A, T <% {def head: A; def tail: T; def isEmpty: Boolean}](a: T, b: Seq[A]): Option[T] = { (a, b) match { case (_, Nil) => Some(a) case (Nil, _) => None case (h1 :#: _, h2 :#: _) if h1 != h2 => None case (h1 :#: t1, h2 :#: t2) => dropWith(t1, t2) } }
scala> dropWith(Stream.from(1), List(1, 2)) res0: Option[scala.collection.immutable.Stream[Int]] = Some(Stream(3, ?)) scala> dropWith("pineapple", "pine") res1: Option[java.lang.String] = Some(apple) scala> dropWith(1 to 5, 1 to 3) <console>:11: error: No implicit view available from scala.collection.immutable.Range.Inclusive => AnyRef{def head: Int; def tail: scala.collection.immutable.Range.Inclusive; def isEmpty: Boolean}. dropWith(1 to 5, 1 to 3) ^ scala> dropWith[Int,Range](1 to 5, 1 to 3) res3: Option[Range] = Some(Range(4, 5))