Дмитрий Быков
Scala — мультипарадигменный язык программирования (сочетает ФП и ООП)
Scala — строго типизированный язык программирования
Scala работает на JVM
Точкой входа в программу считается функция 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
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 говорит о том, что значение будет вычислено
только при первом обращении к значению
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 (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"
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
// извлечение данных из структуры
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
// проверка принадлежности к типу
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
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)
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 возвращает 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 |
// возвращаемся к теме ленивых вычислений
// обратите внимание на типы 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 - надтип всех типов в языке. 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 - тип оберток над значениями
Компилятор с такими типами обходится по особенному - стремится оптимизировать так, чтобы обертка не создавала реальный объект в памяти
Использование:примитивы - 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
Тип с единственным значением "()"
Используется в местах где в C#/Java стоял бы "void"
Обычно связано с сайд-эффектами (печать в консоль, запись в
базу, мутацию какого-то внешнего объекта, и т.д.)
Значения "()" не существует во время исполнения (абстракция на уровне компилятора)
def getUnit: Unit = ()
val unit: Unit = getUnit
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 - подтип всех типов, также называемый 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 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
В 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 // в консоль ничего дополнительно не выведется
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
}
// 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(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 пред 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) => ???
}