24 Mar 2019 | 22mins
This is the small Scala summary I have made while reading the language tutorial. My goal is to make this post as alive as possible, as I learn more things about the language. Syntax comparisons will be made mainly against ruby and javascript.
Use the following pages to help you through your reading: Scala-lang tutorial and Twitter Scala School.
they represent the simplest elements
The integer 1
1
The double 1.572
1.572
The boolean value “true”
true
The text(String) “Hello”
"Hello"
IMPORTANT! Strings on Scala are different from ruby, which accepts both ‘ and “. On Scala always use “” for strings.
1 + 2 = 3
1 and 2 are operands, ‘+’ is the operator
“Hello, “ ++ “Scala”
“Hello, “ and “Scala” are operands, ‘++’ is the operator
"hello".size
“hello” is the target object size is the method
Methods are applied on expressions using the dot notation. Method parameters are passed inside ().
Expressions: What code is when evaluated. Returns a value.
Statements: represent an action or command to be executed. (What code does)
Methods: operate inside an object Function: The rest
In fact, operators are methods with symbolic values or symbolic identifier.
3 + 2 == 3.+(2)
1.to(10) == 1 to 10
Each expression (or object) has a value and a type.
Naming things gives more readability to your program.
Use ‘Val’ to define constants and ‘Def’ to define methods/functions.
val = value
def = definition
Can have one or more parameters, separated with ‘,’, and with its respective types defined after a ‘:’. ex:
def square(x: Double) = x * x
def sumOfSquares(x: Double, y: Double) = square(x) + square(y)
If a method is recursive, i.e. call for itself, you need to specify its return type
def factorial(n: Int): Int =
if (n ==1) 0
else factorial(n - 1) * n
The substitution model is an expression evaluation scheme. Used when the expression can be reduced to a value(not a loop).
Call-by-Value
Call-by-Name
def abs(x: Double) = if (x >= 0) x else -x
true false // Constants
!b // Negation
b && b // Conjunction
b || b // Disjunction
e <= e, e >= e, e < e, e > e, e == e, e != e
Auxiliary functions are defined inside the parent function. Used to isolate a function from the user, just like private functions.
def sqrt(x: Double) = {
def sqrtIter(guess: Double, x: Double): Double =
if (isGoodEnough(guess, x)) guess
else sqrtIter(improve(guess, x), x)
def improve(guess: Double, x: Double) =
(guess + x / guess) / 2
def isGoodEnough(guess: Double, x: Double) =
abs(square(guess) - x) < 0.001
sqrtIter(1.0, x)
}
Definitions outside blocks are visible inside unless they are shadowed. (Would it be shadowed when redefined, right?)
refactoring sqrt method:
def sqrt(x: Double) = {
def sqrtIter(guess: Double): Double =
if (isGoodEnough(guess)) guess
else sqrtIter(improve(guess))
def improve(guess: Double) =
(guess + x / guess) / 2
def isGoodEnough(guess: Double) =
abs(square(guess) - x) < 0.001
sqrtIter(1.0)
}
In most cases semicolons are optional. Used when you have two statements in a single line. When you have 2 long expressions, you can overcome implicit ‘;’ by putting the expressions inside () or by putting the operator ‘+’ on the first line.
someLongExpression +
someOtherExpression
The Object MyExecutableProgram has nested definitions (vals, defs & methods) and is called top-level because is not nested within another definition.
Top-level definitions can be organized an imported as packages
// file foo/Baz.scala
package foo
object Baz {
Bar.someMethod
// Bar is visible because it is in the `foo` package too
}
When some definitions are not visible, you must use fully qualified names to refer them:
// file quux/Quux.scala
package quux
object Quux {
foo.Bar.someMethod
}
Here foo is a package(class?), Bar is an object and someMethod is a method
you can also import an object to not repeat them:
// file quux/Quux.scala
package quux
import foo.Bar
object Quux {
// Bar refers to the imported `foo.Bar`
Bar.someMethod
}
Any Scala application automatically imports the packages scala, java.lang and scala.Predef.
All executable applications should have a main method inside an object
object Hello {
def main(args: Array[String]) = println("hello world!")
}
and can be executed with the following code:
$ scala Hello
Maximum Common Divisor
def gcd(a: Int, b: Int): Int =
if (b == 0) a else gcd(b, a % b)
Factorial
def factorial(n: Int): Int =
if (n == 0) 1 else n * factorial(n - 1)
MCD oscillates and Factorial extends the number of elements on its expression.
Is when the last action of a method is to call itself. In case of the factorial method, it’s not tail recursive because after calling itself with factorial(n-1) it has to multiply to n.
“Both factorial and gcd only call itself but in general, of course, a function could call other functions. So the generalization of tail recursion is that, if the last action of a function consists of calling another function, maybe the same, maybe some other function, the stack frame could be reused for both functions. Such calls are called tail calls.”
What is the stack frame?
It’s possible to require that the following defined function is recursive using the @tailrec annotation. If the function were not tail recursive, it would prompt an error.
Tail recursive factorial:
def factorial(n: Int): Int = {
@tailrec
def iter(x: Int, result: Int): Int =
if (x == 1) result
else iter(x - 1, result * x)
iter(n,1)
}
Case class has a syntax close to a JS object
case class Note(
name: String,
duration: String,
octave: Int
)
we can create values(like objects or instances) of this type calling its constructor(Note(…))
val c3 = Note("C", "Quarter", 3)
and we can access any property by using the dot notation, just like Ruby or Javascript.
A type(here, Symbol) is something that can only be embodied by a fixed set of alternatives. We can express it using the sealed trait definition. For example, a musical symbol can be a note or a rest:
sealed trait Symbol
case class Note(...) extends Symbol
case class Rest(...) extends Symbol
Works as a case function. If the object matches a pattern(LHS) it executes an expression(RHS). The arrow symbol(=>) separates the pattern from the expressions.
Used to distinguish between the different cases of symbols.
def symbolDuration(symbol: Symbol): String =
symbol match {
case Note(name, duration, octave) => duration
case Rest(duration) => duration
}
Here, Note(…) and Rest(…) are constructor patterns and duration is called variable pattern.
When not all cases of a Symbol are handled, the compiler informs us.
When comparing instances of case classes you compare their values. This is different from Ruby classes, where each instance is different from others, even if its properties are equal.
Fix a set of alternatives to a property.
sealed trait NoteName
case object A extends NoteName
case object B extends NoteName
case object C extends NoteName
…
case object G extends NoteName
if a part of a program can be formulated in terms of an is relationship, you will express it as a sealed trait: “A symbol is either a note or a rest.”
if it can be formulated in terms of an has relationship, you will express it as a case class:
“A note has a name, a duration, and an octave number.”
case class != case object
A case class is used to define a class(name, properties, and parent(extends …)) and a case object is used to restrict the values of a property
On functional languages functions are treated as first-class values, meaning they can be passed as parameters and returned as a result. Those are named higher-order functions.
It’s possible to factor out various recursive higher-order functions. Examples:
Sum all integers between a and b ( b > a ):
def sumInts(a: Int, b: Int): Int =
if (a > b) 0 else a + sumInts(a + 1, b)
Sum the cubes of all the integers between a and b
def cube(x: Int): Int = x * x * x
def sumCubes(a: Int, b: Int): Int =
if (a > b) 0 else cube(a) + sumCubes(a + 1, b)
Sum the factorials of all the integers between a and b
def sumFactorials(a: Int, b: Int): Int =
if (a > b) 0 else factorial(a) + sumFactorials(a + 1, b)
The functions above can be summarized passing the respective function (id/cube/factorial) as a parameter to a higher order function (sum).
def sum(f: Int => Int, a: Int, b: Int): Int =
if (a > b) 0
else f(a) + sum(f, a + 1, b)
def id(x: Int): Int = x
def sumInts(a: Int, b: Int) = sum(id, a, b)
def sumCubes(a: Int, b: Int) = sum(cube, a, b)
def sumFactorials(a: Int, b: Int) = sum(factorial, a, b)
A => B
A argument
returns B
Function literal. i.e. don’t need to define a name for it
Anonymous cube function:
(x: int) => x * x * x
(…) is the parameter and x * x * x is the body.
The type of the parameter can be omitted. Multiple parameters are separated by commas.
Are functions defined as below: why they are used? I don’t know…
{ def funcName(x1: T1, …, xn: Tn) = e ; funcName }
The respective functions(id/cube) are not defined and passed as parameters anymore. They are passed as literals on the higher order function (sum).
def sumInts(a: Int, b: Int) = sum(x => x, a, b)
def sumCubes(a: Int, b: Int) = sum(x => x * x * x, a, b)
They seem like an array, but they are not.
They cannot be changed(immutable).
They are recursive.
They are homogeneous; All elements have the same type
Ex:
val fruit: List[String] = List("apples", "oranges", "pears")
val nums: List[Int] = List(1, 2, 3, 4)
val diag3: List[List[Int]] = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 1))
val empty: List[Nothing] = List()
All lists are constructed from:
We use the cons operator because its the only way to add an element to a list. There is no .push method.
val nums = 1 :: 2 :: 3 :: 4 :: Nil
val nums = Nil.::(4).::(3).::(2).::(1)
It’s possible to decompose lists with pattern matching:
nums match {
// Lists of `Int` that starts with `1` and then `2`
case 1 :: 2 :: xs => …
// Lists of length 1
case x :: Nil => …
// Same as `x :: Nil`
case List(x) => …
// The empty list, same as `Nil`
case List() =>
// A list that contains as only element another list that starts with `2`
case List(2 :: xs) => …
}
Insertion sort method:
val cond: (Int, Int) => Boolean = (x:Int,y:Int)=>x<y
def insert(x: Int, xs: List[Int]): List[Int] =
xs match {
case List() => x :: Nil
case y :: ys =>
if (cond(x, y)) x :: y :: ys
else y :: insert(x, ys)
}
insert(2, 1 :: 3 :: Nil) shouldBe (1 :: 2 :: 3 :: Nil)
insert(1, 2 :: 3 :: Nil) shouldBe (1 :: 2 :: 3 :: Nil)
insert(3, 1 :: 2 :: Nil) shouldBe (1 :: 2 :: 3 :: Nil)
This method is interesting because the condition (cond boolean) to place an element is an anonymous function, and its body is another anonymous function (boolean). So we have to define its parameters x,y and integers and also its body as x<y. The (Int,Int) part do not define it, its only the cond type.
.map(anonymous func)
.filter(anonymous func)
.flatMap{anonymous func}
Use options when you don’t have a defined return type. Partially defined functions:
def sqrt(x: Double): Option[Double] =
if (x < 0) None else Some(…)
Use pattern matching to decompose options:
def foo(x: Double): String =
sqrt(x) match {
case None => "no result"
case Some(y) => y.toString
}
.map(=>)
.filter(=>)
.flatMap(=>)
Try[A], represents a computation that attempted to return an A. Can be Success[A] or Failure;
None x Failure: Failure provide a reason of the failure.
Try[A] also have map, filter ,and flatMap
Either[A,B], represents a value that can either be type A or type B, and can be decomposed in two cases: Left and Right.
def sqrt(x: Double): Either[String, Double] =
if (x < 0) Left("x must be positive")
else Right(…)
Either[A,B] have map and flatMap, but it transforms the Right case only. “right biased”.
Either[A,B] has a filterOrElse method, it transforms a Right value into Left value if it does not satisfy a given predicate.
Prior to Scala 2.12, Either was unbiased, so you had to specify the side you wanted to transform.
def greet(name: String): String =
s"Hello, $name!"
greet("Scala") shouldBe "Hello, Scala!"
do not forget to prefix the string literal with “s”!!!
If you want to splice a complex expression:
def greet(name: String): String =
s"Hello, ${name.toUpperCase}!"
greet("Scala") shouldBe "Hello, SCALA!"
IMPORTANT! Strings on Scala are different from ruby, which accepts both ‘’ and “”. On Scala always use “” for strings.
Work as a conjunction of elements, and they can have different types. More generally, a type (T1, …, Tn) is a tuple type of n elements whose ith element has type Ti.
You can retrieve the elements of a tuple using tuple pattern, or you can identify the member by its position (_i)
val is: (Int, String) = (42, "foo")
is._1 shouldBe 42
is._2 shouldBe "foo"
Functions are objects with apply methods.
package scala
trait Function1[A, B] {
def apply(x: A): B
}
{
class AnonFun extends Function1[Int, Int] {
def apply(x: Int) = x * x
}
new AnonFun
}
f(a,b) = f.apply(a, b)
Map
/* xs.map(x => x + 1) */
for (x <- xs) yield x + 1
Filter
/* xs.filter(x => x % 2 == 0) */
for (x <- xs if x % 2 == 0) yield x
flatMap
/* xs.flatMap(x => ys.map(y => (x, y))) */
for (x <- xs; y <- ys) yield (x, y)
Range(1, 10, 2)
Can be rewritten:
case class Range(start: Int, end: Int, step: Int)
Range(start = 1, end = 10, step = 2)
It’s to set default values when defining the constructor, so the parameter can be omitted.
case class Range(start: Int, end: Int, step: Int = 1)
with an undefined number of parameters:
def average(x: Int, xs: Int*): Double =
(x :: xs.to[List]).sum.toDouble / (xs.size + 1)
type Result = Either[String, (Int, Int)]
def divide(dividend: Int, divisor: Int): Result =
if (divisor == 0) Left("Division by zero")
else Right((dividend / divisor, dividend % divisor))
divide(6, 4) shouldBe Right((1, 2))
divide(2, 0) shouldBe Left("Division by zero")
A class defines a new constructor and a new type.
Objects are the elements of a class type. Use the operator ‘new’ to create an object.
Just like a property of a Ruby object, you can get the member of an object with the infix operator ‘.’
A package of functions operating on a data abstraction in the data abstraction itself. i.e. defined inside the class definition.
‘this’ represents the object on which the current method is executed.
require is used to enforce a precondition on the caller of a function.
require function:
class Rational(x: Int, y: Int) {
require(y > 0, "denominator must be positive")
...
}
assert is used to check the code of the function itself.
val x = sqrt(y)
assert(x >= 0)
As said before, operators are methods with symbolic identifiers. So you can override the operators for some types:
class Rational(x: Int, y: Int) {
private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
private val g = gcd(x, y)
def numer = x / g
def denom = y / g
def + (r: Rational) =
new Rational(
numer * r.denom + r.numer * denom,
denom * r.denom
)
def - (r: Rational) = ...
def * (r: Rational) = ...
...
}
Increasing priority:
(all letters)
|
^
&
< >
= !
:
+ -
* / %
(all other special characters)
No instances of an abstract class can be created with the operator new.
subclasses extends superclass:
class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet {
NonEmpty is a subclass and IntSet is its superclass. IntSet is a subclass of Object.
abstract class Base {
def foo = 1
def bar: Int
}
class Sub extends Base {
override def foo = 2
def bar = 3
}
When you need a single instance of a class (Empty for IntSet), you can create a singleton object named Empty. No other instances of it can be created. This type of object are values.
object Empty extends IntSet {
def contains(x: Int): Boolean = false
def incl(x: Int): IntSet = new NonEmpty(x, Empty, Empty)
}
use when you want to inherit code from more than one superclass. Defined just like an abstract class.
trait Planar {
def height: Int
def width: Int
def surface = height * width
}
OBS: classes, objects, and traits can inherit from one class, but from many traits:
class Square extends Shape with Planar with Movable …
The substitution model of computation cannot be used. The state of an object depend on its history. Use ‘var’ to create a variable definition. Use ‘=’ to assign a new value to a variable.
Stateful objects Objects with state have some variable members. Ex: a Bank Account
After a sequence of testing, and the results are the same, there is an operational equivalence.
while loops:
def power(x: Double, exp: Int): Double = {
var r = 1.0
var i = exp
while (i > 0) { r = r * x; i = i - 1 }
r
}
for-loops:
for (i <- 1 until 3) { System.out.print(i + " ") }
Creating instances:
class: require the keyword new
case class: constructor parameters are promoted to members
val aliceAccount = new BankAccount
val c3 = Note("C", "Quarter", 3)
The same definition of BankAccount(class) create different objects (the notion of identity), and the same definition of Note(case class) lead to equal values.
Pattern matching does not work in regular classes.
A case class cannot extend another case class.
A case class is a special case of a class, with predefined overriding on some properties(parameters are promoted to members; Equality redefinition; Java hashCode redefinition; toString redefinition; Create a copy of a case class; Constructor that allows the omission of the new keyword; Extractor for pattern matching).
Used in order to write generic code (for values of different types) without compromising static typing richness.
Upper Bounds
A <: B means: A is a subtype of B, and
A >: B means: A is a supertype of B, or B is a subtype of A.
Lower Bounds
A >: Reptile
Streams are similar to lists, but their tail is evaluated only in demand.
val xs = Stream.cons(1, Stream.cons(2, Stream.empty))
def streamRange(lo: Int, hi: Int): Stream[Int] =
if (lo >= hi) Stream.empty
else Stream.cons(lo, streamRange(lo + 1, hi))
It returns a single Stream type object with start as a head element. The others are only computed when needed.(tail called on stream)
:: for lists
::# for streams
on Stream cons the second parameter(tl) is evaluated by call-by-name rules(=>)
The other methods are the same as List (filter, map, flatMap)
if the tail is called many times, it will be recomputed each time. To avoid this we must store the result of the first evaluation of the tail.
lazy val x = expr
Using a lazy value for the tail, Stream.cons can be implemented more efficiently:
def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] {
def head = hd
lazy val tail = tl
…
}
val builder = new StringBuilder
val x = { builder += 'x'; 1 }
lazy val y = { builder += 'y'; 2 }
def z = { builder += 'z'; 3 }
z + y + x + z + y + x
builder.result() shouldBe "xzyz"
How to use insertionSort for element types other than Int? ‘<’ operator not defined for arbitrary types…
Parametrize lessThan function (comparison operation)
But there is a class in the standard library that represents orderings:
scala.math.Ordering
We can define insertionSort with ordering, but it is too repetitive.
Use Implicit Parameters
def insertionSort[T](xs: List[T])(implicit ord: Ordering[T]): List[T] = {
def insert(y: T, ys: List[T]): List[T] =
… if (ord.lt(y, z)) …
… insert(y, insertionSort(ys)) …
}