Структуры
Назначение структур
Структура - это базовый тип Libretto.
Структуры отличаются от трейтов:
- Структуры определяют набор (кортеж) данных, тогда как трейты определяют программный интерфейс (набор методов).
- Для доступа данным структур используются поля (но не всегда).
- Структуры всегда являются неизменяемыми.
- Для создания структуры не нужен дополнительный код, автоматически определяется метод создания (с именем структуры).
- Структуры создаются только одним способом, нельзя преобразовать структуру одного типа в структуру другого типа.
Определение структуры
Структура определяется при помощи ключевого слова struct, локального имени и далее следующего опционального перечисления имен полей и их типов.
Допускается структура без полей: struct Test
Структура с полями (короткая запись): struct Test(a: A, b: B, ...), где a, b и далее - это имена полей (имена методов, через которые будут доступны значения полей), а A, B и далее - это типы полей (можно использовать кардинальности).
todo Структура с полями (полная запись)
В качестве типа полей могут выступать и другие структуры, и даже трейты:
trait Work {
def work: Int
}
struct Env(env: String)
struct Test(value: Int, env: Env*, work: Work?)
Типы поля могут быть и рекурсивные:
struct Test(test: Test)
Но в таком виде в структуре нет смысла, поскольку невозможно будет создать экземпляр: нет отправной точки, поскольку для создания любого Test нужен любой Test
А в таком виде в этом есть смысл:
struct Test(test: Test?)
def main = {
fix t = Test(Test(()))
}
Отправной точкой для создания Test будет пустота ().
Создание экземпляра структуры
Для создания экземпляра структуры используется метод, имя которого совпадает с именем структуры, а сам он находится в том же пакете, что и структура. Параметры метода совпадают (по числу, порядку перечисления и типу) с определением полей структуры.
Например:
struct Test(value: Int, comments: String*)
def main = {
fix test1 = Test(1, ("one", "один"))
fix test2 = Test(2, "two")
fix test3 = Test(3, ())
}
Здесь определяется структура Test с двумя полями (value и comments). И создаются три экземпляра структуры Test.
Все поля при создании должны быть указаны в соответствии с типами полей.
Поскольку для создания структуры используется именно метод, то его можно вызывать всем допустимыми для метода способами:
struct Test(value: Int, comments: String*)
def main = {
println: Test(1, ("one", "один"))
println: Test(1): ("one", "один")
println: 1.*Test: ("one", "один")
}
Выдаёт:
Test(1,(one,один))
Test(1,(one,один))
Test(1,(one,один))
Если полей нет, то по для создания экземпляра структуры используется метод без параметров (и без скобок):
struct Test
def main = {
fix test1 = Test
fix test2 = Test
fix test3 = Test
}
Есть важное различие между структурой с полями и структурой без полей. В текущей версии компилятора создание структур с полями приводит к созданию нового экземпляра, даже если значения полей одинаковые:
struct Test(v: Int)
def main = {
fix test1 = Test(1)
fix test2 = Test(1)
// test1 и test2 - это разные экземпляры
}
Здесь test1 и test2 это разные (пусть и равные) экземпляры структуры. В будущем, возможно, это поведение будет изменено.
А вот создание структуры без полей не приводит как к таковому созданию нового экземпляра. Всегда возвращается один и тот же экземпляр:
struct Test
def main = {
fix test1 = Test
fix test2 = Test
// test1 и test2 - это один и тот же экземпляр
}
Доступ к полям структуры
Для доступа к полям структуры (к значениям, что были переданы при создании экземпляра структуры) автоматически определяются методы:
- в том же пакете, что и структура;
- в контексте структуры;
- имя соответствует имени поля в определении структуры;
- без параметров;
- результат соответствует типу поля в определении структуры.
struct Test(value: Int)
def main = {
fix test = Test(123)
fix v = test.value
println: v
println: v + 777
}
Выдаёт:
123
900
Предопределенные структуры libretto: Int, Real, String, Unit
Есть предопределенные типы из пакета libretto, которые тоже являются структурами, хотя и отличаются использованием от обычных структур:
IntRealStringUnit
Особенности этих структур:
- Нет методов для доступа к полям, а значением является сам экземпляр структуры.
- Для создания экземпляров используются только литералы или ключевое слово (для
Unitможно использоватьunitилиUnit).
Неизменность структуры
Сам экземпляр структуры является неизменным: нельзя поменять значение поля. Можно только создать новый экземпляр структуры при помощи копирования с заменой значения.
Но если структура в поле содержит трейт, то трейт может иметь собственное изменяемое состояние:
trait T {
def next: Int
}
def t = {
var i = 0
new T {
def next = i.${ i = i + 1 }
}
}
struct S(t: T)
def main = {
fix s = S(t)
println: s.t.next
println: s.t.next
println: s.t.next
}
Выдаёт:
0
1
2
Хотя структура S сама по себе является неизменяемой, т.е. нельзя заменить значение поля t, но состояние трейта T, что содержится в этом поле, может быть изменено, поскольку это допускается для трейтов.
Поэтому строго неизменяемая структура - это структура, типами полей которой тоже являются строго неизменяемые структуры. Предопределенные структуры являются строго неизменяемыми структурами. Any не считается строго неизменной структурой. Объединение считается строго неизменяемым, если включает только строго неизменяемые структуры.
Строковое значение структуры
Для структуры автоматически определяется метод string, возвращающий строковое представление: локальное имя структуры, а затем в скобках перечисление полей в порядке, заданном определением.
struct Test(i: Int, s: String, v: String*, nested: Nested)
struct Nested(r: Real)
def main = {
fix t = Test(123, "777", ("abc", "def"), Nested(3.14))
println: t
println: t.string
}
Выдаёт:
Test(123,777,(abc,def),Nested(3.14))
Test(123,777,(abc,def),Nested(3.14))
Строковое представление предназначено для отладочной печати, но не для сохранения/восстановления экземпляра структуры.
Можно определить собственное строковое представление, если задать метод string.
struct Test(i: Int, s: String, v: String*, nested: Nested)
struct Nested(r: Real)
def Test string = "Test with " + this.i
def main = {
fix t = Test(123, "777", ("abc", "def"), Nested(3.14))
println: t
println: t.string
}
Выдаёт:
Test with 123
Test with 123
todo См. описание специальных методов.
Сравнение структур, хэш-код, equalityKey
Для структур автоматически определяется глубокое сравнение. Экземпляр равен другому экземпляру, если они представляют одну и ту же структура и их поля (рекурсивно) равны.
struct A(i: Int*)
struct B(v: String, a: A)
def main = {
println: B("abc", A((1, 2))) == B("abc", A((1, 2)))
println: B("abc", A((1, 2))) == B("abc", A((1, 3)))
}
Выдаёт:
unit
()
Кроме сравнения автоматически определяется и подсчёт хэш-кода структуры. Если экземпляры равны, то и их хэш-коды равны. Для получения хэш-кода можно использовать метод hashCode из пакета libretto/util:
use libretto/util
struct A(i: Int*)
struct B(v: String, a: A)
def main = {
println: B("abc", A((1, 2))).util/hashCode
println: B("abc", A((1, 2))).util/hashCode
println: B("abc", A((1, 3))).util/hashCode
}
Выдаёт:
709761344
709761344
709761345
Это позволяет использовать структуры в качестве ключе для отображений:
use libretto/util
struct A(i: Int*)
struct B(v: String, a: A)
def main = {
fix map = map[B, String]()
map.!(B("abc", A((1, 2)))) = "test"
println: map.!(B("abc", A((1, 2))))
println: map.!(B("abc", A((1, 3))))
}
Выдаёт:
test
()
Но адекватным ключом для отображения является только строго неизменяемая структура (см. выше определение).
Если для структуры определен метод equalityKey (в строгом контексте структуры, в том же пакете, без параметров), то возвращаемое значение используется для сравнения экземпляров этой структуры и для вычисления хэш-кода.
Метод equalityKey должен быть объявлен именно методом (не полем) и на структуре хотя бы с одним полем. Иначе он не будет работать (будет обычное сравнение и обычное вычисление хэш-кода).
use libretto/util
struct Test(a: Int, b: String)
def Test equalityKey: Int* = this.a
def main = {
println: Test(1, "a") == Test(2, "a")
println: Test(1, "a").util/hashCode
println: Test(2, "a").util/hashCode
println()
println: Test(123, "abc") == Test(123, "def")
println: Test(123, "abc").util/hashCode
println: Test(123, "def").util/hashCode
println()
fix m = map[Test, String]()
m.!(Test(123, "a")) = "Hello!"
println(m.!(Test(777, "a")))
println(m.!(Test(123, "b")))
}
Выдаёт:
()
-870230462
-870230461
unit
-870230340
-870230340
()
Hello!
Другой пример:
use libretto/util
struct Test(a: Int!, b: Int!)
def Test equalityKey = this.a + this.b
def main = {
println: Test(1, 3) == Test(2, 2)
println: Test(1, 3).util/hashCode
println: Test(2, 2).util/hashCode
}
Выдаёт:
unit
-870230459
-870230459
Метод equalityKey может возвращать и последовательность для сравнения:
use libretto/util
struct Test(a: Int*, b: Int*)
def Test equalityKey = this.(a, b)
def main = {
println: Test((1, 2), 3) == Test(1, (2, 3))
println: Test((1, 2), 3).util/hashCode
println: Test(1, (2, 3)).util/hashCode
}
Выдает:
unit
-870229437
-870229437
Только экземпляры одной структуры могут считаться равными. Даже если методы equalityKey возвращают равные значения:
struct Test1(v: Int!)
struct Test2(v: Int!)
def Test1 equalityKey = this.v
def Test2 equalityKey = this.v
def main = {
println: Test1(123) == Test2(123)
println: Test1(123) == Test1(123)
println: Test2(123) == Test2(123)
}
Выдает:
()
unit
unit
Если метод equalityKey для структуры имеет тип - (Nothing кардинальностью Nothing), то сравнение этой структуры делается только по ссылке (а не по полям и не по результату equalityKey). Т.е. экземпляр такой структуры будет равен только самому себе (в текущей реализации компилятора). Сам по себе результат equalityKey в таком случае влияния не оказывает, поскольку этот метод не вызывается при сравнениях и вычислениях хэш-кода:
use libretto/util
struct Test(v: Int)
def Test equalityKey: - = error("Incomp")
def main = {
fix a = Test(123)
fix b = Test(123)
println: a == a
println: b == b
println: a == b
println: a.util/hashCode
println: b.util/hashCode
}
Выдает:
unit
unit
()
1606688005
1018892099
Два числа будут другие при запуске, но будут отличаться друг от друга.
Всего есть четыре вида сравнений и вычисления хэш-кода для структур:
- Единственный экземпляр без полей
struct Single
- Сравнение по значению полей
struct Fields(a: Int)`
- Сравнение по результату
equalityKey
struct Key(a: Int)
def Key equalityKey = this.a
- Сравнение только по ссылке
struct Ref(a: Int)
def Ref equalityKey: - = error("Incomp")
Но это касается сравнения экземпляров одно и той же структуры. Если экземпляры разных структур, то они всегда неравны.
Специальный метод equalityKey ищется только среди полностью определенных (не шаблонных) методов, у которых в контексте явно или через объединение указана соответствующая структура. Методы equalityKey без контекста, с контекстом Any, шаблоны не используются для управления сравнением.
Копирование
При определении структуры, у которой есть хотя бы одно поле, автоматически (но если уже нет пользовательского метода с таким же именем и с одним параметром) создаются методы для копирования структуры с заменой значения одного поля. Имя метода совпадает с именем поля, контекстом является структура, а единственным параметром метода служит новое значение поля.
struct Test(value: Int, modes: String*)
def main = {
fix t1 = Test(123, ())
fix t2 = t1.value(777)
fix t3 = t2.modes: ("r", "w")
println: t1
println: t2
println: t3
}
Выдаёт:
Test(123,())
Test(777,())
Test(777,(r,w))
Для единообразия это работает и для структур с одним полем:
struct Test(value: Int)
def main = {
fix t1 = Test(123)
fix t2 = t1.value(777)
println(t1)
println(t2)
}
Выдаёт:
Test(123)
Test(777)
Алгебраические типы: структуры и объединения
Структуры и объединения вместе позволяют определять алгебраические типы. Структуры являются тип-произведением, объединения - тип-суммой. Соответственно, алгебраически типом будет объединение, которое комбинирует структуры (которые, в свою очередь, тоже могут использовать объединения в качестве типа полей).
Пример определения связного списка целых чисел:
use libretto/util
union IntList = Nil | IntCons
struct Nil
struct IntCons(head: Int, tail: IntList)
def IntCons string: String = this.head.string + " :: " + this.tail.string
def intList(seq: Int*): IntList = {
var ret: IntList = Nil
util/reverse(seq) as v.${
ret = IntCons(v, ret)
}
ret
}
def main = {
println: intList: ()
println: intList: 1
println: intList: (1, 2)
println: intList: (1, 2, 3)
}
Выдаёт:
Nil
1 :: Nil
1 :: 2 :: Nil
1 :: 2 :: 3 :: Nil
IntList и является алгебраическим типом, включая в себя два варианта: Nil (пустой список) и Cons (конструктор списка). IntList рекурсивно используется в качестве типа поля Cons.
Эквивалентное определение (без методов intList и main) на языке Haskell может выглядеть следующим образом:
data IntList = Nil
| IntCons { head :: Integer, tail :: IntList }
instance Show IntList where
show :: IntList -> String
show Nil = "Nil"
show (IntCons head tail) = show head ++ " :: " ++ show tail
Обобщения (Параметрический полиморфизм)
Определение структуры поддерживает использование переменных типа при помощи обобщения (параметрического полиморфизма). См. соответствующее описание.