Sometimes, I find myself wishing scala collections to include some missing functionality, and it's rather easy "extending" a collection, and provide a custom method.
This is a bit more difficult when it comes to building the collection from scratch.
Consider useful methods such as .iterate
.
I'll demonstrate the usecase with a similar, familiar function: unfold
.
unfold
is a method to construct a collection from an initial state z: S
, and a function to generate an optional tuple of the next state, and an element E
, or an empty option indicating the end.
the method signature, for some collection type Coll[T]
should look roughly like:
def unfold[S,E](z: S)(f: S ⇒ Option[(S,E)]): Coll[E]
Now, IMO, the most "natural" usage should be, e.g:
val state: S = ??? // initial state
val arr: Array[E] = Array.unfold(state){ s ⇒
// code to convert s to some Option[(S,E)]
???
}
This is pretty straight forward to do for a specific collection type:
implicit class ArrayOps(arrObj: Array.type) {
def unfold[S,E : ClassTag](z: S)(f: S => Option[(S,E)]): Array[E] = {
val b = Array.newBuilder[E]
var s = f(z)
while(s.isDefined) {
val Some((state,element)) = s
b += element
s = f(state)
}
b.result()
}
}
with this implicit class in scope, we can generate an array for the Fibonacci seq like this:
val arr: Array[Int] = Array.unfold(0->1) {
case (a,b) if a < 256 => Some((b -> (a+b)) -> a)
case _ => None
}
But if we want to provide this functionality to all other collection types, I see no other option than to C&P the code, and replace all Array
occurrences with List
,Seq
,etc'...
So I tried another approach:
trait BuilderProvider[Elem,Coll] {
def builder: mutable.Builder[Elem,Coll]
}
object BuilderProvider {
object Implicits {
implicit def arrayBuilderProvider[Elem : ClassTag] = new BuilderProvider[Elem,Array[Elem]] {
def builder = Array.newBuilder[Elem]
}
implicit def listBuilderProvider[Elem : ClassTag] = new BuilderProvider[Elem,List[Elem]] {
def builder = List.newBuilder[Elem]
}
// many more logicless implicits
}
}
def unfold[Coll,S,E : ClassTag](z: S)(f: S => Option[(S,E)])(implicit bp: BuilderProvider[E,Coll]): Coll = {
val b = bp.builder
var s = f(z)
while(s.isDefined) {
val Some((state,element)) = s
b += element
s = f(state)
}
b.result()
}
Now, with the above in scope, all one needs is an import for the right type:
import BuilderProvider.Implicits.arrayBuilderProvider
val arr: Array[Int] = unfold(0->1) {
case (a,b) if a < 256 => Some((b -> (a+b)) -> a)
case _ => None
}
but this doesn't fell right also. I don't like forcing the user to import something, let alone an implicit method that will create a useless wiring class on every method call. Moreover, there is no easy way to override the default logic. You can think about collections such as Stream
, where it would be most appropriate to create the collection lazily, or other special implementation details to consider regarding other collections.
The best solution I could come up with, was to use the first solution as a template, and generate the sources with sbt:
sourceGenerators in Compile += Def.task {
val file = (sourceManaged in Compile).value / "myextensions" / "util" / "collections" / "package.scala"
val colls = Seq("Array","List","Seq","Vector","Set") //etc'...
val prefix = s"""package myextensions.util
|
|package object collections {
|
""".stripMargin
val all = colls.map{ coll =>
s"""
|implicit class ${coll}Ops[Elem](obj: ${coll}.type) {
| def unfold[S,E : ClassTag](z: S)(f: S => Option[(S,E)]): ${coll}[E] = {
| val b = ${coll}.newBuilder[E]
| var s = f(z)
| while(s.isDefined) {
| val Some((state,element)) = s
| b += element
| s = f(state)
| }
| b.result()
| }
|}
""".stripMargin
}
IO.write(file,all.mkString(prefix,"\n","\n}\n"))
Seq(file)
}.taskValue
But this solution suffers from other issues, and is hard to maintain. just imagine if unfold
is not the only function to add globally, and overriding default implementation is still hard. bottom line, this is hard to maintain and does not "feel" right either.
So, is there a better way to achieve this?