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 // ...
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
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.
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.
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.
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