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

Структуры

Назначение структур

Структура - это базовый тип 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, которые тоже являются структурами, хотя и отличаются использованием от обычных структур:

  • Int
  • Real
  • String
  • Unit

Особенности этих структур:

  • Нет методов для доступа к полям, а значением является сам экземпляр структуры.
  • Для создания экземпляров используются только литералы или ключевое слово (для 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

Два числа будут другие при запуске, но будут отличаться друг от друга.

Всего есть четыре вида сравнений и вычисления хэш-кода для структур:

  1. Единственный экземпляр без полей
struct Single
  1. Сравнение по значению полей
struct Fields(a: Int)`
  1. Сравнение по результату equalityKey
struct Key(a: Int)
def Key equalityKey = this.a
  1. Сравнение только по ссылке
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

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

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