【问题标题】:Functional way to take element in a list until a limit in Scala在 Scala 中获取列表中的元素直到达到限制的功能方法
【发布时间】:2017-04-04 09:54:18
【问题描述】:

该方法的目的是获取列表中的元素,直到达到限制。

例如

我想出了两种不同的实现方式

def take(l: List[Int], limit: Int): List[Int] = {
  var sum = 0
  l.takeWhile { e =>
    sum += e
    sum <= limit
  }
}

这很简单,但是使用了可变状态。

def take(l: List[Int], limit: Int): List[Int] = {
  val summed = l.toStream.scanLeft(0) { case (e, sum) => sum + e }
  l.take(summed.indexWhere(_ > limit) - 1)
}

它看起来更简洁,但它更冗长,并且可能因为需要流而降低内存效率。

有没有更好的办法?

【问题讨论】:

    标签: scala functional-programming


    【解决方案1】:

    您也可以通过折叠一次完成:

      def take(l: List[Int], limit: Int): List[Int] =
        l.fold((List.empty[Int], 0)) { case ((res, acc), next) =>
          if (acc + next > limit)
            (res, limit)
          else
            (next :: res, next + acc)
        }
    

    因为标准列表不是惰性的,折叠也不是,这将始终遍历整个列表。一种替代方法是使用cats' iteratorFoldM 来代替一旦达到限制就会短路的实现。

    您也可以直接使用尾递归来编写短路折叠,类似于以下内容:

    def take(l: List[Int], limit: Int): List[Int] = {
      @annotation.tailrec
      def take0(list: List[Int], accList: List[Int], accSum: Int) : List[Int] =
        list match {
          case h :: t if accSum + h < limit =>  
            take0(t, h :: accList, h + accSum)
          case _ => accList
        }
      take0(l, Nil, 0).reverse
    }
    

    请注意,第二种解决方案可能更快,但也不太优雅,因为它需要额外的努力来证明实现终止,这在使用折叠时很明显。

    【讨论】:

    • 关于您的第二种方法:有一个小错字,它是 L6 上的take0。并且列表以相反的顺序返回。 Scala 在标准库 wdyt 中缺少 foldWhile ?
    【解决方案2】:

    第一种方法非常好,因为你的函数的结果仍然是完全不可变的。

    顺便说一句,这实际上是实现了 scala 集合库的多少功能,它们创建了一个可变构建器以提高效率并从中返回一个不可变集合。

    【讨论】:

    • 我看到了你的一些答案,很好!感谢您的帮助!
    • 谢谢你的好意,鼓励的话。
    【解决方案3】:

    函数式方法是使用递归函数并确保它是堆栈安全的。

    如果你只使用基本的scala:

        import scala.annotation.tailrec
        def take(l: List[Int], limit: Int) : List[Int] = {
          @tailrec
          def takeHelper(l:List[Int], limit:Int, r:List[Int]):List[Int] =
            l match {
                case h::t if (h <= limit ) =>  takeHelper(t, limit-h, r:+h)
                case _ =>  r
          }
          takeHelper(l, limit, Nil)
        }
    

    如果你可以使用scalaz Trampoline,那就更好了:

    import scalaz._
    import scalaz.Scalaz._
    import Free._
     def take(l: List[Int], limit: Int): Trampoline[List[Int]] = {
    
        l match {
          case h :: t if (h <= limit) => suspend(take(t, limit - h)).map(h :: _)
    
    
         case _                      => return_(Nil)
        }
      }
      println(take(List(1, 2, 3, 4, 0, 0, 1), 10).run)
      println(take(List.fill(10000)(1), 100000000).run)
    

    【讨论】:

      【解决方案4】:

      如果你想扩展你自己的自定义方式,你也可以使用类似的东西:

      def custom(con: => Boolean)(i: Int)(a: => List[Int])(body: => Unit): List[Int] = {
            if (con) {
              body
              custom(con)(i + 1)(a)(body)
            }
            else {
              a.slice(0, i)
            }
          }
      

      然后这样称呼它:

      var j = 100
          val t = customTake(j > 80)(0)((0 to 99).toList) {
            j -= 1
          }
          println(t)
      

      【讨论】:

        【解决方案5】:

        我觉得你的第二个版本已经很不错了。你可以稍微调整一下,像这样:

        val sums = l.toStream.scanLeft(0){_ + _} drop 1
        l zip sums takeWhile {_._2 <= limit} map (_._1)
        

        这样你就不用处理索引了,这通常更容易理解。

        【讨论】:

        • 很好,但不需要压缩:val sums=l.toStream.scan(0)(_+_); l.take(sums.lastIndexWhere(_&lt;=limit))
        猜你喜欢
        • 1970-01-01
        • 2017-04-05
        • 1970-01-01
        • 2020-09-02
        • 2014-06-10
        • 2013-09-11
        • 2013-11-05
        • 2020-01-02
        • 1970-01-01
        相关资源
        最近更新 更多