Сообщений 41    Оценка 3615        Оценить  
Система Orphus

Nemerle

Авторы: Сергей Туленцев
Владислав Чистяков

Источник: RSDN Magazine #1-2006
Опубликовано: 23.05.2006
Исправлено: 15.04.2009
Версия текста: 1.0
Введение
О языке
История и авторы
Что же такое Nemerle?
Выражения
Локальные функции
Функции как значения
Частичное применение и другие операции над функциями и операторами
Декомпозиция функций
Объявление локальных переменных
Вывод типов для локальных конструкций
Кортежи (tuples)
Списки
Другие типы и коллекции Nemerle
Сопоставление с образцом (pattern matching)
Образец «конструктор»
Оператор «try»
Рекурсия и производительность
Функциональный подход
Программирование с использованием аккумулятора
Другие операторы
Другие макросы
Литералы
Пару слов о будущем
Вместо заключения

ПРИМЕЧАНИЕ

Компилятор языка Nemerle находится на прилагаемом CD ROM.

Введение

Конкуренция на рынке программного обеспечения не угасает, а наоборот, становится все жестче и жестче. Развивающиеся страны (читай – Китай и Индия) постепенно увеличивают свое присутствие на рынке аутсорсинга, и там, где у них не выходит взять верх умением, берут числом и ценой. Отечественный программист отличается от наших восточных соседей наличием серьезной (можно сказать, академической) школы, а также выработанными суровыми российскими условиями сообразительностью и гибкостью ума (по крайней мере, на это хочется надеяться). Поэтому, чтобы победить в конкуренции с дешевой рабочей силой, нужно использовать инструменты, позволяющие задействовать имеющиеся преимущества. Увы, мэйнстрим-языки типа Java, C++, C# и VB – не те инструменты, которые необходимы для победы в этой нелегкой борьбе. C++, при всей его гибкости, все же недостаточно гибок, и скорость разработки на нем очень низка. К тому же этот язык не прощает ошибок и требует огромных усилий при тестировании. Остальные упомянутые языки, хотя и являются замечательными и простыми в использовании языками программирования, именно этими качествами и плохи в данной ситуации, так как не предоставляют мощных языковых средств, позволяющих радикально упростить решение сложных задач, но в то же время легки для освоения и способствуют снижению порога вхождения в профессию.

Одним словом, назрела насущная необходимость в языках программирования, которые, с одной стороны, более гибки и выразительны, чем C++, а с другой – так же удобны, безопасны и интуитивны, как Java, C# и VB. То есть нужен новый мэйнстрим-язык. Многие пытались и пытаются создать идеальный язык, но пока результаты не впечатляют. Об одной из таких попыток (язык Scala) мы уже рассказывали. В данной статье мы расскажем еще об одном языке, который, пожалуй, еще ближе подошел к нашему представлению об идеале.

О языке

Nemerle – это новый гибридный язык для платформы .NET Framework.

Если придирчиво рассмотреть все возможности языка, то, по сути, в нем нет ничего революционно нового. Все возможности в том или ином виде уже встречались в других языках программирования. Так зачем же мы решили посвятить этому языку так много времени и внимания? Дело в том, что, по нашему мнению, Nemerle удачно сочетает в себе множество лучших идей и возможностей, придуманных на данный момент. При этом язык остается очень стройным и понятным, а код, написанный на нем, легко читается. Как и .NET Framework (или платформа Ява), Nemerle, не привнося практически ничего нового в компьютерную науку, является чем-то новым сам по себе. К тому же, ничего нового в нем нет, только если сравнивать его отдельные возможности с отдельными возможностями довольно экзотических языков (в основном функциональных). По сравнению же с мэйнстрим-языками (C++, Java, C# и VB) Nemerle несет в себе большой и приятный список новшеств. Уникальное сочетание возможностей дает некий кумулятивный эффект, который позволяет говорить о Nemerle как о событии в области компьютерной науки.

История и авторы

Проект Nemerle был начат в январе 2003 года в университете города Вроцлав, Польша. Главные разработчики Nemerle – три аспиранта Вроцлавского университета. Лидером команды является Михаль Москаль (система вывода типов – тема его диссертации). Система макросов и расширяемый парсер – детище Камила Скальски. Основная работа по написанию механизма кодогенерации и сопоставления с образцом (pattern matching) была проделана Павлом Олшта.

Что же такое Nemerle?

Про Nemerle нельзя сказать, что он является чистым развитием другого языка, как это можно сказать про C++ (произошедшем от C) или C# (унаследовавшем большую часть возможностей от Java). Nemerle является полноценным гибридным языком, скорее, впитавшим идеи и подходы других языков, нежели базирующимся на них.

Если говорить кратко, то можно сказать, что Nemerle – это гибрид C# (ООЯ), ML (функционального языка со строгой статической типизацией) и макросов Lisp-а (не путать с макросами C/C++) в качестве подсистемы метапрограммирования.

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

Nemerle является статически типизированным языком, как ML или C#. Но, в отличие от ML-подобных языков, Nemerle допускает основанную на компонентном подходе динамику, и свойственную .NET CLR. Собственно, все компонентные возможности в Nemerle обуславливаются именно тем, что язык работает поверх .NET (или Mono). Так, Nemerle позволяет динамически подгружать сборки, создавать экземпляры неизвестных во время компиляции типов, производить динамический вызов методов (через механизм рефлексии). В общем, в компонентном плане Nemerle ничем не отличается от C#.

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

По сути, проще перечислить отличия Nemerle в поддержке ООП, чем совпадающие черты:

1. При декларации переменных их типы указываются не перед именем переменой, а сзади (отделяя его от имени двоеточием):

Декларация переменной или поля в C#:

public static int variable;
string stringVariable = "Инициализирующий объект";

Декларация переменной или поля в Nemerle:

public static mutable variable : int;
mutable stringVariable : string = "Инициализирующий объект";

Как видите, объявление переменных в Nemerle больше похоже на Паскаль или, точнее, на OCaml (но последний, по всей видимости, знаком меньшему числу программистов).

2. По умолчанию все переменные в Nemerle являются неизменяемыми. Например, объявление поля на Nemerle:

variable : int;

будет эквивалентно следующему объявлению C#:

readonly int variable;

Чтобы объявить изменяемую переменную, нужно начать объявление с ключевого слова mutable:

mutable variable : int;

3. При создании экземпляра класса не надо указывать ключевое слово «new»:

new MyClass() // C#
MyClass()     // Nemerle
throw new ArgumentException("foo") // C#
throw ArgumentException("foo")      // Nemerle

4. В Nemerle немного по-другому ведет себя конструкция using. Во-первых, она «открывает» не только указываемое пространство имен, но и все вложенные в него пространства, так что к ним можно обращаться без полного квалифицирования:

using System;
...
x : Collections.Hashtable(); // нормально в Nemerle и ошибка в C#

Во-вторых, using в Nemerle позволяет получить доступ не только к внутренностям пространства имен, но и к внутренностям классов (их статическим членам и вложенным классам):

using System.Console; // Получаем доступ к внутренностям класса Console.
WriteLine("Hello, World!"); // Вызов статического метода класса Console.

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

5. Приведение типов. В Nemerle есть две конструкции, связанные с приведением типов. Уточнение типа (можно также трактовать как эскалацию типа):

выражение : тип

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

Конструкция «выражение : тип» как бы указывает, что тип выражения или переменной должен быть таким-то. Если выражение не соответствует этому типу, компилятор сгенерирует ошибку во время компиляции. Если по тем или иным причинам приведение типов не может быть осуществлено во время компиляции, и требуется попытаться произвести приведение типов во время исполнения, или если приведение типов приводит к потере точности (например, int -> char), то нужно использовать другую конструкцию приведения типов:

выражение :> тип

В случае неудачи это выражение сгенерирует исключение System.InvalidCastException(). Если компилятор способен вычислить тип выражения, будет выдана ошибка во время компиляции.

Наличие двух операторов, «строгого» и «динамического», позволяет более четко декларировать свои намерения и уменьшить риск непредвиденного поведения во время исполнения.

6. Вместо введенных в C# 2.0 статических классов используется конструкция «module». Как и в статическом классе C# 2.0, все методы модуля являются статическими, но, в отличие от C# 2.0, не требуется указывать перед каждым методом модуля модификатор static, что делает код более читабельным и привычным для тех, кто не знаком с особенностями C#.

7. Приложение C# обязано иметь класс, содержащий статический метод Main(), так как он является точкой входа приложения. Nemerle может вести себя точно так же, но позволяет кроме этого объявлять метод Main() внутри модуля или даже вовсе обходиться без явной точки входа. При этом тело одного из файлов проекта может напрямую содержать выражения. Совокупность выражений, расположенных в глобальной области видимости, является телом подразумеваемой функции Main(). Вот варианты классического «Hello, World!» на C#:

class Applicaion
{
  static void Main()
  {
    System.Console.WriteLine("Hello, World!");
  }
}

и на Nemerle. Вариант 1:

System.Console.WriteLine("Hello, World!");

Вариант 2:

module Applicaion
{
  Main() : void
  {
    System.Console.WriteLine("Hello, World!");
  }
}

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

8. По-другому организована работа с массивами. Тип массива описывается с помощью ключевого слова array и идущего за ним в квадратных скобках типа элемента массива:

mutable a : array[int]; // Nemerle 
int[] a;                // C#

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

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

array[1, 3, 5, 10]; // Nemerle (тип элемента выводится компилятором)
new int[] { 1, 3, 5, 10 }; // C#

Как и в C#, в Nemerle поддерживаются многомерные и вложенные массивы. Синтаксис декларации вложенных массивов прост и понятен. Вот пример объявления и инициализации массива массивов целых:

_jaggedArray1  : array[array[int]] = array[array[1, 2, 3], array[4, 5]];

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

_multiDimArray : array[2, int] = array.[2][[1, 2, 3], [4, 5, 6]];

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

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

array(10); // Nemerle (тип выводится из типа переменной или ее использования)
array(2, 4); // Nemerle – двумерный массив (тип также выводится)
new int[10]; // C#
new int[2, 4]; // C# - двумерный массив

По умолчанию массивы инициализируются значениями null или 0 (в общем, память заполняется нулями). Это стандартное поведение CLR.

В общем-то, поведение вложенных массивов и многомерных массивов сходно с их поведением в C#.

9. Связанные списки. В Nemerle, в отличие от C#, есть встроенная поддержка связанных списков. Это обусловлено тем, что списки являются очень важным элементом функционального стиля программирования, так как их очень удобно обрабатывать с помощью рекурсивных вызовов. Более подробно о списках будет рассказано далее, а пока позвольте продемонстрировать, как объявить, создать и проинициализировать список. Так выглядит декларация поля типа «список»:

mutable seq : list[int];

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

mutable seq1 : list[int] = [1, 3, 5, 10];
mutable seq2 = [1, 3, 5, 10]; // То же с использованием вывода типов.
mutable seq3 : list[int] = [];

Или с помощью присвоения переменной значения другой переменной:

mutable seq = otherSeq;

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

То, что списки являются неизменяемыми, делает их поведение похожим на поведение value-типов или .NET-строк.

Спискам посвящен отдельный раздел этой статьи (см. ниже).

10. Объявление конструкторов. В C# конструктор носит имя класса, что создает некоторые неудобства при рефакторинге или копировании кода между классами. В Nemerle конструктор называется this:

class Foo
{
  public Foo(int x)     { ... } // Конструктор в C#.
}

class Foo
{
  public this(x : int) { ... } // Конструктор в Nemerle.
}

Отличается и вызов базового конструктора или конструктора с другой сигнатурой. Он располагается прямо в теле конструктора и выглядит как вызов метода.

11. Финалайзер. В C# объявление финалайзера имеет синтаксис, сходный с синтаксисом деструктора в C++. Это часто вызывает проблемы у C++-программистов, переходящих на C#. От деструктора они ждут аналогичного поведения, и очень удивляются, когда узнают, что поведение «деструктора» в C# сильно отличается от поведения деструктора в C++.

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

class Foo
{
  ~Foo() { ... } // Финалайзер в C#.
}

class Foo 
{
  protected override Finalize() : void { ... } // Финалайзер в Nemerle.
}

Синтаксис финалайзера в Nemerle значительно более громоздок, чем в C#, но, пожалуй, в данном случае это оправдано.

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

В этом аспекте Nemerle ведет себя аналогично VB.NET.

12. Параметры generic-типов. Тут несколько отличий. Во-первых, вместо угловых скобок для обрамления списка параметров типов используются квадратные скобки. Во-вторых, имена параметров типов могут начинаться с апострофа:

class A<T1, T2>  // C#
{
  readonly T1 x;
  T2 y;
}

class A['t1, T2] // Nemerle
{
  x : 't1;
  y : T2;
}

Собственно, любые идентификаторы в Nemerle могут содержать апостроф, так что это даже не особенность параметров generic-типов.

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

Интересно, что сначала в Nemerle тоже использовались угловые скобки.

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

13. Явная реализация методов интерфейсов. В C# для этой цели используется следующий синтаксис:

class C : Interface1
{
  void Interface1.Method() { }
}

В Nemerle принято другое решение. Решение опять же позаимствовано из VB.NET. Для указания того, что метод реализует интерфейс, может использоваться ключевое слово implements, идущее за сигнатурой метода:

class C : Interface1, Interface2
{
  public MethodImpl() : void implements Interface1.Method, Interface2.Method
  {
    WriteLine("I2_Method();");
  }
}

При этом имя и модификатор прав доступа метода может быть любым.

Откровенно говоря, вряд ли эта возможность будет востребованной. По крайней мере я за все время программирования на C# ни разу не жалел об ее отсутствии. Но, как говорится, пустячок, а приятно. :)

14. Индексаторы. В C# допускается делать только перегрузку безымянных индексаторов. Nemerle в очередной раз копирует расширенное поведение с VB.NET, вводя возможность создания именованных индексаторов. То, что ключевое слово this уже используется для конструктора, заставило использовать для задания безымянного индексатора слово «Item» (опять же, как в VB):

using System.Console;

class C
{
  public Item[i : int] : int { get { i + 1 } }
  public NamedIndexer[i : int] : string { get { i.ToString() } }
}

def c = C();

WriteLine(c[1]);
WriteLine(c.NamedIndexer[3]);

В общем-то, с помощью атрибута IndexerName и в C# есть возможность создать именованный индексатор, но при этом есть ряд проблем. Радует только одно: данная возможность фактически не нужна. Так что можно сказать, что это еще один пустячок, от которого становится приятно, но не более.

15. Значения по умолчанию для параметров (default parameters). Это еще одно заимствование из VB.NET. Многие находят данную возможность удобной, но нужно понимать, что если ваш код будет вызываться из C#, не поддерживающего данную возможность, то могут возникнуть неприятности, так как C#-программистам придется каждый раз заполнять значения этих параметров явно. Так что по возможности лучше избегать использования этой возможности в публичных интерфейсах. Зато во внутреннем коде она может кое-где сократить объем работ.

Вот пример использования этой возможности:

Method(x : int, y : int = 2, z : bool = false) : void
{
  print($"x=$x y=$y z=$z \n");
}
ПРИМЕЧАНИЕ

В данном примере задействована, пожалуй, самая интересная возможность – макросы Nemerle. Макросы Nemerle это отдельная и очень большая тема, и, наверное, не стоит начинать рассказ о языке с нее. Но применение макросов порой делает код настолько чистым и понятным, что мы решили использовать, по крайней мере, один из них в примерах. Суть этого макроса очень проста. Строка, начинающаяся со знака «$», может иметь в себе, скажем так, активные области (splices). Если внутри строки встречается одиночный символ «$», и дальше идет выражение, то его значение вычисляется и преобразуется в строку путем вызова у результирующего объекта метода ToString(). В качестве выражения может выступать идентификатор или произвольное выражение, взятое в скобки.

16. Обращение к параметрам по именам (аналогично тому, как это происходит в атрибутах C#). В Nemerle можно задавать параметры функций не в том порядке, в котором они перечислены в декларации функции. Это позволяет, с одной стороны, улучшить понятность кода (так как имена параметров могут прояснить смысл происходящего), а с другой, избавиться от необходимости задавать значения параметров, имеющих значения по умолчанию, если нужно задать значение только части из них. Вот как может выглядеть вызов метода описанного в предыдущем пункте:

Method(z = true, x = 3);

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

x=3 y=2 z=True

17. ref- и out-параметры. Как и C#, Nemerle поддерживает передачу и возврат параметров по ссылке. Ключевые слова ref и out являются частью описания типа. Поэтому в Nemerle их принято писать после двоеточия. В общем-то, отличия здесь именно в том, как декларируется тип, а не в ref и out, но C#-программист может растеряться в данной ситуации. Поэтому лучше всего будет просто продемонстрировать декларацию таких параметров:

Method(i : ref int) : void { ... } 
Method(i : out int) : void { ... }

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

18. Запись enum-а. Во многих случаях, где требуется перечислить некоторые элементы, в Nemerle, в отличие от C#, применяется не отделение запятыми, а своеобразный инфиксный синтаксис, использующий знак «|». Это приводит к тому, что элементы выглядят единообразно, и хорошо выделяет отдельные элементы. Конструкция enum тоже использует этот подход. Вот как выглядит enum в Nemerle:

[System.Flags]
enum States
{
  | Working = 0x0001
  | Married = 0x0002
  | Graduate = 0x0004
}

Во всем остальном enum в Nemerle ничем не отличается от enum в C#.

19. Варианты (variants) – это мощный, универсальный тип данных в Nemerle. В простейшем случае они очень похожи на перечисления C#:

// C
enum Color
{
  Red, 
  Yellow, 
  Green 
}

//  Nemerle
variant Color
{
  | Red
  | Yellow
  | Green
}

Но в более сложном случае варианты более похожи на иерархию классов:

variant RgbColor
{
  | Red
  | Yellow
  | Green
  | Different
    {
      red : float;
      green : float;
      blue : float;
    }
}

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

blue : RgbColor.Different = RgbColor.Different(0f, 0f, 1f);
red  : RgbColor.Red       = RgbColor.Red();

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

В ОО-мире подобный тип можно было бы реализовать с помощью наследования. Собственно, так как варианты не поддерживаются .NET напрямую, компилятор Nemerle переписывает варианты в приблизительно такую конструкцию:

// Генерируется компилятором Nemerle (декомпиляция в C#).
[Variant("RgbColor.Red,RgbColor.Yellow,RgbColor.Green,RgbColor.Different")]
internal abstract class RgbColor 
{
  public abstract override int _N_GetVariantCode();

  // Вложенные типы
  [VariantOption]
  internal protected sealed class Different : RgbColor
  {
    public Different(float red, float green, float blue)
    {
      this.blue = blue;
      this.green = green;
      this.red = red;
    }

    public override int _N_GetVariantCode() { return 3; }

    // Fields
    [Immutable] public float blue;
    [Immutable] public float green;
    [Immutable] public float red;
  }

  [ConstantVariantOption]
  internal protected sealed class Green : RgbColor
  {
    public static RgbColor.Green _N_constant_object_generator()
    {
      return RgbColor.Green._N_constant_object;
    }

    public override int _N_GetVariantCode() { return 2; }

    [Immutable]
    public static RgbColor.Green _N_constant_object = new RgbColor.Green();
  }

  [ConstantVariantOption]
  internal protected sealed class Red : RgbColor
  {
    public static RgbColor.Red _N_constant_object_generator()
    {
      return RgbColor.Red._N_constant_object;
    }

    public override int _N_GetVariantCode() { return 0; }

    [Immutable]
    public static RgbColor.Red _N_constant_object = new RgbColor.Red();
  }

  [ConstantVariantOption]
  internal protected sealed class Yellow : RgbColor
  {
    // Methods
    public static RgbColor.Yellow _N_constant_object_generator()
    {
      return RgbColor.Yellow._N_constant_object;
    }

    public override int _N_GetVariantCode() { return 1; }

    // Fields
    [Immutable]
    public static RgbColor.Yellow _N_constant_object = new RgbColor.Yellow();
  }
}

Члены классов, начинающиеся с префикса «_N_» – это средство оптимизации, позволяющее компилятору Nemerle в некоторых случаях генерировать более быстрый код. Так _N_GetVariantCode() возвращает целочисленный идентификатор, уникальный в пределах варианта. Этот идентификатор автоматически используется компилятором в конструкциях Nemerle, аналогичных switch из C#. _N_constant_object и _N_constant_object_generator() создаются для подтипов варианта, не имеющих полей. При попытке создать их экземпляры компилятор заменит обращение к конструктору обращением к заранее созданному экземпляру подтипа. Это экономит память и повышает производительность.

По сути, вариант – это синтаксический сахар для некоторого паттерна проектирования. Если реализовывать его вручную, нужно самому написать конструкторы, пометить поля как публичные (public) или же написать соответствующие свойства. И использовать его будет тоже довольно трудно, так как придется осуществлять множество проверок типа во время выполнения. С другой стороны, Nemerle предоставляет мощный механизм для работы с вариантами – сопоставление с образцом (pattern matching, будет рассмотрен далее) и т.п.

20. Заменитель имени параметра или переменной «_». Если в некоторых местах нужно задать имя, но это имя не важно для остального кода, то вместо этого имени можно указать «_». Аналогичными свойствами обладают и имена, начинающиеся с _. Этот прием также в некоторых случаях предотвращает генерацию предупреждений о том, что параметр не используется (компилятор C# от Microsoft не выдает подобных предупреждений, что может скрыть некоторые ошибки). Например, вот как можно описать параметры метода, являющегося обработчиком события:

SomeHandler(_ : object, _ : EventArgs) : void { ... }

Кроме того, «_» можно применять, если нужно проигнорировать возвращаемое значение функции. Например, на форуме RSDN, посвященном .NET, нередко встречается вопрос:

«Почему не работает следующий код:

string str = "Test 1";
str.Replace("1", "2");
Console.WriteLine(str);

»

Ответ на него заключается в том, что string – это неизменяемый тип (immutable). Все методы типа string возвращают новую копию строки. Неопытные программисты не знают этого, и забывают присвоить измененное значение переменной.

В отличие от компиляторов C#, компилятор Nemerle выдает предупреждение, если программист игнорирует значение функции. Это предотвращает ряд ошибок, нередко встречающихся у C#-программистов.

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

_ = strBuilder.Replace("1", "2");

Кроме того, в стандартной библиотеке есть макрос ignore(), который также позволяет игнорировать возвращаемое значение. Он позволяет более явно декларировать свои намерения:

ignore(strBuilder.Replace("1", "2"));

21. Псевдонимы типов. Многие из тех, кто программировал на C#, сталкивались с тем, что в некоторых случаях типы являются настолько длинными, что при прямом их задании код вылезает за допустимые соглашениями о кодировании нормативы (обычно 78-80 символов). Чтобы избежать этого, в C# можно с помощью using давать псевдонимы типов. Вроде бы все замечательно? Не совсем. Когда один и тот же псевдоним нужно использовать в большом количестве модулей, приходится заниматься Copy/Paste-ом директив задания псевдонимов (ведь они видны только в одном файле).

Nemerle позволяет (кроме директивы using) использовать для задания псевдонимов типов конструкцию:

type ПсевдонимТипа = ИмеющийсяТип;

Тип, объявленный таким образом, может использоваться в других файлах, так как он размещается в пространстве имен, а не относится к файлу как псевдоним, задаваемый using-ом.

Интересно, что с помощью этой возможности можно объявлять даже открытые generic-типы:

type C[T] = Nemerle.Collections.Queue[T];

def s = C.[int](); // «.[int]» - это аргументы типа.
s.Push(123); 
System.Console.WriteLine(s.Pop());

22. В Nemerle имеется множество интересных расширений, позволяющих программировать более продуктивно. В основном они сделаны на базе подсистемы метапрограммирования Nemerle. Среди того, что хочется выделить: Design by contract (декларативный контроль за правильностью кода), асинхронное программирование, работа со строками, SQL и многим другим. Все это заслуживает отдельного рассмотрения.

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

#pragma indent

using System
using System.IO
using System.Text

namespace Nemerle.IO
  public class PipeWriter : TextWriter
    output_writer : TextWriter
    filter : string -> string
    line : StringBuilder = StringBuilder ()
    
    /// [filter] is called for each line of the input.
    public this (output_writer : TextWriter, filter : string -> string)
      this.filter = filter
      this.output_writer = output_writer
...

Скорее всего, эта возможность появилась в процессе разработки интерактивного интерпретатора Nemerle. Вводить с консоли C-подобные конструкции не очень-то удобно, вот и был придуман альтернативный синтаксис.

***

Это список отличий на уровне деклараций языка, то есть за вычетом содержимого методов и свойств. Велик ли он? Думаю, не очень. Все же сходств куда больше. Ведь почти все остальное в Nemerle совпадает с C#. Сохранился и ООП-дух языка, если так можно сказать. По крайней мере, нет ощущения, что попал в другую среду. Даже переход с C# на очень близкий по духу VB.NET пройдет, пожалуй, куда сложнее.

Выражения

До сих пор речь шла об отличиях Nemerle от C# на уровне деклараций типов и их членов. На этом уровне Nemerle выглядит как несколько улучшенный C#. Однако намного более интересные отличия кроются на следующем уровне - уровне кода методов и свойств.

Чтобы C#-программисту было проще понять отличия Nemerle от C#, сначала имеет смысл напомнить, из чего состоят тела методов и свойств в C#.

C# является так называемым императивным языком программирования. Это означает, что когда программист пишет программу, он описывает поток выполнения программы. При этом в его распоряжении есть две главные сущности – выражения (expressions) и предложения (statements). Выражения могут состоять из арифметических и других операторов, вызовов функций (обязательно возвращающих значения) и немногочисленных операторов вроде «? :», которые можно использовать в выражениях. Такие конструкции, как if/else, switch, for, while и т.п., являются предложениями. Предложения могут содержать в себе выражения, но выражения не могут напрямую содержать в себе предложения (хотя могут содержать подвыражения). Такое положение вещей затрудняет использование в C# функциональной парадигмы. Попробуйте, например, воспользоваться switch-ем внутри выражения, то есть написать что-то вроде этого:

if (switch (a) { case 1: true break; default: false break; })
  Foo();
else
  Bar();

Ничего не выйдет. Компилятор просто подумает, что вы сошли с ума. А вот в Nemerle аналогичная конструкция считаются в норме вещей:

if (match (a) { | 1 => true | _ => false })
  Foo();
else
  Bar();
ПРИМЕЧАНИЕ

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

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

Хорошая поддержка функциональной (см. http://en.wikipedia.org/wiki/Programming_paradigm) парадигмы не означает, что в Nemerle нет таких конструкций как if/else, for, while и т.п. Они есть, но все они могут быть использованы в выражениях. Это накладывает огромный отпечаток на дизайн языка.

Чтобы в Nemerle можно было использовать и обычный императивный стиль, язык снабжен двумя особенностями. Во-первых, в нем есть блоки, обрамляемые привычными для C-подобных языков фигурными скобками. Блок состоит из списка подвыражений, последнее из которых рассматривается как значение, возвращаемое блоком. Во-вторых, выражение может возвращать тип void. Это позволяет реализовать такие императивные конструкции, как циклы (которые, по сути, не должны ничего возвращать). Вот как выглядит функция, возвращающая максимальное из двух переданных значений:

Max(x : int, y : int) : int
{
  if (x > y) x else y
}

Заметьте, что отсутствует оператор return, а после x и y нет точек с запятой. Возвращаемым значением функции Max() является значение выражения «if (x > y) x else y» которое скорее соответствует выражению C# «x > y ? x : y», нежели предложению if/else этого языка. Более того, в Nemerle if не может идти без else.

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

Max(x : int, y : int) : int
{
  when (x > y)
    x;
  y;
}

Здесь «x» не будет использован как возвращаемое значение функции, а будет просто проигнорирован! Если бы вместо «x» имелись какие-то императивные действия (изменение значений переменных, вызов процедур и т.п.), то код был бы осмысленным, но в таком виде он является ошибкой. Nemerle предупреждает программиста, когда результат вычисления не используется в дальнейшем коде, так что если не игнорировать предупреждения, то особых проблем быть не должно.

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

Nemerle не поощряет императивный стиль, однако он не так ортодоксален в этом вопросе, как другие функциональные языки. Если вы хотите воспользоваться императивными конструкциями вроде return, break или continue, можно подключить пространство имен Nemerle.Imperative, в котором объявлены эти операторы. Многие могут задаться вопросом: «Что значит «подключить»?». Дело в том, что данные конструкции реализованы в Nemerle посредством макросов. А так как видимость макросов в Nemerle регулируется пространствами имен, то для того, чтобы воспользоваться данными конструкциями, нужно добавить в начало файла (или пространства имен) директиву «using Nemerle.Imperative;». Вот как будет выглядеть метод Max() в императивном стиле:

using Nemerle.Imperative;
...
Max(x : int, y : int) : int
{
  when (x > y)
    return x;

  return y;
}

Надо отметить, что использование операторов return, break или continue никак не влияет на возможность писать в функциональном стиле. Директива «using Nemerle.Imperative;» всего лишь дает возможность использовать эти операторы.

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

Забегая вперед, скажу, что все расширенные конструкции (вроде if, when, while, for, foreach) в языке на самом деле реализованы с помощью макросов, а все базовые выражения языка всегда возвращают значение некоторого типа.

А пока что приступим к изучению отличий Nemerle в области выражений.

Локальные функции

Nemerle не просто поддерживает функциональное программирование. Функциональное программирование для него – «родная» парадигма. Поэтому к функциям в этом языке особое отношение. Они являются главными строительными блоками. Не мудрено, что Nemerle поддерживает такую полезную возможность, как локальные функции.

Объявить локальную функцию очень просто. Надо написать ключевое слово def и за ним описать саму функцию. Например:

Main() : void
{
  def LocalFunction(name : string) : string
  {
    "Hello, " + name + "!";
  }

  def Print(text : string) : void
  {
    System.Console.WriteLine(text);
  }

  Print(LocalFunction("Nemerle"));
}

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

Локальная функция во многом походит на анонимный метод в C#, за тем исключением, что у нее есть имя, и нет необходимости использовать ее (вызывать или передавать в другие функции) через делегат. Как и анонимный метод, локальная функция может обращаться к переменным и функциям, объявленным во внешних функциях (то есть в функциях, в которые вложена данная функция).

module App
{
    Main(args : array[string]) : void
    {
      // Объявление локальной функцим «PrintArg»
      def PrintArg(index : int) : void
      {
        System.Console.WriteLine(args[index]);
      }

      PrintArg(0); // Печатает первый аргумент функции Main()
      PrintArg(1); // Печатает второй аргумент функции Main()
    }
}

В данном примере локальная функция использует параметр args метода Main, в который она вложена.

Функции как значения

В Nemerle вы можете передавать функции в качестве аргументов и возвращать их в качестве возвращаемых значений других функций. Таким образом, они ничем не отличаются от обычных типов данных и являются первоклассными сущностями (first class values). Это означает, что функции можно создавать, хранить, передавать другим функциям и классам.

В C# есть делегаты (delegates). Их концепция примерно похожа на функциональные значения Nemerle, однако последние являются более общим решением. И их тип не надо заранее объявлять.

Описание функций в Nemerle выглядит следующим образом:

описании_параметров -> описание_возвращаемого_значения

Типы параметров описываются через знак «*». Например, функция, принимающая параметр типа int и возвращающая значение типа int, будет выглядеть так:

int -> int

А принимающая строку и double, и ничего не возвращающая – так:

string * double -> void

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

// C#
delegate int IntFun(int); // В C# нужно объявлять тип делегата

class Delegates
{
  private static int Func(int x)
  {
    return x * x;
  }

  // В C# нельзя передать напрямую функцию. Предварительно функцию 
  // нужно «обернуть» в делегат.
  private static int RunDelegateTwice(IntFun f, int v)
  {
    return f(f(v));
  }

  public static void Main()
  {
    System.Console.WriteLine("{0}", RunDelegateTwice(new IntFun(Func), 3));
  }
}

// Nemerle
module Delegates
{
  private Func(x : int) : int
  {
    x * x
  }

  private RunFunvalTwice(f : int -> int, v : int) : int
  {
    f(f(v));
  }

  public static Main() : void
  {
    System.Console.WriteLine("{0}", RunFunvalTwice(Func, 3));
  }
}
СОВЕТ

Не ищите особого смысла в этом примере. Это всего лишь абстрактная демонстрация. :)

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

В качестве функциональных значений можно использовать локальные функции, методы, анонимные функции и даже операторы языка. Надо заметить, что в Nemerle функции являются чуть ли не главным средством абстрагирования кода, и там где в других языках используются, например, шаблоны (C++), в Nemerle можно обойтись функцией.

// Nemerle
module MoreFunctions
{
  private RunTwice(f: int -> int, v : int) : int
  {
    f(f(v));
  }

  private RunAdder(x : int) : void
  {
    def InnerFunc(y : int) : int { x + y }
    System.Console.WriteLine("{0}", RunTwice(InnerFunc, 3));
  }

  public Main() : void
  {
    RunAdder(1);
    RunAdder(42);
  }
}

Этот пример выведет 5 и 87. Обратите внимание на то, как значение х захватывается и используется в функции InnerFunc.

Частичное применение и другие операции над функциями и операторами

Любой оператор Nemerle можно использовать в качестве функции. Так функцию RunAdder из предыдущего примера можно переписать следующим образом:

RunAdder(x : int) : void
{
  System.Console.WriteLine("{0}", RunTwice(x + _, 3));
}

Заметьте, что при этом удалось избавиться от локальной функции InnerFunc. Думаю, что этот пример не так-то просто понять тем, кто никогда не сталкивался с миром функционального программирования (где подобные трюки в порядке вещей). Так что поясню более подробно. Выражение «x + _» описывает так называемое частичное применение «partial application». Оператор «+» можно рассматривать как инфиксную функцию с двумя параметрами. Запись «x + _» связывает первый параметр функции «+» с параметром «x» внешней функции, а знак «_» говорит о том, что второй параметр функции «+» остается не связанным. В результате выражение «x + _» порождает безымянную локальную функцию с одним параметром. Эта функция возвращает результат сложения своего параметра с «x». Если написать «_ + _», то результатом будет функция с двумя параметрами, производящая сложение их значений.

Точно так же можно применять частично и обычные функции.

Чтобы продемонстрировать это, лучше привести еще один пример:

RunAdder(x : int) : void
{
  def InnerFunc1(x : int, y : int) : int
  {
    x + y
  }

  def InnerFunc2 = InnerFunc1(x, _);

  System.Console.WriteLine("{0}", RunTwice(InnerFunc2, 3));
}

Здесь InnerFunc2 – это новая функция, получаемая в результате частичного применения функции InnerFunc1. Это тяжело понять, если думать в императивном стиле. Намного проще понять это, если думать, что в данном случае производятся вычисления (а точнее, преобразования) над самой функцией.

В данном примере первый параметр функции связывается с переменной «x», а второй остается не связанным и как бы переходит функции InnerFunc2.

Тот же пример можно переписать без использования частичного применения:

def InnerFunc2(y) { InnerFunc1(x, y) }

Частичное применение возможно и для объектов. Например, конструкция «_.Foo» будет преобразована в безымянную функцию с одним параметром, возвращающую значение свойства Foo.

Лямбда (безымянные функции)

Как и C# 2.0 (а также любой функциональный язык) Nemerle позволяет объявлять анонимные функции (лямбды). Анонимные функции полезны, например, в тех случаях, когда необходимо передать какую-то функцию в итератор и больше ее не использовать. Например, лямбды очень полезны, когда нужно задать некие условия отбора (предикат фильтрации) или алгоритм преобразования. Ведь такие функции очень редко являются универсальными и зачастую в них нужно обращаться к переменным и параметрам внешних функций.

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

// Map преобразует (отображает) один список в другой, используя
// переданную пользователем функцию.
def x : list[int] = [1, 2, 3].Map(fun(x : int) : int { 2 * x });

// FoldLeft (и ее аналог FoldRight) применяет переданную функцию к каждому
// элементу списка, используя последнее возвращенное значение для 
// следующего вызова. Это так называемый «аккумулятор».
// FoldLeft перебирает элементы списка слева направо. FoldRight – наоборот.
def y : int = Nemerle.Collections.List.FoldLeft(x, 0, 
  fun(val : int, acc : int) : int { val + acc });

assert(y == 12);

В общем случае,

fun (список_параметров) { список_выражений }

эквивалентно такому коду

{
  def tmp(список_параметров)
  {
    список_выражений
  };

  tmp
}

Декомпозиция функций

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

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

Объявление локальных переменных

Я не раз наблюдал дискуссии на тему того, нужно ли было в C# вводить локальные переменные, доступные только для чтения (в C++ их помечают ключевым const). Кто-то считал, что это не нужно. Кто-то, наоборот, что нужно и очень. Кто-то считал неудачным ключевое слово const. Но додуматься до того, чтобы сделать все переменные по умолчанию неизменяемыми – такого я точно не помню.

Так вот, создатели Nemerle поступили именно так. Локальная переменная в Nemerle объявляется с помощью ключевого слова def:

def a : int = 1;
def b : string = "Test";

и по умолчанию доступна только для чтения.

ПРИМЕЧАНИЕ

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

Чтобы объявить изменяемую переменную, вместо def нужно использовать ключевое слово mutable:

mutable a : int = 1;
mutable b : string = "Test";
a++;
b = b.Replace("Test", "OK");

Авторы языка сознательно подталкивают программиста к использованию неизменяемых переменных и, соответственно, функционального стиля. Но делается это очень осторожно, так что нет никаких проблем с программированием на Nemerle в императивном стиле. Это получается не хуже, чем на C#.

Вывод типов для локальных конструкций

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

Языки группы ML обладают способностью выводить типы переменных и параметров из их использования. Эта способность компилятора была названа выводом типов (type inference). В Nemerle реализован вывод типов для локальных функций и переменных. Например, тип переменной может быть введен путем анализа последующего использования переменной. Сравните:

// C#
int a = 1; // В C# невозможно сделать локальную переменную неизменяемой
Dictionary<string, int> b = new Dictionary<string, int>();
int с = 1;
string d = "Test";

// Nemerle с аннотацией типов
def a : int = 1;
def b : Dictionary[string, int] = Dictionary.[string, int]();
mutable с : int = 1;
mutable d : string = "Test";

// Nemerle с автоматическим выводом типов
def a = 1;
def b = Dictionary.[string, int]();
mutable с = 1;
mutable d = "Test";

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

Вряд ли для кого-то секрет, что в C# 3.0 тоже появится вывод типов переменных из их инициализации, Nemerle уже сейчас в вопросе вывода типов на голову выше C# 3.0, который еще неизвестно когда появится. И вот почему.

Nemerle умеет выводить типы не только из инициализирующего значения, но и из последующего использования. Причем выводиться могут даже параметры типов:

// С#
Dictionary<string, int> dic = new Dictionary<string, int>();
dic.Add("answer", 42);

// Nemerle
def dic = Dictionary();
dic.Add("answer", 42);

Компилятор Nemerle видит объявление переменной типа Dictionary (который является generic-типом с параметрами TKey и TValue). Поскольку на момент объявления типы не заданы, компилятор попытается вывести их из последующего кода. Следующей строчкой идет вызов метода Dictionary.Add(TKey, TValue), и компилятор устанавливает тип ключа в string, а тип значения – в int. Начиная с этого момента, переменная dic типизирована, и если обратиться к ней с нарушением типизации (например, вызвать метод dic.Add("question", "what?")), то компилятор выдаст соответствующее сообщение об ошибке:

Main.n(6,1,6,8): error : in argument #2 (value), needed a int-, 
got string: common super type of types [int, string] is a set of 
interfaces [System.IConvertible, System.IComparable[int], 
System.IEquatable[int], System.IComparable]. This is not supported

Еще пример:

// Это функция вычисления числа Фибоначчи.
Fibonacci(n : int) : int
{
  // Обратите внимание - не указаны ни типы параметров, 
  // ни тип возвращаемого значения. Компилятор способен вывести их, 
  // так как в некоторых местах используются численные литералы, а 
  // значение, возвращаемое функцией MyLoop(), возвращается функцией
  // Fibonacci().
  def MyLoop(last1, last2, cur)
  {
    if(cur >= n)
      last2
    else
      MyLoop(last2, last1 + last2, cur + 1)
  }

  MyLoop(1, 1, 1)
}

Вывод типов доступен в полной мере для локальных переменных и вложенных функций.

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

class SomeClass
{
  mutable x1 = 42;   // Правильно. Поле x1 имеет тип int
  mutable x2;  // Неправильно. Не задано значение (не из чего вывести тип).
  // Неправильно. Заданное значение не является литералом.
  mutable x3 = ArrayList(); 
}

Кортежи (tuples)

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

Кортежи конструируются с помощью записи

(expr1, expr2, expr3)

и «деконструируются» с помощью

def (expr1, expr2, expr3) = ReturnSomeTuple();

Оператор def объявляет локальные переменные expr1, expr2, expr3 и привязывает их к соответствующим элементам кортежа.

Типы кортежей записываются с помощью «*» (не путать с умножением и тем более с указателями, коих в Nemerle нет вовсе). Например, кортеж (1, 2) будет иметь тип «int * int», а кортеж (42, 27.0, "vasya") будет иметь тип «int * double * string».

Короткий пример:

using System.Console;

// Функция возвращает кортеж, состоящий из двух элементов, числителя и
// знаменателя, из строки в формате «Числитель / знаменатель»
def ExtractNumerAndDenom(stringFraction : string) : int * int
{
  def strArray = stringFraction.Split('/');
  (int.Parse(strArray[0]), int.Parse(strArray[1]))
}

def (nominator, denominator) = ExtractNumerAndDenom("5/8");
WriteLine($"Numerator is $nominator, denominator is $denominator");

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

def tuple = (1, "some string", 3.1415926);

// ОК. var1 теперь равно «some string»
def var1 = tuple[1]; 

def i = int.Parse(System.Console.ReadLine());

// Ошибка! Аргумент индексатора кортежа должен быть константой.
def var2 = tuple[i]; 

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

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

mutable (a, b, c) = (1, "str", 2);

Реально данный код не создает кортежей. Он только пользуется сходным синтаксисом и идеологией.

Так же можно написать код обмена значений переменных в одной строке:

(a, c) = (c, a);

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

a <-> c;

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

// выводит на консоль содержимое массива
def Print(arr)
{
  foreach (i in arr)
    System.Console.Write($"$i ");

  System.Console.WriteLine();
}

def arr = array[1, 2, 3, 4, 5, 6, 7, 8, 9];
def rand = System.Random(123);
// Генерирует случайное значение индекса массива и 
// выводит его значение на консоль.
def RandomIndex()
{
  def index = rand.Next() % arr.Length;
  System.Console.WriteLine($"index = $index");
  index;
}

for (mutable i = 0; i < 10; i++)
{
  // Массив каждый раз пересоздается заново, чтобы вам было понятнее
  // что происходит.
  def arr = array[1, 2, 3, 4, 5, 6, 7, 8, 9];
  arr[RandomIndex()] <-> arr[RandomIndex()];
  Print(arr);
}

Этот код выводит:

index = 5
index = 4
1 2 3 4 6 5 7 8 9
index = 2
index = 0
3 2 1 4 5 6 7 8 9
index = 3
index = 0
4 2 3 1 5 6 7 8 9
index = 4
index = 6
1 2 3 4 7 6 5 8 9
index = 1
index = 1
1 2 3 4 5 6 7 8 9
index = 8
index = 1
1 9 3 4 5 6 7 8 2
index = 6
index = 1
1 7 3 4 5 6 2 8 9
index = 2
index = 1
1 3 2 4 5 6 7 8 9
index = 8
index = 5
1 2 3 4 5 9 7 8 6
index = 2
index = 4
1 2 5 4 3 6 7 8 9

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

SomeFunc(x : int, y : double, z : string) : int

выглядит следующим образом:

int * double * string –> int

а кортежа с тремя элементами:

(1, 3.14, "foo")

выглядит так:

int * double * string

Что называется, «найдите 10 отличий» между описанием параметров функции и кортежем. :)

Фактически описание списка параметров функции – это описание кортежа. Возвращаемое значение функции также является кортежем. Получается, что описание функции можно представить как описания двух кортежей, разделенные знаком «->».

Эта схожесть натолкнула разработчиков Nemerle на мысль о том, что кортежи можно передавать в качестве параметров методов, описание параметров которых совпадает с описанием кортежа. Вот пример, демонстрирующий данную возможность:

using System.Console;

// Функция, получающая 3 параметра.
def Func1(a : int, b : string, c : double)
{
  WriteLine($"a = $a, b = $b, c = $c");
}
// Функция, получающая кортеж (tuple), состоящий из трех полей,
// и функцию (func) с тремя параметрами, ничего не возвращающую.
def Func2(tuple : int * string * double, func : int * string * double -> void)
{
  // Функция "func" принимает три отдельных параметра, но кортеж "tuple"
  // совместим по числу и типу аргументов с аргументами функции.
  // Это позволяет передать в функцию не отдельные параметры, а кортеж.
  func(tuple);
}
// Кортеж, состоящий из трех элементов.
def x = (1, "str", 2.3);
// Передаем в функцию "Func2" кортеж "x" и функцию, параметры которой 
// совместимы с кортежем "x".
Func2(x, Func1);

Этот же пример можно переписать с использованием вывода типов:

using System.Console;

def Func1(a, b, c) { WriteLine($"a = $a, b = $b, c = $c"); }
def Func2(tuple, func) { func(tuple); }
def x = (1, "str", 2.3);

Func2(x, Func1);

Не правда ли, приятно осознавать, что компилятор способен сам вычислить кучу информации, которую в противном случае пришлось бы задавать вручную? :) Ну, да, мы отвлеклись...

Функции, не имеющие аргументов, полагаются имеющими один аргумент типа void. Таким образом, функция:

SomeAnotherFunc() : string
{
}

обладает типом void -> string. Аналогично и для возвращаемых значений.

Списки

Однонаправленный связный список – это структура данных, хорошо известная большинству программистов. Каждый элемент такого списка содержит значение и указатель на следующий элемент, либо специальный признак конца списка. Списки часто используется в Nemerle – достаточно часто, чтобы иметь свой синтаксис. Хотя имя типа списка «list» в Nemerle очень похоже на имя типов List[T] (List<T> в стиле C#) и ArrayList из стандартной библиотеки классов, они имеют совершенно разную реализацию и, соответственно, характеристики. List[T] и ArrayList реализуют абстракцию списка на базе массива, динамически изменяя его размер в случае необходимости и позволяя изменять элементы списка по месту.

Полное название типа списка в Nemerle – Nemerle.Core.list[T], но создать его экземпляр напрямую нельзя, так как этот тип является абстрактным. У этого типа есть два наследника, которые к тому же вложены в этот тип.

ПРИМЕЧАНИЕ

Точнее, этот тип является вариантом в терминах Nemerle, но для облегчения понимания речь о нем будет вестись в терминах классов.

Один из наследников играет роль пустого элемента, определяющего конец списка. Его имя Nil. Второй описывает элемент списка – Cons. Существует только один экземпляр класса Nil, который доступен через специальную статическую переменную. Однако компилятор позволяет создавать экземпляр Nil реально, заменяя его обращением к статической переменной. Cons определяет элемент списка. Его конструктор принимает значение элемента и «хвост» списка (которым может быть другой элемент или Nil).

Вот как можно создать пустой список:

def list1 = Nemerle.Core.list[int].Nil();
System.Console.WriteLine(list1);

Этот код выведет:

[] 

А вот так будет выглядеть создание списка из двух элементов (1 и 3):

def list1 = Nemerle.Core.list[int].Nil();
// Заметьте, что элементы добавляются в начало списка.
def list1 = Nemerle.Core.list[int].Cons(3, list1);
def list1 = Nemerle.Core.list[int].Cons(1, list1);

System.Console.WriteLine(list1);

Этот и несколько следующих примеров выводят:

[1, 3]

Яснее ясного, что столь громоздкая запись не способствует широкому использованию списков. Между тем, язык, претендующий на полноценную поддержку функциональной парадигмы, просто обязан не только поддерживать работу со списками, но и сделать ее максимально простой и удобной. Естественно, что Nemerle тоже предоставляет такие возможности. Во-первых, тип list[T] доступен глобально. Точнее, пространство имен Nemerle.Core используется по умолчанию. Так что пример, приведенный выше, можно записать несколько короче:

def list1 = list[int].Nil();
def list1 = list[int].Cons(3, list1);
def list1 = list[int].Cons(1, list1);

System.Console.WriteLine(list1);

Но это все равно слишком длинно. Чтобы сделать работу со списками действительно простой и удобной, Nemerle предоставляет конструкторы списков и списковые литералы. С использованием списковых литералов этот пример можно записать так:

def list1 = [1, 3]; // list[int].Cons(1, list[int].Cons(3, list[int].Nil()))
System.Console.WriteLine(list1);

Или так, с использованием конструктора списков (выглядящего как оператор «::»):

def list1 = [];         // list[int].Nil()
def list1 = 3 :: list1; // list[int].Cons(3, list1)
def list1 = 1 :: list1; // list[int].Cons(1, list1)

System.Console.WriteLine(list1);

«[]» – это литерал, описывающий пустой список. Тип списка определяется из его дальнейшего использования.

В комментариях указано, во что разворачиваются данные конструкции.

Можно создать список и из готовых значений:

mutable i1 = 1;
mutable i2 = 3;
System.Console.WriteLine([i1, i2]);

А вот так делать нельзя:

mutable i1 = 1;
mutable i2 = 3;
System.Console.WriteLine(i1 :: i2); // Ошибка! i2 не является списком!

И так нельзя:

def list1 = [];
def list1 = list1 :: 1; // Ошибка! Второй аргумент должен быть списком!

А вот так можно:

System.Console.WriteLine(1 :: 3 :: []);

Данные примеры порождают новую неизменяемую переменную в каждой строке, начинающейся с def. Такая переменная имеет область видимости от ее определения и до конца блока или до места, где она переопределяется (то есть до следующего def в этих примерах). Обратите внимание на то, что это не изменение значения переменной, а именно ее переопределение (переопределять можно переменные любых типов, при этом даже можно менять тип переменной, ведь создается другая переменная)! Однако при использовании императивного стиля может быть удобно менять значение переменной, а не порождать новую. Для изменения значения переменной, ссылающейся на список, даже имеется специальный оператор «::=» (конкатенации списков):

mutable list1 = [];
list1 = 3 :: list1;
list1 ::= 1; // аналогично конструкции: list1 = 1 :: list1

System.Console.WriteLine(list1);

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

Список в Nemerle является ссылочным типом и ведет себя очень похоже на строку.

Что касается производительности, то списки довольно эффективны там, где требуется перебор элементов списка, добавления элементов в начало списка или преобразование списков, однако они уступают массивам, если требуется доступ к элементам списка по индексам, нужно знать длину списка, добавлять элементы в конец списка, а также по потреблению памяти. Каждый элемент списка занимает минимум 8 байт на внутренние структуры объекта .NET + размер указателя на следующий элемент + размер хранимого значения. Так, для .NET Framework 1.0-2.0 на 32-битной платформе элемент списка, хранящий значение типа Int32, занимает 8 + 4 + 4 = 16 байт. Это приблизительно соответствует памяти, выделяемой на VARIANT в OLE Automation (скриптах вроде VBScript) или VB до версии 6.0 включительно.

Для конкатенации списков можно использовать оператор «+». Например, следующий код:

System.Console.WriteLine([1, 2, 3] + [4, 5, 6]);

выведет:

[1, 2, 3, 4, 5, 6]

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

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

Обратную операцию, создание списка по массиву, можно осуществить с помощью статической функции FromArray(). Она, вместе со многими вспомогательными функциями, находится в классе (точнее, в терминах Nemerle, в модуле) Nemerle.Collections.List. Список функций, входящих в этот класс, можно найти по ссылке: http://nemerle.org/doc/Nemerle.Collections.ListMembers.html

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

Если с литералами все ясно, то работу конструкторов списков стоит пояснить.

Итак, нужно уяснить главные особенности списков в Nemerle:

они являются неизменяемыми (immutable, не могут быть изменены после создания);

их элементы могут добавляться только в начало;

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

Особые возможности работы со списками (list comprehension)

В Nemerle есть замечательное расширение, позволяющее с легкостью создавать списки, в том числе, и из результата выборки из другого списка (или списков). Эта возможность позаимствована из Haskell и носит аналогичное Haskell-евскому название list comprehension (что можно перевести как конструкторы списков, но, чтобы не путаться, мы будем использовать английское название).

def list1 = [1, 4, 9, 16, 25, 36];
// Следующую строку можно прочитать так: 
// «Создать список newList, содержащий элементы х, принадлежащие
// списку list1 и удовлетворяющие условиям x > 5 и x < 30»
def newList = $[x | x in list1, x > 5, x < 30];

Если добавить к этому какое-то действие, то получится запись, почти не уступающая по краткости и понятности математической формуле, записанной на доске.

// «Вывести на консоль все пары (x, y), такие, что х принадлежит list1 и
// x > 3, а y принадлежит list2 и x < y»
WriteLine($[(x, y) | x in list1, x > 3, y in list2, x < y]);

Выражение:

$[(x, y) | x in list1, x > 3, y in list2, x < y];

будет переведено компилятором в следующий код:

mutable res = [];

foreach (x in list1)
  when (x > 3)
    foreach (y in list2)
      when (x < y)
        res ::= (x, y);

res.Rev() // Список был перевернут, так что его нужно развернуть обратно.

В общем случае,

$[expr | cond0, x1 in list1, cond1, ..., xN in listN, condN]

превратится в:

mutable res = [];

when (cond0)
  foreach (x1 in list1)
    when (cond1)
      ...
      foreach (xN in listN)
        when (condN)
          res ::= expr;

res.Rev()

Несколько примеров:

// Генериует все пары списков, для которых условие истинно
$[(x, y) | x in list1, y in list2, x < y] 
// значения от 1 до 9 включительно, с шагом 2
$[1, 3 .. 9] // порождает список [1, 3, 5, 7, 9]
// значения от 1 до 5 включительно, с шагом 1
$[1 .. 5]    // порождает список [1, 2, 3, 4, 5]
// пары (1, 2), (1, 4) ... (1, 10), (2, 2), (2, 4) ... (3, 10)
$[(x, y) | x in [1 .. 3], y in [2, 4 .. 10]] 

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

$[0 .. theArray.Length - 1]

В качестве источника данных в list comprehension могут выступать не только списки, но и любые коллекции. Например, источником может служить экземпляр System.Collections.Generic.List[T] или массив.

using System.Console;
using System.Collections.Generic;

def arr = array[1,2,3,4,5,6,7,8,9];

// list comprehension, использующий массив в качестве входных данных.
WriteLine($[x | x in arr, x % 3 == 0]);

def lst = List();
// Копируем содержимое массива в List[int].
lst.AddRange(arr :> IEnumerable[int]);
lst.Remove(6); // Удаляем элемент со значением 6.

// list comprehension, использующий коллекцию в качестве входных данных.
WriteLine($[x | x in lst, x % 3 == 0]);

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

[3, 6, 9]
[3, 9]

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

Другие типы и коллекции Nemerle

Коллекции Nemerle в основном являются обертками над коллекциями .NET Framework. Они добавляют к стандартным классам методы, позволяющие использовать их в функциональном стиле, и вообще удобные универсальные методы. Список всех классов стандартной библиотеки Nemerle можно увидеть по этой ссылке:

http://nemerle.org/Class_library.

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

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

match (сопоставляемое_выражение) 
{ | образец_1
  ... 
  | образец_n                             => вычисляемое_выражение
  | образец_x when необязательное_условие => вычисляемое_выражение
  ...
}

Образец «конструктор»

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

using System.Console;

variant RgbColor
{ | Red
  | Yellow
  | Green
  | Different { red : float; green : float; blue : float; }
}

def StringOfColor (color : RgbColor) : string
{
  match (color) 
  { | RgbColor.Red()                       => "red" 
    | RgbColor.Yellow()                    => "yellow"
    | RgbColor.Green()                     => "green"
    | RgbColor.Different(r, g, b) 
      when r == 0 && g == 0 && b >= 1.0f   => "blue"
    | RgbColor.Different(r, g, b)          => $"rgb($r, $g, $b)"
  }
}

WriteLine(StringOfColor(RgbColor.Red()));
WriteLine(StringOfColor(RgbColor.Green()));
WriteLine(StringOfColor(RgbColor.Different(0f, 0f, 1.0f)));
WriteLine(StringOfColor(RgbColor.Different(0.1f, 0.2f, 1.0f)));

Этот код выведет:

red
green
blue
rgb(0.1, 0.2, 1)

match – это специальная конструкция языка, производящая сопоставление с образцом.

Приведенный выше пример преобразуется компилятором Nemerle в примерно такую конструкцию (C#):

static string StringOfColor(RgbColor color)
{
  if (color == RgbColor.Red._N_constant_object)
    return "red";

  if (color == RgbColor.Green._N_constant_object)
    return "yellow";

  if (color == RgbColor.Red._N_constant_object)
    return "green";

  if (color is RgbColor.Different)
  {
    RgbColor.Different different = (RgbColor.Different)color;
    float r = different.red;
    float g = different.green;
    float b = different.blue;

    if (r == 0 && g == 0 && b >= 1.0f)
      return "blue";
    else
      return "rgb(" + r + ", " + g + ", " + b + ")";
  }

  throw new Nemerle.Core.MatchFailureException();
}

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

Еще одним интересным моментом в этом примере является то, что образец может содержать активные области. В данном случае это переменные «r», «g», «b». Они сопоставляются с параметрами конструктора. Компилятор видит, что образец совпадает с конструктором варианта и сопоставляет соответствующие поля варианта с переменными «r», «g», «b». Данную зависимость можно выразить более явно:

| RgbColor.Different(red = r, green = g, blue = b)  => $"rgb($r, $g, $b)"

Задавая сопоставление явно, можно изменить порядок полей:

| RgbColor.Different(blue = b, green = g, red = r) => $"rgb($r, $g, $b)"

Можно даже задать частичное сопоставление:

| RgbColor.Different(blue = b, green = g) => $"rgb($g, $b)"

Опустить часть сопоставления можно и по-другому, воспользовавшись конструкцией «_»:

| RgbColor.Different(r, _, b) => $"rgb($r, unknown, $b)"

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

ПРЕДУПРЕЖДЕНИЕ

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

Кроме того, за образцом может следовать дополнительное условие. Если оно есть, оно должно начинаться с ключевого слова «when». За ним должно следовать булево выражение, которое может использовать переменные, определенные в образце, или любые другие переменные из внешнего (по отношению к оператору match) контекста. Если условие возвращает false, данный образец игнорируется.

Механизм сопоставления с образцом является очень мощным и выразительным инструментом. О справедливости этого утверждения говорит хотя бы тот факт, что конструкция match – одна из очень немногих по настоящему встроенных в язык. Даже конструкция if/else реализована с помощью match.

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

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

def value = -1;

match (value)
{ | 0
  | 1 => WriteLine("0 or 1")
  | x => WriteLine($"value is $x")
}

Этот код выведет:

value is -1

На C# то же самое можно записать примерно таким образом:

int value = -1;

switch (value)
{
  case 0:
  case 1:
    Console.WriteLine("0 or 1");
    break;
  default:
    int x = value;
    Console.WriteLine("value is " + x);
    break;
}

В общем-то, связь с переменной в данном случае не нужна, поскольку значение и так доступно через переменную value. В таких случаях вместо имени переменной можно указать универсальный символ-заменитель «_». Вот как будет выглядеть приведенный выше пример, если его переписать с использованием «_»:

def value = -1;

match (value)
{ | 0
  | 1 => WriteLine("0 or 1")
  | _ => WriteLine($"value is $value")
}

Конструкция «| _ =>» может использоваться в любом варианте сопоставления с образцом как значение, выводимое по умолчанию. Очевидно, что при этом оно должно стоять последним.

Естественно, что для переменной можно задать некоторое условие:

using System;
using System.Console;

def value = 10;

match (value)
{ | 0 | 1        => WriteLine("0 or 1")
  | x when x > 0 => WriteLine($"value is $x")
  | _            => throw ArgumentOutOfRangeException("value", "value < 0")
}

Точно так же, как и в предыдущем случае, вместо переменной можно использовать «_», а проверки осуществлять на исходной переменной.

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

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

using System.Console;

def value = (2, 11);

match (value)
{  // Подобразцы могут быть заданы литералами.
  | (1, 2)             => WriteLine("1, 2")
   // Подобразцы могут содержать знак подстановки «_».
   // Это означает, что соответствующее поле кортежа может быть любым.
  | (3, _)             => WriteLine($"3, ?")
   // Подобразtц может быть переменной. При этом с ней будет сопоставлено 
   // любое значение или значения, удовлетворяющие условию.
  | (2, x) when x > 10 => WriteLine($"x > 10; x=$x")
  | (2, x)             => WriteLine($"1 x=$x")
  | _                  => WriteLine("...")
}

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

Сопоставление с образцом как единственное выражение в функции

Если оператор match является единственным выражением в функции, то Nemerle позволяет опустить саму конструкцию match и рассматривать тело функции как тело оператора match. Например, записи:

def Factorial(x : uint) : ulong
{
  match (x)
  { | 0U | 1U => 1UL
    | _ => x * Factorial(x - 1U)
  }
}

и:

def Factorial(x : uint) : ulong
{ | 0U | 1U => 1UL
  | _ => x * Factorial(x - 1U)
}

эквивалентны. Это позволяет сделать запись маленьких функций более краткой и понятной. Функции вроде факториала, наверно, не хуже выглядят и будучи реализованными на базе оператора if или while, но в случае большего количества элементов или более сложных проверок сокращенная форма match является очень выразительной.

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

using System.Console;

def Function(a, b)
{ | (1, "a") => "1, 'a'"
  | (2, "s") => "2, 's'"
  | (3, _)   => $"3, b='$b'"
  | _        => "other"
}

WriteLine(Function(1, "s"));
WriteLine(Function(2, "s"));
WriteLine(Function(3, "s"));

Образец «Запись»

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

using System.Console;

// Метаатрибут «Record» добавляет к классу реализацию конструктора, 
// инициализирующего поля класса.
[Record]
class Foo
{
  public Number : int;
  public Name   : string;
}

def StringOfFooMatch(foo : Foo) : string
{ | Foo where (Name = "", Number = k) => k.ToString()
  | (Name = s)                        => s
}

WriteLine(StringOfFooMatch(Foo(10, "")));
WriteLine(StringOfFooMatch(Foo(1, "Ivan")));

Этот код выведет:

10
Ivan

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

Это любой литерал (число, символ, строка или null).

MatchIntegerLiteral() : string
{
  def random = Random().Next(4);

  match (random)
  { | 0 => "zero"
    | 1 => "one"
    | 2 => "two"
    | 3 => "three"
    | _ => "other"
  }
}

MatchStringLiteral(lit : string) : RgbColor
{ | "yellow" => RgbColor.Yellow
  | "red"    => RgbColor.Red
  | "green"  => RgbColor.Green
  | _        => throw Exception();
}

Образец «as»

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

match (ConstructList())
{ | [1, _] as wholeList => ProcessWholeList(wholeList)
  | _ => {}
}

// Еще один пример, более интересный
variant Foo
variant Foo
{ | A { x : int; mutable y : string}
  | B
}

def ProduceSomeFoo() { Foo.A(3, null) }

match (ProduceSomeFoo())
{ | A(3, _) as a => a.y = "three"
  | _            => {}
}

В последнем примере компилятор знает, что «a» имеет тип Foo.A, так что он позволяет присваивать значение его полю.

Образцы типа

Их два. Первый – это образец форсирования. Он состоит из идентификатора переменной, двоеточия и имени типа. Например, «val : float» или «(x, y) : Foo * Bar». Этот образец требует, чтобы сведения о типах были доступны во время компиляции. На самом деле это даже не образец. Это просто возможность помочь системе вывода типов, указав ей правильное направление поиска. С тем же успехом можно форсировать вывод типа для переменной или выражения, подвергаемого сопоставлению с образцом.

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

match(car)
{
  | x is SportCar => /* работаем со спортивной машиной */
  | x is Truck => /* работаем с грузовиком */
  | _ => /* делаем что-то по умолчанию. */
}

По сути, образец «is» – это альтернатива динамической диспетчеризации, осуществляемой в ООЯ с помощью виртуальных методов. Это позволяет писать ОО-программы, очень непохожие на те, что пишутся на C# или C++.

Например, наличие образца «is» делает практически ненужным использование таких паттернов проектирования, как «Посетитель». Это бывает очень удобно при работе с различными иерархиями классов.

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

using System.Console;

class Foo { }
[Record]
class A : Foo { public x : int; }
class B : Foo {  }

def MiltiDispatch(x : Foo, y : Foo) : void
{
    // «a» – это имя переменной, связываемой со значением параметра
    // «x» при совпадении образца данного вхождения.
    // Тип переменной «a» будет «A».
  | (a is A, _ is B) when a.x > 0 => WriteLine($"A, B (a.x=$(a.x))");
    // Символом «_» заменяются имена переменных, которые не нужны далее.
  | (_ is A, _ is B)              => WriteLine("A, B");
  | (_ is A, _ is A)              => WriteLine("A, A");
  | (_ is B, _ is A)              => WriteLine("B, A");
  | (_ is B, _ is B)              => WriteLine("B, B");
  | _                             => WriteLine("other");
}

MiltiDispatch(A(0), B());
MiltiDispatch(A(2), B());
MiltiDispatch(B(),  B());

Точно так же можно осуществлять множественную диспетчеризацию для вариантов Nemerle. Причем это даже удобнее, так как не требуется использовать паттерн «is». Вот пример такого использования:

using System.Console;

variant Foo
{
  | A { x : int; }
  | B
}

def MiltiDispatch(x : Foo, y : Foo)
{ | (A as a, B) when a.x > 0 => WriteLine($"A, B (a.x=$(a.x))");
  | (A, B)                   => WriteLine("A, B");
  | (A, A)                   => WriteLine("A, A");
  | (B, A)                   => WriteLine("B, A");
  | (B, B)                   => WriteLine("B, B");
}

MiltiDispatch(Foo.A(0), Foo.B());
MiltiDispatch(Foo.A(2), Foo.B());
MiltiDispatch(Foo.B(),  Foo.B());

Оба этих примера выводят на консоль:

A, B
A, B (a.x=2)
B, B

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

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

using System.Console;

def list1 = [1, 2, 3, 6];

def Length(lst, acc = 0)
{
  match (lst)
  { | _ :: tail => Length(tail, acc + 1)
    | []        => acc
  }
}

WriteLine(Length(list1));

def Reverse(lst, acc = [])
{ 
  match (lst)
  { | head :: tail => Reverse(tail, head :: acc)
    | []           => acc
  }
}

WriteLine(Reverse(list1));

Этот код выводит:

4
[6, 3, 2, 1]

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

Образец конструирования списка может повторяться как угодно и содержать переменные или знаки «_». Вот расширенный пример:

using System.Console;

def list1 = [1, 2, 3, 6, 9];
def list2 = [1, 0, 3, 7, 9];
def list3 = [1, 0, 0, 7, 9];

def Function1(list1)
{ | 1 :: _ :: 3 :: tail => WriteLine(tail);
  | _                   => WriteLine("other");
}

Function1(list1);
Function1(list2);
Function1(list3);

def Function2(list1)
{ | 1 :: x :: 3 :: tail => WriteLine(x :: tail);
  | _                   => WriteLine("other");
}

WriteLine("-------------");

Function2(list1);
Function2(list2);
Function2(list3);

Этот код выводит на консоль:

[6, 9]
[7, 9]
other
-------------
[2, 6, 9]
[0, 7, 9]
other

В качестве образца можно использовать и списковый литерал «[x, y, z, ...]». В нем также могут присутствовать переменные и «_». Например, образец «[_, 42, x]» будет сопоставлен со списком, состоящим из трех элементов, второй элемент которого равен 42. При этом с последним элементом списка будет сопоставлена переменная «x» (через которую можно прочесть его значение).

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

using System.Console;

def list1 = [[1, 2], [3], [6, 9]];
def list2 = [[1, 2], [0, 1, 2], [6, 9]];
def list3 = [[2, 2], [0, 1, 2], [6, 9]];

def Function2(list1 : list[list[int]])
{ | [1, 2] :: x :: [[6, 9]] => WriteLine(x);
  | _                       => WriteLine("other");
}

Function2(list1);
Function2(list2);
Function2(list3);

Этот код выводит на консоль:

[3]
[0, 1, 2]
other

Оператор «try»

Оператор «try» в Nemerle очень похож на аналогичный в C#, но для обработки исключений в разделе catch используется синтаксис сопоставления с образцом, использующий образец «приведения типов». Образцы сравниваются по очереди, при совпадении типа исключения с образцом выполняется соответствующее выражение (идущее после «=>»).

try
{
  //throw ApplicationException();
  mutable x = 0;
  x /= x;
}
catch
{ | _ is ApplicationException => WriteLine("ApplicationException");
  | e                         => WriteLine($"Some Exception: $(e.Message)");
}
finally
{
  WriteLine("finally");
}

В данном примере произойдет деление на ноль и сработает второй обработчик исключений («e => ...»). Если раскомментировать строчку, генерирующую исключение ApplicationException, то сработает первый обработчик, и на консоль будет выведено «ApplicationException».

Сама по себе более краткая запись (ведь не требуется для каждого обработчика писать лишнее ключевое слово и открывать блок) уже является приятным усовершенствованием по сравнению с C#, но намного более важно, что try, как и все остальные операторы языка, является выражением (expression). Это позволяет использовать try/catch/finally как подвыражение. Вот простой пример, демонстрирующий использование try/catch в качестве выражения.

using System.Console;

def ParseIntWithDefault0(str : string) : int
{
  try { int.Parse(str) } catch { | _ is System.FormatException => 0 }
}

WriteLine(ParseIntWithDefault0("123"));
WriteLine(ParseIntWithDefault0("something"));
WriteLine(ParseIntWithDefault0("12345678912345671234567"));

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

123
0

Unhandled Exception: System.OverflowException: Value was either too large or too small for an...

Если в строке содержится что-то отличное от числа, функция int.Parse() генерирует исключение System.FormatException() которое отлавливается обработчиком исключения. Этот обработчик возвращает значение, используемое по умолчанию – «0». Если происходит какое-то другое исключение, например, переполнение вследствие того, что преобразуемое число не вмещается в рамки int, то исключение не отлавливается фильтром исключений, и в конечном итоге выводится на консоль, аварийно завершая приложение. Последняя строка в примере как раз приводит к такому результату.

Рекурсия и производительность

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

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

while (true)
  // тело цикла

можно переписать так:

def MyLoop() 
{
  // тело цикла
  MyLoop();
}
 
MyLoop();

Цикл с условием:

while (условие)
  // тело цикла

можно переписать так:

def MyLoop() 
{
  when (условие)
  {
    // тело цикла
    MyLoop();
  }
}
 
MyLoop();

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

Таким образом, в Nemerle нет никакой разницы между хвостовой рекурсией и циклами.

Более того. Реально все циклы в Nemerle реализованы как макросы и переписываются компилятором в рекурсивные вызовы локальных функций.

Функциональный подход

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

Композиция функций дает большой простор для фантазии. Ниже рассказывается об одном приеме, который поможет вам влиться в мир декомпозиции функций. В качестве примера возьмем уже упоминавшиеся ранее функции Reverse и Length.

Программирование с использованием аккумулятора

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

Пример: «разворот списка»

def Reverse(list1, acc = [])
{
  match (list1)
  {
    | head :: tail => Reverse(tail, head :: acc)
    | [] => acc
  }
}

System.Console.WriteLine (Reverse([1, 2, 3]));

Этот код выводит: 

[3, 2, 1]

Эта функция добавляет первый элемент исходного списка (list1) в аккумулятор acc и вызывает себя для остальных элементов списка. Она аккумулирует головные элементы, пока список не закончится, после чего возвращает acc.

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

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

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

Длина списка

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

def Length(list1, acc = 0) 
{
  match (list1) 
  {
    | _ :: tail => Length(tail, acc + 1)
    | [] => acc
  }
}
 
System.Console.WriteLine(Length([4, 44, 22]));

Этот код выводит:

3

Здесь для расчета длины списка как аккумулятор используется целое.

Fold (свертка)

Есть еще более важная концепция, касающаяся программирования с использованием аккумуляторов: функция свертки (fold). В других языках она может иметь другие названия. Например, reduce или accumulate. Fold – это встроенный метод большинства коллекций Nemerle, так что это удобное средство декомпозиции, позволяющее упростить создание своих аккумуляторных функций. Сигнатура fold:

List.FoldLeft[T, R](lst : list[T], ini : R, f : T * R -> R) : R

FoldLeft принимает список с элементами типа T, исходное (инициализирующее) значение типа R, функцию, принимающую два параметра типа T и R (или кортеж типа T * R), и возвращает значение типа R.

Выражение List.FoldLeft([x1, x2, ..., xN], ini, f) эквивалентно: f(xN, f(... f(x2, f(x1, ini))...). Таким образом, свертка - это способ удобно вызывать функции для элементов списка в определенном порядке. При свертке функция применяется к первому элементу списка, затем последовательно к результату этого вызова и к следующему элементу, пока не будут выполнены вычисления для всего списка.

Возможно, это определение проще понять на примере:

def Fold(lst, acc, f)
{
  match (lst)
  {
    | head :: tail => Fold(tail, f(head, acc), f)
    | [] => acc
  }
}

Это очень похоже на обе функции, приведенные выше. Функция Reverse замещает f(head, acc) на head :: acc, а Length – на 1 + acc. Теперь, поскольку функция f – это параметр Fold, можно реализовать и Reverse, и Length с помощью встроенного метода FoldLeft:

def Reverse(lst) { List.FoldLeft(lst, [], fun(head, tail) { head :: tail }) }
def Length(lst)  { List.FoldLeft(lst,  0, fun(_,     acc) { 1 + acc }) }

Можно также использовать версию FoldLeft являющуюся членом класса list:

def Reverse(lst) { lst.FoldLeft([], fun(head, tail) { head :: tail }) }
def Length(lst)  { lst.FoldLeft(0,  fun(_,     acc) { 1 + acc }) }

Что делает реализацию еще чуточку короче.

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

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

Другие операторы

В данной статье были разобраны операторы, отсутствующие в языках программирования типа C#. Но Nemerle реализует и все операторы, имеющиеся в C#. Причем реализует за счет универсального механизма – макросов.

Макросы Nemerle имеют мало общего с макросами С/C++ (с которыми их легко перепутать по названию). В этой статье не будет полного рассказа об этой замечательной возможности Nemerle. Но пару слов мы все же скажем, так как иначе будет тяжело понять, о чем идет речь.

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

  1. Атрибутную, то есть выглядеть точно так же, как атрибуты в C#. При этом они могут применяться к декларативным конструкциям вроде объявления типов, функций, свойств, полей...
  2. Форму вызова функции. При этом внешне макросы ничем не отличаются от простых функций.
  3. Иметь специфичный синтаксис. Изменение синтаксиса доступно только на уровне выражений, метаатрибутов и операторов. Вы не можете изменить синтаксис определения типа или реализовать свой собственный. Но вы можете определить широкий класс синтаксических конструкций выражений.
  4. Определять реализацию операторов (например, сложения или умножения). Допускается вводить собственные операторы. По сути, бинарный оператор рассматривается как инфиксная функция (а унарный – как пре/пост-фиксная). В качестве имен таких функций могут выступать не цифробуквенные символы.

Большинство операторов в Nemerle реализовано с помощью изменяющих синтаксис макросов.

При этом макросы осуществляют разного рода оптимизации и используют возможности языка вроде вывода типов.

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

Конструкции, for, while, do/while ничем не отличаются от аналогичных конструкций в C#. А вот конструкция if отличается радикальным образом. Собственно, об этом уже говорилось выше, но будет не лишним еще раз упомянуть об этом. Реально конструкция if скорее соответствует конструкции «? :» из C#, так как является выражением и не допускает отсутствия «else». Вы не можете написать:

if (условие)
{
  // список выражений.
}

Вы обязаны написать:

if (условие)
  выражение1
else
  выражение2

Так как выражения могут быть составными, вы (как и в C#) можете писать:

if (условие)
{
  выражение1;
  выражение2;
}
else
{
  выражение3;
  выражение4;
}

Но при этом нужно понимать, что Nemerle рассматривает такую конструкцию, как if с двумя составными подвыражениями. При этом только последние операторы в блоках будут возвращать значение во внешнее выражение.

Например, подобный код:

using System.Console;

def condition = true;

def result = 

if (condition)
{
  def a = 1;
  a + 1;
}
else
{
  def b = 2;
  b * 5;
}

WriteLine(result);

показался бы компилятору C# бредом, так как в C# if не является выражением. А для Nemerle это совершенно корректное выражение. Данный пример выведен на консоль «2» (a + 1). Единственное, что правильнее было бы выделить принадлежность if отступами и поставить в конце выражения «;»:

def result = 
  if (condition)
  {
    def a = 1;
    a + 1;
  }
  else
  {
    def b = 2;
    b * 5;
  };

Лишние «;» Nemerle просто игнорирует.

Несмотря на этот пример, зачастую if в Nemerle используется точно так же как в C#, так как выражения могут ничего не возвращать (точнее возвращать значение типа void). Собственно, это и есть применение императивного стиля программирования. Вот как тот же самый пример можно записать императивно:

using System.Console;

def condition = true;

mutable result = 0;

if (condition)
{
  def a = 1;
  result = a + 1;
}
else
{
  def b = 2;
  result = b * 5;
}

WriteLine(result);

Выводит:

1, 2, 3

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

Реализация foreach в Nemerle вообще выделяется на фоне других операторов. Например, он не требует задания типа переменной:

using System.Console;

def arr = array[1, 2, 3];

foreach (value in arr)
  Write($"$value ");

WriteLine();

Кроме того, вместо “имени” переменной можно использовать произвольный образец, даже литерал. Например:

foreach(1 in [1,2,3,1])
  ...

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

Еще одной уникальной особенностью foreach является возможность фильтрации элементов по критерию принадлежности их к некоторому подтипу. Например:

foreach (meth is IMethod in members)
   ...

Этот пример вызывает тело цикла только для тех элементов коллекции members, которые приводятся к интерфейсу IMethod.

В качестве списка можно использовать «диапазоны» (особый случай list comprehension):

using System.Console;

foreach (value in $[0..20])
  Write($"$value ");

WriteLine();

Выводит:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

При этом генерируется вполне оптимальный код. Данный код, например, переписывается с использованием оператора for.

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

Среди макросов есть приятные расширения языка вроде операторов «%&&», который выполняет битовое «&» над своими операндами, приводит результат к int и проверяет, не равен ли результат нулю. Например:

System.Console.WriteLine(0x01 %&& 0x11);
System.Console.WriteLine(0x01 %&& 0x10);

Выведет:

True
False

Этот пример аналогичен следующему C#-коду:

System.Console.WriteLine((0x01 & 0x11) != 0);
System.Console.WriteLine((0x01 & 0x10) != 0);

Другие макросы

Кроме эмуляции конструкций C# и разного рода приятных расширений синтаксиса, в Nemerle реализованы и более мощные вещи. Например, имеется макро-библиотека, позволяющая радикально упростить создание многозадачных приложений.

Accessor

Макрос Accessor позволяет упростить объявление свойств, не содержащих в себе дополнительной логики. Например, вместо конструкции:

mutable _someField : int;

public SomeField : int
{
  get { _someField }
}

воспользовавшись макросом Accessor, можно обойтись такой:

using Nemerle.Utility; 
 
[Accessor]
mutable _someField : int;

Макросы «design by contract»

Есть набор макросов, предоставляющий возможности «Design by contract». С их помощью можно в декларативной форме задавать пред/постусловия и инвариант для классов и методов. Если условия не удовлетворяются, генерируются исключения. Макросы сами генерируют проверки постусловий во всех необходимых местах. Например, вот как выглядит задание постусловий:

class LinkedList 
{
  public Clear () : void
    ensures this.IsEmpty
  {
    ...
  }
 
  public Length() : int
    ensures value >= 0
  {
    ...
  }
}

Паттерны проектирования

Есть и макросы, реализующие паттерны проектирования. Пока что доступны реализации паттернов «Заместитель» («Proxy») и «Одиночка» («Singleton»).

Ленивое выполнение

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

using Nemerle;
using System.Console;
 
class App
{
  static Foo([Lazy] x : int, y : bool) : void
  {
    WriteLine("----- The foo called! -----");

    if (y)
    {
      WriteLine(x);
      WriteLine(x);
    }
    else
      WriteLine ("nothing");
  }
 
  static SideEffect : int
  {
    get
    {
      WriteLine("somebody is fetching me");
      1
    }
  }
  
  public static Main() : void
  {
    def laz = lazy(SideEffect + 3);

    Foo(laz, false);
    Foo(laz, true);
  }
}

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

----- The foo called! -----
nothing
----- The foo called! -----
somebody is fetching me
4
4

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

using Nemerle;
using System.Console;
 
class InfList
{
  public Value : int;
  public Next : LazyValue[InfList];
 
  public this (v : int)
  {
    Value = v;
    Next = lazy(InfList(v + 1)); 
  }
}

def Loop(elem : InfList)
{
  Write($"$(elem.Value) ");
  Loop(elem.Next);
}

Loop(InfList(1));

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

Макрос «Record»

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

Активные строки

Многие из тех, кто программировал на Perl, PHP или Ruby, знакомы с очень удобным средством формирования строк - «строки со сплайсами (splices)». Этот подход можно назвать «активными строками». Мы уже упоминали о нем в начале статьи. Приведем пример, демонстрирующий использование активной строки внутри другой активной строки. Да-да, не падайте в обморок, это действительно работает. :)

using System.Console;

def a = 123;

WriteLine($"Test $($\"test $a test-\" + a.ToString()) test");

Выводит на консоль:

Test test 123 test-123 test

Макросы для работы с БД

В C# 3.0 и VB.NET 9.0 должна появиться поддержка DLinq. Это специальное расширение языков, позволяющее прямо в языке описывать запросы к БД. Причем компилятор сможет контролировать их корректность и типобезопасность.

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

ExecuteReaderLoop("SELECT * FROM employee WHERE firstname = $myparm", dbcon, 
{
  WriteLine("Name: $firstname, $lastname")
});

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

Литералы

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

WriteLine(1_234_567_890);

читается намного лучше, чем такой:

WriteLine(1234567890);

Кроме того, в Nemerle можно использовать двоичные литералы:

WriteLine("{0:x}", 0b01010101010);

Этот код выведет на консоль «2aa».

Пару слов о будущем

Nemerle очень молод. На момент написания данной статьи его версия была 0.9.3, то есть официально язык еще даже не существует (нет первой версии). И хотя в основном язык уже сформировался, и серьезных доработок не предвидится, но все же полностью исключить их нельзя.

Только что, пока готовилась статья, в языке появилась возможность, анонсированная в C# 3.0, – «функции-расширения». Эта возможность позволяет создавать функции, расширяющие сторонние классы. Например, можно будет добавить метод ToInt32() к классу string и вызывать его так, как будто этот метод был непосредственно реализован в классе:

"123".ToInt32()

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

Кроме методов-расширений планируется добавить также макросы-расширения. Они также будут доступны для вызова через точку, но будут (как и другие макросы) подставлять вместо себя некоторое выражение. Основное преимущество макросов заключается в том, что они могут генерировать специализированный код в зависимости от контекста. Так, можно написать макрос «Indexes()» который во время компиляции будет определять тип коллекции, к которому он применяется, и генерировать вызов соответствующего члена класса (например, Length для массивов и Count для наследников ICollection[T]), или даже заменяться конструкцией «диапазон». Это при дальнейшем раскрытии, в зависимости от контекста, может привести к порождению разных вариантов кода (например, генерации цикла «for», если конструкция использована внутри оператора foreach).

Еще одна замечательная особенность, уже описанная на сайте Nemerle, но не поддерживаемая пока компилятором – это «Traits». Traits – это безопасная замена множественному наследованию, основная цель которой – позволить программисту подключать к классам реализации некоторые относительно независимые возможности. Например, с помощью Traits очень удобно реализовывать стандартные интерфейсы. В общем-то, с помощью макросов и без Traits можно обеспечить подключение реализации. Так, например, в Nemerle есть макросы, автоматически добавляющие реализацию для методов Clone(), Equal и GetHashCode() (использующих значения всех полей), но с Traits это можно будет делать и проще, и безопаснее. А средства рефакторинга (которые, мы надеемся, рано или поздно появятся) смогут работать с ними лучше, чем с результатами работы макросов. Вот пример использования Traits:

trait TCircle[T]
{
  require
  {
    Radius() : int;
    Rotate(x : int) : T;
  }
 
  provide
  {
    Diameter() : int
    {
      Radius() * 2
    }
 
    RevRotate(x : int) : T
    {
      Rotate(-x)
    }
  }
}
...
class SomeCircle 
{
  uses TCircle;
  public Radius() : int { 42 }
  public Rotate(_ : x) : SomeCircle { this }
}

В результате компиляции этого должен получиться следующий код:

interface TCircle[T] where T : TCircle[T] 
{
  Radius() : int;
  Rotate(x : int) : T;
  Diameter() : int;
  RevRotate(x : int) : T;
 
  static impl_Diameter(_this : T) : int
  {
    _this.Radius () * 2
  }
  
  static impl_RevRotate(_this : T, x : int) : T
  {
    _this.Rotate (-x)
  }
}
...
class SomeCircle : TCircle[SomeCircle] 
{
  public Radius() : int { 42 }
  public Rotate(_ : x) : SomeCircle { this }
  public Diameter() : int { TCircle[SomeCircle].impl_Diameter (this) }
  public RevRotate(x : int) : int
  { 
    TCircle[SomeCircle].impl_RevRotate (this, x) 
  }
}
СОВЕТ

Тем, кто хорошо знаком с C#, может показаться странным наличие статических методов в интерфейсе. C# таких баловств не позволяет. Однако это ограничение языка, а не runtime-а. Физически такое вполне возможно. На уровне языка Nemerle тоже не допускает этого. Но для целей реализации более высокоуровневой абстракции этот подход вполне применим.

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

Про Nemerle можно говорить много, но злой редактор скоро начнет бить меня тяжелым тупым предметом по голове (попробовал – помогло, но топор надежнее – прим.ред.), так как нужно срочно сдавать номер. Но продолжение следует...


Эта статья опубликована в журнале RSDN Magazine #1-2006. Информацию о журнале можно найти здесь
    Сообщений 41    Оценка 3615        Оценить