Введение в Scala

ООП. Функции и методы. Коллекции

Кафедра ИВТ и ПМ. Ветров С. В.

2023


## Файлы. Чтение ```scala import scala.io.Source val filename = "fileopen.scala" for (line <- Source.fromFile(filename).getLines) { println(line) } ```
## Файлы. Запись Использованы классы Java ```scala import java.io.PrintWriter // подключение Java класса @main def main() = val f:PrintWriter = new PrintWriter("my_file.txt") f.println("text") f.close() ```
## Методы - Объявление метода в scala начинается со слова `def`.\ Например: ```scala def foo(x:Int):Double = 42.0 + x // Double -- тип возвращаемого значения ``` - Оператор `return` не используется, возвращаемое значение — это значение последнего выражения внутри функции; (это тоже упрощает анализ кода, поощряет написание функций где невозможен выход в неожиданном месте) - Тип возвращаемого значения может быть выведен автоматически (кроме рекурсивных функций) ```scala def foo(x:Int) = 42.0+x // или def foo(x:Int) = { println("Hello, World!") 42+x // возвращаемое значение — значение последнего выражения в функции } ``` - Методы можно объявлять где угодно, в том числе внутри других функций (как с Паскале)

Функции

Функция, сама по себе, являясь значением некоторого функционального типа, может быть записана и в переменную, подобно анонимным функциям в языке C++. Например:

val foo: Int => Double = (x:Int) => 42.0 + x
  • foo — имя функции
  • Int => Double — тип функции, принимающей Int и возвращающий Double
  • = — присваивание значения, в данном случае тела функции
  • (x:Int) => 42.0 + x — тело функции


Как и с переменными, можно не указывать тип:

val isOdd = (_:Int) % 2 == 0        // лямбда, проверяющая чётность числа
// аналогично: def isOddMethod(x:Int)   =   x % 2 == 0
Если переменная всего одна, то можно задать имя: _

Функции как параметры и значения


def gareaterOn(f: Int => Int): (Int, Int) => Boolean =
    (x,y) => f(x) > f(y)
// Int => Int -- тип функция, принимающая Int и возвращающая Int
// (Int, Int) => Boolean — возвр. тип -- логическая функция двух аргументов

// вызов: 
gareaterOn( x => x)(1,2)
// вызов: 
gareaterOn( x => x%10 )(1,2)
// вызов и запись результата в переменную: 
val func = gareaterOnMod10( x => x%10 )
func(1,2)
## Передача по имени (By-name parameters) и ленивые вычисления Параметры переданные по имени будут вычислены только при необходимости. Такие параметры обозначаются `=>` перед именем своего типа. ```scala /** С вероятностью 0.5 возвращает квадрат числа или ноль */ def foo( x: => Int ) = { if ( Random.nextInt() % 2 == 0) pow(x,2) else 0 // оператор return в Scala не используется, // метод вернёт значение последнего приведенного выражения } // сумма будет вычислена только если внутри функции будет выполнено условие foo( 5 + 3 ) // код внутри фигурных скобок выдаёт результат последнего выражения // будет выполнен если внутри функции будет выполнено условие foo( {print("Evaluated"); 5 + 3} ) ```

Композиция функций

Вложенный вызов функций можно записывать последовательно, в строку.

Для этого используются операторы:

  • f andThen g — аналогично g( f(x) )
  • f compose g — аналогично f( g(x) )

	val f = (_:Double) + 2
	val g = (_:Double) * 3

	(f compose g)(1)   // 5
	(f andThen g)(1)   // 9

Каррирование (Currying)

Каррирование (карринг) или частичное применение функции — преобразование функции от многих аргументов в набор функций, каждая из которых является функцией от одного аргумента.

В результате частичного применения функции появляется новая функция, которая принимает оставшиеся аргументы. Например: f(x,y) преобразуется в f'(x)(y), где f'(x) — возвращает функцию.

/** Создаёт новый список на основе старого
 * @param f -- функция фильтрации списка
 * @param lst   -- список из целых чисел
 * @return список чисел, для которых функция f возвращает true
 */
def my_list_filter(f: Int => Boolean, lst: List[Int] ) =
  var filtered: List[Int] = List()        // новый пустой список
  for (element <- lst)                    // перебор элементов списка с записью каждого в element
    if ( f(element) )
      filtered = filtered.appended( element ) // добавление элемента в новый список
  filtered

Каррирование (Currying)


def my_list_filter(f: Int => Boolean, lst: List[Int] ) =
  var filtered: List[Int] = List()        // новый пустой список
  for (element <- lst)                    // перебор элементов списка с записью каждого в element
    if ( f(element) )
      filtered = filtered.appended( element ) // добавление элемента в новый список
  filtered

val isOdd = (_:Int) % 2 == 0        // лямбда, проверяющая чётность числа
// аналогично: def isOddMethod(x:Int)   =   x % 2 == 0

my_list_filter( isOdd,            List(1,2,3,4))          // -> List(2,4)
my_list_filter( (_:Int) % 2 == 0, List(1,2,3,4))          // -> List(2,4)
my_list_filter( _ % 2 == 0,       List(1,2,3,4))          // -> List(2,4)

// каррирование метода my_list_filter, 
// теперь он возвращает функцию, которая должна принять его второй параметр
// первый параметр уже задан
val oddFilter = my_list_filter.curried( isOdd )

// вызов каррированного метода
oddFilter( List(1,2,3,4) )

Каррирование (Currying)


def plusxy: Int => Int => Int    =    x => y => x + y


// вызов
plusxy(1)(2)

Частичные функции (Partial Functions)

  • Похожи на кусочно-заданные функции в математике
  • Частичная функция содержит в себе оператор match (неявно) и сравнивает значение первого и единственного аргумента со значениями в case, при совпадении возвращает значение
  • Типы аргументов указываются не как у обычной функции, а как шаблонные типы, в квадратных скобках:
    [Int, Int]
  • Первый и второй шаблонные типы должны совпадать с проверяемыми и возвращаемыми значениями.

val divide10: PartialFunction[Int, Int] = {
    case 1 => 10
    case 2 => 5
    case 5 => 2
    case 10 => 1
}

Если вариант case для текущего значения аргумента функции не предусмотрен, то бросается исключение. Однако в такую функцию встраивается метод `.isDefinedAt` которым можно проверить, определена ли функция для такого значения.

ООП

Класс - ссылочный тип.

  • class - обыкновенный класс
  • anstract class - абстрактный класс
  • trait ~ абстрактный класс; может содержать абстрактные и обычные методы
  • case class - класс с коротким объявлением, используется при сопоставлении с образцом
  • object - объект (синглтон, экземпляр анонимного класса)
  • object - объект-компаньон (синглтон, имя которого совпадает с именем класса)

Класс

  • Похож на класс в Java
  • Модификатор доступа по умолчанию — public
  • Код конструктора записывается непосредственно в теле класса
		
// Можно использовать отступы и двоеточие (Scala 3)
class Customer:
    // поля (public):
    var id: Int = 0
    var name: String = ""

// можно использовать скобки {}
class Customer {
    // поля (public):
    var id: Int = 0
    var name: String = ""
}

Класс

		
/** Клиент */
class Customer {
    // поля (public):
    var id: Int = 0
    var name: String = ""

    private   var note: String =""
    protected var field: String =""

    // переопределение метода; если нет параметров, то скобки () можно опустить
    override def toString: String =
        f"$id%6d $name%-32s; $note"
        // %-32s — формат: 32 символа, выравнивание по левому краю
}
		
		
    val c: Customer = new Customer
    // Ошибка! Нельзя менять val переменную
    c = new Customer;
    
    // можно менять поля
    c.name = "Martin O."
    c.name = "Martin Odersky"

    println(c.id)
    println(c.name)
    println(c.note) // Error: private

    println( c.toString )
		
		
// вывод
0
Martin Odersky
     0 Martin Odersky                  ; 
		

Класс

		
class Customer {
    // поля (public):
    var id: Int = 0
    var name: String = ""
    
    // область инициализации (вызывается при создании объекта):
    print("Object created")


    // закрытое поле
    private var note: String =""
}

@main
def main() =
    val c: Customer = new Customer 			// Object created
		

Класс

		
class Customer(id:Int, name:String, note:String);
// параметры — private val поля
		

@main
def main() =
    //Ошибка: нет конструктора по умолчанию
    // val c: Customer = new Customer
    val c: Customer = new Customer(0, "Martin Odersky", "")

    // Ошибка: нельзя менять поля
//    c.name = "Martin O."
//    c.name = "Martin Odersky"

    // Ошибка: нет доступа к полям
//    println(c.id)
//    println(c.name)
    // println(c.note) // Error

    // вызов унаследованного метода toString
    println( c.toString )

Класс

В классы с объявлением полей через конструктор можно добавлять методы
		
class Customer(id:Int, name:String, note:String){
    // параметры — private val поля

    override def toString: String =
        f"$id%6d $name%-32s; $note"
}
		

@main
def main() =
    //Ошибка: нет конструктора по умолчанию
    // val c: Customer = new Customer
    val c: Customer = new Customer(0, "Martin Odersky", "")

    // Ошибка: нельзя менять поля
//    c.name = "Martin O."
//    c.name = "Martin Odersky"

    // Ошибка: нет доступа к полям
//    println(c.id)
//    println(c.name)
    // println(c.note) // Error

    // вызов унаследованного метода toString
    println( c.toString )

Абстрактный класс

Похож на абстрактный класс в Java:

  • Может содержать обычные методы
  • Може не содержать абстрактных методов
  • Нельзя наследоваться (extend) более чем от одного абстрактного класса
		
abstract class IntSet {
    // add an element to the set
    def incl(x: Int): IntSet

    // whether an element belongs to the set
    def contains(x: Int): Boolean
    
    // абстрактный класс может содержать методы с реализацией
    def foo() = print("real method")
}

@main
def main() =
    // Ошибка: нельзя создавать экземпляры абстрактного класса
    val s: intSet = new IntSet;
		

Наследование от абстрактного класса

		
class EmptyIntSet extends IntSet {	 	 
  
  def contains(x : Int) = false	 	 
  
  def incl(x : Int) =	 	 
  	  new NonEmptyIntSet(x, this)	 	 
}
		

Trait

Похожи на интерфейсы в Java, но:

  • Могут быть потомками
  • Могут содержать обычные методы
  • Могут содержать разделы инициализации
		
trait Employee:
	// абстрактные методы:
    def id: Int
    def firstName: String
    def lastName: String
    
    def drink_coffe: Unit 			// не возвращает значения
	
	// обычный метод    
    def work =
        print("Do something....")
		
Отличия абстрактного класса и интерфейса в Java

Object

  • Похож на класс со статическими членами
  • Синглтон — единственно возможный экземпляр класса
  • Ленивая инициализация (всегда lazy val)
		
object StringUtils:
    def truncate(s: String, length: Int): String = s.take(length)
    def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*")
    def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty


object MathConstants:
  val PI = 3.14159
  val E = 2.71828

		
		
// Использование
StringUtils.truncate("Да, нет, наверное", 2)  // "Да"

println(MathConstants.PI)   // 3.14159
		

Companion object — объект компаньон

  • Имя совпадает с именем класса
  • Объявляется в одном и том же файле, с одноимённым классом (класс-компаньон)
  • Используется для объявления аналога статических полей и методов
		
import scala.math.*

class Circle(val radius: Double):
  def area: Double = Circle.calculateArea(radius)

object Circle:
  private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)

val circle1 = Circle(5.0)
circle1.area
		

Companion object — объект компаньон

Содержит методы

  • apply — фабричный метод, аналог конструктора, создаёт объекты
  • unapply — аналог деструктора
		
class Person:
  var name = ""
  var age = 0
  override def toString = s"$name is $age years old"

object Person:
  def apply(name: String): Person =		// аналог конструктора 
    var p = new Person
    p.name = name
    p

  def apply(name: String, age: Int): Person = // аналог конструктора 
    var p = new Person
    p.name = name
    p.age = age
    p


val joe = Person("Joe")
val fred = Person("Fred", 29)

//val joe: Person = Joe is 0 years old
//val fred: Person = Fred is 29 years old
		

Ссылочные типы данных — Ref

Опциональный тип Option

Тип вариант Either

## Опциональный тип Option[A] Такой тип может содержать значение типа `A`, а может и не содержать.\ У `Option[A]` есть два подтипа - `Some[A]` — контейнер Some, гарантированно содержащий значение типа A - `None` Этот тип используется там, где, например, возвращаемого значения может и не быть. Метод поиска элемента (find) в коллекции Vector может вернуть номер элемента если он найден, или вернуть None, если такого элемента нет. Как правило опциональные значения обрабатываются сопоставлением с образцом (match) ```scala val v = Vector(23, 53, 8, 45, 0, 4) val k = v.find( _ == 53) // _ == 53 — тело анонимной функции, сравнивающий элемент с числом 53 // _ — имя по умолчанию параметра анонимной функции k match case None => print("No element") case Some(i) => print(f"Element number: $i") // проверка на соответствие значения k подтипу Some с извлечением значения ```
## Основные методы опционального типа - `.getOrElse( default )` -- вернёт default, если объект типа `Option[A]` в себе не содержит значения типа A - `.OrElse( x: Optional[A])` -- вернёт другое опциональное значение, если первое не содержит в себе значений типа A; будет сгенерированно исключение, если и второе значение является `None` - `.map`, `flatMap` -- применяют функцию к опциональному значению, могут вернуть None; второй метод возвращает опциональное значение - `.filter` -- применяет предикат, если false делает опциональное значение None - `.collect` -- применяет частичную функцию

Пример


/** Решает квадратное уравнение с коэффициентами
 * @return (D, x1, x2) */
def square_equation(a: Double, b:Double, c:Double): (Double, Option[Double], Option[Double])=
    val D = pow(b, 2.0) - 4 * a*c
    D match {
        case D if D < 0 => (D, None, None)
        case _          => (D,  Option( (-b-sqrt(D))/2/a ), Option( (-b+sqrt(D))/2/a ) )
    }

Некоторые коллекции

Абстрактные классы и трейты коллекций
Иерархия классов коллекций

Некоторые коллекции


scala.collection.immutable
List[+A], Vector[+A], Set[+A], Map[K, +V]

scala.collection.mutable_
Buffer[A], Set[A], Map[K,V] Builder[-E, C]

// интерфейсы
scala.collection
Seq[+A], Set[+A], Map[K, +V], Iterator[+A]

Информация о том, что конкретные значения, в частности коллекции, неизменны (immutable) позволяет компилятору более эффективно оптимизировать код. В том числе помогает программисту и компилятору организовывать параллельные вычисления не беспокоясь об отсутствии потокобезопансоти.

Некоторые коллекции

Создание коллекций


// односвязный список с O(1) вставкой в начало и O(n) вставкой в конец
val l = List(10,2,3,3,5)
// создание списка; 
val namesAgain = "Joel" :: "Chris" :: "Ed" :: Nil   
// Nil — специальный тип для списков, "отсутствующий" элемент; 
// используется для создания пустых списков и обозначения конца списка

// Vector реализован на основе префиксного дерева (trie), 
// быстрое добавление в начало и конец, произвольный доступ за константное время
// перебор медленнее чем в списке
val v = Vector(1,2,3,3,5)     // implemented as tree of blocks, provides fast random access

// множество
val s = Set(1,2,3,3,5)        // -> Set(1,2,3,5)

// словарь
val m = Map("key1" -> 123, "key2" -> 456)

Заполнение списка случайными числами


import scala.util.Random

// List.fill принимает два набора параметров: размер последовательности и выражение,
val rand_list = List.fill( 10 ) ( Random.nextInt(10) )

Некоторые примеры работы с коллекциями

Доступ к элементам через оператор `()`, а не через `[]` (используется для аргументов обобщённых типов).

l(0)        // -> 10
s(5)        // true
s(55)       // false
m("key1")   // -> 123
m("qwerty")   // Exception

m.get("key1")    // Some(123)
m.get("qwerty")  // None
s.contains(2)    // true

l.head      // -> 10
l.last      // -> 5

s.size      // -> 4
v.size      // -> 5

l - 10        // удаление элемента;   -> List(2,3,3,5)
m - "key1"    // удаление пары c ключом key1 

Работа с коллекциями в функциональном стиле

Использование неизменяемых значений типично для функциональной парадигмы. Похоже на Stream API в Java.

В переменной list лежит отсортированный в порядке возрастания список целых чисел. Со списком необходимо выполнить следующие операции:

  • Взять все числа меньше 100 (список может быть большим)
  • Выбрать все числа, которые делятся на 4
  • Поделить их на 4
  • Вывести на экран в отдельной строке каждый элемент, кроме последнего

val list = List(10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150)

list
	.takeWhile(_ < 100)     // отбор начальных элементов, для которых предикат = true
	.filter( _ % 4 ==0)     // отбор всех, для которых предикат = true
	.init                   // взять все, кроме последнего
	.map( _ / 4)            // применить функцию
	.foreach(println)       // терминальный метод: применяет функцию
Предикат — функция с множеством значений: true и false.
immutable collections\ ![](https://docs.scala-lang.org/resources/images/tour/collections-immutable-diagram-213.svg) mutable collections\ ![](https://docs.scala-lang.org/resources/images/tour/collections-mutable-diagram-213.svg) Использование неизменяемых значений типично для функциональной парадигмы. Операции для **последовательностей** - Добавить слева `+:` - Добавить справа `:+` - Конкатенация `++` Добавление в словарь: `val m2 = m + ("kry3" -> 789)`