Skip to content

Latest commit

 

History

History
757 lines (547 loc) · 32.5 KB

README.RU.md

File metadata and controls

757 lines (547 loc) · 32.5 KB

LENS

1. Коротко о главном

  • Встраиваемый скриптовый язык
  • Платформа .NET
  • Функциональная парадигма, статическая типизация
  • Функции - объекты первого рода, алгебраические типы
  • Взаимодействие со сборками .NET

2. Синтаксис и возможности

Блоки выделяются отступами, выражения разделяются переводом строки. Размер отступов не важен, однако разрешается использование только пробелов.

2.1. Типы данных

В интерпретатор встроена поддержка следующих типов, аналогичных C#:

  • unit - он же void
  • object
  • bool
  • int
  • long
  • float
  • double
  • decimal
  • string
  • char

2.2. Объявление констант и переменных

Изменяемые переменные объявляются ключевым словом var, константы - let. Значение констант не обязательно должно быть константой во время компиляции. Константы нельзя использовать слева от знака присваивания или передавать по ссылке.

var a = 1
let b = 2

При объявлении переменной обычно ей задается начальное значение. Если оно неизвестно, необходимо явно указать тип:

var c, d: int
с = 3

2.3. Операторы

В языке объявлены следующие операторы, перечисленные в порядке приоритета:

  1. Возведение в степень (**)
  2. Умножение (*), деление (/), получение остатка от деления (%)
  3. Сложение (+), вычитание (-)
  4. Сдвиг влево (<:), сдвиг вправо (:>), проверка на null (??)
  5. Сравнение (==, <>, <, >, <=, >=)
  6. Логические операции (&&, ||, ^^)
  7. Битовые операции (&, |, ^)

Оператор сложения также используется для конкатенации строк. Если у объекта есть переопределенный оператор, он будет использован.

2.4. Записи

Запись - то же самое, что структура. Объект, имеющий только поля. Без методов и модификаторов доступа. Объявляется ключевым словом record:

record Student
    Name : string
    Age : int

Все поля структуры являются публичными.

Структуры могут быть рекурсивными, т.е. включать в себя элементы собственного типа.

2.5. Алгебраические типы

Объявляются ключевым словом type и перечислением возможных ярлыков типа. К каждому ярлыку может быть прикреплена метка с помощью ключевого слова of:

type Suit
    Hearts
    Clubs
    Spades
    Diamonds

type Card
    Ace of Suit
    King of Suit
    Queen of Suit
    Jack of Suit
    ValueCard of Tuple<Suit, int>

Ярлыки должны быть глобально уникальными идентификаторами в контексте скрипта, поскольку они же являются статическими конструкторами:

let jack = Jack Hearts
let two = ValueCard new (Diamonds; 2)

2.6. Функции

Функции объявляются в теле программы ключевым словом fun:

fun negate:int (x:int) -> -x

fun hypo:int (a:int b:int) ->
    let sq1 = a * a
    let sq2 = b * b
    Math::Sqrt (sq1 + sq2)

После названия функции идет ее тип, после - список параметров с типами в скобках.

Каждая функция имеет свое пространство имен. Переменные, объявленные в глобальной области видимости, не доступны внутри функций.

2.61. Аргументы функции

После слова fun идет название функции и тип возвращаемого значения, а потом ее аргументы с указанием типа. Если у функции не объявлено ни одного параметра, она будет принимать тип unit для вызова. Литералом unit является выражение ().

Ключевое слово unit является внутренним именованием типа. Его нельзя использовать для описания типа аргумента функции, в качестве generic-параметра другого типа и в операторах default и typeof.

2.6.2. Возвращаемое значение функции

Любая функция должна возвращать значение. Возвращаемым значением является последнее выражение тела функции. Если последнее выражение - управляющая конструкция или вызов функции типа void, функция возвращает тип unit.

Если функция не должна возвращать никакого значения, а ее последнее выражение не является void, следует использоаать литерал ().

2.6.3 Вызов функции

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

fun sum:int (a:int b:int c:int) -> a + b + c
fun getTen:int -> 10

let five = sum 1 1 3
let ten = getTen ()

При вызове функции можно использовать только литералы и имена переменных. Любые более сложные выражения должны быть взяты в скобки.

fun sum:double (a:double b:double) -> a + b

let sum = sqrt sin 1        // sqrt(sin, 1) - wtf?
let sum = sqrt (sin 1)      // компилируется
let someData = sum (sin 1) (cos 2)

2.6.4. Передача аргумента по ссылке

Аргумент в функцию можно передать по ссылке. Для этого как в объявлении, так и при вызове следует использовать модификатор ref:

fun test:bool (str:ref string) ->
    if str.Length > 100 then
        str = str.Substring 0 100
        true
    else
        false
        
var a = "hello world"
var b = "test"
println (test ref a) // true
println (test ref b) // false

После ref может использоваться:

  • Имя переменной, объявленной с помощью var
  • Имя аргумента текущей функции
  • Обращение к полю
  • Обращение к индексу массива

Не может быть использовано:

  • Литерал, выражение или имя константы, объявленной с помощью let
  • Обращение к свойству
  • Обращение к индексу объекта с переопределенным индексатором

2.6.5. Анонимные функции

Анонимные функции могут быть объявлены (практически) в любом месте программы. Помимо отсутствия имени они отличаются от именованных функций следующими моментами:

  1. Анонимная функция замыкает переменные и константы из внешней области видимости.
  2. Тип анонимной функции выводится автоматически, поскольку она не может быть рекурсивной.

Анонимная функция может быть описана следующим образом:

let sum = (a:int b:int) -> a + b
let getTen = -> sum 5 5
let addFive = (a:int) ->
    let b = 5
    sum a b // то же самое, что sum 5

Как видно из следующего примера, оператор -> разделяет параметры функции и ее тело. Даже если параметров нет, -> все равно необходимо указывать.

Типы аргументов анонимной функции можно не указывать, если они могут быть однозначно выведены из места ее применения, например:

  • При передаче анонимной функции в качестве параметра метода или конструктора
  • При присвоении в поле, свойство, элемента массива или уже существующую переменную
  • При использовании оператора приведения типов
  • При использовании оператора композиции функций

2.6.6. Чистые функции и мемоизация

При объявлении именованной функции ее можно пометить модификатором pure. Это означает, что при равных входных параметрах ее результат всегда будет одинаковым. В таком случае при первом вызове ее результат будет закеширован, а при повторных вызовах будет использоваться именно этот кеш, а сама функция не будет повторно вызвана.

Чистота функции не проверяется компилятором. Фактическое наличие побочных эффектов остается на совести программиста.

2.6.7. Порядок объявления и вызова функций

Порядок не играет роли. Рекурсивные вызовы допустимы без какого-либо явного указания (например, в F# требуется модификатор rec), взаимная рекурсия также допустима.

2.6.8. Оператор передачи значения

Для передачи значения в функцию может быть использован оператор <|. Однако этот оператор будет полезен, если аргументы не умещаются на одной строке, или если требуется передать многострочное выражение.

Оператор <| требует увеличения отступа относительно выражения, к которому он применяется.

somefx
    <| value1
    <| (a b) ->
        let sum = a + b
        sum * sum
    <| s -> log s

2.6.9. Оператор передачи контекста

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

someData
    |> Where (a -> a.Value > 10)
    |> Select (a -> a.Value ** 2)
    |> Sum ()

2.6.10 Переменное число аргументов в функции

Можно объявить функцию, которая будет принимать переменное число аргументов и упаковывать их в массив. Для этого необходимо указать модификатор типа с троточием у последнего аргумента:

fun count:int (x:object...) ->
    x.Length
    
let three = count 1 2 3
let five = count true "test" 1.3 3.7 three

Как и в C#, данный аргумент должен быть последним в списке.

2.7. Ключевые слова и конструкции

2.7.1. Создание объектов

Новые объекты создаются с помощью ключевого слова new:

let tuple = new Tuple<string, int> "hello" 2

2.7.2. Условие

Условие записывается с помощью блока if / else:

let a = if 1 > 2 then 3 else 4

Выражение может также использоваться по правую сторону от знака присваивания, если указаны обе ветки (if и else). Если блок else не используется, конструкция if всегда возвращает тип unit.

2.7.3. Цикл

Цикл записывается с помощью блока while:

var a = 0
while a < 10 do
    Console::WriteLine "{0} loop iteration" a
    a = a + 1

Цикл while всегда возвращает значение последнего выражения в теле цикла. Если цикл не был выполнен ни одного раза, будет возвращено выражение default(T).

2.7.4. try-catch

Блоки try-catch записываются следующим образом:

try
    doSomethingHorrible()
catch ex:WebException
    notify "web exception" ex.Message
catch ex:DivideByZeroException
    notify "whoops!"
catch
    notify "something weird has happened"

После блока try также может идти блок finally, который выполняется всегда при выходе из области видимости, возникало ли исключение или нет:

try
    doSomething ()
catch
    notify "something happened"
finally
    freeResources ()

Блок try-catch всегда возвращает unit.

2.7.5. use

Ключевое слово use открывает пространство имен, добавляя объявленные в нем классы в глобальное:

use System.Text.RegularExpressions
let rx = new Regex "[a-z]{2}"

2.7.6. using

Ключевое слово using позволяет объявить блок, которым ограничен интервал жизни ресурса, реализуюшего интерфейс IDisposable:

using fs = (new FileStream "file.txt" FileMode::Create) do
    fs.WriteByte 1

2.7.7. Приведение и проверка типов

Для приведения типов используется оператор as. В отличие от C#, он кидает InvalidCastException в случае неудачи, а не возвращает null. Может быть использован на любых типах, в том числе int / string / bool / object.

Для проверки того, является ли объект экземпляром некоторого класса, используется оператор is. Он возвращает bool.

2.8. Создание структур данных

В языке есть поддержка для упрощенного создания заранее инициализированных коллекций разного типа. Для этого используется специальный синтаксис оператора new.

Данный синтаксис используется только в том случае, если количество элементов заранее известно и оно отлично от нуля. Для объявления пустых структур данных следует пользоваться их классическими конструкторами. Объявить пустой массив можно с помощью System.Array.CreateInstance(...). Возможно, следует добавить для этого случая generic-метод.

Тип коллекции выводится автоматически из типов аргументов. Для этого в коллекции должен присутствовать хотя бы один элемент, отличный от null.

Ключи Dictionary проверяются более строго - они не могут иметь значение null и их тип должен совпадать в точности.

2.8.1. Массивы

// int[]
let ints = new [1; 2; 3]

2.8.2. Списки

// System.Collections.Generic.List<int>
let ints = new [[1; 2; 3]]

2.8.3 Словари

// System.Collections.Generic.Dictionary<string, int>
let dict = new { "hello" => 1; "world" => 2 }

2.8.4 Кортежи

// System.Tuple<int, string, object>
let t = new (1, "hello world", new object())

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

2.9 Функциональные возможности

2.9.1 Приведение делегатов

Анонимные функции по умолчанию являются выражениями типа Func<> или Action<>, в зависимости от того, возвращают ли они некое значение или их последнее выражение имеет тип unit.

Для того, чтобы передать анонимную функцию в качестве параметра с иным типом, можно использовать приведение типов. Для этого типы принимаемых и возвращаемых значений должны в точности соответствовать:

let filter = (x:int) -> x % 2 == 0
let data = (Enumerable::Range 1 100).ToArray ()
let even = Array::FindAll data (filter as Predicate<int>)

2.9.2 Частичное применение

На основе одной функции можно создать другую, передав ей часть параметров, а вместо недостающих указав специальный идентификатор _. Результирующая функция будет принимать оставшиеся параметры:

fun add:int (x:int y:int) -> x + y
let add2 = add 2 _
let alsoAdd2 = add _ 2

let three = add2 1 // int(3)

При наличии перегруженных вариантов функции явно указанные аргументы должны однозначно идентифицировать функцию, в противном случае возникнет ошибка неоднозначности:

fun repeat:str (value:int    count:int) -> string::Join "" (new [value] * count)
fun repeat:str (value:string count:int) -> string::Join "" (new [value] * count)

let repeat2 = repeat _ 2 // error: both functions match

Частичное применение работает как с функциями, так и с конструкторами.

2.9.3 Композиция функций

С помощью оператора композиции можно создавать новые функции из существующих, используя результат одной функции в качестве аргумента для другой:

let parse = (x:string) -> Convert::ToInt32 x
let inc = (x:int) -> x + 1

let compound = parse :> inc
println (compound "2") // 3

Функция справа от оператора :> должна иметь строго 1 параметр, совпадающий с типом возвращаемого значения функции слева. В этом случае удобно использовать частичное применение:

let add = (x:int y:int) -> x + y
let compound = parse :> add _ 1

2.10. Сопоставление с образцом

Сопоставление с образцом позволяет разбирать произвольные структуры данных и извлекать из них необходимые значения, наподобие того, как регулярные выражения работают со строками. Для описания списка шаблонов используется блок match:

match x with
    case 1 then "one"
    case 2 then "two"
    case _ then "other number"

Правила применяются последовательно, пока не найдется удовлетворяющее - тогда будет возвращено выражение результата, указанное после then. Возвращаемым типом является наиболее близкий общий тип, подходящий ко всем указанным выражениям результата. Если ни одно правило не подошло, будет возвращено значение по умолчанию для данного типа (default T).

2.10.1. Типы правил

2.10.1.1. Литерал

В качестве образца можно использовать литералы встроенных типов - int, string, bool и т.д. Также допустим литерал null.

2.10.1.2. Захват имени

Если в качестве образца указан идентификатор, значение сохраняется в переменную с таким названием, которая может быть использована в дополнительных проверках и при возвращении результата.

Идентификатор _ (одно нижнее подчеркивание) не сохраняет значение и может быть использован несколько раз.

После идентификатора можно явно указать тип: тогда правило совпадет только в том случае, если объект является экземпляром данного типа.

match getException () with
    case ex:ArgumentException then "Invalid arg"
    case ex:DivideByZeroException then "Division by zero"
    case _ then "Something went wrong"
2.10.1.3. Диапазон

Числовое значение можно проверить на принадлежность к диапазону: case 1..5. Обе границы диапазона включаются.

2.10.1.4. Кортежи

Кортежи можно разбить на индивидуальные значения, к каждому из которых применяется свое вложенное правило:

let tuple = new (1; 2; "test"; "bla")
match tuple with
    case (x; y; str; _) then fmt "{0} = {1}" str (x + y) // test = 3
2.10.1.5. Массивы и последовательности

Массивы (T[]), списки (List<T>) и последовательности (IEnumerable<T>) можно разбить на элементы:

match array with
    case [] then "empty"
    case [x] then fmt "one item: {0}" x
    case [x; y] then fmt "two items: {0} and {1}" x y
    case [_; _; _] then "three items"
    case _ then "more than 3 items"

К одному из идентификаторов можно применить префикс-многоточие. Тогда этот идентификатор захватит не один элемент, а вложенную последовательность из нуля и более элементов:

fun length:int (array:object[]) ->
    match array with
        case [] then 0
        case [_] then 1
        case [_; ...x] then 1 + (length x)

Для массива и IList<T> подмножество может быть любым элементом, тип подмножества - T[]. Для остальных случаем - только последний элемент, тип - IEnumerable<T>.

2.10.1.6. Записи

Для объявленных в скрипте структур можно применить вложенные правила для каждого поля:

record Point
    X : int
    Y : int

fun describe:string (pt:Point) ->
    match pt with
        case Point(X = 0; Y = 0) then "Zero"
        case Point(X = 0) | Point(Y = 0) then "half-zero"
        case _ then "Just a point"

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

2.10.1.7. Алгебраические типы

Для объявленных в скрипте типов можно проверить значение ярлыка:

type Expr
    IntExpr of int
    StringExpr of string
    AddExpr of Tuple<Expr, Expr>
    SubExpr of Tuple<Expr, Expr>

fun describe:string (expr:Expr) ->
    match expr with
        case IntExpr of x then fmt "Int({0})" x
        case StringExpr of x then fmt "Str({0})" x
        case AddExpr of (x; y) then fmt "{0} + {1}" (describe x) (describe y)
        case SubExpr of (x; y) then fmt "{0} - {1}" (describe x) (describe y)

Для типов без ярлыка следует использовать явное указание типа (см. 2.10.1.1).

2.10.1.8. KeyValue

Для элементов словаря можно использовать особый синтаксис: case key => value.

2.10.1.9. Регулярные выражения

Строку можно сопоставить с регулярным выражением:

match "String" with
    case #^[a-z]+$# then "lower"
    case #^[A-Z]+$# then "upper"
    case #^[a-z]+$#i then "mix"

Допустимы следующующие модификаторы в любом порядке:

  • i = RegexOptions.IgnoreCase
  • m = RegexOptions.Multiline
  • s = RegexOptions.Singleline
  • c = RegexOptions.CultureInvariant

Именованные группы автоматически извлекаются в одноименные переменные:

match "My name is John" with
    case #^My name is (?<name>\w+)$#i then fmt "Hello, {0}" name

По умолчанию, тип извлеченных переменных - string. Для удобства значения можно автоматически сконвертировать в любой тип T, если для него объявлен статический метод: bool T.TryParse(string value, out T result). Для этого тип указывается через двоеточие после имени группы. Если метод TryParse возвращает false, правило не применяется.

match "I have 2 cookies" with
    case #^I have (?<count:int>\d+) cookies$# then fmt "Twice as much will be {0}" (count * 2)

// Result: "Twice as much will be 4"

2.10.2. Альтернативные правила

Можно указать несколько правил, разделенных вертикальной чертой - тогда достаточно совпасть хотя бы одному из них:

match number with
    case 1 | 2 | 3 then "one, two or three"
    case _ then "other number"

Если хотя бы одно правило захватывает какое-либо имя, такое же имя с таким же типом должно быть захвачено во всех альтернативных правилах. Порядок захвата не важен. Именованные группы в регулярных выражениях учитываются, а специальное имя _ - нет.

2.10.3 Проверки when

Каждое выражение case может содержать дополнительную проверку - выражение, которое должно вернуть true, чтобы правило совпало. Для этого применяется ключевое слово when:

match x with
    case y when y % 2 == 0 then "even"
    case _ then "odd"

3. Встраиваемость

Технически, интерпретатор реализован в виде сборки .NET, которую программист может подключить к своей программе, чтобы добавить в нее поддержку скриптового языка.

Сборка содержит основной класс интерпретатора. Схема работы программиста с интерпретатором следующая:

  1. Добавить в проект ссылку на сборки LENS
  2. Создать объект интерпретатора
  3. Зарегистрировать в интерпретаторе свои типы, функции и свойства
  4. Передать интерпретатору текст исполняемой программы

Результатом является объект типа Func<object>, позволяющий исполнять скрипт многократно без необходимости перекомпиляции.

Примерный код этого взаимодействия на языке C# представлен ниже:

public void Run()
{
    var source = "a = 1 + 2";
    var a = 0;

    var compiler = new LensCompiler();
    compiler.RegisterProperty("a", () => a, newA => a = newA);
    
    try
    {
        var fx = compiler.Compile(source);
        fx();

        Console.WriteLine("Success: {0}", a);
    }
    catch (LensCompilerException ex)
    {
        Console.WriteLine("Error: {0}", ex.FullMessage);
    }
}

4. Дополнительные возможности

  • Поддержка переопределенных операторов
  • Раскрутка констант во время компиляции
  • Сохранение сгенерированной сборки в виде исполняемого файла
  • Возможность отключать поиск extension-методов для ускорения компиляции

5. Ограничения

5.1. Планы на будущее

Список планируемых возможностей для следующих версий доступен в виде задач на Github: https://github.com/impworks/lens/issues

5.2. Сознательные ограничения

Поскольку LENS является встраиваемым языком, в нем не будет вещей, присущих классическим языкам программирования, как то:

  • Создание полноценных классов с методами
  • Модификаторы доступа
  • Объявление интерфейсов
  • Управляющие конструкции, прерывающие поток выполнения: return, break, continue