Трейты
Назначение трейтов
Трейт определяет набор методов с полными сигнатурами (типами параметров и результата). Эти методы доступны для вызова через контекст, в котором находится экземпляр соответствующего трейта.
Если структуры определяют неизменяемые данные, то трейты служат антиподами:
- Трейты определяют не данные, а программный интерфейс (сигнатуры методов), что обеспечивает абстракцию и уменьшает связанность.
- Трейты могут служить замыканием
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-выражение без указания имени трейта. Но должны выполняться условия:
-
Имя трейта берётся от имени глобального метода, в котором (синтаксически) используется такое
new-выражение. Поэтому рекомендуется, чтобы оно было с заглавной буквы. -
В глобальном методе можно использовать только одно такое
new-выражение. -
Сигнатуры методов в таком
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, а более специфичный тип.
Обобщения (Параметрический полиморфизм)
Определение трейта поддерживает использование переменных типа при помощи обобщения (параметрического полиморфизма). См. соответствующее описание.