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 нет механизма наследования, а для реализации полиморфизма используются либо объединения, либо трейты.

Объединения являются более простым и естественным средством организации единой работы с разными типами данных.

Например, есть последовательность целых чисел (1, 2, 3), её типом будет Int+ (несколько значений, нет пустоты). Есть последовательность строк ("abc", "def"), её типом будет String+ (несколько значений, нет пустоты).

Но можно сделать последовательность, которая содержит и целые числа, и строки: (1, "abc", 2, 3, "def"). В обычных ОО-языках типом элемента был бы общий предок, а если такого нет, то наиболее общий класс (Object в Java, например).

В Libretto же типом элемента этой последовательности будет объединение (Int | String). Это означает, что элемент - это Int или String. Знак | и обозначает или. При непосредственном использование объединения тип берётся в скобках, а порядок перечисления типов не имеет значения.

А значит смешанная последовательность будет иметь тип (Int | String)+ или (String | Int)+ (несколько значений, нет пустоты):

def main = {
  fix t1: (Int | String)+ = (1, "abc", 2, 3, "def")
  fix t2: (String | Int)+ = (1, "abc", 2, 3, "def")
}

Можно использовать и несколько типов, а не только два (порядок тоже не имеет значения):

def main = {
  fix t: (Int | String | Real)+ = (1, "abc", 2, 3, "def", 3.14)
}

Но в перечислении через | (вне зависимости от числа) могут быть указаны только типы без кардинальности. Не допускаются Any, (), -. Т.е. это могут быть структуры (в том числе предопределенные), трейты и другие объединения:

trait T

struct S(v: Int)

def main = {
  fix t: (T | S | (Int | String))? = 123
}

Вложенность объединений игнорируется. В этом примере переменная t по определению может содержать трейт T, структуру S, структуру Int, структуру String, но реально содержит только число (Int).

Компилятор умеет выводить тип объединений:

def main = {
  fix t = (1, "abc")
  println: type(t)
}

Выдаёт

(libretto/Int | libretto/String)+

В этом примере тип неизменяемой переменной t будет (libretto/Int | libretto/String)+.

union

Чтобы постоянно не писать длинные перечисления типов для объединения, можно задать краткое имя при помощи union:

union IntOrString = Int | String

def main = {
  fix t: IntOrString* = (1, "abc")
  println: type(t)
}

Выдаёт

(libretto/Int | libretto/String)*

В union внешние скобки можно не указывать. Требования к перечисляемым типам точно такое же: типы без кардинальности. Не допускаются Any, (), -. Это могут быть структуры (в том числе предопределенные), трейты и другие объединения (в тои числе заданные через union). Зацикливание union не допускается: union A = Int | A вызовет ошибку компиляции.

Например, можно использовать union только с одним типом:

struct S(v: Int)

union Alias = S

def main = {
  fix t: Alias = S(123)
}

Но для создания структуры должен использоваться метод S, а создать через Alias(123) невозможно, поскольку такой структуры нет, а тип Alias является только кратким именем для S.

Наследования против объединения

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

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

struct S(v: Int)

union Union1 = S | Int
union Union2 = S | String

def main = {
  fix t = S(123)
  fix u1: Union1+ = (t, 123)
  fix u2: Union2+ = (t, "abc")
}

Здесь S используется в двух объединения одновременно.

Объединения и case

Объединения можно разобрать при помощи case:

union U = Int | String

def work(v: U*) = {
  v.{
    case i: Int = println("Int: " + i)
    case s: String = println("String: " + s)
  }
}

def main = {
  work: (1, "abc", 2)
}

Выдаёт:

Int: 1
String: abc
Int: 2

Важно, что здесь разбираются все случаи объединения: Int или String. Если добавить else = (с пустой правой частью), то компилятор проверит, что в case действительно разобраны все варианты объединения:

union U = Int | String

def work(v: U*) = {
  v.{
    case i: Int = println("Int: " + i)
    case s: String = println("String: " + s)
    else =
  }
}

def main = {
  work: (1, "abc", 2)
}

Если поменять определение на union U = Int | String | Real, то компилятор выдаст ошибку, поскольку не разобран случай с Real.

Использование разбора при помощи case (или аналога) в обычных ОО-языках имеет проблему расширения. Если иерархия классов не является закрытой, то её подкласс может быть определен позже, но уже существующий код, разбирающий объекты этой иерархии при помощи case, не будет знать об этом подклассе, что нарушит семантику программы.

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

Вызов методов и полиморфизм

Если объединение встречается в контексте, то выбор метода будет производиться при помощи автоматического разбора этого объединения:

union U = Int | String

def Int work = println("Int: " + this)
def String work = println("String: " + this)

def main = {
  (1, "abc", 2).work
}

Выдаёт:

Int: 1
String: abc
Int: 2

Но для всех типов объединения должен быть подходящий метод. Не обязательно непосредственно, возможно и с обобщением:

def Int work = println("Int: " + this)
def Any work = println("Any: " + this)

def main = {
  (1, "abc", 2, 3.14).work
}

Выдаёт:

Int: 1
Any: abc
Int: 2
Any: 3.14

Для Int используется метод work с контекстом Int, а для Real, String - с контекстом Any.

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

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

def Int work = this.real // Real
def String work = this + "!" // String

def main = {
  fix t = (1, "abc").work
  println: type(t)
}

В этом примере тип t будет (Real | String)+, поскольку типом work с контекстом Int будет Real, а с контекстом String - String.

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

struct S1(title: String)

struct S2(title: String, value: Int)

struct S3(title: String)

def main = {
  fix seq = (S1("S1"), S2("S2", 123), S3("S3"))
  seq.title.${
    println: $
  }
}

Выдаёт

S1
S2
S3

Достаточно того, что у всех структур есть поле с одинаковым именем title, которое доступно и через объединение этих структур.

Обобщения

В настоящее время обобщения объединения не поддерживается. Но, возможно, в будущем будет возможно написать union List[A] = Nil | Cons[A].

Объединения в контексте определения метода

Объединение можно указать в контексте определения метода:

def (Int | String | Real) work = println: this

def main = {
  (1, "abc").work
}

Выдаёт:

1
abc

При выборе метода для объединения этот контекст будет учитываться:

union IntOrString = Int | String

def IntOrString work = println: "(Int | String):" + this
def Real work = println: "Real: " + this

def main = {
  (1, "abc", 3.14).work
}

Выдаёт:

(Int | String):1
(Int | String):abc
Real: 3.14

В текущей версии Libretto объединение в контексте разворачивается на несколько методов при компиляции:

Например, если есть определение:

def (Int | String) work = println: "(Int | String):" + this

То при компиляции оно будет развёрнуто на два:

def Int work = println: "(Int | String):" + this
def String work = println: "(Int | String):" + this

Это историческое решение, но при обычном использовании это не даёт особенностей.