我常常遇到以下情况:假设我有这三个功能
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 function,我可以在一个表达式中定义此函数。
所以我的目标是用一个表达式定义calculate
函数。我想出了两个解决方案。
1 - 使用 scalaz 我可以像这样定义它(使用scalaz有更好的方法吗?):
def calculate(a: Long) =
firstFn |> {first => secondFn(first) |> {second => thirdFn(first, second, second + a)}}
我不喜欢这个解决方案,它是嵌套的。 val
越多,我的嵌套就越深。
2 - 通过for
理解,我可以实现类似的目标:
def calculate(a: Long) =
for (first <- Option(firstFn); second <- Option(secondFn(first))) yield thirdFn(first, second, second + a)
一方面,这个解决方案具有扁平结构,就像Clojure中的let
一样,但另一方面,我需要将函数的结果包装在Option
中并接收Option
作为结果。 calculate
(我很好处理空值,但我不......并且不想这样做。)
有没有更好的方法来实现我的目标?处理这种情况的惯用方法是什么(可能我应该留在val
s ...但let
这样做的方式看起来很优雅?
另一方面它连接到Referential transparency。所有这三个函数都是引用透明的(在我的例子中firstFn
计算一些像Pi一样的常量),所以理论上它们可以用计算结果代替。我知道这一点,但编译器没有,所以它无法优化我的第一次尝试。这是我的第二个问题:
我可以以某种方式(可能带注释)给编译器提示,我的函数是引用透明的,以便它可以为我优化这个函数(例如在那里放一些缓存)?
感谢大家的精彩回答!选择一个最佳答案是不可能的(可能是因为它们都很好)所以我会接受最高票的答案,我认为这是公平的。
答案 0 :(得分:12)
在非递归的情况下,让我们重新组织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)}}
当然,那仍然是嵌套的。无法避免。但是你说你喜欢monadic形式。所以这是身份monad
case class Identity[A](x : A) {
def map[B](f : A => B) = Identity(f(x))
def flatMap[B](f : A => Identity[B]) = f(x)
}
这是你的monadic计算。通过调用.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
答案 1 :(得分:7)
坚持原始形式:
def calculate(a: Long) = {
val first = firstFn
val second = secondFn(first)
thirdFn(first, second, second + a)
}
它简洁明了,甚至对Java开发人员也是如此。它大致相当于let,只是不限制名称的范围。
答案 2 :(得分: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))
但现在你可能需要多次命名相同的东西。
答案 3 :(得分:3)
如果您觉得第一种形式更干净/更优雅/更易读,那么为什么不坚持下去呢?
首先,阅读来自Martin Odersky的Scala编译器的this recent commit message并将其铭记于心......
也许这里真正的问题是立即跳起来声称它是次优的。 JVM在优化这类事情方面非常热门。有时候,这简直太神奇了!
假设您在真正需要加速的应用程序中存在真正的性能问题,您应该从分析器报告证明开始,这是一个重要的瓶颈,在适当配置和加热JVM。
然后,只有这样,你是否应该考虑如何让它更快,最终可能会牺牲代码清晰度。
答案 4 :(得分:3)
为什么不在这里使用模式匹配:
def calculate(a:Long)= firstFn match {case f =&gt; secondFn(f)匹配{case s =&gt; thirdFn(f,s,s + a)}}
答案 5 :(得分:0)
如何使用currying来记录函数返回值(前面的参数组中的参数在上级组中可用)。
有点奇怪,但相当简洁,没有重复的调用:
def calculate(a: Long)(f: Int = firstFn)(s: Long = secondFn(f)) = thirdFn(f, s, s + a)
println(calculate(1L)()())