ov7a
ov7a

Reputation: 1594

How to make a generic implicit class for any collection in Scala?

I want to write a method, which returns None if a collection is empty and Some(collection) in other case.

Best I can get is

implicit class CollectionExtensions[A, Repr](self: TraversableLike[A, Repr]){
  def toOption: Option[Repr] = if (self.nonEmpty) Some(self.asInstanceOf[Repr]) else None
}

But casting .asInstanceOf[Repr] seems wrong. What is the correct way?

Upvotes: 3

Views: 799

Answers (2)

dk14
dk14

Reputation: 22374

In order to recover original Repr type, you can use self.repr (scaladoc)

implicit class CollectionExtensions[A, Repr](self: TraversableLike[A, Repr]){
  def toOption: Option[Repr] = if (self.nonEmpty) Some(self.repr) else None
}

If you would just stick with Option[TraversableLike[A, Repr]] as @chengpohi's answer suggests, operations like map on it (list.toOption.map(_.map(x => x))) would return you Option[Traversable[T]] loosing the original Repr type (like List[Int]). repr helps with that:

def repr: Repr

The collection of type traversable collection underlying this TraversableLike object. By default this is implemented as the TraversableLike object itself, but this can be overridden.


However, the funny thing is, if you look at repr's code (here):

def repr: Repr = this.asInstanceOf[Repr]

It does same thing, but at least it wrapped (hidden?) nicely in scala-library and more abstract, so by using it you could account for potential redefinitions.


Also, it worth mentioning that this non-empty-collection approach is popular in scalaz/cats:

scala> import scalaz._; import Scalaz._
import scalaz._
import Scalaz._

scala> List(1, 2, 3).toNel
res8: Option[scalaz.NonEmptyList[Int]] = Some(NonEmptyList(1, 2, 3))

scala> nil[Int].toNel
res9: Option[scalaz.NonEmptyList[Int]] = None

toNel here means toNonEmptyList, so it's not as abstract as your solution. Cats have OneAnd[A,Repr] and some helpful implicits. See http://typelevel.org/cats/datatypes/oneand.html

Upvotes: 4

Kolmar
Kolmar

Reputation: 14224

Here are some other approaches you can use in addition to @dk14 answer:

  • Use an implicit class for Repr with TraversableOnce[A]. This will also support Iterator, because Iterator extends TraversableOnce, but not TraversableLike.

    implicit class CollectionExtensions[A, Repr](val self: Repr with TraversableOnce[A]) extends AnyVal {
      def toOption: Option[Repr] = if (self.nonEmpty) Some(self) else None
    }
    
  • Use an implicit class just for Repr, but request evidence that it's implicitly convertible to Traversable. This approach also supports Arrays and Strings, because they don't extend Traversable at all, but are implicitly convertible to it.

    implicit class CollectionExtensions[Repr](val self: Repr) extends AnyVal {
      def toOption[A](implicit ev: Repr => TraversableOnce[A]): Option[Repr] = {
        val traversable = ev(self)
        if (traversable.isEmpty) None else Some(self)
      }
    }
    

Both of those approaches preserve the original type:

scala> List(1, 2, 3).toOption
res1: Option[List[Int]] = Some(List(1, 2, 3))

scala> Iterator(1, 2, 3).toOption
res2: Option[Iterator[Int]] = Some(non-empty iterator)

scala> Array.empty[Int].toOption
res3: Option[Array[Int]] = None

scala> Map(1 -> 2).toOption
res4: Option[scala.collection.immutable.Map[Int,Int]] = Some(Map(1 -> 2))

scala> "123".toOption
res5: Option[String] = Some(123)

scala> "".toOption
res6: Option[String] = None

Upvotes: 6

Related Questions