简体   繁体   中英

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?

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM