【问题标题】:Scala: transform a collection, yielding 0..many elements on each iterationScala:转换一个集合,每次迭代产生 0..many 元素
【发布时间】:2016-06-06 16:18:21
【问题描述】:

给定 Scala 中的一个集合,我想遍历这个集合,并且对于每个我想发出(生成)从 0 到多个元素的对象,这些元素应该合并到一个新集合中。

例如,我希望是这样的:

val input = Range(0, 15)
val output = input.somefancymapfunction((x) => {
  if (x % 3 == 0)
    yield(s"${x}/3")
  if (x % 5 == 0)
    yield(s"${x}/5")
})

构建一个output 集合,其中包含

(0/3, 0/5, 3/3, 5/5, 6/3, 9/3, 10/5, 12/3)

基本上,我想要 filter (1 → 0..1) 和 map (1 → 1) 的超集:映射 (1 → 0..n)。

我尝试过的解决方案

当务之急的解决方案

显然,可以在非功能性方式中这样做,例如:

var output = mutable.ListBuffer()
input.foreach((x) => {
  if (x % 3 == 0)
    output += s"${x}/3"
  if (x % 5 == 0)
    output += s"${x}/5"
})

平面图解决方案

我知道flatMap,但又是这样:

1) 如果我们谈论的是任意数量的输出元素,就会变得非常丑陋:

val output = input.flatMap((x) => {
  val v1 = if (x % 3 == 0) {
    Some(s"${x}/3")
  } else {
    None
  }
  val v2 = if (x % 5 == 0) {
    Some(s"${x}/5")
  } else {
    None
  }
  List(v1, v2).flatten
})

2) 需要在其中使用可变集合:

val output = input.flatMap((x) => {
  val r = ListBuffer[String]()
  if (x % 3 == 0)
    r += s"${x}/3"
  if (x % 5 == 0)
    r += s"${x}/5"
  r
})

这实际上比从一开始就使用可变集合更糟糕,或者

3) 需要大逻辑大修:

val output = input.flatMap((x) => {
  if (x % 3 == 0) {
    if (x % 5 == 0) {
      List(s"${x}/3", s"${x}/5")
    } else {
      List(s"${x}/3")
    }
  } else if (x % 5 == 0) {
    List(s"${x}/5")
  } else {
    List()
  }
})

恕我直言,它看起来也很丑陋,需要复制生成代码。

滚动你自己的地图功能

最后但同样重要的是,我可以推出自己的此类功能:

def myMultiOutputMap[T, R](coll: TraversableOnce[T], func: (T, ListBuffer[R]) => Unit): List[R] = {
  val out = ListBuffer[R]()
  coll.foreach((x) => func.apply(x, out))
  out.toList
}

几乎可以随心所欲地使用:

val output = myMultiOutputMap[Int, String](input, (x, out) => {
  if (x % 3 == 0)
    out += s"${x}/3"
  if (x % 5 == 0)
    out += s"${x}/5"
})

我真的忽略了某些东西,而标准 Scala 集合库中没有这样的功能吗?

类似问题

这个问题与Can I yield or map one element into many in Scala? 有一些相似之处——但那个问题讨论的是 1 个元素 → 3 个元素的映射,我想要 1 个元素 → 任意数量的元素映射。

最后说明

请注意,这不是关于除数/除数的问题,这些条件仅用于说明目的。

【问题讨论】:

  • 澄清一下——如果输入集合包含 15,那么输出集合是否应该同时包含 15/3 和 15/5?
  • @Ben 是的,确切地说,1 个元素 (15) 应该映射到 2 个(“15/3”、“15/3”)。

标签: scala functional-programming scala-collections


【解决方案1】:

你可以试试收藏:

val input = Range(0,15)
val output = input.flatMap { x =>
     List(3,5) collect { case n if (x%n == 0) => s"${x}/${n}" }
}
System.out.println(output)

【讨论】:

    【解决方案2】:

    不要为每个除数单独设置一个case,而是将它们放在一个容器中并在for理解中迭代它们:

    val output = for {
      n <- input
      d <- Seq(3, 5)
      if n % d == 0
    } yield s"$n/$d"
    

    或者等效地在collect中嵌套在flatMap中:

    val output = input.flatMap { n =>
      Seq(3, 5).collect {
        case d if n % d == 0 => s"$n/$d"
      }
    }
    

    在更一般的情况下,不同的情况可能有不同的逻辑,您可以将每个情况放在单独的部分函数中并迭代部分函数:

    val output = for {
      n <- input
      f <- Seq[PartialFunction[Int, String]](
        {case x if x % 3 == 0 => s"$x/3"},
        {case x if x % 5 == 0 => s"$x/5"})
      if f.isDefinedAt(n)
    } yield f(n)
    

    【讨论】:

      【解决方案3】:

      这是我对自定义函数的建议,使用 pimp my library 模式可能会更好

      def fancyMap[A, B](list: TraversableOnce[A])(fs: (A => Boolean, A => B)*) = {
        def valuesForElement(elem: A) = fs collect { case (predicate, mapper) if predicate(elem) => mapper(elem) }
        list flatMap valuesForElement
      }
      
      fancyMap[Int, String](0 to 15)((_ % 3 == 0, _ + "/3"), (_ % 5 == 0, _ + "/5"))
      

      【讨论】:

        【解决方案4】:

        您也可以使用一些函数库(例如 scalaz)来表达这一点:

        import scalaz._, Scalaz._
        
        def divisibleBy(byWhat: Int)(what: Int): List[String] = 
          (what % byWhat == 0).option(s"$what/$byWhat").toList
        
        (0 to 15) flatMap (divisibleBy(3) _ |+| divisibleBy(5))
        

        这使用semigroup 追加操作|+|。对于Lists,这个操作意味着一个简单的列表连接。所以对于函数Int =&gt; List[String],这个追加操作将产生一个函数,运行这两个函数并追加它们的结果。

        【讨论】:

          【解决方案5】:

          如果你有复杂的计算,有时你应该在操作全局累加器中添加一些元素,你可以使用名为Writer Monad的流行方法

          scala 中的准备工作有点庞大,但由于Monad interface,结果非常可组合

          import scalaz.Writer
          import scalaz.syntax.writer._
          import scalaz.syntax.monad._
          import scalaz.std.vector._
          import scalaz.syntax.traverse._
          
          type Messages[T] = Writer[Vector[String], T]
          
          def yieldW(a: String): Messages[Unit] = Vector(a).tell
          
          val output = Vector.range(0, 15).traverse { n =>
            yieldW(s"$n / 3").whenM(n % 3 == 0) >>
            yieldW(s"$n / 5").whenM(n % 5 == 0)
          }.run._1 
          

          【讨论】:

            【解决方案6】:

            我会给我们一个fold

            val input = Range(0, 15)
            val output = input.foldLeft(List[String]()) {
                case (acc, value) =>
                    val acc1 = if (value % 3 == 0) s"$value/3" :: acc else acc
                    val acc2 = if (value % 5 == 0) s"$value/5" :: acc1 else acc1
                    acc2
            }.reverse
            

            output 包含

            List(0/3, 0/5, 3/3, 5/5, 6/3, 9/3, 10/5, 12/3)
            

            fold 接受一个累加器 (acc)、一个集合和一个函数。使用累加器的初始值调用该函数,在本例中为空的List[String],以及集合的每个值。该函数应该返回一个更新的集合。

            在每次迭代中,我们采用不断增长的累加器,如果内部 if 语句是 true,则将计算添加到新的累加器中。该函数最终返回更新后的累加器。

            fold 完成时,它返回最终的累加器,但不幸的是,它的顺序相反。我们只需用.reverse 反转累加器即可。

            有一篇关于折叠的好论文:A tutorial on the universality and expressiveness of fold,作者 Graham Hutton。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2018-07-13
              • 2011-08-15
              • 2014-03-12
              • 1970-01-01
              • 2018-10-19
              • 1970-01-01
              • 2020-02-17
              相关资源
              最近更新 更多