8

我花了一个小时阅读这个网站上的许多帖子以及其他关于 lambda 和闭包的帖子。我想我理解它们是什么,即它们是如何工作的,但我不明白它们为什么存在。我看到的许多例子都含糊地提到了它们的“力量”,但在每一种情况下,我都能想到一种更简单的方法来完成所说明的事情。也许这是由于示例故意过于简单(为了便于理解),或者我可能很密集。但我真正想要的是看到一个清晰的例子,说明你可以用闭包或 lambda 来完成你做不到的事情无一完成。也许这是一个很复杂的问题,因为所有编程范式最终都归结为相同的机器指令,并且可以用一种语言完成的任何事情都可以以某种方式用另一种语言完成。所以我想我真正要问的是一个用闭包做的事情的例子,它比没有做的更优雅(我见过的任何例子似乎都不是这种情况)。

这就是我要说的。

什么是“关闭”?,Scheme中的一个例子:

(define (make-counter)
  (let ((count 0))
    (lambda ()
      (set! count (+ count 1))
      count)))

(define x (make-counter))

(x) returns 1

(x) returns 2

...etc...

我真的不知道Scheme,但我想我知道发生了什么。但是,这不是更直接并完成同样的事情吗?伪代码:

function incrementCount(var counter) {
    counter++;
    return counter;
}

counter = 1;
counter = incrementCount(counter);
counter = incrementCount(counter);

这是关于 lambdas 的另一个:什么是 lambda(函数)?

给出的 JavaScript 示例:

var adder = function (x) {
    return function (y) {
        return x + y;
    };
};
add5 = adder(5);
add5(1) == 6

再说一遍,为什么要那样做?为什么不直接说:

function adder(x, y) {
    return x + y;
}
adder(5, 1);

我看到的每个示例似乎都是一种过于复杂的方法,可以用基本功能轻松完成。这些例子当然不能说明我能看到的任何惊人的酷“力量”。所以请有人启发我,我一定错过了一些东西。谢谢。

4

4 回答 4

5

Lambda 演算实际上非常简单,并且本身并没有那么有用。然而,当您开始学习 lambda 演算的含义和使用函数式设计模式的应用程序控制策略时,您将成为一个更好、更强大的程序员。

前提是函数也是值,这使得它对于抽象许多概念非常强大。您的两个示例显示了实现闭包和柯里化的最简单方法。这些都是微不足道的例子。当您开始使用函数式编程时,这些模式将一遍又一遍地出现,作为抽象出复杂性的非常强大的方法。

关注点分离

函数式编程在您开始使用高阶函数进行抽象时很有用,典型的例子是 - 我想对对象集合做一些事情。

因此,在命令式程序中,您使用 for 循环:

result = Array(length);
for(i = 0; i < length; i++)
{
   result[i] = dosomething(input[i]);
}

请注意,在代码块中,dosomething函数位于 for 循环的中间。您无法真正将您对数组的每个元素所做的操作与实际的 for 循环控制结构分开。

然而,通过使用函数式风格,循环的控制结构被抽象出来,使用map更高阶的函数,你可以清楚地分离循环和函数应用这两个概念。这是方案中的等效代码。

(define result (map dosomething collection))

map照顾您正在遍历数组的每个元素的事实。dosomething照顾您正在对数组的每个元素执行操作的事实。对此的推理变得更加清晰,更改代码变得更加容易。现在想象一下,for你的代码中的每个循环都被这个结构替换了,它将节省多少行代码。

廉价的构造函数

解决闭包和柯里化的概念。对象和函数之间存在等价关系。本质上,可以使函数看起来和表现得像对象。Javascript 充分利用了这一事实。你可以用对象做的任何事情,你也可以用函数做。事实上,通过摆脱对象是什么和函数是什么之间的区别,你已经有效地整理了你的思想,并且有了一种思考问题的方式——即通过通过功能构建的代码。

我在这里使用 clojure 代码,因为这是我喜欢的:

这是普通加法器示例的 clojure 代码,在加法器示例中,您正在调用addn带有数字 5 的 a 以获取将 5 添加到数字的函数。一般用例可能是:

(defn addn [n] (fn [m] (+ m n))
(def add5 (addn 5))

(map add5 [1 2 3 4 5 6])
;; => [6 7 8 9 10 11]

假设你有一个函数download-and-save-to,它接受一个 url 和一个数据库,将 url 保存到数据库表并在成功时返回 true,你可以做与你做的完全相同的抽象+

(defn download-url-to [db]
  (fn [url] (download-and-save-to url db)))

(def download-url-mydb (download-url-to mydb))
(map download-url-mydb [url1 url2 url3 url4 url5])
;; => [true true false true true]

请注意,即使您正在做截然不同的事情,该结构也是等效的。想想你将如何使用 java 或 c++ 来解决这个问题。类定义、工厂方法、类抽象、继承……本质上会涉及更多代码。你必须经历更多的仪式才能获得同样的效果。

敞开心扉

由于您可以如此简洁地表达想法,函数式编程可以让您掌握许多很难用更冗长的语言表达的概念。您选择的工具将使探索某些问题变得更容易或更难。

我强烈推荐 clojure - (http://www.4clojure.com/) 和 lighttable - (http://www.lighttable.com/, http://www.kickstarter.com/projects/ibdknox/light-table )开始。从我个人的经验来看,我在 clojure 社区学习和探索概念的一年时间类似于学习 java、c++ 和 python 结合的大约 10 年。


哦,我有没有提到我学习所有这些东西有多么有趣?Monads、combinators、propagators、continuations……当你有正确的工具来理解它们时,所有那些看起来可怕的学术概念实际上都触手可及。

于 2012-12-02T11:28:31.253 回答
4

这个问题很难回答,因为你已经涵盖了所有的基础:你已经看过示例、描述、解释——你甚至意识到 lambda 表达式编译成一种机器语言,它本身没有 lambda 表达式一个主要特征。

尽管如此,我将尝试给出一个示例,该示例似乎是我个人使用 lambda 表达式的最常见用例之一。我使用 C#,它本质上是一种命令式语言,但具有 lambda 表达式作为内置功能,因此我的示例将使用 C#。

想象一下,您有一个需要值的函数。但是这个值计算起来很昂贵,所以要求是:

  • 如果函数不需要该值,则根本不对其进行评估。
  • 如果函数多次需要该值,则只应评估一次。

Lambda 表达式使这变得非常容易,因为您可以使用捕获的变量来“缓存”计算值:

bool computed = false;
MyType value = null;

var getValue = () => {
    if (!computed)
    {
        value = computeValue();    // this is the expensive operation
        computed = true;
    }
    return value;
};

现在我可以传入getValue任何函数,无论这个函数对它做什么(即它调用它多少次),我知道它computeValue()只会被调用一次。

要在没有 lambda 的情况下做同样的事情,我必须创建一个等效于这个 lambda闭包的类,并让它实现某种接口,例如:

sealed class Closure : IGetMyTypeValue
{
    bool Computed = false;
    MyType Value = null;

    public Closure() { }

    public MyType GetValue()   // implements IGetMyTypeValue.GetValue()
    {
        if (!Computed)
        {
            Value = ComputeValue();    // this is the expensive operation
            Computed = true;
        }
        return Value;
    }

    private MyType ComputeValue()
    {
        // expensive code goes here
    }
}

[...]

var closure = new Closure();
// pass closure to a method that expects an IGetMyTypeValue

必须编写这个大类的缺点是:

  • 你必须为每一个如此昂贵的计算编写一个新类。这是非常重复的工作;使用 lambda 表达式允许编译器自动执行该操作。
  • 您必须为每种类型的操作编写一个单独的接口。
于 2012-12-02T10:39:14.733 回答
2

我不知道你是否熟悉 JavaScript,但闭包和 lambda 是模拟面向对象行为的基本功能。

考虑以下代码:

var MyClass = function(buttonId) {
  var clickCounter = 0;

  var myButton = document.getElementById(buttonId);
  myButton.addEventListener(
    'onclick',
    function() {
      clickCounter += 1;
  });
};

var myButtonClickCounter = new MyClass();

在 JavaScript 中没有诸如私有成员之类的东西,但我们可以创建类似它们的结构。以下是解释了它在做什么的代码:

// I define a class constructor that takes a string as an input parameter
var MyClass = function(buttonId) {
  // I define a private member called click
  var clickCounter = 0;

  // I get the reference to the button with the given id.
  var myButton = document.getElementById(buttonId);
  // I attach to it a function that will be triggered when you click on it.
  // The addEventListener method takes two arguments as input:
  //  - the name of the event
  //  - the function to be called when the event is triggered
  myButton.addEventListener(
    'onclick',
    function() {
      clickCounter += 1;
  });
};

// I create an instance of MyClass for a button called myButton
var myButtonClickCounter = new MyClass('myButton');

在这种情况下,传递给该addEventListener方法的函数是一个lambda 函数。是的,你总是可以用其他方式定义它,但这只是实现细节。在这种情况下,我可以在定义中创建另一个变量MyClass而不是 lambda 函数,如下所示:

var MyClass = function(buttonId) {
  var clickCounter = 0;

  var clickHandler = function() {
      clickCounter += 1;
  };

  var myButton = document.getElementById(buttonId);
  myButton.addEventListener(
    'onclick',
    clickHandler
  );
};

关于 JavaScript,我能告诉你的clickCounter是在那个 lambda 函数的闭包范围内,因为它是在其链 ( MyClass) 上方的函数中定义的,但clickHandler也会出现在该范围内,而不仅仅是clickCounter.

假设这个类需要为 4 个不同的按钮创建一个监听器,您需要为每个按钮创建一个处理程序,并且每次触发一个时,所有变量都将出现在其闭包范围内。并且拥有 8 个变量引用(4 个处理程序 + 4 个计数器)比只有 4 个计数器更糟糕。

拥有这个闭包的另一个优点是它clickCounter是 MyClass 的私有成员,因为它在其主体中定义为变量,因此从外部看不到。相反,clickHandler(即使是它的 lambda 函数形式)是在内部 MyClass定义的,因此它将能够通过闭包来引用它。

编辑: 为了以clickCounter某种方式有用,假设您需要一个公共方法来告诉您计数器是否已达到某个值。我们可以这样写(在定义的底部):

var MyClass = function(buttonId) {
  var clickCounter = 0;

  var myButton = document.getElementById(buttonId);
  myButton.addEventListener(
    'onclick',
    function() {
      clickCounter += 1;
  });

  // This method takes a value x as input, and returns
  // a boolean value, that tells you whether the button
  // has been clicked more than the given x.
  this.hasReached = function(x) {
    return (clickCounter >= x);
  };
};

// Somewhere else in your code...
var myButtonClickCounter = new MyClass('myButton');
...
if( myButtonClickCounter.hasReached(4) )
{
  // Do some great stuff
}

您还可以拥有简单地返回或更改私有变量值的经典 getter 和 setter,这取决于您的需要。在这个特定的实现中,我不想直接公开私有变量(强烈建议这样做,因为如果类是唯一知道如何操作其成员的类,则代码更易于维护)。

注意:请记住,这只是一个展示闭包用法的​​示例,在 JavaScript 中定义类还有更有效的方法!

于 2012-12-02T11:30:08.230 回答
0

其他一些已发布的答案引用了使用闭包创建私有变量,但我认为 John Resig 的高级 JavaScript 教程中的这个示例使其更加清晰。也许其他人会从中受益。

资料来源:http ://ejohn.org/apps/learn/#54

function Ninja(){
  var slices = 0;

  this.getSlices = function(){
    return slices;
  };
  this.slice = function(){
    slices++;
  };
}

var ninja = new Ninja();
ninja.slice();

console.log(ninja.getSlices()); // 1!
console.log(slices); // undefined!
于 2013-03-29T01:37:02.877 回答