Функції і дані (об’єкти)
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, і для стандартної бібліотеки документація розміщена онлайн.
Trait 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.