F#预计算逻辑的正确理解

3
这个问题是基于我之前的一个有关可变值的问题 (原问题链接) 扩展而来。 我非常确定这个问题的主题,预计算,与链接的问题有很多关联。
请看下面的例子,这些例子来自我正在学习的书:
let isWord (words : string list) =
    let wordTable = Set.ofList words     // Expensive computation!
    fun w -> wordTable.Contains(w)

val isWord : words:string list -> (string -> bool)

这个函数接受一个字符串列表,返回一个检查输入字符串是否在列表中的函数。有了这个小巧可爱的辅助函数,我们来看两个例子:

let isCapital = isWord ["London"; "Paris"; "Warsaw"; "Tokyo"];;
val isCapital : (string -> bool)

let isCapitalSlow word = isWord ["London"; "Paris"; "Warsaw"; "Tokyo"] word
val isCapitalSlow : (string -> bool)

我原本以为这两个函数做的事情完全一样,但实际上并非如此。据书上所说,第一个函数会从给定的列表中预先计算集合,而第二个函数则是在每次调用函数时才计算集合。
正如我在PL课程中学到的那样,为了评估lambda演算表达式,必须将每个参数传递给主体。如果缺少任何一个参数,表达式就无法被评估。
基于此,我得出结论:第一个函数没有参数,所以当列表被提供时,它可以立即开始评估;而第二个函数直到参数word被给定才开始评估。到这里还好,但是在思考以上链接的问题后,我不确定我的理解是否正确。
从以上问题和答案出发思考,似乎只要存在无需特定情况的表达式部分,就会被评估并进行预计算,只会执行一次,就像第一个例子一样。这可能对优化和性能产生很大影响,因此我想澄清自己对这个主题的理解。

没有检查过,似乎isCapital是一个值。这个值是一个函数,在调用isWord时返回。因此,当计算isCapital时,wordTable只被计算一次。但是isCapitalSlow不是一个值 - 它是一个函数,所以每次调用它时都会调用isWord。 - Bent Tranberg
如果还不清楚,记住函数是F#语言的一等成员。isCapital是一个值,但该值是计算出来的函数。另一方面,isCapitalSlow是一个编码(术语?)函数,而不是计算出来的函数,因此不是一个值。isWord是一个计算函数的函数。 - Bent Tranberg
2个回答

3
我得出结论,第一个lambda函数没有参数,所以当列表给出时可以立即开始计算,但第二个lambda函数在给出参数之前无法开始计算。
这完全正确。
评估似乎会一直进行,直到它变得无法评估,可能是因为缺乏信息、参数或其他任何东西。 这本质上也是正确的,但比你的表述简单。 "缺乏信息" 不是什么非常复杂的东西——它仅仅是Lambda函数是值,直到指定其参数才能计算。
如果我们使用“fun x -> ...”符号重写所有内容,可能更容易理解:
let isWord = fun (words : string list) =
    let wordTable = Set.ofList words
    fun w -> wordTable.Contains(w)

let isCapital = 
  isWord ["London"; "Paris"; "Warsaw"; "Tokyo"]

let isCapitalSlow = fun word -> 
  isWord ["London"; "Paris"; "Warsaw"; "Tokyo"] word

评估从上到下进行。
1. 分配给isWord的表达式是一个函数,因此该函数体无法被评估。 2. 分配给isCapital的表达式是一个函数应用,因此它可以被评估。这将依次计算wordTable的值并返回一个函数 - 它是一个函数且无法被评估。 3. 分配给isCapitalSlow的表达式是一个函数,不能被评估。 4. 如果您稍后调用isCapitalSlow "Prague",这将是一个函数应用,因此它可以被评估。然后它会使用城市列表作为参数调用isWord,而isWord会依次调用Set.ofList来构建wordTable,并产生一个函数,随后该函数会使用word作为参数被评估。

每个 F# something 都是一个值,没有任何参数它就无法被评估。好的,这就是那么简单的事情。感谢您详细的回答! - MyBug18
稍微澄清一下 - 每个 F# 的东西都是一个_表达式_,包括 fun x -> x + 1x + 11。其中一些表达式是值,不能被评估,例如 1fun x -> x + 1。有些不是值,可以被评估,例如 1 + 2(在 lambda 演算中,当应用函数时进行替换,你会得到这个结果)。 - Tomas Petricek

1

由于您似乎熟悉C#,我们可以将其重写为C#类:

class IsWord
{
    HashSet<string> set;
    public IsWord(string[] words) => set = new HashSet<string>(words);
    public bool Contains(string word) => set.Contains(word);
}

等效的函数会是什么样子?

Func<string, bool> isCapital = 
    new IsWord(new[] { "London", "Paris", "Warsaw", "Tokyo" }).Contains;

Func<string, bool> isCapitalSlow = 
    (word) => new IsWord(new[] { "London", "Paris", "Warsaw", "Tokyo" }).Contains(word);

请注意,isCapital 仅创建一次类的实例,并返回其包含方法。所以每次调用 isCapital,实际上只调用了 HashSet.Contains
isCapitalSlow 中,每次调用该方法时都会创建一个 IsWord 实例和一个 HashSet。这自然会更慢。
在惯用的 F# 中,您可以编写如下代码:
let isWord words =
    let wordTable = Set.ofList words     
    let contains word = wordTable |> Set.contains word
    contains

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接