如何在Scala中从文件中读取不可变数据结构

5

我有一个数据结构,其中包含一组任务的工作。Job和Task数据都在这样的文件中定义:

jobs.txt:
JA
JB
JC

tasks.txt:
JB  T2
JA  T1
JC  T1
JA  T3
JA  T2
JB  T1 

创建对象的过程如下:
- 读取每个作业,创建它并通过id进行存储
- 读取任务,通过id检索作业,创建任务,并将任务存储在该作业中

一旦文件被读取,此数据结构就不再被修改。因此,我希望作业中的任务可以存储在不可变集合中。但是我不知道如何以高效的方式实现它。(注意:存储作业的不可变映射可以保持不变)

以下是代码的简化版本:

class Task(val id: String) 

class Job(val id: String) {
    val tasks = collection.mutable.Set[Task]() // This sholud be immutable
}

val jobs = collection.mutable.Map[String, Job]() // This is ok to be mutable

// read jobs
for (line <- io.Source.fromFile("jobs.txt").getLines) { 
    val job = new Job(line.trim)
    jobs += (job.id -> job)
}

// read tasks
for (line <- io.Source.fromFile("tasks.txt").getLines) {
    val tokens = line.split("\t")
    val job = jobs(tokens(0).trim)
    val task = new Task(job.id + "." + tokens(1).trim)
    job.tasks += task
}

感谢您提前给出的每个建议!

4个回答

4
最高效的方法是将所有内容读入可变结构中,然后在最后转换为不可变结构,但对于具有许多字段的类,这可能需要大量冗余编码。因此,考虑使用与底层集合相同的模式:具有新任务的工作是一个新工作。
这是一个例子,甚至不需要读取作业列表-它从任务列表中推断出来。(这是在2.7.x下运行的示例;2.8的最新版本使用“Source.fromPath”而不是“Source.fromFile”。)
object Example {
  class Task(val id: String) {
    override def toString = id
  }

  class Job(val id: String, val tasks: Set[Task]) {
    def this(id0: String, old: Option[Job], taskID: String) = {
      this(id0 , old.getOrElse(EmptyJob).tasks + new Task(taskID))
    }
    override def toString = id+" does "+tasks.toString
  }
  object EmptyJob extends Job("",Set.empty[Task]) { }

  def read(fname: String):Map[String,Job] = {
    val map = new scala.collection.mutable.HashMap[String,Job]()
    scala.io.Source.fromFile(fname).getLines.foreach(line => {
      line.split("\t") match {
        case Array(j,t) => {
          val jobID = j.trim
          val taskID = t.trim
          map += (jobID -> new Job(jobID,map.get(jobID),taskID))
        }
        case _ => /* Handle error? */
      }
    })
    new scala.collection.immutable.HashMap() ++ map
  }
}

scala> Example.read("tasks.txt")
res0: Map[String,Example.Job] = Map(JA -> JA does Set(T1, T3, T2), JB -> JB does Set(T2, T1), JC -> JC does Set(T1))

另一种方法是读取作业列表(创建新的Job(jobID,Set.empty [Task])作为作业),然后处理任务列表包含未在作业列表中的条目的错误条件。(每次读取新任务时仍需要更新作业列表映射。)


我喜欢这种方法。但是我只会编写一个addTask方法,它返回一个具有相同数据加上新任务的新Job。这会稍微改变一下逻辑,因为目前Job好像了解自己初始化的太多了。 :-) - Daniel C. Sobral
我这样做是为了突出旧工作被新工作替换的重点概念。但我同意在某个地方使用addTask会更好。有很多地方可以争论(它应该采用Option[Job],还是作为可变映射的闭包?)。 - Rex Kerr
谢谢,我喜欢这个解决方案,因为它通过构造函数或addTask方法创建新的Job。我还是很新的scala用户(我来自java),我还不确定在这种情况下,是否值得为了不可变性而付出许多对象创建的代价,因为对我来说性能非常重要(在实际情况中,我有超过2个类,它们之间有复杂的链接,并且有成千上万的对象)。 - Filippo Tabusso
如果性能很重要,那么首先使用可变结构,然后再切换到不可变结构。例如,你可以创建一个JobUnderConstruction类继承自Job,它有一个额外的可变属性taskAccumulator,并且有一个toJob方法,在最后返回一个Job对象。 - Rex Kerr

2

我对它进行了一些更改,使其能在Scala 2.8上运行(主要是使用fromPath代替fromFile,以及在getLines后面加上())。它可能使用了一些Scala 2.8的特性,尤其是groupBy。也许还用到了toSet,但是在2.7上很容易适应。

我没有文件来测试它,但我将这些东西从val改为了def,至少类型签名匹配。

class Task(val id: String)  
class Job(val id: String, val tasks: Set[Task])

// read tasks 
val tasks = (
  for {
    line <- io.Source.fromPath("tasks.txt").getLines().toStream
    tokens = line.split("\t") 
    jobId = tokens(0).trim
    task = new Task(jobId + "." + tokens(1).trim) 
  } yield jobId -> task
).groupBy(_._1).map { case (key, value) => key -> value.map(_._2).toSet }

// read jobs 
val jobs = Map() ++ (
  for {
    line <- io.Source.fromPath("jobs.txt").getLines()
    job = new Job(line.trim, tasks(line.trim))
  } yield job.id -> job
)

1
您可以始终延迟对象的创建,直到从文件中读入所有数据,例如:
case class Task(id: String) 
case class Job(id: String, tasks: Set[Task])

import scala.collection.mutable.{Map,ListBuffer}
val jobIds = Map[String, ListBuffer[String]]()

// read jobs
for (line <- io.Source.fromFile("jobs.txt").getLines) { 
    val job = line.trim
    jobIds += (job.id -> new ListBuffer[String]())
}

// read tasks
for (line <- io.Source.fromFile("tasks.txt").getLines) {
    val tokens = line.split("\t")
    val job = tokens(0).trim
    val task = job.id + "." + tokens(1).trim
    jobIds(job) += task
}

// create objects
val jobs = jobIds.map { j =>
    Job(j._1, Set() ++ j._2.map { Task(_) })
}

为了处理更多的字段,你可以(付出一些努力)创建一个可变版本的不可变类,用于构建。然后根据需要进行转换:
case class Task(id: String)
case class Job(val id: String, val tasks: Set[Task])
object Job {
    class MutableJob {
        var id: String = ""
        var tasks = collection.mutable.Set[Task]()
        def immutable = Job(id, Set() ++ tasks)
    }
    def mutable(id: String) = {
        val ret = new MutableJob
        ret.id = id
        ret
    }
}

val mutableJobs = collection.mutable.Map[String, Job.MutableJob]() 

// read jobs
for (line <- io.Source.fromFile("jobs.txt").getLines) { 
    val job = Job.mutable(line.trim)
    jobs += (job.id -> job)
}

// read tasks
for (line <- io.Source.fromFile("tasks.txt").getLines) {
    val tokens = line.split("\t")
    val job = jobs(tokens(0).trim)
    val task = Task(job.id + "." + tokens(1).trim)
    job.tasks += task
}

val jobs = for ((k,v) <- mutableJobs) yield (k, v.immutable)

谢谢。您的解决方案对我所提供的示例是可行的,但在实际情况下,Job和Task都有比仅仅id更多的字段。例如,Job还有到期日期(Date),而Task有长度(Int),等等... - Filippo Tabusso
再次感谢,这正是我第一次面临问题时考虑的解决方案。然而,在我看来,它需要太多的额外代码,这意味着会有更多的错误、维护等问题。 - Filippo Tabusso

0
一种选择是拥有一个可变的但是短暂的配置器类,类似于上面的MutableMap,然后以不可变的形式将其传递给您的实际类。
val jobs: immutable.Map[String, Job] = {
  val mJobs = readMutableJobs
  immutable.Map(mJobs.toSeq: _*)
}

当然,您可以按照您已经编码的方式实现readMutableJobs

抱歉我没有表达清楚:jobs Map 可以是可变的,但是单个 Job 中的 tasks Set 应该是不可变的(我已经编辑了我的问题)。 - Filippo Tabusso
我认为可以说,你可以将相同的方法应用于可变/不可变任务,就像它在作业映射上一样有效!例如,在创建Job构造函数时,将不可变的任务副本传递进去。 - oxbow_lakes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接