Инженер по данным в "Криптоните"
Декомпозиция в Scala 3
https://scalabook.gitflic.space
О чем будет доклад
Основные средства декомпозиции в Scala 3:
- Параметры конструктора в trait-ах
- Прозрачные trait-ы
- Экспортирование элементов
- Различие trait и abstract class
- Распространённые ошибки при декомпозиции
trait Combinator[A]:
def combine(a1: A, a2: A): A
val empty: A
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
Сделай что-нибудь
object IntCombinator extends Combinator[Int]:
val empty: Int = 0
def combine(a1: Int, a2: Int): Int = a1 + a2
IntCombinator.combineAll(List.empty) // 0
IntCombinator.combineAll(List(1)) // 1
IntCombinator.combineAll(List(1, 2, 3)) // 6
Реализовать
trait Combinator[A]:
def combine(a1: A, a2: A): A
val empty: A
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
Параметры конструктора
trait Combinator[A](empty: A):
def combine(a1: A, a2: A): A
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
object IntCombinator extends Combinator[Int](0):
def combine(a1: Int, a2: Int): Int = a1 + a2
IntCombinator.combineAll(List(1, 2, 3)) // 6
trait Combinator[A](empty: A):
def combine(a1: A, a2: A): A
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
object IntCombinator extends Combinator[Int]:
def combine(a1: Int, a2: Int): Int = a1 + a2
// missing argument for parameter empty of constructor Combinator
// in trait Combinator: (empty: Int): Playground.Combinator[Int]
trait Combinator[A](empty: A):
def combine(a1: A, a2: A): A
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
trait IntCombinator extends Combinator[Int](0):
def combine(a1: Int, a2: Int): Int = a1 + a2
// trait IntCombinator may not call constructor of trait Combinator
Fail, fail, fail
trait Combinator[A](using empty: A):
def combine(a1: A, a2: A): A
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
given Int = 0
object IntCombinator extends Combinator[Int]:
def combine(a1: Int, a2: Int): Int = a1 + a2
IntCombinator.combineAll(List.empty) // 0
IntCombinator.combineAll(List(1)) // 1
IntCombinator.combineAll(List(1, 2, 3)) // 6
Контекстные параметры
trait Combinator[A](empty: A):
println(s"Пустое значение - $empty")
object StringCombinator extends Combinator[String]("пусто")
StringCombinator
// Пустое значение - пусто
Параметры конструктора инициализируются до
trait Combinator[A]:
val empty: A
println(s"Пустое значение - $empty")
object StringCombinator extends Combinator[String]:
val empty: String = ""
StringCombinator
// Пустое значение - null
А в чем разница?
А в чем разница?
Смешанная композиция
trait Empty[A](val empty: A)
trait Combinator[A](combine: (A, A) => A):
val empty: A
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
object IntCombinator
extends Combinator[Int](_ + _), Empty[Int](0)
IntCombinator.combineAll(List.empty) // 0
IntCombinator.combineAll(List(1)) // 1
IntCombinator.combineAll(List(1, 2, 3)) // 6
Смешанная композиция
trait EmptyInt1:
val empty: Int = -1
trait EmptyInt2:
val empty: Int = 1
trait Combinator[A]:
def combine(a1: A, a2: A): A
val empty: A
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
object IntCombinator extends Combinator[Int],
EmptyInt1, EmptyInt2:
override val empty: Int = 0
def combine(a1: Int, a2: Int): Int = a1 + a2
IntCombinator.combineAll(List(1, 2, 3)) // 6
Смешанная композиция
Различие trait и abstract class
- Можно расширить несколько trait-ов
- Можно расширить не более 1 абстрактного класса
- Абстрактные классы совместимы с Java
to trait or not to trait?
Для повторного использования в нескольких несвязанных классах - trait
Если поведение не будет использоваться повторно - class
Если необходимо наследовать в коде Java - abstract class
Различие trait и abstract class
trait IntCombine:
def combine(a1: Int, a2: Int): Int = a1 + a2
trait EmptyInt1:
val empty: Int = -1
trait EmptyInt2:
val empty: Int = 1
trait Combinator[A]:
def combine(a1: A, a2: A): A
val empty: A
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
object IntCombinator1 extends Combinator[Int], EmptyInt1, IntCombine
object IntCombinator2 extends Combinator[Int], EmptyInt2, IntCombine
val condition: Boolean = true
val x = Set(if condition then IntCombinator1 else IntCombinator2)
// Тип х - Set[Combinator[Int] & IntCombine]
Прозрачные trait-ы
transparent trait IntCombine:
def combine(a1: Int, a2: Int): Int = a1 + a2
transparent trait EmptyInt1:
val empty: Int = -1
transparent trait EmptyInt2:
val empty: Int = 1
trait Combinator[A]:
def combine(a1: A, a2: A): A
val empty: A
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
object IntCombinator1 extends Combinator[Int], EmptyInt1, IntCombine
object IntCombinator2 extends Combinator[Int], EmptyInt2, IntCombine
val condition: Boolean = true
val x = Set(if condition then IntCombinator1 else IntCombinator2)
// Тип х - Set[Combinator[Int]]
Примеры прозрачных trait-ов:
- Product, Serializable, Comparable
- IterableOps, StrictOptimizedSeqOps
Прозрачные trait-ы
Открытые классы
- class-ы нельзя расширять извне
- только если очень хочется
Открытые классы
open class Combinator[A](empty: A, combine: (A, A) => A):
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
Файл Combinator.scala:
object IntCombinator extends Combinator[Int](0, _ + _)
IntCombinator.combineAll(List.empty) // 0
IntCombinator.combineAll(List(1)) // 1
IntCombinator.combineAll(List(1, 2, 3)) // 6
Файл IntCombinator.scala:
- open (case) class не может быть sealed или final
- trait и abstract class по умолчанию open
- разница `class` (case class) с `sealed class` (sealed case class) в том, что первый можно расширить, "подавив" предупреждение
Открытые классы
Что кроме наследования?
trait Combinator[A]:
def combine(a1: A, a2: A): A
val empty: A
def combineAll(list: List[A]): A =
list match
case Nil => empty
case h :: t => combine(h, combineAll(t))
object IntCombinator:
private val combinator: Combinator[Int] = new:
val empty: Int = 0
def combine(a1: Int, a2: Int): Int = a1 + a2
export combinator.combineAll
IntCombinator.combineAll(List.empty) // 0
IntCombinator.combineAll(List(1)) // 1
IntCombinator.combineAll(List(1, 2, 3)) // 6
Экспортирование элементов
А где же type class?
trait Combinator[A]:
val empty: A
def combine(a1: A, a2: A): A
given Combinator[Int] with
val empty: Int = 0
def combine(a1: Int, a2: Int): Int = a1 + a2
def combineAll[A](list: List[A])(using c: Combinator[A]): A =
list match
case Nil => c.empty
case h :: t => c.combine(h, combineAll(t))
combineAll(List.empty) // 0
combineAll(List(1)) // 1
combineAll(List(1, 2, 3)) // 6
Паттерн type class
- Отделение поведения от типа
- Добавление поведения к "недоступным" типам
- Добавление поведения без изменения исходного класса
- Определение поведения без типа
- Добавление более 1 поведения к типу
Пример: cats, scalaZ
Для чего нужны type class-ы?
given Combinator[Int] with
val empty: Int = 1
def combine(a1: Int, a2: Int): Int = a1 * a2
combineAll(List(1, 2, 3, 4, 5)) // 120
trait Combinator[A]:
val empty: A
def combine(a1: A, a2: A): A
def combineAll[A](list: List[A])(using c: Combinator[A]): A =
list match
case Nil => c.empty
case h :: t => c.combine(h, combineAll(t))
Combinator для List
combineAll(List(List()))
combineAll(List(List(1), List(1, 2, 3, 4, 5)))
given list[A]: Combinator[List[A]] with
val empty: List[A] = List.empty[A]
def combine(a1: List[A], a2: List[A]): List[A] =
a1 ++ a2
combineAll(List(List(1), List(1, 2, 3, 4, 5)))
// List(1, 1, 2, 3, 4, 5)
case class NonEmptyList[A](head: A, tail: List[A])
given Combinator[NonEmptyList[Int]] with
val empty: NonEmptyList[Int] = ...
def combine(
l1: NonEmptyList[Int],
l2: NonEmptyList[Int]): NonEmptyList[Int] =
NonEmptyList(head = l1.head,
tail = l1.tail ++ (l2.head :: l2.tail))
Ошибки при декомпозиции
trait Combinator[A]:
val empty: A
def combine(a1: A, a2: A): A
given Combinator[NonEmptyList[Int]] with
val empty: NonEmptyList[Int] = ...
Ошибки при декомпозиции
given Combinator[NonEmptyList[Int]] with
val empty: NonEmptyList[Int] = null
val empty1: NonEmptyList[Int] = throw new Exception("")
var empty2: NonEmptyList[Int] = _
val empty3: NonEmptyList[Int] = ???
def combine ...
Перекладывание ответственности на клиента
"Родитель" == "Потомок"
Почему так делать не стоит?
trait Combinator[A]:
val empty: Option[A] = None
def combine(a1: A, a2: A): A
case class NonEmptyList[A](head: A, tail: List[A])
given Combinator[NonEmptyList[Int]] with
def combine ...
God object
trait Combinator[A]:
def combine(a1: A, a2: A): A
trait FullCombinator[A] extends Combinator[A]:
val empty: A
def combineAll[A](list: List[A],
empty: A)(using c: Combinator[A]): A =
list match
case Nil => empty
case h :: t => c.combine(h, combineAll(t, empty))
def combineAll[A](list: List[A])(using c: FullCombinator[A]): A =
combineAll(list, c.empty)
Выделение более абстрактной структуры
Запрос "недостающей" информации у клиента
Выделение "родителя"
- Trait-ы отлично подходят для модуляции компонентов и описания интерфейсов (с обязательными (абстрактными) и предоставляемыми (определенными) службами)
- отделение данных от операций над ними (по сути выделение поведения в отдельные структуры) позволяет расширять и поддерживать их (почти) независимо друг от друга
- возможность "отсечь лишнее" (export, transparent) помогает предоставлять разработчикам-клиентам только необходимую функциональность и избегать ошибок
- при построении архитектуры желательно избегать "ложного наследования": попытки отсечь часть родительской функциональности
Основные выводы
Статья
Слайды
Вопросы?
Декомпозиция в Scala 3
By artemkorsakov
Декомпозиция в Scala 3
- 121