【问题标题】:Does this specific exercise lend itself well to a 'functional style' design pattern?这个特定的练习是否适合“功能风格”设计模式?
【发布时间】:2017-03-02 03:12:39
【问题描述】:

假设我们有一个包含在文件 Array.json 中的一维 javascript 对象数组,其密钥架构未知,即在读取文件之前不知道密钥。

然后我们希望输出一个带有标题或第一个条目的 CSV 文件,它是来自所有对象的一组以逗号分隔的键。

文件的每一行都应包含逗号分隔的值,这些值对应于文件中的每个键。

数组.json

[
    abc:123,
    xy:"yz",
    s12:13,
],
    ...
[
    abc:1
    s:133,
]

一个有效的输出:

abc,xy,s12,s

123,yz,13,
1,,,133

我正在自学“函数式”编程,但我认为这个问题不适用于函数式解决方案。 我相信这个问题需要为输出标头保留一些状态,并且随后每一行都依赖于该标头。

我希望一次性解决问题。我的目标是大型数据集的效率、最少的遍历,以及如果可能的话,可并行性。如果这不可能,那么您能否提供证据或理由来解释原因?

编辑:有没有办法在功能上解决这样的问题?:

假设您以某种特定的顺序通过数组一次。然后 从一开始,第一个标题集看起来像abc,xy,s12 目的。带有 CSV 条目 123,yz,13 。然后在下一个对象上添加一个 标题集的附加键,因此 abc,xy,s12,s 将是标题 CSV 条目将是 1,,,133 。最后我们不需要 第二次通过数据集。我们可以追加额外的 结果集的逗号。这是我们可以接近单一的一种方式 通过....

是否有旨在解决此类问题的功能性工具(functions),我应该考虑什么? [我所说的功能工具是指 Monads、FlatMap、Filters 等]。 或者,我应该考虑 Futures 之类的东西吗?

目前我一直在尝试使用 Java8 来解决这个问题,但我对 Scala 等的解决方案持开放态度。理想情况下,我能够确定 Java8s 的函数式方法是否可以解决问题,因为这是我目前正在使用的语言在。

【问题讨论】:

  • 什么是Array.json。这是斯卡拉吗?而且... functional approach 与否只是编写解决方案的一种方法。 functional tools 是什么意思?
  • 那是输入源。我会尝试清理它。
  • 我会尽量更清楚地了解功能性工具。
  • 不能一次完成。考虑:输入的最后一行可能会改变输出的第一行(因为它可能会添加一个尚未见过的键)。因此,您必须在检查输入时收集 something,然后在最后检查并输出键列表,然后是值列表。两遍
  • 你如何写出结果而不通过它?这完全等同于对密钥的另一次传递,并且您为此拒绝了几个答案。

标签: scala functional-programming java-8 java-stream


【解决方案1】:

由于 csv 输出会随着输入的每一行而改变,因此您必须在将其写出之前将其保存在内存中。 如果您考虑从 csv 文件的内部表示创建输出文本格式数据的另一个“传递”(csv 的内部表示实际上是 Map[String,List[String]]您必须遍历它才能将其转换为文本)那么不可能一次性完成。

但是,如果这是可以接受的,那么您可以使用 Stream 从 json 文件中读取单个项目,将其合并到 csv 文件中,然后执行此操作直到流为空。

假设 csv 文件的内部表示是

trait CsvFile {
  def merge(line: Map[String, String]): CsvFile
}

您可以将单个项目表示为

trait Item {
  def asMap: Map[String, String]
}

你可以使用foldLeft来实现它:

def toCsv(items: Stream[Item]): CsvFile = 
  items.foldLeft(CsvFile(Map()))((csv, item) => csv.merge(item.asMap))

或者使用递归得到同样的结果

@tailrec def toCsv(items: Stream[Item], prevCsv: CsvFile): CsvFile =
  items match {
    case Stream.Empty => prevCsv
    case item #:: rest =>
      val newCsv = prevCsv.merge(item.asMap)
      toCsv(rest, newCsv)
  }

注意:当然你不必为 CsvFile 或 Item 创建类型,你可以分别使用 Map[String,List[String]] 和 Map[String,String]

更新:

由于要求CsvFile trait/class 提供更多详细信息,下面是一个示例实现:

case class CsvFile(lines: Map[String, List[String]], rowCount: Int = 0) {
  def merge(line: Map[String, String]): CsvFile = {
    val orig = lines.withDefaultValue(List.fill(rowCount)(""))
    val current = line.withDefaultValue("")
    val newLines = (lines.keySet ++ line.keySet) map {
      k => (k, orig(k) :+ current(k))
    }
    CsvFile(newLines.toMap, rowCount + 1)
  }
}

【讨论】:

  • 如果您愿意,我可以提供抽象方法的实现,或者包含有关foldleft@tailrec 的详细信息,或者使用Stream 以避免多次传递数据。
  • 感谢您关于尾递归和 foldLeft 的建议。因为我只是在学习函数式编程,所以我还在摸索你的代码。但这看起来符合我的要求,因为我们只需要遍历我们的数据集一次即可进行内部表示。
  • 我会尝试一下,然后再决定,但我想我可能会接受这个作为解决方案。感谢您帮助我了解有关“函数式”编程的更多信息!
  • 你能给出合并的定义吗?鉴于您需要保持顺序,并考虑没有出现键的行,我认为那里有很多计算复杂性。
  • 当然,我包含了一个示例实现。如果我没记错的话,它的复杂性与数据集中唯一列的数量成线性关系。而 toCsv 相对于项目的数量是线性的。我认为它仍然与输入文件中的行数成线性关系。 (但我可能在这里错了)
【解决方案2】:

这可能是一种方法:

val arr = Array(Map("abc" -> 123, "xy" -> "yz", "s12" -> 13), Map("abc" -> 1, "s" -> 133)) 
    val keys = arr.flatMap(_.keys).distinct  // get the distinct keys for header
    arr.map(x => keys.map(y => x.getOrElse(y,""))) // get an array of rows

【讨论】:

  • 我希望一次性解决问题。我的目标是大型数据集的效率、最少的遍历和可并行性。抱歉,我没有从一开始就说明这一点。
【解决方案3】:

在函数式编程中有状态是完全可以的。但是函数式编程中不允许有可变状态或变异状态。

函数式编程提倡创建新的更改状态,而不是在原地改变状态。

因此,可以读取和访问程序中创建的状态,除非您正在变异或产生副作用。

进入正题。

val list = List(List("abc" -> "123", "xy" -> "yz"), List("abc" -> "1"))

list.map { inner => inner.map { case (k, v) => k}}.flatten

list.map { inner => inner.map { case (k, v) => v}}.flatten

REPL

scala> val list = List(List("abc" -> "123", "xy" -> "yz"), List("abc" -> "1"))
list: List[List[(String, String)]] = List(List((abc,123), (xy,yz)), List((abc,1)))


scala> list.map { inner => inner.map { case (k, v) => k}}.flatten
res1: List[String] = List(abc, xy, abc)

scala> list.map { inner => inner.map { case (k, v) => v}}.flatten
res2: List[String] = List(123, yz, 1)

或者使用 flatMap 代替 map 和 flatten

val list = List(List("abc" -> "123", "xy" -> "yz"), List("abc" -> "1"))

list.flatMap { inner => inner.map { case (k, v) => k}}

list.flatMap { inner => inner.map { case (k, v) => v}}

【讨论】:

  • 你想在这里回答什么?
  • 这里隐藏的问题是,由于 Array.json 文件中的所有“行”都是 json 没有定义的架构,所以在你看到最后一个 @ 之前,你永远不会拥有完整的键集987654325@。如果他们有任何嵌套怎么办?另外...您应该为每个“json”生成values的“行”,其顺序与符合csv with header的键的顺序相同。
  • 我试图说明架构是未知的。 @Singh 当我写“一维javascript对象数组”时,我认为这暗示了“无嵌套”。让我知道我是否应该更清楚地说明这一点。
  • 我希望一次性解决问题。我不确定我从一开始就说清楚了。在多次传递中它更微不足道。
【解决方案4】:

在函数式编程中,不允许可变状态。但是不可变的状态/值很好。

假设您已将 json 文件读入值输入:List[Map[String,String]],下面的代码将解决您的问题:

val input = List(Map("abc"->"123", "xy"->"yz" , "s12"->"13"), Map("abc"->"1", "s"->"33"))
val keys = input.map(_.keys).flatten.toSet
val keyvalues = input.map(kvs => keys.map(k => (k->kvs.getOrElse(k,""))).toMap)
val values = keyvalues.map(_.values)
val result = keys.mkString(",") + "\n" + values.map(_.mkString(",")).mkString("\n")

【讨论】:

  • 我只是加粗了我的要求。我认为这是在多次传递中发生的,因为您首先映射键集。
  • 我认为多次通过并不意味着效率低下。当您以函数式方式编程时,这意味着没有可变状态,您可以轻松地使程序并发运行。例如,这种情况下,可以使用input.par.map进行并发执行,提高效率。
  • “我认为多次通过并不意味着效率低下。”非常正确。一次性完成此操作并没有什么神奇之处。
  • 感谢您的尝试和洞察力。我真的很想尝试一次性解决问题,因为我认为它更优雅。通过单次传递,我并不想删除并发性。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-17
  • 2010-10-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多