在纯函数式编程语言中,数据(例如字符串、整数、浮点数等)是否也只是函数?

12

我在思考像 Ruby 这样的纯面向对象语言,其中所有东西,包括数字、整数、浮点数和字符串本身都是对象。纯函数式语言也是这样吗?例如,在 Haskell 中,数字和字符串也是函数吗?

我知道 Haskell 是基于 λ 演算的,它将所有东西,包括数据和操作,表示为函数。对我来说似乎很合理,一个“纯函数式语言”会将所有东西都建模为函数,并且遵循函数必须始终返回相同输出的定义,且没有状态。


7
一位知名的科学家曾经在公开讲座上谈论了天文学。他描述了地球如何绕着太阳运转,而太阳则反过来绕着一个被称为我们的星系的广阔恒星集合的中心运转。最后,房间后面的一位老太太起身说:“你告诉我们的那些都是废话。世界实际上是一个平板,支撑在一个巨大的乌龟背上。” 科学家露出了一丝傲慢的微笑,回答道:“那这只乌龟站在什么上面呢?”“你很聪明,年轻人,非常聪明。”老太太说,“但是这个乌龟下面还有无数只乌龟!” - HostileFork says dont trust SE
我认为这强烈取决于您所谓的函数是什么。除非我们从定义开始考虑我们认为的函数,否则我们几乎无法讨论什么是函数,什么不是函数。 - Petr
2
@HostileFork:幸运的是,乌龟也使用函数(http://en.wikipedia.org/wiki/Logo_%28programming_language%29)。一路使用Lambda! - C. A. McCann
5个回答

15

2
我正要发布的那个链接。 - luqui
6
我认为将Ruby或其他纯面向对象编程语言作为比喻有些不妥,因为虽然Ruby并非所有东西都是一个对象,但是每个“值”都是一个对象——这也是“纯面向对象”一词通常被定义的方式。然而,在Haskell或其他纯函数式编程语言中却不是如此,即并非每个值都是一个函数。 - sepp2k
@sepp2k,你说得很对,比较是有缺陷的。然而,我的参数列表示例有点牵强。代码块也不是对象,但可以包装在对象中(Proc类),因此即使一些可以成为对象的东西也不是。关于Ruby的更多信息,请参见此答案:https://dev59.com/onA75IYBdhLWcg3wMl4Z#3429889 - wrhall

14

@wrhall给出了一个好的答案。但你也有一定的正确性,在纯λ演算中一切都是函数是一致的,这种语言是图灵完全的(能够表达任何Haskell等语言能表达的纯计算)。

这带来了一些非常奇怪的事情,因为你可以对任何东西进行的唯一操作就是将其应用于另一些东西。你有一些值f并想了解它的一些信息,你唯一的选择就是将它应用于某个值x以得到f x,这是另一个函数,唯一的选择是将其应用于另一个值y,以此类推得到f x y等等。

通常我会将纯λ演算解释为对不是函数的东西进行变换,但只能表达函数本身。也就是说,我可以创建一个函数(使用一些Haskelly语法糖来实现递归和let):

purePlus = \zero succ natCase -> 
   let plus = \m n -> natCase m n (\m' -> plus m' n)
   in plus (succ (succ zero)) (succ (succ zero))

在这里,我表达了计算2+2,而不需要知道非函数的存在。我只是将我定义的函数所需的参数作为参数,并且这些参数的值可以是church编码或者它们可以是“真实”的数字(无论那意味着什么)——我的定义并不关心。

您也可以认为Haskell是同样的情况。没有特别的理由认为有东西不是函数,也没有特别的理由认为一切都是函数。但是Haskell的类型系统至少可以防止您将参数应用于数字(任何想到fromInteger的人现在都需要闭上嘴巴!:-))。在上述解释中,这是因为数字不一定被建模为函数,因此您不一定可以将参数应用于它们。

如果现在还不清楚,那么这整个答案都是一个技术/哲学的离题,而您问题的简单答案是“不,不是所有功能语言都是函数。函数就是您可以将参数应用于的东西,仅此而已。”


我不确定我理解你所说的“但是Haskell的类型系统至少可以防止你将参数应用于数字(任何现在想到fromInteger的人都需要闭嘴!”这句话。你能详细说明一下吗? - AndrewC
@luqui: "这会给你带来一些非常奇怪的事情,因为你能对任何东西做的唯一一件事就是将其应用于其他东西。你什么时候才能观察到某些东西呢?" 实际上,解决这个难题的一个答案是添加 IO 单子和一些 IO 原语集合... - Luis Casillas
@AndrewC,噢,你可以做些有趣的事情,涉及到instance Num (a -> b),它和Haskell相关,但是相对较为离题。 - luqui
哦,真讽刺,竟然是我在你说的话中错过了那个意思! :) - AndrewC
难道不应该是 let plus = \m n -> natCase m n (\m' -> plus m' (succ n)) 吗?另外,也许更好的命名方式是 pure_2Plus2 - Will Ness

6
一个非常不同的角度看待这个问题:在Haskell中,各种数据都可以用一种叫做 Church编码 的技术表示为函数。这是一种控制反转的形式:不是将数据传递给消耗它的函数,而是将数据隐藏在一组闭包中,并通过传递描述如何处理此数据的回调来使用它。
例如,任何使用列表的程序都可以被翻译成一个使用函数而不是列表的程序。
-- | A list corresponds to a function of this type:
type ChurchList a r = (a -> r -> r)  --^ how to handle a cons cell
                   -> r              --^ how to handle the empty list
                   -> r              --^ result of processing the list

listToCPS :: [a] -> ChurchList a r
listToCPS xs = \f z -> foldr f z xs

该函数以具体列表为起点,但这并非必需。您可以使用纯函数来构建 ChurchList 函数:

-- | The empty 'ChurchList'.
nil :: ChurchList a r
nil = \f z -> z

-- | Add an element at the front of a 'ChurchList'.
cons :: a -> ChurchList a r -> ChurchList a r
cons x xs = \f z -> f z (xs f z)

foldChurchList :: (a -> r -> r) -> r -> ChurchList a r -> r
foldChurchList f z xs = xs f z

mapChurchList :: (a -> b) -> ChurchList a r -> ChurchList b r
mapChurchList f = foldChurchList step nil
    where step x = cons (f x)

filterChurchList :: (a -> Bool) -> ChurchList a r -> ChurchList a r
filterChurchList pred = foldChurchList step nil
    where step x xs = if pred x then cons x xs else xs

最后一个函数使用了Bool,但是当然我们也可以用函数替换Bool

-- | A Bool can be represented as a function that chooses between two 
-- given alternatives.
type ChurchBool r = r -> r -> r

true, false :: ChurchBool r
true a _ = a
false _ b = b

filterChurchList' :: (a -> ChurchBool r) -> ChurchList a r -> ChurchList a r
filterChurchList' pred = foldChurchList step nil
    where step x xs = pred x (cons x xs) xs

这种转换基本上适用于任何类型,因此理论上,您可以摆脱Haskell中的所有“值”类型,仅保留()类型,(- >)IO类型构造函数,return>> = 用于IO,以及一组合适的IO原语。这显然是非常不切实际的,并且性能会更差(尝试编写tailChurchList :: ChurchList a r -> ChurchList a r 以了解其中的滋味)。

这是一个很好的答案,但事实证明该编码中的类型略有不正确。请参见https://dev59.com/FWw15IYBdhLWcg3wd7lj#6596700。 - luqui
可爱的解释,说明了如何从目前的值到仅有函数的理论上改变Haskell。 - AndrewC
为什么要保留 ()?它有一个完全合理的 Church 编码,即 id - C. A. McCann

6
“纯函数式”中的“纯”指的是“无副作用”的那种纯度。这与人们谈论“纯面向对象语言”时使用的“纯”的含义几乎没有关系,后者只意味着该语言仅在对象中纯粹地操作。
原因在于,“仅”作为“纯”的一种合理区分,可用于分类面向对象语言,因为存在像Java和C++这样的语言,它们明显具有与对象不太相关的值,还有像Python和Ruby这样的语言,可以认为每个值都是一个对象1
而对于函数式语言,实际上不存在“纯函数式”语言,即该语言可以操作的每个值都是函数。当然,你可以在这样的语言中编程。最基本版本的λ演算没有任何非函数的概念,但你仍然可以通过想出将要计算的东西表示为函数的方法来进行任意计算。2 但是,尽管lambda演算的简洁和极简主义往往非常适合证明关于编程的事情,但在这种“原始”的编程语言中编写实质性程序实际上是很困难的。像数字这样的基本事物的函数表示在实际物理机器上实现起来也往往效率很低。
但是,鼓励函数式风格但允许在任何地方进行未跟踪的副作用的语言与实际上强制要求您的函数是“纯”函数(类似于数学函数)的语言之间有一个非常重要的区别。面向对象编程非常强烈地依赖于使用不纯的计算3,因此没有实际上是在这个意义上纯粹的面向对象编程语言。
因此,“纯函数式语言”中的“纯”与“纯面向对象语言”中的“纯”意义非常不同。4在每种情况下,“纯vs非纯”的区别对于另一种语言来说完全没有趣味可言,因此没有非常强烈的动机来标准化术语的使用。

1 我所知道的所有“纯面向对象”的语言都存在一些边角情况,但这并不是很有趣。很明显,在那些1是某个类的实例,而且该类可以被子类化的语言中,对象比在1不是对象的语言中更加深入。

2 所有计算都涉及到表示。计算机什么也不知道,只有我们用来表示数字的位模式和恰好与数字操作对应的位模式操作(因为我们设计它们如此)。

3 这也不是根本的问题。您可以设计一个“纯”面向对象的语言,以这种意义上的纯形式。我倾向于编写大部分OO代码以保持纯洁。

4 如果这看起来晦涩难懂,您可能会反思在其他上下文中,“功能性”,“对象”和“语言”这些术语具有极其不同的含义。


0

getChar :: IO Char是一个函数还是不是?Haskell报告没有给出定义。但它指出getChar 是一个函数(参见这里)。(好吧,至少我们可以说它是一个函数。)

所以我认为答案是是的

我认为除了"一切都是函数"之外,很难有"函数"的正确定义。(什么是"正确定义"?好问题...)考虑下面的例子:

{-# LANGUAGE NoMonomorphismRestriction #-}
import Control.Applicative

f :: Applicative f => f Int
f = pure 1

g1 :: Maybe Int
g1 = f

g2 :: Int -> Int
g2 = f

f是函数还是数据类型?这要看情况。


那么"value"为"foo"呢?它是一个函数吗?除非你想以零元函数的方式思考,否则答案很明显是“不是”。虽然零元函数的概念可能是可辩护的,但我认为它没有帮助;例如,你必须放弃“所有Haskell函数都只接受一个参数”的说法。问题“f是函数还是数据类型”是一种错误的二分法。值和函数之间没有大的区别;函数只是一种特殊的值。 - Ben
我的看法是:g1不是一个函数,而g2是;f是一个多态值,可以在特定上下文中用作函数。像任何多态值一样,它的不同单态实例将具有不同的属性,但当您操作“完全通用”的f时,您无法访问其中任何一个,因为它们不适用于所有上下文中的f。 “成为函数”的属性并不比“成为列表”更特殊;g3 = f :: [Int]并没有显示“所有东西都是列表”,g2也没有显示“所有东西都是函数”。 - Ben
“值和函数之间没有明显的分界线”——是的,这正是我试图用例子来展示的)) “除非你想以零元函数的术语来思考,否则答案很明确是‘不’”——答案是“是”,因为 HR 使用了零元函数符号。 - Yuras
你为什么认为Haskell报告将值视为零元函数?如果你是根据“输入函数”部分中有getChar :: IO Char这一事实来判断的,那我可以指出,在几段话之后,他们明确说了“getChar操作”,但也提到了“interact函数”。 - Vitus
此外,在数学中,我们通常将 n 元函数视为 Π i∈{1, .., n} Ai → B(其中Π是笛卡尔积)。然后,零元函数的定义域为空笛卡尔积,即{()}。但是显然Haskell区分 f::() -> Bg::B(尽管它们是同构的)。 - Vitus
显示剩余2条评论

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