oxbow_lakes
oxbow_lakes

Reputation: 134260

Is there a built-in more elegant way of filtering-and-mapping a collection by element type?

If I want to narrow, say, an Iterable[A] for all elements of a particular type (e.g. String) I can do:

as filter { _.isInstanceOf[String] }

However, it's obviously desirable to use this as an Iterable[String] which can be done via a map:

as filter { _.isInstanceOf[String] } map { _.asInstanceOf[String] }

Which is pretty ugly. Of course I could use flatMap instead:

as flatMap[String] { a => 
  if (a.isInstanceOf[String]) 
    Some(a.asInstanceOf[String]) 
  else
    None
}

But I'm not sure that this is any more readable! I have written a function, narrow, which can be used via implicit conversions:

as.narrow(classOf[String])

But I was wondering if there was a better built-in mechanism which I have overlooked. Particularly as it would be nice to be able to narrow a List[A] to a List[String], rather than to an Iterable[String] as it will be with my function.

Upvotes: 4

Views: 696

Answers (4)

Daniel C. Sobral
Daniel C. Sobral

Reputation: 297155

The Scala syntax sugar for isInstanceOf / asInstanceOf is pattern matching:

as flatMap { case x: String => Some(x); case _ => None }

Because that uses flatMap, it should usually return the same collection you had to begin with.

On Scala 2.8, there's an experimental function that does that kind of pattern, defined inside the object PartialFunction. So, on Scala 2.8 you can do:

as flatMap (PartialFunction.condOpt(_ : Any) { case x: String => x })

Which looks bigger mostly because I did not import that function first. But, then again, on Scala 2.8 there's a more direct way to do it:

as collect { case x: String => x }

Upvotes: 8

psp
psp

Reputation: 12158

Shape preserving: I'm a bit rushed right now so I'm leaving the cast in there, but I'm pretty sure it can be eliminated. This works in trunk:

import reflect.Manifest
import collection.Traversable
import collection.generic.CanBuildFrom
import collection.mutable.ListBuffer

object narrow {
  class Narrower[T, CC[X] <: Traversable[X]](coll: CC[T])(implicit m1: Manifest[CC[T]], bf: CanBuildFrom[CC[T], T, CC[T]]) {
    def narrow[B: Manifest]: CC[B] = {
      val builder = bf(coll)      
      def isB(x: T): Option[T] = if (Manifest.singleType(x) <:< manifest[B]) Some(x) else None

      coll flatMap isB foreach (builder += _)
      builder mapResult (_.asInstanceOf[CC[B]]) result
    }
  }

  implicit def toNarrow[T, CC[X] <: Traversable[X]](coll: CC[T])(implicit m1: Manifest[CC[T]], bf: CanBuildFrom[CC[T], T, CC[T]]) = 
    new Narrower[T,CC](coll)

  def main(args: Array[String]): Unit = {
    println(Set("abc", 5, 5.5f, "def").narrow[String])
    println(List("abc", 5, 5.5f, "def").narrow[String]) 
  }
}

Running it:

Set(abc, def)
List(abc, def)

Upvotes: 1

Eastsun
Eastsun

Reputation: 18859

You may use in future:

for(a :Type <- itr) yield a

But it doesn't work now. For more information, go to the following links: http://lampsvn.epfl.ch/trac/scala/ticket/1089 http://lampsvn.epfl.ch/trac/scala/ticket/900

Upvotes: 1

Daniel Spiewak
Daniel Spiewak

Reputation: 55115

For the record, here's a full implementation of narrow. Unlike the signature given in the question, it uses an implicit Manifest to avoid some characters:

implicit def itrToNarrowSyntax[A](itr: Iterable[A]) = new {
  def narrow[B](implicit m: Manifest[B]) = {
    itr flatMap { x => 
      if (Manifest.singleType(x) <:< m)
        Some(x)
      else
        None
    }
  }
}

val res = List("daniel", true, 42, "spiewak").narrow[String]  
res == Iterable("daniel", "spiewak")

Unfortunately, narrowing to a specific type (e.g. List[String]) rather than Iterable[String] is a bit harder. It can be done with the new collections API in Scala 2.8.0 by exploiting higher-kinds, but not in the current framework.

Upvotes: 3

Related Questions