Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Выражения

Выражения

Запись {...} сама является выражением и содержит выражения, которые выполняются в порядке перечисления. Результатом {...} является результат последнего выражения. Если внутри выражений нет, то результат - пустота ().

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 (отдельной конструкцией) . (путь) .* (путь)
  • * div idiv mod
  • + - (инфиксный)
  • == != > >= < <= in
  • not
  • and
  • or
  • инфиксная запись вызова метода (не оператора)
  • 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

Каждое условие состоит из частей:

  1. case-условие (с опциональным if (...)) или else
  2. символ =
  3. выражение (кроме специального вида последнего условия 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 условий:

  1. case x - срабатывает всегда, переменной x присваивается разбираемое значение
  2. case x: тип - срабатывает, если разбираемое значение динамически соответствует указанному типу, переменной x указанного типа присваивается разбираемое значение.
  3. case ~: тип - срабатывает, если разбираемое значение динамически соответствует указанному типу.
  4. case {...} - срабатывает, если разбираемое значение равно результату выполнения выражения, указанного в фигурных скобках. Используется сравнение, полностью эквивалентное оператору ==.
  5. case () - срабатывает, если разбираемое значение пустое.
  6. 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

Обработка пустоты

Часто возникает задача обработать пустоту, причём убрав её из типа результата. Т.е. перейти от кардинальности ? к чистому типу и(или) от кардинальности * к кардинальности +.

Есть несколько вариантов.

  1. Метод onEmpty, который заменяет пустоту на указанное значение:
fix v: Int* = …
fix r: Int+ = v.*onEmpty(0)

Можно не возвращать значение, а кидать исключение: v.*onEmpty(error("Wrong value"))

  1. Метод onEmpty с заменой пустоты и обработкой непустого значения:
fix v: Int* = …
fix r: Int+ = v.*onEmpty(-1)#(a){ a + 100 }

Прибавляет 100 к непустому значению, но возвращает -1 для пустоту.

todo: показать, что a может быть коллекцией

  1. Использование 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

Разрешение имен

Порядок разрешения имен без префикса:

  1. Локальные переменные/функции (если имя не после точки в пути).
  2. Методы, определенные в текущем пакете.
  3. Методы, определенные в пакете контекста.
  4. Методы, определенные в пакете libretto.

Имена с префиксами ищутся в соответствующем префиксу пакете.