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

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

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

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

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

Пакеты создаются путем объявления одного или нескольких имен пакетов в начале файла 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

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

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

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

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

Первая категория операторов импорта аналогична тому, что использует 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 несколько элементов, их можно перечислить перед использованием *:

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

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

val arr = new ArrayList[String]
// arr: ArrayList[String] = []

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

val a = List(1, 2, 3)
// a: List[Int] = List(1, 2, 3)
val b = Set(1, 2, 3)
// b: Set[Int] = Set(1, 2, 3)
val c = Map(1 -> 1, 2 -> 2)
// c: Map[Int, Int] = Map(1 -> 1, 2 -> 2)

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

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

package foo

import scala.util.Random

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

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

package foo

class ClassA:
  import scala.util.Random   // внутри ClassA
  def printRandom {
    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)
// a: Double = 0.0
val b = cos(PI)
// b: Double = -1.0

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

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

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

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

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

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

package accounts

import _root_.accounts.*

Импорт given

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

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

object B:
  import A.*       // import all non-given members
  import A.given   // import the given instance

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

object B:
  import A.{given, *}

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

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

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

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

import A.{given TC}

Если импортируется только один given, то фигурные скобки можно опустить:

import A.given TC

Импорт данных нескольких типов T1,...,Tn выражается несколькими given селекторами:

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

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

object Instances:
  given intOrd as Ordering[Int]
  given listOrd[T: Ordering] as Ordering[List[T]]
  given ec as ExecutionContext = ...
  given im as 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"
        case _ =>  "Other"

  given stringMonthConverter: MonthConverter[String] with
    def convert(s: String): String = 
      s match
        case "jan" => "January"
        case "feb" => "February"
        case _ =>  "Other"

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

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

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

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

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

genericMonthConverter(1)
// res1: String = "January"
genericMonthConverter("jan")
// res2: String = "January"

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


Ссылки: