F# 咖喱效率?

F# 咖喱效率?,f#,functional-programming,currying,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

我有一个如下所示的函数:

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”中编写的函数体的求值似乎会延迟到所有参数都已知为止—特别是,
(Set.ofList setElems).Contains
等不变位会在每次执行
isHtmlPath
时重新求值

我如何才能最好地保持F#简洁易读的特性,同时仍然获得更有效的行为,其中集合构造是预先评估的

以上只是一个例子;我正在寻找一种通用的方法,以避免陷入实现细节的泥潭中——在可能的情况下,我希望避免被诸如实现的执行顺序之类的细节分散注意力,因为这对我来说通常并不重要,并且会破坏函数式编程的一个主要卖点

  • 吃咖喱没什么害处。咖喱有时也会引入闭包。他们通常也很有效率。 参考我以前问过的。如有必要,您可以使用内联来提高性能

  • 但是,本例中的性能问题主要是由代码引起的:

    normalize p |>(列表集合的集合)。包含

  • 在这里,您需要执行
    Set.ofList setElems
    ,甚至您也需要执行它。它花费O(n logn)时间。 您需要将
    setElems
    的类型更改为F#Set,而不是List。顺便说一句,对于小集合,使用列表甚至比查询集合更快

    只要F#不区分纯代码和不纯代码,我怀疑我们会看到这种优化。但是,您可以使咖喱语显式

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

    isHtmlSet
    现在将只调用
    isInSet
    一次以获得闭包,同时执行列表的

    @Kha的答案是正确的。F#无法重写

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

    除非它知道
    g
    没有效果,而且在今天的.NET框架中,通常无法对99%的表达式的效果进行推理。这意味着程序员仍然要像上面那样明确地编码求值顺序。

    来自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
    
    但这是否更优雅还是个问题

    另一个(疯狂的)想法是,可以使用计算表达式来实现这一点。允许您这样做的计算生成器的定义是非常不标准的(这不是人们通常使用的东西,它与单子或任何其他理论都没有任何关系)。但是,应该可以这样写:

    type CurryBuilder() = 
      member x.Bind((), f:'a -> 'b) = f
      member x.Return(a) = a
    let curry = new CurryBuilder()
    
    curry
    计算中,您可以使用
    let
    表示要获取函数的下一个参数(在对前面的代码求值之后):

    以下是一些资源,提供了有关计算表达式的更多信息:

    • 使用计算表达式的常用方法在我的书的免费示例章节中有描述:(PDF)

    • 上面的例子使用了(PDF)中详细描述的一些翻译细节


    我意识到我可以改变我的函数语义来解决性能问题,但这并不容易,也不可取。当前代码易于阅读;如果我需要传入集合,代码已经变得越来越长了-每个新的
    isHtmlPath
    -like函数都有更多的样板文件。所以,我想要一个更好的设计模式:在不需要我(懒惰的程序员)逐案思考的情况下预计算可预计算位。@Eamon我明白你的观点。我认为F#现在不会进行这种优化。所以我们程序员有责任设计高效的接口。你有推荐阅读计算表达式的链接吗?我明白了,但不太了解细节…@Eamon:我在答案的末尾添加了两个链接。感谢这个伟大的例子和计算表达式令人费解的用法!这最终说服了我更仔细地研究它们——即使我不确定这里是否真的需要它们。@Eamon:我同意这不是计算表达式的正确用法:-)尽管如此,它们还是非常有趣和强大!我认为,即使是这个答案中的第一个解决方案也太复杂了——Kha的答案更容易阅读,更简短,即使你忽略了
    preProcess
    的定义。我认为这就是我要做的——它也可以写得更简短:
    让isInSet setElems normalize=normalize>(Set.of list setElems)。包含
    。但是,从风格上讲,在普通代码中,您更喜欢更通用、更灵活的“返回lamba表达式”,还是更不灵活、更不通用但更短的函数组合版本?
    
    type CurryBuilder() = 
      member x.Bind((), f:'a -> 'b) = f
      member x.Return(a) = a
    let curry = new CurryBuilder()
    
    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