23

Often I face following situation: suppose I have these three functions

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

and I also have calculate function. My first approach can look like this:

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

It looks beautiful and without any curly brackets - just one expression. But it's not optimal, so I end up with this code:

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

Now it's several expressions surrounded with curly brackets. At such moments I envy Clojure a little bit. With let function I can define this function in one expression.

So my goal here is to define calculate function with one expression. I come up with 2 solutions.

1 - With scalaz I can define it like this (are there better ways to do this with scalaz?):

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

What I don't like about this solution is that it's nested. The more vals I have the deeper this nesting is.

2 - With for comprehension I can achieve something similar:

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

From one hand this solution has flat structure, just like let in Clojure, but from the other hand I need to wrap functions' results in Option and receive Option as result from calculate (it's good it I'm dealing with nulls, but I don't... and don't want to).

Are there better ways to achieve my goal? What is the idiomatic way for dealing with such situations (may be I should stay with vals... but let way of doing it looks so elegant)?

From other hand it's connected to Referential transparency. All three functions are referentially transparent (in my example firstFn calculates some constant like Pi), so theoretically they can be replaced with calculation results. I know this, but compiler does not, so it can't optimize my first attempt. And here is my second question:

Can I somehow (may be with annotation) give hint to compiler, that my function is referentially transparent, so that it can optimize this function for me (put some kind of caching there, for example)?

Edit

Thanks everybody for the great answers! It's just impossible to select one best answer (may be because they all so good) so I will accept answer with the most up-votes, I think it's fair enough.

Community
  • 1
  • 1
tenshi
  • 26,268
  • 8
  • 76
  • 90
  • 2
    Nice question. I would say -- although this depends on who's definition ;-) -- "idiomatic" in Scala is with the "explicit curlies" and nesting in most cases. I would be hard pressed to argue Scala syntax is particularly elegant in terms of functional language features. –  Feb 03 '11 at 01:02
  • I guess I'm not sure how your calculate function is different to / worse than Clojure's let: (defn calculate [^long a] (let [first (first-fn) second (second-fn first)] (third-fn first second (+ second a)))) – Sean Corfield Feb 03 '11 at 01:36
  • 1
    You could add a `get` to the end of the for comprehension, but all you are doing is replacing parenthesis with curly brackets. On the other hand, the difference between clojure's let and scala is parenthesis and brackets vs curly brackets. – Daniel C. Sobral Feb 03 '11 at 13:55

6 Answers6

13

in the non-recursive case, let is a restructuring of 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)}}

Of course, that's still nested. Can't avoid that. But you said you like the monadic form. So here's the identity 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)
}

And here's your monadic calculate. Unwrap the result by calling .x

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

But at this point it sure looks like the original val version.

The Identity monad exists in Scalaz with more sophistication

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

James Iry
  • 19,367
  • 3
  • 64
  • 56
  • It's worth noting that implicit conversions to (and from) identity work in this context also, so you don't need to explicitly wrap things in `Identity`. – Rex Kerr Feb 03 '11 at 01:17
  • The for comprehension seems badly formatted. I'm gonna "fix" it, but if it turns out it was the way you wanted it, please accept my excuse in advance. – Daniel C. Sobral Feb 03 '11 at 13:47
  • @James: Thanks, I like solution with `Identity`. Looks more consistent than `Option` for me. – tenshi Feb 03 '11 at 18:19
  • @Rex: I imported `import scalaz._; import Scalaz._` and tried this: `for (f <- firstFn; s <- secondFn(f)) yield thirdFn(f, s, s + a)`, but compiler complains about `firstFn`: value flatMap is not a member of Int – tenshi Feb 03 '11 at 18:22
  • @Easy Angel - You do need to write your own implicit conversion; I didn't mean to imply that the conversion was automatically present in Scalaz. – Rex Kerr Feb 03 '11 at 20:17
  • @Rex Kerr: Oh... sorry, now I understand... I wrote one for James' example and it works... scalaz' `Identity` has needed implicit conversion in `Identitys` trait, but the problem is that `Identity` itself has no `flatMap` method :) Thanks – tenshi Feb 03 '11 at 20:33
8

Stick with the original form:

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

  thirdFn(first, second, second + a)
}

It's concise and clear, even to Java developers. It's roughly equivalent to let, just without limiting the scope of the names.

Craig P. Motlin
  • 26,452
  • 17
  • 99
  • 126
  • 1
    In fact, in Scala. blocks **are** expressions, whose value is the value of the last subexpression therein. So there is really not much difference – Andrea Nov 16 '16 at 18:26
4

Here's an option you may have overlooked.

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

If you actually want to create a method, this is the way I'd do it.

Alternatively, you could create a method (one might name it let) that avoids nesting:

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))

But now you might need to name the same things multiple times.

Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
  • With your first solution invocation will look like this: `calculate(5)()()`... looks extraordinary at first sight :) I think it's better suted for private methods, because user can actually use all 3 arguments and break my beautiful algorithm :). I like your second idea... it actually inspired for something similar - with some implicit magic and some helper classes I can write something like this: `firstFn and secondFn in ((f, s) => thirdFn(f, s, s + a))`. – tenshi Feb 03 '11 at 18:37
3

If you feel the first form is cleaner/more elegant/more readable, then why not just stick with it?

First, read this recent commit message to the Scala compiler from none other than Martin Odersky and take it to heart...


Perhaps the real issue here is instantly jumping the gun on claiming it's sub-optimal. The JVM is pretty hot at optimising this sort of thing. At times, it's just plain amazing!

Assuming you have a genuine performance issue in an application that's in genuine need of a speed up, you should start with a profiler report proving that this is a significant bottleneck, on a suitably configured and warmed up JVM.

Then, and only then, should you look at ways to make it faster that may end up sacrificing code clarity.

Kevin Wright
  • 49,540
  • 9
  • 105
  • 155
  • 1
    If the functions could possibly have side effects then it _isn't even equivalent_ to the case where the functions aren't repeated. I think you vastly underestimate how difficult it is to prove (even for the JVM) that the case with vars and the case without are the same unless the code is already doing essentially no work, in which case you're unlikely to be concerned about it being too slow. – Rex Kerr Feb 03 '11 at 14:07
  • @rex - absolutely, 100%, but the question was specifically about optimization. There was nothing to suggest that either performance or side effects had actually been observed as problems here. – Kevin Wright Feb 03 '11 at 18:34
  • 1
    @Kevin: great commit message :) You inspired me to make my "mini benchmarks" (I know... this itself can be considered source of evil :). So results: my first approach (not optimized) - 84ms, optimized version - 65ms, for comprehension with options - 233ms, scalaz version with `|>` - 311ms, `and` ... `in` version, that I wrote in reply to Rex Kerr - 160 ms, and finally Rex Kerr's first solution with 3 arguments - 77ms. – tenshi Feb 03 '11 at 19:22
  • @Easy - problem solved then. The way to go is obvious if performance is your main concern! And it's not as though *any* of the possibilities were especially hard to read... Chalk up another victory for evidence based optimization! – Kevin Wright Feb 03 '11 at 21:47
  • @Kevin: I don't think, that I will ever use `calculate` like this: `(1 to 1000000) foreach (calculate)` :) I think the most time would be spent in `firstFn`, `secondFn` and `thirdFn`, but it's not really important how much time this wiring will take. These "mini benchmarks" were just for fun and out of curiosity :) – tenshi Feb 03 '11 at 21:56
  • 2
    Optimizations are like orgasms, pursued too vigorously by some, often exactly what you need, and never good when premature. It's not the idea of optimization that concerned me here, just the apparent prematurity. – Kevin Wright Feb 04 '11 at 13:09
3

Why not use pattern matching here:

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

Silvio Bierman
  • 703
  • 5
  • 9
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)()())
Don Mackenzie
  • 7,953
  • 7
  • 31
  • 32