无点功能如何实际“功能”?

时间:2015-05-10 03:21:30

标签: haskell functional-programming pointfree

Conal这里认为,nullary构造的类型不是函数。然而,无点函数被描述为例如在Wikipedia上,当它们在它们的定义中没有明确的参数时,它看起来更像是一个currying的属性。它们究竟是如何运作的?

具体来说:f = id . mapf = map在这种情况下有何不同?同样,f只是绑定到恰好是map只是“返回”f = 2的函数的值(类似于2“返回”的方式{ {1}})然后接受参数。但是f = id . map被称为函数,因为它是无点的。

2 个答案:

答案 0 :(得分:16)

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 -> yx,函数才是y类型的函数;在这种情况下,我们将a -> b用作x[a] -> [b]用于y

map3的定义是“部分无点”(减少点数?);该定义命名其第一个参数f,但未提及第二个参数。

所有这一点的要点是“无点定义”是定义的质量,而“作为函数”是的属性。无点函数的概念实际上没有意义,因为给定函数可以通过多种方式定义(其中一些是无点的,另一些则不是)。每当你看到某人谈论无点功能时,他们就意味着一个无点的定义

您似乎担心map1 = map不是函数,因为它只是对现有值map的绑定,就像x = 2一样。你这里的想法很混乱。请记住,函数在Haskell中是一流的; “功能性的东西”是“价值的东西”的子集,而不是一类不同的东西!因此,当map是现有值(函数)时,则map1 = map只是将新名称绑定到现有值。定义函数map1;这两者并不相互排斥。

您可以通过查看代码来回答“这是免费的”这个问题吗?一个函数的定义。您可以通过查看 types 来回答“这是一个功能”这个问题。

答案 1 :(得分:5)

与某些人可能认为Haskell中的所有内容都不相关的内容相反。认真。数字,字符串,布尔值等不是函数。甚至不是无效的功能。

Nullary Functions

nullary函数是一个不带参数并执行一些“副作用”计算的函数。例如,考虑这个无效的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中的多参数函数可以使用currying或使用具有多个字段的数据结构进行模拟。

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的价值}。
  3. 例如,\x -> x + x是一个函数,其中domain的值为xcodomain的值为x + x。这里domain是逆变的(即数据汇),因为数据通过domain进入函数。同样,codomain是协变的(即数据源),因为数据是通过codomain从函数中传出的。

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

    多参数功能

    正如我之前解释的那样,尽管Haskell中的所有函数都是一元的,但我们可以使用currying或具有多个数据结构的字段来模拟多参数函数。

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

    [-] -> [+]
    

    现在,函数的域和codomain可以分别用其他类型替换。例如,在currying中,函数的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这样的非函数值是“nullary functions”。虽然这种直觉很有洞察力但却是错误的。值5不是一个纳函数。这是一种不能降低β的表达。同样,值fromJust (Just 5)不是一个必然的函数。这是一个表达式,可以将β降低到5,这不是一个函数。

    然而,表达式fromJust (Just (\x -> x + x))是一个函数,因为它可以被beta减少为\x -> x + x这是一个函数。

    有点和无点函数

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

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

    请注意,join (+)是一个函数,因为它会将beta缩减为函数\x -> x + x。它看起来不像一个函数,因为它没有点(即显式声明的参数)。但是,它仍然是一种功能。

    Pointfree功能与currying无关。 Pointfree函数是关于编写没有点的函数(例如join (+)而不是\x -> x + x)。 Currying是当一个函数返回另一个函数时,从而允许部分应用(例如\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本身,有点\x -> f x可以转换为无点f,反之亦然。请注意,f g xs = map g xs只是f = \g xs -> map g xs的语法糖。

    另一方面,f = id . map是一个函数,不是因为它是无点的,而是因为id . map beta缩减为函数\x -> id (map x)。 BTW,由id组成的任何函数都等同于它自己(即id . f = f . id = f)。因此,id . map相当于map本身。 f = mapf = id . map之间没有区别。

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

    P.S。有关无点函数的介绍,请阅读:

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