Выражения
Выражения
Запись {...} сама является выражением и содержит выражения, которые выполняются в порядке перечисления. Результатом {...} является результат последнего выражения. Если внутри выражений нет, то результат - пустота ().
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.
Имена с префиксами ищутся в соответствующем префиксу пакете.