⬆️ ⬇️

Scala. Introduction

Hi habrareyudi.



Not so long ago, I became interested in one of the many languages ​​now under the JVM - Scala . There are many reasons for this, the main one is the ever increasing inconvenience when working with cpp-like languages. My eyes fell alternately on Ruby, Groovy, Python, but they all left an impression of tools that were not entirely suitable for my usual range of work tasks (Python is still good, but untyped languages ​​have their limitations). Scala, on the contrary, seemed to be a perfectly fit language. Since the search in Habr did not catch any articles about it (there were several, but not to say the least, introductory), I decided to write a small review and share it with the masses.



A bit of free language philosophy



What are the main goals pursued by the creators of the language? According to my worldview they are:

First, compatibility. Among their tasks, the developers put maintaining compatibility with the Java-language and thousands of examples of govnododa development on it to solve a variety of tasks.

Secondly, the intensive saturation of the language with functional features, which, mainly (but not completely), make up its differences from Java.

Thirdly, facilitating our work with you. Indeed, the Scala compiler understands the programmer from a half-word; I did not have time to write code specifically to make him understand that I was not a camel.

Fourth, support and encouragement of writing modular, loosely coupled software components, combined with extensive adaptability of existing ones. The objectives are not that completely opposite, but generating known difficulties for simultaneous achievement. Well, let's see what happens.

Fifth, it is support for concurrency. Unfortunately, my hands and head did not reach this area (I hope so far), but the focus on this moment is constantly on all resources in the language.



To experiment with the language, just put the appropriate plugin on your favorite IDE from here .

')

So let's look at the language itself ...



General ideas of the language, examples of syntax



Perhaps the most important is the “unified model of objects”. This term is interpreted by the authors as follows: “each value is an object, each operation is a method call”. This is, of course, not “everything is an object”, but entities in comparison with Java have departed, and the smaller the entities, the easier life is :) In application terms, this means that numbers and symbols have become immutable objects living in a common heap, all operations acquire reference semantics. For example, the code 5 + 5 is quite valid, and will generate a new object in the heap, which the garbage collector will quickly streamline (in fact, I quietly hope that the compiler will understand the depth of the plan and generate nothing :)).



After such a sublime introduction, you can look at the solution of the classical problem:

object Main {

def main(args:Array[ String ]) :Unit = {

print( "Hello, " + args(0) + "!" )

}

}




In it we see the following:



In addition, let's take a look at another short example:

println( ( "Hello, " + args(0) + "!" ).toUpperCase )

println( "Hello, " + args(0) + "!" toUpperCase )




As it follows from the use of the operator . absolutely not necessary. The language syntax fully allows the use of a space instead (you can also write method arguments without brackets and commas immediately after the method name). And as we see, this turns out to be quite useful: in the first line there is a high-priority operator . makes us write unnecessary cluttering code brackets, in the second we get a more concise and clear form of writing.



As an aid, the Scala developer also supports interactive mode. That is, you can start the interpreter and enter commands one by one. The interpreter built into the IDE somehow works irregularly, its separate version is in the Ubuntu repositories, I think the rest of the distros are also fine, the happy owners of Windows, as always, have to suffer :) The interpreter runs in the most unusual way:

$ scala

Welcome to Scala version 2.7.3final (Java HotSpot(TM) Server VM, Java 1.6.0_16).

Type in expressions to have them evaluated.

Type :help for more information.

scala>



A very small example:

scala> 1 to 10

res0: Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)



Here we see an example of a method call with a parameter. If someone has not guessed, the object of the class Int 1 calls the method to with a parameter of the same type 10, the result is a range of values.



Let's try now to write another function. Let her count us the sum of numbers in a given range, so:

scala> def sum(a: Int, b: Int): Int = {

| var result = 0

| for (i <- a to b) result += i

| result

| }

sum: (Int,Int)Int



scala> sum(1, 5)

res3: Int = 15



Here are three more important points:



Function Operations



What can we do with them here? Yes anything =) Functions are full-fledged objects of the program. They can be stored as properties of objects, passed as parameters and return values, and actually created at run time. These properties allow you to build so-called high-order functions that operate on their own kind.



To illustrate, consider the classic example of calculating a sum:

scala> def sum(f: Int => Int, a: Int, b: Int): Int =

| if (a > b) 0 else f(a) + sum(f, a + 1, b) sum: ((Int) => Int,Int,Int)Int



In this example, the sum function is defined, which represents the familiar, I hope, all sum operator. Parameters have the following meanings:

f is the function of converting an integer from the summation limits to the sum element. Pay attention to the declaration of the type of the parameter: the sign => means that the parameter is a function, the types of accepted values ​​are listed to its left in parentheses (if the parameter is one, as in this example, they can be omitted), the type of the returned result is to the right.

It works trivially: it calculates the value of the function at the lower end of the range and adds it to the result of calculating itself in the range 1 smaller.

Also in this example, another feature of the language is visible - if is an expression that has a value (by the way, the previously used for is also an expression, its result is of type Unit ). If the condition is true, then its result is the first option, otherwise the second one.

a and b are the limits of summation.



Another pair of functions id and square , they are equal to their parameter and its square, respectively.

scala> def id(x: Int): Int = x

id: (Int)Int

scala> def square(x: Int): Int = x * x

square: (Int)Int



Here you have to make another lyrical digression: functions in Scala have declarative style declarations. They describe not how to get the result, but what it is equal to. But if you want to organize sequential calculations in the function body, there are no problems - we have blocks.



Now you can take advantage of what we wrote earlier.

scala> sum(id, 1, 5)

res1: Int = 15

scala> sum(square, 1, 5)

res2: Int = 55



Here is the culmination of this part - we take and transfer the function to another function. No interfaces, anonymous classes, delegates: here it is - a little happiness.



I here intentionally did not give examples of nested and anonymous functions, currying. All this Scala can, but everything can not be included in a small review. I think this example is sufficient to understand the importance and, most importantly, the convenience of high-order functions as a programming tool. Finally, I can advise you to read this chapter of a wonderful book on programming.



Features of classes



Let's describe a simple class. Let it be a complex number. Create the following code:

class Complex(r: Double, i: Double) {

def real = r

def image = i

def magnitude = Math .sqrt(r*r + i*i)

def angle = Math .atan2(i, r)



def + (that: Complex) = new Complex( this .real + that.real, this .image + that.image)



override def toString = real+ " + i*" +image+ " | " +magnitude+ "*e^(i*" +angle+ "))"

}



object Main {

def main(args:Array[ String ]) :Unit = {

val first = new Complex(1, 5)

val second = new Complex(2, 4)

val sum = first + second

println(first)

println(second)

println(sum)

}

}




First, the class is declared with some parameters. As it is easy to guess by continuation, these are the constructor's parameters, which are available throughout the lifetime of the object.

Secondly, several methods are declared in the class - selectors. One family for the Cartesian representation and one for the polar. As you can see, both of them use constructor parameters.

Thirdly, an addition operator is declared in the class. It is declared as an ordinary method, it also accepts the Complex and returns it.

Finally, for this class, the toString function, which is familiar to all Java programmers, is toString . It is important to note that overriding methods in Scala should always be explicitly indicated using the override keyword.



Despite the enormous practical value of this class has several disadvantages, namely:



Well, let's try to correct the shortcomings using the tools of this beautiful language.

class Complex(val real: Double, val image: Double) extends Ordered[Complex] {

def magnitude = Math .sqrt(real*real + image*image)

def angle = Math .atan2(image, real)

def + (that: Complex) = new Complex( this .real + that.real, this .image + that.image)

def compare(that: Complex): Int = this .magnitude compare that.magnitude

override def toString = real+ " + i*" +image+ " | " +magnitude+ "*e^(i*" +angle+ "))"

}



object Main {

def main(args:Array[ String ]) :Unit = {

val first = new Complex(1, 5)

val second = new Complex(2, 4)

if (first > second )

println( "First greater" )

if (first < second )

println( "Second greater" )

if (first == second )

println( "They're equal" )

}

}




So, what's new:



It's time to say a few words about the trait idea. This is a special type of class that cannot have constructors, but can have any methods and attributes. Usually they establish some protocol for interacting with their possible heirs. Using this protocol, they can obtain the necessary information from the child and implement some behavior in it. Accordingly, any class (or object) can be inherited from an arbitrary number of traits (and only from one class). For example, Ordered declares the abstract method compare and, on its basis, complements the successor class with <, <=,>, etc. It should be noted here that in a good way it is worth redefining the operator == provided to us, since it also gives truth for non-identical objects, and equals methods with hashCode should also be redefined in such cases.

“All this is good,” the experienced fighter of the outsourcing market will say, “but what to do if a banal domain-class is required, with godless attribute modifiers?”.

Naturally, we have a solution :)

class User {

private [ this ] var _name: String = ""

def name = _name toUpperCase

def name_=(name: String ) = {

_name = if (name != null ) name else ""

}

}




To understand how to use all this, let's take a look at the result of executing the following code (for brevity, I did not include the description of the object and the main method here):

val user = new User( "Scala!!!" )

println(user.name)

user.name = "M. Odersky"

println(user.name)


SCALA!!!

M. ODERSKY





Attention, conclusion: a method with the name <something> _ = is called when using the construction <object>. <Something> = <something else>. As far as I know in Scala this is the second hack (the first is the conversion () to the call of the apply method), as Guido bequeathed with the implicit conversion from the use of the operator to the call of the method.



Pattern-matching



Start have a little distance. In Scala, there are so-called case classes (of course, and objects too). They are declared with the case keyword, after which the compiler takes the liberty to do the following:

  1. Create a constructor function with the same name as the class.
  2. Implement in the class toString, equals, hashCode based on the arguments of the constructor.
  3. Create selectors for all constructor arguments.


All this magic gives us a way to use the match method. Let's take a look at an example:

abstract class User



case class KnownUser(val name: String ) extends User



case class AnonymousUser() extends User



object Test {

val users = List (KnownUser( "Mark" ), AnonymousUser(), KnownUser( "Phil" ))



def register(user: User): Unit = user match {

case KnownUser(name) => println( "User " + name + " registered" )

case AnonymousUser() => println( "Anonymous user can't be registered" )

}



def main(args: Array[ String ]) =

users. foreach ( register )

}


So, the general picture of the code: there is an abstract user class, there are two of its casual descendants: known and anonymous users. We want to register a certain list of users for (here we include fantasy) an appointment. For what we use pattern-matching, which allows us to determine the different behavior of the method for different types of objects and provides a selection of data from these objects.



After such a vital example, you can use a little theory about the work of the match method. For each case expression, it performs a check on the type matches the class of the template and the matching of the constructor parameters to the template. A template in general can include:

  1. Constructors of other case classes. Everything is completely recursive, the nesting depth of the template is limited to the programmer’s madness is not limited.
  2. Template variables They become available in the body of the result calculation function.
  3. Symbols _ denoting any meaning that does not interest us.
  4. Literals language. For example, 1 or "Hello" . "Hello"


In this way, we get a tool that allows us to describe how to get some value from an object based on its structure (class) and / or data stored in it.



People familiar with the basic principles of OOP will of course immediately notice that this problem is completely solved using virtual functions (moreover, the proposed approach is not the best practice). However, their use carries with it two difficulties: firstly, it complicates the support of the code with a large number of such functions (we want to register users both for events, and in groups, and in blogs, etc., that for each case we create a virtual method ?), secondly, it does not solve the problem so that objects of the same type can in principle have a different structure and not be able to provide some data.



I would like to pay special attention to the second problem. What would the above code look like in Java? One class, if an anonymous user is set in the name null and checked every time (aesthetes like me turn on methods like isAnonymous , consisting of comparing fields with the same null ). The problems are obvious - implicitly and insecurely. There are a great number of such examples, when different variations of the structure of objects are combined into one class, and those that are not used in a particular case get clogged up with nulls, or even worse, the default value is invented. Scala allows you to explicitly describe variations in the structure of objects, and provides a convenient mechanism for working with these variations.



In conclusion, a couple of thoughts about when this technique can be effectively used as a substitute for virtual functions:



For all items, there are also reverse statements, for example, using pattern matching for a dozen classes does not seem to me a good idea.



Type inference



I think you have already noticed that in the code I specified types only when declaring classes and methods. In the code blocks, I almost always omitted them. The fact is that if the programmer does not explicitly indicate the type, Scala tries to determine it from the context. For example, when initializing the value of a constant in the definition of def s = "Scala" compiler will define the type of the constant as a string. All this also works on generic types, for example, the fragment above val users = List(KnownUser("Mark"), AnonymousUser(), KnownUser("Phil")) creates a constant of type List[User] , automatically rising to a suitable level in the hierarchy inheritance and using it to parameterize a container type. In practice, this means that you can save a lot on such ads (for fun, write one that does the same code in Java or C # :)).



Conclusion



Hmm ... To the beginning of the post already scroll long. Clearly it's time to finish. And I would like to say more about a lot: about the most interesting mechanism for describing generalized classes, about implicit conversions and the fact that they are in fact explicit, lazy initialization of constants.



I myself have yet to study the multithreading model and a peculiar set of primitives for its reaization, to deal with language support for xml, play around with the DSL structure, look at their flagship project - Lift ...



However, I still dare to draw a couple of conclusions:



That's all. Criticism is welcome.

Finally, a question for the masses: is this topic interesting, is it worth writing a sequel?



UPD: corrected grammar, thanks to all who helped in this. Especially ganqqwerty for mass disassembly with commas.



_________

Source Code Highlighter .

The text was prepared in Habra Editor

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



All Articles