Продолжается подписка на наши издания! Вы не забыли подписаться?

Краткий обзор языка Ela

Автор: Василий Воронков
Опубликовано: 30.12.2010
Версия текста: 1.1
Цель данной статьи
Синтаксис и семантика
Базовые конструкции языка
Операторы
Условный оператор
Циклы
Объявление переменных
Обработка исключений
Типы данных
Целые и вещественные числа
Строки и символы
Булевы
Кортежи
Динамические индексированные массивы
Связные списки
Записи
Последовательности
Функции
Полиморфные варианты
Ленивые значения
Модули
Тип Unit
Сопоставление с образцом
Общая информация
Образцы
Конструкции для сопоставления с образцом
И еще несколько полезных вещей
Comprehension
Генераторы
Вместо заключения

Цель данной статьи

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

Синтаксис и семантика

Здесь я буду сравнивать Ela с языками C# (на котором Ela написана и который является самым распространенным языком под платформу .NET) и JavaScript (который также является одним из наиболее широко известных динамических языков программирования и, подобно Ela, имеет C-подобный синтаксис).

Итак, Ela является языком с С-подобным синтаксисом, а это значит, что в коде будет много фигурных скобок и точек с запятой. В большинстве случаев синтаксис не должен вызывать вопросов у программистов, знакомых хотя бы с одним С-подобным языком. Однако есть и небольшие отличия от того, к чему привыкли программисты того же JavaScript.

В Ela не используется механизм аналогичный semicolon insertion в JavaScript, однако правила по расстановке точек с запятой все же не такие строгие, как в C#. Точки с запятой в Ela можно опускать у последнего выражения в блоке, а также если выражение завершается на фигурную скобку или же сразу после него идет фигурная скобка. Фигурные скобки также можно опускать в тех случаях, когда у вас есть лишь одно выражение. Например, если функция состоит из одного выражения — то фигурные скобки необязательны:

let item(arr, index) if (arr.length > index) `Some(arr[index]) else `None;

При желании вы можете записать эту функцию и так:

let item(arr, index) {
  if (arr.length > index) 
    `Some(arr[index]) 
  else 
    `None
}

Другое важное отличие от JavaScript – это то, что в Ela используется лексическая область видимости, т.е. такая же, как и в «прародителе» обсуждаемого синтаксиса, в языке С. В этом смысле Ela ближе к C#, чем к JavaScript, однако, в отличие от C#, Ela допускает затенение переменных в родительском блоке, так же как и С/C++.

Например, следующий код является совершенно корректным в Ela и приведет к ошибке компиляции в C#:

var x = 1;

{
  var x = 2;
}

Фигурные скобки в Ela всегда задают лексический блок. Также лексический блок задают и конструкции вроде объявления функции, организации цикла (for, while), условные операторы (if, when, unless), обработки исключений (try/catch) и сопоставления с образцом (match, is). Об этих конструкциях более подробно будет рассказано ниже.

Еще одна существенная черта синтаксиса Ela – это то, что в Ela все является выражением (expression) , все возвращает значение.

Например, следующий код является совершенно корректным:

var x = 
  if ($param > 0) 
    createList($param) 
  else 
    [];

Корректным является даже такой код на Ela:

"Hello, world!";
5 + 5;
2;

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

Warning ELA1002: Value returned by this expression is not used.

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

ignore "Hello, world!";
ignore 5 + 5;
ignore 2;

Само собой некоторые императивные конструкции вроде for, break, while и т.д. не могут возвращать ничего полезного, поэтому считается, что они возвращают значение типа unit (он же void в C#) и в таком случае предупреждение не генерируется. Предупреждение также не генерируется, если выражение является последним во всем файле с кодом – так как значение данного выражения будет значением, возвращаемым интерпретатором Ela вызывавшему его коду. Например, если для исполнения файлов Ela вы используете приложение Ela Console, то значение последнего выражение (если оно отлично от unit) будет выведено в консоль.

И наконец последний момент, о котором я упомяну в данном разделе, это типизация языка. Ela, в отличие от C#, является динамически типизированным языком, т.е. информация о типе становится доступна только во время исполнения кода. Таким же поведением обладает и JavaScript, однако, в отличие от JavaScript, Ela является строго типизированным языком, т.е. в ней нет неявных приведений к булевому типу, нельзя складывать строки и числа и т.д.

В остальном синтаксис и семантика основных конструкций Ela не вызовет у вас никаких вопросов. Например, группировка выражений осуществляется с помощью круглых скобок. Также в Ela есть операторы инкремента и декремента с такой же семантикой, как и в С; есть отдельный оператор для сравнения (==) и отдельный оператор для присваивания (=), причем последний, так же как и в С, возвращает результат операции. Так как Ela является строго типизированным языком, то в ней отпадает необходимости в операторах «строгого» сравнения (=== и !==) из JavaScript. Остальной набор операторов языка будет хорошо знаком вам по тому же C# - в Ela есть все основные арифметические операторы, битовые операторы, операторы сдвига, а также ряд дополнительных, о которых будет рассказано в следующей секции.

Базовые конструкции языка

Операторы

Ela поддерживает следующие бинарные операторы.

Оператор

Описание

Пример

<=>

Обмен переменных значениями

x <=> y

||

Условное «Или»

x || y

&&

Условное «И»

x && y

is

Оператор для сопоставления с образцом

x is int

>>>

Композиция функций слева направо

x >>> y >>> z

<<<

Композиция функций справа налево

x <<< y <<< z

::

Конструктор списков

x::y::[]

|>

Оператор «Forward pipe»

x(arg) |> y |> z

<|

Оператор «Backward pipe»

z <| y <| x(arg)

|

Битовое «Или»

x | y

^

Исключающее «Или»

x ^ y

&

Битовое «И»

x & y

==

Оператор «Равенство»

x == y

!=

Оператор «Неравенство»

x != y

>

Оператор «Больше»

x > y

<

Оператор «Меньше»

x < y

>=

Оператор «Больше или равно»

x >= y

<=

Оператор «Меньше или равно»

x <= y

>>

Оператор правого сдвига

x >> 8

<<

Оператор левого сдвига

x << 8

+

Сложение

x + y

-

Вычитание

x – y

*

Умножение

x * y

/

Деление

x / y

%

Получение остатка при делении

x % y

**

Возведение в степень

x ** y

:>

Приведение типов

x :> int

Большинство из приведенных выше операторов наверняка вам уже знакомы, про те операторы, которых нет ни в C#, ни в JavaScript, я расскажу чуть подробнее.

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

Операторы композиции функций и pipe-операторы соответствуют таковым в языке F# и используются при работе с функциональными типами. С помощью операторов композиции вы можете создавать новые функции во время исполнения программы, а pipe-операторы позволяют записывать цепочку вызова функций в более декларативном ключе. Все эти операторы правоассоциативны. Об этих операторах будет более подробно рассказано в секции, посвященной функциям.

Оператор возведения в степень в качестве правого операнда принимает степень, в которую вы хотите возвести число. Например, выражение вида «x * x» будет полностью аналогично «x ** 2».

Поведение оператора для приведения типов должно быть интуитивно понятно, он работает практически так же, как и так называемый «С-каст» в языке C# и тоже может генерировать исключения в тех случаях, когда приведение невозможно.

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

Ela поддерживает следующие унарные операторы:

Оператор

Описание

Пример

+

Унарный плюс

x + y

-

Унарный минус

x - y

!

Логическое отрицание

!x

~

Поразрядное дополнение операнда

~x

&

Оператор «Взять значение»

&x

++

Инкремент (постфиксная и префиксная формы)

x++; ++x;

--

Декремент (постфиксная и префиксная формы)

x--; --x;

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

Однако это еще не все – Ela также поддерживает создание пользовательских операторов. Вы можете объявлять как бинарные, так и унарные операторы.

Вот пример бинарного оператора:

let @(+++)(x, y) x :> string + y :> string;

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

var x = 2;
var y = 3;
x +++ y

Вывод программы:

23

Пример унарного оператора:

let ~(!!)(x) if (x > 0) x else -x;
var x = -2;
!!x

Вывод программы

2

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

Пользовательские операторы не должны совпадать с уже существующими, т.е. перегрузка имеющихся операторов не поддерживается. Также при объявлении пользовательских операторов вы можете использовать в качестве названий только следующие символы: !%&*+-./<=>?@^|~.

Название оператора всегда берется в круглые скобки. Eсли перед названием идет символ «@», то оператор считается бинарным и должен принимать два аргумента, если – символ «~», то оператор считается унарным и может иметь только один аргумент. Также операторы можно объявлять только с помощью ключевого слова let.

Условный оператор

Условный оператор (if) имеет в Ela точно такой же синтаксис, как в C# и JavaScript, однако он является выражением, как вы уже заметили по приведенным ранее примерам. Более того, else обязателен – благодаря этому мы всегда можем быть уверены, что оператор if, независимо от условия, всегда возвращает какое-либо выражение – или в конце блока if, или в конце блока else.

Например, следующий код является неверным и приведет к ошибке компиляции:

var x = 2;

if (x > 0)
  cout x;

Запустив данный код на выполнение, вы увидите такое сообщение об ошибке:

Error ELA107: A conditional expression (if/else) is missing a required 'else' clause.

В тех случаях, когда вам все же требуется if без else, можно использовать специальный оператор when. Например, вышеприведенный пример кода мы можем переписать так:

var x = 2;

when (x > 0)
  cout x;

В отличие от if у оператора when никогда не бывает блока else. По этой причине оператор when всегда возвращает значение типа unit – независимо от того, выполнилось ли его условие или нет.

Также для удобства был введен специальный оператор unless. Данный оператор ведет себя точно так же, как и оператор when, но с одним-единственным отличием – код внутри unless выполняется только в том случае, если условие равно false. Например, код вида:

when (x != 0)
  cout x;

Полностью эквивалентен:

unless (x == 0)
  cout x;

Циклы

Ela поддерживает циклы for и while, а также операторы break, return и continue. Цикл for несколько отличается по синтаксису от того, что используется в С-подобных языках и имеет три варианта. У цикла while – четыре варианта. Для перехода к следующей итерации любого из описываемых здесь циклов можно использовать оператор continue, для выхода из цикла – оператор break.

Цикл for-to

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

for (x to 10) 
  cout x;

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

Возможна и такая запись:

for (x = 1 when (x % 2 == 0) to 10) 
  cout x;

Данный цикл выведет уже список четных целых чисел в диапазоне от 1 до 10.

Цикл for-downto

Этот цикл аналогичен for-to и имеет единственное отличие – он перебирает значения по убывающей, а не по возрастающей. Цикл for-downto также поддерживает guards:

for (x = 10 when (x % 2 == 0) downto 1) 
  cout x;

Результатом этого кода будет список всех четных чисел от 1 до 10, выведенный в обратом порядке.

Цикл for-in

Данный цикл ведет себя аналогично циклу foreach в C#, однако поддерживает guards и сопоставление с образцом.

Например, предположим, что у нас есть список, элементами которого являются записи (о типе данных «запись» будет более подробно написано ниже). Разобрать этот список в цикле можно, используя такой код:

let list = [ (x:1,y:2), (x:3,y:4), (x:5,y:6), (x:7,x:8), (x:9,x:10) ];

for ((y = y) as rec when (y % 2 == 0) in list)
  cout rec;

Данный пример связывает значение поля «y» с одноименной локальной переменной, связывает весь экземпляр элемента списка с локальной переменной «rec» и выбирает из списка только те записи, значение поля «y» которых является четным.

Цикл while

Это самый простой цикл в Ela, он полностью аналогичен циклам while из других С-подобных языков:

while (x < 10)
  x++;

Следует иметь в виду, что компилятор Ela специально оптимизирует циклы вида while(true), устраняя лишнюю проверку. Поэтому, если вам нужен бесконечный цикл, то его лучше всего организовать его именно таким путем.

Цикл do-while

Этот цикл аналогичен циклу while, однако отличается от него тем, что код внутри цикла всегда будет выполнен хотя бы один раз, независимо от условия:

do
  cout "do while"
while (false);

Если вы запустите данный пример, то он выведет на консоль «do while».

Цикл until

Цикл until аналогичен циклу while, но выполнение цикла происходит до тех пор, пока выражение справа от until возвращает «False». Иначе говоря, такой цикл:

while (!x) {
  
}

равнозначен такому:

until (x) {
  
}

Цикл do-until

Как и в случае с простым циклом until, данный вид цикла аналогичен циклу do-while, но выполняется до тех пор пока выражение справа от until возвращает «False». Соответственно:

do {
  
} while (!x)

равнозначен:

do {
  
} until (x);

Объявление переменных

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

var x = 2;

происходит связывание значения 2 и имени x.

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

Ela поддерживает изменяемые и неизменяемые имена. Изменяемые имена – это такие, значение которых может быть изменено по ходу выполнения программы. Они объявляются с помощью ключевого слова var, по аналогии с JavaScript, как вы видели в примере выше. Однако, в отличие от JavaScript, предварительное объявление имени, как и его инициализация, являются обязательным, и если вы попробуете использовать необъявленное ранее имя, то получите ошибку времени компиляции.

Выполнение следующего кода:

var x = 2;
x + y

выведет следующее сообщение:

Error ELA208: Undefined variable 'y'.

Ela также поддерживает и неизменяемые имена, которые объявляются с помощью ключевого слова let. Имена, объявленные с помощью let, можно рассматривать как константы – вы не сможете изменить их значений по ходу выполнения программы, – однако если значением такого имени является изменяемая структура данных (например, массив), то у вас по-прежнему останется возможность изменять эту структуру «по месту». Например:

let arr = [| 0, 1, 2 |];
arr[0] = 1; //OK
arr = [| 3, 4, 5 |]; //Error ELA213: Unable to change a value of an immutable variable 'arr'.

Есть и возможность объявления «закрытых» имен. Для этого после var или let нужно добавить модификатор private. Такие переменные не будут видны за пределами модуля (о модулях см. в нижеследующих секциях).

//наша константа не будет видна за пределами модуля
let private E = 2.7182818284590451D; 

Важной особенностью объявления имен в Ela является поддержка сопоставления с образцом. Например, получая из функции кортеж вы можете «разобрать» этот кортеж на отдельные имена таким вот образом:

let (x, y) = myFunc();

Более подробно о сопоставлении с образцом речь также пойдет ниже.

Обработка исключений

Ela поддерживает структурную обработку исключений на манер языков C# и JavaScript, однако в большинстве случаев вместо генерации исключений рекомендуется использовать полиморфные варианты.

Генерация исключения осуществляется с помощью оператора throw. В качестве исключения может выступать любой тип данных – например, вы можете сгенерировать исключение, состоящее из строки или целого числа, как и в JavaScript.

Для обработки исключений используется конструкция try/catch:

var result = (); //Объявляем переменную и инициализируем ее типом unit

try
  result = myFunc();
catch (e)
  cout e.message;

Не забываем, что конструкция try/catch так же является выражением, поэтому вышеприведенный пример можно записать и так:

var result = 
  try
    myFunc();
  catch (e)
    cout e.message;

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

try
  result = myFunc();
catch (e)
  on `ListError -> processListError()
  on `TupleError -> processTupleError()
  on _ -> processUnknownError();

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

Типы данных

Целые и вещественные числа

Ela поддерживает 32 и 64-битные целые числа со знаком, а также 32 и 64-битные числа с плавающей запятой (floating point numbers). Внутренние названия для этих типов, которое можно использовать, к примеру, при проверке типа с помощью оператора is, таковы: int (32-битное целое), long (64-битное целое), single (число с плавающей запятой) и double (число с плавающей запятой двойной точности):

var i = 12; //int
var l = 12L; //long
var s = 12.2; //single
var d = 12.2D; //double

При использовании этих типов данных имейте в виду, что все 32-битные числа размещаются на стеке, а все 64-битные – уже создаются в куче.

Строки и символы

Символы (char) используются для хранения одного Unicode-символа и имеют внутреннее представление как 32-битное целое. Программистам C# должен быть хорошо знаком этот тип данных. Символы, так же как и 32-битные числа, создаются на стеке.

Строки (string) в Ela реализованы поверх стандартного типа .NET Framework System.String и всегда имеют Unicode-представление.

var c = 'a'; //char
var s = "Hello, world!"; //string

Строки и символы также поддерживают эскейп-последовательности: \r (перевод каретки), \n (новая строка), \t (символ табуляции), \" (вставка кавычки), \' (вставка одинарной кавычки), \uxxxx (вставка Unicode-символа) и другие.

Строки в Ela могут рассматриваться как массив, поэтому к ним применимы некоторые операции, которые можно совершать с массивами. Например, в циклах for-in они неявно приводятся к типу seq (об этом см. ниже), еще вы можете обращаться к отдельным символам внутри строки через индексер. Однако, как и в .NET, строки являются неизменяемым типом данных – поэтому для изменения строки, вам необходимо создать ее новый экземпляр. Также у строк есть встроенное свойство length, которое возвращает длину строки.

Булевы

Булевы – это целые числа, имеющие свой собственный литерал, которые могут принимать только два значения – true или false. Булевы – последний из примитивов Ela, который создается на стеке.

var x = true;
var y = false;

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

Кортежи

Кортежи (tuple) представляют собой доступный только для чтения гетерогенный массив из двух и более элементов. Вот так вы можете объявить в Ela кортеж:

let x = (0, 1, 2);

С кортежами можно работать как с массивами – например, обращаться к их элементам по индексу, однако нельзя изменять элементы кортежей. У кортежей есть встроенное поле length, позволяющее узнать длину кортежа.

Динамические индексированные массивы

Индексированные массивы (array) – это те же кортежи, но только позволяющие создавать последовательность из любого количества элементов и не являющиеся неизменяемой структурой данных. Индексированные массивы могут изменять свою длину:

let x = [| 0, 1 |];
x.add(2); //добавление нового элемента
x.remove(1); //удаление элемента с индексом 1
x.insert(0, 22); //вставка нового элемента по индексу 0
x.clear(); //удаление всех элементов

Как видно из примера, для добавления нового элемента в массив нужно использовать методы add и insert, для удаления – remove. Схожее поведение вы можете наблюдать и у стандартных коллекций в .NET Framework. Точно так же, как и в C#, обращение к несуществующему элементу массива приведет к ошибке.

Массивы также имеют встроенное поле length, позволяющее получить длину массива.

Связные списки

Однонаправленные связные списки (list) – это неизменяемая структура данных для хранения последовательностей элементов, которая хорошо подходит для рекурсивного анализа через сопоставление с образцом.

Списки можно объявлять так:

let x = [0, 1, 2];

Списки также могут конструироваться с помощью оператора-конструтора списков «::», который является правоассоциативным. Например, вышеприведенный код полностью аналогичен нижеследующему:

let x = 0::1::2::[];

Оператор «::» можно использовать и для добавления элемента в начало списка, при этом добавляемый элемент должен быть в левой части выражения, а сам список – в правой. Так как списки являются неизменяемой структурой данных, то добавление нового элемента фактически приводит к созданию нового списка.

Вы также можете складывать списки, используя оператор «+», однако имейте в виду, что это довольно затратная по времени выполнения операция.

Стандартный объект список имеет также встроенные поля length, head и tail, возвращающие, соответственно, длину, голову и хвост списка.

Записи

Записи (record) представляют собой последовательности значений, к элементам которых можно обращаться как по индексу, так и по имени:

let x = ( car: "Nissan", model: "Z", year: 2008 );
let year = x.year;

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

Программистам JavaScript записи наверняка напомнят тип данных «объект», имеющий в JavaScript схожий литерал. Программисты C# могут вспомнить об анонимных типах. Главным отличием записей от объектов в JavaScript является невозможность неявного добавления нового поля – если вы попробуете обратиться к несуществующему полю (неважно, на запись или на чтение), то получите ошибку:

let x = ( car: "Nissan", model: "Z", year: 2008 );
let year = x.year;
let period = x.warrantyPeriod; //Error ELA417: There is no such field as 'warrantyPeriod' in the target object.
x.warrantyPeriod = 2; //Error ELA417: There is no such field as 'warrantyPeriod' in the target object.

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

Последовательности

Последовательности (seq) представляют собой доступные только для чтения однонаправленные последовательности элементов. Ближайшим к ним аналогом может быть курсор или же тип IEnumerator из .NET Framework. В Ela нет специального литерала для последовательностей, однако списки, кортежи, записи, строки и массивы могут к ним приводиться, что позволяет обрабатывать их в одном ключе:

for (x in [0, 1, 2])
  cout x;

for (x in [| 0, 1, 2 |])
  cout x;

for (x in "abcdef")
  cout x;

В этих примерах происходит неявное приведение массива, строки и списка к типу seq. Вы также можете явно привести массив, строку, список или кортеж к данному типу, используя оператор приведения «:>». Например:

let t = (0, 1);
let seq = t :> seq;

Имейте в виду, что последовательности сохраняют свое состояние. Если вы в цикле получите первые два элемента последовательности и прервете цикл, то потом, если вы продолжите пользоваться тем же экземпляром последовательности, перебор элементов начнется сразу с третьего элемента. Когда последовательность доходит до конца, она автоматически переводится на начало. Также с помощью последовательностей в Ela могут быть представлены ленивые структуры данных (об этом см. ниже).

Функции

Функции являются самым важным типом данных в Ela. Прежде всего, функции в Ela являются первоклассными объектами, т.е. Ela поддерживает first-class functions, а это значит, что вы можете совершать с функциями те же самые операции, что и с любыми другими значениями. Основной синтаксис для объявления функций очень похож на синтаксис для лямбд в C# 3.0:

x -> x % 2

Приведенная выше функция принимает один параметр x и возвращает остаток при делении этого параметра на 2. Это пример так называемой анонимной функции – анонимной просто в силу того, что, как вы видите, ни с каким именем эта функция не связывается. Как объявить «неанонимную» функцию? Очевидно, что надо просто произвести связывание функции с именем. Например:

let func = x -> x % 2

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

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

let func(x) x % 2;

Два этих примера полностью равносильны.

Параметры функций являются в Ela неизменяемыми, поэтому такой код приведет к ошибке:

let f(x) x++; //Error ELA213: Unable to change a value of an immutable variable 'x'.

К тому же, как вы могли заметить, в функциях для возвращения значения не требуется ключевое слово return. Принцип прост – самое последнее выражение в функции возвращает значение. Если телом функции является блок, объявленный через фигурные скобки, то возвращать значение будет самое последнее выражение в этом блоке, и пр. Однако Ela поддерживает и оператор return, возвращающий значение в сочетании с принудительным выходом из функции. Единственное отличие return от того же C# или JavaScript в том, что после ключевого return обязательно должно идти выражение. Да, и конечно же, все функции в Ela всегда возвращают какое-либо значение, однако иногда это может быть значение типа unit.

Любая функция в Ela замыкается на родительский контекст, т.е. внутри функции вы можете использовать имена, объявленные во внешнем блоке:

let msg = "value is ";
let func(x) msg + x :> string;

В данном примере функция «func» захватывает переменную «msg», приводит параметр «x» к строке, используя оператор приведения «:>», производит конкатенацию строк и возвращает результат.

Функции можно также объявлять с помощью механизма частичного применения. Предположим, что у нас есть функция «sum», складывающая два значения:

let sum(x, y) x + y;

Мы можем создать новую функцию на основе функции «sum», которая будет работать для одного значения:

let sum2 = sum(_, 2);

Функция «sum2» принимает уже один-единственный параметр и складывает все числа со значением «2».

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

let reduce(seq, fun) {
  var el = ();
  for (e in seq) {
    if (el is unit)
      el = e;
    else 
      el = fun(el, e);    
  }  
  el
}

reduce([0, 1, 2, 3, 4], _ + _);

Данный пример реализует классическую функцию высшего порядка «reduce», которая благодаря использованию типа данных seq, может работать как со списками, так и с массивами. Функция «reduce» принимает в качестве параметре другую функцию для двух аргументов («fun»), которая вызывается для всех элементов последовательности – сначала для первого и второго, затем для своего предыдущего результата и для третьего элемента и пр. В качестве функции в примере используется обычный оператор сложения, который превращен в функцию для двух аргументов, используя специальный оператор «_».

При работе с функциями можно использовать операторы forward pipe («|>») и backward pipe («<|»), которые могут быть знакомы некоторым по языку F#. К примеру, представим, что у нас есть такой код:

let calc(x, y) x + y; //сначала вычисляем
let check(x) if (x > 0) x else 0; //вторым шагом делаем проверку
let print(x) cout "The result is " + x :> string; //и наконец выводим результат

let res = calc(2, 2);
let res2 = check(res);
print(res2);

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

print(check(calc(2, 2)));

Однако в таком случае пострадает читабельность кода. Forward pipe оператор позволит нам сохранить читабельность и лаконичность, причем читабельность будет даже выше, чем в первом случае:

calc(2, 2) |> check |> print;

С помощью backward pipe этот же код можно записать так:

print <| check <| calc(2, 2);

Однако в тех случаях, когда вам требуется часто вызывать одну и ту же цепочку функций, даже использование pipe-операторов приведет к дублированию кода. В таких случаях вы можете использовать операторы для композиции функций – forward composition («>>>») и backward composition («<<<»).

Например, так можно записать наш пример с использованием forward composition:

let f = calc >>> check >>> print;
f(2, 2);

Аналогично с использованием backward composition:

let f = print <<< check <<< calc;
f(2, 2);

Имейте в виду, что в данном случае вы по сути создаете новую функцию, связанную с именем «f», которую можно многократно использовать.

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

Например, цикл вида:

var x = 0;

while (x < 10) {
  cout x;
  x++;
}

Может быть переписан через рекурсивную функцию (и при этом отпадет необходимость в изменяемой переменной):

let rec(x) {
  cout x;
  when (x < 10) 
    rec (x + 1);
}

Как видите, функция «rec» захватила собственное имя «rec», объявленное в родительском контексте и использует его для того, чтобы вызвать саму себя.

В данном примере вы можете наблюдать и другой интересный момент, а именно хвостовую рекурсию. Хвостовая рекурсия – это вызов функцией самой себя, который является последней инструкцией в теле функции. Ela умеет оптимизировать хвостовую рекурсию. Благодаря этому, в вышеприведенном примере рекурсивный вызов будет убран и заменен на обычный условный переход. Фактически по логике исполнения пример с функцией полностью аналогичен примеру с циклом.

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

Полиморфные варианты

Эти типы данных схожим с теми, которые поддерживаются в языке OCaml. Если вы знакомы с концепцией алгребраических типов, то также найдете здесь немало общего. В отличие от алгребраических типов, полиморфные варианты не требуют предварительного объявления, можно сказать, что в вашей программе есть один-единственный алгебраический тип, конструкторы которого автоматически объявляются по первому месту использования:

let x = `Some(2);

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

let y = `None;

В таком случае круглые скобки можно опускать.

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

Имейте в виду, что, хотя создание полиморфного варианта похоже на вызов функции, конструктор варианта – это совсем не функция и не обладает особенностями функции (например, не является первоклассным объектом). Конструктор варианта стоит рассматривать скорее как специальный литерал.

Ela автоматически генерирует конструкторы для вариантов по первому месту их использования. Например:

let x = `Some(1); //создается новый конструктор `Some
let y = `Some(2); //используется ранее созданный конструктор `Some
let z = `Some(1, 2); //создается новый конструктор `Some для кортежа из двух элементов

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

По этой причине в Ela была введена дополнительная возможность – пользовательские конструкторы для вариантов. Синтаксис объявления пользовательских конструкторов похож на синтаксис объявления функций, однако их название должно начинаться с апострофа:

let `Some(x, y) (x, y);

Также конструкторы можно объявлять только используя ключевое слово let, использование ключевого слова var приведет к ошибке компиляции. Наконец, в отличие от функций, телом конструктора может быть исключительно литерал кортежа или записи. Вышеприведенный пример показывает, как можно описать простейший конструктор `Some для аргументов «x» и «y», создающий кортеж из двух элементов.

Опять-таки – пользовательские конструкторы не являются функциями, вы не можете вызывать их как функции, более того, все конструкторы находятся в отдельном пространстве имен, и у вас могут быть функции, совпадающие с ними по имени. Наконец, допустимо объявление нескольких конструкторов с одним именем и разным количеством аргументов (что недопустимо в случае обычных функций). Повторное объявление конструктора с уже используемым именем и количеством аргументов также не приведет к ошибке, а вызовет затенение предыдущего конструктора – независимо от того, был ли этот конструктор описан явно или же сгенерирован автоматически.

Например:

let x = `Some(1);  //создается новый конструктор
let y = `Some(2);  // используется существующий автосгенерированный конструктор
let Some(x) x + 1; // пример обычной функции
let `Some(x) (x, x * 2); // создается пользовательский конструктор, 
                         // затеняющий предыдущий
let z = `Some(3);  // используется пользовательский конструктор

cout x;
cout z;

Данный код выведет на консоль:

(>Some 1)
(>Some 3,6)

Также с помощью пользовательских конструкторов вы можете создавать полиморфные варианты на основе записей. Например:

let `Some(x) ( val: x, toString: () -> x :> string );
let x = `Some(2);
cout x.toString();

Ленивые значения

Ленивые значения (lazy) используются при отложенных и асинхронных вычислениях. Для того, чтобы получить значения типа lazy вы должны воспользоваться операторами lazy или async. Например:

let x = lazy 2 + 2;
let y = async 2 + 2;

В первом случае выражение, следующее после lazy, не вычисляется до тех пор, пока оно вам не потребуется. Во втором вычисление запускается сразу же, однако в отдельном потоке, а имя «y» связывается со специальным объектом типа lazy, значение в который будет записано после завершения потока (это удобно, если какое-то действие занимает много времени, например, вы ждете ответа по сети, и вам при этом не хочется блокировать работу всего приложения).

Тип lazy сам по себе нельзя использовать в каких-либо операциях, например, следующий код приведет к ошибке:

let x = lazy 2 + 2;
x + 2 //Error ELA409: Unable to perform operation '+' on types 'lazy' and 'int'

Для получения значения в Ela используется специальный оператор получить значение – «&». Например, чтобы сделать предыдущий пример рабочим, надо переписать его так:

let x = lazy 2 + 2;
&x + 2

В этом случае происходит вычисление выражения, объявленного с использованием оператора lazy.

Аналогично и с асинхронными значениями – но в их случае операция получения значения приведет к блокировке вызывающего потока до тех пор, пока не завершится асинхронное вычисление.

Раз полученное значение повторно не вычисляется – поэтому вы можете использовать оператор «&» несколько раз для одних и тех же значений.

Модули

Модуль (module) – это любой исполнимый файл на Ela, при этом названием модуля является по умолчанию название файла. Для того, чтобы открыть существующий модуль, нужно использовать конструкцию open. Предположим, что мы создали файл «Calc.ela» с таким вот кодом:

let add(x, y) x + y;
let sub(x, y) x – y;

Открыть данный модуль можно используя конструкцию open:

open Calc;
cout Calc.add(2, 2);

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

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

open Calc with add;
cout add(2, 2);

Вы также можете задать локальный на уровне вашего модуля псевдоним для имени:

open Calc with sum = add, minus = sub;
cout sum(2, 2) + minus(2, 2);

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

open MyLongModuleName as Calc;

Наконец, так как Ela позволяет реализовывать модули не только на самой себе, но и на .NET, вы можете указать из какой сборки нужно загружать модуль. Делается это так:

open Collection[elalib];

В данном примере «elalib» – это название сборки, реализованной на языке C#, в которой объявлен атрибут ElaForeignModuleAttribute, содержащий информацию об имени модуля («Collection») и реализующем его типе.

Вы, наверное, удивляетесь, почему описание модулей делается в секции, посвященной типам данных. Но дело в том, что наиболее интересная особенность модулей заключается в том, что модули являются первоклассными объектами – т.е. вы, к примеру, можете передать модуль в функцию как любое другое значение. Например, у нас есть модуль «List», который содержит одну-единственную функцию:

let head(list) 
  on x::_ -> `Some(x) on [] -> `None;

Данная функция возвращает вариант `Some с головой списка, если переданный в нее список не является пустым, и вариант `None, если список пуст. Вот как вы можете использовать данную функцию из другого модуля:

// открываем и исполняем модуль List (в файле List.ela)
open List;

// объявляем функцию func, которая в качестве аргумента принимает некий объект
// и вызывает у него функцию head
let func(mod, list) mod.head(list);

// передаем в функцию наш модуль в качестве объекта
func(List, [1, 2, 3]);

Результатом работы этого кода будет `Some(1).

Тип Unit

Тип unit более знаком некоторым программистам как void. Тип unit – это фактически пустой кортеж, экземпляр которого является единственным для всего вашего приложения:

let x = ();

Вы можете использовать unit так же, как и любой другой тип данных – его можно возвращать из функции, присваивать переменной и пр. Имейте в виду, что в Ela, в отличие от C#, отсутствует null, поэтому на плечи unit ложится также и его роль.

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

Общая информация

Сопоставление с образцом – это одна из главных возможностей Ela. Реализация сопоставления с образцом в Ela достаточно близка к тому, что вы можете увидеть в таких языках как OCaml, F#, Scala, Nemerle, однако Ela – это динамический язык, и данный факт вносит ряд своих особенностей.

Для тех, кто не знаком с концепцией сопоставления с образцом, можно сначала почитать о его реализациях в F# или Nemerle. В Ela вы найдете немало общего с этими языками. Вкратце, сопоставление с образцом – это языковая возможность для сравнительного разбора выражений. Любой код с использованием сопоставления с образцом можно «преобразовать» в цепочку из условных операторов – сопоставление с образцом позволяет делать то же самое, но с большим уровнем декларативности. Например, такой вот код:

let result = match (arr)
  on [| x, y, z |] -> x + y + z
  on _ -> 0

равносилен такому:

let result = 0;

when (arr.length == 3) 
    result = arr[0] + arr[1] + arr[2];

Как вы уже заметили, конструкция для сопоставления с образцом объявляется с помощью ключевого слова match, в скобках следует выражение, которое нужно подвергнуть разбору, а за ним – вхождения match или образцы. Фигурные скобки вокруг тела match не требуются. В целом синтаксис данной конструкции напоминает switch из С-подобных языков, однако match является куда более мощным механизмом.

В следующей секции перечислены образцы, которые поддерживает Ela.

Образцы

Образец «Или»

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

on 2 || 3 -> ...

Образец «И»

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

on 2 && 3 -> ...

Имейте в виду, что как и в случае аналогичных бинарных операторов, приоритет образца «И» выше, чем приоритет образца «Или».

Образец «as»

Сопоставление с данным образцом всегда проходит успешно. Образец «as» связывает все выражение с заданным именем. В указанном примере список разбирается на элементы голова и хвост, которые связываются с именами x и xs, а весь экземпляр списка связывается с именем list.

on x::xs as list -> ...

Образец «Литерал»

Данный образец сравнивает выражение с литералами примитивных типов – 32 и 64 битных целых, 32 и 64 битных вещественных чисел, строк, символов и булевых.

on 22 -> ...
on "string" -> ...
on true -> ...

Образец «unit»

Данный образец сравнивает выражение с экземпляром типа unit. Образец считается успешным, если сопоставляемое выражение является unit-ом. Данный образец равносилен проверка типа выражения: x is unit

on () -> ...

Образец «is»

Данный образец проверяет тип выражения. Если слева указывается имя, то происходит связывание этого имени со значением разбираемого выражения. В Ela поддерживаются следующие типы: int, long, single, double, bool, char, string, list, array, tuple, record, unit, seq, lazy и function. Сопоставление происходит успешно, если тип разбираемого выражения соответствует указанному.

on is int -> ...
on x is long -> ...

Образец «Переменная»

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

on x -> ...

Образец «Любое значение»

Данный образец ведет себя так же, как и образец «Переменная», с тем отличием, что связывания с переменной не происходит, и значение разбираемого выражения попросту игнорируется. Если этот образец является образцом верхнего уровня, то он обязательно должен идти в конце match-a – в противном случае все остальные вхождения match-a будут проигнорированы.

on _ -> ...

Образец «Кортеж»

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

on x, y, z -> ...
on (x, 2, 3) -> ...

Образец «Массив»

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

on [| x, y, z |] -> ...

Образец «Список»

Этот образец аналогичен образцу «Массив», но используется только для разбора связных список. Длина связного списка должна точно соответствовать указанному в образце количеству элементов. Образец вида «[ ]» сопоставляется с пустым списком.

on [x, y, z] -> ...
on [] -> ...

Образец «Голова-хвост»

Данный образец используется для разбора списка в том случае, если нам не известно общее количество элементов в списке. Образец выполняется успешно, если разбираемое выражение – список, который содержит как минимум один элемент. При этом первые элементы образца будут связаны с отдельными элементами списка, а последний – с остатком списка. В качестве остатка может также выступать и пустой список.

on x::xs -> ...
on x::2::(x2,y2)::y::xs -> ...

Образец «Вариант»

Этот образец используется для сопоставления с полиморфными вариантами, т.е. с кортежами или записями, который были созданы с помощью специального конструктора полиморфных вариантов. Если вы опускаете круглые скобки после названия конструктора, то образец будет выполнен успешно если название указанного в нем конструктора совпадает с названием конструктора разбираемого выражения, независимо от количества элементов в кортеже варианта. Если вы указываете круглые скобки, то для успешного совпадения должны быть перечислены все элементы варианта. Образец вида `Tagname() производит сопоставление с «пустым» вариантом.

on `Some(x) -> ...
on `Some -> ...

Образец «Любой вариант»

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

on `(x) -> ...

Образец «Поле»

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

on (Name = x, Age = 30) -> ...

Образец «Структура»

Данный образец аналогичен образцу «Поле», но не производит связывания или сравнения полей, а просто проверяет, что в разбираемом объекте присутствуют указанные поля.

on (:Name,:Age) -> ...

Образец «Взять значение»

Данный образец позволяет сравнить значение разбираемого выражения не с литералом, а со значением переменной, которая может быть объявлена в родительском блоке. При этом значением может выступать как «обычный» тип, так и тип lazy – в последнем случае произойдет его вычисление:

on &x -> …

Образец «Сравнение»

Данный образец позволяет использовать один из бинарных операторов сравнения («==», «!=», «>», «>=», «<» или «<=»). Вы можете производить сравнение с литералами или образцами «Взять значение», причем если в правой части образца указать имя, то будет произведено связывание разбираемого значение с этим именем. При желании имя справа можно опускать.

on [> 0, < 2, >= 10] -> …
on x != &y -> …

Конструкции для сопоставления с образцом

Match

Основная конструкция для сопоставления с образцом – выражение match. Образцы в ней могут быть вложенными. Причем каждое вхождение match-а создает свой лексический блок, поэтому имена переменных в разных вхождениях могут совпадать. Если при выполнении match-а не произошло успешное сопоставление ни с одним образцом, то будет сгенерировано исключение.

Match так же поддерживает guards:

on x::xs when (x > 2) -> ...

В данном случае связывание списка с именами «x» и «xs» произойдет только в том случае, если первый элемент списка больше 2.

Если все тело функции состоит из одного match-а, то самое объявление match можно опустить:

let tail(list)
on _::xs -> `Some(xs)
    on [] -> `None;

Также сокращенную форму для сопоставления с образцом поддерживает и конструкция для обработки исключений try/catch, что уже было показано в соответствующем разделе.

Is

Синтаксис оператора is похож на синтаксис аналогичного оператора C#, однако в C# он используется только для проверки типа. В Ela он может использоваться как для проверки типа (ведь для этого есть специальный образец), так и для других образцов. Если сопоставление происходит успешно, то оператор возвращает true, если нет – то false.

Например:

x is int;  
x is [0, 1, 2, 3];  
x is `Some(y);

Корректным является и такой код:

99 + 1 is 100;

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

Оператор is позволяет использовать следующие паттерны: собственно образец «is», или проверку типа (при этом достаточно просто указать название типа, как в примере выше), образец «Литерал», образец «Вариант», образец «Любой вариант», образец «Список» и образец «Массив». Вы также можете использовать и другие образцы, однако их нужно помещать в круглые скобки. Например:

x is (h::t)
x is (0, 1, 2)
x is ([0,1,2] || [3, 4, 5])

Имейте в виду, что, опять же в отличие от С#, оператор is задает свой лексический блок, поэтому такой код приведет к ошибке:

x is (h::ht);
cout ht; //Error ELA208: Undefined variable ‘ht’

Однако такой код будет полностью корректным:

when (x is (h::ht))
  cout ht;

Let и Var

Аналогично сопоставление с образцом можно использовать и при связывании значения с именем, используя конструкции let или var:

let func() (0, 1);
let (0, y) = func();
let x::xs = [0, 1, 2];

Данная конструкция позволяет использовать образцы «Переменная», «Список» и «Массив», а также и другие образцы, которые уже нужно помещать в круглые скобки:

let (firstName = x, lastName = y) = (firstName: "John", lastName: "Smith");

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

For

Как уже упоминалось, сопоставление с образцом можно использовать в циклах for-in (при этом циклы for-to и for-downto поддерживают только guard-ы). Например:

let list = [ [0,1], [0,2], [0,3] ];

for ([0,x] in list) 
  cout x;

Сопоставление с образцом в циклах for-in позволяет использовать следующие образцы: «Переменная», «Литерал», «Вариант», «Любой вариант», «Список», «Массив», а также и другие образцы уже нужно помещать в круглые скобки:

for ((0::xs) in list)
  cout xs;

Вы также можете комбинировать сопоставление с образцом с условиями (guards):

for ((0::x::xs) when (x > 0) in list)
  cout xs;

Если сопоставления с образцом не происходит, то код внутри блока for-in не выполняется. Допустим, если в вышеприведенном примере ни один их элементов списка не начинаются с 0, то код «cout xs» никогда не будет выполнен.

И еще несколько полезных вещей

Comprehension

Непереводимый на русский язык термин сompre­hen­sion используется в ряде языков для обозначения конструкций, используемых для формирования списков или массивов. В языках вроде Haskell и Nemerle синтаксис для comprehension соответствует математической set builder нотации. Ela же пошла другим путем – в ней отсутствует специальный синтаксис для comprehension, однако вместо него можно использовать обычные циклы.

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

for (x to 10) 
  cout x;

Как его превратить в list comprehension? Достаточно написать по краям квадратные скобки:

[ for (x to 10) x ]

Аналогично array comprehension:

[| for (x to 10) x |]

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

[ 
  for (x when x % 2 == 0 to 10) 
    for (y = 10 when y % 2 != 0 downto 0) 
         (x, y)
]

Подобный подход схож с тем, который используется в языке F#.

Генераторы

Генераторы – это специальные функции, которые, в отличие от обычных функций, могут возвращать значение несколько раз. Большинству .NET программистов концепция генераторов больше знакома по итераторам, которые появились в C# 2.0. Ela использует сходный синтаксис:

let gen() for (x to 10) yield x;
let seq = gen();

Функция «gen» – это генератор. Результатом выполнения данной функции является объект типа seq (который описывался выше).

Ela автоматически распознает, что функция является генератором, если хоть раз в теле функции встречается ключевое слово yield. Имейте в виду, что никакого другого способа вернуть из генератора значение, кроме как использовать оператор yield, нет. К примеру, использование обычного return приведет к ошибке компиляции.

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

Вместо заключения

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

На настоящий момент Ela распространяется под лицензией LGPL v. 2.1, а это означает, что, так как весь язык реализован в виде одной динамической библиотеки, вы можете использовать ее в составе проприетарных проектов, однако если вы хотите внести в исходный код какие-либо исправления, то стоит поделиться этими исправлениями со мной.

Вместе с Ela распространяется также и утилита командной строки Ela Console (elac.exe), которая подпадает под то же лицензионное соглашение, что и сам язык. Вы можете использовать elac для запуска файлов с кодом Ela (обычно имеющих расширение *.ela) на выполнение, а также как интерактивную среду (для этого достаточно запустить elac без параметров). Консоль умеет делать много полезных вещей, поддерживает отладку кода по идеологии REPL и пр., позволяет настраивать линкер и компилятор – чтобы узнать обо всех возможностях elac запустите ее с ключиком «–help». Также, что немаловажно, если вы создадите в том же каталоге, где установлена elac, стандартный конфигурационный файл .NET, то все настройки из раздела appSettings будут интерпретироваться elac так же, как и параметры командной строки. Наконец, elac, как и сам язык Ela, может исполняться под управлением .NET 3.5/4.0 или Mono 2.0.

Если у вас есть какие-либо вопросы или предложения по языку Ela, то можете написать в форум на RSDN или же задать их мне лично (почтовый адрес есть в карточке пользователя на RSDN).


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

Ваши предложения и комментарии мы ожидаем по адресу: mag@rsdn.ru
Copyright © 1994-2002 Оптим.ру