Implicit class for subtypes of a generic class
Asked Answered
S

3

6

I want to enhance all Iterables with some custom code. For this I wrote the following:

implicit class RichIterable[A, B <: Iterable[A]](b: B) {
  def nonEmptyOpt: Option[B] = if (b.nonEmpty) Some(b) else None
}

Now, when I want to use this method on a List that definitely is a subclass of Iterable like so

List(1, 2, 3).nonEmptyOpt

I get

value nonEmptyOpt is not a member of List[Int]

How can I resolve this?

Sultan answered 25/8, 2016 at 15:16 Comment(0)
I
9

Given a parameter with only type B <: Iterable[A], the compiler doesn't know how to easily figure out what A is, because it is not necessarily easily computed from B (needing to search for least upper-bounds).

Instead you can do this by redefining the type constraints, without using tricks. Essentially, B should really be a type constructor that is bounded above by Iterable. Then, your implicit class is a conversion from some B[A] to your enriched class. Having a parameter of B[A] helps the compiler compute A, because it expects it to be the argument of the type constructor B.

implicit class RichIterable[A, B[X] <: Iterable[X]](b: B[A]) {
  def nonEmptyOpt: Option[B[A]] = if (b.nonEmpty) Some(b) else None
}

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

scala> List.empty[Int].nonEmptyOpt
res1: Option[List[Int]] = None
Ilarrold answered 25/8, 2016 at 16:29 Comment(3)
Just curious, is anything lost/changed by going "existential"? RichIterable[A, B[_] <: Iterable[_]](b: B[A]) {...Boatright
@Boatright This loses the exacta return type for all subclasses that don't have exactly 1 type parameter, that is the same as in the Iterable they extend. Some examples of that are Map[A, B] or scala.xml.NodeSeq. E.g., Map(1->2).nonEmptyOpt will be Option[Iterable[...]], not Option[Map[...]]Burbage
@Kolmar, I've been going back and forth between B[X] ... and B[_] ..., and so far I've been unable to discern any difference in Map() handling.Boatright
D
7

Little trick I stumbled upon once:

scala> implicit class RichIterable[A, B <: Iterable[A]](b: B with Iterable[A]) {
 |   def nonEmptyOpt: Option[B] = if (b.nonEmpty) Some(b) else None
 | }
defined class RichIterable

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

Note the B with Iterable[A] on the parameter.

By the way, when debugging implicits, it helps sometimes to try to apply them explicitly (before change):

scala> new RichIterable(List(1,2,3)).nonEmptyOpt
<console>:15: error: inferred type arguments [Nothing,List[Int]] do not conform to class RichIterable's type parameter bounds [A,B <: Iterable[A]]
          new RichIterable(List(1,2,3)).nonEmptyOpt

So, the compiler is having a hard time figuring out the type of A. The type refinement apparently helps it along.

Distressed answered 25/8, 2016 at 15:23 Comment(0)
S
0

A simpler solution would be:

implicit class RichIterable[A](b: Iterable[A]) {
  def nonEmptyOpt: Option[Iterable[A]] = if (b.nonEmpty) Some(b) else None
}

scala> List(1,2,3).nonEmptyOpt
res0: Option[Iterable[Int]] = Some(List(1, 2, 3))
Supranational answered 25/8, 2016 at 18:38 Comment(1)
The problem with this approach is you've converted a List to an Iterable.Pyrolysis

© 2022 - 2024 — McMap. All rights reserved.