A Scala Tutorial for Java Programmers

鲁阳焱
2023-12-01

A Scala Tutorial for Java Programmers

By Michel Schinz and Philipp Haller

Introduction

This document gives a quick introduction to the Scala language andcompiler. It is intended for people who already have some programmingexperience and want an overview of what they can do with Scala. Abasic knowledge of object-oriented programming, especially in Java, isassumed.

A First Example

As a first example, we will use the standard Hello world program. Itis not very fascinating but makes it easy to demonstrate the use ofthe Scala tools without knowing too much about the language. Here ishow it looks:

  1. object HelloWorld {
  2. def main(args: Array[String]) {
  3. println("Hello, world!")
  4. }
  5. }

The structure of this program should be familiar to Java programmers:it consists of one method called main which takes the commandline arguments, an array of strings, as parameter; the body of thismethod consists of a single call to the predefined method printlnwith the friendly greeting as argument. The main method does notreturn a value (it is a procedure method). Therefore, it is not necessaryto declare a return type.

What is less familiar to Java programmers is the objectdeclaration containing the main method. Such a declarationintroduces what is commonly known as a singleton object, thatis a class with a single instance. The declaration above thus declaresboth a class called HelloWorld and an instance of that class,also called HelloWorld. This instance is created on demand,the first time it is used.

The astute reader might have noticed that the main method isnot declared as static here. This is because static members(methods or fields) do not exist in Scala. Rather than defining staticmembers, the Scala programmer declares these members in singletonobjects.

Compiling the example

To compile the example, we use scalac, the Scala compiler. scalacworks like most compilers: it takes a source file as argument, maybesome options, and produces one or several object files. The objectfiles it produces are standard Java class files.

If we save the above program in a file calledHelloWorld.scala, we can compile it by issuing the followingcommand (the greater-than sign > represents the shell promptand should not be typed):

  1. > scalac HelloWorld.scala

This will generate a few class files in the current directory. One ofthem will be called HelloWorld.class, and contains a classwhich can be directly executed using the scala command, as thefollowing section shows.

Running the example

Once compiled, a Scala program can be run using the scala command.Its usage is very similar to the java command used to run Javaprograms, and accepts the same options. The above example can beexecuted using the following command, which produces the expectedoutput:

  1. > scala -classpath . HelloWorld
  2. Hello, world!

Interaction with Java

One of Scala’s strengths is that it makes it very easy to interactwith Java code. All classes from the java.lang package areimported by default, while others need to be imported explicitly.

Let’s look at an example that demonstrates this. We want to obtainand format the current date according to the conventions used in aspecific country, say France. (Other regions such as theFrench-speaking part of Switzerland use the same conventions.)

Java’s class libraries define powerful utility classes, such asDate and DateFormat. Since Scala interoperatesseemlessly with Java, there is no need to implement equivalentclasses in the Scala class library–we can simply import the classesof the corresponding Java packages:

  1. import java.util.{Date, Locale}
  2. import java.text.DateFormat
  3. import java.text.DateFormat._
  4. object FrenchDate {
  5. def main(args: Array[String]) {
  6. val now = new Date
  7. val df = getDateInstance(LONG, Locale.FRANCE)
  8. println(df format now)
  9. }
  10. }

Scala’s import statement looks very similar to Java’s equivalent,however, it is more powerful. Multiple classes can be imported fromthe same package by enclosing them in curly braces as on the firstline. Another difference is that when importing all the names of apackage or class, one uses the underscore character (_) insteadof the asterisk (*). That’s because the asterisk is a validScala identifier (e.g. method name), as we will see later.

The import statement on the third line therefore imports all membersof the DateFormat class. This makes the static methodgetDateInstance and the static field LONG directlyvisible.

Inside the main method we first create an instance of Java’sDate class which by default contains the current date. Next, wedefine a date format using the static getDateInstance methodthat we imported previously. Finally, we print the current dateformatted according to the localized DateFormat instance. Thislast line shows an interesting property of Scala’s syntax. Methodstaking one argument can be used with an infix syntax. That is, theexpression

  1. df format now

is just another, slightly less verbose way of writing the expression

  1. df.format(now)

This might seem like a minor syntactic detail, but it has importantconsequences, one of which will be explored in the next section.

To conclude this section about integration with Java, it should benoted that it is also possible to inherit from Java classes andimplement Java interfaces directly in Scala.

Everything is an Object

Scala is a pure object-oriented language in the sense thateverything is an object, including numbers or functions. Itdiffers from Java in that respect, since Java distinguishesprimitive types (such as boolean and int) from referencetypes, and does not enable one to manipulate functions as values.

Numbers are objects

Since numbers are objects, they also have methods. And in fact, anarithmetic expression like the following:

  1. 1 + 2 * 3 / x

consists exclusively of method calls, because it is equivalent to thefollowing expression, as we saw in the previous section:

  1. (1).+(((2).*(3))./(x))

This also means that +, *, etc. are valid identifiersin Scala.

The parentheses around the numbers in the second version are necessarybecause Scala’s lexer uses a longest match rule for tokens.Therefore, it would break the following expression:

  1. 1.+(2)

into the tokens 1., +, and 2. The reason thatthis tokenization is chosen is because 1. is a longer validmatch than 1. The token 1. is interpreted as theliteral 1.0, making it a Double rather than anInt. Writing the expression as:

  1. (1).+(2)

prevents 1 from being interpreted as a Double.

Functions are objects

Perhaps more surprising for the Java programmer, functions are alsoobjects in Scala. It is therefore possible to pass functions asarguments, to store them in variables, and to return them from otherfunctions. This ability to manipulate functions as values is one ofthe cornerstone of a very interesting programming paradigm calledfunctional programming.

As a very simple example of why it can be useful to use functions asvalues, let’s consider a timer function whose aim is to perform someaction every second. How do we pass it the action to perform? Quitelogically, as a function. This very simple kind of function passingshould be familiar to many programmers: it is often used inuser-interface code, to register call-back functions which get calledwhen some event occurs.

In the following program, the timer function is calledoncePerSecond, and it gets a call-back function as argument.The type of this function is written () => Unit and is the typeof all functions which take no arguments and return nothing (the typeUnit is similar to void in C/C++). The main function ofthis program simply calls this timer function with a call-back whichprints a sentence on the terminal. In other words, this programendlessly prints the sentence “time flies like an arrow” everysecond.

  1. object Timer {
  2. def oncePerSecond(callback: () => Unit) {
  3. while (true) { callback(); Thread sleep 1000 }
  4. }
  5. def timeFlies() {
  6. println("time flies like an arrow...")
  7. }
  8. def main(args: Array[String]) {
  9. oncePerSecond(timeFlies)
  10. }
  11. }

Note that in order to print the string, we used the predefined methodprintln instead of using the one from System.out.

Anonymous functions

While this program is easy to understand, it can be refined a bit.First of all, notice that the function timeFlies is onlydefined in order to be passed later to the oncePerSecondfunction. Having to name that function, which is only used once, mightseem unnecessary, and it would in fact be nice to be able to constructthis function just as it is passed to oncePerSecond. This ispossible in Scala using anonymous functions, which are exactlythat: functions without a name. The revised version of our timerprogram using an anonymous function instead of timeFlies lookslike that:

  1. object TimerAnonymous {
  2. def oncePerSecond(callback: () => Unit) {
  3. while (true) { callback(); Thread sleep 1000 }
  4. }
  5. def main(args: Array[String]) {
  6. oncePerSecond(() =>
  7. println("time flies like an arrow..."))
  8. }
  9. }

The presence of an anonymous function in this example is revealed bythe right arrow => which separates the function’s argumentlist from its body. In this example, the argument list is empty, aswitnessed by the empty pair of parenthesis on the left of the arrow.The body of the function is the same as the one of timeFliesabove.

Classes

As we have seen above, Scala is an object-oriented language, and assuch it has a concept of class. (For the sake of completeness, it should be noted that some object-oriented languages do not have the concept of class, but Scala is not one of them.)Classes in Scala are declared using a syntax which is close toJava’s syntax. One important difference is that classes in Scala canhave parameters. This is illustrated in the following definition ofcomplex numbers.

  1. class Complex(real: Double, imaginary: Double) {
  2. def re() = real
  3. def im() = imaginary
  4. }

This complex class takes two arguments, which are the real andimaginary part of the complex. These arguments must be passed whencreating an instance of class Complex, as follows: new Complex(1.5, 2.3). The class contains two methods, called reand im, which give access to these two parts.

It should be noted that the return type of these two methods is notgiven explicitly. It will be inferred automatically by the compiler,which looks at the right-hand side of these methods and deduces thatboth return a value of type Double.

The compiler is not always able to infer types like it does here, andthere is unfortunately no simple rule to know exactly when it will be,and when not. In practice, this is usually not a problem since thecompiler complains when it is not able to infer a type which was notgiven explicitly. As a simple rule, beginner Scala programmers shouldtry to omit type declarations which seem to be easy to deduce from thecontext, and see if the compiler agrees. After some time, theprogrammer should get a good feeling about when to omit types, andwhen to specify them explicitly.

Methods without arguments

A small problem of the methods re and im is that, inorder to call them, one has to put an empty pair of parenthesis aftertheir name, as the following example shows:

  1. object ComplexNumbers {
  2. def main(args: Array[String]) {
  3. val c = new Complex(1.2, 3.4)
  4. println("imaginary part: " + c.im())
  5. }
  6. }

It would be nicer to be able to access the real and imaginary partslike if they were fields, without putting the empty pair ofparenthesis. This is perfectly doable in Scala, simply by definingthem as methods without arguments. Such methods differ frommethods with zero arguments in that they don’t have parenthesis aftertheir name, neither in their definition nor in their use. OurComplex class can be rewritten as follows:

  1. class Complex(real: Double, imaginary: Double) {
  2. def re = real
  3. def im = imaginary
  4. }

Inheritance and overriding

All classes in Scala inherit from a super-class. When no super-classis specified, as in the Complex example of previous section,scala.AnyRef is implicitly used.

It is possible to override methods inherited from a super-class inScala. It is however mandatory to explicitly specify that a methodoverrides another one using the override modifier, in order toavoid accidental overriding. As an example, our Complex classcan be augmented with a redefinition of the toString methodinherited from Object.

  1. class Complex(real: Double, imaginary: Double) {
  2. def re = real
  3. def im = imaginary
  4. override def toString() =
  5. "" + re + (if (im < 0) "" else "+") + im + "i"
  6. }

Case Classes and Pattern Matching

A kind of data structure that often appears in programs is the tree.For example, interpreters and compilers usually represent programsinternally as trees; XML documents are trees; and several kinds ofcontainers are based on trees, like red-black trees.

We will now examine how such trees are represented and manipulated inScala through a small calculator program. The aim of this program isto manipulate very simple arithmetic expressions composed of sums,integer constants and variables. Two examples of such expressions are1+2 and (x+x)+(7+y).

We first have to decide on a representation for such expressions. Themost natural one is the tree, where nodes are operations (here, theaddition) and leaves are values (here constants or variables).

In Java, such a tree would be represented using an abstractsuper-class for the trees, and one concrete sub-class per node orleaf. In a functional programming language, one would use an algebraicdata-type for the same purpose. Scala provides the concept ofcase classes which is somewhat in between the two. Here is howthey can be used to define the type of the trees for our example:

  1. abstract class Tree
  2. case class Sum(l: Tree, r: Tree) extends Tree
  3. case class Var(n: String) extends Tree
  4. case class Const(v: Int) extends Tree

The fact that classes Sum, Var and Const aredeclared as case classes means that they differ from standard classesin several respects:

  • the new keyword is not mandatory to create instances ofthese classes (i.e., one can write Const(5) instead ofnew Const(5)),
  • getter functions are automatically defined for the constructorparameters (i.e., it is possible to get the value of the vconstructor parameter of some instance c of classConst just by writing c.v),
  • default definitions for methods equals andhashCode are provided, which work on the structure ofthe instances and not on their identity,
  • a default definition for method toString is provided, andprints the value in a “source form” (e.g., the tree for expressionx+1 prints as Sum(Var(x),Const(1))),
  • instances of these classes can be decomposed throughpattern matching as we will see below.

Now that we have defined the data-type to represent our arithmeticexpressions, we can start defining operations to manipulate them. Wewill start with a function to evaluate an expression in someenvironment. The aim of the environment is to give values tovariables. For example, the expression x+1 evaluated in anenvironment which associates the value 5 to variable x, written{ x -> 5 }, gives 6 as result.

We therefore have to find a way to represent environments. We could ofcourse use some associative data-structure like a hash table, but wecan also directly use functions! An environment is really nothing morethan a function which associates a value to a (variable) name. Theenvironment { x -> 5 } given above can simply be written asfollows in Scala:

  1. { case "x" => 5 }

This notation defines a function which, when given the string"x" as argument, returns the integer 5, and fails with anexception otherwise.

Before writing the evaluation function, let us give a name to the typeof the environments. We could of course always use the typeString => Int for environments, but it simplifies the programif we introduce a name for this type, and makes future changes easier.This is accomplished in Scala with the following notation:

  1. type Environment = String => Int

From then on, the type Environment can be used as an alias ofthe type of functions from String to Int.

We can now give the definition of the evaluation function.Conceptually, it is very simple: the value of a sum of two expressionsis simply the sum of the value of these expressions; the value of avariable is obtained directly from the environment; and the value of aconstant is the constant itself. Expressing this in Scala is not moredifficult:

  1. def eval(t: Tree, env: Environment): Int = t match {
  2. case Sum(l, r) => eval(l, env) + eval(r, env)
  3. case Var(n) => env(n)
  4. case Const(v) => v
  5. }

This evaluation function works by performing pattern matchingon the tree t. Intuitively, the meaning of the above definitionshould be clear:

  1. it first checks if the tree t is a Sum, and if itis, it binds the left sub-tree to a new variable called l andthe right sub-tree to a variable called r, and then proceedswith the evaluation of the expression following the arrow; thisexpression can (and does) make use of the variables bound by thepattern appearing on the left of the arrow, i.e., l andr,
  2. if the first check does not succeed, that is, if the tree is nota Sum, it goes on and checks if t is a Var; ifit is, it binds the name contained in the Var node to avariable n and proceeds with the right-hand expression,
  3. if the second check also fails, that is if t is neither aSum nor a Var, it checks if it is a Const, andif it is, it binds the value contained in the Const node to avariable v and proceeds with the right-hand side,
  4. finally, if all checks fail, an exception is raised to signalthe failure of the pattern matching expression; this could happenhere only if more sub-classes of Tree were declared.

We see that the basic idea of pattern matching is to attempt to matcha value to a series of patterns, and as soon as a pattern matches,extract and name various parts of the value, to finally evaluate somecode which typically makes use of these named parts.

A seasoned object-oriented programmer might wonder why we did notdefine eval as a method of class Tree and itssubclasses. We could have done it actually, since Scala allows methoddefinitions in case classes just like in normal classes. Decidingwhether to use pattern matching or methods is therefore a matter oftaste, but it also has important implications on extensibility:

  • when using methods, it is easy to add a new kind of node as thiscan be done just by defining a sub-class of Tree for it; onthe other hand, adding a new operation to manipulate the tree istedious, as it requires modifications to all sub-classes ofTree,
  • when using pattern matching, the situation is reversed: adding anew kind of node requires the modification of all functions which dopattern matching on the tree, to take the new node into account; onthe other hand, adding a new operation is easy, by just defining itas an independent function.

To explore pattern matching further, let us define another operationon arithmetic expressions: symbolic derivation. The reader mightremember the following rules regarding this operation:

  1. the derivative of a sum is the sum of the derivatives,
  2. the derivative of some variable v is one if v is thevariable relative to which the derivation takes place, and zerootherwise,
  3. the derivative of a constant is zero.

These rules can be translated almost literally into Scala code, toobtain the following definition:

  1. def derive(t: Tree, v: String): Tree = t match {
  2. case Sum(l, r) => Sum(derive(l, v), derive(r, v))
  3. case Var(n) if (v == n) => Const(1)
  4. case _ => Const(0)
  5. }

This function introduces two new concepts related to pattern matching.First of all, the case expression for variables has aguard, an expression following the if keyword. Thisguard prevents pattern matching from succeeding unless its expressionis true. Here it is used to make sure that we return the constant 1only if the name of the variable being derived is the same as thederivation variable v. The second new feature of patternmatching used here is the wildcard, written _, which isa pattern matching any value, without giving it a name.

We did not explore the whole power of pattern matching yet, but wewill stop here in order to keep this document short. We still want tosee how the two functions above perform on a real example. For thatpurpose, let’s write a simple main function which performsseveral operations on the expression (x+x)+(7+y): it first computesits value in the environment { x -> 5, y -> 7 }, thencomputes its derivative relative to x and then y.

  1. def main(args: Array[String]) {
  2. val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y")))
  3. val env: Environment = { case "x" => 5 case "y" => 7 }
  4. println("Expression: " + exp)
  5. println("Evaluation with x=5, y=7: " + eval(exp, env))
  6. println("Derivative relative to x:\n " + derive(exp, "x"))
  7. println("Derivative relative to y:\n " + derive(exp, "y"))
  8. }

Executing this program, we get the expected output:

  1. Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y)))
  2. Evaluation with x=5, y=7: 24
  3. Derivative relative to x:
  4. Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0)))
  5. Derivative relative to y:
  6. Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1)))

By examining the output, we see that the result of the derivativeshould be simplified before being presented to the user. Defining abasic simplification function using pattern matching is an interesting(but surprisingly tricky) problem, left as an exercise for the reader.

Traits

Apart from inheriting code from a super-class, a Scala class can alsoimport code from one or several traits.

Maybe the easiest way for a Java programmer to understand what traitsare is to view them as interfaces which can also contain code. InScala, when a class inherits from a trait, it implements that trait’sinterface, and inherits all the code contained in the trait.

To see the usefulness of traits, let’s look at a classical example:ordered objects. It is often useful to be able to compare objects of agiven class among themselves, for example to sort them. In Java,objects which are comparable implement the Comparableinterface. In Scala, we can do a bit better than in Java by definingour equivalent of Comparable as a trait, which we will callOrd.

When comparing objects, six different predicates can be useful:smaller, smaller or equal, equal, not equal, greater or equal, andgreater. However, defining all of them is fastidious, especially sincefour out of these six can be expressed using the remaining two. Thatis, given the equal and smaller predicates (for example), one canexpress the other ones. In Scala, all these observations can benicely captured by the following trait declaration:

  1. trait Ord {
  2. def < (that: Any): Boolean
  3. def <=(that: Any): Boolean = (this < that) || (this == that)
  4. def > (that: Any): Boolean = !(this <= that)
  5. def >=(that: Any): Boolean = !(this < that)
  6. }

This definition both creates a new type called Ord, whichplays the same role as Java’s Comparable interface, anddefault implementations of three predicates in terms of a fourth,abstract one. The predicates for equality and inequality do not appearhere since they are by default present in all objects.

The type Any which is used above is the type which is asuper-type of all other types in Scala. It can be seen as a moregeneral version of Java’s Object type, since it is also asuper-type of basic types like Int, Float, etc.

To make objects of a class comparable, it is therefore sufficient todefine the predicates which test equality and inferiority, and mix inthe Ord class above. As an example, let’s define aDate class representing dates in the Gregorian calendar. Suchdates are composed of a day, a month and a year, which we will allrepresent as integers. We therefore start the definition of theDate class as follows:

  1. class Date(y: Int, m: Int, d: Int) extends Ord {
  2. def year = y
  3. def month = m
  4. def day = d
  5. override def toString(): String = year + "-" + month + "-" + day

The important part here is the extends Ord declaration whichfollows the class name and parameters. It declares that theDate class inherits from the Ord trait.

Then, we redefine the equals method, inherited fromObject, so that it correctly compares dates by comparing theirindividual fields. The default implementation of equals is notusable, because as in Java it compares objects physically. We arriveat the following definition:

  1. override def equals(that: Any): Boolean =
  2. that.isInstanceOf[Date] && {
  3. val o = that.asInstanceOf[Date]
  4. o.day == day && o.month == month && o.year == year
  5. }

This method makes use of the predefined methods isInstanceOfand asInstanceOf. The first one, isInstanceOf,corresponds to Java’s instanceof operator, and returns trueif and only if the object on which it is applied is an instance of thegiven type. The second one, asInstanceOf, corresponds toJava’s cast operator: if the object is an instance of the given type,it is viewed as such, otherwise a ClassCastException isthrown.

Finally, the last method to define is the predicate which tests forinferiority, as follows. It makes use of another predefined method,error, which throws an exception with the given error message.

  1. def <(that: Any): Boolean = {
  2. if (!that.isInstanceOf[Date])
  3. error("cannot compare " + that + " and a Date")
  4. val o = that.asInstanceOf[Date]
  5. (year < o.year) ||
  6. (year == o.year && (month < o.month ||
  7. (month == o.month && day < o.day)))
  8. }

This completes the definition of the Date class. Instances ofthis class can be seen either as dates or as comparable objects.Moreover, they all define the six comparison predicates mentionedabove: equals and < because they appear directly inthe definition of the Date class, and the others because theyare inherited from the Ord trait.

Traits are useful in other situations than the one shown here, ofcourse, but discussing their applications in length is outside thescope of this document.

Genericity

The last characteristic of Scala we will explore in this tutorial isgenericity. Java programmers should be well aware of the problemsposed by the lack of genericity in their language, a shortcoming whichis addressed in Java 1.5.

Genericity is the ability to write code parametrized by types. Forexample, a programmer writing a library for linked lists faces theproblem of deciding which type to give to the elements of the list.Since this list is meant to be used in many different contexts, it isnot possible to decide that the type of the elements has to be, say,Int. This would be completely arbitrary and overlyrestrictive.

Java programmers resort to using Object, which is thesuper-type of all objects. This solution is however far from beingideal, since it doesn’t work for basic types (int,long, float, etc.) and it implies that a lot ofdynamic type casts have to be inserted by the programmer.

Scala makes it possible to define generic classes (and methods) tosolve this problem. Let us examine this with an example of thesimplest container class possible: a reference, which can either beempty or point to an object of some type.

  1. class Reference[T] {
  2. private var contents: T = _
  3. def set(value: T) { contents = value }
  4. def get: T = contents
  5. }

The class Reference is parametrized by a type, called T,which is the type of its element. This type is used in the body of theclass as the type of the contents variable, the argument ofthe set method, and the return type of the get method.

The above code sample introduces variables in Scala, which should notrequire further explanations. It is however interesting to see thatthe initial value given to that variable is _, which representsa default value. This default value is 0 for numeric types,false for the Boolean type, () for the Unittype and null for all object types.

To use this Reference class, one needs to specify which type to usefor the type parameter T, that is the type of the elementcontained by the cell. For example, to create and use a cell holdingan integer, one could write the following:

  1. object IntegerReference {
  2. def main(args: Array[String]) {
  3. val cell = new Reference[Int]
  4. cell.set(13)
  5. println("Reference contains the half of " + (cell.get * 2))
  6. }
  7. }

As can be seen in that example, it is not necessary to cast the valuereturned by the get method before using it as an integer. Itis also not possible to store anything but an integer in thatparticular cell, since it was declared as holding an integer.

Conclusion

This document gave a quick overview of the Scala language andpresented some basic examples. The interested reader can go on, for example, byreading the document Scala By Example, whichcontains much more advanced examples, and consult the Scala Language Specification when needed.

from: http://docs.scala-lang.org/tutorials/scala-for-java-programmers.html

 类似资料:

相关阅读

相关文章

相关问答