Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Трейты

Назначение трейтов

Трейт определяет набор методов с полными сигнатурами (типами параметров и результата). Эти методы доступны для вызова через контекст, в котором находится экземпляр соответствующего трейта.

Если структуры определяют неизменяемые данные, то трейты служат антиподами:

  • Трейты определяют не данные, а программный интерфейс (сигнатуры методов), что обеспечивает абстракцию и уменьшает связанность.
  • Трейты могут служить замыканием var переменных при создании. Это позволяет создавать изменяемые объекты (объекты с внутренним состоянием), тогда как значение полей структуры изменить нельзя.
  • Трейты могут создаваться разными способами, а структуры только единственным.
  • В качестве трейта можно подавать преобразованные экземпляры других трейтов или структур. Тогда как структуры между собой не преобразуются.

ВНИМАНИЕ: Поддержка трейтов в Libretto ещё не реализована полностью.

Определение трейта

Трейт определяется при помощи ключевого слова trait и далее следующего опционального перечисления сигнатур методов при помощи def, но без указания контекста и тела методов.

Трейт может быть пустой:

trait Test

Или так

trait Test {    
}

Это означает, что никаких требований к сигнатуре методов нет.

Но на практике в трейте всё же определяются методы.

Один метод:

trait Test {
  def value: Int
}

Или несколько:

trait Test {
  def value: Int
  def add(v: Int): Int
  def string: String
}

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

trait Test {
  def value: Int
  def add(v: Int): Int
  def string: String
}

def work(t: Test) = {
  println: t.value // метод из трейта
  println: t.add(777) // метод из трейта
  println: string // метод из трейта
}

Полное создание экземпляра трейта (объекта)

Сам трейт описывает только некоторую сущность, в контексте которой доступны указанные методы. Но надо как-то эту сущность создать. Один из способов - это создание экземпляра трейта при помощи new-выражения.

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

Результатом new-выражения будет значение указанного типа трейта.

trait Test {
  def value: Int
  def add(v: Int): Int
  def string: String
}

def work(t: Test) = {
  println: t.string
  println: t.value
  println: t.add(777)  
}

def test(): Test = {
  new Test {
    def value: Int = 123
    def add(v: Int): Int = 123 + v
    def string: String = "Test(123)"
  }
}

def main = {
  work(test())
}

Выдаёт:

Test(123)
123
900

Вывод типов методов

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

trait Test {
  def value: Int
  def add(v: Int): Int
  def string: String
}

def work(t: Test) = {
  println: t.string
  println: t.value
  println: t.add(777)  
}

def test(): Test = {
  new Test {
    def value = 123
    def add(v) = 123 + v // здесь v: Int
    def string = "Test(123)"
  }
}

def main = {
  work(test())
}

Выдаёт:

Test(123)
123
900

Но в сложных моделях рекомендуется полностью указывать типы при определении методов в new. Это упрощает чтение кода и дает защиту от некоторых ошибок.

Замыкания

Методы трейта являются замыканиями, позволяющими получать доступ к переменным кода, в котором вызывается new.

trait Test {
  def value: Int
  def add(v: Int): Int
  def string: String
}

def work(t: Test) = {
  println: t.string
  println: t.value
  println: t.add(777)  
}

def test(i: Int): Test = {
  new Test {
    def value = i
    def add(v) = i + v
    def string = "Test(" + i.string + ")"
  }
}

def main = {
  work(test(123))
}

Выдаёт:

Test(123)
123
900

Более того, использование в замыкании var-переменных позволяет создавать экземпляры трейтов, которые имеют внутреннее изменяемое состояние, т.е. они являются аналогами объектов:

trait Counter {
  def inc: ()
  def `!`: Int
}

def counter(start: Int) = {
  var value = start
  new Counter {
    def inc = {
      value = value + 1 // работа с var, а не с полем
    }
    def `!` = value // работа с var, а не с полем
  }
}

def main = {
  fix counter = counter(100) // создание объекта
  println("value: " + counter.!)
  println("value: " + counter.!)
  counter.inc
  println("after inc: " + counter.!)
  counter.inc
  counter.inc
  println("after inc and inc: " + counter.!)
}

Выдаёт:

value: 100
value: 100
after inc: 101
after inc and inc: 103

Более подробно см. описание замыкания.

Преобразование в трейт

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

ВНИМАНИЕ: в настоящее время возможности автоматического или ручного преобразования в трейт ограничены и требуют доработки компилятора. Но некоторые простые виды преобразований уже работают.

Ручное преобразование в трейт

ВНИМАНИЕ: в настоящее время возможности автоматического или ручного преобразования в трейт ограничены и требуют доработки компилятора. Но некоторые простые виды преобразований уже работают.

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

trait Test {
  def value: Int
}

struct S(value: Int)

def work(test: Test) = {
  println(test.value)
}

def main = {
  fix test = Test(S(123))
  work(test)
}

Выдаёт:

123

В этом примере экземпляр структуры S подходит для преобразования в трейт Test, поскольку для S определен метод value, которые требует трейт Test. Само ручное преобразование происходит при помощи вызова метода Test.

trait Test {
  def value: Int
}

def Int value = this

def work(test: Test) = {
  println(test.value)
}

def main = {
  fix test = Test(123)
  work(test)
}

Выдаёт:

123

Здесь в трейт Test преобразуется значение типа Int, поскольку определен требуемый метод value в контексте Int.

trait Test {
  def value: Int
}

trait Test2 {
  def value: Int
  def string: String
}

def work(test: Test) = {
  println(test.value)
}

def main = {
  fix test2 = new Test2 {
    def value = 123
    def string = "abc"
  }
  fix test = Test(test2)
  work(test)
}

Выдаёт:

123

В этом примере более специфичный трейт Test2 вручную преобразуется в менее специфичный Test. Методы Test2 считаются определенными в контексте Test2, поэтому этот трейт подходит для преобразования (есть требуемый метод value).

Автоматическое преобразование в трейт

ВНИМАНИЕ: в настоящее время возможности автоматического или ручного преобразования в трейт ограничены и требуют доработки компилятора. Но некоторые простые виды преобразований уже работают.

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

trait Test {
  def value: Int
}

trait Test2 {
  def value: Int
  def string: String
}

def work(test: Test) = {
  println(test.value)
}

def Int value = this

struct S(value: Int)

def main = {
  fix test2 = new Test2 {
    def value = 1
    def string = "abc"
  }
  work(test2)
  work(S(2))
  work(3)
}

Выдаёт:

1
2
3

Смешанное создание экземпляра трейта

При создании экземпляра трейта при помощи new-выражения можно явно не определять все методы трейта, если есть экземпляр структуры или трейта, на котором определены недостающие методы. Этот экземпляр передаётся аргументом в скобках () после имени трейта, экземпляр которого создаётся:

trait Test {
  def a: Int
  def b: String
}

def work(t: Test) = {
  println: t.a
  println: t.b
}

struct S(a: Int)

def main = {
  fix s = S(123)
  fix t = new Test(s) {
    // определение a будет взято из s
    def b = "abc"    
  }
  work(t)
}

Выдаёт:

123
abc

В данном примере определение метода a будет использовано для экземпляра s (т.е. поле структуры S), а метод b определяется в теле new-выражения.

Определение в теле new-выражения всегда приоритетно:

trait Test {
  def a: String
  def b: String
}

def work(t: Test) = {
  println: t.a
  println: t.b
}

struct S(a: String, b: String)

def main = {
  fix s = S("a for S", "b for S")
  fix t = new Test(s) {
    def a = "a"
    // b взято из S
  }
  work(t)
}

Выдаёт

a
b for S

todo: поддержка нескольких экземпляров в аргументах?

this в методах трейта

При создании экземпляра трейта при помощи new-выражения можно получить доступ до этого экземпляра в определяемых методах, используя this:

trait Test {  
  def a: Int
  def b: String
}
 

def work(t: Test) = {
  println(t.a)
  println(t.b)
}

def main = {
  fix t = new Test {
    def a = 123
    def b = this.a.string
  }
  work(t)
}

Выдаёт:

123
123

Строковое значение трейта

todo

Трейт с именем метода (экспериментальная возможность)

ВНИМАНИЕ: экспериментальная возможность

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

Поэтому есть вариант объединения определения трейта и его создания. Для этого используется new-выражение без указания имени трейта. Но должны выполняться условия:

  1. Имя трейта берётся от имени глобального метода, в котором (синтаксически) используется такое new-выражение. Поэтому рекомендуется, чтобы оно было с заглавной буквы.

  2. В глобальном методе можно использовать только одно такое new-выражение.

  3. Сигнатуры методов в таком new-выражении должны быть полными (в том числе с указанием полного типа результата).

Метод, использующий new-выражение без имени трейта, можно считать конструктором.

def Counter(start: Int) = {
  var value = start
  new {
    def inc: () = {
      value = value + 1 // работа с var, а не с полем
    }
    def `!`: Int = value // работа с var, а не с полем
  }
}

def main = {
  fix counter = Counter(100) // создание объекта
  println("value: " + counter.!)
  println("value: " + counter.!)
  counter.inc
  println("after inc: " + counter.!)
  counter.inc
  counter.inc
  println("after inc and inc: " + counter.!)
}

Выдаёт:

value: 100
value: 100
after inc: 101
after inc and inc: 103

В этом примере в методе Counter одновременно определяется трейт

trait Counter {
  def inc: ()
  def `!`: Int
}

и создаётся его экземпляр.

Использование new-выражения без указания имени трейта не означает, что создание экземпляра трейта возможно только в этом месте. Создать экземпляр можно, например, по имени метода, в котором такое new-выражение используется:

def Test(v: Int) = {
  new {
    def value: Int = v
  }
}

def work(t: Test) = {
  println("Value: " + t.value)
}

def main = {
  work(Test(123))
  // свой экземпляр трейта Test
  fix t = new Test {
    def value = 777
  }
  work(t)
}

Выдаёт:

Value: 123
Value: 777

Доступно и автоматическое преобразование:

def Test(v: Int) = {
  new {
    def value: Int = v
  }
}

def work(t: Test) = {
  println("Value: " + t.value)
}

def Int value = this

def main = {
  work(Test(123))
  work(777)
}

Выдаёт:

Value: 123
Value: 777

Безопасность

При любом способе создания (полное, смешанное, преобразование в трейт) экземпляр трейта является самостоятельной сущностью. Её никак нельзя преобразовать обратно в исходный материал, на базе которого сделан экземпляр трейта. Это заложенная в Libretto безопасность. Код работает только с тем уровнем абстракции, что ему положен.

trait Test {
  def a: String
}

def work(t: Test) = {
  println(t.a)
  t.*{
    case s: S = println("Found S: " + s)
    else = println("Not found")
  }
}

struct S(a: String)

def main = {
  fix s = S("abc")
  work(s) // автопреобразование в Test
}

Выдаёт:

abc
Not found

Хотя экземпляр Test получен из экземпляра структуры S, но динамическая проверка типов не позволяет получить доступ к оригинальному экземпляру S.

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

trait Test {
  def a: String
  def source: Any
}

def work(t: Test) = {
  println(t.a)
  t.source.*{
    case s: S = println("Found S: " + s)
    else = println("Not found")
  }
}

struct S(a: String)
def S source = this

def main = {
  fix s = S("abc")
  work(s) // автопреобразование в Test
}

Выдаёт:

abc
Found S: S(abc)

В данном случае сам экземпляр S передаёт доступ к себе через определение метода source, который трейт Test использует для доступа к источнику.

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

Обобщения (Параметрический полиморфизм)

Определение трейта поддерживает использование переменных типа при помощи обобщения (параметрического полиморфизма). См. соответствующее описание.