Max Maier
Max Maier

Reputation: 1045

A case class as a "wrapper" class for a collection. What about map/foldLeft/

What I try to do is to come up with a case class which I can use in pattern matching which has exactly one field, e.g. an immutable set. Furthermore, I would like to make use of functions like map, foldLeft and so on which should be passed down to the set. I tried it as in the following:

case class foo(s:Set[String]) extends Iterable[String] {
    override def iterator = s.iterator
}

Now if I try to make use of e.g. the map function, I get an type error:

var bar = foo(Set() + "test1" + "test2")
bar = bar.map(x => x)

 found   : Iterable[String]
 required: foo
   bar = bar.map(x => x)
                ^

The type error is perfectly fine (in my understanding). However, I wonder how one would implement a wrapper case class for a collection such that one can call map, foldLeft and so on and still receive an object of the case class. Would one need to override all these functions or is there some other way around?

Edit

I'm inclined to accept the solution of Régis Jean-Gilles which works for me. However, after Googling for hours I found another interesting Scala trait named SetProxy. I couldn't find any trivial examples so I'm not sure if this trait does what I want:

  1. come up with a custom type, i.e. a different type than Set
  2. the type must be a case class (we want to do pattern matching)
  3. we need "delegate" methods map, foldLeft and so on which should pass the call to our actual set and return the resulting set wrapped arround in our new type

My first idea was to extend Set but my custom type Foo already extends another class. Therefore, the second idea was to mixin the trait Iterable and IterableLike. Now I red about the trait SetProxy which made me think about which is "the best" way to go. What are your thoughts and experiences?

Since I started learning Scala three days ago, any pointers are highly appreciated!

Upvotes: 4

Views: 875

Answers (2)

Régis Jean-Gilles
Régis Jean-Gilles

Reputation: 32719

Hmm this sounds promissing to me but Scala says that variable b is of type Iterable[String] and not of type Foo, i.e. I do not see how IterableLike helps in this situation

You are right. Merely inheriting from IterableLike as shown by mpartel will make the return type of some methods more precise (such as filter, which will return Foo), but for others such as map of flatMap you will need to provide an appopriate CanBuildFrom implicit. Here is a code snippet that does just that:

import collection.IterableLike
import collection.generic.CanBuildFrom
import collection.mutable.Builder

case class Foo( s:Set[String] ) extends Iterable[String] with IterableLike[String, Foo] {
  override def iterator = s.iterator
  override protected[this] def newBuilder: scala.collection.mutable.Builder[String, Foo] = new Foo.FooBuilder
  def +(elem: String ): Foo = new Foo( s + elem )
}

object Foo  {
  val empty: Foo = Foo( Set.empty[String] )
  def apply( elems: String* ) = new Foo( elems.toSet )

  class FooBuilder extends Builder[String, Foo] {
    protected var elems: Foo = empty
    def +=(x: String): this.type = { elems = elems + x; this }
    def clear() { elems = empty }
    def result: Foo = elems
  }

  implicit def canBuildFrom[T]: CanBuildFrom[Foo, String, Foo] = new CanBuildFrom[Foo, String, Foo] {
    def apply(from: Foo) = apply()
    def apply() = new FooBuilder
  }
}

And some test in the repl:

scala> var bar = Foo(Set() + "test1" + "test2")
bar: Foo = (test1, test2)

scala> bar = bar.map(x => x) // compiles just fine because map now returns Foo
bar: Foo = (test1, test2)

Upvotes: 5

mpartel
mpartel

Reputation: 4492

Inheriting IterableLike[String, Foo] gives you all those methods such that they return Foo. IterableLike requires you to implement newBuilder in addition to iterator.

import scala.collection.IterableLike
import scala.collection.mutable.{Builder, SetBuilder}

case class Foo(stuff: Set[String]) extends Iterable[String] with IterableLike[String, Foo] {
  def iterator: Iterator[String] = stuff.iterator

  protected[this] override def newBuilder: Builder[String, Foo] = {
    new SetBuilder[String, Set[String]](Set.empty).mapResult(Foo(_))
  }
}

// Test:
val a = Foo(Set("a", "b", "c"))
val b = a.map(_.toUpperCase)
println(b.toList.sorted.mkString(", "))  // Prints A, B, C

Upvotes: 1

Related Questions