Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure's 'let' equivalent in Scala

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.

like image 893
tenshi Avatar asked Feb 03 '11 00:02

tenshi


4 Answers

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

like image 196
James Iry Avatar answered Oct 02 '22 15:10

James Iry


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.

like image 38
Craig P. Motlin Avatar answered Oct 02 '22 15:10

Craig P. Motlin


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.

like image 31
Rex Kerr Avatar answered Oct 02 '22 13:10

Rex Kerr


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.

like image 20
Kevin Wright Avatar answered Oct 02 '22 15:10

Kevin Wright