23

我经常遇到以下情况:假设我有这三个功能

def firstFn: Int = ...
def secondFn(b: Int): Long = ...
def thirdFn(x: Int, y: Long, z: Long): Long = ...

我也有calculate功能。我的第一种方法可能如下所示:

def calculate(a: Long) = thirdFn(firstFn, secondFn(firstFn), secondFn(firstFn) + a)

它看起来很漂亮,没有任何花括号——只有一种表达方式。但这不是最优的,所以我最终得到了这段代码:

def calculate(a: Long) = {
  val first = firstFn
  val second = secondFn(first)
  
  thirdFn(first, second, second + a)
}

现在是用大括号括起来的几个表达式。在这样的时刻,我有点羡慕 Clojure。使用let 函数,我可以在一个表达式中定义此函数。

所以我的目标是calculate用一个表达式定义函数。我想出了2个解决方案。

1 - 使用scalaz我可以这样定义它(有没有更好的方法来使用 scalaz?):

  def calculate(a: Long) = 
    firstFn |> {first => secondFn(first) |> {second => thirdFn(first, second, second + a)}}

我不喜欢这个解决方案的是它是嵌套的。我拥有的 s越多val,这个嵌套就越深。

2 - 通过for理解,我可以实现类似的目标:

  def calculate(a: Long) = 
    for (first <- Option(firstFn); second <- Option(secondFn(first))) yield thirdFn(first, second, second + a)

一方面,这个解决方案具有扁平结构,就像let在 Clojure 中一样,但另一方面我需要将函数的结果包装起来并作为结果Option接收(这很好,我正在处理空值,但我不...... .并且不想)。Optioncalculate

有没有更好的方法来实现我的目标?处理这种情况的惯用方式是什么(也许我应该和vals 呆在一起......但是let这样做的方式看起来很优雅)?

另一方面,它与参照透明度有关。所有三个函数都是引用透明的(在我的示例中firstFn计算一些常数,如 Pi),因此理论上它们可以用计算结果替换。我知道这一点,但编译器不知道,所以它无法优化我的第一次尝试。这是我的第二个问题:

我能否以某种方式(可能带有注释)向编译器提示我的函数是引用透明的,以便它可以为我优化这个函数(例如,在那里放置某种缓存)?

编辑

感谢大家的精彩回答!选择一个最佳答案是不可能的(可能是因为它们都很好)所以我会接受投票最多的答案,我认为这很公平。

4

6 回答 6

13

在非递归的情况下, let 是 lambda 的重构。

def firstFn : Int = 42
def secondFn(b : Int) : Long = 42
def thirdFn(x : Int, y : Long, z : Long) : Long = x + y + z

def let[A, B](x : A)(f : A => B) : B = f(x)

def calculate(a: Long) = let(firstFn){first => let(secondFn(first)){second => thirdFn(first, second, second + a)}}

当然,这仍然是嵌套的。这是无法避免的。但是你说你喜欢单子形式。所以这是身份单子

case class Identity[A](x : A) {
   def map[B](f : A => B) = Identity(f(x))
   def flatMap[B](f : A => Identity[B]) = f(x)
}

这是你的一元计算。通过调用 .x 解包结果

def calculateMonad(a : Long) = for {
   first <- Identity(firstFn)
   second <- Identity(secondFn(first))
} yield thirdFn(first, second, second + a)

但在这一点上,它确实看起来像原始的 val 版本。

Identity monad 存在于 Scalaz 中,更加复杂

http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/Identity.scala.html

于 2011-02-03T01:08:25.880 回答
8

坚持原来的形式:

def calculate(a: Long) = {
  val first = firstFn
  val second = secondFn(first)

  thirdFn(first, second, second + a)
}

它简洁明了,即使对 Java 开发人员也是如此。它大致相当于 let,只是不限制名称的范围。

于 2011-02-03T18:34:55.600 回答
4

这是您可能忽略的一个选项。

def calculate(a: Long)(i: Int = firstFn)(j: Long = secondFn(i)) = thirdFn(i,j,j+a)

如果你真的想创建一个方法,我会这样做。

或者,您可以创建一个避免嵌套的方法(可以命名它let):

class Usable[A](a: A) {
  def use[B](f: A=>B) = f(a)
  def reuse[B,C](f: A=>B)(g: (A,B)=>C) = g(a,f(a))
  // Could add more
}
implicit def use_anything[A](a: A) = new Usable(a)

def calculate(a: Long) =
  firstFn.reuse(secondFn)((first, second) => thirdFn(first,second,second+a))

但是现在您可能需要多次命名相同的事物。

于 2011-02-03T01:09:13.727 回答
3

为什么不在这里使用模式匹配:

def calculate(a: Long) = firstFn match { case f => secondFn(f) match { case s => thirdFn(f,s,s + a) } }

于 2011-02-03T10:16:14.033 回答
3

如果你觉得第一种形式更干净/更优雅/更易读,那为什么不坚持下去呢?

首先,请阅读Martin Odersky 对 Scala 编译器的最新提交消息,并将其牢记在心……


也许这里真正的问题是立即跳枪声称它是次优的。JVM 非常擅长优化这类事情。有时,这简直太神奇了!

假设您在一个真正需要加速的应用程序中遇到真正的性能问题,您应该从一个分析器报告开始,证明这是在适当配置和预热的 JVM 上的一个重要瓶颈。

然后,并且只有在那时,您是否应该寻找使其更快的方法,但最终可能会牺牲代码的清晰度。

于 2011-02-03T09:16:17.073 回答
0

How about using currying to record the function return values (parameters from preceding parameter groups are available in suceeding groups).

A bit odd looking but fairly concise and no repeated invocations:

def calculate(a: Long)(f: Int = firstFn)(s: Long = secondFn(f)) = thirdFn(f, s, s + a)

println(calculate(1L)()())
于 2011-02-03T11:54:38.653 回答