Escaping from search

I do not see how all that support early returns, IMHO it discourage them even more.

Suppose this contrived example:

/** Returns the half of the sum of the product of each tuple plus one; as long as all values are positive, -1 otherwise. */
def process(input: List[(Int, Int)])): Unit = {
  def halfSumTupleProducts(data: List[(Int, Int)]): Int = {
    val sumProducts = data.map { case (x, y) =>
      val temp = if (x > 0 && y > 0) x * y else return -1
      temp + 1
    }.sum
    sumProducts / 2
  }

  val result = halfSumTupleProducts(data = input)
  println(result)
}

Given your definition of single exit just meaning a function should only return to one place: “he statement immediately following the call”, then where should that return return to?
Which is the next statement?

  1. Should it return after the if / else and continue with the addition?
  2. Should it return from lambda function inside the map and continue the map processing?
  3. Should it exit the map and continue with the sum?
  4. Should it exit the whole expression and continue with the division?
  5. Should it exit the whole inner function and continue with the println?
  6. Should it exit the whole parent function and continue what its caller was doing even if that function shouldn’t return anything?
  7. Should it even exit the outer scope where this was called?
  8. Should it exit this whole call stack until the current thread started?
  9. Should it exit the whole program?

I known I just started to be ridiculous on purpose but it shows why early returns could be ambiguous in their definition. However, actually, you can think of an exception as an early return and it does exactly that, it can even exit the whole program.

So, why would we really bother about all that when in the 99% of the cases, we can just refactor the code to something like this:

def halfSumTupleProducts(data: List[(Int, Int)]): Int = {
  @annotation.tailrec
  def loop(remaining: List[(Int, Int)], acc: Int): Int =
    remaining match {
      case (x, y) :: tail =>
        if (x <= 0 || y <= 0) -1 // Early return.
        else loop(remaining = tail, acc + (x * y) + 1)

      case Nil =>
        acc // Normal return.
    }

  loop(remaining = data, acc = 0)
}

Clear, simple, concise, readable, understandable.
And it does have many exit points, just that all of them are the last expression. (of their corresponding scope).

and what about the 1% that is not “refactorable” to something like this. Well, there are a couple of alternatives, but I guess using your own exception for controlling the flow and breaking early could be good enough and doesn’t require extra machinery from the language.

I assume that a “return” statement applies to the innermost enclosing def.

Here’s an idea. Perhaps the “return” keyword should not be allowed in any position where it is currently optional. In other words, if it makes no difference, don’t allow it. That could reduce some potential confusion.

What about if I refactor the lambda function like this:

if (x > 0 && y > 0) (x * y + 1) else return -1

How does it know that it is not optional here? Since the lambda and the inner def have the same return type?

Also, what should happen if someone transforms a def that used a return into a function using eta-expassion inside another def, should the return preserve its previous scope or should it now return from the current innermost enclosing def?

Again, I won’t deny that there are a few cases where an early return is required. I just think that: First, that is an advanced use case that shouldn’t be mentioned to beginners. And second, that it doesn’t require extra machinery from the language (i.e. Its own reserved word), exceptions already cover that use case.

Now, someone could write some DSL to make the use case more pleasant; and such DSL may be part of the stdlib. I believe that is what was done in Dotty.

I don’t assume that. Returns should be somehow named and associated lexically with blocks.

I think these questions about how to interpret odd uses of “return” miss the point. Let the language designers decide what is appropriate, and if someone wants to use return is such complicated ways, then let them figure it out for themselves.

Normal use cases simplify code and are easy to understand. These fringe cases are a red herring as far as I am concerned. If you want to ban return for these contrived cases, then have at it, but I don’t think it should be discouraged for normal uses.

Hi BalmungSan, Your opinion seems to be that early return does not help. There seem to be two camps of thought. I’d like to understand your point of view.

How should the Scala std library have implemented the find function as well as exists, forall, contains, in your opinion? Would you have prefered it be done with a var and while? as seems to be the strategy of take/drop and friends?


trait LinearSeqOptimized[+A, +Repr <: LinearSeqOptimized[A, Repr]] extends LinearSeqLike[A, Repr] { self: Repr =>

  ...

  def find(p: A => Boolean): Option[A] = {
    var result: Option[A] = None
    breakable {
      for (x <- this)
        if (p(x)) { result = Some(x); break }
    }
    result
  }

What’s wrong with the idea of having named returns which are never ambiguous?

What is a named return? A new name for goto?

Many of the methods in stdlib are and will be implemented using vars and while loops for performance anyway.

If you want non-local returns then you’ll have scala.util.control.NonLocalReturns in Scala 3: Deprecated: Nonlocal Returns There will be nothing lost in terms of functionality, only some extra boilerplate would be needed and that’s it. Scala stdlib will use them where it’s beneficial.

A return that silently throws and catches exception (until that breaks of course as abstractions leak) is a disaster for functional programming, where closures are treated as data, stored and moved around, taking the implicit throw away from the implicit try / catch. Making that explicit is a good decision as it will make it obvious where a potential problem is.

Discussion here mixes two things:

  • whether to use return at all
  • whether to use only local return or also the non-local one
1 Like

No it is not a goto. A named return is paired with a block. You can only return from a block. A block introduces a tag, and you have to give that tag to the return to return from it.
In Common Lisp for example, a named function introduces a block of the same name, but you can also have one or more explicit named blocks within an anonymous function. In Scheme we can use call-with-current-continuation for the same purpose.

One way to implement this is the def block[A](body:(A=>Nothing)=>A):A = {...} which I illustrated in the original post. The way it works in the OP is that the implementation of block (whose syntax is similar to call-with-current-continuation) calls the unary function given as its argument, and moreover, it calls it with another unary function which the body can call with the desired return value. This means the user can have many different returns, each with a different name of his choosing.

In the following example, break_1 and break_2 are functions and the programmer decides their name. Calling break_1 returns from the outer block; calling break_1 returns from the inner block. Since they are functions, they can be passed as values, so invoking the return need not be in the same lexical scope as the block itself.

block{ 
  break_1 =>
    block{
       break_2 =>
       ... some code...
       ... if something break_1(my_return_value_1)
       ... some code
       ... if something break_2(my_return_value_2)
       ... some code
    }
}

I once again took a look at the NonLocalReturns link in Scala 3, and I still don’t see how it solves the problem. When there are multiple concentric returning {...} blocks, now does throwReturn know which one to return from?

By default throwReturn returns from closest returning { ... } block.

API is here: http://dotty.epfl.ch/api/scala/util/control/NonLocalReturns$.html
Source is here: https://github.com/lampepfl/dotty/blob/master/library/src/scala/util/control/NonLocalReturns.scala

Usage example: https://scastie.scala-lang.org/nBRnpCvSQ564VpIlvLDDGA

import scala.util.control.NonLocalReturns._

@main def main = {
  println {
    returning {
      returning {
        throwReturn(5)
        3
      }
      8
    }
  }

  println {
    returning { returner ?=>
      returning {
        throwReturn(5)(using returner)
        3
      }
      8
    }
  }
}

Prints:

8
5
2 Likes

I see. So the syntax is not so different than the simpler one what I proposed. It looks like a named return. right?
BTW what is ?=>. Is it a new type of => ? or is returning a macro which rewrites is body?

Yes, you can give it a name if you want. I gave a name to 1 out of 4 returning blocks.

It’s not a macro. It’s a new syntax that replaces implicits: Context Functions

2 Likes

NB: it’s worth reading the entire section on Contextual Abstractions. Basically, the many different capabilities that are currently lumped under the keyword implicit are being broken into several different, more-focused concepts. They’re all related, though, so the linked page makes more sense if you read the whole section.

This bit, Context Functions, is actually new, and enables you to remove a bit of boilerplate that is necessary in Scala 2.

Hi :slight_smile:
My opinion about early returns is the same about mutability.

It is not that is is not useful ever.
It is that you actually do not require those for the majority of the code people usually write (and thus, beginners shouldn’t need to be taught about those, at least not until they become comfortable with the language).

However, as with everything, there are exceptions (bad pun intended).
Advanced use cases would require those (even if the code could be refactored to do not need them); either because performance, either because the code is easier to read that way, etc.

Now, concretely about early returns, my point in this thread has been that:
It doesn’t need direct compiler support, because Exceptions already provide a superset of that functionality; and maybe some DSL provided by the stdlib (which IIUC is what Dotty does).

I have always believed that the stdlib is not a good example of idiomatic code.
If there is a piece of code that is subject of needing good performance, that is the stdlib.
So I actually do not have any opinion on how those should be implemented, whatever the maintainers considered was the best idea as a tradeoff between performance and maintainability.

BTW, as a side quick note.
You can easily implement all those with a tail recursive function for List, ArraySeq and Iterator.
So, if the stdlib would have wanted to be “pure”(whatever that means), that would be the way.

1 Like

Yes, named returns seem like a reasonable idea to me. They look a lot like break and continue. When I switched from Python to Scala several years ago, I was disappointed at the absense of break and continue, so I implemented them myself as an exercise (and they were also added in the standard library). I have since learned to avoid break and continue in most cases, but I still come across situations in which they seem awfully handy. I sometimes end up writing a small nested helper function with an early return to implement the equivalent functionality.