Part 2. About everything and about anything
Today we will discuss a series of rocky idioms that do not fit in the first part of the article. We will consider the issues of language interoperation with Java and, most importantly, the misuse of object-oriented features of Scala.
Cycle structure
In Scala, almost everything is an expression, and even if Unit
returns something, you can always get your ()
output. After long programming in languages ​​where statements are prevalent, many of us (and I am not an exception) have a desire to shove all the calculations into one expression, making a long-term train out of them. The following example I brazenly dragged from Effective Scala. Suppose we have a sequence of tuples:
val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10))
We can process it famously (split into groups, sum up within groups, sort in descending order) with a single expression:
val orderedVotes = votes .groupBy(_._1) .map { case (which, counts) => (which, counts.foldLeft(0)(_ + _._2)) }.toSeq .sortBy(_._2) .reverse
Is this code simple, clear, expressive? Perhaps - for the rocky dog ​​who ate it. However, if we break an expression into named components, it will be easier for everyone:
val votesByLang = votes groupBy { case (lang, _) => lang } val sumByLang = votesByLang map { case (lang, counts) => val countsOnly = counts map { case (_, count) => count } (lang, countsOnly.sum) } val orderedVotes = sumByLang.toSeq .sortBy { case (_, count) => count } .reverse
Probably, this example is not clear enough - what, I was even too lazy to think of it myself. But believe me, I came across very long constructions that their authors did not even bother to carry on several lines.
Very often they come to Scala through Spark, and using Spark, one just wants to link more “carriages” - transformations into a long and expressive “train”. It is difficult to read such expressions; their narrative thread is lost rather quickly.
Extra long expressions and operator notation
I hope everyone knows that 2 + 2
in Scala is syntactic sugar for the expression 2.+(2)
. This type of record is referred to as operator notation. Thanks to it, there are no operators as such in the language, but only methods, albeit with non-literal names, and she herself is a powerful tool that allows you to create expressive DSLs (actually, for this, symbolic notation was added to the language). You can write calls of methods without points and object fun arg fun1 arg1
as long as you wish: object fun arg fun1 arg1
. It's crazy awesome if you want to do a readable DSL:
myList should have length 10
But, in most cases, operator notation in combination with long expressions brings continuous inconveniences: yes, operations on collections without parentheses look steeper, only you can understand them when they are divided into named components.
Trains and postfix notation
Postfix operators, under certain conditions, can turn the head of the unfortunate parser, so in recent versions of Scala, these expressions must be explicitly imported:
import language.postfixOps
Try not to use this language feature and design your DSL so that your users do not have to use it. It's pretty simple to do.
Scala supports uninitialized values. For example, this may be useful to you when creating beans. Let's look at the following Java class:
class MyClass { // -, Object null. // -. String uninitialized; }
We can achieve the same behavior from Scala:
class MyClass { // Scala, // . var uninitialized: String = _ }
Please do not do this without thinking . Initialize values ​​wherever you can. Use this language construct only if the framework or library you are using insist on it violently. With careless use, you can get tons of NullPointerException
. However, you should be aware of this feature: once such knowledge will save time. If you want to delay initialization, use the lazy
keyword.
Nullable
that may come from the outside in Option
.null
: use Option
, Either
, Try
, etc.Sometimes there are situations when null values ​​are part of a model. Perhaps this situation arose long before you joined the team, and even more so long before the introduction of Scala. As the saying goes: if booze cannot be prevented, it should be headed. And this will help you a pattern called Null Object. Often this is just another case class in ADT:
sealed trait User case class Admin extends User case class SuperUser extends User case class NullUser extends User
What do we get? Null, user and type safety.
Methods
In Scala, there is the possibility of overloading class constructors. And this is not the best way to solve a problem. I will say more, this is not an idiomatic way to solve a problem. If we talk about practice, this function is useful if you use Java reflection and your Scala code is called from Java or you need this behavior (and why not make a Builder)? In other cases, the best strategy is to create a companion object and define several apply
methods in it.
The most noteworthy cases are constructor overload due to ignorance of default parameters (default parameters).
Most recently, I witnessed the following outrage:
// ! case class Monster (pos: Position, health: Int, weapon: Weapon) { def this(pos: Position) = this(pos, 100, new Claws) def this(pos: Position, weapon: Weapon) = this(pos, 100, weapon) }
Casket opens easier:
case class Monster( pos: Position, health: Short = 100, weapon: Weapon = new Claws )
Want to reward your monster with a bazooka? Yes, no problem:
val aMonster = Monster(Position(300, 300, 20), weapon = new Bazooka)
We have made the world better, the monster is more peace-loving, and at the same time we have ceased to overload everything that moves. Peaceable? Definitely. After all, a bazooka is also a musical instrument (Wikipedia will not keep silent about it).
This applies not only to designers: people often overload and conventional methods (where this could have been avoided).
Operator Overloading
It is considered quite controversial Scala features. When I just plunged into the language, operator overload was used everywhere, by everyone, and wherever possible. Now this feature has become less popular. Initially, operator overloading was done, first of all, in order to be able to create DSL, as in Parboiled, or routing for akka-http.
Do not overload the operators without the need, and if you think that you have this need, then all the same do not overload.
And if you overload (you need DSL or your library does something mathematical (or difficult to express in words)), be sure to duplicate the operator with a function with a normal name. And think about the consequences. So, Thanks scalaz operator |@|
( Applicative Builder ) was named Maculay Culkin. And here is the picture of the "culprit":
Of course, after you repeatedly overload the constructors, to complete the picture you want to stick getters and setters.
Scala provides excellent interaction with Java. It can also make your life easier with the design of the so-called Beans. If you are not familiar with Java or the Beans concept, you may need to familiarize yourself with it.
Have you heard of Project Lombok ? The standard Scala library has a similar mechanism. It is called BeanProperty
. All you need to do is create a bean and add a BeanProperty
annotation to each field for which you want to create a getter or setter.
In order to get the name of the typeisProperty
for variables of a boolean type, addscala.beans.BooleanBeanProperty
to your scope.
The @BeanProperty
can also be used for class fields:
import scala.beans.{BooleanBeanProperty, BeanProperty} class MotherInLaw { // , : @BeanProperty var name = "Megaera" // . @BeanProperty var numberOfCatsSheHas = 0 // . @BooleanBeanProperty val jealous = true }
For case classes it works too:
import scala.beans.BeanProperty case class Dino(@BeanProperty name: String, @BeanProperty var age: Int)
Let's play with our dinosaur:
// , val barney = Dino("Barney", 29) barney.setAge(30) barney.getAge // res4: Int = 30 barney.getName // res14: String = Barney
Since we did not make the name a variable, when we try to use the setter, we get the following:
barney.setName <console>:15: error: value setName is not a member of Dino barney.setName
The emergence of case classes is a breakthrough for the JVM platform. What is their main advantage? That's right, in their immutability (immutability), as well as the presence of ready-made equals
, toString
and hashCode
. However, often and in them it is possible to meet similar:
// var. case class Person(var name: String, var age: Int)
Sometimes case classes have to be made changeable: for example, if you imitate beans, as in the example above.
But often this happens when a deep junior does not understand what is immunity. With developers, a higher level is no less interesting, because they are well aware of what they are doing:
case class Person (name: String, age: Int) { def updatedAge(newAge: Int) = Person(name, newAge) def updatedName(newName: String) = Person(newName, age) }
However, not everyone knows about the copy
method. This is the norm. I have seen something like this more than once, which is already there, at one time I myself was so hooligan. The copy
works in the same way as its namesake, which is defined for tuples:
// , . person.copy(age = 32)
Sometimes case classes tend to swell up to 15–20 fields. Before the advent of Scala 2.11, this process was somehow limited to 22 elements. But now your hands are untied:
case class AlphabetStat ( a: Int, b: Int, c: Int, d: Int, e: Int, f: Int, g: Int, h: Int, i: Int, j: Int, k: Int, l: Int, m: Int, n: Int, o: Int, p: Int, q: Int, r: Int, s: Int, t: Int, u: Int, v: Int, w: Int, x: Int, y: Int, z: Int )
Well, I lied to you: my hands, of course, became freer, but nobody canceled the restrictions of the JVM.
Large case classes are bad. This is very bad. There are situations when there is an excuse for this: the subject area in which you work does not allow aggregation, and the structure appears to be flat; you work with an API designed by deep idiots who sit on powerful tranquilizers.
And you know, most often you have to deal with the second option. It would be desirable, that case-classes easily and easy fit on API. And I understand you, if so.
But I have listed only valid excuses for the monstrousness of your case classes. There is the most obvious: in order to update the field, deeply hidden deep into the nested classes, you have to suffer a lot. Each case class must be carefully disassembled, replaced by a value and collected. And there is a remedy for this ailment: you can use lenses.
Why are lenses called lenses? Because they are able to focus on the main thing. You focus the lens on a certain part of the structure, and get it, along with the possibility of its (structure) to update. First, let's declare our case classes:
case class Address(street: String, city: String, postcode: String) case class Person(name: String, age: Int, address: Address)
And now fill them with data:
val person = Person("Joe Grey", 37, Address("Southover Street", "Brighton", "BN2 9UA"))
Create a lens for the street (suppose our character wanted to move):
import shapeless._ val streetLens = lens[Person].address.street
We read the field (please note that the string type will be displayed automatically):
val street = streetLens.get(person) // "Southover Street"
Update field value:
val person1 = streetLens.set(person)("Montpelier Road") // person1.address.street == "Montpelier Road"
An example was brazenly stolen "from here "
You can perform a similar operation on the address. As you can see, it is quite simple. Unfortunately, and perhaps fortunately, Scala does not have built-in lenses. Therefore, you will have to use a third-party library. I would recommend you to use shapeless
. Actually, the above example was written with the help of this very accessible library for a beginner.
There are many other implementations of lenses, if you want, you can use scalaz , monocle . The latter provides more advanced mechanisms for using optics, and I would recommend it for further use.
Unfortunately, in order to describe and explain the mechanism of action of the lenses, a separate article may be required, so I think that the above information is enough to start my own research of optical systems.
We take an experienced Java developer and force him to write on Scala. Not a couple of days pass, as he desperately begins to look for enums. Does not find them and is upset: in Scala there is no keyword enum
or, at least, enumeration
. Further, there are two variants of events: either he will go on an idiomatic solution, or he will start inventing his own transfers. Often laziness wins, and as a result we see this:
object Weekdays { val MONDAY = 0 // ... }
And then what? Here's what:
if (weekday == Weekdays.Friday) { stop(wearing, Tie) }
What's wrong? In Scala, there is an idiomatic way to create enumerations, it is called ADT (Algebraic Data Types), in Russian algebraic data types . Used, for example, in Haskell. Here is what it looks like:
sealed trait TrafficLight case object Green extends TrafficLight case object Yellow extends TrafficLight case object Red extends TrafficLight case object Broken extends TrafficLight
Verbose, self-made listing, of course, was shorter. Why write so much? Let's declare the following function:
def tellWhatTheLightIs(tl: TrafficLight): Unit = tl match { case Red => println("No cars go!") case Green => println("Don't stop me now!") case Yellow => println("Ooohhh you better stop!") }
And we get:
warning: match may not be exhaustive. It would fail on the following input: Broken def tellWhatTheLightIs(tl: TrafficLight): Unit = tl match { ^ tellWhatTheLightIs: (tl: TrafficLight)Unit
We get an enumeration free from binding to any constants, as well as checking for completeness of pattern matching. And yes, if you use “enum for the poor,” as one well-known colleague called them, use pattern matching. This is the most idiomatic way. It is worth noting that this is mentioned at the beginning of the book Programming in Scala . Not every bird will fly to the middle of the Dnieper, just as not every rockist will read Magnum Opus .
Not bad about algebraic data types are told, oddly enough, on Wikipedia . Regarding Scala, there is a fairly accessible post and presentation , which may seem interesting to you.
Admit it, you wrote methods that take a Boolean as an argument? In the case of Java, the situation is generally catastrophic:
PrettyPrinter.print(text, 1, true)
What can mean 1? Let us trust in intuition and assume that this is the number of copies. And what does true
answer for? It could be anything. Okay, I give up, I'll go to the source and see what it is.
In Scala, you can use ADT:
def print(text: String, copies: Int, wrapWords: WordWrap)
Even if you inherited a code that requires logical arguments, you can use the default parameters.
// , // , ? PrettyPrinter.print(text, copies = 1, WordWrap.Enabled)
Tail recursion is faster than most cycles. If she, of course, tail. For confidence, use the @tailrec
annotation. Situations are different, not always recursive solution is simple, accessible and understandable, then use while
. There is nothing wrong with that. Moreover, the entire library of collections is written in simple cycles with preconditions.
The main thing that you should know about list generators, or, as they are called, “for comprehensions”, is that their main purpose is not to implement loops.
Moreover, using this construction to iterate over indices will be a rather expensive procedure. A while
or using tail recursion is much cheaper. And clearer.
“For comprehension” is syntactic sugar for the map
, flatMap
and withFilter
. The keyword yield
used for the subsequent aggregation of values ​​in the resulting structure. Using "for comprehension" you actually use the same combinators, just in a veiled form. Use them directly:
// 1 to 10 foreach println // for (i <- 1 to 10) println(i)
Besides the fact that you called the same code, you also added a certain variable i
, which does not belong here at all. If you need speed, use a while
.
About variable names
Interesting history of variable names in question-answer format:
Question : Where did i
, j
, k
come from as cycle parameters?
Answer : From mathematics. And they got into programming thanks to a Fortran, in which the type of a variable is determined by its name: if the first letter of the name begins with I, J, K, L, M, or N, this automatically means that the variable is of integer type. Otherwise, the variable will be considered real (you can use the IMPLICIT
directive to change the default type).
And this nightmare has been living with us for almost 60 years. If you do not multiply the matrices, then there is no excuse for using i
, j
and k
even in Java. Use index
, row
, column
- anything. But if you write on Scala, try to avoid iterating with variables inside for
. From it.
A supplement to this section will be a video , which details everything you wanted to know about list generators.
In Scala, almost everything is an expression. The exception is return
, which should not be used under any circumstances. This is not an optional word, as many people think. This is a construct that changes the semantics of the program. Read more about it here.
Imagine there are tags in Scala. And I have no idea why they were added there. Until Scala 2.8 was released, the continue
label was located at this address, later it was eliminated.
Fortunately, labels are not part of the language, but are implemented by throwing out and catching exceptions (we'll talk about what to do with exceptions later in this article).
In my practice, I have not yet met a single case in which such behavior could somehow be justified. Most of the examples that I find on the net are far-fetched and sucked out of my finger. No, well, you look:
This example is taken from here :
breakable { for (i <- 1 to 10) { println(i) if (i > 4) break // . } }
This exceptional topic deserves a large and separate article. Perhaps even a series of articles. You can talk about it for a long time. Firstly, because Scala supports several fundamentally different approaches to exception handling. , , .
Scala checked exceptions. - — . , . , . , . — . , C++ Java, Scala . - . goto
. . , flow. , , , — Scala .
, , Validation
scalaz, scala.util.Try
, Either
. Option
, , . - , .
:
object Main extends App { Console.println("Hello World: " + (args mkString ", ")) }
« ». , , . . DelayedInit
. . App
, , DelayedInit
. App
.
It should be noted that this trait is implemented using the DelayedInit functionality, which means that fields of the object will not have been initialized before the main method has been executed.
-:
, DelayedInit, , main.
:
Future versions of this trait will no longer extend DelayedInit.
App
? . «Hello world»? . main
.
Scala. I will give a simple example:
tvs.filter(tv => tv.displaySize == 50.inches).headOption
, :
tvs.find(tv => tv.displaySize == 50.inches)
«» :
list.size = 0 // list.isEmpty // ok !list.empty // list.nonEmpty // ok tvs.filter(tv => !tv.supportsSkype) // tvs.filterNot(tv => tv.supportsSkype) // ok
, IntelliJ IDEA, . Scala IDE, , .
Scala . Scala collections Tips and Tricks , . , . .
, .
typedef
,EDU- DataArt , , Scala, . , , , ( ) .
Source: https://habr.com/ru/post/330816/
All Articles