【问题标题】:How to implement the by-need lazy evaluation in an OOP language, so that it complies with functional programing paradigm?如何在 OOP 语言中实现按需惰性求值,使其符合函数式编程范式?
【发布时间】:2015-07-06 13:03:51
【问题描述】:

我在从 OOP 思维转换到函数式思维时遇到了一些麻烦。我当前的问题是我有一个不可变的、持久的数据结构,它用于(比如说)构建 URL-s:

class UrlBuilder {

  public UrlBuilder withHost(String domain) {
    return new UrlBuilder(/*...*/);
  }

  public UrlBuilder withPort(Int port) {
    return new UrlBuilder(/*...*/);
  }

  // ...

  public String build() {
    // ...
  }
}

懒惰地评估字符串的build()方法非常昂贵,所以我想缓存结果。

在 OOP 中这没问题,因为我可以这样做:

class UrlBuilder {
  private String url;

  // ...

  public String build() {
    if (null == this.url) {
      this.url = doExpensiveEvaluation();
    }
    return this.url;
  }
}

如果我需要线程安全,我会使用双重检查锁定并完成它。但据我了解,这违反了功能范式,因为它引入了副作用(修改对象的内部状态)。

我知道在 Scala 中有 lazy 关键字,它完全符合我的需要:实现所谓的 by-need 惰性。但是我怎样才能在 OOP 语言中做同样的事情呢?我其实很好奇他们是如何在 Scala 中实现这一点的。

我试图将缓存结果的责任倒转给我的UrlBuilder 的消费者,但这在消费者端引起了同样的问题:

class Consumer {
  private UrlBuilder urlBuilder;
  private String url;
  // ...
  public String getUrl() {
    if (null == this.url) {
      this.url = urlBuilder.build(); // same as before!
    }
    return this.url;
  }
}

因此我在标题中提出了问题。

编辑:要明确一点:我问的是除 Scala 之外的 OOP 语言的实现。它可以是 Java 或 C#,但我也想知道如何在 JavaScript 之类的东西中做到这一点。正如我所提到的,我可以只使用锁定,但我正在寻找一种无需使用锁定的纯功能解决方案。

我的印象是函数式编程是开箱即用的线程安全的,因此锁定对我来说就像一个丑陋的 OOP 解决方案。但当然,我也会接受一个证明这是不可能的答案。 Ben Reich 的The comment bellow 几乎说明了一切:如果 Scala 开发人员在没有锁定的情况下无法做到这一点,那么我可能会死去尝试。

【问题讨论】:

  • 修改无法观察到的内部状态在函数式编程中并没有错。
  • 我很困惑为什么lazy 在这里不适合你。你能解释为什么它还不够吗?您是否在问如何用另一种语言(如 Java)实现 lazy?我还要小心你的一些语言:面向对象和函数式编程并不矛盾——Scala 支持这两种范式!
  • 使用lazy时可以查看反编译的代码,更好的理解实现。在此处阅读更多信息:stackoverflow.com/a/17642466/1223622
  • @Bergi 你可能就在这里。如果只有一种方法可以通过开箱即用的线程安全来做到这一点。您能否指出一些参考资料来支持您的主张?
  • @MaciejSz:嗯,也许“如果它没有改变任何可观察到的东西,那么它就不是 side effect 的定义”?

标签: scala functional-programming lazy-evaluation


【解决方案1】:

我们说的是java,不是吗?为什么不直接同步呢?

class LazyClass 
{

    Integer someValue = null;
    public synchronized Integer someReallyExpensiveMethod() {
        if (someValue == null)
        {
            someValue = 1 + 2 + 3; // .. + 32 + .. this takes a long time
        }
        return someValue;
    }

}

【讨论】:

  • 我投了赞成票,因为这可能是唯一的解决方案,之前 Ben Reich 在this comment 中提到过。当然我会在这里使用双重检查锁定而不是仅仅同步方法。
  • 或者如果 someValue 已经生成,可能不同步访问器。 someRXM 的包装器仍然需要检查以防止竞争。
  • 不需要 volatile 或包装就足够了。一次只有一个线程可以访问此方法。只有当其他方法正在访问 someValue 时,您才会有奇怪的行为。
【解决方案2】:

这个怎么样:

object UrlBuilder{
    def empty = new InnerBuilder("")

    class InnerBuilder(...){
        def withHost(host: String) = new InnerBuilder(...)
        def withPort(port: Int) = new InnerBuilder(...)
        def build(): String = ...
    }

这样你就没有任何可变状态 }

并像这样使用它:

UrlBuilder.empty
          .withHost(...)
          .withPort(...)
          .build()

【讨论】:

  • 这并没有改变以下代码将执行两次build 并且不缓存值的事实:val url = UrlBuilder.empty.withHost(...).withPort(...); url.build(); url.build()
  • build()方法的实现是最重要的部分,你省略了。
  • 它可能只是一个惰性 val,一切都将保持不变,并且只计算一次
  • 问题实际上是如何实现lazy val。
【解决方案3】:

我找到了 Rich Hickey 对这个问题 in this article 的最佳答案。这是关于所谓的 transient 数据结构的闭包实现。它们本质上是对持久数据结构的可变副本进行操作,但对外界透明地执行此操作(在后台使用锁定)。

除了描述数据结构的工作原理之外,该文章基本上指出,只要无法观察到突变,就可以进行突变。

事实证明,这是一个哲学问题。文章中的引述很好地总结了这一点:

如果一棵树倒在树林里,它会发出声音吗?

如果纯函数改变一些本地数据以产生不可变的返回值,可以吗?

— Rich Hickey,Clojure

【讨论】:

    猜你喜欢
    • 2010-11-21
    • 1970-01-01
    • 2012-04-27
    • 2010-09-09
    • 2021-10-22
    • 2020-06-28
    • 1970-01-01
    • 2020-11-01
    • 1970-01-01
    相关资源
    最近更新 更多