Scala 3 — Book

Пакеты и импорт

Language

Scala использует packages для создания пространств имен, которые позволяют модульно разбивать программы. Scala поддерживает стиль именования пакетов, используемый в Java, а также нотацию пространства имен “фигурные скобки”, используемую такими языками, как C++ и C#.

Подход Scala к импорту похож на Java, но более гибкий. С помощью Scala можно:

  • импортировать пакеты, классы, объекты, trait-ы и методы
  • размещать операторы импорта в любом месте
  • скрывать и переименовывать участников при импорте

Эти особенности демонстрируются в следующих примерах.

Создание пакета

Пакеты создаются путем объявления одного или нескольких имен пакетов в начале файла Scala. Например, если ваше доменное имя acme.com и вы работаете с пакетом model приложения с именем myapp, объявление пакета выглядит следующим образом:

package com.acme.myapp.model

class Person ...

По соглашению все имена пакетов должны быть строчными, а формальным соглашением об именах является <top-level-domain>.<domain-name>.<project-name>.<module-name>.

Хотя это и не обязательно, имена пакетов обычно совпадают с именами иерархии каталогов. Поэтому, если следовать этому соглашению, класс Person в этом проекте будет найден в файле MyApp/src/main/scala/com/acme/myapp/model/Person.scala.

Использование нескольких пакетов в одном файле

Показанный выше синтаксис применяется ко всему исходному файлу: все определения в файле Person.scala принадлежат пакету com.acme.myapp.model в соответствии с предложением package в начале файла.

В качестве альтернативы можно написать package, которые применяются только к содержащимся в них определениям:

package users:

  package administrators:  // полное имя пакета - users.administrators
    class AdminUser        // полное имя класса - users.administrators.AdminUser

  package normalusers:     // полное имя пакета - users.normalusers
    class NormalUser       // полное имя класса - users.normalusers.NormalUser

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

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

Операторы импорта

Операторы импорта используются для доступа к сущностям в других пакетах. Операторы импорта делятся на две основные категории:

  • импорт классов, трейтов, объектов, функций и методов
  • импорт given предложений

Первая категория операторов импорта аналогична тому, что использует Java, с немного другим синтаксисом, обеспечивающим большую гибкость. Пример:

import users.*                            // импортируется все из пакета `users`
import users.User                         // импортируется только класс `User`
import users.{User, UserPreferences}      // импортируются только два члена пакета
import users.{UserPreferences as UPrefs}  // переименование импортированного члена

Эти примеры предназначены для того, чтобы дать представление о том, как работает первая категория операторов import. Более подробно они объясняются в следующих подразделах.

Операторы импорта также используются для импорта given экземпляров в область видимости. Они обсуждаются в конце этой главы.

import не требуется для доступа к членам одного и того же пакета.

Импорт одного или нескольких членов

В Scala импортировать один элемент из пакета можно следующим образом:

import scala.concurrent.Future

несколько:

import scala.concurrent.Future
import scala.concurrent.Promise
import scala.concurrent.blocking

При импорте нескольких элементов их можно импортировать более лаконично:

import scala.concurrent.{Future, Promise, blocking}

Если необходимо импортировать все из пакета scala.concurrent, используется такой синтаксис:

import scala.concurrent.*

Переименование элементов при импорте

Иногда необходимо переименовать объекты при их импорте, чтобы избежать конфликтов имен. Например, если нужно использовать Scala класс List вместе с java.util.List, то можно переименовать java.util.List при импорте:

import java.util.{List as JavaList}

Теперь имя JavaList можно использовать для ссылки на класс java.util.List и использовать List для ссылки на Scala класс List.

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

import java.util.{Date as JDate, HashMap as JHashMap, *}

В этой строке кода говорится следующее: “Переименуйте классы Date и HashMap, как показано, и импортируйте все остальное из пакета java.util, не переименовывая”.

Скрытие членов при импорте

При импорте часть объектов можно скрывать. Следующий оператор импорта скрывает класс java.util.Random, в то время как все остальное в пакете java.util импортируется:

import java.util.{Random as _, *}

Если попытаться получить доступ к классу Random, то выдается ошибка, но есть доступ ко всем остальным членам пакета java.util:

val r = new Random   // не скомпилируется
new ArrayList        // доступ есть

Скрытие нескольких элементов

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

scala> import java.util.{List as _, Map as _, Set as _, *}

Перечисленные классы скрыты, но можно использовать все остальное в java.util:

scala> new ArrayList[String]
val res0: java.util.ArrayList[String] = []

Поскольку эти Java классы скрыты, можно использовать классы Scala List, Set и Map без конфликта имен:

scala> val a = List(1, 2, 3)
val a: List[Int] = List(1, 2, 3)

scala> val b = Set(1, 2, 3)
val b: Set[Int] = Set(1, 2, 3)

scala> val c = Map(1 -> 1, 2 -> 2)
val c: Map[Int, Int] = Map(1 -> 1, 2 -> 2)

Импорт можно использовать в любом месте

В Scala операторы import могут быть объявлены где угодно. Их можно использовать в верхней части файла исходного кода:

package foo

import scala.util.Random

class ClassA:
  def printRandom(): Unit =
    val r = new Random   // класс Random здесь доступен
    // ещё код...

Также операторы import можно использовать ближе к тому месту, где они необходимы:

package foo

class ClassA:
  import scala.util.Random   // внутри ClassA
  def printRandom(): Unit =
    val r = new Random
    // ещё код...

class ClassB:
  // класс Random здесь невидим
  val r = new Random   // этот код не скомпилится

“Статический” импорт

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

Синтаксис для импорта всех статических членов Java класса Math:

import java.lang.Math.*

Теперь можно получить доступ к статическим методам класса Math, таким как sin и cos, без необходимости предварять их именем класса:

import java.lang.Math.*

val a = sin(0)    // 0.0
val b = cos(PI)   // -1.0

Пакеты, импортированные по умолчанию

Два пакета неявно импортируются во все файлы исходного кода:

  • java.lang.*
  • scala.*

Члены object Predef также импортируются по умолчанию.

Например, такие классы, как List, Vector, Map и т.д. можно использовать явно, не импортируя их - они доступны, потому что определены в object Predef

Обработка конфликтов имен

Если необходимо импортировать что-то из корня проекта и возникает конфликт имен, достаточно просто добавить к имени пакета префикс _root_:

package accounts

import _root_.accounts.*

Импорт экземпляров given

Как будет показано в главе “Контекстные абстракции”, для импорта экземпляров given используется специальная форма оператора import. Базовая форма показана в этом примере:

object A:
  class TC
  given tc: TC
  def f(using TC) = ???

object B:
  import A.*       // импорт всех non-given членов
  import A.given   // импорт экземпляров given

В этом коде предложение import A.* объекта B импортирует все элементы A, кроме given экземпляра tc. И наоборот, второй импорт, import A.given, импортирует только given экземпляр. Два предложения импорта также могут быть объединены в одно:

object B:
  import A.{given, *}

Обсуждение

Селектор с подстановочным знаком * помещает в область видимости все определения, кроме given, тогда как селектор выше помещает в область действия все данные, включая те, которые являются результатом расширений.

Эти правила имеют два основных преимущества:

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

Импорт по типу

Поскольку given-ы могут быть анонимными, не всегда практично импортировать их по имени, и вместо этого обычно используется импорт подстановочных знаков. Импорт по типу предоставляет собой более конкретную альтернативу импорту с подстановочными знаками, делая понятным то, что импортируется.

import A.{given TC}

Этот код импортирует из A любой given тип, соответствующий TC. Импорт данных нескольких типов T1,...,Tn выражается несколькими given селекторами:

import A.{given T1, ..., given Tn}

Импорт всех given экземпляров параметризованного типа достигается аргументами с подстановочными знаками. Например, есть такой объект:

object Instances:
  given intOrd: Ordering[Int]
  given listOrd[T: Ordering]: Ordering[List[T]]
  given ec: ExecutionContext = ...
  given im: Monoid[Int]

Оператор import ниже импортирует экземпляры intOrd, listOrd и ec, но пропускает экземпляр im, поскольку он не соответствует ни одному из указанных шаблонов:

import Instances.{given Ordering[?], given ExecutionContext}

Импорт по типу можно смешивать с импортом по имени. Если оба присутствуют в предложении import, импорт по типу идет последним. Например, это предложение импорта импортирует im, intOrd и listOrd, но не включает ec:

import Instances.{im, given Ordering[?]}

Пример

В качестве конкретного примера представим, что у нас есть объект MonthConversions, который содержит два определения given:

object MonthConversions:
  trait MonthConverter[A]:
    def convert(a: A): String

  given intMonthConverter: MonthConverter[Int] with
    def convert(i: Int): String =
      i match
        case 1 =>  "January"
        case 2 =>  "February"
        // остальные случаи здесь ...

  given stringMonthConverter: MonthConverter[String] with
    def convert(s: String): String =
      s match
        case "jan" => "January"
        case "feb" => "February"
        // остальные случаи здесь ...

Чтобы импортировать эти given-ы в текущую область, используем два оператора import:

import MonthConversions.*
import MonthConversions.{given MonthConverter[?]}

Теперь создаем метод, использующий эти экземпляры:

def genericMonthConverter[A](a: A)(using monthConverter: MonthConverter[A]): String =
  monthConverter.convert(a)

Вызов метода:

@main def main =
  println(genericMonthConverter(1))       // January
  println(genericMonthConverter("jan"))   // January

Как уже упоминалось ранее, одно из ключевых преимуществ синтаксиса “import given” состоит в том, чтобы прояснить, откуда берутся данные в области действия, и в import операторах выше ясно, что данные поступают из объекта MonthConversions.

Contributors to this page: