- Встраиваемый скриптовый язык
- Платформа .NET
- Функциональная парадигма, статическая типизация
- Функции - объекты первого рода, алгебраические типы
- Взаимодействие со сборками .NET
Блоки выделяются отступами, выражения разделяются переводом строки. Размер отступов не важен, однако разрешается использование только пробелов.
В интерпретатор встроена поддержка следующих типов, аналогичных C#:
unit
- он жеvoid
object
bool
int
long
float
double
decimal
string
char
Изменяемые переменные объявляются ключевым словом var
, константы - let
.
Значение констант не обязательно должно быть константой во время компиляции.
Константы нельзя использовать слева от знака присваивания или передавать по ссылке.
var a = 1
let b = 2
При объявлении переменной обычно ей задается начальное значение. Если оно неизвестно, необходимо явно указать тип:
var c, d: int
с = 3
В языке объявлены следующие операторы, перечисленные в порядке приоритета:
- Возведение в степень (
**
) - Умножение (
*
), деление (/
), получение остатка от деления (%
) - Сложение (
+
), вычитание (-
) - Сдвиг влево (
<:
), сдвиг вправо (:>
), проверка на null (??
) - Сравнение (
==
,<>
,<
,>
,<=
,>=
) - Логические операции (
&&
,||
,^^
) - Битовые операции (
&
,|
,^
)
Оператор сложения также используется для конкатенации строк. Если у объекта есть переопределенный оператор, он будет использован.
Запись - то же самое, что структура. Объект, имеющий только поля. Без методов
и модификаторов доступа. Объявляется ключевым словом record
:
record Student
Name : string
Age : int
Все поля структуры являются публичными.
Структуры могут быть рекурсивными, т.е. включать в себя элементы собственного типа.
Объявляются ключевым словом 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)
Функции объявляются в теле программы ключевым словом 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)
После названия функции идет ее тип, после - список параметров с типами в скобках.
Каждая функция имеет свое пространство имен. Переменные, объявленные в глобальной области видимости, не доступны внутри функций.
После слова fun
идет название функции и тип возвращаемого значения,
а потом ее аргументы с указанием типа. Если у функции не объявлено
ни одного параметра, она будет принимать тип unit
для вызова.
Литералом unit
является выражение ()
.
Ключевое слово unit
является внутренним именованием типа. Его нельзя использовать
для описания типа аргумента функции, в качестве generic-параметра другого типа и в
операторах default
и typeof
.
Любая функция должна возвращать значение. Возвращаемым значением является последнее
выражение тела функции. Если последнее выражение - управляющая конструкция или вызов
функции типа void
, функция возвращает тип unit
.
Если функция не должна возвращать никакого значения, а ее последнее выражение не является
void
, следует использоаать литерал ()
.
Функция вызывается, когда ей передаются все требуемые параметры. Для того, чтобы
вызвать функцию без параметров, ей нужно передать параметр типа 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)
Аргумент в функцию можно передать по ссылке. Для этого как в объявлении, так и при вызове
следует использовать модификатор 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
- Обращение к свойству
- Обращение к индексу объекта с переопределенным индексатором
Анонимные функции могут быть объявлены (практически) в любом месте программы. Помимо отсутствия имени они отличаются от именованных функций следующими моментами:
- Анонимная функция замыкает переменные и константы из внешней области видимости.
- Тип анонимной функции выводится автоматически, поскольку она не может быть рекурсивной.
Анонимная функция может быть описана следующим образом:
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
Как видно из следующего примера, оператор ->
разделяет параметры функции и
ее тело. Даже если параметров нет, ->
все равно необходимо указывать.
Типы аргументов анонимной функции можно не указывать, если они могут быть однозначно выведены из места ее применения, например:
- При передаче анонимной функции в качестве параметра метода или конструктора
- При присвоении в поле, свойство, элемента массива или уже существующую переменную
- При использовании оператора приведения типов
- При использовании оператора композиции функций
При объявлении именованной функции ее можно пометить модификатором pure
. Это
означает, что при равных входных параметрах ее результат всегда будет
одинаковым. В таком случае при первом вызове ее результат будет закеширован,
а при повторных вызовах будет использоваться именно этот кеш, а сама функция
не будет повторно вызвана.
Чистота функции не проверяется компилятором. Фактическое наличие побочных эффектов остается на совести программиста.
Порядок не играет роли. Рекурсивные вызовы допустимы без какого-либо явного
указания (например, в F# требуется модификатор rec
), взаимная рекурсия
также допустима.
Для передачи значения в функцию может быть использован оператор <|
.
Однако этот оператор будет полезен, если аргументы не умещаются на одной строке, или
если требуется передать многострочное выражение.
Оператор <|
требует увеличения отступа относительно выражения, к которому он применяется.
somefx
<| value1
<| (a b) ->
let sum = a + b
sum * sum
<| s -> log s
Для вызова функций по цепочке, особенно со сложными аргументами, удобно использовать оператор передачи контекста, аналогичный точке. Он позволяет размещать длинное выражение на нескольких строках.
someData
|> Where (a -> a.Value > 10)
|> Select (a -> a.Value ** 2)
|> Sum ()
Можно объявить функцию, которая будет принимать переменное число аргументов и упаковывать их в массив. Для этого необходимо указать модификатор типа с троточием у последнего аргумента:
fun count:int (x:object...) ->
x.Length
let three = count 1 2 3
let five = count true "test" 1.3 3.7 three
Как и в C#, данный аргумент должен быть последним в списке.
Новые объекты создаются с помощью ключевого слова new
:
let tuple = new Tuple<string, int> "hello" 2
Условие записывается с помощью блока if / else:
let a = if 1 > 2 then 3 else 4
Выражение может также использоваться по правую сторону от знака присваивания,
если указаны обе ветки (if
и else
). Если блок else
не используется, конструкция
if
всегда возвращает тип unit
.
Цикл записывается с помощью блока while
:
var a = 0
while a < 10 do
Console::WriteLine "{0} loop iteration" a
a = a + 1
Цикл while
всегда возвращает значение последнего выражения в теле цикла.
Если цикл не был выполнен ни одного раза, будет возвращено выражение default(T)
.
Блоки 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
.
Ключевое слово use
открывает пространство имен, добавляя объявленные в нем
классы в глобальное:
use System.Text.RegularExpressions
let rx = new Regex "[a-z]{2}"
Ключевое слово using
позволяет объявить блок, которым ограничен интервал жизни
ресурса, реализуюшего интерфейс IDisposable
:
using fs = (new FileStream "file.txt" FileMode::Create) do
fs.WriteByte 1
Для приведения типов используется оператор as
. В отличие от C#, он кидает
InvalidCastException
в случае неудачи, а не возвращает null
. Может быть
использован на любых типах, в том числе int
/ string
/ bool
/ object
.
Для проверки того, является ли объект экземпляром некоторого класса, используется
оператор is
. Он возвращает bool
.
В языке есть поддержка для упрощенного создания заранее инициализированных
коллекций разного типа. Для этого используется специальный синтаксис оператора new
.
Данный синтаксис используется только в том случае, если количество элементов
заранее известно и оно отлично от нуля. Для объявления пустых структур данных
следует пользоваться их классическими конструкторами. Объявить пустой массив можно
с помощью System.Array.CreateInstance(...)
. Возможно, следует добавить для этого
случая generic-метод.
Тип коллекции выводится автоматически из типов аргументов. Для этого в коллекции должен присутствовать хотя бы один элемент, отличный от null.
Ключи Dictionary
проверяются более строго - они не могут иметь значение null
и их тип
должен совпадать в точности.
// int[]
let ints = new [1; 2; 3]
// System.Collections.Generic.List<int>
let ints = new [[1; 2; 3]]
// System.Collections.Generic.Dictionary<string, int>
let dict = new { "hello" => 1; "world" => 2 }
// System.Tuple<int, string, object>
let t = new (1, "hello world", new object())
В кортеже должно быть от 1 до 7 элементов. Кортежи неограниченной длины, возможно, будут поддерживаться в следующей версии.
Анонимные функции по умолчанию являются выражениями типа 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>)
На основе одной функции можно создать другую, передав ей часть параметров, а вместо недостающих
указав специальный идентификатор _
. Результирующая функция будет принимать оставшиеся параметры:
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
Частичное применение работает как с функциями, так и с конструкторами.
С помощью оператора композиции можно создавать новые функции из существующих, используя результат одной функции в качестве аргумента для другой:
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
Сопоставление с образцом позволяет разбирать произвольные структуры данных и извлекать из
них необходимые значения, наподобие того, как регулярные выражения работают со строками.
Для описания списка шаблонов используется блок match
:
match x with
case 1 then "one"
case 2 then "two"
case _ then "other number"
Правила применяются последовательно, пока не найдется удовлетворяющее - тогда будет возвращено
выражение результата, указанное после then
. Возвращаемым типом является наиболее близкий общий тип,
подходящий ко всем указанным выражениям результата. Если ни одно правило не подошло, будет
возвращено значение по умолчанию для данного типа (default T
).
В качестве образца можно использовать литералы встроенных типов - int
, string
, bool
и т.д.
Также допустим литерал null
.
Если в качестве образца указан идентификатор, значение сохраняется в переменную с таким названием, которая может быть использована в дополнительных проверках и при возвращении результата.
Идентификатор _
(одно нижнее подчеркивание) не сохраняет значение и может быть использован
несколько раз.
После идентификатора можно явно указать тип: тогда правило совпадет только в том случае, если объект является экземпляром данного типа.
match getException () with
case ex:ArgumentException then "Invalid arg"
case ex:DivideByZeroException then "Division by zero"
case _ then "Something went wrong"
Числовое значение можно проверить на принадлежность к диапазону: case 1..5
.
Обе границы диапазона включаются.
Кортежи можно разбить на индивидуальные значения, к каждому из которых применяется свое вложенное правило:
let tuple = new (1; 2; "test"; "bla")
match tuple with
case (x; y; str; _) then fmt "{0} = {1}" str (x + y) // test = 3
Массивы (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>
.
Для объявленных в скрипте структур можно применить вложенные правила для каждого поля:
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"
Поля, для которых проверки не указаны, могут иметь любые значения.
Для объявленных в скрипте типов можно проверить значение ярлыка:
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).
Для элементов словаря можно использовать особый синтаксис: case key => value
.
Строку можно сопоставить с регулярным выражением:
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"
Можно указать несколько правил, разделенных вертикальной чертой - тогда достаточно совпасть хотя бы одному из них:
match number with
case 1 | 2 | 3 then "one, two or three"
case _ then "other number"
Если хотя бы одно правило захватывает какое-либо имя, такое же имя с таким же типом
должно быть захвачено во всех альтернативных правилах. Порядок захвата не важен.
Именованные группы в регулярных выражениях учитываются, а специальное имя _
- нет.
Каждое выражение case
может содержать дополнительную проверку - выражение, которое
должно вернуть true
, чтобы правило совпало. Для этого применяется ключевое слово when
:
match x with
case y when y % 2 == 0 then "even"
case _ then "odd"
Технически, интерпретатор реализован в виде сборки .NET, которую программист может подключить к своей программе, чтобы добавить в нее поддержку скриптового языка.
Сборка содержит основной класс интерпретатора. Схема работы программиста с интерпретатором следующая:
- Добавить в проект ссылку на сборки LENS
- Создать объект интерпретатора
- Зарегистрировать в интерпретаторе свои типы, функции и свойства
- Передать интерпретатору текст исполняемой программы
Результатом является объект типа 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);
}
}
- Поддержка переопределенных операторов
- Раскрутка констант во время компиляции
- Сохранение сгенерированной сборки в виде исполняемого файла
- Возможность отключать поиск extension-методов для ускорения компиляции
Список планируемых возможностей для следующих версий доступен в виде задач на Github: https://github.com/impworks/lens/issues
Поскольку LENS является встраиваемым языком, в нем не будет вещей, присущих классическим языкам программирования, как то:
- Создание полноценных классов с методами
- Модификаторы доступа
- Объявление интерфейсов
- Управляющие конструкции, прерывающие поток выполнения:
return
,break
,continue