oxbow_lakes
oxbow_lakes

Reputation: 134270

Adding custom collection operations in scala 2.13 to arbitrary collections of specific types

Note - the operation described below now exists in the standard library as partitionMap but I believe it's still a valid question as to how to achieve more general ends

Question regarding scala 2.13 - how do I consume/construct collections of specific types when adding custom collections operations where I need to restrict the element types of the input collections? e.g. how do I define:

def split[CC[_], A, B](coll: CC[Either[A, B]]): (CC[A], CC[B])

Following the documentation I've managed to achieve this as follows:

import collection.generic.IsIterable
import scala.collection.{BuildFrom, Factory}

class SplitOperation[Repr, S <: IsIterable[Repr]](coll: Repr, itr: S) {
  def split[A, B, AS, BS](
    implicit bfa: BuildFrom[Repr, A, AS], 
             bfb: BuildFrom[Repr, B, BS], 
             ev: itr.A =:= Either[A, B]): (AS, BS) = {

    val ops = itr(coll)
    val as = bfa.fromSpecific(coll)(ops.iterator.map(ev).collect { case Left(a) => a })
    val bs = bfb.fromSpecific(coll)(ops.iterator.map(ev).collect { case Right(b) => b })

    (as, bs)
  }
}

implicit def SplitOperation[Repr](coll: Repr)(implicit itr: IsIterable[Repr]): SplitOperation[Repr, itr.type] =
new SplitOperation(coll, itr)

However, I need to supply types at the use-site otherwise I get diverging implicit expansion.

scala> List(Left("bah"), Right(1), Left("gah"), Right(2), Right(3))
res1: List[scala.util.Either[String,Int]] = List(Left(bah), Right(1), Left(gah), Right(2), Right(3))

scala> res1.split
            ^
       error: diverging implicit expansion for type scala.collection.BuildFrom[List[scala.util.Either[String,Int]],A,AS]

But the following works:

scala> res1.split[String, Int, List[String], List[Int]]
res4: (List[String], List[Int]) = (List(bah, gah),List(1, 2, 3))

EDIT

class SplitOperation[X, CC[_], S <: IsIterable[CC[X]]](coll: CC[X], itr: S) {
  def split[A, B](implicit bfa: BuildFrom[CC[X], A, CC[A]], bfb: BuildFrom[CC[X], B, CC[B]], ev: itr.A =:= Either[A, B]): (CC[A], CC[B]) = {
    val ops = itr(coll)
    val as = bfa.fromSpecific(coll)(ops.iterator.map(ev).collect { case Left(a) => a })
    val bs = bfb.fromSpecific(coll)(ops.iterator.map(ev).collect { case Right(b) => b })

    (as, bs)
  }
}

implicit def SplitOperation[A, B, CC[_]](coll: CC[Either[A, B]])(implicit itr: IsIterable[CC[Either[A, B]]]): SplitOperation[Either[A, B], CC, itr.type] =
  new SplitOperation(coll, itr)

Gives me a slight improvement. Now I only need to provide type parameters A and B at the call site:

scala> l.split[String, Int]
res2: (List[String], List[Int]) = (List(bah, gah),List(1, 2))

Upvotes: 3

Views: 480

Answers (2)

Julien Richard-Foy
Julien Richard-Foy

Reputation: 9663

In your case you don’t want to abstract over the “kind” of the collection type constructor (CC[_] vs CC[_, _], etc.), you always use the CC[_] kind, so you don’t need to use IsIterable.

I think it is also not necessary to support “Sorted” collections (eg, SortedSet) because there is no Ordering instance for Either, so you don’t need to use BuildFrom.

implicit class SplitOperation[A, B, CC[X] <: IterableOps[X, CC, CC[X]]](coll: CC[Either[A, B]]) {
  def split: (CC[A], CC[B]) = {
    val as = coll.iterableFactory.from(coll.iterator.collect { case Left(a) => a })
    val bs = coll.iterableFactory.from(coll.iterator.collect { case Right(b) => b })
    (as, bs)
  }
}

https://scastie.scala-lang.org/64QxHwteQN2i3udSxCa3yw

Upvotes: 3

oxbow_lakes
oxbow_lakes

Reputation: 134270

This seems to work:

class SplitOperation[A, B, CC[_], S <: IsIterable[CC[Either[A, B]]]](coll: CC[Either[A, B]], itr: S) {
  def split(implicit bfa: BuildFrom[CC[Either[A, B]], A, CC[A]], bfb: BuildFrom[CC[Either[A, B]], B, CC[B]], ev: itr.A =:= Either[A, B]): (CC[A], CC[B]) = {
    val ops = itr(coll)
    val as = bfa.fromSpecific(coll)(ops.iterator.map(ev).collect { case Left(a) => a })
    val bs = bfb.fromSpecific(coll)(ops.iterator.map(ev).collect { case Right(b) => b })

    (as, bs)
  }
}

implicit def SplitOperation[A, B, CC[_]](coll: CC[Either[A, B]])(implicit itr: IsIterable[CC[Either[A, B]]]): SplitOperation[A, B, CC, itr.type] =
  new SplitOperation(coll, itr)

Upvotes: 4

Related Questions