ООП във функционален език

Предния път

  • Типова йеархия. Any, AnyVal, AnyRef, Unit, Null, Nothing
  • “Чисти” контролни структури – if, pattern matching, for
  • “Нечисти” (референтно непрозрачни) контролни структури – while, try/catch, side-effecting for
  • Съставни структури – n-торки, Range, List, Set, Map
  • Функционално и императивно програмиране – що са те?
  • Модели на изчисление

Задача

Напишете функция, проверяваща, че скобите в един израз са балансирани

def balanced(e: List[Char]): Boolean = ???

Обектно-ориентирано програмиране

?

Кой е това?

Кой е това?

Alan Kay
предлага термина ООП (c. 1967)

ООП?

“I made up the term ‘object-oriented’, and I can tell you I didn’t have C++ in mind.” – Alan Kay

Dr. Alan Kay on the Meaning of
“Object-Oriented Programming”

“I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages… OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.” – Alan Kay

Забележете, че не се споменават класове, наследяване и др.

В основата на ООП – съобщенията

  • Множество познати ни ООП принципи (като SOLID) се фокусират върху практики за дизайн на един клас
  • ООП всъщност е:
    • система от обекти,
    • комуникиращи помежду си
  • Добър ООП дизайн обхваща цялостната комуникация и участващите обекти
  • В познатите езикови ни конструкции съобщения са методите

Енкапсулация

  • getter-и, setter-и, copy конструктори и т.н. НЕ са ООП
  • Всъщност са антитеза на ООП
  • Обектите не са структури от данни
  • Енкапсулацията при обектите се отнася до това, че скриват своето състояние и структурите, които използват, от другите обекти
  • Обектите си взаимодействат единствено през ясен протокол (интерфейс/възможни съобщения) – поведение на обекта
  • При ООП липсва споделено състояние

Енкапсулация - пример

Range, List, Set, Map – всеки има напълно различна имплементация, но общ протокол на комункация

Комуникация в биологията
(паралел)

  • Нашето тяло е сложна система от комуникиращи си клетки/органи/…
  • Различни среди на комуникация
    • хормони
    • сигнали по нервната система
    • дори вътре в клетката – ядрото изпраща messenger RNA към рибозомите

Late Binding

  • Конкретното поведение, което ще се изпълни, се разбира едва по време на изпълнение
  • Подтиповия полиморфизъм е един аспект на late binding
  • В по-голям мащаб: подмяна на части от системата без да се спира цялата система

ООП + ФП?

  • ООП не предполага странични ефекти
  • Може да се използва по immutable и функционален подход

“So: both OOP and functional computation can be completely compatible (and should be!). There is no reason to munge state in objects, and there is no reason to invent “monads” in FP. We just have to realize that “computers are simulators” and figure out what to simulate." – Alan Kay

“I will be giving a talk on these ideas in July in Amsterdam (at the ‘CurryOn’ conference).”

Не дойде 😭

ООП и дистрибутирани системи

  • Алан Кей говори за ООП в контекста на компютри в мрежа
  • Дистрибутираните системи са недетерминирани по природа
  • Но дори при тях ФП може да помогне много
  • Езици като Erlang ги моделират по възможно най-функционален подход
    • обекти (а.к.а. процеси/актьори) със чисто функционално поведение
    • недерменирана комуникация чрез съобщения между тях
  • В Scala – библиотеката Akka
  • Тема на друг курс (но е възможно да я засегнем мъничко)

ООП в Scala

  • Типизация
  • Класове, обекти, интерфейси и др.
  • Uniform Access Principle
  • Модулярност чрез ООП конструкции
  • Подтипов полиморфизъм и late binding
  • Scala и the expression problem?
  • Extension методи

Дефиниране на клас

  • Параметри на клас – конструктор
  • Членове
  • Модификатори на достъп

Да дефинираме клас Rational

Дефиниране на обект

object Math {
  val Pi = 3.14159

  def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
  
  def squared(n: Int) = n * n
}

Math.Pi
Math.gcd(27, 12)
val m: Math.type = Math
m.squared(9)

apply методи

Всеки обект с apply метод може да бъде използван като функция:

object AddTwo {
  def apply(n: Int): Int = n + 2
}

val theLongAnswer = AddTwo.apply(40) // 42
val theAnswer = AddTwo(40) // 42

apply методи

class Interval(a: Int, b: Int, inclusive: Boolean = true) {
  require(a <= b)
  
  def apply(n: Int) =
    if (inclusive) a <= n && n <= b
    else a < n && n < b
}

val percentageInterval = new Interval(0, 100)
percentageInterval(42) // true
percentageInterval(110) // false

Обекти-другарчета (придружаващи/companion обекти)

  • В Scala класовете нямат статични методи
  • Вместо това помощни функции могат да бъдат дефинирани в техните придружаващи обекти 🤝
  • Обект придружава клас, ако
    • е дефиниран със същото име като класа и
    • се намира в същия файл

Обекти-другарчета (придружаващи/companion обекти)

class Rational {
  // ...
}

object Rational {
  val Zero = Rational(0) // използва apply, дефиниран долу
  
  def apply(n: Int, d: Int = 1) = new Rational(n, d)
  
  def sum(rationals: Rational*): Rational =
    if (rationals.isEmpty) Zero
    else rationals.head + sum(rationals.tail)
}

Rational.sum(Rational(1, 2), Rational(5), Rational(3, 5)) // вече не е нужно да пишем new

Придружаващи обекти

List(1, 2, 3) се свежда до List.apply(1, 2, 3),
което е функция с променлив брой параметри

Придружаващи обекти

Имат достъп и до private/protected членовете:

class Rational private (n: Int, d: Int) {
  private def toDouble = n.toDouble / d
  
  // ...
}

object Rational {
  def apply(n: Int, d: Int = 1) = new Rational(n, d)
  
  def isSmaller(a: Rational, b: Rational) = a.toDouble < b.toDouble
}

Rational.isSmaller(Rational(1, 2), Rational(3, 4)) // true

implicit конверсия

Rational(2, 3) + 1 // грешка при компилиране, + приема Rational, не Int
implicit def intToRational(n: Int): Rational = Rational(n)

Rational(2, 3) + 1 // Rational(5, 3), работи

implicit конверсия

implicit def intToRational(n: Int): Rational = Rational(n)

Rational(2, 3) + 1 // Rational(5, 3)
1 + Rational(2, 3)  // Rational(5, 3), също работи

Преобразува се до:

Rational(2, 3) + intToRational(1) // Rational(5, 3)
intToRational(1) + Rational(2, 3)  // Rational(5, 3), също работи

Когато компилаторът не открие метод с очакваните име и параметри
решава да потърси за възможна имплицитна конверсия към тип,
който има този метод

implicit конверсия – ред на търсене

  1. В текущия scope (чрез текущ или външен блок или чрез import)
  2. В продружаващия обект на който и да е от участващите типове

Още за implicit конверсии

  • Добре е да се ограничават
  • Изискват import scala.language.implicitConversions
  • Препоръчително е използването на конверсии с по-конкретни типове пред по-общи
  • Ctrl+Alt+Shift и + в IntelliJ показва implicit конверсиите
  • Ctrl+Alt+Shift и - ги скрива

case класове

case class Person(name: String, age: Int, address: String)

val vasil = Person("Vasil", 38, "Sofia")
  • неизменим value клас
  • всички изброени параметри автоматично стават val полета
  • автоматично генериране на:
    • придружаващ обект с apply

    • equals, hashCode, toString

        Person("Vasil", 38, "Sofia") == Person("Vasil", 38, "Sofia") // true
    • copy – позволява инстанциране на нова версия, базирана на съществуващата

         def getOlder(person: Person): Person = person.copy(age = person.age + 1)
    • още няколко удобства – за тях по-натам

Влагане на case класове

case class Person(name: String, age: Int, address: Address)
case class Address(country: String, city: String, street: String)

val radost = Person("Radost", 24, Address("Bulgaria", "Veliko Tarnovo", "ul. Roza"))

Поведение на case класове

case class Circle(radius: Double) {
  def area = math.Pi * radius * radius
}
case class Person(name: String, age: Int, address: Address) {
  def sayHiTo(person: Person): String =
    s"Hi ${person.name}! I am $name from ${address.country}"
}

Универсален apply

В Scala 3 автоматично се генерира придружаващ обект с apply за всеки клас (не само за case класовете):

class Rational(n: Int, d: Int)

Rational(1, 2) // работи
new Rational(1, 2) // също работи

Абстрактни типове – trait

trait Ordered[A] {
  def compare(that: A): Int
  
  def <(that: A): Boolean = compare(that) < 0
  def <=(that: A): Boolean = compare(that) <= 0
  def >(that: A): Boolean = compare(that) > 0
  def >=(that: A): Boolean = compare(that) >= 0
}

Абстрактни типове – trait

class Rational(n: Int, d: Int) extends Ordered[Rational]) {
  // ...

  def compare(that: Rational): Int = (this - that).numer
}

Rational(3, 4) < Rational(1, 2) // false

Uniform Access Principal

trait Humanoid {
  def name: String
  def age: Int
}
class Person(n: String, a: Int) extends Humanoid {
  val name = n
  val age = a
}

class Robot(brand: String, serialNumber: String, a: Int) extends Humanoid {
  def name = s"$brand--$serialNumber"
  val age = a
}
val personName = new Person("Alex", 21).name
val robotName = new Robot("mi6-42", "000007", 1).name

UAC – интерфейсът не се променя от това дали дадено име е имплементирано чрез ичисление (def)
или чрез съхранена стойност (val)

Uniform Access Principal и case класове

trait Humanoid {
  def name: String
  def age: Int
}

case class Person(name: String, age: Int) extends Humanoid
case class Robot(brand: String, serialNumber: String, age: Int) extends Humanoid {
  def name = s"$brand--$serialNumber"
}

Множествено наследяване

trait A {
  val hello = "Hello"
}
trait B extends A
trait C extends A

class X extends B with C // всеки последващ trait се изрежда с with

new X().hello // Hello, diamond структура не създава проблем

Множествено наследяване

trait A {
  def hello(to: String): String = s"Hello to $to from A"
}
trait B extends A {
  override def hello(to: String): String = s"Hello to $to from B"
}
trait C extends A {
  override def hello(to: String): String = s"Hello to $to from C"
}

class X extends B with C

new X().hello("FMI") // Hello to FMI from C
  • trait-овете по-вдясно override-ват имплементацията от trait-овете вляво
  • Има възможност да работи като декорация, ако ви е любопитно попитайте ни за пример в Slack

Подтипов полиморфизъм

trait Shape {
  def name: String
  def area: Double
}

case class Circle(r: Double) extends Shape {
  def name = "circle"
  def area: Double = math.Pi * r * r
}

case class Rectangle(a: Double, b: Double) extends Shape {
  def name = "rectangle"
  def area: Double = a * b
}

val shape: Shape = Circle(2)
shape.area

// типът на фигурата се определя по време на изпълнение
val randomShape: Shape = getRandomShape()
randomShape.area // програмата знае коя имплементация да използва

Рефиниране/имплементиране на типове при инстанциране

val unitSquare = new Shape {
  val name = "square"
  def area = 1
}

trait параметри

trait Friendly(name: String):
   def hello = s"Hello, I am $name"

case class Person(name: String) extends Friendly(name)

Person("Dimitar").hello // Hello, I am Dimitar

import клаузи

import scala.util.Try // само типа Try

Try(10)
import scala.util._ // всичко от util пакета

Try(10)
Success(10)
import math.Math.{ gcd, Pi } // няколко неща от обекта Math

gcd(42, 18) * Pi

import клаузи

import math.Math._ // всичко от oбекта Math

squared(11)
gcd(42, 10)
import scala.collection.immutable.Set
import scala.collection.mutable // импорт на част от пътя

Set(1, 2, 3)
mutable.Set(4, 5, 6)
import scala.collection.immutable.Set
import scala.collection.mutable.{ Set => MutableSet } // преименуване

Set(1, 2, 3)
MutableSet(4, 5, 6)

import клаузи

  • Могат да са във всеки scope, не е нужно да са в началото на файла:

    class Rational(n: Int, d: Int) {
      import Math.gcd
    
      gcd(n.abs, d.abs)
      // ...
    }
  • Автоматично във всеки файл се включват следните import-и:

    import java.lang._
    import scala._
    import scala.Predef._

export клаузи

Позволяват делегация:

object IntUtils {
  def twice(n: Int): Int = 2 * n
  def squared(n: Int): Int = n * n
}

object DoubleUtils {
  def twice(n: Double): Double = 2 * n
  def squared(n: Double): Double = n * n
}

object MathUtils {
  export IntUtils._
  export DoubleUtils._
}

MathUtils.twice(2) // 4
MathUtils.twice(2.0) // 4.0
  • export-натите имена стават членове на обекта
  • синтактично е със същия формат като import

export клаузи

class Scanner {
  def scan(image: Image): Page = ???
  def isOn: Boolean = ???
}

class Printer {
  def print(page: Page): Image = ???
  def isOn: Boolean = ???
}

class Copier {
  private val scanner = new Scanner
  private val printer = new Printer
  
  export scanner.scan
  export printer.print
  
  def isOn = scanner.isOn && printer.isOn
}

val copier = new Copier
val image = ???
val copiedImage = copier.print(copier.scan(image))

image == copiedImage // true, hopefully :D

AnyVal класове

case class PersonId(id: String) extends AnyVal
case class LocationId(id: String) extends AnyVal

def createAddressRegistration(person: PersonId, location: LocationId) = ???
val stoyan = PersonId("100")
val ruse = LocationId("5")
createAddressRegistration(stoyan, ruse) // успех
createAddressRegistration(ruse, stoyan) // грешка, не е възможно да ги объркаме
  • не създават допълнителен обект, вместо това се репрезентират от типа, който обвиват
  • носят повече type safety в някои ситуации
  • обвитата стойност задължително трябва да е val в обиващия клас
  • поради JVM ограничения не могат да обвият повече от едно поле

AnyVal класове

case class Meter(amount: Double) extends AnyVal {
  def +(m: Meter): Meter = Meter(amount + m.amount)
  def *(coefficient: Double): Meter = Meter(coefficient * amount)
  
  override def toString = s"$amount meters"
}
case class Circle(radius: Meter) {
  def circumference: Meter = radius * 2 * math.Pi
}

Circle(Meter(2)).circumference.toString // 12.566370614359172 meters
  • техните методи се извикват статично

Типизиране – съвместимост на типове

val a: A = new B

// кога тип B е съвместим с тип A?
  • Номинално – типове се проверяват за съвместимост по тяхното име (и по явна релация с други имена)
    • Аз съм бухал, защото са ми казали, че съм бухал
    • Аз като бухал съм птица, защото всички бухали са птици
    • “B наследява A”
  • Структурно – съвместимост на типове се определя по структурата на обекта (по неговото поведение)
    • Аз съм бухал, защото гукам като бухал и защото мога да летя
    • Аз като бухал съм птица, защото мога да летя
    • “B има същите методи (т.е. същата структура) като A”

Структурно типизиране в Scala

case class Eagle(name: String) {
  def flyThrough(location: String): String =
    s"Hi, I am old $name and I am looking for food at $location."
}

case class Owl(age: Int) {
  def flyThrough(location: String): String =
    s"Hi, I am a $age years old owl and I am flying through $location. Hoot, hoot!"
}
def checkLocations(locations: List[String],
                   bird: { def flyThrough(location: String): String }): List[String] = 
  for {
    location <- locations
  } yield bird.flyThrough(location)

checkLocations(List("Sofia", "Varna"), Owl(7))

Структурно типизиране в Scala

case class Eagle(name: String) {
  def flyThrough(location: String): String =
    s"Hi, I am old $name and I am looking for food at $location."
}

case class Owl(age: Int) {
  def flyThrough(location: String): String =
    s"Hi, I am a $age years old owl and I am flying through $location. Hoot, hoot!"
}
type Bird = {
  def flyThrough(location: String): String
}

def checkLocations(locations: List[String], bird: Bird): List[String] = for {
  location <- locations
} yield bird.flyThrough(location)

checkLocations(List("Sofia", "Varna"), Eagle("Henry"))

ООП досега

  • ООП като цялостна система от обекти, взаимодействащи помежду си
  • Дефиниране на класове и обекти, параметри на клас
  • Неизменими value/data обекти чрез case class
  • Абстракции чрез trait. Uniform Access Principle
  • Подтипов полиморфизъм
  • implicit конверсии
  • Type safety чрез обвиващи AnyVal класове
  • Номинално и структурно типизиране

Типова алгебра

Scala 3 добавя обединение (|) и сечение (&) на типове

Сечение на типове

trait LovingAnimal {
  def name: String
  def hug = s"A hug from $name"
}

case class Owl(name: String, age: Int) {
  def flyThrough(location: String): String = s"Hi, I am a $age years old owl. Hoot, hoot!"
}

val lovelyOwl: Owl & LovingAnimal = new Owl("Oliver", 7) with LovingAnimal
lovelyOwl.hug // A hug from Oliver

Обединение на типове

def toInteger(value: String | Int | Double): Int = value match {
  case n: Int => n
  case s: String => s.toInt
  case d: Double => d.toInt
}

toInteger("10") // 10
toInteger(10) // 10
toInteger(10.0) // 10
toInteger(List(10)) // не се компилира

Обединение на типове

def toInteger(value: String | Int | Double): Int = value match {
  case n: Int => n
  case s: String => s.toInt
}
|def toInteger(value: String | Int | Double): Int = value match {
|                                                   ^^^^^
|                                  match may not be exhaustive.
|
|                                  It would fail on pattern case: _: Double

Превърнете в грешка чрез
-Xfatal-warnings:

scalacOptions += "-Xfatal-warnings"

Обединение на типове

def registerUser(registrationForm: RegistrationForm): RegistrationFormError | User = ???

The Expression Problem

The goal is to define a datatype by cases, where one can add new cases to the datatype and new functions over the datatype, without recompiling existing code, and while retaining static type safety (e.g., no casts).

The Expression Problem (алтернативно)

  • Добавяне на нов тип без промяна на съществуващия код
  • Добавяне на нова операция без промяна на съществуващия код

ООП подход

trait Shape {
  def area: Double
}

case class Circle(r: Double) extends Shape {
  def area: Double = math.Pi * r * r
}

case class Rectangle(a: Double, b: Double) extends Shape {
  def area: Double = a * b
}

ФП подход

trait Shape
case class Circle(r: Double) extends Shape
case class Rectangle(a: Double, b: Double) extends Shape

def area(s: Shape): Double = s match {
  case Circle(r) => math.Pi * r * r
  case Rectangle(a, b) => a * b
}

case класовете могат да бъдат използвани в pattern matching

Добавяне на операция във ФП – лесно

def circumference(s: Shape): Double = s match {
  case Circle(r) => 2 * math.Pi * r
  case Rectangle(a, b) => 2 * (a + b)
}

Добавяне на операция в ООП – трудно, промяна на всички класове

trait Shape {
  def area: Double
  def circumference: Double
}

case class Circle(r: Double) extends Shape {
  def area: Double = math.Pi * r * r
  def circumference = 2 * math.Pi * r
}

case class Rectangle(a: Double, b: Double) extends Shape {
  def area: Double = a * b
  def circumference = 2 * (a + b)
}

Добавяне на тип в ООП – лесно

case class Square(a: Double) extends Shape {
  def area: Double = a * a
  def circumference: Double = 4 * a
}

Добавяне на тип във ФП – трудно

case class Square(a: Double) extends Shape

def area(s: Shape): Double = s match {
  case Circle(r) => math.Pi * r * r
  case Rectangle(a, b) => a * b
  case Square(a) => a * a
}

def circumference(s: Shape): Double = s match {
  case Circle(r) => 2 * math.Pi * r
  case Rectangle(a, b) => 2 * (a + b)
  case Square(a) => 4 * a
}

The Expression Problem

  • Всеки език е добре да предоставя изразни средства и за двата проблема
  • ООП подходът е подходящ за типове с предварително неизвестен брой случаи и малко основни операции
  • Функционалният подход е подходящ за типове с предварително фиксирани случаи

Extension Methods

  • Добавяне на методи към съществуващи типове
  • Само в текущия scope

Extension Methods

extension (n: Int) {
  def squared = n * n
  def **(exp: Double) = math.pow(n, exp)
}

3.squared // 9
2 ** 3 // 8.0

import-ват се по името на метода:

// file IntExtensions.scala
package scalafmi.intextensions

extension (n: Int) {
  def squared = n * n
  def **(exp: Double) = math.pow(n, exp)
}
// file Demo.scala
import scalafmi.intextensions.{ squared, ** }

3.squared // 9
2 ** 3 // 8.0

Extension Methods

extension (xs: List[Double]) {
  def avg = xs.sum / xs.size
}

List(1.0, 2.0, 3.0).avg // 2.0
List("a", "b", "c").avg // грешка, value avg is not a member of List[String]
extension [A](xs: List[A]) {
  def second = xs.tail.head
}

List(1.0, 2.0, 3.0).second // 2.0
List("a", "b", "c").second // b

Extension Methods в Scala 2

  • Scala 2 също позволява добавяне на методи
  • Използва се механизма за implicit конверсия

Extension Methods в Scala 2

class EnrichedInt(val n: Int) extends AnyVal {
  def squared = n * n
  def **(exp: Double) = math.pow(n, exp)
}
implicit def intToEnrichedInt(n: Int) = new EnrichedInt(n)

3.squared // 9
2 ** 3 // 8.0

Extension Methods в Scala 2

implicit class EnrichedInt(val n: Int) extends AnyVal {
  def squared = n * n
  def **(exp: Double) = math.pow(n, exp)
}

3.squared // 9
2 ** 3 // 8.0

Тук не е нужен import scala.language.implicitConversions

Примери от стандартната библиотека

1 -> "One" // (1, "One"), -> се добавя към всички типове

// extension methods се използва за добавяне на методите за колекции върху String
"abcdef".take(2) // ab

import scala.concurrent.duration.DurationInt
5.seconds // scala.concurrent.duration.FiniteDuration = 5 seconds

DSL за тестове

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class ExampleSpec extends AnyFlatSpec with Matchers {
  "+" should "sum two numbers" in {
    2 + 3 shouldEqual 5
  }
}

ООП дизайн?

ООП дизайн – скрити домейн обекти

def buyTea(cc: CreditCard, paymentService: PaymentService): Tea = {
  val teaCup = new Tea(...)
  paymentService.charge(cc, teaCup.price)
  teaCup
}
case class Charge(cc: CreditCard, amount: Double)

def buyTea(cc: CreditCard): (Tea, Charge) = {
  val teaCup = new Tea(...)
  (teaCup, Charge(cc, teatCup.price)
}

Отлагане на страничния ефект =>

  • скрити домейн концепции изплуват на яве (Charge обект)
  • моделираме дейности като данни
  • които допълнително можем да трансформиране функционално
    • купуване на n кафета и събиране на Charge-ове
    • анализ на Charge-ове от различни потребители
  • по-добра тестваемост

Таблица на типовите елементи в Scala

Въпроси :)?

// reveal.js plugins