【问题标题】:Example that requires a closure or lambda需要闭包或 lambda 的示例
【发布时间】:2012-11-20 00:54:00
【问题描述】:

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

这就是我要说的。

What is a 'Closure'? 的主要答案,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 的另一个:What is a lambda (function)?

给出的 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);

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

【问题讨论】:

  • 这里有一些来自 python POV 的好东西。 pythonconquerstheuniverse.wordpress.com/2011/08/29/…
  • lambda 非常强大,因为它们可以出现在函数不能出现的表达式中。这允许您在不实际或不可能的情况下使用“内联”函数。例如,在 Python 中,这意味着对于任何排序或迭代函数,key 参数可以是一个函数。
  • 闭包允许你隐式捕获状态。一个例子是回调模式:在 C 中,当您注册回调时,回调通常采用包含显式状态的参数。使用闭包,您只需将一个字段设置为闭包,然后在闭包中访问对象的状态。
  • 快速获得柯里化真正威力的最佳示例是解析组合子。看看 Parsec 或任何其他类似的实现。
  • incrementCount(counter); incrementCount(counter); 都将评估为 2。不是吗?

标签: lambda closures computer-science terminology


【解决方案1】:

其他一些已发布的答案引用了使用闭包创建私有变量,但我认为 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!

【讨论】:

    【解决方案2】:

    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 代码,在加法器示例中,您正在调用带有数字 5 的 addn 以获取将 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 社区学习和探索概念的一年,相当于学习了大约 10 年的 java、c++ 和 python 组合。


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

    【讨论】:

    • 我不同意 - lambda 演算本身就非常有用,因为它是一个非常简单的模型,可以用来表示任意复杂的语义。您可以嵌入到您的类型系统中,您可以在将代码转换为纯 lambda 后对其进行推理,等等。
    • 同意 =) 虽然我不是计算机,但我更难理解严格由 lambda 构建的程序
    • 您不必读/写原始 lambda。您可以构建更高级别的抽象——使用宏来添加一些语法糖:例如,Church 对顶部的 cons 单元格、Church 数字顶部的十进制算术、模式匹配、cond 而不是 lambda 布尔函数等。从一个琐碎的、懒惰的 lambda 演算开始,获得一种人类可读的、漂亮的、高级的语言是非常容易的。几百行代码,你就会拥有一种比 Java 更强大的语言。
    • @zcaudate 感谢您的回答,其中有一些非常深刻的思想开放观点。 :-) 我开始看到光明...
    • @The111 很高兴为您服务 =)
    【解决方案3】:

    我不知道你是否熟悉 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 函数的 closure 范围内,因为它是在其链上方的函数中定义的 (MyClass),但 clickHandler也将出现在该范围内,而不仅仅是clickCounter

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

    这个闭包的另一个优点是clickCounter 是MyClass 的一个私有成员,因为它在它的主体中被定义为一个变量,所以它从外部是不可见的。相反,clickHandler(即使是它的 lambda 函数形式)是在 inside MyClass 中定义的,因此它可以通过 closure 引用它。 p>

    编辑: 为了使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 中定义类还有更有效的方法!

    【讨论】:

    • 谢谢,我非常喜欢这个例子,而且我对 JS 很熟悉,所以它很容易准备好,并且与我之前做过的一些事情相差不远。我了解闭包使clickCounter 的值保持活动状态,并在每次单击时递增。但是在累积了一些点击之后,我们会使用什么语法来访问(读取)这个值?
    • 我编辑了答案并添加了一个如何使用它的示例,希望对您有所帮助!
    【解决方案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 的 closure 的类,并让它实现某种接口,例如:

    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 表达式可以让编译器自动完成。
    • 您必须为每种类型的操作编写单独的接口。

    【讨论】:

    • 你不需要一个 lambda 来让 computeValue() 只被调用一次。您的示例主要依赖于 getValue 作为闭包(绑定到 computed 变量)——例如,您可以在 Python 中执行此操作,而无需使用 lambda。
    • @EOL:我对 Python 的了解还不够,无法回复这个问题。如果您可以在没有 lambda 的情况下向我展示 Python 中的等效代码,我可以对其发表评论。你想把它粘贴到某个地方然后链接到它吗?
    • @EOL:当然。闭包的好处是它们以统一的方式解决了一些不同的问题,例如:Timwi 的示例将在 C 中使用 static 变量来解决,而回调问题(从回调中访问相关状态)在 C 中通过传递明确说明。但是使用闭包,它们都可以使用一个常见的习语来解决:闭包。
    • @Timwi:这是您的代码的 Python 版本:pastebin.com/ycdsdxpV。它使用简单的常规函数​​定义,而不是 lambda。 (我在这里将 lambda 表示为“匿名函数”,这可能不是您的本意。:))
    • @EOL:您的代码只使用了全局变量。这不是一个闭包,并且具有非常不同的含义。您不能在代码中创建任意数量的这些闭包;你被你定义的有限的全局变量所困。
    猜你喜欢
    • 2022-01-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多