Scala basic I

от переменных до ООП

Дмитрий Быков

Пара общих слов о Scala

Scala — мультипарадигменный язык программирования (сочетает ФП и ООП)

Scala — строго типизированный язык программирования

Scala работает на JVM

Hello, world!

Точкой входа в программу считается функция main расположенная в любом объекте.

У программы может быть множество точек входа.


object Test {
    def main(args: Array[String]): Unit = {
        println("Hello, world!")
    }
}
                

При наследовании от App, тело объекта считается телом функции main.


object Test extends App {
    println("Hello, world!")
}
                

Выражения, значения и переменные

Выражение - это вычислимое утверждение:
42 + 42
Результат выражения можно положить в изменяемую переменную:
var myVar = 42 + 42
myVar = 10 // Ok

Или в неизменяемое значение:

val myVal = 42 + 42
myVal = 10 // Error

В Scala принято использовать неизменяемые значения

Базовые типы

Базовые типы

val bool: Boolean = true
val int: Int = 42
val char: Char = 'a'
val byte: Byte = 127            // signed
val short: Short = 255          // signed
val long: Long = 32
val float: Float = 2
val double: Double = 13.0
val string: String = "Scala"    // String не базовый тип
val unit: Unit = ()

Для значений можно, но не обязательно, явно указывать тип

val a: Int = 42
val b = 42

Для задания типов могут использоваться специальные литералы:

val hex: Int = 0xff
val long = 42L
val float = 42F
val double = 42D

Операторы


+    -     *     /    %                                         // арифметические операторы
==   !=    >     <    >=                                        // операторы сравнения
&&   ||    !                                                    // логические операторы
&    |     ^     ~    <<    >>    >>>                           // битовые операторы
=    +=    -=    *=    /=    %=    <<=    >>=    &=    ^=    |= // операторы присвоения
()   []    ,     ;                                              // прочие
                

Приоритет операторов (от высшего к низшему):

lazy. Ленивые значения

Ключевое слово lazy говорит о том, что значение будет вычислено

только при первом обращении к значению


def strWithLog(str: String): String = {
    println(s"Calculated $str")
    str
}

lazy val itWillNotShow = strWithLog("lazy")

val itWillShow = strWithLog("not lazy")
// сейчас в консоль вывелось Calculated not lazy

itWillNotShow + itWillShow
// после этой команды в консоль выводиться Calculated lazy
                

Управляющие конструкции

Управляющие конструкции

if/else

if (false) {
    println("yes")
} else if (false) {
    println("yes yes")
} else {
    println("no")
}

// в случае, когда в теле if/else всего одна команда, фигурные скобки можно не писать
if (true) println("yes")

if (true)
    println("yes")
else
    println("no")
                

if/else - это вычисляемое выражение, следовательно его результат можно присвоить значению


val value = if (false) "true" else "false"
                

Управляющие конструкции

pattern-matching

val nameNumber = 2

val name = nameNumber match {
    case 1 => "Alex"
    case 2 => "Max"
    case 3 => "Fred"
    case _ => "Unknown"
} // out: Max

// проверка регулярных выражений
val SeriesReg = "([0-9]{4})".r

val series = "1234" match {
    case SeriesReg(series) => series
    case _ => "No"
} // out: 1234
                

Управляющие конструкции

pattern-matching


// извлечение данных из структуры
case class User(id: String, name: String, age: Int)

val userString = User("1234", "Fred", 21) match {
    case User(_, name, age) => s"$name is $age years old"
} // out: Fred is 21 years old

val li = List(1, 2, 3, 4) match {
    case head :: tail  => s"first element is $head; others is $tail"
    case Nil => "empty list"
} // out: first element is 1; others is List(2, 3, 4)

// использование "гардов" (guards)
val str = 14 match {
    case x if x % 2 == 0 => "Even"
    case _ => "Odd"
} // out: Even
                

Управляющие конструкции

pattern-matching

// проверка принадлежности к типу
sealed trait Animal
case class Cat(name: String, age: Int) extends Animal
case class Dog(name: String) extends Animal
case class Bird(name: String, flyHeight: Double) extends Animal

val animal: Animal = Dog("Rex")

animal match {
    case Cat(name, age) => println(s"Cat with name = $name and age = $age")
    case Dog(name) => println(s"Dog with name = $name")
    case Bird(name, flyHeight) => println(s"Bird with name = $name and flyHeight = $flyHeight")
}

animal match {
    case _: Cat => println("Just cat")
    case _: Dog => println("Just dog")
    case _: Bird => println("Just bird")
}
                

Управляющие конструкции

while/do-while

// while
var i = 0
while (i < 5) {
    print(s"$i; ")
    i += 1
}

// do-while
var j = 0
do {
    println(s"$j; ")
    j += 1
} while (j < 5)
                

Управляющие конструкции

for

val li = List("a", "b", "c")
val ma = Map(("Max", 18), ("Alex", 24))

// перебор элементов коллекции
for (elem <- li) println(elem)
for ((name, age) <- ma) println(s"$name is $age years old")

// генератор последовательностей
for (i <- 1 to 3) println(i)        // от 1 до 3 включительно
for (i <- 1 until  3) println(i)    // от 1 до 3, исключая 3

for (i <- Range.inclusive(1, 3)) println(i)           // от 1 до 3 включительно
for (i <- Range(1, 3)) println(i)                     // от 1 до 3, исключая 3
for (i <- Range(1, 10, 3)) print(s"$i ")              // Range с шагом ; out: 1 4 7

for {
    i <- 1 to 3
    k <- 1 until 3
} print(s"$i : $k | ")   // out: 1 : 1 | 1 : 2 | 2 : 1 | 2 : 2 | 3 : 1 | 3 : 2 |
                

Управляющие конструкции

for

// for - это тоже выражение. Во всех случаях выше for возвращает Unit.
// Чтобы for начал возвращать значения необходимо использовать ключевое слово yield

val unit: Unit      = for (i <- List(1, 2)) i + 2          // out: Unit
val list: List[Int] = for (i <- List(1, 2)) yield i + 2    // out: List(3, 4)

// конструкция for-yield называется for-comprehension

val users = Map(
    "Max" -> 13,
    "Alex" -> 21,
    "Fred" -> 41
)
// использование "гардов" (guards)
val filtered = for {
    (name, age) <- users
    if age >= 18
    str = s"$name, you are too old"
} yield str
println(filtered) // out: List(Alex, you are too old, Fred, you are too old)
                

Функции

Функции

Функции-значения

// анонимная функция
(x: Int) => x.toString

// функцию можно присвоить значению
val myFunction = (x: Int) => {
    val x1 = x + 1
    val x2 = x1 + 1
    x2 + 1          // последнее вычисляемое выражение в функции является возвращаемым значением функции
}
println(myFunction(3))  // out: 6

// множество параметров
val myBigFunction = (x: Int, y: Int, name: String) => s"$name ${x + y}"
println(myBigFunction(3, 3, "Max"))   // out: Max 6

// если задать тип значению, то у параметров тип можно не указывать
val myTypedFunction: (Int, String) => String =
    (x, y) => s"$y $x"
val myTypedFunction2: Function2[Int, String, String] =
    (x, y) => s"$y $x"

// тип (Int, String) => String - это синтаксический сахар для типа Function2[Int, String, String]
// максимум функция может быть от 22х параметров
                

Функции

Функции-методы

def doubleSum(x: Int, y: Int): Int = {
    val z = x + y
    z * 2
}

def plus(x: Int, y: Int): Int = x + y

// для методов можно задать значения по умолчанию
def method(x: Int = 20, y: Int = 20): Int = x + y
println(method())        // out: 40
println(method(30))      // out: 50
println(method(30, 30))  // out: 60

// передача в функцию именованных параметров
def wordFrom(w1: String = "one",
             w2: String = "two",
             w3: String = "three",
             w4: String = "four") = s"$w1 $w2 $w3 $w4"

println(wordFrom(w2 = "2", w4 = "4"))
// out: "one 2 three 4"
                

Функции

Функции высшего порядка

// функция, принимающая другую функцию в качестве параметра - функция высшего порядка
def sumWithExtractor(extractor: String => Int, key: String, x: Int): Int =
    extractor(key) + x

val ext: String => Int = str => str.length
def ext2(s: String): Int = s.length

println(sumWithExtractor(ext, "000", 1))  // out: 4
println(sumWithExtractor(ext2, "000", 1))  // out: 4

// функция так же может возвращать другую функцию
def functionMethod(s: String): Int => Int =
    x => s.length + x

println(functionMethod("000")(1))  // out: 4
                

Функции

Множественные списки параметров и каррирование

// функция с двумя списками параметров
def method(x: Int, s: String = "some")(y: Int, s2: String): String = (x + y).toString + s + s2

// каррированные функции
val a = method _                // (Int, String) => (Int, String) => String
val b = method(1, "a") _        // (Int, String) => String
val c = method(1, "a")(12, _)   // String => String

println(a(1, "a")(12, "b"))  // out: 13ab
println(b(12, "b"))          // out: 13ab
println(c("b"))              // out: 13ab

// зачем ещё могут быть нужны функции с множественным списком аргументов
def syntax(i: Int)(func: String => String): String = func((i + 1).toString)

val res = syntax(10) { x =>
    s"| $x |"
}

println(res)  // out: | 11 |
                

Функции

call-by-name / call-by-value

// возвращаемся к теме ленивых вычислений
// обратите внимание на типы byValue и byName
def callByValue(byValue: String, flag: Boolean): String = if (flag) byValue else "No"
def callByName(byName: => String, flag: Boolean): String = if (flag) byName else "No"

def calculation(s: String): String = {
    println(s"Calculating $s")
    s
}

callByValue(calculation("byValue true"), true)    // out: Calculating byValue true
callByValue(calculation("byValue false"), false)  // out: Calculating byValue false
callByName(calculation("byName true"), true)      // out: Calculating byName true
callByName(calculation("byName false"), false)    // ...

// call by name функция-значение
val callByName: (=> String, Boolean) => String = (byName, flag) => if (flag) byName else "No"
                

Функции и методы

Рекурсия

def fibOld(n: Int): List[Int] = {
    var res = List.empty[Int]
    var i = n
    while (i >= 1) {
        res match {
            case Nil => res = List(1)
            case _ :: Nil => res = List(1, 1)
            case first :: second :: _ => res = first + second :: res
        }
        i -= 1
    }
    res
}

// функции помеченные аннотацией tailrec обязаны быть функциями с хвостовой рекурсией
@tailrec
def fib(n: Int, acc: List[Int]): List[Int] =
    if (n < 1) acc
    else acc match {
        case Nil => fib(n - 1, List(1))
        case _ :: Nil => fib(n - 1, List(1, 1))
        case first :: second :: _ => fib(n - 1, first + second :: acc)
    }

println(fibOld(10))    // out: List(55, 34, 21, 13, 8, 5, 3, 2, 1, 1)
println(fib(10, Nil))  // out: List(55, 34, 21, 13, 8, 5, 3, 2, 1, 1)
                

Иерархия типов

Система типов

Преобразование типов

Для явного преобразования типов используется специальные функции, объявленные на самих типах:

val short = 42.toShort
val int = "42".toInt

Система типов

Any

Any - надтип всех типов в языке. Supertype


// компилятор выведет тип Any как общий тип между Int и String
val any: Any =
    if (true)
        12
    else
        "string"

// каждый значение в языке может быть приведено к Any
val any0: Any = 12
val any1: Any = "String"
val any2: Any = 12.0
val any3: Any = Right(Some(Left(Some(None)))) // изначально это тип Either[Option[Either[Option[Option[]]]]]
val any4: Any = (x: Int) => x.toString
                

Система типов

AnyVal

AnyVal - тип оберток над значениями

Компилятор с такими типами обходится по особенному - стремится оптимизировать так, чтобы обертка не создавала реальный объект в памяти

Использование:

примитивы - Int, Long, Double, и т.д.

Обертки позволяют добавить методы или дополнительные типы


// Помогает работать с примитивными типами как с чем-то осмысленным
case class Age(val value: Int) extends AnyVal
def isOlderThen18(age: Age): Boolean = age.value >= 18

// добавление операций
class Meter(val value: Double) extends AnyVal {
    def +(m: Meter): Meter = new Meter(value + m.value)
}
new Meter(3.4) + new Meter(4.3) // после компиляции здесь просто будут операции над Double
                

Система типов

Unit

Тип с единственным значением "()"

Используется в местах где в C#/Java стоял бы "void"

Обычно связано с сайд-эффектами (печать в консоль, запись в
базу, мутацию какого-то внешнего объекта, и т.д.)

Значения "()" не существует во время исполнения (абстракция на уровне компилятора)


def getUnit: Unit = ()
val unit: Unit = getUnit
                

Система типов

AnyRef

AnyRef - надтип для всех ссылочных типов.

В jvm - соответствует java.lang.Object, т.е. объектам в heap

Использование:

В практике почти не используется, но нужен для полноты системы типов

Система типов

Null

Null - подтип для всех подтипов AnyRef.

Т.е. для любого X <: AnyRef справедливо Null <: X.

Это делается автоматически на уровне компилятора.

Литерал null имеет этот тип.

На место любого ссылочного типа можно подставить null.

Также это позволяет работать системе вывода типов при проставлении null.


val myNull: Null = null

val string: String = if (randomBoolean) {
    "Hello world" // String
} else {
    null // Null
}
                

Система типов

Nothing

Nothing - подтип всех типов, также называемый bottom type.

Т.е. для любого X справедливо Nothing <: X.

В Scala нет значений с типом Nothing.

Нужен для полноты системы типов.

Выражение в результате которого выбрасывается exception имеет тип Nothing.


val int: Int = if (randomBoolean) {
    42 // Int
} else {
    ??? // Nothing (??? - функция, которая при вызове кидает NotImplementedException)
}
                

Система типов

ООП

ООП

Классы

Класс описывает с помощью ключевого слова class. Для классов всегда определён конструктор по умолчанию, который сохраняет параметры, указанные после имени класса, в приватные поля класса.
Чтобы поле стало публичным, перед именем параметра необходимо указать val или var.


class Person(name: String, val age: Int) {
    // внутри класс можно определять приватные и публичные поля
    val address = "some address"
    private val workingHours = 21
    // приватные и публичные функции
    def sayHelloTo(to: String): String = s"Hello, $to"
    private def thingAbout(thought: String): Unit = println(s"$name think about $thought")

    // функции, вызываемые в теле класса, будут вызываться при каждой инициализации
    println("Create Fred")
}

val fred = new Person("Fred", 21)
// println(fred.name) ошибка, так как name - приватное поле класса
println(fred.age)
fred.sayHelloTo("Max")
                

ООП

Вспомогательные конструкторы

Для каждого класса можно (но не стоит) объявлять вспомогательные конструкторы (auxiliary constructors).


class Person(private var firstName: String, private var secondName: String) {
    // для конструктора не указывается тип и не ставится знак =
    def this(fullName: String) {
        // обязательно в самой первой команде использовать конструктор по умолчанию
        this("", "")
        this.firstName = fullName.split(" ").head
        this.secondName = fullName.split(" ").last
    }

    def printName: Unit = println(s"$firstName $secondName")
}
val fred = new Person("Fred Black")
fred.printName // out: Fred Black

// конструктор по умолчанию можно сделать приватным
class AnotherPerson private (firstName: String, secondName: String) {}
val error = new AnotherPerson("Name", "SecondName")
                

ООП

case class

// параметры, передаваемые для создания, в отличии от случая с обычным классом
// автоматически являются публичными
case class Person(fistName: String, secondName: String) {
    def sayName(): Unit = println(s"$fistName $secondName")
}

// при инициализации можно опустить ключевое слово new
val fred = Person("Fred", "Black")
println(fred.fistName) // поле доступно, в отличии от класса
fred.sayName() // out: Fred Black

// есть возможность копировать с использованием именованных аргументов
val notFred = fred.copy(fistName = "Max")
notFred.sayName() // out: Max Black

val fullName = notFred match {
    // для кейс классов автоматически работает pattern-matching
    // для просто классов пришлось бы дополнительно писать код
    case Person(fistName, secondName) => s"$fistName $secondName"
}

// кейс классы сравниваются по значениям, а не по ссылке
println(Person("Max", "Black") == Person("Max", "Black")) // out: true
                

ООП

object

В scala нет ключевого слова static.

Каждый объект гарантированно создаётся всего лишь один раз.


object MyObject {
    // объекты конструируются при первом вызове метода из объекта
    // или при первом присваивании объекта
    println("MyObject constructed")
    def getTime: Long = java.time.Instant.now().toEpochMilli
}

val time = MyObject.getTime // out: MyObject constructed
val time2 = MyObject.getTime // в консоль ничего дополнительно не выведется
                

ООП

companion object
class Circle(val r: Double) {
    import Circle._
    // класс имеет доступ к приватным полям объекта-компаньона
    def area: Double = calculateArea(r)
}
// объект-компаньон должен называться именем класса и лежат в одном файле с ним
object Circle {
    private def calculateArea(r: Double) = 3.14 * r * r

    // с помощью apply class можно создать, как и case class
    // однако, apply может возвращать любой тип
    def apply(r: Double): Circle = new Circle(r)
    def apply(): Double = 3.14

    // unapply добавлять сделать pattern-matching по классу
    def unapply(c: Circle): Option[Double] = Some(c.r)
}

val circle0 = new Circle(12)    // всё ещё можно создать через new
val circle = Circle(12)         // но можно воспользоваться функцией apply
println(Circle()) // out: 3.14

circle match {
    case Circle(r) => r
}
                

ООП

trait

// train схож с interface в Java/C#
trait Math {
    // private def и val должны быть определены
    private val PI: Double = 3.14
    // может содержать только объявление без определения
    def radius: Double
    // но можно для функций и значений задать определение по умолчанию
    def area: Double = PI * radius * radius
}

// можно создать анонимный экземпляр трейта
val math = new Math {
    override def radius: Double = 2
}
println(math.area) // out: 12.56

// с помощью extends можно наследовать trait
case class Circle(radius: Double) extends Math
val circle = Circle(2)
println(circle.area) // out: 12.56
                

ООП

abstract class

// абстрактный класс похож на трейт, за парой исключений
// для абстрактного класса можно определить конструктор
abstract class Abstract(val radius: Double, name: String) {
    private val PI: Double = 3.14
    def area: Double = PI * radius * radius
}

class Circle(radius: Double) extends Abstract(radius, "Circle")

val circle = new Circle(2)
println(circle.area)    // out: 12.56
println(circle.radius)  // out: 2.0
// println(circle.name) ошибка

// ещё одно отличие от trait в том, что мы не может наследоваться
// сразу от двух абстрактных классов
abstract class Foo
abstract class Bar
// class FooBar extends Foo with Bar - ошибка

trait Foo2
trait Bar2
// ошибки нет
class FooBar2 extends Foo2 with Bar2
                

ООП

Наследование

В Scala наследование сущностей определяют ключевые слова: extends и with.

with может быть использовано только после extends и определяет

исключительно наследование от trait.

В Scala частично допускается множественное наследование:

сущность может быть наследована от одного класса и множества трейтов


trait Foo
trait Bar
trait FooBar

class A extends Foo with Bar with FooBar

trait Foo2
trait Bar2
class B extends A with Foo2 with Bar2

// class C extends A with B ошибка
                

ООП

Наследование

override и super


trait Foo {
    def foo: Double
}

class ClassFoo extends Foo {
    def foo: Double = 3.14
}

class ClassFooExtender extends ClassFoo {
    // для перегрузки в наследнике используется ключевое слово override
    // для доступ к полям и методам базовой сущности используется ключевое слово super
    override def foo: Double = super.foo + 3.14
}
                

ООП

sealed

Ключевое слово sealed пред trait и abstract class обязывает

описывать всех наследников внутри файла, где объявлена наследуемая сущность


sealed trait Figure
case class Point(x: Double, y: Double) extends Figure
case class Line(x: Point, y: Point) extends Figure
case class Vector(start: Point, end: Point) extends Figure
case class Circle(radius: Double) extends Figure
case class Square(topLeft: Point, width: Double, height: Double) extends Figure

val someFigure: Figure = Circle(12)
someFigure match {
    case Point(x, y) => ???
    case Line(x, y) => ???
    case Vector(start, end) => ???
    case Circle(radius) => ???
    case Square(topLeft, width, height) => ???
}
                

Полезные ссылки

Scala docs