
In the network and on Habré there are already quite a few introductory-level articles about how to start writing on Scala, and revealing the features of the functional approach.
Some time ago we completely translated one of the main web projects to Scala. During this time, I watched the evolution of developers, including my own, and I had a long list of constructions that I’m writing to write if you’ve previously written in Java, and for whom the right decision on Scala may not be immediately obvious. These recommendations may not be very clear to those who still write in Java and have not seen Scala code before. I will not explain the work of standard functions and functional concepts, everything is searched for keywords in the network.
Let's start with a trivial case: the Scala API you use returns Option. You want to get a value from it and process it. Java programmers would write it like this:
val optionalValue = service.readValue() if(optionalValue.isDefined) {
What is bad in this code? For the Scala world, there are several unacceptable features: first, optionalValue, in the Scala code, Option returns a lot of interfaces, and this is great because it requires writing error handling rather than hammering on it, hoping that the error will be understood in the general error handler (which will produce something unintelligible, such as, "Unknown error, repeat later"). Maybe you are very responsible and think: in Java, I handled all the errors! Maybe, but experience has shown that, rewriting a large class on Scala, despite a lot of various checks, you consistently find a couple of places where the error was not processed and you have to find ways to do it, because writing code that clearly throws NPE does not allow conscience. In short, by adding the optional prefix you will often get twins of variables in which there is no special meaning. The second is that the test for emptiness Option in an explicit form, as will be shown below, is too brutal. And, thirdly, the call to Option.get, which generally should have been banned (whenever I see it, it means that the code can be rewritten much cleaner). In fact, nothing from the point of view of the type system protects such code. Testing if someone can rewrite or forget and then you get an analogue of NPE, which completely devalues the use of the Option class.
')
In fact, the options to write this code are more beautiful than two. The first occurs when, if you have a value, then you need to take additional steps, and the absence of a value is not required. Then, using the fact that Option - Iterable, you can write this:
for(value <- service.readValue()) { processValue(value) }
The second is when you need to handle both cases. Then it is recommended to use pattern matching:
service.readValue() match { case Some(value) => processValue(value) case None => throw new IllegalArgumentException("No value!") }
Please note that each of the options is devoid of the described disadvantages.
We continue. Often, obtaining a value is associated with the handling of exceptions, and often such constructs are born:
var value: Type = null try { value = parse(receiveValue()) } catch { case e: SomeException => value = defaultValue }
There are several flaws here too: we use mutable variables, we explicitly indicate the type, although it is more or less obvious and we use null, which is not very necessary in a good scala program and brings only trouble. Using the fact that all expressions in Scala return values, you can write the example above as follows:
val value = try { parse(receiveValue()) } catch { case e: SomeException => defaultValue }
The code gets cleaner and we get rid of variability. Sometimes the idea of the author of the initial code is even more interesting: he has already met Option and knows that this is good, and, especially, he feels that they are needed here:
var value: Option[Type] = null try { value = Some(parse(receiveValue())) } catch { case e: SomeException => value = None }
By the way, there is an interesting feature: if parse, suddenly, God forbid, returns null, which may be, then we will get Some (null), not None, which could be expected, therefore, at least, it was necessary would write Option (parse (receiveValue ())), and even better, use the standard package scala.util.control.Exception._ like this:
val value = catching(classOf[SomeException]).opt({ parse(receiveValue()) }).getOrElse(defaultValue)
Good. But what if we have a list of options where some of the elements matter, and some do not, and we need to get a list of filled values in order to work with them. A developer who has become familiar with the standard Scala library will immediately recall the filter method, which creates a collection of existing elements that satisfy the predicate, may even remember filterNot, and write:
list.map(_.optionalField).filterNot(_ == None).map(_.get)
As described above, this expression is vicious, but what to do with it immediately is not clear. After thinking for a while, you can come to the conclusion that you really want to actually make flatten, but List and Option are different monads that do not even commute! And here rescues the fact that Scala is not only a functional language, but also an object-oriented one, because both List and Option are in fact Iterable, where map and flatten are defined, bingo! The Scala compiler is able to display the type correctly and we write:
list.map(_.optionalField).flatten
What can be safely reduced to:
list.flatMap(_.optionalField)
This is great!
Finally, a simple example from
Twitter "Effective Scala" , for the same list of options. This example is one of my last discoveries. Unfortunately, it is rarely applicable to the code of our project, but still its beauty is captivating. So, we have a list of options and want to convert it by executing one code for existing values, and another for non-existing values. Basically, we write to the forehead:
iterable.map(value => value match { case Some(value) => whenValue(value) case None => whenNothing() })
This is pretty clean, but due to the fact that the map method accepts the function and the way we define Partial Functions in Scala, we can write more elegantly:
iterable.map({ case Some(value) => whenValue(value) case None => whenNothing() })
By the way, with transfer of functions in map one more feature is connected. Sometimes you can see the code:
iterable.map(function(_))
If you wrote this, then in addition to the function being passed, another one will be created that will take the value passed to the map and simply call function. That is, do nothing. In this case, it is simpler and cleaner to transfer to the map, and to any other higher-order functions, the functions themselves, without generating additional closures like this:
iterable.map(function)
Well, that's all for this time. I hope the examples above will help improve your Scala code base. It is a pity that, according to the examples given, the plug-ins to IntelliJ IDEA and Maven, which check the quality of the Scala code, cannot tell what is good and what is bad, stating only the presence of null or a variable in the code, and not offering solutions. I hope you have them now.
Next time I want to tell you about the use of standard collections. And your personal recipes to make the code better, it would be interesting to learn from the comments.