Функції і дані (об’єкти)

ред.
class Rational(x: Int, y: Int) {
  def numer = x
  def denom = y
  
  override def toString = 
    numer + "/" + denom
}

Описує новий тип даних та його конструктор (функцію що повертає значення типу). Викликається конструктор так:

x = new Rational(1, 2) // створення екземпляру
x.numer == 1 // доступ до атрибутів

class Rational(x: Int, y: Int) {
  require(y != 0, "denominator must not be zero")
  ..
}

Зробить так, що констуктор буде генерувати IllegalArgumentException щоразу коли передати нульовий знаменник.

Подібна функція assert, буде генерувати AssertionError.

Також, можна описати додаткові конструктори:

class Rational(x: Int, y: Int) {
  def this(x: Int) = this(x, 1) // якщо передати лише одне ціле, то раціональне число ініціалізується цим цілим.

Будь-який метод об’єкта з одним параметром може використовуватись як інфіксний.

x add y == x.add(y)

Можна також описати метод з іменем що складається з одного, чи послідовності спеціальних символів:

def +(other: Rational): Rational

Для унарних треба писати:

def unary_- : Rational = new Ratinal(-number, denom)

Між мінусом та двокрапкою повинен бути пробіл, інакше компілятор спробує описати не унарний мінус, а унарний метод -: , а потім захоче ще двокрапку.

Черговість операцій (precedence) визначається першим символом, і описана в таблицях.

Ієрархії класів

ред.

Абстрактний клас може містити елементи без означень. Але не можна створити його екземпляр:

abstract class IntSet {
  def incl(x: Int) : IntSet
  def contains(x: Int): Boolean
}

class Empty extends IntSet {
  def contains(x: Int): Boolean = false
  def incl(x: Int): IntSet = new NonEmpty(x, new Empty, new Empty)
}

class NonEmpty(elem: Int, left: IntSet, right IntSet) extends IntSet {
  def contains(x: Int): Boolean =
    if (x < elem) left contains x
    else if (x > elem) right contains x
    else true
  
  def incl(x: Int): IntSet =
    if (x < elem) new NonEmpty(elem, left incl x, right)
    else if (x > elem) new NonEmpty(elem, left, right incl x)
    else this
}

Тут бачимо приклад Persistent data structure. Це такі структури даних, які при створенні нових на їх основі нікуди не зникають, а залишаються частиною нових.

IntSet - це надклас для Empty та NotEmpty, а вони - його підкласи, конформні йому (можуть використовуватись там, де вимагається суперклас).

Якщо не вказувати надклас для об’єкта, ним буде стандартний Object з java.lang. Всі надкласи для класу і його надкласів називаються базовими класами.

Для Empty, базові класи це IntSet та Object.

Щоб переписувати неабстрактні означення надкласу треба використати override. Він корисний тим, що гарантує що ви не помилитесь в імені методу який перевантажуєте, і не перевантажите якийсь метод випадково використавши для свого методу таке саме ім’я як в надкласі.

Сінглтон

ред.

Логічно було б мати лише один екземпляр Empty. Це досягається просто якщо слово class в описі Empty замінити на object:

object Empty extends IntSet {
  def contains(x: Int): Boolean = false
  ...

Hello, world!

ред.

Досі ми виконували наші програми в REPL. Але можна писати і цілком самостійні програми. Кожна така програма містить об’єкт з методом main:

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

Коли програма скомпілюється, її можна запустити командою scala Hello. Або java Hello, не важливо.

Dynamic method dispatch - код який викликається при виклику метода, залежить від конкретного типу об’єктів що викликаються.

Організація класів (пакети, стандартна бібліотека)

ред.

Класи та об’єкти поміщаються в пакети, якщо зверху файлу написати

package ім’я.пакету

Після цього, до об’єкта можна звертатись через повне ім’я (Fully Qualified Name): package.Name

Якщо щоразу писати ім’я пакету перед іменем класу ліньки, то можна написати:

import package.Name // що додасть об’єкт Name в поточний простір імен
import package.{Name1, Name2} // додасть кілька об’єктів
import package._ // додасть всі об’єкти пакету

В кожну scala-програму автоматично імпортується все з

  • пакету scala
  • пакету java.lang
  • об’єкту-сінглтона scala.Predef

Ось повні імена для вже відомих нам об’єктів: scala.Int, scala.Boolean, java.lang.Object, scala.Predef.require, scala.Predef.assert.

Пакети документуються за допомогою scaladoc, і для стандартної бібліотеки документація розміщена онлайн.

Scala не має множинного наслідування класів. Але можна наслідуватись від кількох Trait-ів.

class Some extends superclass with Trait1, Trait2 ...

Можна зразу наслідуватись не від класу:

class some extends Trait1

Trait - це щось схоже на абстрактний клас, в конструктора якого не може бути параметрів.

Ієрархія класів

ред.

scala.Any - позначає будь-який тип, який може бути одним з двох:

  • scala.AnyRef (синонім для java.lang.Object) - будь-який об’єкт
  • scala.AnyVal - будь-який примітивний тип.

scala.Any містить методи "==", "!=", "equals", "hashCode", "toString".

Є два типи які не є підтипами Any: Nothing та Null.

Nothing - тип порожньої колекції, або сигнал що функція не дає результату. Тип виразу throw Exc: Nothing.

А Null можна передати всюди де очікують AnyRef, бо він підтип КОЖНОГО! об’єкта що наслідує AnyRef.

Параметри типів

ред.

Замість того щоб писати IntList, можна написати List[T]:

class Cons[T](val head: T, val tail: List[t]) extends List[T] {
  def isEmpty = false
}

class C(val a: T) - це еквівалент class C(b: T) { def a = b } (зразу створює атрибути класу).

class Nil[T] extends List[T] {
  def isEmpty = true
  def head: Nothing = throw new NoSuchElement("Nil head")
  def tail: Nothing = throw new NoSuchElement("Nil tail")
}

Функції теж можна описувати з параметрами типів. Надалі конкретні типи отримуватимемо пишучи List[Int].

Типи не потрібні компілятору для здійснення обчислень підстановками. Вони лише допомагають перевіряти програму під час компіляції. Таке називається type erasure та існує в Scala, Java, ML, Haskell. Не існує в С++, F# та C#, які тримають дані про типи й під час рантайму. (І в python, який взагалі динамічно типізований, тобто там типи мають не змінні а значення).

Функції як об’єкти

ред.

Тип функції A => B - це лише скорочення від scala.Function1[A, B], що описується як

package scala
trait Function1[A, B] {
  def apply(x: A): B
}

Для функцій з більшою кількістю параметрів є Function2, Function3 і так далі, аж до 22.

Анонімні (лямбда-)функції теж є об’єктами, наприклад (x: Int) => x * x стає:

class AnonFunction extends Function1[Int, Int] {
  def apply(x: Int) = x * x
}
new AnonFunctioin

Проте, apply та інші методи не є об’єктами інакше ми б отримали нескінченну рекурсію. Такі методи перетворюються на об’єкти всюди де ми очікуємо мати об’єкт типу функція, за допомогою виразу (x: Int) => f(x). Такий вираз називається ета-розширенням (η-expansion).

Повністю об’єктно-орієнтована мова - це мова в якій всі значення - об’єкти.

Поліморфізм

ред.

Поліморфізм в ООП можна досягти двома способами - за допомогою підтипів та за допомогою узагальнень (generics).

Можна описати функцію для вказаної множини типів:

def assertAllPos[S <: IntSet](r: S): S =

Тут S може ставати будь-яким підтипом IntSet.

Аналогічно у виразі S >: T, S - надтип T.

Декомпозиція

ред.

Наші класи мають методи класифікатори (для визначення типу) isEmpty і т.п., та аксесори (для доступу до компонентів) head, tail. Якщо атрибутів багато, чи класів багато, то всі ці методи описувати непродуктивно, бо кількість класифікаторів зростає на N з додаванням кожного N+1-шого класу.

Одне з рішень цієї проблеми - використовувати замість класифікаторів та аксесорів функції:

def isInstanceOf[T]: Boolean // повертає true, якщо передати аргумент типу T
def asInstanceOf[T]: T // приводить переданий об’єкт до класу T, або кидає виняток ClassCastException

Проте це рішення не відповідає стилю мови Scala. Тут цю проблему краще вирішують за допомогою співставлення з шаблоном.

Метою аксесорів та класифікаторів є зворотня композиція об’єкта:

  • Який підклас використали при створенні об’єкта?
  • Які аргументи при цьому передали в конструктор?

Давайте опишемо case class, який можна використовувати в співставленні з шаблоном:

trait Expr
case class Number(value: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

Case class автоматично додає об’єкти виду

object Sum {
  def apply(e1: Expr, e2: Expr) = new Sum(e1, e2)
}

і тепер ми можемо створювати об’єкти викликаючи метод без new.

А інші методи класів можемо описувати співставлення з шаблоном:

def eval(e: Expr): Int = e match {
  case Number(n) => n
  case Sum(e1, e2) => eval(e1) + eval(e2)
}

Загалі співставлення з шаблоном виглядає наступним чином:

e match {
  pattern1 => exp1
  pattern2 => exp2
  ...
}

Якщо жоден шаблон не підійде - отримаємо MatchError.

Як паттерн можна підставляти конструктори, змінні, _ (змінна що ігнорується), константи. Змінні в паттернах завжди починаються з маленької, а константи - з великої літери, окрім констант null, true та false.