Scala/Coursera/Об’єкти
Функції і дані (об’єкти)
[ред.]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
.