Язык Libretto
Особенности Libretto
-
Статическая типизация, компиляция в JVM байт-код, работа в Java-среде.
-
Динамическая компиляция (в том числе несколько уровней вложенности): возможность запуска кода, исходный текст которого сгенерирован во время работы другого кода. Для использования в качестве встраиваемого языка.
-
Система типов на неизменяемых структурах, трейтах (с неявной привязкой) и объединениях. Наследования нет.
-
Частью типа является указание кардинальности: единственное или множественное значение (последовательность), наличие или отсутствие пустого значения.
-
Понятие пути. Точка
.служит разделителем шагов пути. Каждый шаг пути осуществляет отображение последовательности без вложенности - это аналогflat mapилиbind(в терминологии монад). Пути служат для итерации по последовательностям с накоплением результата и для изменения контекста. -
Понятие контекста. Он всегда есть, доступен явно через
$, используется неявно для выбора метода. Для изменения контекста используются шаги пути. -
Метод определяется снаружи от контекста, в котором он исполняется (механизм расширений).
-
Функциональные возможности: анонимные функции как значения, неизменяемые структуры и последовательности, поддержка замыканий, локальные (вложенные) функции, в теле метода или функции всегда только выражения без операторов (expression, но не statement).
-
Замыкание как единственное средство создания объекта (экземпляра трейта). Изменяемые данные описываются при помощи изменяемых локальных переменных (
var). Для задания состояния используется локальное (по месту) создание экземпляра трейта с замыканием, содержащим изменяемые переменные. -
Числа с большой разрядностью в качестве базовых численных типов (
IntиReal). -
Ограниченная (в текущей реализации) поддержка обобщения (параметрического полиморфизма).
-
Ограниченная поддержка синтаксических шаблонов методов, но ожидается полная замена на обобщения или макросы.
-
Ограниченная поддержка макросов выражений: динамическая компиляция пути, представленного аргументом метода. Для реализации языка запросов в рамках сохранения стандартного Libretto синтаксиса программы.
Работа в Java среде
Компилятор Libretto является программой, работающей в Java-окружении. Программы на языке Libretto для запуска компилируются в JVM байт-код.
Как указано выше, поддерживается динамическая компиляция во время работы Libretto программы. Для этого достаточно только самого компилятора Libretto.
Но Libretto не позиционируется как язык для Java-окружения. Java-окружение - это всего лишь способ запуска. Система типов Libretto не соответствует системе типов Java, а только эмулируется при помощи типов Java. И эта эмуляция может изменяться по мере развития компилятора.
Программы на Libretto распространяются в скомпилированном в байт-код виде только в качестве готового для использования Servlet, который включает всё необходимые для работы библиотеки. И допускается работа только с этими версиями библиотек. Для остального обмена программами пока используются исходные тексты. В планах промежуточное (между исходными текстами и байт-кодом) представление для обмена, но пока оно не реализовано.
В связи с этой выбранной концепцией запуска программы на Libretto не имеют возможности лёгкой интеграции с Java-миром. Но возможность интеграции присутствует - через специальный механизм JVM-трейтов (см. todo). Это несколько громоздкий механизм, но обеспечивает полную эффективность (связывание на стадии компиляции с полноценной статической проверкой Java-типов).
История
Первоначально появился язык запросов к объектным базам данных (онтологиям), хранившимся в собственной системе. Язык создавался для программного использования (через API) из Java-программ или для пользовательских запросов через GUI (как аналог поиска). Этот язык запросов был навеян XPath, но работал с объектами вместо узлов XML. Ключевой идей были пути, состоящие из шагов. Пути и далее присутствуют во всех версиях языка Libretto.
Само название Libretto для языка появилось в январе 2010 года.
Libretto со временем превратился (в том числе под некоторым влиянием языка Scala) в полноценный язык программирования (условное название версии Libretto 0.9) - с определением типов данных и кода для исполнения. Но первое время сохранялся синтаксис, пришедший из XPath: контекст как ., разделители шагов пути /, пространства имён через :.
С развитием возможностей программирования появилась и стала развиваться возможность создания веб-приложений на языке Libretto. Сперва на базе Play Framework 1.x (2011 год), затем и на собственном веб-фреймворке (с 2012 года).
В версии Libretto 1.0 (2012-2016 года) была динамическая типизация с поддержкой ООП (классы, множественное наследование) и с опциональной строгой проверкой типов во время исполнения. Для указания контекста стал применяться $, для разделения шагов ., а разделителем пространства имён стал /. Программы выполнялись интерпретатором, работающим в Java среде. Версия Libretto 1.0 использовалась параллельно с Libretto 1.5/1.75 до полного перехода.
В версии Libretto 1.5 (2014-2015 года) типизация стала статической. Её базовыми типами стали трейты и неизменяемые структуры, а реализации методов определялись снаружи сущностей, но с указанием контекста (сущности, к которой они привязаны). Трейты неявно привязывались к подходящим по сигнатуре структурам, образую статическую иерархию типов. В целом язык стал проще по сравнению с Libretto 1.0. Появилась компиляция в байт-код JVM через промежуточный Java-код (через модель компилятора Janino, если быть точнее). Libretto 1.5 превратился в Libretto 1.75 после изменения системы типов.
В версии Libretto 1.75 (с 2015 года по настоящее время) сохранилась статическая типизация, но взамен трейтов появилась возможность явного определения объединения типов. Позже трейты вернулись (в 2019 году), но не только как средство описания автоматически привязываемых интерфейсов, но и как способ создания объектов с изменяемым состоянием (в противовес неизменяемым структурам). Компиляция в байт-код JVM стала прямой, без промежуточной модели Java-кода (с 2016 года).
Язык стал использоваться не только как средство разработки веб-приложений, но и как встраиваемый язык с динамической компиляцией (но статической типизацией).
Здесь и далее рассматривается актуальная реализация Libretto (1.75)
Почему язык называется Libretto
From: Andrei Mantsivoda andrei@baikal.ru
To: Anton Malykh malykh@gmail.com
Date: Mon, 4 Jan 2010 18:23:25 +0800
Subject: название
Склоняюсь к тому, чтобы назвать язык Libretto.
1) красивое “летящее” итальянское слово - как хотели, чтобы отразить легкость и компактность языка
2) переводится как “книжечка”, в опере понимается как “сценарий”, “скрипт”, пояснение - нам по смыслу прекрасно подходит (оттуда же library).
3) имеет международный статус и хорошую репутацию, гарантировано избавлено от негативных коннотаций.
4) не нашел известных систем с таким названием, есть только ряд ноутбуков тошибы, но это далеко
5) легко проговаривается и запоминается - начинается с мягкого “л”, а в середине косточка “бр” есть. Три слога задают прекрасную ритмику. Звучит на всех языках одинаково (даже ударение одинаковое). По звучанию близко к другому хорошему слову - “свобода” (по-итальянски, libertà, по-английски, liberty).
6) слово выглядит симпатично, бросается в глаза. Двойное “т” нарядное, задает акцент.
7) Наконец, слово из другой оперы (в прямом и переносном смысле) по сравнению с обычными названиями (сравни SPARQL, SQL), свежо должно звучать. Отражает вкусы разработчика.
Установка и запуск
Libretto и Libretto IDE
Две основные программы для запуска Libretto:
-
Консольный запуск (
libretto) -
Старая IDE (
libretto-ide)
Portable архивы (Linux, Windows, macOS)
Включают всё необходимое для запуска (в том числе JRE)
Linux:
64-битная x86_64 (Java 17): https://libretto-lang.org/download/linux/libretto-linux-x86_64.tar.gz (60МБ)
Windows
32-битная версия (Java 8), для старых Windows (XP и т.п.): https://libretto-lang.org/download/windows/libretto-windows-x86-old.zip (73МБ)
64-битная версия (Java 17), для актуальных систем: https://libretto-lang.org/download/windows/libretto-windows-x86_64.zip (46МБ)
macOS
64-битная x86_64 (Java 17): https://libretto-lang.org/download/macos/libretto-macos-x86_64.zip (57МБ)
Распаковать и запускать command-файлы. Если запускать через Finder, то первый запуск нужно сделать через контекстное меню -> Open/Открыть
Linux (apt/dpkg)
Для Linux с поддержкой apt/dpkg (Debian, Ubuntu, Mint и т.п.).
Внимание: Если sudo запрещен для пользователя, то смените пользователя на root (команда su -) и все действия, содержащие sudo, делайте от пользователя root. Либо добавьте пользователя в группу sudo, если хотите иметь доступ до sudo.
Установка:
- (a) Загрузить публичный ключ для подписей:
sudo curl -L -o /etc/apt/trusted.gpg.d/libretto.gpg http://libretto-lang.org/apt/libretto.gpg
- (б) Если curl не найден, то можно использовать
wget:
sudo wget -O /etc/apt/trusted.gpg.d/libretto.gpg http://libretto-lang.org/apt/libretto.gpg
- Добавить репозиторий:
echo "deb http://libretto-lang.org/apt/ libretto stable" | sudo tee /etc/apt/sources.list.d/libretto.list
- Обновить информацию о пакетах:
sudo apt update
- Установить libretto (при отсутствии JRE 11+ оно установится автоматически):
sudo apt install libretto
После установки доступны команды libretto-ide (IDE) и libretto (консольная версия). libretto-ide добавляется в приложения графического окружения (если оно поддерживает добавление).
Для запуска используется JRE, доступное через команду java. Если JRE несколько, то их можно переключать через sudo update-alternatives --config java или через sudo update-java-alternatives (с параметром).
Параметры для запуска Java задаются скриптом ~/Libretto/launcher.conf (если этого файла нет, то он создаётся при первом запуске Libretto).
Системное обновление (обновляет только команды запуска, но не саму Libretto/Libretto IDE!)
- Обновить информация о пакетах:
sudo apt update
- (а) Либо обновить все доступные пакеты:
sudo apt upgrade
- (б) Либо обновить только команды запуска Libretto:
sudo apt install libretto
Обновление Libretto/Libretto IDE производится обычным способом: через Главное меню ⇒ Help ⇒ Update, либо через запуск libretto -e 'lib/updateLibretto()' в системной консоли.
Свои данные Libretto хранит в ~/Libretto каталоге.
Удаление Libretto (кроме данных ~/Libretto):
sudo apt remove libretto
Ручной запуск (через jar-файл)
Требуется Java 8+, рекомендуется Java 21 или более современная версия.
-
Нужно скачать файл https://libretto-lang.org/ide/libretto-run.jar.
-
Запуск при помощи одной из команд (примеры содержимого cmd-файлов для Windows, для других сред запуска используйте подходящий синтаксис):
java -Xss5m -Xmx800m -classpath libretto-run.jar org.librettolang.run.IDE %* - запуск IDE
java -Xss5m -Xmx800m -classpath libretto-run.jar org.librettolang.run.Libretto %* - запуск консольной версии Libretto
java -Xss5m -Xmx800m -classpath libretto-run.jar org.librettolang.run.Server %* - запуск консольной версии сервера
java -Xss5m -Xmx800m -classpath libretto-run.jar org.librettolang.run.ServletGenerator %* - запуск генератора (компилятора) сервлета
java -classpath libretto-run.jar org.librettolang.run.Update %* - запуск консольной версии апдейта.
Управление памятью через опции запуска
Размер стека (m - мегобайты): -Xss5m
Размер heap (m - мегобайты): -Xmx4000m
Место правки опций для предлагаемых сборок:
Windows (32-битная portable): нет возможности
Windows (64-битная portable): файлы app\libretto-ide.cfg и app\libretto.cfg
macOS (portable): скрипты запуска libretto-ide.command и libretto.command
Linux (portable): скрипты запуска bin/libretto-ide.sh и bin/libretto.sh
Linux (apt): файл ~/Libretto/launcher.conf
Ручной запуск: опции запуска java или javaw
Механизм #! для скриптов
Изначально в грамматику языка Libretto заложена поддержка #-комментария первой строкой исходного текста. При соответствующем окружении это позволяет исполнять Libretto-программы при помощи механизма Shebang (#!), т.е. как системные скрипты.
Поскольку нет фиксированного пути программы консольного Libretto (он может меняться в зависимости от способов установки), то для запуска рекомендуется использовать env (позволяет запускать программу через поиск по $PATH). В предположении, что консольный Libretto вызывается через libretto, первая строка исходного текста Libretto-программы для запуска будет выглядеть так: #!/usr/bin/env libretto
Пример:
Есть файл test.ltt:
$ cat test.ltt
#!/usr/bin/env libretto
use libretto/util
def main = println: s"Heap size: #{util/memoryMax div 1024 div 1024}MB"
Установить флаг запуска:
$ chmod +x test.ltt
И можно запустить:
$ ./test.ltt
Heap size: 2000MB
Для доступа к параметрам командной строки можно использовать метод def osArgs: String* из пакета libretto/util
$ cat test.ltt
#!/usr/bin/env libretto
use libretto/util
def main = println: s"Args: #{util/osArgs.*join(",")}
$ ./test.ltt 1 "2 3"
Args: 1,2 3
Имена, пакеты, use
Имена. Зарезервированные слова
Зарезервированные слова нельзя указывать в качестве идентификаторов напрямую.
_ascasedefelsefixifindexnewobjectpackagereturnthis- нельзя использовать в качестве имени переменнойunitusevar
Но допускается использование зарезервированных слов в качестве идентификаторов при помощи обратных кавычек (кроме отдельных отмеченных выше случаев).
fix `new` = 123
Если зарезервированное имя является частью имени с префиксом, то заключается в обратные кавычки только локальная часть:
println: lib/`new`
Пакеты: package и use
Модульность обеспечивается использованием пакетов, которые образуют пространство имен и участвуют в алгоритме разрешения имен.
Имя определяемого пакета задается при помощи package, которых в одном исходном тексте может быть несколько. Соответственно, один исходный текст может содержать несколько пакетов внутри.
Если package не указан, то считается package _default
При помощи use можно получить доступ до пакета из другого пакета. Прямой доступ до сущностей при помощи префикса.
use name as prefix
Префикс должен быть уникальным в рамках пакета, в котором он задается для использования.
Можно не указывать префикс, тогда в качестве префикса автоматически будет использоваться последняя часть полного имени пакета после /:
use libretto/util - равносильно use libretto/util as util
Все use распространяются на весь пакет, даже если один пакет определен в нескольких исходных текстах.
Исходный текст-1:
package org/example/test
use com/teacode/html
// здесь можно использовать префикс html
Исходный текст-2 того же проекта
package org/example/test
// здесь можно использовать префикс html,
// который определен в исходном тексте-1
Один и тот же пакет можно использовать через разные префиксы, определяя соответствующие use
use org/example/test
use org/example/test as t
use org/example/test as t2
test/name // эти
t/name // три варианта
t2/name // ссылаются на одну сущностью
По умолчанию доступен префикс libretto для доступа к пакету libretto, но можно дополнительно определить другой префикс
use libretto as l
def main = l/println("Hello!")
Имена пакетов
Имена пакетов формируются из локальных кусков, разделенных /. В качестве имени рекомендуется использовать url, где доменная часть записана в обратном порядке и с разделением /.
http://test.example.org/project ⇒ org/example/test/project
Использование префиксов
Можно использовать только локальные имена сущностей из пакета, но тогда разрешение имени будет происходить по некоторому алгоритму поиска сущности.
Если нужно указать точное имя сущности, то можно использовать составное имя с префиксом, указанным (автоматически или вручную) при use. Тогда будет использоваться сущность, которая стоит за получаемым полным именем:
use org/example/test as t
t/name // сущность с именем name из пакета org/example/test
Предопределенные структуры (Int, Real, String, Unit)
Целые числа (libretto/Int)
Целые числа представлены структурой libretto/Int. Такие числа не имеют ограничения разрядности (в JVM представлены java.math.BigInteger). Разрядность растёт по мере увеличения числа. Числа могут быть как положительные, так и отрицательные, нуль строго 0 без знака.
Поддерживаются операции, которые тоже дают в результате Int: + (сложение), - (вычитание), * (умножение), idiv (целочисленное деление). При выполнении этих операций нет переполнения разрядности.
При использовании операции div (обычное деление) результатом будет Real, а не Int (см. описание Real).
Целые десятичные числа Int представляются литералами, содержащими цифры от 0 до 9 без префикса.
Целые двоичные числа Int представляются литералами, содержащими цифры 0 и 1 с префиксом 0b
Целые шестнадцатеричные числа представляются литералами, содержащими цифры от 0 до F с префиксом 0x. Цифры могут быть представлены как в нижнем (a-f), так и в верхнем (A-F) регистре.
Между цифрами в литералах может быть использован символ _ в качестве незначащего визуального разделителя. Символ _ как разделитель может быть использован несколько раз, но слева и справа от него должны быть цифры.
Пример:
def main = {
println: 1234567
println: 1_2_3_4_5_6_7
println: -0x1_0
println: 0x1000000
println: 0x1_000_000
println: -0b11
println: 0b1111111111111111
println: 0b1111_1111_1111_1111
}
Выдает:
1234567
1234567
-16
16777216
16777216
-3
65535
65535
Целое число можно определить при помощи экспоненциальной записи литерала, если результат не является вещественным:
def main = {
fix i1: Int = -1_23.4567e4
fix i2: Int = 1234000E-3
println: i1
println: i2
}
Выдает:
-1234567
1234
Если нужна скорость или экономия памяти, то возможно использование целых чисел с ограниченной разрядностью из пакета libretto/num.
Вещественные числа (libretto/Real)
Вещественные числа представлены структурой libretto/Real. Такие числа при обычном использовании (за исключением деления div) не имеют округления и ограничения разрядности (в JVM представлены java.math.BigDecimal). Числа могут быть как положительные, так и отрицательные.
При вычислении нет ошибок, связанных с переходом от десятичного к двоичному представлению с округлением:
def main = {
println: 0.1 + 0.2
}
Выдает: 0.3
Операции +, -, * не ограничивают разрядность результат и не делают округлений (MathContext.UNLIMITED). Поэтому при таких вычислениях разрядность может возрастать, что приведёт к замедлению вычислений и увеличению потребляемой памяти.
При делении div разрядность ограничена (MathContext.DECIMAL128 - IEEE 754R Decimal128, 34 десятичные цифры). Это же происходит при обычном делении (div) целых чисел Int: сперва они преобразуются в Real без ограничений, а затем производится деление с указанным ограничением.
Вещественные числа Real представляются литералами, содержащими цифры от 0 до 9 без префикса, целая часть от дробной отделяется точкой . в качестве десятичного разделителя.
Между цифрами вещественного числа в литералах может быть использован символ _ в качестве незначащего визуального разделителя. Символ _ как разделитель может быть использован несколько раз, но слева и справа от него должны быть цифры.
def main = {
println: 3.14_15_92_6
}
Выдает: 3.1415926
Вещественное число можно определить при помощи экспоненциальной записи литерала, если результат не является целым:
def main = {
fix r1: Real = -1.23456e3
fix r2: Real = 1_000_001E-7
println: r1
println: r2
}
Выдает:
-1234.56
0.1000001
Строковое представление вещественного числа может быть в экспоненциальной записи. Для получения строкового представления без экспоненты можно использовать метод libretto/util/plainString
use libretto/util
def main = {
fix r: Real = 0.1000001 div 1000000000
println: r
println: r.util/string
println: r.util/plainString
}
Выдает:
1.000001E-10
1.000001E-10
0.0000000001000001
Если нужна скорость или экономия памяти, то возможно использование вещественных чисел с ограниченной разрядностью из пакета libretto/num.
Строки и символы (libretto/String)
Строки свободной длины представлены структурой libretto/String. В строках допускается использование символов Unicode, в том числе с кодами больше 0xFFFF.
Базовый строковый литерал использует двойные кавычки ", внутри допускается использование экранирования символов при помощи \:
\\- символ\\"- символ"\'- символ'\r- символ CARRIAGE RETURN с кодом0x0D\n- символ LINE FEED с кодом0x0A\b- символ BACKSPACE с кодом0x08\t- символ табуляции с кодом0x09\f- символ FORM FEED с кодом0x0С
Можно указывать символ при помощи кода (Unicode) двумя вариантами:
\uABCD, где ABCD - это строго четыре шестнадцатеричные цифры кода символа
\{ABCDE}, где ABCDE - это любое число шестнадцатеричных цифр кода символа
def main = {
println("\u0041\u0042\u0043")
println("\u{41}\u{42}\u{43}")
println("\u{1f603}")
}
Выдает:
ABC
ABC
😃
Строка без экранируемых символов определяется при помощи пары << и >>, допускается использовать символы с любыми кодами (в том числе переносы строк:
def main = {
print(<<a
b
c
>>)
}
Выдает:
a
b
c
Нет явного типа для представления отдельных символов. Для этого используется строка (String), но содержащая один символ (Unicode codepoint, если говорить более строго).
Для разбора строки на подстроки (каждая из одного codepoint) используется метод char, который возвращает последовательность строк, каждая из которых содержит только один символ (codepoint)
def main = {
fix str = "Hello! \u{1f603}"
println: str.chars.*size
println: str.chars
}
Выдает:
8
(H,e,l,l,o,!, ,😃)
todo добавить про обратную сборку join?
Структуры
Назначение структур
Структура - это базовый тип 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
Обобщения (Параметрический полиморфизм)
Определение структуры поддерживает использование переменных типа при помощи обобщения (параметрического полиморфизма). См. соответствующее описание.
Трейты
Назначение трейтов
Трейт определяет набор методов с полными сигнатурами (типами параметров и результата). Эти методы доступны для вызова через контекст, в котором находится экземпляр соответствующего трейта.
Если структуры определяют неизменяемые данные, то трейты служат антиподами:
- Трейты определяют не данные, а программный интерфейс (сигнатуры методов), что обеспечивает абстракцию и уменьшает связанность.
- Трейты могут служить замыканием
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, а более специфичный тип.
Обобщения (Параметрический полиморфизм)
Определение трейта поддерживает использование переменных типа при помощи обобщения (параметрического полиморфизма). См. соответствующее описание.
Объединения
Определение
В 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
Это историческое решение, но при обычном использовании это не даёт особенностей.
Выражения
Выражения
Запись {...} сама является выражением и содержит выражения, которые выполняются в порядке перечисления. Результатом {...} является результат последнего выражения. Если внутри выражений нет, то результат - пустота ().
fix r = {1; 2; 3}
или
fix r = {
1
2
3
}
Разделитель ;
Разделитель ; между выражениями внутри {} блока не является обязательным при переносе выражения на новую строку. При записи выражений в одну строку внутри {} блока должен быть использован разделитель ;.
Переменные (fix, var)
fix/var-переменные можно определять только внутри {...}
fix-переменная является неизменяемой.
var-переменная является изменяемой.
Имя переменной должно быть уникальным в рамках одного {}-блока. Имя this не допускается. Другие резервированные слова можно использовать при помощи обратных кавычек
При вложенности имя переменной перекрывает предыдущую одноименную переменную:
def main = {
fix a = 0
println(a)
{
fix a = 1
println(a)
}
println(a)
}
Выдает:
0
1
0
Параметры методов тоже являются fix-переменными, которые можно перекрывать:
def w(v: Int) = {
println(v)
{
fix v = 777
println(v)
}
println(v)
}
def main = {
w(123)
}
Выдает:
123
777
123
Тип переменной может быть выведен при инициализации автоматически, а может быть указан явно.
def main = {
var res: Int* = ()
res = 1
res += 2 // добавление в конец
res .= 0 //добавление в начало
println: res
}
Выдает: (0, 1, 2)
Операции, операторы
Деление
div - деление, при использовании на Int даёт Real.
idiv - целочисленное деление, на Int даёт Int
Логические операторы
У and, or, not тип результата Unit?, но критерием истинности операнда является непустота.
Второй операнд логических операций and, or вычисляется лениво.
Оператор in
Оператор in допускает слева значение без кардинальности или с ?-кардинальностью . Множественные значения слева не допускаются (проверяется статически при компиляции).
Справа может быть либо последовательность, либо единственное значение, либо пустота.
Если слева пустота, то результат in тоже пустота.
Если справа непустая последовательность, то результатом in является unit тогда и только тогда, когда значение слева равно хотя бы одному элементу последовательности. Проверка производится перебором последовательности, что довольно неэффективно.
Если справа единственное значение, то результатом in является unit тогда и только тогда, когда значение слева не является пустотой и равняется значению справа. a in b в случае единственного значения справа эквивалентно a.($ in b)
Если справа пустота, то результат in тоже пустота.
Справа допускается эффективное использование ..-последовательностей. Если слева значение во время исполнения имеет тип Int, а справа ..-последовательность (напрямую или косвенно), то вместо проверки всех элементов последовательности производится сравнение с границами последовательности, что гораздо эффективнее обычного in. В таком случае a in b..c эквивалентно b <= a <= c.
def main = {
def test(v: Int) = println: v in 0..9
test(-1)
test(3)
test(7)
}
Выдаёт:
()
unit
unit
Приоритет операций/конструкций
От наибольшего приоритета (выполняется раньше) к наименьшему (выполняется позже):
- строковый шаблон
newимя литерал#$xml { }( )вызов метода-(отрицание)=(как часть пути)+=(как часть пути).=(как часть пути)if(как часть пути)return(как часть пути) ..if(отдельной конструкцией).(путь).*(путь)*dividivmod+-(инфиксный)==!=>>=<<=innotandor- инфиксная запись вызова метода (не оператора)
return(отдельной конструкций)
if-конструкция
Условие в if проверяется на непустоту. Пустое значение - это “ложь” (выполняется ветка else при её наличии). Непустое значение - это “истина” (выполняется основная ветка).
if-конструкция функциональная, т.е. возвращает значение.
fix result = if (cond) 0 else 1 - result будет типа Int
Если разные типы по веткам, то берется их общий тип:
fix result = if (cond) "a" else 0 - result будет типа (String | Int)
При отсутствии ветки else она считается равносильной else ()
case-блок
case-блок используется для разбора случаев, это аналог цепочки if, но работающих с определенным значением.
Есть два вида условий: case и else, они указываются в блоке с фигурными скобками. Если фигурные скобки используются с предварительным символом *, идущем после точки ., то в качестве значения для разбора используется левая часть пути до точки, собираемая в последовательность. Если фигурные скобки используются с предварительным символом *, но без точки, или вовсе без предварительного символа *, то значением для разбора является $ - текущее значение контекста .
Пример как часть пути:
def main = {
fix value = (1, 2, 3)
value.*{
case x = println(x)
}
println: "==="
value.{
case x = println(x)
}
}
Выдает:
(1,2,3)
===
1
2
3
Пример без точки:
def main = {
(1, 2, 3).${
println("===")
{
case x = println(x)
}
}
}
Выдает:
===
1
===
2
===
3
Каждое условие состоит из частей:
case-условие (с опциональнымif (...)) илиelse- символ
= - выражение (кроме специального вида последнего условия
else =без выражения)
Условия разделяются переносами строк либо символами ;
def main = {
(1,"a",2,"d").{
case x: Int = println(x)
else = println("-")
}
println("===")
(1,"a",2,"d").{ case x: Int = println(x); else = println("-") }
}
Выдает:
1
-
2
-
===
1
-
2
-
Выражением может быть и блок выражений (фигурные скобки):
def main = {
(1,2,3).{
case {1} = println("one")
case {2} = {
println("two")
}
case {3} = { println("three") }
}
}
Выдает:
one
two
three
Выражения всегда выполняются в контексте Unit.
Перебор условий производится сверху вниз последовательно до первого срабатывания условия. В случае срабатывания условия результатом case-блока является выражение после символа =, дальнейший разбор не проводится. Если ни одно условие не сработало, то результатом case-блока является пустота ().
def main = {
(1, 2, 3).{
println: {
case {1} = "one"
case {1} = "one-2"
case {2} = "two"
}
}
}
Выдает:
one
two
()
Виды case условий:
case x- срабатывает всегда, переменнойxприсваивается разбираемое значениеcase x: тип- срабатывает, если разбираемое значение динамически соответствует указанному типу, переменнойxуказанного типа присваивается разбираемое значение.case ~: тип- срабатывает, если разбираемое значение динамически соответствует указанному типу.case {...}- срабатывает, если разбираемое значение равно результату выполнения выражения, указанного в фигурных скобках. Используется сравнение, полностью эквивалентное оператору==.case ()- срабатывает, если разбираемое значение пустое.case (x)- срабатывает, если разбираемое значение непустое, переменнойxуточненного типа (без пустоты) присваивается разбираемое значение.
Пример обработки пустоты:
def main = {
fix v: Int* = (1, 2, 3)
fix r = v.*{
case () = "empty"
case (v) = type(v)
}
println: r
println: type(r)
}
Выдает:
libretto/Int+
libretto/String
Пример c else:
def main = {
println: ("a", 123, 3.14, unit).{
case i: Int = "int"
case r: Real = "real"
else = "?"
}
}
Выдает: (?,int,real,?)
Пример прямого сравнения с выражением:
def main = {
println: (1,2,3).{
case {1} = "one"
else = "?"
}
}
Выдает: (one,?,?)
Другой пример прямого сравнения:
def main = {
println: (1,2,3).*{
case {(1, 2, 3)} = "ok"
else = "?"
}
}
Выдает: ok
После case, но до = можно использовать if с дополнительным условием в круглых скобках. Всё условие срабатывает, если срабатывает сам case и одновременно if-условие не является пустым. В if-условии можно использовать переменную, если она определяется при помощи case (в вариантах case x, case x: тип, case (x)).
def main = {
println: (1,2,3,"abc").{
case i: Int if (i mod 2 == 0) = "even"
case i: Int = "odd"
else = "?"
}
}
Выдает: (odd,even,odd,?)
else может быть любым по порядку, может использоваться несколько раз, но он будет срабатывать всегда, поэтому следующие условия будут проигнорированы.
def main = {
println: (1,2,3).{
else = "first"
else = "second"
}
}
Выдает: (first,first,first)
При компиляции анализируются разбираемые варианты, если варианты заведомо не покрывают все входящие значения, то автоматически добавляется последним условием пустота: else = ()
Если последним (и только последним) условием указан специальный вариант else = без выражения, то компилятор должен гарантировать, что все предшествующие условия разбирают все возможные входящие значения, ветка с пустотой в таком случае автоматически не добавляется. Если компилятор не может этого гарантировать, то будет ошибка компиляции.
def main = {
fix r = (1, "a", unit).{
case i: Int = "int"
case s: String = "string"
case u: Unit = "unit"
else =
}
println: r
println: type(r)
}
Выдает:
(int,string,unit)
libretto/String+
Другой пример:
struct A(aValue: Int)
struct B(bValue: Int)
def main = {
fix v = (A(123), B(777))
fix r = v.{
case a: A = a.aValue
case b: B = b.bValue
else =
}
println: r
println: type(r)
}
Выдает:
(123,777)
libretto/Int+
Методы while, filter, indexWhere, sort
while реализован методом из пакета libretto, а не конструкцией языка. Первый параметр - это условие (ленивое выражение), выполняется на каждой итерации. Второй параметр - это тело (ленивое выражение), которое выполняется в случае непустоты условия, затем производится следующая итерация.
Еще связанные методы из пакета libretto:
filter
indexWhere
todo sort
Обработка пустоты
Часто возникает задача обработать пустоту, причём убрав её из типа результата. Т.е. перейти от кардинальности ? к чистому типу и(или) от кардинальности * к кардинальности +.
Есть несколько вариантов.
- Метод
onEmpty, который заменяет пустоту на указанное значение:
fix v: Int* = …
fix r: Int+ = v.*onEmpty(0)
Можно не возвращать значение, а кидать исключение: v.*onEmpty(error("Wrong value"))
- Метод
onEmptyс заменой пустоты и обработкой непустого значения:
fix v: Int* = …
fix r: Int+ = v.*onEmpty(-1)#(a){ a + 100 }
Прибавляет 100 к непустому значению, но возвращает -1 для пустоту.
todo: показать, что a может быть коллекцией
- Использование
case:
fix v: Int* = …
fix r: Int+ = v.*{
case (a) = a + 100
else = -1
}
Аналогично прибавляет 100 к непустому значению, но возвращает -1 для пустоты.
Путь
Шаги
Путь состоит из шагов, разделенных точкой. Может состоять только из одного шага (точка отсутствует).
Обычный шаг включает в себя выражение, которое опционально может быть дополнено конструкцией as.
*-шаг отделяется при помощи .* (если шаг первый, то точка опускается). Может быть следующих видов:
…
Выражение текущего шага исполняется, результат рассматривается как последовательность (может не содержать значений, содержать одно или несколько значений). Для каждого элемента этой последовательности вызывается следующий шаг (если он обычный), текущее значение элемента доступно в следующем шаге через $ (с типом строго одно значение). Если есть конструкция as, то значение доступно до конца пути или до *-шага (что раньше достигнуто) через указанное имя переменной.
Методы для шагов (idx, do, where, onEmpty)
Полезные методы из пакета libretto, которые пригодятся для шагов:
idx- получение индекса (от 0) по имениas-переменной:
(10,20,30) as i.{ idx(i) } // результат 0, 1, 2
where- передача контексте в следующих шаг в случае выполнения условия
(10, 20, 30).where($ == 20) // результат (20)
onEmpty- замена пустоты на значение
(10, 20, 30).(where($ == 20).*onEmpty(-1)) // (-1, 20, -1)
todo sort
Разрешение имен
Порядок разрешения имен без префикса:
- Локальные переменные/функции (если имя не после точки в пути).
- Методы, определенные в текущем пакете.
- Методы, определенные в пакете контекста.
- Методы, определенные в пакете
libretto.
Имена с префиксами ищутся в соответствующем префиксу пакете.
Модель памяти
Все значения ссылочные, создаются в памяти heap. Удаление неиспользуемых значений при помощи GC.
Передача параметров всегда по ссылке, передаваемые последовательности всегда неизменяемые. В случае ленивой передачи параметров (“по значению”) компилятор автоматически оборачивает код в анонимную функцию, которую и передаёт по ссылке.
Возможно, что в будущем базовые типы из libretto/num без кардинальности будут создаваться на стеке и передаваться по значению. Сейчас такого поведения нет.
libretto/lazy/value - глобальные объекты
todo - переписать, уточнив терминологию
В языке отсутствуют обычные глобальные переменные, поскольку они кажутся простыми, но потенциально опасны при многопоточном использовании в веб-приложении.
Есть экземпляры структур без полей. Но именно отсутствие полей не позволяет использовать их в качестве глобальных переменных (нет состояния).
Но можно привязать значение к экземпляру структуры без полей при помощи метода value из библиотеки libretto/lazy, что позволяет получить частичную замену глобальным переменным.
Метод value принимает два значения. Первое - это экземпляр структуры строго без полей. Этот объект служит идентификатором глобального значения, из разных точек программы использование одного глобального объекта обозначает работу с одной сущностью.
Второе значение - это код, который будет вызываться для инициализация значения, привязанного к глобальному объекту.
Метод value возвращает значение, если оно уже привязано к указанному глобальному объекту, либо вызывает код инициализации, результат которого привязывается к указанному глобальному объекту и возвращается из метода.
Пример:
use libretto/lazy
struct Test
def test = lazy/value(Test()): {
println("Init")
123
}
def main = {
println: test
println: test
}
Выдает:
Init
123
123
В данном примере привязка значения к Test() осуществляется один раз - при первом вызове test, когда вызывается переданный в value код инициализации. При повторном вызове test значение уже привязано, поэтому используется именно оно без вызова кода инициализации.
Важное отличие от обычных глобальных переменных в том, что момент инициализации может происходить несколько раз на усмотрение компилятора. Например, в веб-приложении значение сбрасывается и привязывается заново при изменении исходных текстов. Это нужно обязательно учитывать.
Поскольку привязанное значение может быть использовано из разных потоков, то либо это должно быть неизменяемое значение, либо работа с ним должна быть синхронизирована вручную (например, при помощи synchronized из libretto/util).
Сам вызов value синхронизируется автоматически, поэтому ручную синхронизацию не нужно использовать.
Специальные методы (string, equalityKey, !, =)
Метод string
Метод string используется для строкового представления сущности, в контексте которой этот метод определен.
Это может быть структура или трейт. Метод может быть определен внутри структуры или трейта. Или как глобальный метод.
Метод не должен принимать параметры.
struct Test(string: String)
struct Value(value: Int)
def Value string = "Value: "+ this.value
trait Calc {
def string: String
}
trait Work {
}
def Work string = "Work string"
def main = {
println: Test("Test string")
println: Value(123)
println: new Calc {
def string = "Calc string"
}
println: new Work {}
}
Выдаёт:
Test string
Value: 123
Calc string
Work string
Тип результата метода string может быть любой, не обязательно String. И может быть любая кардинальность:
struct Test(string: Int*)
struct Value(value: Int*)
def Value string = this.value
trait Calc {
def string: Int*
}
trait Work {
}
def Work string = (123, 123)
def main = {
println: Test((123, 123))
println: Value((123, 123))
println: new Calc {
def string = (123, 123)
}
println: new Work {}
}
Выдаёт:
(123,123)
(123,123)
(123,123)
(123,123)
Но конечное преобразование в String в таком случае делает компилятор. Для предсказуемого поведения лучше определять метод string с типом результата String.
Специальный метод string ищется только среди полностью определенных (не шаблонных) методов, у которых в контексте явно или через объединение указан соответствующий трейт или структура. Методы string без контекста, с контекстом Any, шаблоны не используются для генерации строкового представления.
Метод equalityKey
Метод equalityKey используется только для структуры, он определяет способ сравнение экземпляров структур. См. соответствующий раздел в описании структур.
Специальный метод equalityKey ищется только среди полностью определенных (не шаблонных) методов, у которых в контексте явно или через объединение указана соответствующая структура. Методы equalityKey без контекста, с контекстом Any, шаблоны не используются для управления сравнением.
Для трейтов этот метод не поддерживается.
Метод !
При помощи обратных кавычек `` можно определять методы с именами, которые не допускает парсер напрямую. Например, можно определить и вызвать метод ! как `!`:
def `!` = "test"
def main = {
println: `!`
}
Выдаёт:
test
Но особенность метода ! в том, что его нужно определять при помощи обратных кавычек ``, но вызывать можно без них:
def `!` = "test"
def main = {
println: !
}
Выдаёт:
test
Поскольку это обычный метод, то нет ограничений на число параметров, контекст или тип результат.
В Libretto имя ! имеет смысл apply (применить). Например, в этом смысле он используется в анонимных функциях и отображениях.
Методы для ! и =
Например, в отображениях метод ! с одним параметром служит для получения значения, привязанного к указанному ключу. А для привязки используется комбинация ! и знака равно =:
def main = {
fix m = map[Int, String]()
m.!(1) = "one"
println: m.!(1)
}
Выдаёт:
one
Вызов m.!(1) = "one" преобразуется в m.`!=`(1, "one").
struct Test(v: Int)
def Test `!=`(a: Int, b: Int) = a + b + this.v
def main = {
fix t = Test(123)
println: t.!(777) = 100
}
Выдаёт:
1000
Если параметров для ! нет, то вызова преобразуется в != с одним параметром:
struct Test(v: Int)
def Test `!=`(a: Int) = a + this.v
def main = {
fix t = Test(123)
println: t.! = 777
}
Выдаёт:
900
Это работает не только для =, но и для += и .=. И для любого числа параметров. Общее правило: вызов превращается в !=, !+=, !.=, в аргументы этого вызова сперва добавляются аргументы, что указаны для ! (если они есть), а последним аргументом добавляется выражение после =:
a.! = xпреобразуется вa.`!=`(x)a.! += xпреобразуется вa.`!+=`(x)a.! .= xпреобразуется вa.`!.=`(x)a.!(b) = xпреобразуется вa.`!=`(b, x)a.!(b) += xпреобразуется вa.`!+=`(b, x)a.!(b) .= xпреобразуется вa.`!.=`(b, x)a.!(b, c) = xпреобразуется вa.`!=`(b, c, x)a.!(b, c) += xпреобразуется вa.`!+=`(b, c, x)a.!(b, c) .= xпреобразуется вa.`!.=`(b, c, x)- и т.д.
При определении такого метода с = в названии его нужно оборачивать в обратные кавычки ``
def Int `!=`(a: Int, b: Int, c: Int, d: Int, e: Int) =
this + a + b + c + d + e
def main = {
println: 123.!(1,2,3,4) = 5
}
Выдаёт:
138
Например, простой буфер целых чисел:
trait IntBuffer {
def `!`: Int*
def `!=`(v: Int*): ()
def `!.=`(v: Int*): ()
def `!+=`(v: Int*): ()
}
def intBuffer(initValue: Int*) = {
var buf: Int* = initValue
new IntBuffer {
def `!` = buf
def `!=`(v) = buf = v
def `!.=`(v) = buf .= v
def `!+=`(v) = buf += v
}
}
def main = {
fix b = intBuffer: (1, 2, 3)
println: b.!
b.! .= 0
println: b.!
b.! += (4, 5)
println: b.!
b.! = 6..9
println: b.!
}
Выдаёт
(1,2,3)
(0,1,2,3)
(0,1,2,3,4,5)
(6,7,8,9)
Методы для = (общее правило)
Правило для ! и = - это частный случай более общего правила вызова методов с =.
Если присваивание (=, +=, .=) не связано с переменной (в левой части), то оно при компиляции заменяется на вызов метода:
-
Имя метода (простое или с префиксом) формируется добавлением
=,+=,.=к локальной части имени, стоящего последним шагом слева от присваивания.a =… превращается в`a=`a/b +=… превращается вa/`b+=` -
В аргументы вызова сперва переходят аргументы в скобках слева от
=(если такие есть), а правая часть от присваивания добавляется последним аргументом.a = 123превращается в`a=`(123)a/b(c, d) += xпревращается вa/`b+=`(c, d, x)
При определении такого метода с = в названии его нужно оборачивать в обратные кавычки ``
Такой подход можно использовать для имитации т.н. getter и setter. В качестве getter будут выступать обычные методы без параметров, а в качестве setter как раз метод с =.
def Test() = {
var aValue = 0
new {
def `a=`(v: Int): () = aValue = v
def a: Int = aValue
}
}
def main = {
fix obj = Test()
obj.a = 123
println: obj.a
obj.a = obj.a + 777
println: obj.a
}
Выдаёт:
123
900
В этом примере трейт Test выглядит как имеющий изменяемое поле a, но работа с ним - это вызов двух методов: a для чтения и `a=` для записи.
Замыкания (трейты, анонимные и локальные функции)
Замыкания
Метод трейта, анонимная функция, локальная функция могут служить замыканием для доступа к сущностями, которые определены в другой области видимости.
При помощи замыкания возможен доступ ко всем переменным (fix, var, as, case), параметрам функции/метода (кроме this), локальным функциям.
def call(f: Func[Int, Unit]) = {
f.!(100)
}
def test(param: Int) = {
fix a = 777
def local(v: Int) =
println: "local: " + (param + v + a) // замыкание с param и a
fix f = func: #(v: Int){
println: "func: " + (param + v + a) // замыкание с param и a
}
local(100)
call(f)
}
def main = {
test(123)
}
Выдаёт:
local: 1000
func: 1000
Замыкание с локальной функцией:
def main = {
def inc(v: Int) = v + 1
fix f = func: #(i: Int){ println: inc(i) } // замыкание с inc
println("Before")
f.!(100)
}
Выдаёт:
Before
101
this в замыкании
В замыкании использовать this метода, в котором производится определение, напрямую невозможно. В теле анонимной функции или локальной функции доступ к this запрещен компилятором. В теле метода трейта this разрешен, но обозначает сам экземпляр трейта, а не this обрамляющего метода.
Поэтому для доступа к this в замыкании нужно значение получать через промежуточную fix переменную.
def Int createSumFunc: Func[Int, Int] = {
fix ctx = this // подготовка для замыкания
func: #(a: Int){ ctx + a } // замыкание с ctx
}
def main = {
fix sumFunc = 123.createSumFunc
println: sumFunc.!(777)
}
Выдаёт:
900
Особенность замыкания с var
var-переменная даёт доступ замыканию не до самого значения, а до контейнера, в котором производится накопление, изменение значения. Поэтому значения в этом контейнере могут отличаться между моментом создания (определения) замыкания и моментом его запуска (использования):
def main = {
var i = 0
var funcs: Func[Unit]* = ()
while(i < 4): {
funcs += func: #{ println(i) } // определение
i = i + 1
}
println("After while")
funcs.${ $.! } // использование
}
Выдаёт:
After while
4
4
4
4
Все созданные анонимные функции, которые используют var i, вызываются после цикла while, когда значение i (т.е. значение в контейнере, который стоит за именем i) будет уже 4.
А вот замыкание с неизменяемыми переменными (fix, as, case, параметры метода или функции) получает именно значения на момент замыкания.
def main = {
fix funcs: Func[Unit]* = 0..3 as i.
func: #{ println(i) } // определение
println("After path")
funcs.${ $.! } // использование
}
Выдаёт:
After path
0
1
2
3
Для обхода проблемы var нужно использовать fix переменную, в которую помещать текущее (на момент определения) значение var-переменной:
def main = {
var i = 0
var funcs: Func[Unit]* = ()
while(i < 4): {
fix currentI = i // именно текущее значение
funcs += func: #{ println(currentI) } // определение
i = i + 1
}
println("After while")
funcs.${ $.! } // использование
}
Выдаёт:
After while
0
1
2
3
Из-за такой особенности замыкание с изменяемыми переменными запрещено в некоторых языках. Но в Libretto эта проблема меньше выражена, поэтому замыкание обеспечивает доступ к var. И это позволяет организовать хранение состояний объектов (экземпляров трейтов).
Замыкание с var как состоянием объекта
В Libretto нет объектов или классов (в прямом виде). А структуры являются неизменяемыми. Именно 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
todo трейты
Замыкание с var для накопления результата
Замыкание с var переменной может использоваться для накопления результата:
def nTimes(n: Int, f: Func[Any*]) = {
1..n.${ f.! } // использование
()
}
def main = {
var c = 0
nTimes(10): #{ c = c + 10 } // определение
println(c)
}
Выдаёт:
100
В том числе накопление последовательности:
def range(from: Int, to: Int, f: Func[Int, Any*]) = {
from..to as i .${ f.!(i) } // использование
()
}
def main = {
var buffer: Int*
range(10, 19): #(v){ buffer += v } // определение
println: buffer
}
Выдаёт:
(10,11,12,13,14,15,16,17,18,19)
todo см. буфер
Обобщения (параметрический полиморфизм)
Текущее состояние
Libretto ограниченно поддерживает обобщения (вид параметрического полиморфизма).
Пользователь может определять только трейты и структуры с переменными типа. Но ограничения на переменные типа не описываются (можно подставлять любые типы).
Нельзя определять обобщения объединений. (todo)
Использовать переменные типа в определениях методов или функций нельзя на уровне пользовательского программирования, но в стандартном окружении Libretto есть встроенные (библиотечные) методы, которые используют переменные типа. В будущем, возможно, появится и соответствующая пользовательская возможность.
Зачем нужны обобщения
Например, нужно определить структуру для хранения пар значений (кортеж из двух элементов). Если сделать обычным способом, то придётся указывать наиболее общий тип Any:
struct Pair(a: Any, b: Any)
Но это неудобно для работы при статической типизации. Придётся делать преобразования типов при получении элементов.
Можно определять структуру для каждого случая:
struct PairIntInt(a: Int, b: Int)
struct PairIntString(a: Int, b: String)
struct PairValueTree(a: Value, b: Tree)
// и так далее
Но это очень громоздко. Поэтому и нужен параметрический полиморфизм - возможность использования переменных типа (в данном случае переменные типа структуры).
В Libretto частично реализован простой вариант параметрического полиморфизма - “обобщения” (generics). В будущем, возможно, будут добавлены ограничения на параметры типов (ограниченная квантификация).
Например, в Libretto можно сделать обобщенное определение пары:
struct Pair[A, B](a: A, b: B)
Здесь A и B в квадратных скобках [] - это переменные типа Pair, которые превратят Pair в конкретный тип структуры при подстановке значений типов.
Это означает, что можно использовать Pair с указание конкретных типов для подстановки в A и B тоже при помощи квадратных скобок [].
Pair[Int, Int]
Pair[Int, String]
Pair[Value, Tree]
Обобщения в Libretto можно использовать для трейтов и структур. В будущем, возможно, будет реализовано обобщение для объединений и пользовательских методов.
Важно, что обобщения в Libretto используются для работы с отображениями (мэпами) при помощи обобщенного трейта Map[K, V] и обобщенного метода map[K, V]. Это позволило не делать отображения конструкцией языка, а реализовать на уровне библиотеки (пакета libretto).
Имя и квадратные скобки []
В текущей реализации считается, что квадратные [] скобки, которые служат для указания имен (при определении) и значений (при использовании) переменных типа, всегда располагаются после имени и синтаксически считаются его неотъемлемой частью.
Например, метод upcast с подстановкой типа SomeUnion имеет имя upcast[SomeUnion], которое используется целиком в таком виде:
upcast[SomeUnion](value)- обычный синтаксис вызоваvalue.*upcast[SomeUnit]- вызов через .*upcast[SomeUnit]: value- вызов через :
Аналогично и с именами трейтов и структур.
Обобщения трейтов
Можно использовать переменные типа в квадратных скобках для определения обобщения трейта.
trait Test[A, B] {
def work(a: A): B
}
Это означает, что при использовании (подстановке значений переменных типа) будет автоматически создан трейт с уточнёнными типами методов.
Число имён переменных типа от 1 и далее.
Имена переменных типа должны быть уникальные, рекомендуется начинать с заглавной буквы. Обычно применяют просто заглавные буквы, но могут быть и другие варианты.
При разрешении имен типов в определениях методов трейта имя переменных типа приоритетно перед именами типов.
// пример плохого стиля
trait Test[Int] {
def result: Int // Int - это имя переменной типа, а не `Int` из `libretto`
}
def main = {
fix t: Test[String] = new Test[String] {
def result = "abc"
}
println: t.result
}
Выдаёт:
abc
В этом примере Int в определении Test обозначает имя переменной типа (которое будет String в этом примере), а не тип Int из пакета libretto. Чтобы избегать сложностей понимания, лучше не использовать имена переменных типа, которые могут быть восприняты как имена каких-то известных типов. Отсюда и обычное использование заглавных букв, которые больше подходят для имен переменных типа, а не для имени конкретного типа.
В определении методов трейта имена переменных типов используются всегда без указания кардинальности. Тип переменной типа (в том числе с опциональной кардинальностью) будет подставлен при использовании трейта:
Test[Int, String?] - в определении вместо A будет использован Int, а вместо B - String?, т.е. условно можно записать так:
// пример условный, не компилируется
trait Test[Int, String?] {
def work(a: Int): String?
}
Возможно, что в будущем будет добавлено управление переменными типа при помощи обобщенных псевдотрейтов. (todo)
Одну и ту же переменную типа можно использовать несколько раз в определениях метода:
trait Calc[A] {
def startValue: A
def calc(a: A): A
}
def main = {
fix calc = new Calc[Int*] {
def startValue = 1..3
def calc(a) = a.{ -$ }
}
println: calc.calc(calc.startValue)
}
Выдаёт:
(-1,-2,-3)
Если трейт определен с переменными типа, то использовать его можно только с подстановкой типов в квадратных скобках [].
// пример не компилируется
trait Test[A]
def main = {
fix test: Test? = () // ошибка: неизвестный Test
}
И, наоборот, если трейт определен без переменных типа, то нельзя его использовать с подстановкой типов в квадратных скобках [].
// пример не компилируется
trait Test
def main = {
fix test: Test[Int]? = () // ошибка: неизвестный Test
}
Для пользовательских трейтов с параметрами типов не допускается множественное определение с одним именем, даже если изменено число параметров типов:
// пример не компилируется
trait Test[A]
trait Test[A, B] // ошибка: уже есть Test
Единственное исключение - это псевдоним Func (из пакета libretto), который автоматически компилятором отображается в Func0, Func1 и т.д. (в зависимости от числа значений параметров типа). Его поддержка сделана на уровне компилятора. Возможно, что будущем это будет разрешено и для пользовательских определений.
Ещё одно ограничение в том, что нельзя одновременно определять трейт с параметрами типов и одноименный трейт вообще без параметров типа:
// пример не компилируется
trait Test[A]
trait Test // ошибка: уже есть Test
Определение трейта с переменными типа допускает рекурсию:
trait Entry[A] {
def value: A
def entries: Entry[A]*
def string: String
}
def main = {
def entry(entryValue: String, entryEntries: Entry[String]*) = {
new Entry[String] {
def value = entryValue
def entries = entryEntries
def string = "[" + this.value.string +
", " + this.entries.string.*join(",") + "]"
}
}
fix root = entry("root", (entry("a", ()), entry("b", entry("c", ()))))
println: root
}
Выдаёт:
[root, [a, ],[b, [c, ]]]
Допускается использование и других обобщенных трейтов и структур, в том числе с типизацией переменными типа (todo Map, new трейт):
trait Base[A] {
def value: A
}
trait Test[A, B] {
def map(b: Base[A]): Base[B]
}
def main = {
def intBase(v: Int) = new Base[Int] {
def value = v
}
def stringBase(v: String) = new Base[String] {
def value = v
}
fix t = new Test[Int, String] {
def map(b) = {
stringBase: (b.value + 777).string
}
}
println: t.map(intBase(123)).value
}
Выдаёт:
900
В текущей реализации нельзя указать какие-либо ограничения на переменные типа пользовательских трейтов. При их использовании допускается подстановка любых типов в качестве значений переменных типа.
Обобщения структур
todo типизация с разными значениями переменных типа и без типа
Обобщения объединений
Обобщение объединений пока ещё не поддерживается, но, возможно, будет реализовано в будущих версиях Libretto.
Обобщения методов
Пользователь не может определить свои методы с параметрами типа. Но есть предопределенный (с поддержкой на уровне компилятора) набор методов, которые принимают значения для параметров типов.
Из пакета libretto:
upcast[A]- безопасное гарантированное преобразование в типAcast[A]- динамическое преобразование в типAс исключениемdyn[A]- динамическое преобразование в типAс указанием поведенияforce[A]- принудительное преобразование в типA(только для специфического использования)map[K, V]- создание отображения (мэпа) с ключами типаKи значениями типаVbuffer[A]- создание буфера с элементами типаA
Из пакета libretto/num:
array[A]- преобразование в массив элементов типаAnewArray[A]- создание массива элементов типаA
Из пакета libretto/mem/q:
queue[A]- создание блокирующей очереди элементов типаA
И ещё методы из пакета libretto/jvm (см. соответствующий раздел).
Ковариантность, контравариантность, инвариантность
Ковариантность и контравариантность характеризует возможность подстановки контейнеров типов в зависимости от возможности подстановки самих типов. Например, подтипом списка элементов типа A является список элементов типа B, когда B является подтипом A, а реализация не допускает изменения состава списка (неизменяемый список). Иначе будет нарушение принципа подстановки (Б.Лисков). Это пример ковариантности.
В Libretto нет прямого наследования трейтов, а иерархия трейтов определяется возможностью использования одного трейта вместо другого. Поэтому проблема ковариантности и контравариантности решается автоматически (и естественно для программиста) в зависимости от места использования параметра типа: только в параметре методов (контравариантность), только в результате методов (ковариантность). В случае инвариантности будет невозможность преобразования.
trait Test[A] {
def value: A // только тип результата
}
def main = {
fix test: Test[Any] = new Test[Int] {
def value = 123
}
println: test.value
}
Выдаёт:
123
Переменная типа (A) используется только как тип результата метода, поэтому вместо Test[Any] можно подать значение Test[Int] (при помощи неявного преобразования трейта).
trait Test[A] {
def out(v: A): () // только тип параметра
}
def main = {
fix test: Test[Int] = new Test[Any] {
def out(v) = { println(v); () }
}
test.out(123)
}
Выдаёт:
123
В этом примере, наоборот, переменная типа (A) используется как тип параметра метода, поэтому вместо Test[Int] можно подать Test[Any] (при помощи неявного преобразования трейта).
Пример определения обобщенного List
Пример классического связного списка в обобщённом варианте при помощи трейта (можно сделать и при помощи структуры):
use libretto/util
struct Nil
def Nil string = "Nil"
trait Cons[A] {
def head: A
def tail: (Cons[A] | Nil)
def string: String
}
def intList(seq: Int*) = {
var ret: (Cons[Int] | Nil) = Nil()
seq.*util/reverse as v.${
fix t = ret // текущее значение для замыкания
ret = new Cons[Int] {
def head = v
def tail = t
def string = this.head.string + " :: " +
this.tail.string
}
}
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
Пока Libretto не поддерживает обобщение объединения, но теоретически можно было бы добавить определение union List[A] = Cons[A] | Nil.
Обратите внимание, что fix t = ret используется для получения текущего значения ret для замыкания (см. про проблемы замыкания с var).
todo: Поскольку список неизменяемый, то можно подавать список Int вместо, например, списка Int | String:
Отображения (мэпы, Map[K, V], map[K, V])
Общая информация
Отображения (мэпы, ассоциативные массивы) не являются конструкцией языка, а реализованы при помощи обобщенных трейта Map[K, V] и метода map[K, V], определенных в пакете libretto.
Отображения позволяют привязывать значения (V переменная типа) к ключам (K переменная типа).
Порядок ключей не определен.
Это изменяемое отображение: изменения осуществляются “по месту”, а не возвратом нового экземпляра.
Тип Map[K, V]
Трейт Map[K, V] используется в качестве типа для отображения, определен в пакете libretto.
Тип ключа K допускается только без указания кардинальности (строго одно значение). Объединение допускается в качестве типа ключа.
Тип значения V не ограничен (может быть с кардинальностью). Типом значения может быть и трейт, и объединение. Но если допускается пустота, то ключ со значением всё равно будет удаляться при установке пустоты в качестве значения.
Требования к ключам
Ключ должен выполнять правила сравнения и вычисления хэша. Это автоматически выполняется для структур, но не выполняется для трейтов. Поэтому в качестве ключей можно использовать либо предопределенные структуры Int, Real, String, Unit, либо структуры. Статической проверки компилятором этого правила нет.
Поведение для структуры в качестве ключей может изменяться определением equalityKey для этой структуры. См. соответствующее описание.
Создание: map[K, V]
Для создания экземпляра пустого отображения используется метод map[K, V], определенный в пакете libretto. Результатом является экземпляр трейта Map[K, V].
def main = {
fix m0 = map[Int, String]() // вывод типа
fix m1: Map[String, String*] = map[String, String*]()
println: m0.keys
println: m1.keys
}
Выдаёт
()
()
Привязка значения: .!(v) = k
Для установки или изменения привязки значения к ключу используется вызов .!(k) = v, где k - значение ключа (нового или уже существующего), v - значение, которое будет привязано к ключу. Типы k и v соответствуют типизации Map. Тип результата самого метода - пустота ().
def main = {
fix m = map[Int, String]()
m.!(1) = "one"
m.!(1) = "One"
m.!(2) = "Two"
println: m.keys
}
Выдаёт
(1,2)
Вызов метода установки != должен быть через точку и со скобками для ключа: m.!(1) = "one" - это правильно, m!1 = "one" или m!(1) = "one" - это неправильно (устаревшие конструкции, будут убраны).
Метод set для установки тоже не должен использоваться (устаревший, будет убран).
Если в качестве значения передаётся пустота (), то ключ и привязанное значение будут убраны из отображения:
def main = {
fix m = map[Int, String]()
m.!(1) = "one"
m.!(1) = "One"
m.!(2) = "Two"
println: m.keys
m.!(1) = () // удалить ключ 1
println: m.keys
}
Выдаёт:
(1,2)
2
Удаление при значении () (пустота) происходит всегда. Вне зависимости от того, допускает ли типизация Map пустоту или нет.
def main = {
fix m = map[String, String*]()
m.!("abc") = ("a", "b", "c")
println: m.keys
m.!("abc") = ()
println: m.keys
}
Выдаёт:
abc
()
Получение значения: .!(v)
Для получения значения, привязанного к ключу, используется метод .!(v). Если ключа нет, то возвращается пустота. Соответственно, типом результата будет V (из определения Map), но с кардинальностью, допускающей пустоту (? или *).
def main = {
fix m = map[String, Int]()
m.!("abc") = 123
println(m.!("abc"))
println(m.!("abc") + 777) // Int сложение
println(m.!("def"))
println(m.!("def") + 777)
}
Выдаёт:
123
900
()
()
В этом примере для Map[String, Int] результат получения ключа по .!(v) будет типа Int?, где () будет появляться в случае отсутствия ключа.
Имя !
Отображение можно рассматривать как функцию, которая по значению ключа возвращает либо привязанное значение, либо пустоту. В Libretto для подобных действий существует имя метода ! - оно имеет смысл apply (применить).
При определении метода имя берётся в обратных кавычках `!`, а при вызове можно их не указывать: !.
Для установки (привязки к ключу) значения тоже используется метод != с именем, содержащим !. Это принято для единообразия вызовов: m.!(k) = v для установки и m.!(k) для получения значения.
Последовательность ключей: keys
Для получения последовательности всех имеющихся ключей в отображении используется метод keys.
def main = {
fix m = map[String, Int]()
println: m.keys
m.!("abc") = 123
println: m.keys
m.!("def") = 777
println: m.keys
m.!("ghi") = 100
println: m.keys
m.!("ghi") = ()
println: m.keys
m.!("def") = ()
println: m.keys
m.!("abc") = ()
println: m.keys
}
Выдаёт:
()
abc
(abc,def)
(abc,def,ghi)
(abc,def)
abc
()
Ключ, которому выставляется значение пустоты (), удаляется из отображения.
Тип результата keys соответствует типу K в определении Map[K, V], но с кардинальностью *.
def main = {
fix m = map[Int, Unit]()
m.!(123) = unit
m.!(777) = unit
println: m.keys
var sum = 0
m.keys as key.${ sum = sum + key } // сложение Int
println: sum
}
Выдаёт:
(777,123)
900
Устаревшие методы: set, ! и != без точек и скобок
Для установки и получения нужно обязательно использовать методы с ! в имени, через точку и со скобками для ключа. Остальные методы считаются устаревшими и будут убраны в будущих версиях
m.set(k, v)заменить наm.!(k) = vm.set(k): vзаменить наm.!(k) = vm!(k) = vзаменить наm.!(k) = vm!k = vзаменить наm.!(k) = vm!(k)заменить наm.!(k)m!kзаменить наm.!(k)
Множества (Set[V], set[V])
Общая информация
Множества не являются конструкцией языка, а реализованы при помощи обобщенных трейта Set[V] и метода set[V], определенных в пакете libretto.
Множество (Set) - это коллекция, которая содержит элементы. Проверка на вхождение элемента в множество эффективна (в отличие от простой последовательности). Но у множества отсутствует порядок, а элементы в множестве содержатся только в единственном экземпляре.
Это изменяемое отображение: изменения осуществляются “по месту”, а не возвратом нового экземпляра.
Тип Set[V]
В пакете libretto определен трейт Set:
trait Set[V] {
def set(v: V): Unit?
def remove(v: V): Unit?
def `!`(V: V): Unit?
def values: general(V, Nothing*)
def clear: ()
def len: Int
}
Требования к типу элемента [V]: это тип без кардинальности (строго одно значение). Допускаются объединения.
Требования к элементам
Тип элемента должен выполнять правила сравнения и вычисления хэша. Это автоматически выполняется для структур, но не выполняется для трейтов. Поэтому в качестве типа элементов можно использовать либо предопределенные структуры Int, Real, String, Unit, либо структуры. Статической проверки компилятором этого правила нет.
Поведение для структуры в качестве элементов множества может изменяться определением equalityKey для этой структуры. См. соответствующее описание.
Создание: set[V]
Для создания Set используется метод (в условной записи) из пакета libretto:
def set[V]: Set[V]
Методы в Set
Трейт Set определяет следующие методы:
set служит для добавления элемента (в случае его отсутствия). Возвращает unit, если элемент добавлен. И (), если элемент уже был в Set.
remove удаляет элемент (при его наличии). Возвращает unit, если элемент был и удален. И (), если элемента не было.
! служит для проверки вхождения элемента в Set.
values возвращает содержимое Set как последовательность. Операция довольно медленная, поскольку создаётся копия. Порядок элементов в возвращаемой последовательности может быть любым. Нужно сортировать последовательность, если порядок важен.
clear очищает Set, удаляя все элементы (если они есть).
len возвращает число элементов в Set.
Пример
use libretto/util
def info(str: String, v: Any*): () = {
println: str+": "+util/string(v)
()
}
def main = {
fix s = set[Int]
info("len"): s.len
info("values"): s.values
info("set(777)"): s.set(777)
info("len"): s.len
info("values"): s.values
info("!(777)"): s.!(777)
info("set(777)"): s.set(777)
info("set(123)"): s.set(123)
info("len"): s.len
info("values"): s.values
info("!(123)"): s.!(123)
info("!(777)"): s.!(777)
info("!(900)"): s.!(900)
info("remove(123)"): s.remove(123)
info("!(123)"): s.!(123)
info("remove(123)"): s.remove(123)
info("len"): s.len
info("values"): s.values
println("clear")
info("len"): s.len
println
fix s2 = set[String]
0..9 as v.${ s2.set("test"+v) }
println: s2.values
println: s2.values.*sort($)
}
Выдаёт:
len: 0
values: ()
set(777): unit
len: 1
values: 777
!(777): unit
set(777): ()
set(123): unit
len: 2
values: (777,123)
!(123): unit
!(777): unit
!(900): ()
remove(123): unit
!(123): ()
remove(123): ()
len: 1
values: 777
clear
len: 1
(test4,test5,test2,test3,test8,test9,test6,test7,test0,test1)
(test0,test1,test2,test3,test4,test5,test6,test7,test8,test9)
Анонимные функции (FuncN, Func)
Функциональные возможности
Libretto поддерживает базовые функциональные возможности. Можно определять анонимные функции с замыканием. Такие анонимные функции являются значениями, которые можно передавать в другие методы или функции (высших порядков), возвращать как результат.
Трейты FuncN и Func
Анонимная функция - это код, который можно вызывать по требованию (лениво), передавая нужные аргументы и получая результат.
В Libretto анонимные функции определяются трейтами FuncN из базового пакета libretto.
Анонимная функция без параметров, но возвращает значение:
trait Func0[R] {
def `!`: R
}
Анонимная функция с одним параметром:
trait Func1[P0, R] {
def `!`(p0: P0): R
}
Анонимная функция с двумя параметрами:
trait Func2[P0, P1, R] {
def `!`(p0: P0, p1: P1): R
}
И т.д.
Общее правило: Анонимная функция определяется трейтом с именем FuncN, где N - это число параметров анонимной функции (от 0 включительно). Сам же трейт параметризуется типами в количестве N + 1. Первые N типов - это последовательное перечисление типов параметров, последний тип - это всегда тип результата. Например:
Func0[Int] - анонимная функция, которая не принимает параметры и возвращает Int
Func3[Int, Int, String, String*] - анонимная функция, которая принимает три параметра (типов Int, Int, String) и возвращает тип String*.
Для запоминания: в обычном определении def f(a: A, b: B): C тип результата всегда последний, так и в в определении трейта для анонимной функции результат тоже всегда последний параметрический тип.
Для удобства, чтобы не считать число параметров, есть универсальный трейт-псевдоним Func (тоже из пакета libretto), который работает как FuncN, но без указания числа параметров. Количество параметров анонимной функции подсчитает сам компилятор.
Func[Int] - это псевдоним Func0[Int]
Func[Int, Int, String, String*] - это псевдоним Func3[Int, Int, String, String*]
Во всех трейтах Func/FuncN определен только один метод !, который и служит для вызова кода анонимной функции. ! здесь и во всём Libretto имеет смысл apply (применить).
При определении трейта имя метода ! оборачивается в обратные кавычки как `!`, но при вызове метода можно использовать имя без кавычек: !.
Определение анонимной функции
Поскольку анонимная функция - это трейт с одним методом !, то можно определить конкретный экземпляр при помощи обычного способа создания экземпляра трейта:
def printValue(f: Func[String]) = {
println("printValue")
println(f.!)
}
def main = {
fix f = new Func[String] {
def `!` = "Hello"
}
printValue(f)
}
Выдаёт:
printValue
Hello
Естественно, допустимо замыкание:
def printValue(f: Func[String]) = {
println("printValue")
println(f.!)
}
def createFunc(name: String): Func[String] = {
new Func[String] {
def `!` = "Hello, " + name
}
}
def main = {
fix name = "world"
fix f = createFunc(name)
printValue(f)
}
Выдаёт:
printValue
Hello, world
Такое определение вполне возможно и не противоречит духу Libretto, но оно достаточно громоздкое. Для более простого определения используется встроенная функция func из пакета libretto и #-выражение, которое передаётся единственным аргументом в func.
Определение анонимной функции без параметров при помощи func и #{ ... } с использованием замыкания:
def printValue(f: Func[String]) = {
println("printValue")
println(f.!)
}
def main = {
fix name = "world"
fix f = func: #{ "Hello, " + name }
printValue(f)
}
Выдаёт:
printValue
Hello, world
Определение анонимной функции с двумя параметрами:
def printValue(f: Func[Int, Int, Int]) = {
println("result for 1 and 2: " + f.!(1, 2))
}
def main = {
fix f = func: #(a: Int, b: Int) { a + b }
printValue(f)
}
Выдаёт:
result for 1 and 2: 3
Общий синтаксис #-выражения: #(параметры, если есть){ выражение }
Параметры перечисляются в таком же виде, как в обычном методе (с обязательным указанием типа). Но если параметров нет, то круглые скобки не ставятся. Выражение (тело анонимной функции) является обычным выражением Libretto и может занимать несколько строчек и определять переменные (поскольку является частью {}-выражения):
def printValue(f: Func[Int, Int, String, String]) = {
println("result for 1, 2, \"@\": " + f.!(1, 2, "@"))
}
def main = {
fix f = func: #(a: Int, b: Int, suffix: String) {
fix a100 = a + 100
fix b100 = b + 100
a100.string + b100.string + suffix
}
printValue(f)
}
Выдаёт:
result for 1, 2, "@": 101102@
Между # и () пробел не ставится. Между круглыми скобами () и фигурными {} обычно принято ставить пробел. Но если круглых скобок () нет (анонимная функция без параметров), то между # и фигурными скобкаи {} пробел не ставится.
Вызов func возник по историческим причинам, поскольку #-выражение использовались для определения устаревшей “лямбды”. Актуальные анонимные функции появились позднее и требовали какого-то отличия, чем и стал вызов func. В будущем возможен полный отказ от func. Кроме того, в некоторых случаях можно func уже не указывать - см. ниже описание таких случаев.
В целом общее правило: как тип использовать Func или FuncN, а для определения использовать #-выражение с вызовом func (который можно убрать в некоторых случаях).
Замыкание
Как и при создании экземпляра трейта, при определении анонимной функции можно использовать замыкание:
def create(a: Func[Int]): Func[Int, Int] = {
func: #(b: Int){ a.! + b }
}
def main = {
fix num = 123
fix f = func: #{ num }
fix sumF = create(f)
println(sumF.!(777))
}
Выдаёт:
900
В том числе и var для замыкания:
def addAll(bufferFunc: Func[Int, ()]) = {
0..3 as i.${ bufferFunc.!(i) }
()
}
def main = {
var buffer: Int* = ()
fix f = func: #(v: Int){ buffer += v }
addAll(f)
println(buffer)
}
Выдаёт:
(0,1,2,3)
Типизация результата
При использовании трейта Func/FuncN нужно явно указывать тип результата (последним параметрическим типом). При использовании func и #-выражения вывод типа результата производится автоматически:
def main = {
fix f = func: #(x: Int) { x + "abc" }
println: type(f)
}
Выдаёт
libretto/Func1[libretto/Int,libretto/String]
Но иногда надо указать тип руками (например, менее специфичный). Для этого нужно преобразовать результат явно в нужный тип. Например, при помощи fix с указанием типа:
def main = {
fix f = func: #(x: Int) {
fix ret: (Int | String) = x + "abc"
ret
}
println: type(f)
}
Выдаёт:
libretto/Func1[libretto/Int,(libretto/Int | libretto/String)]
Либо при помощи upcast:
def main = {
fix f = func: #(x: Int) {
upcast[(Int | String)]: x + "abc"
}
println: type(f)
}
Выдаёт:
libretto/Func1[libretto/Int,(libretto/Int | libretto/String)]
Если результат не нужен, то можно использовать ():
def work(f: Func[()]) = {
f.!
}
def main = {
fix f = func: #{ println("Hello"); () }
work(f)
}
Выдаёт
Hello
$, this, return
Например, два эквивалентных определения:
fix f = new Func[String] {
def `!` = expr1
}
и
fix f = func: #{ expr2 }
В обоих определениях стартовым контекстом $ для выражения является unit.
А вот this работает по-разному.
В трейте допускается this (в expr1), что будет означать сам экземпляр трейта. Это позволяет использовать рекурсию:
def main = {
fix fact = new Func[Int, Int] {
def `!`(n: Int): Int = { if (n < 2) 1 else n * this.!(n-1) }
}
1..5 as i.${
println: i+"! = "+fact.!(i)
}
}
Выдаёт:
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
А вот #-выражение не позволяет использовать this (в expr2), это вызывает ошибку компиляции.
Проблема в том, что будет конфликт: this может восприниматься и как this со значением самой анонимной функции, и как внешний this для замыкания:
def Int test = func: #{ println(this) } - здесь this какого типа?
Кроме того, использование this для обозначения самой анонимной функции (как в создании экземпляра трейта) требует знания типа результата этой анонимной функции (для типизации Func/FuncN), а он будет выведен только после компиляции тела анонимной функции, где этот this и используется.
Есть важное отличие и при использовании return.
В первом определении (через трейт) компилятор допускает использование return (в expr1), который делает возврат из определяемого метода `!`.
Во втором определении (через func и #) компилятор полностью запрещает использование return (в expr2). По аналогичной с this причине. return может относиться как к самой анонимной функции, так и к внешней функции или методу:
def test: Int = func: #{ return 123 } - это возврат из анонимной функции (как при создании экземпляра трейта) или из метода test?
И опять же для return надо знать тип результата анонимной функции, а он выводится только после компиляции тела, где этот return и используется.
Ещё стоит отметить, что в будущем return может быть ограничен в использовании, поэтому уже сейчас предпочтительно либо стараться не использовать return, либо (для нелокальных возвратов) использовать связку breaking и break (todo).
Вызов анонимной функции
Поскольку анонимная функция представлена трейтом с единственным методом !, то для вызова он же и используется обычным способом через точку .. Особенность в том, что при вызове имя ! не нужно оборачивать в обратные скобки (в отличие от определения метода с таким именем).
Если параметров нет, то вызывается метод ! без скобок. Если параметры есть, то они передаются в скобках. В целом ! ведёт себя полностью как обычный метод (с анонимной функцией в контексте), но со специфическим именем.
def main = {
fix f0 = func: #{ "f0" }
fix f2 = func: #(a: Int, b: Int) { "f2: "+ (a + b) }
println: f0.!
println: f2.!(1, 2)
println: f2.!(1): 2
}
Выдаёт:
f0
f2: 3
f2: 3
Метод ! во всём Libretto имеет смысл apply (применить).
Без func
Если анонимная функция определяется при помощи #-выражения непосредственно как аргумент параметра типа FuncN/Func при вызове метода, то можно не использовать вызов func:
def work(f: Func0[String]) = {
println(f.!)
}
def main = {
fix f = func: #{ "a" } // func нужен
work(f)
work(#{ "b" }) // можно без func
work(func: #{ "c" }) // можно с func
}
Выдаёт:
a
b
c
Но этот способ не работает при overloading, т.е. если есть несколько подходящих методов с параметром Func/FuncN.
В будущем возможен полный отказ от вызова func. Этот вызов нужен только из-за устаревшего определения “лямбды”.
Параметры без типа
Если анонимная функция определяется при помощи #-выражения непосредственно как аргумент параметра типа FuncN/Func без использования func при вызове метода, то можно не указывать и типы параметров этой анонимной функции:
def work(f: Func[Int, Int]) = {
println(f.!(123))
}
def main = {
work: #(a){ a + 777 } // здесь a типа Int
}
Выдаёт:
900
Компилятор сам выводит типы из определения соответствующего FuncN/Func параметра.
{...}, ${...}, #{...}
Конструкции, которые выглядят похожими. Но смысл разный:
{...}- исполнение кода в скобках{}, результатом является результат последнего выражения (или пустота(), выражений нет)${...}- исполнение кода в скобках{}, но результатом является текущий контекст$.#{...}- часть определения анонимной функции без параметров, тело функции задаётся в скобках{}
Внутри скобок {} всех трёх конструкций допускается несколько выражений, разделенных переносом строк (или ;), в том числе определения переменных fix/var.
Ограничение по числу параметров
В настоящее время Func имеет ограничение в 10 параметров анонимной функции (Func10):
trait Func10[P0, P1, P2, P3, P4, P5, P6, P7, P8, P9, R] {
def `!`(p0: P0, p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9): R
}
Но в будущем (по мере необходимости) число параметров может быть увеличено.
Другие (произвольные) экземпляры Func/FuncN
Поскольку базой анонимной функции является трейты Func/FuncN, то есть и другие способы создания экземпляров, которые тоже будут считаться анонимными функциями.
Например, Map является функцией (но в тип результата добавляется пустота, обозначающая отсутствие ключа):
def work(f: Func[Int, String?]) = {
1..3 as v.${
println(s<< #{v}: #{f.!(v).*onEmpty("?")}>>)
}
}
def main = {
println("Function")
work: #(v) {
if (v mod 2 == 0) "even" else "odd"
} // передаём обычную анонимную функцию
println("Map")
fix m = map[Int, String]()
m.!(1) = "one"
work: m // передаём Map с неявным преобразованием трейта
}
Выдаёт:
Function
1: odd
2: even
3: odd
Map
1: one
2: ?
3: ?
И даже структура с полем ! может служить функцией (как и ручное определение метода !):
def work(f: Func[String]) = {
println(" " + f.!)
}
struct Test(`!`: String)
def main = {
println("Function")
work: # {
"func"
} // передаём обычную анонимную функцию
println("Struct")
fix t = Test("test")
work: t // передаём структуру с неявным преобразованием в трейт
}
Выдаёт:
Function
func
Struct
test
Всё, что попадает под сигнатуру метода ! трейтов Func/FuncN, можно рассматривать как анонимные функции, пусть и со специфической реализацией.
breaking и break
break позволяет делать возвраты (в том числе глубокие). Это продвинутый аналог return, но возврат относится не к телу метода, а к определению breaking. break имеет некоторые дополнительные расходы ресурсов при работе, поэтому нужно использовать только там, где это оправдано.
Метод breaking принимает один аргумент - это код, который будет исполнен в контексте. Если код исполнен, то результатом breaking и будет результат кода:
def main = {
fix r = 123.breaking: { $ + 777}
println: r
}
Выдаёт:
900
Но код внутри breaking можно прервать досрочно при помощи break. break принимает один аргумент, который и станет результатом breaking в случае срабатывания этого break.
def main = {
0..3 as v.${
fix r = breaking: {
if (v == 0)
break: "zero"
if (v == 1)
break: "one"
"???"
}
println: s<<#{v}: #{r}>>
}
}
Выдаёт:
0: zero
1: one
2: ???
3: ???
break, как видно, может использоваться в нескольких местах, относящихся к одному breaking.
Тип результата breaking является общим типом для прямого результата (без срабатывания break) и всех типов результатов, переданных при помощи break:
def main = {
0..3 as v.${
fix r = breaking: {
if (v == 0)
break: "abc"
if (v == 1)
break: unit
3.14
}
println: s<<#{v}: #{r} (#{type(r)})>>
}
}
Выдаёт:
0: abc ((libretto/Real | libretto/String | libretto/Unit))
1: unit ((libretto/Real | libretto/String | libretto/Unit))
2: 3.14 ((libretto/Real | libretto/String | libretto/Unit))
3: 3.14 ((libretto/Real | libretto/String | libretto/Unit))
Здесь результатом breaking является объединение Real (прямой результат), Int и Unit (результаты break).
break может располагаться в области видимости breaking, но физически исполняться в другом месте:
def test(code: Func[()]) = {
code.!
123
}
def main = {
0..3 as v.${
fix r = breaking: {
test: #{ if (v == 1) break: "Hello" }
}
println: s<<#{v}: #{r}>>
}
}
Выдаёт:
0: 123
1: Hello
2: 123
3: 123
break в этом примере располагается в области видимости breaking, но “физически” срабатывает внутри метода test. Это позволяет делать глубокие возвраты значения.
Вложенные breaking поддерживаются, но break относится к самому внутреннему breaking:
def main = {
fix r = breaking: {
breaking: {
break: "Hello!"
}
123
}
println: r
}
Выдаёт:
123
Если вынести “физический” момент срабатывания break за пределы breaking, то будет исключение во время исполнения:
def main = {
fix r = breaking: {
func: #{ break: 123; () }
}
r.*case x: Func[()] => x.!
}
Выдаётся исключение Uncaught break 123 (breakId 1).
Если breaking заведомо заканчивается только вызовом break, то всё равно нужно учитывать тип блока для breaking.
Пример (поиск последовательным перебором целого числа, которое больше 0 и которое без остатка делится на 3, 5, и 7 одновременно):
def main = {
var c = 1
fix r = breaking: {
while (unit): {
if (c mod 3 == 0 and c mod 5 == 0 and c mod 7 == 0) break: c
c = c + 1
}
}
println: r
println: type(r)
}
Выдаёт
105
libretto/Int?
Здесь вечный цикл while(unit), который прерывается только break, но компилятор в настоящее время не определяет это, предполагая завершение while. Поскольку у while тип результата пустота, то у блока для breaking тоже тип пустота. Поскольку break возвращает результат Int, то обобщение с пустотой даёт Int? - это и есть тип результата breaking.
Поэтому в таком случае нужно в блок для breaking добавить значение с типом - (наиболее специфичный тип Libretto). Обобщение с этим типом не меняет тип. Таким значением может быть, например, вызов error.
Подправленная версия:
def main = {
var c = 1
fix r = breaking: {
while (unit): {
if (c mod 3 == 0 and c mod 5 == 0 and c mod 7 == 0) break: c
c = c + 1
}
error("Unreachable code")
}
println: r
println: type(r)
}
Выдаёт:
105
libretto/Int
libretto/num
64-битное целое число (libretto/num/I64)
Структура libretto/num/I64 представляет 64-битное знаковое целое число. С ним возможно использование операций +, -, *, idiv.
use libretto/num
def main = {
fix i1 = num/i64(777)
fix i2 = num/i64(123)
println: i1 + i2
println: i2 - i1
println: i1 * i2
println: i1 idiv i2
}
Выдает:
900
-654
95571
6
При помощи метода libretto/num/i64 определяются литералы, аргумент должен быть литералом libretto/Int. Другие методы преобразования определены в пакете libretto/num:
def Int i64: I64
def I64 int: Int
Битовые операции с libretto/num/I64, определенные методами в пакете libretto/num:
def I64 and(v: I64): I64
def I64 or(v: I64): I64
def I64 xor(v: I64): I64
def I64 not: I64
def I64 `>>>`(v: I64): I64 // беззнаковый битовый сдвиг
def I64 `>>`(v: I64): I64 // знаковый битовый сдвиг
def I64 `<<`(v: I64): I64 //
Пример использования:
use libretto/num
def binary(v: num/I64) = {
fix one = num/i64(1)
var acc = v
var res: String = ""
0..63.${
res = (if (acc.and(one) == one) "1" else "0") + res
acc = acc.`>>>`(one)
}
println(res)
}
def main = {
fix minI64 = num/i64(-9_223_372_036_854_775_808)
fix maxI64 = num/i64(9_223_372_036_854_775_807)
fix one = num/i64(1)
binary(minI64)
binary(maxI64)
binary(one)
println()
binary(minI64.not)
binary(maxI64.not)
binary(one.not)
println()
binary(minI64.or(maxI64))
binary(minI64.and(maxI64))
binary(maxI64.and(one))
binary(maxI64.xor(one))
println()
binary(one.`<<`(num/i64(3)))
binary(minI64.`>>`(num/i64(3)))
binary(minI64.`>>>`(num/i64(3)))
}
Результат:
1000000000000000000000000000000000000000000000000000000000000000
0111111111111111111111111111111111111111111111111111111111111111
0000000000000000000000000000000000000000000000000000000000000001
0111111111111111111111111111111111111111111111111111111111111111
1000000000000000000000000000000000000000000000000000000000000000
1111111111111111111111111111111111111111111111111111111111111110
1111111111111111111111111111111111111111111111111111111111111111
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000001
0111111111111111111111111111111111111111111111111111111111111110
0000000000000000000000000000000000000000000000000000000000001000
1111000000000000000000000000000000000000000000000000000000000000
0001000000000000000000000000000000000000000000000000000000000000
64-битное вещественное число (libretto/num/R64)
Структура libretto/num/R64 представляет собой 64-битное знаковое вещественное число (IEEE 754). С ним возможно использование операций +, -, *, div
use libretto/num
def main = {
fix i1 = num/r64(7.77)
fix i2 = num/r64(1.23)
println: i1 + i2
println: i2 - i1
println: i1 * i2
println: i1 div i2
}
Выдает:
9.0
-6.539999999999999
9.5571
6.317073170731707
При помощи метода libretto/num/r64 определяются литералы, аргумент должен быть литералом libretto/Real. Другие методы преобразования определены в пакете `libretto/num:
def Real r64: R64
def Int r64: R64
def I64 r64: R64
def R64 real: Real
Операции с libretto/num/R64, определенные в пакете libretto/num:
def R64 sqrt: R64
def R64 log: R64
def R64 exp: R64
def R64 pow(p: R64): R64
Массив (Array из libretto/num)
В библиотеке libretto/num реализована поддержка массивов (изменяемая нумерованная последовательная коллекция фиксированной длины).
Длина массива зафиксирована при создании. Невозможно изменить длину уже созданного массива, нужно создать новый, куда скопировать содержимое старого массива.
Массив является изменяемым. Можно заменить элемент с указанным индексом на другой.
Массивы не рекомендуется к обычному использованию, только для целей оптимизации.
Массив задаётся трейтом Array из пакета libretto/num. Запись определения Array, который типизируется типом элемента массива:
trait Array[A] {
def `!`(position: Int): A
def set(position: Int, newValue: A): ()
def len: Int
}
Метод ! для доступа к элементу массива по индексу (от 0 включительно). В случае выхода за границы будет исключение.
Метод set для установки элемента массива по индексу (от 0 включительно). В случае выхода за границы будет исключение.
Метод len для получения длины массива (фиксированное значение).
Можно создать массив из последовательности при помощи метода array из пакета libretto/num: def array[A](seq: A): Array[TypeSingle[A]] (запись условная)
Пример:
use libretto/num
def main = {
fix arr = (10,20,30).*num/array
println: type(arr)
println: arr.len
0..(arr.len - 1) as i.${
println: s"#{i}: #{arr.!(i)}"
}
arr.set(0): 100
arr.set(1): 101
arr.set(2): 103
0..(arr.len - 1) as i.${
println: s"#{i}: #{arr.!(i)}"
}
}
Выдает:
libretto/num/Array[libretto/Int]
3
0: 10
1: 20
2: 30
0: 100
1: 101
2: 103
Другой способ создания при помощи метода newArray (запись условная!):
def newArray[A](len: Int, fillValue: ByName[A]): Array[A]
Указывается длина создаваемого массива и выражение (заполнитель), которое вызывается для каждого элемента последовательно при инициализации массива.
В массиве можно хранить пустоту и даже последовательности.
Пример:
use libretto/num
def main = {
fix arr = num/newArray[Any*](10, ())
println: type(arr)
println: arr.len
println: arr.!(0)
arr.set(0): (1, 2, 3)
println: arr.!(0)
}
Выдает:
libretto/num/Array[libretto/Any*]
10
()
(1,2,3)
Пример динамического заполнителя:
use libretto/num
def main = {
var v = 0
fix arr = num/newArray[Int](10): v.${ v = v + 100 }
0..(arr.len - 1) as i.${
println: s"#{i}: #{arr.!(i)}"
}
}
Выдает:
0: 0
1: 100
2: 200
3: 300
4: 400
5: 500
6: 600
7: 700
8: 800
9: 900
Вызов newArray можно не типизировать типом элемента. В этом случае типом элементов массива будет тип результата выражения-заполнителя.
Массивы можно делать многомерными. Пример:
use libretto/num
def main = {
fix arr = num/newArray(3, num/newArray(5, 0))
0..2 as a.${
0..4 as b.${ print: " "+arr.!(a).!(b) }
println()
}
0..2 as v.${ arr.!(v).set(v): (v + 1) }
println()
0..2 as a.${
0..4 as b.${ print: " "+arr.!(a).!(b) }
println()
}
}
Выдает:
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
1 0 0 0 0
0 2 0 0 0
0 0 3 0 0
Компиляция: предупреждения, прагмы
Предупреждения
По умолчанию выдаются предупреждения:
-
Использование устаревших конструкций, которые будут удалены в ближайшее время.
-
Использование (определение) неизвестной прагмы.
-
Использование (определение) прагмы, которая ощутимо влияет на ход компиляции:
_pragma_jvm_export,_pragma_jvm_forced_compilation,_pragma_strict_unfold. -
Помощь, которая выдаётся при использовании (определении) прагмы
_pragma_info.
Текущие предупреждения:
Несколько as в одном шаге:
def main = {
1.3 as a as b.println(a + " " + b)
}
Возможное исправление:
def main = {
1.3 as a.println(a + " " + a)
}
Использование $ в правой части от = в case или else:
def main = {
123.{
case x = println(s"value is #{$}")
else = $
}
}
Это ошибка в коде, $ в правой части всегда unit, а не контекст всего case-блока (не 123, как можно было бы ожидать).
Прагмы-структуры
Это структуры, которые определяются в любом месте приложения-программы. Имена прагма-структур начинаются с _pragma_ (нужно использовать обратные кавычки вокруг имени). Прагма-структура не должна содержать поля.
Пример:
struct `_pragma_all_warnings` // в обратных кавычках
def test(a: Int) = 123 // неиспользуемая a
def main = {}
Далее рассмотрены прагма-структуры, добавляющие предупреждения.
_pragma_cardinality_warning
_pragma_compare_warning
Предупреждение об ошибочных сравнениях (разные типы или множественные значения).
struct `_pragma_compare_warning`
def main = {
println: 1 < "abc"
println: 1 < (1, 2)
}
Исправление зависит от решаемой задачи. Может быть и ошибочный код.
_pragma_context_this_unit_warning
Предупреждение об использовании this или unit после точки.
struct `_pragma_context_this_unit_warning`
def Int test = {
println: 1..3.this
}
def main = {
777.test
println: 1..3.unit
}
Возможное исправление:
struct `_pragma_context_this_unit_warning`
def Int test = {
println: 1..3.{ this }
}
def main = {
777.test
println: 1..3.{ unit }
}
_pragma_context_warning
_pragma_contextless_warning
Предупреждение о вызове методов без контекста, но справа от точки
struct `_pragma_contextless_warning`
def test = 123
trait Test
struct Str(string: String)
def main = {
println: unit.test
println: unit.Test(123)
println: unit.Str("abc")
}
Возможное исправление:
struct `_pragma_contextless_warning`
def test = 123
trait Test
struct Str(string: String)
def main = {
println: unit.{ test }
println: unit.{ Test(123) }
println: unit.{ Str("abc") }
}
_pragma_dynamic_warning
Предупреждение о динамическом преобразовании, которое никогда не произойдет.
struct `_pragma_dynamic_warning`
def main = {
fix a: Int = 123
println: a.*{
case str: String = str // предупреждение
else = "?"
}
println: a.*dyn[String] // предупреждение
}
Исправление зависит от решаемой задачи. Возможна и ошибка кода.
_pragma_empty_step_warning
Предупреждение об использование шага обработки пустоты.
struct `_pragma_empty_step_warning`
def main = {
fix i1: Int? = 123
println: i1.?(-1).($ + 777)
fix i2: Int? = ()
println: i2.?(-1).($ + 777)
}
Возможное исправление:
struct `_pragma_empty_step_warning`
def main = {
fix i1: Int? = 123
println: i1.*onEmpty(-1): #(x){x + 777}
fix i2: Int? = ()
println: i2.*onEmpty(-1): #(x){x + 777}
}
_pragma_lambda
_pragma_method_0_call
_pragma_old_dyn_warning
Предупреждение о dyn без полного типа.
struct `_pragma_old_dyn_warning`
def main = {
fix a: Any* = 123
println: a.*dyn(#!)
println: a.*dyn(#+)
}
Возможное исправление:
struct `_pragma_old_dyn_warning`
def main = {
fix a: Any* = 123
println: a.*dyn(#~)
println: a.*dyn(#~+)
}
Но ещё лучше так:
struct `_pragma_old_dyn_warning`
def main = {
fix a: Any* = 123
println: a.*dyn[Int]
println: a.*dyn[Int+]
}
_pragma_!_call
Использование устаревших вызовов ! без точки.
Все перечисленные прагма-структуры автоматически активируются при включении прагмы всех предупреждений:
_pragma_info
Выдаётся предупреждение, которое содержит список всех прагм, поддерживаемых компилятором.
struct `_pragma_info`
def main: () = {
println("Hello, world!")
}
_pragma_all_warnings
Прагма работает как все включенные прагмы предупреждений
Прагма-структура, которая влияет на процесс компиляции:
_pragma_strict_unfold
_pragma_jvm_forced_compilation
Принудительное преобразование всей программы в байт-код при компиляции (для отлова Method too large)
struct `_pragma_jvm_forced_compilation`
def v = (1,2,3)
def main = ()
def test = {
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v.v
}
С указанной прагмой будет ошибка компиляции Method too large [method _default/test defined as libretto/Any _default/test].
Прагмы-методы
Это методы, которые определяются в любом месте приложения-программы, имя которых начинаются с _pragma_ (нужно использовать обратные кавычки).
_pragma_jvm_export
Тело метода _pragma_jvm_export должно быть литералом строки, которая задает каталог для экспорта класс-файлов во время компиляции.
def `_pragma_jvm_export` =
<<d:\123>> // путь должен быть до реального каталога
def main = {
}
Оформление кода
Перечисленные рекомендации не проверяются компилятором, поэтому являются только советами, которые позволяет создавать пригодный и привычный для чтения код.
Стиль записи нескольких слов слитно без пробелов, при котором каждое следующее слово внутри пишется с заглавной буквы, будет далее называться camel case.
Именование
Имена пакетов определяются как измененная запись некоторого url (см. соответствующий раздел). Но обычной практикой является использование только строчных букв с разделением слов при помощи подчёркивания _ или вовсе без разделителя: com/teacode/html, com/teacode/xmlstream, libretto/jvm_jar/websocket, libretto/jvm_jar/nimbus_jose_jwt - в том числе для совместимости с файловыми системами, которые не являются чувствительными к регистру.
Имена структур и трейтов с заглавной буквы и в camel case, главное слово - это существительное (чаще в единственном, но возможно и множественном числе) либо иногда прилагательное (причастие прошедшего времени): Person, TypeInfo, LinkedList, RemovedNumbers, Pinned.
Имя поля (структуры) со строчной буквы и в camel case, главное слово - это существительное (в единственном или множественном числе) либо прилагательное (причастие прошедшего времени): name, parents, sorted, hiddenFiles
Имена методов (обозначающих действие) со строчной буквы и в camel case, главное слово - это глагол: print, removeFromList.
Но имена методов, которые используют new-выражение без указания имени трейта (определение трейта “по месту”), должны выбираться по принципу имён структур и трейтов, а не по принципу обычных методов.
В некоторых случаях глагол можно опускать в имени метода, если используется предлоги, которые позволяют интуитивно понять происходящее действие (to, from и т.п.).
Имена методов, похожих по поведению на поля, определяются аналогично именам полей.
Если метод не имеет параметров и не имеет побочных эффектов, то используется вариант без скобок (чаще всего действие похоже на вычислимое поле): title.
Если метод не имеет параметров, но имеет побочные эффекты, то используется вариант с пустыми скобами: println().
todo (устарело): В чистом виде setter/getter отсутствуют и нет рекомендаций по использованию глаголов set или get для имен методов. Имя метода получения значения допустимо оформлять как имя поля. Метод изменения допустимо оформлять с использованием имени поля, но с параметром (параметрами).
Имена переменных (в том числе as и case-переменных) и параметров со строчной буквы и в camel case, чаще всего главное слово - это существительное или буква (в очевидных случаях).
Префиксы как часть имени
Интересным направлением стиля оформления является использование префиксов (при импорт-подключении пакетов) как смысловой части имени сущности.
Например, есть сущность Node (узел), который используется в разных пакетах. При возможном частом одновременном использовании этих пакетов можно использовать более сложные имена: в одном пакете это, например, будет SourceNode, в другом пакете - TreeNode.
Но можно часть сложного имени перенести в имя пакета, одновременно используя простое имя Node. Например, Node в пакете org/example/source и тоже Node в пакете org/example/tree. Тогда при использовании префиксного имени с импортом по умолчанию будут доступны имена source/Node и tree/Node.
Имена файлов и кодировка
При оформлении библиотек и веб-приложений нет требований к именам файлов и каталогов, только необходимо использовать разрешение .ltt для исходных текстов на Libretto, сами тексты должны быть в UTF-8 кодировке (без BOM).
Но рекомендуется использовать понятную внутреннюю организацию: разделять части пакета по разным файлам, подпакеты размещать в подкаталогах. Имена файлов давать строчными буквами (на случай использования файловой системы без чувствительности к регистру символов), в качестве разделителя слов в имени использовать _ или не использовать разделитель вообще.
Пробелы, переносы строк
Используются отступы, состоящие из двух пробелов, использовать символ табуляции в явном виде не рекомендуется.
Если блок с фигурными скобами записывается в несколько строк, то открывающая скобка { ставится в конце строки, затем идёт тело блока с дополнительным отступом, а закрывающая скобка } ставится следующей строкой со соответствующим отступом:
def test(v: Unit?) = {
if (v) {
println("unit")
}
else {
println("()")
}
}
Прочие особенности синтаксиса
Не рекомендуется использовать бесскобочный способ записи вызова метода (в первую очередь при помощи :) в сложных конструкциях. Нужно учитывать приоритеты, которые могут отличаться от ожидаемых. Например, a(b: c, d) работает как a(b(c, d)) а не как a(b(c), d).
Пример:
def b(p0: String) = "b("+p0+")"
def b(p0: String, p1: String) = "b("+p0+", "+p1+")"
def a(p0: String) = "a("+p0+")"
def a(p0: String, p1: String) = "a("+p0+","+p1+")"
def main = {
println: a(b: "1", "2")
println: a((b: "1", "2"))
println: a((b: "1"), "2")
}
Выдает:
a(b(1, 2))
a(b(1, 2))
a(b(1),2)
Конструкции с case могут показаться более громоздкими, но часто они даёт лучшую читабельность кода.
Параметры “по имени” (ByName)
Предупреждение
Возможно, в будущем этот механизм будет отключен.
Передача “по имени”, трейт ByName
При обычном исполнении все аргументы метода вычисляются перед вызовом метода. Но иногда нужно какой-то аргумент метода не выполнять при передаче, а обернуть в некоторую сущность (т.н. thunk), которую выполнить только в нужный момент уже внутри метода. Можно назвать это передачей “по имени”.
Обычный вызов:
def work(a: Int, b: Int) = {
println("work")
println(a + b)
}
def main = {
work(123.println($), 777.println($))
}
Выдаёт:
123
777
work
900
Здесь 123 и 777 выводятся в консоль до work. Это передача значения “по ссылке” или “по значению” (для неизменяемых объектов принципиальной разницы нет).
Если определить тип параметра как Func/FuncX, то аргументом можно передавать анонимную функцию, созданную при помощи #-выражения (опционально без func, если определение по месту вызова). Это является аналогом передачи “по имени”, но несколько громоздко.
def work(a: Func[Int], b: Func[Int]) = {
println("work")
println(a.! + b.!)
}
def main = {
work(#{123.println($)}, #{777.println($)})
}
Выдаёт:
work
123
777
900
Здесь же 123 и 777 выводятся в консоль после work.
Чтобы упростить вызов, можно вместо Func в параметрах метода использовать ByName (тоже из пакета libretto):
def work(a: ByName[Int], b: ByName[Int]) = {
println("work")
println(a.! + b.!)
}
def main = {
work(123.println($), 777.println($))
}
Выдаёт:
work
123
777
900
Это полный аналог предыдущего примера, но вызов work проще, поскольку не нужно использовать # для оборачивания аргументов. Компилятор делает это сам, если видит использование ByName в параметре метода.
ByName[Int] можно использовать только в параметре метода, тогда это аналог Func[Int] + маркер для компилятора, что аргумент нужно автоматически обернуть в анонимную функцию.
На уровне метода ByName превращается в обычный Func/Func0, который и нужно дальше использовать (если есть необходимость явно указать тип):
def work(a: ByName[Int]) = {
println(type(a))
fix f: Func[Int] = a // если нужно явно указать тип
println(a.!) // одно и то же
println(f.!) // одно и то же
}
def main = {
work(123) // 123 автоматические оборачивается #{ 123 }
}
Выдаёт:
libretto/Func0[libretto/Int]
123
123
Для дальнейшей передачи нужно использовать Func/Func0 (чтобы избежать дополнительного оборачивания)
def workFunc(f: Func[Int]) = {
println(f.!)
}
def work(a: ByName[Int]) = {
workFunc(a)
}
def main = {
work(123)
}
Выдаёт:
123
Более сложный случай, если передаваемый аргумент не просто возвращает значение, а его нужно выполнить в каком-то контексте. Для этого используется ByName с двумя параметрическими типами. Первый обозначает тип контекста для аргумента (будет подставлен автоматически), второй - это (как всегда) тип результата:
def workFunc(f: Func[Int, String]) = {
println(f.!(123))
}
def workByName(f: ByName[Int, String]) = {
println(f.!(123))
}
def main = {
workFunc(#(v: Int){v.{ "=" + ($ + 777).string }})
workByName("=" + ($ + 777).string)
}
Выдаёт
=900
=900
Вызов workByName компилятором превращается в вызов, аналогичный workFunc. Т.е. аргумент оборачивается в Func/Func1, а единственный параметр автоматически подаётся как $-контекст аргументу.
Соответственно, ByName[Int, String] виден как Func[Int, String] в методе. И то, что будет контекстом для аргумента, передаётся явным единственным параметром. Как и в случае более простого ByName (с одним параметрическим типом), этот ByName (с двумя параметрическими типами) является только маркером компилятору, который должен автоматически обернуть аргумент в анонимную функцию. Дальше нужно работать только с типами Func/Func1.
ByName удобен для создания методов, работа с которыми выглядит как управляющая конструкция языка.
def from0to9(body: ByName[Int, Int]): Int = {
var sum = 0
0..9 as v.${
sum = sum + body.!(v)
}
sum
}
def main = {
println: from0to9: {
$ * 100 // $ типа Int
}
}
Выдаёт:
4500
Серьёзные недостатки ByName
Хотя ByName довольно мощный, но есть существенные недостатки:
-
Сложное понимание, особенно связь между
ByNameиFunc/Func0/Func1. -
Неоднородная “хакерская” хитрость, когда параметр в месте вызова анонимной функции автоматически (т.е. скрыто) превращается в контекст в месте определения.
-
При изучении кода в месте вызова метода невозможно понять, где вызов “по имени”, без просмотра сигнатуры метода.
-
Эмуляция управляющих конструкций усложняет понимание: базовые конструкции известны, а для пользовательских придётся изучать документацию.
-
Усложнение компиляции.
Поэтому не рекомендуется активное использование ByName. Пока решения нет, но, возможно, в будущем ByName и вовсе будет убран из языка.
Альтернативой является использование обычных Func/Func0/Func1 с определением аргумента при помощи #-выражения без использования func и без указания типа (по желанию).
Например:
def nTimes(n: Int, body: Func[Any*]) = {
1..n.${
body.!
}
}
def main = {
nTimes(3): #{
println("Hello")
}
}
Выдаёт:
Hello
Hello
Hello
При просмотре исходного кода сразу понятно, что второй аргумент метода nTimes вызывается лениво (“по имени”), даже без просмотра сигнатуры самого метода.
Аналогично с передачей параметра.
trait Helper {
def `!=`(key: Int, value: String): ()
}
def init(body: Func[Helper, ()]): Map[Int, String] = {
fix m = map[Int, String]()
fix helper = new Helper {
def `!=`(key, value) = m.!(key) = value
}
body.!(helper)
m
}
def main = {
fix m = init: #(helper) {
helper.!(1) = "one"
helper.!(2) = "two"
}
println(m.keys)
}
Выдаёт:
(1,2)
Такой вариант пусть и слегка более громоздкий, но более понятный: сразу виден ленивый (“по имени”) код, есть именование параметра, которое более понятно, чем использование $. При желании можно даже протипизировать этот параметр (helper: Helper).
Устаревшие типы (LambdaN, Block), # в параметре
Устаревшие LambdaN, Block, # в параметре
Исторически в Libretto есть псевдоструктуры в пакете libretto для представления т.н. “лямбд” и “блоков”: Block (синоним Lambda0), Lambda0, Lambda1, Lambda2 и т.д.
Их особенность в том, что они используют наименее специфичный тип Any* для параметров и результата (но с возможным скрытым динамическим преобразованием), имеют возможность скрытой передачи контекста. А для вызова используют устаревшая конструкция ! без точки (с возможностью передачи некоторых аргументов без скобок).
Первоначально #-выражение использовалось только для определения Block/LambdaN
Эти структуры (типы) крайне не рекомендуется использовать в новом коде, поскольку они будут убираться в будущих версиях Libretto Из-за того, что скрытая передача контекста, вызов ! без точки и скрытое динамическое преобразования реализованы достаточно неинтуитивно и сложно.
Взамен можно использовать Func с типом Any*. Например, Func[Any*, Any*] вместо Lambda1, если не используется контекст.
А если нужен контекст, то передавать в явном виде через параметр, т.е. Func[Any, Any*, Any*] вместо Lambda1.
// устаревший вариант
def testOld(v: Lambda1) = {
println(123.v!777)
}
// актуальный вариант
def testNew(v: Func[Any, Any*, Any*]) = {
println(v.!(123, 777))
}
def main = {
testOld: #(v: Any*){ $.string+", "+v }
testNew: func: #(ctx: Any, v: Any*) { ctx.string+", "+v } // func можно убрать
}
Выдаёт
123, 777
123, 777
Если же нужно уточнение типов параметров, то использовать соответствующе типизированный Func/FuncN:
// устаревший вариант
def testOld(v: Lambda1) = {
println(v!123)
}
// актуальный вариант
def testNew(v: Func[Int, Any*]) = {
println(v.!(123))
}
def main = {
testOld: #(v: Int){ v + 777 }
testNew: func: #(v: Int){ v + 777 } // func можно убрать
}
Выдаёт:
900
900
Ещё в определении параметров методов можно использовать #-конструкцию. Это тоже устаревшая конструкция для передачи “по имени”, современным аналогом которой является ByName.
Вместо test(#v) можно использовать test(v: ByName[Any, Any*]), но вызывать переданную анонимную функцию другим способом:
// устаревший вариант
def testOld(#v) = {
fix a = 123.v! // устаревший вызов
println(a)
}
// актуальный вариант
def testNew(v: ByName[Any, Any*]) = {
fix a = v.!(123) // актуальный вызов
println(a)
}
def main = {
testOld({println($); "abc"})
testNew({println($); "abc"})
}
Выдаёт:
123
abc
123
abc
Вместо test(#: Int v) можно использовать test(v: ByName[Int, Any*]), но вызывать переданную анонимную функцию другим способом:
// устаревший вариант
def testOld(#: Int v) = {
fix a = 123.v! // устаревший вызов
println(a)
println(type(a))
}
// актуальный вариант
def testNew(v: ByName[Int, Any*]) = {
fix a = v.!(123) // актуальный вызов
println(a)
println(type(a))
}
def main = {
testOld({$ + 1})
testNew({$ + 1})
}
Выдаёт:
124
libretto/Any*
124
libretto/Any*
Но лучше вместо # в параметрах переходить на прямое использование Func/FuncN, поскольку ByName не является простой для понимания и использованию вещью.
// устаревший вариант
def testOld(#: Int v) = {
fix a = 123.v! // устаревший вызов
println(a)
println(type(a))
}
// актуальный вариант
def testNew(v: Func[Int, Any*]) = {
fix a = v.!(123) // актуальный вызов
println(a)
println(type(a))
}
def main = {
testOld({$ + 1})
testNew(#(v){v + 1})
}
Выдаёт:
124
libretto/Any*
124
libretto/Any*
Структура библиотеки
Имя библиотеки и пакеты Libretto
Все пакеты исходных текстов на языке Libretto библиотеки должны быть прямыми пакетами или подпакетами корневого пакета библиотеки. Например, если библиотека называется com/teacode/pdf, то внутри возможны только пакеты com/teacode/pdf или подпакеты этого пакета. Это условие проверятся при подключении библиотек в Libretto-систему.
Но при распространении библиотек использование символа / затруднено из-за файловых систем или URL. Поэтому имя каталога или zip-архива, хранящего Libretto-библиотеку, составляется как имя корневого пакета, в котором символы / заменены на точки .. Например, библиотека с корневым пакетом com/teacode/pdf хранится либо в каталоге com.teacode.pdf, либо в архиве com.teacode.pdf.zip
Каталоги библиотеки
Каталоги (верхнего уровня) библиотеки:
libretto- исходные тексты Librettopublic- публичные файлыjvm- данные для JVM
Исходные тексты Libretto
Исходные тексты лежат в каталоге libretto корня библиотеки. Внутренняя структура подкаталогов и файлов ограничивается только возможностями файловой структуры, но все исходные тексты программы на Libretto должны иметь расширение .ltt. Имена каталогов и файлов могут быть никак не связаны с именами пакетов, которые используются.
Публичные файлы (для веб-приложения и publicText)
Файлы, которые будут публичными и доступными статически через веб-приложение должны располагаться в каталоге public корня библиотеки. Внутренняя структура подкаталогов и файлов ограничивается только возможностями файловой структуры.
Доступ к публичным файлам библиотеки во время исполнения возможен двумя способами:
-
Через URL веб-приложения, который формируется следующим образом:
/public/корневой пакет библиотеки/путь до файла/файл Например, если в библиотеке с корневым пакетомcom/teacode/abcв каталогеpublicесть каталогjs, в котором лежит файлtest.js, то при использовании этой библиотеки в веб-приложении этот файл будет доступен по адресу/public/com/teacode/abc/js/test.js -
Программным способом при помощи метода
publicTextбиблиотекиlibretto/lib. Этот метод принимает два строковых литерала, первых из которых указывает имя библиотеки (корневой пакет библиотеки), второй - имя файла относительноpublicв этой библиотеке (используется/в качестве разделителя каталогов вне зависимости от ОС). Существование файла проверяется в момент компиляции. Метод возвращает текстовое представление файла в кодировке UTF-8. Например, если в библиотеке с корневым пакетомcom/teacode/abcв каталогеpublicесть каталогjs, в котором лежит файлtest.js, то этот файл в текстовом виде доступен через вызовlib/publicText("com/teacode/abc", "js/test.js")
В любом случае для использования публичных файлов соответствующая библиотека должна быть подключена при помощи use в любом месте программы или веб-приложения.
jar-файлы (для JVM)
Возможно подключение jar-файлов, которые будут использоваться при запуске через JVM. jar-файлы складываются к подкаталог jar подкаталога jvm корневого каталога библиотеки (т. е. в подкаталог jvm/jar).
Имя jar-файла значения не имеет. При использовании библиотеки в приложении автоматически подключаются все jar-файлы из указанного каталога.
Libretto не делает никаких попыток разрешить конфликты jar, не занимается их версионностью. Все jar-файлы всех используемых Libretto-библиотек подключаются одновременно. Если будут дубли или разные версии, то поведение не определено (зависит от используемого JRE).
Поскольку jar-файлы берутся скопом из всех библиотек, то возможны их дубли. Поэтому желательны внутренние правила использования jar-файлов. Какие-то специфичные или обособленные Libretto-библиотеки могут сами по себе содержать jar-файлы. Соответственно, если есть желание использовать jar-файлы в своей библиотеке, то лучше подключить уже существующие, где уже есть необходимые jar-файлы. Но если jar-файлы достаточно универсальные, то лучше сделать отдельные Libretto-библиотеки, которые будут содержать только jar-файлы (и заглушку на Libretto). Тогда библиотеки смогут подключать их для использования.
JVM
Версия Java
Уровень генерируемого байткода Java 5, но сам компилятор и библиотеки требуют минимального уровня Java 8. Для работы достаточно стандартных моделей (JRE), полный JDK не нужен.
Производительность (JVM)
Байт-код JVM
Генерируемый байт-код Libretto в целом соответствует уровню байт-кода типичной компилируемой Java-программы. Но есть особенности как Libretto, так и его компилятора текущей версии, которые ухудшают производительность в сравнении с ручным написанием Java-программы.
Часть этих особенностей возможно исправить по мере развития компилятора Libretto. Другая часть определяется семантикой Libretto.
Пустые () значения
Если тип имеет кардинальность ? или является (), то для представления пустоты используется null, что эффективно. Если тип имеет кардинальность *, то для пустоты используется специальный объект в единственном экземпляре, представляющий пустую последовательность. Это позволяет использовать единообразный код (не обрабатывать null отдельно).
Такая особенность может как положительно, так и отрицательно влиять на производительность. Но не планируется изменять такое отображение типов.
unit значение
Для unit значения используется единственный экземпляр специального объекта, что достаточно эффективно, но не так эффективно как работа с () типом и его значением (). Поэтому для методов, которые должны хоть что-то вернуть (аналог void), предпочтительно использовать (), а не unit - это и является рекомендаций для Libretto.
unit же используется чаще с типом Unit? как заменитель true.
Int/Real типы чисел
Обычные типы чисел Libretto (Int для целых и Real для дробных) всегда используют представление с большой разрядностью: java.math.BigInteger для Int и java.math.BigDecimal для Real. Поэтому вычисления Libretto будут более медленные, чем вычисления Java на примитивных типах (int, float и т.д.). Кроме того, будет больше расход памяти (эти объекты всегда создаются в heap и имеют больший размер).
В качестве альтернативы можно использовать типы чисел из пакета libretto/num. Они имеют фиксированную разрядность, поэтому имеют меньший размер и скорость вычисления ценой потери точности. Как бонус в Libretto возможно использование беззнаковых типов, которых изначально нет в Java.
В текущей реализации компилятора Libretto числа фиксированной разрядности всегда используются как box-объекты (java.lang.Integer, java.lang.Float и т.д.). Даже при использовании типа без указания кардинальности, поэтому создаются в heap, что приводит к лишним расходам памяти и потери скорости. Это не ограничение языка, а упрощение компилятора, что может быть исправлено в будущих версиях без изменения поведения.
Строки (String) и символы
В Libretto строки позволяют использовать символы с кодом больше 0xFFFF, которые на уровне Java представлены двумя char символами (кодирование UTF-16), но которые на уровне Libretto выглядят именно как один символ.
В настоящее время на уровне Libretto отсутствует тип для отдельного символа. Для представления символа используется тоже строка, но из одного символа (как результат разбиения строки на символы) или числовой код символа (как результат разбиения строки на коды). Это, конечно, понижает производительность работы со строками по сравнению с чистой Java, если считается, что символ всегда содержится только в одном char (что может ошибочно на некоторых данных).
Другая вытекающая из этого особенность Libretto - это необходимость более медленной работы при посимвольном проходе по строке, поскольку нужно определять, сколько char занимает тот или иной символ. В текущей версии Libretto оптимизируется проход по строке путём использования двух видов строк: простые строки, где символы только из одного char и остальные (сложные, UTF-16) строки. Но некоторая потеря производительности всё равно есть даже при работе только с простыми строками.
Возможно, что в будущем появится тип для символьного представления, но с сохранением возможности использования всего диапазона кодов Unicode.
Литералы
Поскольку литералы Libretto типов String, Int, Real представлены классами Java, то используется оптимизация повторного использования. Для кэширования объекты заданы как значения статических полей нескольких служебных классов, создаваемых в результате компиляции. Поэтому повторное использование литералов в Libretto достаточно эффективное, без лишнего повторного создания. Но первоначальная загрузка может быть немного дольше из-за необходимости создать сразу несколько значений литералов.
Очень длинные литералы (ограничение 64КБ в UTF8) в Libretto при инициализации упомянутых служебных классов собираются из небольших (допустимых) кусков строк.
Структуры
Для структуры без полей используется один и тот же экземпляр. Экземпляры структур с полями повторно не используются, создаются заново. Но в будущем возможна оптимизация, когда уже существующие экземпляры структур с полями могут быть использованы повторно (с сохранением поведения сравнения, в том числе с учётом equalityKey).
Объединение
Из-за ограничений системы типов Java нельзя напрямую отобразить объединение типов Libretto. Взамен используется работа с Object в случае объединения с дальнейшим кастингом к нужному типу после проверок. Это, конечно, менее эффективно, чем использовать иерархию классов или интерфейсов Java, но даёт гораздо больше возможностей.
var переменные
Переменные, определяемые при помощи var, всегда используют вспомогательный объект-контейнер. Это упрощает реализацию замыкания (closure) и передачи ссылок-буферов (todo уточнить), поскольку ссылка на этот объект-контейнер является неизменяемой.
Но для типов без кардинальности или с кардинальностью ? такое представление не является эффективным в сравнении с хранением переменных на стеке (что возможно в Java для примитивных типов или null). Поэтому вычисления в Libretto в таком случае даже при использовании типов из libretto/num более медленные, чем в Java при использовании примитивных типов, больше и расход памяти. Возможно, такое поведение будет исправлено в будущих версиях компилятора.
Для var с кардинальностью +/* используется буфер copy-on-write на базе растущего массива (подобно java.util.ArrayList создаётся новый увеличенный массив, когда предыдущий заполнен). Поэтому эффективными являются:
-
Присваивание или первоначальная инициализация без дальнейшего изменения.
var temp = seq // здесь не будет копирования test(temp) // здесь тожеvar temp = seq1 temp = seq2 // здесь не будет копирования test(temp) // здесь тожеХотя в таком случае лучше использовать
fix. -
Отсутствие изменений после получения значения.
var temp: Int* = () temp += 1 ... test(temp) // здесь не будет копирования temp += 100 // всегда медленная операция копирования
fix “переменные”
fix всегда использует значение по ссылке из heap. В будущем, возможно, будет реализация использования численных констант примитивных Java типов (для типов Libretto без кардинальности).
Последовательности
Размер любой последовательности в настоящее время представлен типом int на уровне JVM, т.е. это 32-битное знаковое значение. Что разрешает максимум 2 147 483 647 элементов в последовательности. Ограничение реализации, а не самого языка. В языке для индексов и размера используется Int большой размерности.
Построение последовательностей
Поскольку последовательности неизменяемые, то при сборе новой последовательности при помощи скобок (..., ..., ...) производится копирование. И это может быть медленно:
def main = {
var seq: Int* = ()
1..10 as i .${
seq = (seq, i) // медленно
}
println(seq)
}
В таких случаях лучше использовать добавление в переменную при помощи +=:
def main = {
var seq: Int* = ()
1..10 as i .${
seq += i
}
println(seq)
}
Добавление же в начало при помощи .= гораздо более медленное, чем +=. Это надо учитывать. И добавление сразу последовательности v .= (a, b, c) обычно более эффективное, чем поэлементное добавление v .= c; v .= b; v .= a.
Последовательности диапазона ..
Последовательности диапазона, заданного при помощи .. гораздо эффективнее и экономнее обычных последовательностей, поскольку они хранят только начало и конец диапазона, остальные значения расчётные.
Есть динамическая (во время исполнения) проверка на размер .. последовательности. Если длина не помещается в 31 бит (32 бита int минус знаковый бит), то возникает исключение во время исполнения. Но это не мешает делать .. с большими числами, но с длиной, которая помещается в 31 бит.
Если же начало и конец диапазона укладываются в знаковое 32-битное знаковое значение, то используется ещё более эффективное внутреннее представление (int в Java).
Но это касается только диапазона, заданного в Libretto типом Int. Поддержки для других целочисленных типов из libretto/num нет, но в будущем может появиться.
Доступ по индексу
Все встроенные последовательности обеспечивают эффективный доступ по индексу. Исключением могут быть только последовательности, что определяются программно (todo название трейта).
Передача последовательностей
Последовательности неизменяемые, поэтому при вызове методов не копируются, передаются как есть. Если нужно передавать изменяемую последовательность, то нужно использовать буфер (todo уточнить), что позволяет улучшить производительность за счёт отсутствия лишнего построения новых последовательностей.
Сравнение
Для некоторых типов всегда используется сложное сравнение, которое вместо сравнения на равенство определяет взаимный порядок, что менее эффективно. Но это (историческая) особенность компилятора. В будущем возможно внутреннее разделение на сравнение простое (равно или не равно) и сравнение порядка (больше, меньше или несравнимо). (todo уже сделано?)
Кроме того, исторически и ошибочно сделано автопробразование Real/Int и R64/I64 при сравнении. Это медленно и не соответствует семантике Map.
def main = {
println: 1 == 1.0 // 1 равно 1.0 (ошибочное решение!)
fix m = map[Number, String]()
m.!(1) = "one"
println: m.!(1)
println: m.!(1.0) // а для Map 1 не равно 1.0
}
Выдаёт:
unit
one
()
В будущем, возможно, это автопреобразование будет убрано.
Константы
В Libretto нет глобальных переменных или констант. Некоторые простые методы компилятор Libretto автоматически оптимизирует, кэшируя результат их исполнения:
def value = (1, 2, 1 + 2) // результат будет кэширован
Но гарантировать такое кэширование в текущей версии Libretto нельзя.
В некоторых случаях можно избавится от лишних вычислений использованием локальной функции вместо глобального метода и доступом к константам (при помощи замыкания):
def someValue = ... // медленно
def work(v: Int) = ... // здесь работаем с someValue
def main = {
...
expr.${ work($) }
}
преобразовать в
def someValue = ... // медленно
def main = {
...
fix sv = someValue // вычисляем и запоминаем
def work(v: Int) = ... // здесь работаем с sv вместо someValue
...
expr.${ work($) }
}
В более сложных случаях можно использовать lazy (todo).
Вызов метода (динамика)
При выборе метода по контексту компилятор выбирает претендентов статически. Но окончательный выбор (в случае альтернативы) производится динамически во время исполнения:
def Any test = "any"
def Int test = "int"
def String test = "string"
def main = {
fix values = ("abc", 123, unit)
values.test // здесь динамический выбор метода из трёх
}
При выборе метода не используются возможности JVM вызова виртуального метода (обычный способ для Java в аналогичной ситуации). Взамен последовательно проверяется тип (аналог instanceof в Java) контекста до нахождения подходящего метода. Корректность проверена статически, но производительность такого метода хуже, чем при вызове виртуального метода.
Поскольку в планах развития Libretto есть поддержка [полу]динамического выбора не только по контексту, но и по аргументам, то переход на вызов виртуального метода станет точно невозможен.
Method too large
Из-за лучше выразительности код на Libretto не такой громоздкий, как аналогичный код на Java. Но это может приводить к тому, что визуально небольшой код может приводить к ошибке Method too large на уровне Java окружения.
Чаще всего это вызвано большим объёмом проверок типа контекста при динамическом выборе метода (см. выше). Особенно при использовании больших объединений.
Пока никаких средств борьбы с этим на уровне компилятора нет. Только ручным изменением кода метода. Например, вынесением частей в локальные или глобальные методы.
Перебор элементов последовательности
Иногда возникает задача перебрать значения элементов последовательности, но без накопления результата. Для этого можно использовать код вида:
(expr).${ body } // body работает с $
body будет выполнено в контексте каждого элемента, но не будет лишнего накопления результата. Результатом выполнения выражения будет сама первоначальная последовательность.
Опционально можно использовать as:
(expr) as n.${ body } // body работает с n
Компилятор распознает и оптимизирует такой код, считая его неким аналогом for (которого в Libretto в явном виде нет).
Возвращаемое значение
В некоторых случаях последним выражением метода является вышепоказанный перебор элементов последовательности без накопления результата:
def work(...) = {
...
(expr).${ println($) }
}
Хотя здесь нет лишнего накопления результата, но последовательность будет возвращена из метода (с выводом соответствующего типа результата метода). Если в этом нет необходимости, то эффективнее явно указать пустоту как результат:
def work(...): () = {
...
(expr).${ println($) }
()
}
Трейты, экземпляры, преобразование в трейт
Трейты Libretto в JVM представлены как интерфейсы (interface).
Экземпляры трейтов (создаваемые при помощи new) - это JVM-классы, которые реализуют соответствующий интерфейс. В точке new создаётся JVM-объект - экземпляр класса. Если этот класс является замыканием, связанные переменные передаются в конструкторе, а затем являются полями класса.
Преобразование (автоматическое или ручное) в трейт всегда происходит при помощи оборачивания экземпляра структуры или трейта в JVM-класс, который реализует интерфейс, соответствующий целевому трейту.
Иерархии трейтов как иерархии JVM-интерфейсов нет. Условная иерархия трейтов создаётся только за счёт оборачивания в новый класс. Это может быть менее эффективно, но это семантика трейтов Libretto.
Анонимные функции
Анонимные функции являются частным случаем трейтов, поэтому полностью реализуются через механизм трейтов.
Конкретному трейту Func/FuncN соответствует JVM-интерфейс.
Экземпляр анонимной функции создаётся как экземпляр JVM-класса, реализующего соответствующий JVM-интерфейс. Если этот класс является замыканием, связанные переменные передаются в конструкторе, а затем являются полями класса.
Локальные функции
Локальные (вложенные) функции всегда отображаются в классы Java использованием переменных замыкания в качестве значений полей вместо определения статических методов и передачей переменных замыкания дополнительными параметрами. Даже при отсутствии переменных замыкания - результатом будет класс без полей (todo уточнить). Т.е.технически локальные функции - это анонимные функции с другим синтаксисом. Это может немного ухудшать эффективность работы локальной функции по сравнению с глобальным методом.
Кроме того, это означает, что каждый раз в месте определения (def) локальной функции вызывается создание объекта соответствующего класса. Поэтому определения локальных функций нужно выносить из нагруженных циклов, если есть возможность:
Было:
seq.${
...
def work(...) = ... // каждый раз создаётся объект локальной функции
work(...)
...
}
Стало:
def work(...) = ... // создание вне цикла
seq.${
...
work(...)
...
}
В будущем возможна доработка компилятора.
breaking/break
Для реализации break всегда используется исключение с ловлей его на уровне breaking. Для лучшей производительности исключение не строит стек-трейс.
Хвостовая рекурсия
В настоящее время компилятор Libretto никак не оптимизирует хвостовую рекурсию (в отличие, например, от Scala). В случае необходимости можно вручную использовать механизм Trampoline (todo пример).
Взаимодействие с JVM
Взаимодействие с JVM делается в Libretto при помощи методов, которые принимают строковые литералы. (todo см. раздел) Это выглядит как работа с рефлексией, но компилятор уже на стадии компиляции делает все необходимые проверки и связь кода. Эффективность таких вызова эквивалентна обычным вызовам из Java, а ошибки (отсутствие подходящих сущностей) выдаются на стадии компиляции.
Ленивая загрузка классов
Для улучшения отзывчивости компилятор Libretto не генерирует сразу весь байт-код программы. Наоборот, байт-код генерируется из внутреннего представления Libretto только при запросе (загрузке) связанного класса.
Недостатком такого подхода является возможное проявление ошибки Method too large не во время компиляции, а во время выполнения программы Libretto.
Для проверки можно использовать прагму (todo), которая заставляет принудительно сгенерировать весь байт-код всех классов, что получились после компиляции Libretto.
Взаимодействие с JVM и Java-средой (libretto/jvm)
Libretto
Язык программирования Libretto не является языком для Java-среды. У него совсем иная система типов, которая напрямую в JVM не отображается. Поэтому Java-окружение рассматривается лишь как удобная платформа для запуска программ Libretto.
Но взаимодействие с JVM и Java-средой (библиотеками и т.п.) необходимо, поэтому была реализована библиотека libretto/jvm, которая это обеспечивает, пусть и несколько громоздким способом (но это не влияет на эффективность).
libretto/jvm
Для взаимодействия с JVM используются трейты специального вида и методы библиотеки libretto/jvm. При компиляции работа с трейтами и методами превращается в байт-код, напрямую работающими с классами/интерфейами JVM. Есть статическая проверка взаимодействия на стадии компиляции.
JVM-трейты
Для связи с JVM классами, интерфейсами и массивами используются трейты, но специального вида. JVM-трейт определяется в любом пакете, имя трейта значения не имеет, но у него не должно быть методов (должно быть пустое тело).
trait Test
Далее нужно задать JVM-тип, в который будет отображаться этот трейт. Для этого в том же пакете определяется метод jvmType с этим трейтом в контексте. Телом метода должен быть только строковый литерал, ничего другого не допускается (это проверяется во время компиляции). Строковый литерал содержит имя JVM-типа, который будет использован на уровне байт-кода при обращении к указанному JVM-трейту на уровне программы на Libretto.
В качестве разделителей пакетов в JVM-типах используется точка (не / как в Libretto).
def Test jvmType = "java.util.List"
В этом примере трейт Test будет соответствовать JVM-типу java.util.List.
Примитивные типы (byte, double и т.д.) использовать нельзя, допускаются только ссылочные типы.
Но допускается использование массивов, для этого суффиксом должны быть символы []:
def Test jvmType = "java.util.List[]"
В данном примере будет массив из java.util.List. А вот массивы могут содержать примитивные типы. Кроме того, допускаются многомерные массивы:
def Test jvmType = "int[][]"
Правильность указания JVM-типа проверяется на стадии компиляции.
Трейты с разными именами на уровне Libretto могут быть привязаны к одному и тому же JVM-типу. Но на уровне Libretto это всё равно будут разные трейты, которые статически несовместимы между собой.
trait Test
def Test jvmType = "int[]"
trait Test2
def Test2 jvmType = "int[]"
def Test conv: Test2 = this // ошибка компиляции
Этот пример не скомпилируется, поскольку Test нельзя обычными средствами Libretto статически преобразовать в Test2. В такой ситуации нужно использовать специальные методы из libretto/jvm для проверки и преобразования типов.
Поскольку JVM-трейты всегда пустые по определению, то можно было бы подать (обернуть в трейт) любой объект, что сломало бы корректность программы. Поэтому оборачивание в такой JVM-трейт запрещено на стадии компиляции:
trait Test
def Test jvmType = "int[]"
def main = {
Test(123) // ошибка компиляции
}
Этот пример не скомпилируется.
Запрещено и прямое создание экземпляра JVM-трейта:
trait Test
def Test jvmType = "int[]"
def main = {
new Test {} // ошибка компиляции
}
Этот пример тоже не скомпилируется.
Предопределенные типы
Есть Libretto структуры, для которых уже задано автоматическое преобразование в JVM-тип и обратно:
libretto/Int⇔java.math.BigIntegerlibretto/Real⇔java.math.BigDecimallibretto/jvm/RawString⇔java.lang.Stringlibretto/num/R64⇔java.lang.Doublelibretto/num/R32⇔java.lang.Floatlibretto/num/I64⇔java.lang.Longlibretto/num/I32⇔java.lang.Integerlibretto/num/I16⇔java.lang.Shortlibretto/num/I8⇔java.lang.Bytelibretto/num/Char16⇔java.lang.Characterlibretto/num/Boolean⇔java.lang.Boolean
Кроме того, поддерживается libretto/Any как эквивалент java.lang.Object.
Остальные JVM-типы (классы, интерфейсы и массивы) должны использоваться через JVM-трейт с соответствующим jvmType (см. выше).
Кардинальность
При работе с JVM-трейтами без кардинальности предполагается, что соответствующие JVM-типы не получают null-значение, иначе возможно исключение NullPointerException во время работы программы. Для корректной работы с null значением необходимо использовать кардинальность ?, тогда пустота значения Libretto (JVM-трейта) эквивалентна null на уровне JVM (и наоборот).
Другие кардинальности (+, *) при работе с JVM не поддерживаются напрямую (см. соответствующий раздел).
Строки и JVM
Для представления строк Libretto на уровне JVM не используется класс java.lang.String напрямую. Для того, чтобы работать со Libretto-строками в JVM мире, определена структура libretto/jvm/RawString, именно она и соответствует java.lang.String.
Для перехода от libretto/String в libretto/jvm/RawString и обратно в пакете libretto/jvm определены методы:
def RawString string: String
def String raw: RawString
Пример:
use libretto/jvm
def main = {
fix s = "abc"
fix a: jvm/RawString = s.jvm/raw
println: s
println: a
fix b: String = a.string
println: b
}
Выдает:
abc
abc
abc
RawString предназначен в первую очередь для взаимодействия с JVM, использование для каких-то других рабочих целей не рекомендуется.
Примитивные типы
Поддерживается автоматический boxing/unboxing для примитивных типов JVM. Но для этого соответствующие типы на уровне Libretto должны использоваться без кардинальности (пустота null не допускается):
libretto/num/R64⇔doublelibretto/num/R32⇔floatlibretto/num/I64⇔longlibretto/num/I32⇔intlibretto/num/I16⇔shortlibretto/num/I8⇔bytelibretto/num/Char16⇔charlibretto/num/Boolean⇔boolean
Автоматически boxing/unboxing не работает на уровне массивов, они должны быть определены как есть: int[], java.lang.Integer[] и т.д.
Последовательность и Many
Нельзя напрямую работать с кардинальностями */+, но можно получить последовательность как единое значение. Тип этого значения в Libretto представлен трейтом libretto/jvm/Many
При помощи методов libretto/jvm/Many и libretto/jvm/value можно делать преобразования в обе стороны:
def Many(value: Any*): Many
def Many value: Any*
Пример:
use libretto/jvm
def main = {
fix test: Int* = (1,2,3)
fix m: jvm/Many = test.*jvm/Many // последовательность как одно значение
// здесь можно работать с m на уровне JVM
fix back: Int* = m.value.*dyn[Int*] // обратное преобразование, в явный тип Int*
println: test == back
}
Выдает: unit
Many принимает и одиночные значение, но оборачивает их в последовательность, после чего возвращается полученный Many.
Параметризация методов типами
Как указано выше, одному JVM-типу может соответствовать несколько JVM-трейтов в Libretto, поэтому при вызове некоторых методов из libretto/jvm, которые возвращают результат из мира JVM, нужно знать тип - JVM-трейт, которым будет представлен результат.
Для этого вызов метода параметризуется типом через []. Общее правило: если параметров типа несколько, то тип результата всегда указывается первым:
-
jvm/newObject[Date]()-Dateкак тип результата. -
list.jvm/call[num/I32]("size")-num/I32как тип результата. -
jvm/callStatic[(), S]("gc")-()как тип результата. -
jvm/getFieldStatic[num/R64, Math]("E")-num/R64как тип результата.
Мнемоническое правило: порядок перечисления типов в [] и язык Java, в котором тип результата всегда указывается первым.
Создание объекта (newObject)
Для создания JVM-объекта используется метод libretto/jvm/newObject, который типизируется JVM-трейтом, обозначающим JVM-класс (именно класс, не интерфейс или массив). Результат вызова newObject будет указанного типа. Допускается только тип без кардинальности. При отсутствии аргументов при вызове обязательны круглые скобки .
use libretto/jvm
trait Date
def Date jvmType = "java.util.Date"
def main = {
fix d = jvm/newObject[Date]()
println: d
println: type(d)
}
Выдает (дата-время будут отличаться):
Wed Sep 02 14:36:38 SGT 2020
_default/Date!
В newObject можно дополнительно передавать аргументы конструктора.
use libretto/jvm
trait File
def File jvmType = "java.io.File"
def main = {
fix a = jvm/newObject[File]("a".jvm/raw)
fix f = jvm/newObject[File](a, "b".jvm/raw)
println: f
}
Выдает (под Windows):
a\b
Как указано, работает автоматическое преобразование аргументов в примитивные типы.
use libretto/jvm
use libretto/num
trait UUID
def UUID jvmType = "java.util.UUID"
def main = {
fix m = jvm/newObject[UUID](1.num/i64, 2.num/i64)
println: m
}
Выдает:
00000000-0000-0001-0000-000000000002
Вызовы методов (call, callStatic)
Для вызова нестатических методов интерфейсов и классов используется метод libretto/jvm/call. Вызов метода call параметризуется типом результата. Объект, на котором вызывается метод, берётся из контекста ($). Первый аргумент вызова (обязательный) - это имя метода, должен быть представлен строковым литералом. Далее могут быть опциональные аргументы для вызываемого JVM-метода.
use libretto/jvm
use libretto/num
trait List
def List jvmType = "java.util.ArrayList"
def main = {
fix list = jvm/newObject[List]()
println: list.jvm/call[num/I32]("size")
println: list.jvm/call[num/Boolean]("add", "abc".jvm/raw)
println: list.jvm/call[num/I32]("size")
}
Выдает:
0
true
1
Если метод не возвращает результат (void в JVM), то вместо типа результата указывается тип пустоты () . Его же можно указывать в том случае, если результат возвращается, но не нужен при вызове.
use libretto/jvm
use libretto/num
trait List
def List jvmType = "java.util.ArrayList"
def main = {
fix list = jvm/newObject[List]()
println: list.jvm/call[num/I32]("size")
list.jvm/call[()]("add", "def".jvm/raw)
list.jvm/call[()]("add", 0.num/i32, "abc".jvm/raw)
println: list.jvm/call[num/I32]("size")
println: list
}
Выдает:
0
2
[abc, def]
Для вызова статического метода используется libretto/jvm/callStatic, он параметризуется двумя типами: тип результата и тип класса/интерфейса, у которого будет вызван статический метод. Первый аргумент - это имя метода (должен быть строковым литералом). Остальные аргументы передаются в статический метод.
use libretto/jvm
trait S
def S jvmType = "java.lang.System"
def main = {
jvm/callStatic[(), S]("gc")
}
Вызывает GC.
Пример передачи параметра:
use libretto/jvm
use libretto/num
trait Math
def Math jvmType = "java.lang.Math"
def main = {
fix r = jvm/callStatic[num/I32, Math]("abs", (-123).num/i32)
println: r
}
Выдает: 123
Алгоритм выбора методов/конструкторов
Алгоритм выбора методов/конструкторов при конфликте типов параметров совпадает с таковым в Libretto. Но одна особенность: примитивные типы считаются более специфичными, чем обёрточный класс или Object.
Например, аргументом передаётся значение типа libretto/num/I32, а под него попали методы, принимающие:
-
java.lang.Object -
java.lang.Integer -
int
Алгоритм посчитает наиболее специфичным метод, принимающий int, хотя реально (внутри скомпилированного JVM-кода) значение представлено при помощи java.lang.Integer.
Если же требуется выбрать метод именно с java.lang.Integer, то нужно подавать аргументом значение типа libretto/num/I32?, тогда параметр int не попадёт в выбранные, а из java.lang.Object и java.lang.Integer будет использоваться java.lang.Integer как наиболее специфичный.
При равенстве типов параметров выбор идет по типу результата: выбирается более специфичный.
Если у методов одинаковые типы параметров и одинаковый тип результата, то более специфичным считается тот, что определен в более специфичном классе.
Если выбрать нужный метод нельзя, то возникает ошибка компиляции.
Ограничение статических методов интерфейсов
При использовании вызова статического метода интерфейса возникает исключение: java.lang.IncompatibleClassChangeError: Method ... must be InterfaceMethodref constant. Происходит под Java 9+, под Java 8 работает нормально.
Проблема в изменениях Java. Байт-код Libretto генерируется под Java 5, тогда как InterfaceMethodref требуется начиная с Java 9, а поддерживается начиная с Java 8. Сейчас сложно переключиться на генерацию Libretto компилятором байт-кода под Java 8 поскольку нужно задавать не только данные о глубине стека, но и так называемые стэкмэпы, которые требуются с Java 6 версии.
В будущем возможно исправление этой недоработки, сейчас же нужно воздержаться от прямых вызовов таких методов: сделать “переходник” на Java.
Статические поля (getFieldStatic, setFieldStatic)
Метод libretto/jvm/getFieldStatic служит для получения значения статического поля у класса/интерфейса. Вызов метода параметризуется двумя типами: тип результата и тип класса/интерфейса, у которого будет прочитано статическое поле. Первый аргумент - это имя поля (должен быть строковым литералом).
use libretto/jvm
use libretto/num
trait Math
def Math jvmType = "java.lang.Math"
def main = {
println: jvm/getFieldStatic[num/R64, Math]("PI")
println: jvm/getFieldStatic[num/R64, Math]("E")
}
Выдает:
3.141592653589793
2.718281828459045
Метод установки значения статического поля у класса: libretto/jvm/setFieldStatic, параметризуется типом класса/интерфейса, у которого будет установлено статическое поле. Первый аргумент - это имя поля (должно быть строковым литералом). Второй аргумент - это новое значение поля.
Пример (зависит от com/teacode/pdf):
use libretto/jvm
use libretto/num
use com/teacode/pdf
trait A
def A jvmType = "com.lowagie.text.FontFactory"
def main = {
fix saved = jvm/getFieldStatic[num/Boolean, A]("defaultEmbedding")
println: saved
jvm/setFieldStatic[A]("defaultEmbedding", num/true)
println: jvm/getFieldStatic[num/Boolean, A]("defaultEmbedding")
jvm/setFieldStatic[A]("defaultEmbedding", saved)
}
Выдает:
false
true
Работа с массивами (newArray, arraySize, arraySet, arrayGet)
Создание массива методом libretto/jvm/newArray, параметризуется типом массива (не типом элемента массива), аргументом передаётся размер (libretto/num/I32)
Получение длины массива методом libretto/jvm/arraySize, массив передается контекстом, вызов без круглых скобок.
Выставление значения массива по индексу методом libretto/jvm/arraySet, первый аргумент - это индекс (libretto/num/I32), второй аргумент - значение элемента. Сам массив передается контекстом. Значение элемента должно соответствовать типу массива.
Получение значения массива по индексу методом libretto/jvm/arrayGet, параметризуется типом элемента массива, аргумент - индекс (libretto/num/I32). Массив передаётся контекстом.
use libretto/jvm
use libretto/num
trait Arr
def Arr jvmType = "int[]"
def Arr print() = {
fix size = this.jvm/arraySize
println: s"size=#{size}"
0..(size.int - 1) as i.${
fix v = this.jvm/arrayGet[num/I32](i.num/i32)
println: s" #{i}: #{v}"
}
}
def main = {
fix arr = jvm/newArray[Arr](7.num/i32)
arr.print()
arr.jvm/arraySet(1.num/i32, 123.num/i32)
arr.print()
}
Выдает:
size=7
0: 0
1: 0
2: 0
3: 0
4: 0
5: 0
6: 0
size=7
0: 0
1: 123
2: 0
3: 0
4: 0
5: 0
6: 0
Ограничения JVM-типов на уровне Libretto
Преобразование типов при JVM-взаимодействии имеет свою специфику, поскольку система типов Libretto отличается от системы типов JVM. Например, в JVM есть интерфейс java.util.List, который реализуется, например, классом java.util.ArrayList. Это означает, что вместо List можно подставлять ArrayList.
use libretto/jvm
use libretto/num
trait List
def List jvmType = "java.util.List"
trait ArrayList
def ArrayList jvmType = "java.util.ArrayList"
def test(list: List) = {
println: list.jvm/call[num/I32]("size")
}
def main = {
fix list: List = jvm/newObject[ArrayList]()
test(list)
}
Но этот пример не компилируется. Хотя ArrayList в JVM можно безопасно преобразовать в List, на на уровне Libretto трейты ArrayList и List всегда несовместимы.
В связи с этим для работы с преобразованием типов рекомендуется использовать специальные методы, а не обычные средства Libretto.
Проверка и преобразования типов (isInstanceOf, convert, forcedCast)
Вызов метода libretto/jvm/convert параметризуется JVM-типом, в который нужно преобразовать переданное аргументом значение. Преобразование на уровне JVM безопасное (от более специфичного к менее специфичному типу). Если статически нельзя преобразовать тип, то возникнет ошибка компиляции.
Вызов метода libretto/jvm/forcedCast аналогично параметризуется JVM-типом, в который нужно преобразовать переданное аргументом значение. Но преобразование динамическое при помощи кастинга (от менее специфичного типа к более специфичному), во время компиляции не проверяется. Если во время исполнения невозможно сделать это преобразование, то будет соответствующее исключение.
use libretto/jvm
use libretto/num
trait List
def List jvmType = "java.util.List"
trait ArrayList
def ArrayList jvmType = "java.util.ArrayList"
def test(list: List) = {
fix arrayList = jvm/forcedCast[ArrayList](list)
println: arrayList.jvm/call[num/I32]("size")
}
def main = {
fix arrayList = jvm/newObject[ArrayList]()
fix list: List = jvm/convert[List](arrayList)
test(list)
}
Выдает: 0
Метод isInstanceOf параметризуется JVM-типом. При вызове метода контекст проверяется на соответствие этому типу во время исполнения. Если соответствует, то результат unit. Если не соответствует - ().
use libretto/jvm
trait List1
def List1 jvmType = "java.util.ArrayList"
trait List2
def List2 jvmType = "java.util.ArrayList"
def main = {
fix list = jvm/newObject[List1]()
println: list.*{
case ~: List1 = "List1"
else = "not List1"
}
println: list.*{
case ~: List2 = "List2"
else = "not List2"
}
println: list.jvm/isInstanceOf[List1]
println: list.jvm/isInstanceOf[List2]
}
Выдает:
List1
not List2
unit
unit
Исключения (throw, catch)
Метод libretto/jvm/throw используется для выбрасывания исключения. Переданное значение должно быть экземпляром java.lang.Throwable или его подклассов - это проверяется статически при компиляции.
Пример:
use libretto/jvm
use libretto/util
trait Exception
def Exception jvmType = "java.lang.Exception"
def main = {
fix exc = jvm/newObject[Exception]("Hello!".jvm/raw)
println("Before")
jvm/throw(exc).*util/finally: {
println("After")
}
}
Выдает:
Before
After
Error (5 ms): java.lang.Exception: Hello!
at _default/main (internal editor:8)
at <jvm>
Метод libretto/jvm/catch позволяет ловить исключения. Вызов метода параметризуется классом исключения (должен быть java.lang.Throwable или его подкласс - проверяется статически), которое будет поймано в случае его возникновения. Будет ловиться исключение как напрямую указанного класса, так и его подклассов. Аргумент - это код, который будет выполнен и в котором будет ловиться исключение.
Тип результата метода catch является типом результата переданного кода, обобщённого с типом исключения.
Пример:
use libretto/jvm
trait Exception
def Exception jvmType = "java.lang.Exception"
def main = {
fix t? = unit
fix r = jvm/catch[Exception]: {
fix exc = jvm/newObject[Exception]("Hello!".jvm/raw)
if (t)
jvm/throw(exc)
else
777
}
println: r
println: type(r)
}
Выдает:
java.lang.Exception: Hello!
(_default/Exception | libretto/Int)!
Т.е. в этом примере результатом catch является либо само исключение (Exception), либо целое число - это если не было исключения.
Более сложный пример:
use libretto/jvm
trait Exception
def Exception jvmType = "java.lang.Exception"
trait RuntimeException
def RuntimeException jvmType = "java.lang.RuntimeException"
def main = {
println: (1,2,3).{
fix r = jvm/catch[Exception]: {
if ($ == 2) {
fix exc = jvm/newObject[RuntimeException]("Hello!".jvm/raw)
jvm/throw(exc)
}
$ + 100
}
r.*{
case r: Exception = r.jvm/call[jvm/RawString?]("getMessage").string
case i: Int = -i
}
}
}
Выдает: (-101,Hello!,-103)
Обратите внимание, что ловится Exception, тогда как кидается его подкласс RuntimeException. И разбор case идёт именно по Exception, поскольку трейт RuntimeException на уровне Libretto не является подклассом трейта Exception (см. подробно соответствующее описание).
Метод catch работает на более низком уровне по сравнению с try из libretto/util. В частности, catch строго отлавливает указанный класс или подкласс исключений, никакой обработки (например, игнорирование ControlThrowable или того подобного) он не делает. Обработку ControlThrowable необходимо делать самостоятельно.
Реализация классов/интерфейсов в Libretto (implement, extend)
При помощи метода libretto/jvm/implement можно превратить экземпляр трейта (этот экземпляр передается аргументом) в экземпляр JVM-интерфейса (тип интерфейса параметризует вызов).
Каждому методу из интерфейса в трейте должен соответствовать одноименный метод с таким же числом аргументов. Типы преобразуются как обычно. Если в интерфейсе результат void, то результат трейта игнорируется (но он должен соответствовать JVM-миру).
Пример:
use libretto/jvm
trait Runnable
def Runnable jvmType = "java.lang.Runnable"
trait Example {
def run(): ()
}
def main = {
fix example = new Example {
def run() = {
println("Hello!")
()
}
}
fix runnable = jvm/implement[Runnable](example)
runnable.jvm/call[()]("run")
}
Выдает: Hello!
Здесь трейт Example служит для реализации интерфейса java.lang.Runnable (в котором только метод void run()). implement возвращает реализацию интерфейса на базе экземпляра Example. Метод run вызывается уже не на трейте, а на интерфейсе.
Чуть сложнее пример. Реализуется Lambda1 (интерфейс для внутреннего представления одноместной анонимной функции при компиляции Libretto в JVM).
use libretto/jvm
trait L1
def L1 jvmType = "org.ontobox.libretto.jvm.Lambda1"
def Example(str: String) = {
new {
def apply(ctx: Any, p0: jvm/Many): jvm/Many = {
fix m = p0.value
jvm/Many: m + str
}
}
}
def main = {
fix l1 = jvm/implement[L1](Example("_"))
fix r = l1.jvm/call[jvm/Many]("apply", unit, jvm/Many: (1, "a", unit)).value
r as v.${
println(v)
}
}
Выдает:
1_
a_
unit_
Много переходов к jvm/Many и обратно. Это необходимо, поскольку на уровне JVM нет *-кардинальности (см. соответствующее описание).
В реализации интерфейса при помощи implement разрешён overloading, но на уровне интерфейса. В трейте, соответственно, всё равно будет только один метод, который должен принимать вызовы через все overloaded методы.
Есть возможность расширения класса. Аналогично implement, но метод libretto/jvm/extend. Вызов параметризуется jvm-трейтом, обозначающий класс для расширения, аргумент вызова - это трейт, который реализует абстрактные или override уже существующие методы класса.
Ограничения: класс не должен быть final, должен содержать пустой (т.е. без параметров) public или protected конструктор. Методы для реализации должны быть абстрактные. Методы для override не должны быть final или private.
Пример:
use libretto/jvm
use libretto/num
trait Number
def Number jvmType = "java.lang.Number"
def ExampleNumber(v: Real): ExampleNumber = {
new {
def intValue: num/I32 = v.int.num/i32
def longValue: num/I64 = v.int.num/i64
def floatValue: num/R32 = v.num/r32
def doubleValue: num/R64 = v.num/r64
}
}
def main = {
fix en = jvm/extend[Number](ExampleNumber(123.45))
println: en.jvm/call[num/I32]("intValue")
println: en.jvm/call[num/I64]("longValue")
println: en.jvm/call[num/R32]("floatValue")
println: en.jvm/call[num/R64]("doubleValue")
}
Выдает:
123
123
123.45
123.45
Number - это абстрактный класс, для реализации которого нужно определить четыре метода, что и делается при помощи трейта ExampleNumber.
Вложенные классы Java
В Java есть статические вложенные классы, имя которых доступно через точку. Например, библиотечный Base64.Encoder. Но это имя превращается в Base64$Encoder на уровне JVM. Поэтому для работы с подобными классами нужно использовать именно $ как разделитель, а точка . используется только как разделитель частей имени пакета. Т.е. полное имя будет java.util.Base64$Encoder.
Соответствующий пример кодирования/декодирования Base64:
use libretto/jvm
trait B64
def B64 jvmType = "java.util.Base64"
trait Encoder
def Encoder jvmType = "java.util.Base64$Encoder"
trait Decoder
def Decoder jvmType = "java.util.Base64$Decoder"
trait Bytes
def Bytes jvmType = "byte[]"
def charSet = "UTF-8".jvm/raw
def encode(str: String): String = {
fix encoder = jvm/callStatic[Encoder, B64]("getEncoder")
fix stringBytes = str.jvm/raw.jvm/call[Bytes]("getBytes", charSet)
fix encoded = encoder.jvm/call[jvm/RawString]("encodeToString", stringBytes)
encoded.string
}
def decode(encoded: String): String = {
fix decoder = jvm/callStatic[Decoder, B64]("getDecoder")
fix decodedBytes = decoder.jvm/call[Bytes]("decode", encoded.jvm/raw)
fix decoded = jvm/newObject[jvm/RawString](decodedBytes, charSet)
decoded.string
}
def main = {
fix original = <<Hello, world!>>
println: original
fix encoded = encode(original)
println: encoded
fix decoded = decode(encoded)
println: decoded
}
Выдает:
Hello, world!
SGVsbG8sIHdvcmxkIQ==
Hello, world!
Сравнение == и equals
Сравнение JVM-объектов можно делать и при помощи вызова equals (как то делается в Java). Но и == на уровне Libretto тоже работает (вызывается тот же equals).
use libretto/jvm
use libretto/num
trait UUID
def UUID jvmType = "java.util.UUID"
def main = {
fix uuid1 = jvm/newObject[UUID](1.num/i64, 2.num/i64)
fix uuid2 = jvm/callStatic[UUID, UUID]("fromString",
"00000000-0000-0001-0000-000000000002".jvm/raw)
fix uuid3 = jvm/callStatic[UUID, UUID]("fromString",
"00000000-0000-0001-0000-000000000003".jvm/raw)
println: uuid1
println: uuid2
println: uuid3
println()
println: uuid1.jvm/call[num/Boolean]("equals", uuid2)
println: uuid1 == uuid2
println()
println: uuid1.jvm/call[num/Boolean]("equals", uuid3)
println: uuid1 == uuid3
}
Выдаёт:
00000000-0000-0001-0000-000000000002
00000000-0000-0001-0000-000000000002
00000000-0000-0001-0000-000000000003
true
unit
false
()
Enum
Enum технически представлены обычным JVM-классом. Для получения экземпляра enum можно использовать одноименное статическое поле: jvm/getFieldStatic[ExEnum, ExEnum]("Name") - возвращает экземпляр Name enum ExEnum.
Пример работы с enum Thread.State.
use libretto/jvm
trait Thread
def Thread jvmType = "java.lang.Thread"
// Это enum Thread.State
trait ThreadState
def ThreadState jvmType = "java.lang.Thread$State"
def main = {
fix thread = jvm/callStatic[Thread, Thread]("currentThread")
fix state = thread.jvm/call[ThreadState]("getState")
println: state
fix runnable = jvm/getFieldStatic[ThreadState, ThreadState]("RUNNABLE")
println: state == runnable
}
Выдаёт:
RUNNABLE
unit
Поскольку это не просто enum, но и вложенный класс, то вместо Thread.State нужно использовать Thread$STate
JVM-обобщения (generics)
На уровне байт-кода обобщения (generics) теряют свою информацию о параметризации. Поэтому нужно определять JVM-тип без обобщения, затем использовать ручной динамический кастинг к нужному типу при помощи forcedCast.
Например, если в программе на Java используется java.util.List<String>, то на уровне JVM это будет общий тип java.util.List, методы которого оперируют не со String, а с Object. Для получения экземпляров String необходимо сделать ручной кастинг к String.
Аналогично с другими типами:
use libretto/jvm
use libretto/num
trait System
def System jvmType = "java.lang.System"
trait Map
def Map jvmType = "java.util.Map"
trait Set
def Set jvmType = "java.util.Set"
trait Iterator
def Iterator jvmType = "java.util.Iterator"
def main = {
// Map<String, String>
fix map = jvm/callStatic[Map, System]("getenv")
fix keys = map.jvm/call[Set]("keySet")
fix iterator = keys.jvm/call[Iterator]("iterator")
while(iterator.jvm/call[num/Boolean]("hasNext").unitValue): {
fix key = iterator.jvm/call[Any]("next") // java.lang.Object
fix stringKey = key.*jvm/forcedCast[jvm/RawString] // java.lang.String
println(stringKey)
}
}
В этом примере Map<String, String> из Java доступен в JVM как Map, т.е. типизация ключа и значения “сбрасывается” до Object. И, при необходимости, нужно делать кастинг вручную. В данном случае это преобразование ключа из java.lang.Object в java.lang.String. Хотя для вывода на экран в этом нет необходимости, но это сделано для демонстрации методики.
Вспомогательные сущности в пакете libretto/jvm/java
Сущности, которые часто используются для взаимодействия с JVM-кодом, Java-библиотеками.
Трейты в пакете libretto/jvm/java:
Throwableсоответствуетjava.lang.ThrowableListсоответствуетjava.util.ListMapсоответствуетjava.util.MapSetсоответствуетjava.util.SetIteratorсоответствуетjava.util.Iterator
Вспомогательные сущности в пакете libretto/jvm/scala
Сущности, которые часто используются для взаимодействия с кодом, написанным на языке Scala.
Трейты в пакете libretto/jvm/scala:
Optionсоответствуетscala.OptionSeqсоответствуетscala.collection.SeqIteratorсоответствуетscala.collection.IteratorFunction1соответствуетscala.Function1
Методы для работы с Option:
def Option(any: Any?): Option- созданиеOptiondef Option value: Any?- получение значения изOption
Методы для работы с Seq:
def toSeq(value: Any*): Seq- созданиеSeq
Методы для создания FunctionX:
def Function1(func: Func[Any, Any]): Function1
Подключение библиотек (jar-файлов)
Поскольку не все необходимые Java-библиотеки доступны в стандартном JRE, то есть возможность подключения сторонних jar-файлов. Для этого используются Libretto-библиотеки, в которые и кладутся необходимые jar-файлы. Более подробно см. описание структуры Libretto-библиотек.