📜 ⬆️ ⬇️

Do not use return in Scala

Today I would like to bring to your attention the translation of a small article by Robert Norris, perhaps familiar to you under the nickname tpolecat . This person is quite well known in the Scala community as the author of the doobie library and a member of the cats project.


In his publication, Robert tells us that using return can negatively affect the semantics of your code, and also sheds light on a couple of interesting features of the return implementation in Scala. You can find the original article in the author’s blog here .


So, every time Martin launches the Coursera course, we have people on #scala asking why they ’ll take style points off for return . Therefore, here's some valuable advice:


The keyword return not “optional” or “implied” in context — it changes the meaning of your program, and you should never use it.

Take a look at this small example:


 //         , //   . def add(n: Int, m: Int): Int = n + m def sum(ns: Int*): Int = ns.foldLeft(0)(add) scala> sum(33, 42, 99) res0: Int = 174 //   ,    return. def addR(n:Int, m:Int): Int = return n + m def sumR(ns: Int*): Int = ns.foldLeft(0)(addR) scala> sumR(33, 42, 99) res1: Int = 174 

So far, everything is in order. There is no obvious difference between sum and sumR , which might lead you to believe that return is just an optional keyword. But let's slightly refactor both methods, manually zainyliniv add and addR :


 //  add. def sum(ns: Int*): Int = ns.foldLeft(0)((n, m) => n + m) scala> sum(33, 42, 99) res2: Int = 174 //  . //  addR. def sumR(ns: Int*): Int = ns.foldLeft(0)((n, m) => return n + m) scala> sumR(33, 42, 99) res3: Int = 33 // ... 

What the ...?!


In short, then:


When the control flow reaches the return , the current calculation stops and an immediate return occurs from the method in which return is found.

In our second example, the return does not return a value from an anonymous function — it returns a value from the method it is inside. Another example:


 def foo: Int = { val sumR: List[Int] => Int = _.foldLeft(0)((n, m) => return n + m) sumR(List(1,2,3)) + sumR(List(4,5,6)) } scala> foo res4: Int = 1 

Non-local return


When the function object containing the call to return is executed non-locally, the termination of the calculation and the return of the result from it occurs by raising the NonLocalReturnControl[A] exception . This detail of the implementation easily and without special ceremony seeps out:


 def lazily(s: => String): String = try s catch { case t: Throwable => t.toString } def foo: String = lazily("foo") def bar: String = lazily(return "bar") scala> foo res5: String = foo scala> bar res6: String = scala.runtime.NonLocalReturnControl 

If anyone now Throwable me that intercepting Throwable is a bad Throwable , I can tell him that it is a bad form to use exceptions to control the flow of execution. The stupidity called breakable from the standard library is similarly structured and, like return, should never be used.


Another example. What if the return is closed into a lambda expression that remains alive even after its native method has worked? Rejoice, at your disposal is a time bomb that jerks at the first attempt to use.


 scala> def foo: () => Int = () => return () => 1 foo: () => Int scala> val x = foo x: () => Int = <function0> scala> x() scala.runtime.NonLocalReturnControl 

An added bonus is the fact that NonLocalReturnControl inherited from NoStackTrace , so you will not have any clue as to where this bomb was made. Cool stuff.


What type of return ?


In the return a construct, the returned expression a must match the type of the result of the method in which the return is located, however, the expression return a itself is of type. Based on its meaning “to stop further calculations,” you must have guessed what type it is. If not, here is a bit of enlightenment for you:


 def x: Int = { val a: Int = return 2; 1 } //  2 

See, the type analyzer does not swear, so we can assume that the type of the expression return a always coincides with the type a . Let's now test this theory by trying to write something that should not work:


 def x: Int = { val a: String = return 2; 1 } 

Hmm, also does not swear. What is going on? Whatever the type of return 2 , it must be reducible to Int and String at the same time. And since both of these classes are final , and Int is also AnyVal , you know what this is all about.


 def x: Int = { val a: Nothing = return 2; 1 } 

Exactly, to Nothing . And whenever you encounter Nothing , you would be wise to turn around and go the other way. Since Nothing is uninhabited (there is no single value of this type), the return result has no normal representation in the program. Any expression that has a Nothing type, when trying to calculate it, must either enter an infinite loop, or terminate the virtual machine, or (by the method of elimination) transfer control somewhere else, which we can observe here.


If you now thought: “Actually, in this example, we, logically, just cause a continuation , we do it all the time in Scheme, and I absolutely do not see the problem here,” well. Here's a cookie for you. But everyone except you, they think it's crazy.


Return violates referential transparency


This, as it were, is obvious. But suddenly you are not quite aware of what these words mean. So, if I have this code:


 def foo(n:Int): Int = { if (n < 100) n else return 100 } 

then, if it were referentially transparent, I would be entitled to rewrite it without changing the meaning like this:


 def foo(n: Int): Int = { val a = return 100 if (n < 100) n else a } 

Of course, it will not work: the execution of return causes a side effect.


But what if I really need it?


Not necessary. If you find yourself in a situation where, in your opinion, you need to leave the method ahead of time, in fact you need to alter the structure of the code. For example, this


 //       , //     . def max100(ns: List[Int]): Int = ns.foldLeft(0) { (n, m) => if (n + m > 100) return 100 else n + m } 

can be rewritten using simple tail recursion:


 def max100(ns: List[Int]): Int = { def go(ns: List[Int], a: Int): Int = if (a >= 100) 100 else ns match { case n :: ns => go(ns, n + a) case Nil => a } go(ns, 0) } 

This conversion is always possible. Even the complete elimination of the return from the language does not increase the number of programs that cannot be written on Scala, and by one. You may need a little effort on yourself to accept the uselessness of return , but as a result you will realize that it is much easier to write code without sudden returns than to wrestle with trying to predict the side effects caused by non-local control flow transitions.


From the translator:
Many thanks to Bortnikova Eugenia for proofreading. Special thanks to firegurafiku for clarifying the translation. Thanks to Vlad Ledovskikh, for a couple of practical advice that made the translation a little more accurate.


')

Source: https://habr.com/ru/post/331138/


All Articles