【问题标题】:How are point-free functions actually "functions"?无点函数实际上是如何“函数”的?
【发布时间】:2015-05-11 00:31:31
【问题描述】:

Conal 认为空元构造的类型不是函数。但是,无点函数在 Wikipedia 上的描述是这样的,当它们在定义中没有明确的参数时,它似乎是一个柯里化的属性。它们的功能究竟如何?

具体来说:f = mapf = id . map 在这种情况下有何不同?如,f = map 只是对一个值的绑定,该值恰好是一个函数,其中f 简单地“返回”map(类似于f = 2“返回”2)然后接受参数.但是f = id . map 被称为函数,因为它是无点的。

【问题讨论】:

  • 无点函数是一个接受另一个函数而不是值的函数。我不明白它怎么会不是一个函数。
  • 您是否将无点与高阶混淆了?
  • 无点函数接受参数,而零“函数”不接受。无点函数的参数没有显式命名,但它仍然需要参数。
  • @Shou 不。不过我觉得我累了,我的最后一个帖子是错误的。我知道一个无点函数作为一个函数,它不是显式地获取它的所有参数,而是返回一个接受最后一个参数的函数。 isAllUpper xs = all isUpper xsisAllUpper = all isUpper;后者是无点的。
  • 在您的编辑中,它们并没有什么不同:两者都是无点函数。一个无效“函数”的例子是x = 'a'x 根本不接受任何参数。

标签: haskell functional-programming pointfree


【解决方案1】:

Conal 的博文归结为“非函数不是函数”,例如False 不是函数。这很明显;如果您考虑所有可能的值并删除具有函数类型的值,那么剩下的就是......不是函数。

这与无点定义的概念完全无关。

考虑以下函数定义:

map1, map2, map3, map4 :: (a -> b) -> [a] -> [b]

map1 = map

map2 = id . map

map3 f = map f

map4 _ [] = []
map4 f (x:xs) = f x : map4 f xs

这些都是同一个函数的定义(还有无数种方法可以定义与map函数等效的东西)。 map1 显然是一个无点定义; map4 显然不是。它们显然也都有一个函数类型(同一个!),那么我们怎么能说无点定义不是函数呢?仅当我们将“函数”的定义更改为不同于 Haskell 程序员通常所指的定义时(即函数的类型为 x -> y,对于某些 xy;在这种情况下,我们'将a -> b 用作x,将[a] -> [b] 用作y)。

map3的定义是“部分无点”(降点?);该定义将其第一个参数命名为f,但没有提及第二个参数。

所有这一切的重点是,“无点”是定义的一种品质,而“作为函数”是的一种属性。无点函数 的概念实际上没有意义,因为给定的函数可以有多种定义方式(其中一些是无点的,有些不是)。每当您看到有人谈论无点函数时,他们的意思是无点定义

您似乎担心map1 = map 不是函数,因为它只是对现有值map 的绑定,就像x = 2 一样。你在这里混淆了概念。请记住,函数在 Haskell 中是一流的; “作为函数的事物”是“作为价值的事物”的子集,而不是不同类别的事物!因此,当map 是一个现有值这是一个函数 时,是的map1 = map 只是将一个新名称绑定到一个现有值。它定义了函数map1;两者并不相互排斥。

您可以通过查看代码来回答“这个问题是否毫无意义”;函数的定义。您可以通过查看类型来回答“这是一个函数”的问题。

【讨论】:

  • 7 似乎是一个糟糕的例子,因为它实际上是一个函数,类型为 Num a => a(它接受一个类型和一个 Num 字典并产生一个值)。然而,一旦应用了该函数,结果(比如7 :: Int)就不是函数了。
  • @dfeuer 我不喜欢这样思考数字!或键入类。从字典中计算是一个实现问题,而不是7 :: Num a => a 意味着。但即使考虑到这一点,7 也不符合“函数”的定义,即“具有函数类型的事物”。您不会将7 :: Num a => a 应用于要计算7 :: Int 的任何东西,就像您将not 应用于False 以计算True 一样;您只需在类型受限的位置引用它。我认为将其称为函数只是搅浑水,而不是澄清。
  • @Ben 更准确的说法是7 可以是一个函数。为(a -> b)定义一个Num实例,7是一个函数。不过,无论范围内的实例如何,都保证像 ()FalseJust 45 这样的东西不是函数。
  • 字典本身确实是一个实现问题,但它是一个接受类型和某种Numness 的函数的概念似乎是不可避免的。 “在类型受限的位置引用它”似乎非常模糊。试图确定它会让你“只要aNum 类型,7 就具有a 类型”。但是建设性(Haskell 的类型系统肯定是建设性的),全称只能通过某种函数来证明!
  • @dfeuer 这是一种不同的函数,不是你可以在 Haskell 中实际编写的函数(除了通过使用类型类约束来暗示它)。 Haskell 函数是具有A -> B 形式类型的值,适用于任何(可能相互依赖的)类型AB。从类型和字典到您正在谈论的值的函数不是(Haskell)值,并且它的“类型”不是所需的形式,因为输入类型必须类似于(Dict, *),不是(Haskell)类型(可以说是一种?)。
【解决方案2】:

与某些人可能认为 Haskell 中的一切都不是函数相反。严重地。数字、字符串、布尔值等不是函数。甚至没有零函数。

零函数

空函数是不带参数并执行一些“副作用”计算的函数。例如,考虑这个无效的 JavaScript 函数:

main();

function main() {
    alert("Hello World!");
    alert("My name is Aadit M Shah.");
}

不带参数的函数只有在有副作用的情况下才能返回不同的结果。因此,它们类似于 Haskell 中的 IO 操作,不带参数并执行一些副作用计算:

main = do
    putStrLn "Hello World!"
    putStrLn "My name is Aadit M Shah."

一元函数

相比之下,Haskell 中的函数永远不能为空。事实上,Haskell 中的函数总是一元的。 Haskell 中的函数总是只有一个参数。 Haskell 中的多参数函数可以使用柯里化或使用具有多个字段的数据结构来模拟。

add' :: Int -> Int -> Int -- an example of using currying
add'   x  y  = x + y

add'' :: (Int, Int) -> Int -- an example of using multi-field data structures
add'' (x, y) = x + y

协变和逆变

Haskell 中的函数是一种数据类型,就像您在 Haskell 中定义的任何其他数据类型一样。但是,函数很特殊,因为它们是contravariant in the argument type and covariant in the return type

当您定义一个新的代数数据类型时,其类型构造函数的所有字段都是协变的(即数据源)而不是逆变的(即数据汇)。协变字段产生数据,而逆变字段消耗数据。

例如,假设我创建了一个新的数据类型:

data Foo = Bar { field1 :: Char, field2 :: Int }
         | Baz { field3 :: Bool }

这里的字段field1field2field3 是协变的。它们分别产生CharIntBool 类型的数据。考虑:

let x = Baz True -- I create a new value of type Foo
in  field3 x     -- I can access the value of field3 because it is covariant

现在,考虑函数的定义:

data Function a b = Function { domain   :: a -- the argument type
                             , codomain :: b -- the return   type
                             }

当然,函数实际上并没有如下定义,但我们假设它是这样定义的。一个函数有两个字段domaincodomain。当我们创建 Function 类型的值时,我们不知道这两个字段中的任何一个。

  1. 我们不知道domain 的值,因为它是逆变的。因此,它需要由用户提供。
  2. 我们不知道codomain 的值,因为尽管它是协变的,但它可能取决于domain,我们不知道domain 的值。

例如,\x -> x + x 是一个函数,其中domain 的值为xcodomain 的值为x + x。这里domain 是逆变的(即数据接收器),因为数据通过domain 进入函数。同样,codomain 是协变的(即数据源),因为数据通过 codomain 来自函数。

Haskell 中代数数据结构的字段(如我们之前定义的Foo)都是协变的,因为数据通过它们的字段从这些数据结构中出来。数据永远不会像 domain 函数字段那样进入这些结构。因此,它们永远不会是逆变的。

多参数函数

正如我之前解释的,虽然 Haskell 中的所有函数都是一元的,但我们可以使用柯里化或具有多个数据结构的字段来模拟多参数函数。

为了理解这一点,我将使用一个新的符号。减号 ([-]) 表示逆变类型。加号 ([+]) 表示协变类型。因此,从一种类型到另一种类型的函数表示为:

[-] -> [+]

现在,函数的域和共域可以分别替换为其他类型。例如在柯里化中,函数的 codomain 是另一个函数:

[-] -> ([-] -> [+]) -- an example of currying

请注意,当协变类型被替换为另一种类型时,新类型的方差将被保留。这是有道理的,因为这相当于一个有两个参数和一个返回类型的函数。

另一方面,如果我们用另一个函数替换域:

([+] -> [-]) -> [+]

请注意,当我们用另一种类型替换逆变类型时,新类型的方差会翻转。这是有道理的,因为虽然([+] -> [-]) 作为一个整体是逆变的,但它的输入类型变成了整个函数的输出,它的输出类型变成了整个函数的输入。例如:

function f(g) {       // g is contravariant for f (an input value for f)
    return g(x) + 10; // x is covariant for f (an output value for f)
                      // x is contravariant for g (an input value for g)
                      // g(x) is contravariant for f (an input value for f)
                      // g(x) is covariant for g (an output value for g)
                      // g(x) + 10 is covariant for f (an output value for f)
}

Currying 模拟多参数函数,因为当一个函数返回另一个函数时,我们会得到多个输入和一个输出,因为为返回类型保留了方差:

[-] -> [-] -> [+]        -- a binary function
[-] -> [-] -> [-] -> [+] -- a ternary function

具有多个字段作为函数域的数据结构也可以模拟多参数函数,因为函数的参数类型会翻转方差:

([+], [+])        -- the fields of a tuple are covariant
([-], [-]) -> [+] -- a binary function, variance is flipped for arguments

非函数

现在,如果您看一下数字、字符串和布尔值等值,这些值不是函数。但是,它们仍然是协变的。

例如,5 自身产生一个值5。同样,Just 5 产生一个值Just 5fromJust (Just 5) 产生一个值5。这些表达式都没有消耗一个值,因此它们都不是逆变的。但是,在Just 5 中,函数Just 使用值5,而在fromJust (Just 5) 中,函数fromJust 使用值Just 5

因此,Haskell 中的所有内容都是协变的,除了函数的参数(它们是逆变的)。这很重要,因为 Haskell 中的每个表达式都必须计算出一个值(即产生一个值,而不是消耗一个值)。同时,我们希望函数消耗一个值并产生一个新值(从而促进数据的转换,beta reduction)。

最终的结果是我们永远不能有一个逆变表达式。例如,表达式Just 是协变的,而表达式Just 5 也是协变的。但是,在表达式Just 5 中,函数Just 使用值5。因此,逆变仅限于函数参数并受函数范围的限制。

因为 Haskell 中的每个表达式都是协变的,所以人们经常将像 5 这样的非函数值视为“空函数”。虽然这种直觉是有见地的,但它是错误的。值 5 不是空函数。这是一个不能被 beta 归约的表达式。同样,值fromJust (Just 5) 也不是空函数。它是一个表达式,可以 beta 简化为 5,它不是一个函数。

但是,表达式 fromJust (Just (\x -> x + x)) 是一个函数,因为它可以通过 beta 简化为 \x -> x + x,这是一个函数。

Pointful 和 Pointfree 函数

现在,考虑函数\x -> x + x。这是一个有意义的函数,因为我们通过给它命名 x 来明确声明函数的参数。

每个函数也可以用 pointfree 样式编写(即不显式声明函数的参数)。例如,函数\x -> x + x 可以用无点样式编写为join (+),如following answer 中所述。

请注意,join (+) 是一个函数,因为它在 beta 中简化为函数 \x -> x + x。它看起来不像一个函数,因为它没有点(即显式声明的参数)。但是,它仍然是一个函数。

Pointfree 函数与柯里化无关。 Pointfree 函数是关于编写没有点的函数(例如 join (+) 而不是 \x -> x + x)。柯里化是指一个函数返回另一个函数,从而允许部分应用(例如,\x -> \y -> x + y,可以用无点样式编写为(+))。

名称绑定

在绑定f = map 中,我们只是给map 提供了替代名称f。请注意,f 不会“返回”map。它只是map 的替代名称。例如,在绑定x = 5 中,我们不会说x 返回5,因为它没有。 x 这个名字既不是函数也不是值。它只是一个标识5 值的名称。同样,在f = map 中,名称f 只是标识map 的值。据说f 这个名字表示一个函数,因为map 表示一个函数。

绑定f = map 是无点的,因为我们没有明确声明f 的任何参数。如果我们愿意,我们可以写f g xs = map g xs。这将是一个有意义的定义,但由于eta conversion,我们可以更简洁地以无点形式将其写成f = map。 eta 转换的概念是\x -> f x 等价于f 本身,并且可以将pointful \x -> f x 转换为pointfree f,反之亦然。请注意,f g xs = map g xs 只是 f = \g xs -> map g xs 的语法糖。

另一方面,f = id . map 是一个函数,不是因为它是无点的,而是因为 id . map beta 简化为函数 \x -> id (map x)。顺便说一句,任何由id 组成的函数都等价于它自己(即id . f = f . id = f)。因此,id . map 等同于 map 本身。 f = mapf = id . map 没有区别。

请记住,f 不是“返回”id . map 的函数。为了方便起见,它只是给表达式id . map 取了一个名字。

附:有关无点函数的介绍,请阅读:

What does (f .) . g mean in Haskell?

【讨论】:

  • “空函数是有副作用的,因为它们不带参数。”嗯……你能把它改写成少一点不合逻辑的说法吗?
  • @leftaroundabout 已编辑。这是否阐明了陈述?
猜你喜欢
  • 2012-06-10
  • 2013-09-19
  • 2014-03-11
  • 1970-01-01
  • 2017-09-15
  • 1970-01-01
  • 1970-01-01
  • 2010-12-31
  • 1970-01-01
相关资源
最近更新 更多