Part 1. Functional
This article (to be completely honest - a set of notes) is devoted to errors that beginners make when stepping on the path of Scala: not only juniors, but also experienced programmers with gray hair in a beard. Many of them have always worked before only with imperative languages ​​such as C, C ++ or Java, so the Scala idioms for them are incomprehensible and, moreover, not obvious. Therefore, I took the liberty of cautioning converts and telling them about their typical mistakes, both completely innocent and those in the Scala world punishable by death.
Publication structure:
At the very beginning of my career, I found myself in a very interesting situation: I, then still a very young developer, had to explain the fanciful idioms to my senior colleagues. It so happened - and I am grateful to life for this largely invaluable experience. Now I help in developing Scala to developers of all levels, from young to old, because in the company where I work, there is an internal system of employee training. At the moment, I, together with other mentors, are engaged in checking and supporting Scala courses.
It was originally planned to write this article in English under the sonorous title: “Scala for juniors and junior seniors”. But working with the Russian text turned out to be much faster and more convenient, so I had to sacrifice an untranslatable pun in the title. Just keep in mind that the article is designed not only for pure junior programs, but also for everyone who starts their acquaintance with the Scala language, no matter how much experience they have with imperative programming.
This article, by and large, is a hodgepodge of practical advice, in view of which it is devoid of any complex multi-level academic structure of the presentation of the material. Instead, the article is divided into two parts: in the first we will talk about functional programming idioms in Scala, and in the second we will discuss object-oriented idioms. And we start with the most undervalued possibility of Scala - type aliases (type aliases).
For many novice developers who have never had experience with typedef
, this language feature will seem useless. However, this is not quite the case: in C, type aliases are used by the standard library at every step and are one of the means of ensuring code portability between platforms; in addition, they markedly improve the readability of code in which pointers of a large degree of indirection are implicated. An example known to all - the canonical declaration of the signal
function is completely unreadable:
void (*signal(int sig, void (*func)(int)))(int);
However, adding an alias for pointers to a signal handler function solves this problem:
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
In C ++, without type aliases, static metaprogramming is unthinkable; in addition, they make it possible not to go crazy with the full names of template types.
In languages ​​such as Ocaml and Haskell, there is the keyword type
, the semantics of which is much more complicated than in Scala. For example, the type
keyword allows you to create not only synonyms of types , but also algebraic data types :
(* Ocaml*) type suit = Club | Diamond | Heart | Spade;;
And this is not limited to this: in Ocaml and SML you can also create
union types (union types):
(* OCaml *) type intorstring = Int of int | String of string;; (* SML *) datatype intorreal = INT of int | REAL of real
At the moment (Scala 2.12) it does not know how: the functionality declared with the help of the type
keyword is limited to type
synonyms, as well as the declaration of path-dependent types. However, in future versions this feature is planned to be added (thanks to senia for the clarification ). Why is it generally worth giving other types to already known types? First, it adds an additional semantic load, and, for example, the meaning of a DateString
type becomes clearer than just a String
, and the Map[Username, Key]
looks better than a Map[String, String]
. Secondly, synonymization allows you to reduce large and complex type signatures: The Map[Username, Key]
looks good, but Keystore
much shorter and clearer.
Of course, synonym types have their drawbacks: You see the type of Person
, and you cannot understand whether it is an object, a class, or an alias.
You should definitely not abuse this tool, but there are a number of situations where they will actually be helpful:
() => Unit
, associating this type of function with the name Action
.[, []]
You can find more examples here , just squander a bit below to the section with examples.
Scala assignment is not exactly what you are used to. Let's look at this operation and see that it is not as simple as it may seem at first glance:
// , scala> val address = ("localhost", 80) address: (String, Int) = (localhost,80) scala> val (host, port) = address host: String = localhost port: Int = 80
We have just decomposed a tuple into two variables, but the process is not limited to tuples:
scala> val first::rest = List(1,2,3,4,5) first: Int = 1 rest: List[Int] = List(2, 3, 4, 5)
We can perform similar operations with the case class
:
case class Person(name: String, age: Int) val max = Person("Max", 36) // max: Person = Person(Max,36) val Person(n, a) = max // n: String = Max // a: Int = 36
Moreover:
scala> val p @ Person(n, a) = max // p: Person = Person(Max,36) // n: String = Max // a: Int = 36
In the latter case, by the name of p
we will get the record of the case class
, and by the name of n
we will get the name, by a
- the age.
The sophisticated reader has already noticed that assignment behaves exactly the same way as pattern matching. Similar functionality is implemented in other languages, for example, Python and Erlang. Use this functionality primarily to decompress data structures. But do not abuse: unpacking of complex data structures greatly affects readability.
Many of you are already familiar with the Optional
type in Java 8. In Scala, the Option
type performs the same functions. And to many Java adepts this type may be known from the Guava library in Google.
Yes, Optional
used to avoid null
, and later NullPointerException
. Yes, it has methods isEmpty
and nonEmpty
. In the Guava version there is an isPresent
method. And many who used or did not use Optional
in Java or other languages, incorrectly use it in Scala.
However, not everyone is aware that in the same Guava, theOptional
define atransform
method that behaves similarly to a skalovskymap
.
Option
misuse is a common problem. Option
, first of all , is needed to conceptually show the likely missing entity, and not to run away from NPE. Yes, there is a problem, and the problem is serious. Someone even invents his own language for this. But let's go back to the misuse of Option
in Scala:
if (option.isEmpty) default else // c NoSuchElementException ( ) option.get
We do a check, and we, like, nothing should explode. Believe me, you can make a mistake in the industrial code, and the condition may turn out to be quite the expression that was expected. And even tests can be written incorrectly. Not you, so your predecessors.
In general, in the example above there is another problem. Your flow depends on some Boolean, and its integrity is broken.
Some developers have the ability to customize tests for already "working" code. Correct and shorter the above code can be written as:
option getOrElse default
The more compact your code is, the easier it is to find an error in it, and the more difficult this error is to make. There is a useful orElse
method that allows you to concatenate various Option
.
Often you need to transform the value inside Option, if it exists at all. For this, there is a map
method: it takes out a value, converts it, and packs it back into a container.
val messageOpt = Some("Hello") val updMessageOpt = messageOpt.map(msg => s"$mgs cruel world!") updMessageOpt: Option[String]
And sometimes it happens like this:
val messageOptOpt = Some(Some("Hello"))
Option
can be immensely invested in each other. This problem is solved by the flatMap
method or the flatten
method. The first one works like a map
— it transforms the internal value, but at the same time it flattens the structure, the second simply simplifies the structure.
Imagine that we have a certain function that returns Option
:
def concatOpt (s0: String, s1: String) = Option(s0 + s1)
then we can get a similar result:
messageOpt.map(msg => concatOpt(msg, " cruel world")) res0: Option[Option[String]] = Some(Some(Hello cruel world)) // `flatMap`: messageOpt.flatMap(msg => concatOpt(msg, " cruel world")) res6: Option[String] = Some(Hello cruel world) // flatten messageOptOpt.flatten == Some("Hello") res1: Option[String] = Some(Hello)
In Scala, there is another mechanism that can visibly facilitate the work with Option
, and it may be known to you as "For comprehension".
val ox = Some(1) val oy = Some(2) val oz = Some(3) for { x <- ox; y <- oy; z <- oz } yield x + y + z // res0: Option[Int] = 6
If any of the Option
types is equal to None
, after yield
user will receive an empty container, or rather, the value of an empty container. In the case of Option
this is None
. In the case of the list - Nil
.
And most importantly, try to do everything, just not to call the get
method. This leads to potential problems.
I know that you are well done and have checked everything. I'm sure your mom thinks so too, but it doesn’t give you a reason to pull get
.
Option
has get
, list has head
, and also has init
and tail
. Here is what you can get by calling the above methods on an empty list:
// : init: java.lang.UnsupportedOperationException head: java.lang.NoSuchElementException last: java.lang.NoSuchElementException tail: java.lang.UnsupportedOperationException
Of course, this will never happen to you if you check a sheet for emptiness.
The beginning rocker will do this using the notoriousif
-else
in its path.
Calling list.head
and associates is one of the best ways to deprive yourself
sleep when working with lists.
Wriggle a rattlesnake, do everything you can to avoid using list.head
and his friends.
Instead of head
good option would be to use the headOption
method. The lastOption
method behaves similarly. If you are in any way tied to indexes, you can use the isDefinedAt
method, which takes an integer argument (index) as a parameter. Everything described above still involves checks that you can forget about. There is a thousand and one more reasons for you to consciously lower them. The correct and idiomatic alternative would be to use pattern matching. The fact that the list is an algebraic type will not let you forget about Nil
, you can safely avoid the head
and tail
calls, saving you a few lines of code:
def printRec(list: List[String]): Unit = list match { // , // n, k , . That's the power! case Nil => () case x::xs => println(x) printRec(xs) }
A bit about performance
From the point of view of performance for a single-linked list, which is a ScalarList
(akascala.collection.immutable.List
), the cheapest operation will be writing to the top of the list, rather than to the end. To write to the end of the list is required to go through the entire list to the end. The complexity of writing to the beginning of the list is O (1), to the end of O (n). Do not forget about it.
In the code that just got acquainted with Scala with an enviable periodicity are found Option[List[A]]
. As in the arguments of the function, and as a return type. Often, those who created such a masterpiece use the following argument: "So we may or may not have a list, what will I use null
instead of it?".
Well, let's imagine another situation: Option
represents a conceptually possible unsuccessful outcome, and the list is a set of returned data. Imagine that we have a server that returns Option[List[Message]]
. If everything is good, we get a list of messages inside Some
. If there are no messages, we get an empty list inside Some
. And if an error occurred on the server, we get None
. Reasonable and viable?
And no! If we have an error in the system, we definitely need to know which one. And for this we need to return or Throwable
or some code. Does Option
us to do this? Not really, but Try
and Either
can help you with this.
The list can be empty in the same way as Option
, so you can safely pass an empty list if something goes wrong. I have not yet seen counter-examples where the Option[List]
construct could be viable. I would be very happy if you can find such examples and share them with me.
Most recently, I stumbled upon another interesting use of Option
. Let's look at the signature of the following function:
def process (item: Option[Item]): Option[UpdatedItem] = ???
There is no need to complicate the conversion using an additional container: this makes the function less common and visually clutters up the function signature. Instead, use the function type A => B
And if you want to save the type of the source container, wrap the original result in this container and use the map
or flatmap
for the subsequent data transformation.
The presence of tuples (tuples) is an interesting feature of a number of functional (and not only) languages. In functional languages, tuples may be used in the same way as records. We describe a tuple with the necessary data and wrap it in a new type, for example, using a newtype
in Haskell , resulting in a new type, the implementation of which is not known to the user. In purely functional languages ​​without tuples anywhere: they allow you to wonderfully represent dictionaries (dictionaries). Convolution without them would be less obvious.
In some languages, for example, Erlang, records appeared much later than tuples. Moreover, records (records) in Erlang are also tuples.
Scala is an object-oriented language. Yes, with support for functional programming elements. I am sure that many will disagree with me, but let's not forget that everything in Scala is an object. The presence of case
classes largely reduces the need for tuples: we get immutable records, which can also be compared with patterns (we will discuss this later). Each case
class already has its own type.
Tuples often have to be used by those who come from object-oriented languages: these language tools are unusual for them. Let's start with the fact that they are not called.
If the tuple is not used as an anonymous garbage dump, it should be called.
If you want to use a tuple to store data, use thecase class
for this.
For the functional style, it is considered to be good use of the previously mentioned aliases for types (type aliasing):
type Point = (Double, Double)
In the future, you refer to quite named types, and you will not have such terrible things:
// def drawLine(x: (Double, Double), y: (Double, Double)): Line = ??? // def drawLine(x: Point, y: Point): Line = ???
In Scala, you can reach the element of a tuple by index. For example:
// ! val y = point._2 //
This is especially sad when working with collections:
// ! points foreach { point: Point => println(s"x: ${point._1}, y: ${point._2}") }
And so do not. Of course, there are exceptional cases when this kind of measures increase readability:
// rows.groupBy(_._2)
But in most cases, the syntax with an underscore is better not to use. It is generally better to forget about him and not to remember. In Scala, there are more natural ways to do without this syntax.
In Scala, you can always do without a pair._2
. And it needs to be done.
To understand and understand why this is the case, let's turn to functional languages.
Question : Dear editors, why do indexes of lists in Scala start from zero, and tuples start from one? Vasily, Pokhabinsk.
Answer : Hello, Vasily. The answer is simple: because it is historically. In SML, functions #1
and #2
exist to access elements of a tuple. In Haskell
there are only two functions for accessing the elements of a tuple: fst
and snd
.
-- - . Haskell -- . . fst tuple
But to get the third or fifth element of the tuple just will not work. Do not believe? And in vain . And do not believe it if I tell you that the comparison with the sample is the most natural . And not just in Haskell
.
Ocaml
let third (_, _, elem) = elem
Erlang
1> Tuple = {1,3,4}. {1,3,4} 2> Third = fun ({_Fst, _Snd, Thrd}) -> Thrd end. #Fun<erl_eval.6.50752066> 3> Third(Tuple). 4
Python
And here is an example from a non-functional language:
>> (ip, hostname) = ("127.0.0.1", "localhost") >>> ip '127.0.0.1' >>> hostname 'localhost' >>>
Now let's apply this knowledge to Scala
// , trait Rectangle { def topLeft: Point ... } // val (x0, y0) = rectangle.topLeft // : points foreach { case (x, y) => println(s"x: ${x}, y: ${y}") }
Nobody canceled the standard matching mechanism using the match
keyword either.
Also tuples can be used as anonymous garbage cans, and this is sometimes justified. The fact is that in many functional languages ​​there is pattern matching at the level of function signatures:
-- haskell -- : map :: (a -> b) -> [a] -> [b] -- -- -- -- : map _ [] = [] -- x:xs , , -- Haskell head:tail . : - cons -- :: map fun (head:tail) = fun head : map fun tail
A similar mechanism is used in SML and Erlang. Unfortunately, Scala has no such opportunity. Therefore, tuples can be used for grouping and subsequent pattern matching:
// Haskell, :( def map [A, B] (f: A => B, l: List[A]): List[B] = (f, l) match { case (f, Nil) => List.empty case (f, head::tail) => f(head) :: map(f, tail) }
It is often necessary to update the value in one of the elements of the tuple. The copy
method is suitable for this.
val dog = ("Rex", 13) val olderDog = tuple.copy(_2 = 14)
You can read about the use of tuples in Haskell and SML if you follow the links.
In reality, using tuples is not the best way to represent coordinates (at least in Scala). In Scala, it is better to use case class
classes for this. The need for tuples is mainly dictated by the availability of universal libraries for cases when it is necessary to minimize the composite record in a generalized form. For example zip
or groupBy
. So, if you want to use tuples, use them only when writing generic algorithms. In all other cases, it is better to have a good old case class
.
Can you list all the cases when Scala uses _
? According to the survey results , only 7% of Scala-developers can do this. The underscore is used repeatedly in the language and in different contexts. Here it is well illustrated. In most cases, you cannot do without underscores: the syntax requires them for multiple imports or imports with exceptions. There are other reasonable applications (even where you can do without them). However, they do not add readability inside lambda expressions. Difficulties in reading lambdas arise when the number of underscore arguments exceeds 2.
When compared with a sample, they can also ruin your life. Is it clear what Fork
and Leaf
?
def weight(tree: CodeTree): Int = tree match { case Fork(_, _, _, weight) => weight case Leaf(_, weight) => weight }
And so?
def weight(tree: CodeTree): Int = tree match { case Fork(left, right, chars, weight) => weight case Leaf(char, weight) => weight }
As you may have noticed, these values ​​are not used. But this is not the case when they need to be overwritten with underscores. Believe me, the programming speed does not rest on the typing speed: you can write a few extra characters for the sake of readability.
In this article, I tried (and it didn’t work out, not for me to judge) to tell you about the main functional idioms of Scala and to draw a number of parallels with other functional languages. In the next part, I will talk about the idioms associated with OOP and collections, and also present my thoughts on infrastructure issues that are tormenting many novice developers. I really hope that you liked this article. Thank you for being patient and reading it to the end. To be continued.
Source: https://habr.com/ru/post/323706/