F#currying效率?

时间:2010-04-24 08:45:59

标签: f# functional-programming currying

我的功能如下:

let isInSet setElems normalize p = 
        normalize p |> (Set.ofList setElems).Contains

此函数可用于快速检查元素是否在语义上是某些集合的一部分;例如,检查文件路径是否属于html文件:

let getLowerExtension p = (Path.GetExtension p).ToLowerInvariant()
let isHtmlPath = isInSet [".htm"; ".html"; ".xhtml"] getLowerExtension

然而,当我使用如上所述的函数时,性能很差,因为在“isInSet”中编写的函数体的评估似乎被延迟,直到所有参数都已知 - 特别是诸如{{1}的不变位每次执行(Set.ofList setElems).Contains时都会重新评估。

我怎样才能最好地保持F#的娴熟,可读性,同时仍然可以获得预先评估集合构造的更有效的行为。

以上是只是一个例子;我正在寻找一种避免让我陷入实施细节的一般方法 - 尽可能我希望避免被实施的执行顺序等细节分心,因为这对我来说通常并不重要有点破坏功能编程的一个主要卖点。

4 个答案:

答案 0 :(得分:9)

只要F#不区分纯代码和不纯代码,我怀疑我们会看到这种优化。但是,你可以明确说明。

let isInSet setElems =
    let set = Set.ofList setElems
    fun normalize p -> normalize p |> set.Contains

isHtmlSet现在只调用isInSet一次以获取关闭,同时执行ofList

答案 1 :(得分:5)

@Kha的回答很明显。 F#无法重写

// effects of g only after both x and y are passed
let f x y =
    let xStuff = g x
    h xStuff y

// effects of g once after x passed, returning new closure waiting on y
let f x =
    let xStuff = g x
    fun y -> h xStuff y

除非它知道g没有任何影响,并且在今天的.NET Framework中,通常无法推断99%的所有表达式的影响。这意味着程序员仍然负责如上所述明确编码评估顺序。

答案 2 :(得分:5)

Kha 的答案显示了如何通过直接使用闭包来手动优化代码。如果这是您经常需要经常使用的频繁模式,那么还可以定义一个高阶函数,从两个函数构建高效代码 - 第一个函数执行预处理参数和第二个参数,它在获得剩余参数后执行实际处理

代码如下所示:

let preProcess finit frun preInput =  
  let preRes = finit preInput
  fun input -> frun preRes input

let f : string list -> ((string -> string) * string) -> bool =
  preProcess 
    Set.ofList                           // Pre-processing of the first argument
    (fun elemsSet (normalize, p) ->      // Implements the actual work to be 
      normalize p |> elemsSet.Contains) // .. done once we get the last argument

这是一个问题,但这是否更优雅......

另一个(疯狂的)想法是你可以使用计算表达式。允许你这样做的计算构建器的定义非常不标准(它不是人们通常用它们做的事情,它与monad或任何其他理论没有任何关系)。但是,应该可以这样写:

type CurryBuilder() = 
  member x.Bind((), f:'a -> 'b) = f
  member x.Return(a) = a
let curry = new CurryBuilder()

curry计算中,您可以使用let!表示您想要获取函数的下一个参数(在评估前面的代码之后):

let f : string list -> (string -> string) -> string -> bool = curry {
  let! elems = ()
  let elemsSet = Set.ofList elems
  printf "elements converted"
  let! normalize = ()
  let! p = ()
  printf "calling"
  return normalize p |> elemsSet.Contains }

let ff = f [ "a"; "b"; "c" ] (fun s -> s.ToLower()) 
// Prints 'elements converted' here
ff "C"
ff "D"
// Prints 'calling' two times

以下是一些资源,其中包含有关计算表达式的更多信息:

答案 3 :(得分:2)

  1. Currying不疼。 Currying有时会引入闭合。它们通常也很有效率。 参考之前我问的this question。如有必要,您可以使用内联来提高性能。

  2. 但是,示例中的性能问题主要是由于您的代码:

    normalize p |> (Set.ofList setElems).Contains

  3. 在这里你需要执行Set.ofList setElems即使你在咖喱。它花费O(n log n)时间。 您需要将setElems的类型更改为F#Set,而不是立即列出。顺便说一下,对于小集合,使用列表比查询更快。