函数式编程语言如何工作?

时间:2010-05-01 19:30:51

标签: oop programming-languages haskell functional-programming paradigms

如果函数式编程语言无法保存任何状态,那么它们如何做一些简单的事情,比如从用户那里读取输入?他们如何“存储”输入(或存储任何数据?)

例如:这个简单的C事件将如何转换为像Haskell这样的函数式编程语言?

#include<stdio.h>
int main() {
    int no;
    scanf("%d",&no);
    return 0;
}

(我的问题受到了这篇优秀文章的启发:"Execution in the Kingdom of Nouns"。阅读它让我更好地理解了什么是面向对象的编程,Java如何以一种极端的方式实现它,以及函数编程如何语言是一种对比。)

9 个答案:

答案 0 :(得分:78)

答案 1 :(得分:23)

这里有很多好的答案,但它们很长。我将尝试提供一个有用的简短回答:

  • 函数式语言将状态放在C所做的相同位置:命名变量和堆上分配的对象。不同之处在于:

    • 在函数式语言中,“变量”在进入范围(通过函数调用或let-binding)时获取其初始值,并且该值在之后不会更改 。类似地,在堆上分配的对象立即使用其所有字段的值进行初始化,之后不会更改。

    • “状态的变化”不是通过改变现有变量或对象而是通过绑定新变量或分配新对象来处理的。

  • IO通过技巧工作。产生字符串的副作用计算由一个以World作为参数的函数描述,并返回包含字符串和新World的对。世界包括所有磁盘驱动器的内容,发送或接收的每个网络数据包的历史记录,屏幕上每个像素的颜色以及类似的东西。这个诀窍的关键是严格限制对世界的访问,以便

    • 没有程序可以复制世界(你会把它放在哪里?)

    • 没有程序可以扔掉世界

    使用这个技巧可以创造一个独特的世界,其状态随着时间的推移而演变。语言运行时系统用函数式语言编写,通过更新唯一的World而不是返回一个新的来实现副作用计算。

    Simon Peyton Jones和Phil Wadler在他们的标志性文章"Imperative Functional Programming"中精彩地解释了这个技巧。

答案 2 :(得分:19)

我正在中断对新答案的评论回复,以提供更多空间:

我写道:

  

据我所知,这个IO故事(World -> (a,World))在应用于Haskell时是一个神话,因为该模型仅解释纯顺序计算,而Haskell的IO类型包括并发性。通过“纯粹顺序”,我的意思是除了由于计算之外,甚至不允许世界(宇宙)在命令式计算的开始和结束之间改变。例如,当您的计算机正在消失时,您的大脑等不能。并发可以通过更像World -> PowerSet [(a,World)]的东西来处理,这允许非确定性和交错。

Norman写道:

  

@Conal:我认为IO故事很好地概括了非确定性和交错;如果我没记错的话,在“尴尬的小队”论文中有一个很好的解释。但我不知道一篇好文章能够清楚地解释真正的并行性。

@Norman:概括在什么意义上?我建议通常给出的指称模型/解释World -> (a,World)与Haskell IO不匹配,因为它没有考虑非确定性和并发性。可能有一个更复杂的模型适合,例如World -> PowerSet [(a,World)],但我不知道这样的模型是否已经制定出来并显示出足够的&amp;是一致的。鉴于IO由数千个FFI导入的命令式API调用填充,我个人怀疑可以找到这样的野兽。因此,IO正在实现其目的:

  

开放问题:IO monad已成为Haskell的罪魁祸首。 (每当我们不理解某些东西时,我们就把它扔进IO monad。)

(来自Simon PJ的POPL演讲 Wearing the hair shirt Wearing the hair shirt: a retrospective on Haskell 。)

Tackling the Awkward Squad 的3.1节中,Simon指出了对type IO a = World -> (a, World)无效的内容,包括“当我们添加并发时,这种方法不能很好地扩展”。然后他提出了一个可能的替代模型,然后放弃了对指称性解释的尝试,说

  

然而,我们将采用基于过程演算语义的标准方法的操作语义。

未能找到精确的&amp;有用的指称模型是我为什么看到Haskell IO偏离精神和我们称之为“函数式编程”的深层利益,或者Peter Landin更具体地命名为“外延编程”的根本原因。 See comments here.

答案 3 :(得分:17)

功能编程源自lambda Calculus。如果您真的想了解功能编程,请查看http://worrydream.com/AlligatorEggs/

学习lambda微积分并让您进入令人兴奋的功能编程世界是一种“有趣”的方式!

了解Lambda微积分如何有助于函数式编程。

因此,Lambda Calculus是许多真实编程语言的基础,如Lisp,Scheme,ML,Haskell,....

假设我们想要描述一个为任何输入添加三个的函数,我们会写:

plus3 x = succ(succ(succ x)) 

读“plus3是一个函数,当应用于任何数字x时,产生x的后继者的继承者”

请注意,任何数字加3的函数都不能命名为plus3;名称“plus3”只是命名此功能的简便方法

plus3 x) (succ 0) ≡ ((λ x. (succ (succ (succ x)))) (succ 0))

注意我们使用lambda符号作为一个函数(我认为它看起来有点像鳄鱼我猜这是鳄鱼蛋的想法来自哪里)

lambda符号是 Alligator (函数),x是它的颜色。您还可以将x视为一个参数(Lambda微积分函数实际上只假设有一个参数)其余的您可以将其视为函数体。

现在考虑抽象:

g ≡ λ f. (f (f (succ 0)))

参数f用于函数位置(在调用中)。 我们将g称为高阶函数,因为它需要另一个函数作为输入。 您可以将其他函数调用f视为“ eggs ”。 现在我们已经创建了两个函数或“ Alligators ”,我们可以这样做:

(g plus3) = (λ f. (f (f (succ 0)))(λ x . (succ (succ (succ x)))) 
= ((λ x. (succ (succ (succ x)))((λ x. (succ (succ (succ x)))) (succ 0)))
 = ((λ x. (succ (succ (succ x)))) (succ (succ (succ (succ 0)))))
 = (succ (succ (succ (succ (succ (succ (succ 0)))))))

如果你注意到你可以看到我们的λf鳄鱼吃了我们的λx鳄鱼然后是λx鳄鱼并且死了。然后我们的λx鳄鱼在λf的鳄鱼蛋中重生。然后重复该过程,左边的λx鳄鱼现在吃右边的另一个λx鳄鱼。

然后你可以使用“ Alligators ”的这一套简单的规则来设计语法,从而设计语法,从而诞生了函数式编程语言!

因此,您可以看到您是否了解Lambda Calculus,您将了解Functional Languages的工作原理。

答案 4 :(得分:14)

在Haskell中处理状态的技术非常简单。而且你不需要了解monad来处理它。

在具有状态的编程语言中,您通常会在某处存储某些值,执行某些代码,然后存储新值。在命令式语言中,这种状态只是“在后台”的某个地方。在(纯)函数语言中,您可以明确地使用它,因此您可以显式编写转换状态的函数。

因此,不要使用某种类型的X,而是编写将X映射到X的函数。就是这样!您从考虑状态转向考虑要对状态执行哪些操作。然后,您可以将这些功能链接在一起,并以各种方式将它们组合在一起以制作整个程序当然,您不仅限于将X映射到X.您可以编写函数以将各种数据组合作为输入,并在最后返回各种组合。

莫纳德是帮助组织这项工作的众多工具之一。但monad实际上并不是问题的解决方案。解决方案是考虑状态转换而不是状态。

这也适用于I / O.实际上会发生这样的事情:不是通过直接等效的scanf来获取用户的输入,而是将其存储在某个地方,而是编写一个函数来说明你对{{1}的结果做了什么。如果你有它,然后将该函数传递给I / O API。当你在Haskell中使用scanf monad时,这正是>>=所做的。因此,您永远不需要在任何地方存储任何I / O的结果 - 您只需要编写说明您希望如何转换它的代码。

答案 5 :(得分:8)

(某些功能语言允许不纯的功能。)

对于纯函数语言,真实世界的交互通常作为函数参数之一包含在内,如下所示:

RealWorld pureScanf(RealWorld world, const char* format, ...);

不同的语言有不同的策略来抽象世界,远离程序员。例如,Haskell使用monads来隐藏world参数。


但是功能语言本身的纯粹部分已经是图灵完整的,这意味着在C中可行的任何事情在Haskell中也是可行的。命令式语言的主要区别在于不是修改状态:

int compute_sum_of_squares (int min, int max) {
  int result = 0;
  for (int i = min; i < max; ++ i)
     result += i * i;  // modify "result" in place
  return result;
}

将修改部分合并到函数调用中,通常将循环转换为递归:

int compute_sum_of_squares (int min, int max) {
  if (min >= max)
    return 0;
  else
    return min * min + compute_sum_of_squares(min + 1, max);
}

答案 6 :(得分:3)

功能语言可以保存状态!他们通常只是鼓励或强迫你明确这样做。

例如,查看Haskell的State Monad

答案 7 :(得分:3)

答案 8 :(得分:1)

Haskell中:

main = do no <- readLn
          print (no + 1)

您当然可以将功能语言中的变量分配给变量。你无法改变它们(因此基本上所有变量都是函数式语言中的常量)。