Scala 3 — Book

Моделирование данных

Language

Scala поддерживает как функциональное программирование (ФП), так и объектно-ориентированное программирование (ООП), а также слияние этих двух парадигм. В этом разделе представлен краткий обзор моделирования данных в ООП и ФП.

Моделирование данных в ООП

При написании кода в стиле ООП двумя вашими основными инструментами для инкапсуляции данных будут трейты и классы.

Трейты

Трейты Scala можно использовать как простые интерфейсы, но они также могут содержать абстрактные и конкретные методы и поля, а также параметры, как и классы. Они предоставляют вам отличный способ организовать поведение в небольшие модульные блоки. Позже, когда вы захотите создать конкретные реализации атрибутов и поведения, классы и объекты могут расширять трейты, смешивая столько трейтов, сколько необходимо для достижения желаемого поведения.

В качестве примера того, как использовать трейты в качестве интерфейсов, вот три трейта, которые определяют хорошо организованное и модульное поведение для животных, таких как собаки и кошки:

trait Speaker {
  def speak(): String  // тело метода отсутствует, поэтому метод абстрактный
}

trait TailWagger {
  def startTail(): Unit = println("tail is wagging")
  def stopTail(): Unit = println("tail is stopped")
}

trait Runner {
  def startRunning(): Unit = println("I’m running")
  def stopRunning(): Unit = println("Stopped running")
}
trait Speaker:
  def speak(): String  // тело метода отсутствует, поэтому метод абстрактный

trait TailWagger:
  def startTail(): Unit = println("tail is wagging")
  def stopTail(): Unit = println("tail is stopped")

trait Runner:
  def startRunning(): Unit = println("I’m running")
  def stopRunning(): Unit = println("Stopped running")

Учитывая эти трейты, вот класс Dog, который их все расширяет, обеспечивая при этом поведение для абстрактного метода speak:

class Dog(name: String) extends Speaker with TailWagger with Runner {
  def speak(): String = "Woof!"
}
class Dog(name: String) extends Speaker, TailWagger, Runner:
  def speak(): String = "Woof!"

Обратите внимание, как класс расширяет трейты с помощью ключевого слова extends.

Точно так же вот класс Cat, реализующий те же трейты, а также переопределяющий два конкретных метода, которые он наследует:

class Cat(name: String) extends Speaker with TailWagger with Runner {
  def speak(): String = "Meow"
  override def startRunning(): Unit = println("Yeah ... I don’t run")
  override def stopRunning(): Unit = println("No need to stop")
}
class Cat(name: String) extends Speaker, TailWagger, Runner:
  def speak(): String = "Meow"
  override def startRunning(): Unit = println("Yeah ... I don’t run")
  override def stopRunning(): Unit = println("No need to stop")

Примеры ниже показывают, как используются эти классы:

val d = new Dog("Rover")
println(d.speak())      // печатает "Woof!"

val c = new Cat("Morris")
println(c.speak())      // "Meow"
c.startRunning()        // "Yeah ... I don’t run"
c.stopRunning()         // "No need to stop"
val d = Dog("Rover")
println(d.speak())      // печатает "Woof!"

val c = Cat("Morris")
println(c.speak())      // "Meow"
c.startRunning()        // "Yeah ... I don’t run"
c.stopRunning()         // "No need to stop"

Если этот код имеет смысл — отлично, вам удобно использовать трейты в качестве интерфейсов. Если нет, не волнуйтесь, они более подробно описаны в главе “Моделирование предметной области”.

Классы

Классы Scala используются в программировании в стиле ООП. Вот пример класса, который моделирует “человека”. В ООП поля обычно изменяемы, поэтому оба, firstName и lastName объявлены как var параметры:

class Person(var firstName: String, var lastName: String) {
  def printFullName() = println(s"$firstName $lastName")
}

val p = new Person("John", "Stephens")
println(p.firstName)   // "John"
p.lastName = "Legend"
p.printFullName()      // "John Legend"
class Person(var firstName: String, var lastName: String):
  def printFullName() = println(s"$firstName $lastName")

val p = Person("John", "Stephens")
println(p.firstName)   // "John"
p.lastName = "Legend"
p.printFullName()      // "John Legend"

Обратите внимание, что объявление класса создает конструктор:

// код использует конструктор из объявления класса
val p = new Person("John", "Stephens")
// код использует конструктор из объявления класса
val p = Person("John", "Stephens")

Конструкторы и другие темы, связанные с классами, рассматриваются в главе “Моделирование предметной области”.

Моделирование данных в ФП

При написании кода в стиле ФП вы будете использовать следующие понятия:

  • Алгебраические типы данных для определения данных.
  • Трейты для функциональности данных.

Перечисления и суммированные типы

Суммированные типы (sum types) — это один из способов моделирования алгебраических типов данных (ADT) в Scala.

Они используются, когда данные могут быть представлены с различными вариантами.

Например, у пиццы есть три основных атрибута:

  • Размер корки
  • Тип корки
  • Начинки
  • Они кратко смоделированы с помощью перечислений, которые представляют собой суммированные типы, содержащие только одноэлементные значения:

В Scala 2 sealed классы и case object объединяются для определения перечисления:

sealed abstract class CrustSize
object CrustSize {
  case object Small extends CrustSize
  case object Medium extends CrustSize
  case object Large extends CrustSize
}

sealed abstract class CrustType
object CrustType {
  case object Thin extends CrustType
  case object Thick extends CrustType
  case object Regular extends CrustType
}

sealed abstract class Topping
object Topping {
  case object Cheese extends Topping
  case object Pepperoni extends Topping
  case object BlackOlives extends Topping
  case object GreenOlives extends Topping
  case object Onions extends Topping
}

Scala 3 предлагает конструкцию enum для определения перечислений:

enum CrustSize:
  case Small, Medium, Large

enum CrustType:
  case Thin, Thick, Regular

enum Topping:
  case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions

Когда у вас есть перечисление, вы можете импортировать его элементы как обычные значения:

import CrustSize._
val currentCrustSize = Small

// перечисления в сопоставлении с шаблоном
currentCrustSize match {
  case Small => println("Small crust size")
  case Medium => println("Medium crust size")
  case Large => println("Large crust size")
}

// перечисления в операторе `if`
if (currentCrustSize == Small) println("Small crust size")
import CrustSize.*
val currentCrustSize = Small

// перечисления в сопоставлении с шаблоном
currentCrustSize match
  case Small => println("Small crust size")
  case Medium => println("Medium crust size")
  case Large => println("Large crust size")

// перечисления в операторе `if`
if currentCrustSize == Small then println("Small crust size")

Вот еще один пример того, как создать суммированные типы с помощью Scala, это не будет называться перечислением, потому что у случая Succ есть параметры:

sealed abstract class Nat
object Nat {
  case object Zero extends Nat
  case class Succ(pred: Nat) extends Nat
}

Суммированные типы подробно рассматриваются в разделе “Моделирование предметной области” этой книги.

enum Nat:
  case Zero
  case Succ(pred: Nat)

Перечисления подробно рассматриваются в разделе “Моделирование предметной области” этой книги и в справочной документации.

Продуктовые типы

Тип продукта — это алгебраический тип данных (ADT), который имеет только одну форму, например, одноэлементный объект, представленный в Scala case object; или неизменяемая структура с доступными полями, представленная case class.

case class обладает всеми функциями класса, а также содержит встроенные дополнительные функции, которые делают его полезным для функционального программирования. Когда компилятор видит ключевое слово case перед class, то применяет следующие эффекты и преимущества:

  • Параметры конструктора case class по умолчанию являются общедоступными полями val, поэтому поля неизменяемы, а методы доступа генерируются для каждого параметра.
  • Генерируется метод unapply, который позволяет использовать case class в выражениях match различными способами.
  • В классе создается метод copy. Он позволяет создавать копии объекта без изменения исходного.
  • Создаются методы equals и hashCode для реализации структурного равенства.
  • Генерируется метод по умолчанию toString, полезный для отладки.

Вы можете вручную добавить все эти методы в класс самостоятельно, но, поскольку эти функции так часто используются в функциональном программировании, использование case класса гораздо удобнее.

Этот код демонстрирует несколько функций case class:

// определение case class
case class Person(
  name: String,
  vocation: String
)

// создание экземпляра case class
val p = Person("Reginald Kenneth Dwight", "Singer")

// полезный метод toString
p                // : Person = Person(Reginald Kenneth Dwight,Singer)

// можно получить доступ к неизменяемым полям
p.name           // "Reginald Kenneth Dwight"
p.name = "Joe"   // error: can’t reassign a val field

// при необходимости внести изменения используйте метод `copy`
// для “update as you copy”
val p2 = p.copy(name = "Elton John")
p2               // : Person = Person(Elton John,Singer)

Дополнительные сведения о case классах см. в разделах “Моделирование предметной области”.

Contributors to this page: