vuvuzela
vuvuzela

Reputation: 111

Is it possible to work with a list of generic values with different type parameters in Scala?

I want to achieve the following:

  1. There is a list of strings I need to process.
  2. There are several different kinds of these processors, each of which knows which part of the string to read.
  3. I need to work in 2 phases: first, processors need to see each input string to build processor-specific data; second, each input string is processed by each of the processors, and the resulting strings are combined into one.

It's easy to do it in a mutable way: there's a common base class for all processors, the different kinds of data they aggregate is encapsulated in the concrete implementations; the interface consists of just 2 functions --- "look at input string and build internal data" and "process input string using your internal data."

As I am writing it in Scala, I am wondering if there exists a pure functional approach. The problem is that now the base trait for these processors is parameterized by the type of their internal data, and there doesn't seem to be a way to have a list of processors of different kinds.

This problem can be demonstrated on a simpler case: say I'd stick with the mutable approach, but for some reason have parameterized the type of what the processor takes from the string:

trait F[V] {
  def get(line: String) : V
  def aggregate(value: V)
  def process(value: V) : String
}

class F1 extends F[Int] // ...
class F2 extends F[HashMap[Int, Int]] // ...

for (s <- List("string1", "string2"); 
  f <- List(new F1(), new F2()) 
{
  f.aggregate(f.get(s)); // Whoops --- doesn't work   
}

It doesn't work because f.get(s) returns Any. Looks like I need to express in Scala's type system that List(new F1(), new F2()) contains F[?] that are different but consistent in that if I take an element of that list, it has some concrete value of its type parameter, and f.get(s) is of that type, which should be accepted by f.aggregate().

In the end, I would like to have something like this (with omissions because I don't get how to do it):

trait F[D] {
  def initData : D
  def aggregate(line: String, data: D) : D
  def process(line: String, data: D) : String
}

class F1 extends F[Int] // ...
class F2 extends F[HashMap[Int, Int]] // ...

// Phase 1
// datas --- List of f.initData, how to?
for (s <- List("string1", "string2")) {
  for (f <- List(new F1(), new F2()) {
    // let fdata be f's data
    // update fdata with f.aggregate(s, fdata)
  }
}

// Phase 2
for (s <- List("string1", "string2")) {
  for (f <- List(new F1(), new F2()) {
    // let fdata be f's data
    // for all fs, concatenate f.process(s, fdata) into an output string
  }
}

Questions:

  1. Is this task solvable in pure functional way in Scala?
  2. Is this task solvable in other functional languages?
  3. This situation looks like quite a general one. Is there a name for it I could search?
  4. Where is the best place to read about it, assuming little to no background on theory of types and functional programming languages?

Upvotes: 1

Views: 182

Answers (2)

Dirk
Dirk

Reputation: 31053

Edit Just noticed, that my former solution was overly verbose, consing up a temporary data structure without any need.

I am not sure, what you mean with "purely functional". The following solution (if it is a solution to your problem) is "purely functional", as it has no side effects except the final println call in main.

Note, that the List[F[_]](...) is important, since otherwise, the compiler will infer a very specific internal type for the elements of the list, which doesn't go well with the aggregateAndProcess function.

trait F[D] {

    type Data = D  // Abbreviation for easier copy+paste below. Does not
                       // contribute to the actual solution otherwise

    def initData: Data
    def aggregate(line: String, data: Data) : Data
    def process(line: String, aggData: Data): String
}

class F1 extends F[Int] {
    def initData: Data = 1
    def aggregate(line: String, data: Data) : Data = data + 1
    def process(line: String, aggData: Data): String = line + "/F1" + aggData
}

class F2 extends F[Boolean] {
    def initData: Data = false
    def aggregate(line: String, data: Data) : Data = !data
    def process(line: String, aggData: Data): String = line + "/F2" + aggData
}

object Main {

    private def aggregateAndProcess[T](line: String, processor: F[T]): String =
        processor.process(line, processor.aggregate(line, processor.initData))

    def main(args: Array[String]) {

        val r = for {
            s <- List("a", "b")
            d <- List[F[_]](new F1, new F2)
        } yield
            aggregateAndProcess(s, d)

        println(r.toList)
    }
}

Note, though, that I am still unsure as to what you actually want to accomplish. The F interface doesn't really specify, which information flows from which method into whatever location at what time, so: this is still a best-guess efford.

Upvotes: 0

alno
alno

Reputation: 3616

Also, you may use abstract types instead of generics, so:

trait F {
  type D
  def initData: D
  def aggregate(line: String, data: D): D
  def process(line: String, data: D): String
}

class F1 extends F { type D = Int } // ...
class F2 extends F { type D = Map[Int, Int] } // ...

val strings = List("string1", "string2")
for (f <- List(new F1(), new F2())) {
  val d = strings.foldLeft(f.initData) { (d, s) => f.aggregate(s, d) }

  for (s <- strings)
    f.process(s, d)
}

Don't sure, if I undrestood correct order of operation, but it may be a starting point.

Upvotes: 1

Related Questions