高阶 lambda 函数中的变量作用域

Variable scope in a higher-order lambda function

在解决 8 Queens 问题时,有人使用了以下代码行:

sameDiag try qs = any (\(colDist,q) -> abs (try - q) == colDist) $ zip [1..] qs

try是一个项目; qs 是相同项目的列表。

  1. 谁能解释一下 lambda 函数中的 colDistq 是如何绑定到任何东西的?

  2. lambda 函数体中使用的 tryq 是如何进入同一作用域的?

  3. 在某种程度上这是一个 Haskell 习语,这种设计方法有助于解决什么问题?

zip :: [a] -> [b] -> [(a,b)] 取两个列表并将它们连接成对,在末尾丢弃任何剩余的列表。

any :: (a -> Bool) -> [a] -> Bool 接受一个函数和一个 a 的列表,然后 returns True 如果任何值返回 true 或不返回。

因此 colDistqzip [1..] qs 生成的列表中对的第一个和第二个元素,当它们应用于 any.

q 仅绑定在 lambda 函数体内 - 这与 lambda 演算相同。由于 try 之前在函数定义中被绑定,因此它在此内部范围内仍然可用。如果您想到 lambda 演算,尽管 xy 在不同时间被绑定,术语 \x.\y.x+y 是有意义的。

至于设计方法,这种方法比尝试手动迭代或递归列表要简洁得多。它的意图对我来说似乎很清楚(相对于它来自的更大的代码库)。

函数 any 是一个带有 2 个参数的 higher-order function:

  • 第一个参数的类型为a -> Bool,即从aBool
  • 的函数
  • 第二个参数的类型为[a],即a;
  • 类型的项目列表

第一个参数是一个函数,它从作为第二个参数传递的列表中获取任何元素,并且returns一个Bool 基于该元素。 (好吧,它可以采用 a 类型的任何值,而不仅仅是该列表中的值,但很明显 any 不会用 a 的某些任意值调用它但列表中的那些。)


然后您可以通过稍微重构来简化对原始片段的思考:

sameDiag :: Int -> [Int] -> Bool
sameDiag try qs = any f xs
  where
    xs = zip [1..] qs
    f = (\(colDist, q) -> abs (try - q) == colDist)

可以转化为

sameDiag :: Int -> [Int] -> Bool
sameDiag try qs = any f xs
  where
    xs = zip [1..] qs
    f (colDist, q) = abs (try - q) == colDist)

又可以转化为

sameDiag :: Int -> [Int] -> Bool
sameDiag try qs = any f xs
  where
    xs = zip [1..] qs
    f pair = abs (try - q) == colDist) where (colDist, q) = pair

(注意 sameDiag 也可以有更通用的类型 Integral a => a -> [a] -> Bool 而不是当前的单态类型)

——那么 f pair = ... 中的 pair 是如何绑定到一个值的呢?嗯,很简单:它只是一个函数;调用它的人必须为 pair 参数传递一个值。 — 当调用 any 并将第一个参数设置为 f 时,调用函数 any 调用 f,并使用列表的各个元素 xs 作为参数 pair.

的值传入

并且,由于 xs 的内容是一对列表,因此可以将此列表中的单个对传递给 f,因为 f 期望它只是.


编辑: any 的进一步解释以解决提问者的评论:

Is this a fair synthesis? This approach to designing a higher-order function allows the invoking code to change how f behaves AND invoke the higher-order function with a list that requires additional processing prior to being used to invoke f for every element in the list. Encapsulating the list processing (in this case with zip) seems the right thing to do, but is the intent of this additional processing really clear in the original one-liner above?

在调用 f 之前,any 确实没有额外的 处理 。除了简单地遍历传入的列表 xs 之外,还有非常简单的簿记:在迭代期间对元素调用 f,并立即中断迭代并返回 True 第一次f returns True 对于任何列表元素。

any 的大部分行为都是 "implicit",尽管它由 Haskell 的惰性求值、基本语言语义以及现有函数处理,Haskell =16=] 由(至少我下面的版本组成,any' — 我还没有查看 any 的内置 Prelude 版本,但我我确信它没有太大的不同;只是可能进行了更多的优化)。

事实上,any 很简单,在 GHCi 提示符下用一行代码重新实现它几乎是微不足道的:

Prelude> let any' f xs = or (map f xs)

现在让我们看看 GHC 计算的是什么类型:

Prelude> :t any'
any' :: (a -> Bool) -> [a] -> Bool

— 与内置的 any 相同。那么让我们试运行一下:

Prelude> any' odd [1, 2, 3]  -- any odd values in the list?
True
Prelude> any' even [1, 3]    -- any even ones?
False
Prelude> let adult = (>=18)
Prelude> any' adult [17, 17, 16, 15, 17, 18]

— 看看您有时是如何编写出几乎看起来像带有高阶函数的英语代码的?