我在从面向对象思维转换到函数式思维方面遇到了一些麻烦。我目前的问题是,我有一个不可变的、持久的数据结构,它被用来(比方说)构建url:
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;
}
}
因此我在标题中提出了问题。
编辑:澄清一下:我问的是OOP语言的实现,而不是Scala。它可以是Java或c#,但我也想知道如何在JavaScript之类的东西中做到这一点。正如我所提到的,我可以只使用锁,但我在寻找一个不必使用锁的纯功能解决方案。
在我的印象中,函数式编程是开箱即用的线程安全,因此锁对我来说就像一个丑陋的OOP解决方案。当然,我也会接受一个证明不可能的答案。Ben Reich的评论几乎说明了一切:如果Scala开发人员不能在没有锁的情况下做到这一点,那么我可能会死在尝试中。
我们说的是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;
}
}
这个怎么样:
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()
我在Rich Hickey的这篇文章中找到了这个问题的最佳答案。它是关于所谓的瞬态数据结构的闭包实现。它们本质上是对持久数据结构的可变副本进行操作,但是对外部世界是透明的(在后台使用锁定)。
除了描述数据结构是如何工作的,文章基本上指出,只要不能观察到突变,就可以进行突变。
事实证明这是一个哲学问题。文章中的这段话很好地总结了这一点:
如果一棵树倒在树林里,它会发出声音吗?
如果一个纯函数为了产生一个不可变的返回值而改变了一些局部数据,那是可以的吗?
- Rich Hickey, Clojure