Conal这里认为,nullary构造的类型不是函数。然而,无点函数被描述为例如在Wikipedia上,当它们在它们的定义中没有明确的参数时,它看起来更像是一个currying的属性。它们究竟是如何运作的?
具体来说:f = id . map
和f = map
在这种情况下有何不同?同样,f
只是绑定到恰好是map
只是“返回”f = 2
的函数的值(类似于2
“返回”的方式{ {1}})然后接受参数。但是f = id . map
被称为函数,因为它是无点的。
答案 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 -> y
和x
,函数才是y
类型的函数;在这种情况下,我们将a -> b
用作x
,[a] -> [b]
用于y
。
map3
的定义是“部分无点”(减少点数?);该定义命名其第一个参数f
,但未提及第二个参数。
所有这一点的要点是“无点定义”是定义的质量,而“作为函数”是值的属性。无点函数的概念实际上没有意义,因为给定函数可以通过多种方式定义(其中一些是无点的,另一些则不是)。每当你看到某人谈论无点功能时,他们就意味着一个无点的定义。
您似乎担心map1 = map
不是函数,因为它只是对现有值map
的绑定,就像x = 2
一样。你这里的想法很混乱。请记住,函数在Haskell中是一流的; “功能性的东西”是“价值的东西”的子集,而不是一类不同的东西!因此,当map
是现有值(函数)时,则map1 = map
只是将新名称绑定到现有值。定义函数map1
的也;这两者并不相互排斥。
您可以通过查看代码来回答“这是免费的”这个问题吗?一个函数的定义。您可以通过查看 types 来回答“这是一个功能”这个问题。
答案 1 :(得分:5)
与某些人可能认为Haskell中的所有内容都不相关的内容相反。认真。数字,字符串,布尔值等不是函数。甚至不是无效的功能。
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 }
此处字段field1
,field2
和field3
是协变的。它们分别生成Char
,Int
和Bool
类型的数据。考虑:
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
}
当然,函数实际上并没有如下定义,但我们假设它是。函数有两个字段domain
和codomain
。当我们创建Function
类型的值时,我们不会知道这两个字段中的任何一个。
domain
的价值,因为它是逆变的。因此,它需要由用户提供。codomain
的价值,因为虽然它是协变的但它可能取决于domain
而我们不知道domain
的价值}。例如,\x -> x + x
是一个函数,其中domain
的值为x
,codomain
的值为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 5
,fromJust (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 = map
和f = id . map
之间没有区别。
请记住f
不是“返回”id . map
的函数。为方便起见,它只是表达式id . map
的名称。
P.S。有关无点函数的介绍,请阅读: