我的功能如下:
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#的娴熟,可读性,同时仍然可以获得预先评估集合构造的更有效的行为。
以上是只是一个例子;我正在寻找一种避免让我陷入实施细节的一般方法 - 尽可能我希望避免被实施的执行顺序等细节分心,因为这对我来说通常并不重要有点破坏功能编程的一个主要卖点。
答案 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
以下是一些资源,其中包含有关计算表达式的更多信息:
在我的书的免费样本章节中描述了使用计算表达式的常用方法:Chapter 12: Sequence Expressions and Alternative Workflows(PDF)
上面的示例使用了the F# specification(PDF)
答案 3 :(得分:2)
Currying不疼。 Currying有时会引入闭合。它们通常也很有效率。 参考之前我问的this question。如有必要,您可以使用内联来提高性能。
但是,示例中的性能问题主要是由于您的代码:
normalize p |> (Set.ofList setElems).Contains
在这里你需要执行Set.ofList setElems
即使你在咖喱。它花费O(n log n)时间。
您需要将setElems
的类型更改为F#Set,而不是立即列出。顺便说一下,对于小集合,使用列表比查询更快。