是否有“从列表的开头选择并减少直到结果满足谓词”的函数式编程习语?

Posted

技术标签:

【中文标题】是否有“从列表的开头选择并减少直到结果满足谓词”的函数式编程习语?【英文标题】:Is there a functional programming idiom for "pick from beginning of a list and reduce until the result satisfies a predicate"? 【发布时间】:2016-04-08 14:45:59 【问题描述】:

假设我有一个数字列表,我需要知道我必须从它的开头选择多少元素才能至少获得所需的总和。

算法很简单:我从列表的开头选择数字,直到所有选择的数字的总和超过某个数量。

我可以这样写成命令式:

fun pickEnough(list: List<Double>, enough: Double): List<Double>? 
    var soFar = 0.0
    var index = 0
    for (index in 0..list.size) 
        soFar += list[index]
        if (soFar > enough) 
            return list.subList(0, index)
        
    
    return null

一个低效但更通用的解决方案是生成所有可能的子列表并选择第一个减少结果足够好的:

fun <T> pickEnough(list: List<T>, reducer: (T, T) -> T, enough: (T) -> Boolean): List<T>? =
list.indices
    .map  index -> list.sublist(0, index) 
    .first  sublist -> enough(sublist.reduce(reducer)) 

pickEnough(listOf(5,8,0,0,8),  a, b -> a + b,  it > 10 ) // [5, 8]

是否有一个既定的功能习语,或者可能是在性能和​​表现力上比我试图概括这篇文章更好的组合?

该示例在 Kotlin 中,但我更喜欢与语言无关的答案,尽管任何语言的答案都值得赞赏,只要它们提供描述此操作的更高级别的习语。

【问题讨论】:

【参考方案1】:

您想要的是scan,后跟takeWhilescan 类似于折叠,只是它返回一系列连续的状态值。您可以返回一对(x, soFar) 的连续状态,其中包含序列中的当前值和当前运行总计。然后,您可以从该序列中获取当前值未导致超出所需总数的尽可能多的值。例如在 F# 中你可以这样做:

let pickEnough (l: seq<double>) (enough: double): seq<double> =
    Seq.scan (fun (_, soFar) x -> (x, x+soFar)) (0.0, 0.0) l |> 
    Seq.skip 1 |> 
    Seq.takeWhile (fun (x, soFar) -> soFar - x < enough) |> 
    Seq.map fst

【讨论】:

是的,Haskell 有类似的scanl (&c) 函数:en.wikipedia.org/wiki/Prefix_sum#Scan_higher_order_function 这个答案不在 Kotlin 中,也不能直接移植。 @JaysonMinard - 该问题要求提供与语言无关的答案,scantakeWhile 是与语言无关的概念。 @JaysonMinard 这篇文章完全回答了我的问题。我不希望答案专门针对 Kotlin,我在问题中提到了这一点。 是的,现在看,错过了问题的最后一句话。【参考方案2】:

这是我对 Lee 的回答的 Kotlin 版本:

fun <A, B> Iterable<A>.scanl(initial: B, f: (B, A) -> B): List<B> 
   return listOf(initial).plus(if (this.count() == 0) 
       emptyList()
    else 
       this.drop(1).scanl(f(initial, this.first()), f)
   )


fun pickEnough(list: List<Int>, enough: Int): List<Int>? 
    return list
      .scanl(0 to 0) 
        pair, x ->
        x to (x + pair.second)
      
      .drop(1)
      .takeWhile 
        pair ->
        val (x, soFar) = pair
        soFar - x < enough
      
      .map  it.first 

I put my code with some tests on gist.

【讨论】:

【参考方案3】:

我用这个:

fun IntArray.pickEnough(initV: Int, reducer: (Int, Int) -> Int, predicate: (Int) -> Boolean): List<Int> 
    var sum = initV
    return list.takeWhile 
        sum = reducer(sum, it)
        predicate(sum)
    

【讨论】:

【参考方案4】:

在 Clojure 中有一个 reduced 函数:

;; Clojure
(reduce (fn [[sum list] el]
            (if (< 10 sum)
              (reduced list)
              [(+ sum el) (conj list el)]))
          [0 []]
          [5 8 0 0 8]) ;; => [5 8]

由于没有指定如果列表不够大返回什么,在这种情况下它将返回一个向量:[sum-of-the-array original-array]

当然,这很容易改变。

【讨论】:

以上是关于是否有“从列表的开头选择并减少直到结果满足谓词”的函数式编程习语?的主要内容,如果未能解决你的问题,请参考以下文章

是否有更快的方法来检查是否存在数千个 NSManagedObject 项目?

是否有一个 jOOQ 工具来验证生成的定义是否仍然正确?

是否可以检查用户是否有摄像头和麦克风以及是否已通过 Javascript 授予权限?

是否有可能发现照片是否被拍摄?

是否有可能检查我进入的对象是否存在但指向 Null? [关闭]

是否有用于检查 iPhone 中是否安装了 eSIM 的 API?