Core Features of Scala ====================== This document attempts to be an "executive summary" of important Scala features. It assumes you've had exposure to other OOP languages. Program Structure ----------------- + Scala programs consist of "objects," "classes," and "traits." Each entity resides in a file with the same name as the entity it contains. For example object Hello would be stored in a file Hello.scala. Classes are like classes in other languages (Java, C++). With a class you can create as many instances of the class as you like using "new." In contrast objects are "singletons." With an object there is only one instance and it has the name you gave it. The type of an object is anonymous yet objects, like any class instance, can have methods, etc. + Execution of the program begins in the "main" method of some singleton object. You specify the object containing the main method when you run your program. Example: object Greetings { def main(args: Array[String]): Unit = { // The main method must be defined like this. println("Hello, World!") } } This would be stored in Greetings.scala (note capital 'G' just like the object's name). Compile it: $ scalac Greetings.scala Run it: $ scala Greetings Hello, World! + Like Java, Scala programs are divided into packages. Entities in different packages can have the same name without conflict (like files in different folders can have the same name). Putting things into the "default" package is discouraged. Thus package edu.vtc.cis3030 object Greetings { ... } // As before. Compile it: $ scalac -d . Greetings.scala Run it: $ scala edu.vtc.cis3030.Greetings It is necessary to fully qualify the name of the main object when you run the program. Otherwise the JVM won't know in which package to look for it. Vals, Vars, and Immutability ---------------------------- + Immutable data is data that can't be changed once it is initialized (constructed). Generally you should use immutable data as much as you can and resort to mutable (changeable) data only when necessary. + In Scala a "val" creates an association or "binding" between a name and an object. That binding can't be changed once it is created. Thus vals are immutable. val x = 42 x = 21 // Error: "reassignment to val." + A "var" also creates a binding between a name and an object. However, vars are mutable so the binding can be changed later if desired. You should avoid vars if possible; their unrestricted use makes programs harder to understand and thus buggier. It is also more difficult for compilers to optimize or parallelize programs that contain lots of mutable data. var x = 42 x = 21 // Okay. + Objects are mutable or immutable depending on their type. - Strings are immutable (as in Java). Any method that "changes" a string really returns a new string with the new value. - Arrays are mutable. In effect every array element is a var. val myArray = Array(10, 11, 12) // Use Array constructor. myArray(0) = 1 This changes the object referenced by myArray to be Array(1, 11, 12). Notice that arrays are accessed with round parenthesis, not square brackets. Notice that array indicies start at zero (as with Java and C). Finally notice that even though myArray is a val (the name "myArray" always refers to the same object), the array itself can be changed because arrays are mutable. - Lists are immutable. val myList = List(10, 11, 12) myList(0) = 1 // Error: no method "update" in class List. - Tuples are immutable. val myTuple = (1, "Hello", 2.0) myTuple._1 = 0 // Error: reassignment to val. This attempts to change the first element of the tuple from 1 to 0 but it fails. In effect, tuple elements are vals. When you create your own classes you can decide for yourself if they are mutable or immutable; it depends on how you write the methods. You should favor immutabilty. Syntax Features --------------- + Everything is an expression that evaluates to a value. You can execute a sequence of expressions by enclosing them in braces and separating them with semi-colons. val x = 1 + { a - b; a + b } This evaluates the expressions a - b and a + b (in that order). It throws away the result of a - b (making it a waste of time to evaluate it since that expression has no side effects). The result of a brace enclosed sequence is the last expression evaluated. val x = 1 + { println("Hi"); a + b } In this case the first expression in the sequence has a side effect (it prints something) and so evaluating it isn't a waste of time. As before the sequence returns the last expression evaluated (a + b in this case). val x = 1 + { println("Hi"); a + b } This is the same, just formatted differently. + Scala has "semi-colon inference" that allows you to not bother with the separating semi-colons in most cases. val x = 1 + { println("He") a + b } This is the same as earlier (note missing semi-colon). + The body of a method is a single expression, the result of which is the value returned by the method. In many cases a brace enclosed sequence of expressions is used instead. def sum(a: Int, b: Int) = a + b // One expression defines the method body def noisySum(a: Int, b: Int) = { println("I'm in method noisySum") val answer = a + b answer // Last expression evaluated is result of brace enclosed sequence. } The body of noisySum is still just one expression (that happens to be a brace enclosed sequence). + Braces also define a scope in a manner similar to that of other programming languages. val result = 1 + { val temp1 = 42 val temp2 = temp1 + 24 2*temp2 } Here the vals temp1 and temp2 only exist in the scope delimited by the braces. The fact that this scope is part of some larger expression doesn't change that. Outside the braces temp1 and temp2 do not exist. You can, in fact, normally nest entities arbitrarily in Scala. For example: val result = 1 + { def sum(x: Int, y: Int): Int = x + y class Helper { def someMethod(x: Int) = x + 1 } val myHelper = new Helper sum(1, myHelper.someMethod(2)) } Can you tell what result evalutes to in the expression above? + If a method takes only one parameter when you call the method you can optionally leave off the dot and the parentheses around the argument. val c = new Car c.turnLeft(20.0) // Turns the car left by 20 degrees as a side effect. c turnLeft 20.0 // Same as above. Such methods look like infix operators. + Scala allows you to define methods with operator-like names. All the normal operators that you use are defined as methods in this way. val a = 1 val b = 2 val c = a + b // Really: c = a.+(b); calls "+" method in class Int. + You can, in effect, overload operators or define new operators. class Complex { def +(that: Complex) = ... // The name "that" is traditional. Any name is fine. } val c1 = new Complex(...) val c2 = new Complex(...) val c3 = c1 + c2 // Really c3 = c1.+(c2) using your "+" method. Type Unit --------- + IMPORTANT: A type is a set of values and a set of operations on those values. + Most types are inhabited by many values. The type Int, for example, contains 2^32 values (32 bit integers). The type String has infinitely many values (is that really true?). + Scala has a special type "Unit" that has only one value called "the unit value." In programs that value is spelled "()." [Compare: One of the 2^32 values of Int is spelled "42."] val x = () // x bound to the unit value; infered to have type Unit. + The type Unit is used in cases where no value is expected. Since there is only one value in the Unit type, it conveys no information and so works well when nothing is expected. Scala has no type void in the Java sense. Java's void is a type with no values at all but it has special properties making it inconvenient. + Methods that return Unit must have side effects or else they wouldn't do anything useful. def sum(a: Int, b: Int) = a + b // Results in Int, also no side effects def greet(name: String) = println("Hello, " + name) // Results in Unit, but has side effect of outputing a string. (println returns Unit) Conditional Expressions ----------------------- + In Scala there are no statements only expressions. For example "if expressions" evaluate to one value or another depending on a condition. val answer = if (condition) 1 else 0 Here "answer" is bound to the value 1 if the condition is true, and zero otherwise. As always is is permitted to replace the expressions in either branch of the if with a brace enclosed sequence of expressions. val answer = if (condition) { println("Condition is true") 1 } else { println("Condition is false") 0 } + It is permitted (and common) to ignore the result of an if expression. The creates something that looks like an if statement in other languages. def someMethod(n: Int) = { if (condition) // Result of if expression is ignored. println("Condition is true") else println("Condition is false") 1 // Silly method always results in 1. } + When the types of the two expressions in an if expression differ, Scala finds the "least upper bound type" in the type hierarchy. This is the most specific type that describes both branches of the if. class Animal class Cat extends Animal class Dog extends Animal val creature = if (condition) new Cat else new Dog The compiler infers the type Animal for creature. + An if expression without an else clause behaves as if it had an "else ()" attached (that is, an else clause that evalutes to the Unit value). + If expressions let you use vals when you might think you have to use vars. var x = 0 if (condition) { x = 1 } The above is in a Java-like style. In Scala you would more naturally write val x = if (condition) 1 else 0 Notice the use of a val instead of a var. Loops ----- + In Scala "for" loops are technically not loops and thus are sometimes called "for comprehensions." They actually use functional programming techniques under the hood. We'll talk about this later. + The basic for loop consists of a "generator" expression that evaluates to an object with suitable methods that allow extraction of component objects one at a time. Each component object is bound to a name also specified in the loop. val myArray = Array(1, 3, 5) for (i <- myArray) println(i) This binds i to each component object in myArray and then evalutes the expression forming the loop body. Here "myArray" is the generator expression. For loops written in this way are expressions returning Unit. + When a "yield" is used the for loop returns a useful value, the type of which depends on the type of the generator. val myArray = Array(1, 3, 5) val result = for (i <- myArray) yield 2*i This returns an Array[Int] containing 2, 6, and 10. It returns an array and not some other container because the generator is an array. + There are helpful methods in class Int (really RichInt to which Ints can be implicity converted) that make it possible to use a for comprehension like a for loop in other languages. for (i <- 1 to 10) println(i) // Prints 1 .. 10. for (i <- 1 to 10 by 2) println(i) // Prints 1, 3, 5, 7, 9 for (i <- 1 until 10) println(i) // Prints 1 .. 9. These methods are really just normal methods and can be used in other contexts val range1 = 1 to 10 // Really 1.to(10) val range2 = range1 by 2 // Really range1.by(2) or (1.to(10)).by(2) The type returned by "1 to 10" is a special Range class that has the right methods to be used as a generator expression. + While loops have syntax like that of C and Java. However, to work the condition must mutate as the loop executes. Thus vars are needed. While loops execute far more quickly than for comprehensions, however. The optimization of for comprehensions is an outstanding issue in the Scala community. Type Inference -------------- + Scala is statically typed, like Java, yet you often don't have to provide "type annotations." val x = 1 // The type of x is Int; this is deduced ("inferred") by the compiler. + Scala does require the types of method parameters be annotated. def sum(a: Int, b: Int) = x + y // The parameters have types mentioned. + The result type of a non-recursive method can be inferred. See the example of "sum" above. This also works: def sum(a: Int, b: Int): Int = x + y // Result type explicity given. The compiler checks that the result type agrees with the type of the expression used as the method body. If it does not a type error is produced by the compiler. + Recursive methods require their result types be annotated. The compiler will tell you if you forget. def factorial(n: Int) = if (n == 0) 1 else n * factorial(n - 1) "Error: recursive method factorial needs result type." def factorial(n: Int): Int = if (n == 0) 1 else n * factorial(n - 1) Class Definitions ----------------- + Classes in scala are similar to classes in Java. + Each class can have a "primary" constructor consisting of the body of the class definition. class Cat { println("Code that 'floats' inside class body is primary constructor code") // Methods can be defined in the expected way. def meow(loudness: Double) = ... } + Parameters to the primary constructor are syntactically class parameters. class Cat(weight: Double) { ... } Here 'weight' can be used inside the constructor or other methods directly. It is provided to the instance when the class is instantiated: new Cat(8.0) + Class parameters can be marked as vals (or vars) in which case they are publically visible without explict getter and setter methods. class Cat(val weight: Double) { ... } It is okay to say c.weight where c is a Cat instance. The class parameter is like a public field in this case. It is also permitted to mark class parameters as private like this: class Cat(private val weight: Double) { ... } This is useful if you want the parameter to be like a private field and thus visible to the methods, while still not being visible external to the class. + Auxillary constructions can also be defined using the special word "this." These constructions must invoke another constructor as their first action. class Cat(val weight: Double) { // Auxillary constructor. def this(baseWeight: Double, expansionFactor: Int) { this(expansionFactor * baseWeight) // Call primary constructor. // Possibly do other things. } def... // other methods } As a result of these rules the primary constructor is always invoked first. Auxillary constructors "add value" to the primary constructor by doing additional operations. + It is permitted to define a class without any methods or fields. In that case you can leave off the braces. class Cat(val weight: Double, val numberOfTeeth: Int) Nothing more is needed if you are content with wrapping a few values in an object. + When instantiating a class the parentheses are not needed if there are no parameters to the constructor. class Cat { ... } val x = new Cat These syntactic abbreviations are surprisingly convenient. + Fields and methods can be private. Such fields and methods can only be used inside the class. class Cat { private def warmUpVocalCords = ... // Can only be used inside the class. private def makeSound(loudness: Double) = ... // Can only be used inside the class. def meow(loudness: Double) = { warmUpVocalCords // Call to "no parens" method does not require parentheses. makeSound(loudness) } } Good software engineering principles state that data should have as restricted a visibility as feasible. Scala defaults to public access which violates this principle but is often convenient. Don't be afraid to use private fields and methods, however. Keeping things as private as possible is often the "right" way to do it. + Scala classes do not have static fields but Scala does allow you to create a "companion object" for a class. The companion object has the same name as the class and contains methods that can be used without making a class instance (essentially, Java-style static methods become methods of the companion object in Scala). // Class definition (also called the "companion class" of the object below)... class Cat { ... } // Companion object... object Cat { def speciesName = "Felis Catus" // Type inference deduces result type of String. } Methods of the companion object can be invoked using a syntax you might expect: println(Cat.speciesName) Here 'Cat' refers to the (companion) object and not, technically, to the class. Normally a class and its companion object are defined in the same source file; the only exception to the rule of creating a separate source file for each class, object, or trait. Notice that methods without parameters can be defined without any parentheses at all. Such "no parens" methods are traditionally created for parameterless methods without side effects. Explicit empty parentheses are traditionally used for parameterless methods with side effects.