Nemerle – вопросы дизайна

Автор: Чистяков Владислав Юрьевич
Источник: RSDN Magazine #1-2011
Опубликовано: 20.02.2012
Версия текста: 1.0
Введение
Список синтаксических расхождений
Объявление локальных переменных и параметров
Объявление параметров функций
Приведение типов
if без else
Объявление и инициализация массивов
Создание объектов (конструкторы)
Описание параметров типов
Присвоения и операции инкремента / декремента
Заключение
Список литературы

Введение

После выхода официальной версии Nemerle 1.0 программисты, ранее его не использовавшие, начали частенько задавать вопрос: «Почему в Nemerle есть те или иные синтаксические различия с C#». Вопрос резонный, так как в основном Nemerle близок по синтаксису и семантике к C#. Наверно, если бы Nemerle был полным надмножеством C#, то C#-программистам было бы проще осваивать Nemerle.

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

Список синтаксических расхождений

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

C#

Nemerle (изменяемые переменные)

Nemerle (неизменяемые переменные)

              // с использованием вывода типов
              var x = 42;
var str;
var dic = GetDictionary();
// с указанием типовint x = 42;
string str;
Dictionary<string, int> dic = GetDictionary();
              // с использованием вывода типов
              mutable x = 42;
mutable str;
mutable dic = GetDictionary();

// с указанием типовmutable x   : int = 42;
mutable str : string;
mutable dic : Dictionary[string, int] = GetDictionary();
              // с использованием вывода типов
              def x = 42;
def str;
def dic = GetDictionary();

// с указанием типовdef x   : int = 42;
def str : string;
def dic : Dictionary[string, int] = GetDictionary();

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

В Nemerle считается, что предпочтительнее использовать неизменяемые переменные (def-связывания).

Изменяемые переменные (и поля) объявляются с использованием ключевого слова mutable. Это ключевое слово (ввиду своей длины) хорошо заметно в коде и тем самым акцентирует внимание на изменяемых значениях внутри алгоритма.

Использование ключевых слов (mutable и def) делает синтаксис объявления переменных и параметров более регулярным (последовательным).

Объявление параметров функций

C#

Nemerle

              string Method(string message, int count = 42)
T Method<T>(T value)
void Swap<T>(ref T a, ref T b)
Method(message : string, count : int = 42) : string
Method[T](value : T) : T
Swap[T](a : ref T, b : ref T) : void

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

x, y, x : int// недопустимо!

Данный синтаксис унаследован у ML.

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

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

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

Лексема «:» отлично выделяет описание типов в коде. Его сложнее перепутать, например, с созданием экземпляра объекта и т.п.

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

Method1(count : int) : void
{
  count++; // ошибка времени компиляции!
}

Method2(mutable count : int) : void
{
  count++; // OK!
}

Следует также отметить, что модификатор mutable никак не отражается на описании или использовании самого метода. Изменяя значение изменяемого параметра, вы не сможете случайно изменить значение, переданное в качестве этого параметра. Для этого, как и в C#, необходимо воспользоваться модификатором ref. Кстати, обратите внимание на то, что модификаторы ref и out указываются у типа, а не перед параметром. Это логично, так как они относятся к описанию типа параметра.

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

C#

Nemerle

((string)value).Length
var dic = (Dictionary<string, int>)obj;
(int)'B' + 'A'
(value :> string).Length
def dic = obj :> Dictionary[string, int];
'B' : int + 'A'// не поддерживаемые в C# особенностиdef dic = obj :> Dictionary[_, _];
def dic = obj :> Dictionary[string, _];

В Nemerle есть два оператора приведения типов:

Введение двух операторов приведения типов позволяет компилятору более детально контролировать код и предотвращать появление некоторых ошибок.

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

C#

Nemerle

              var str = obj asstring;

if (str != null)
  DoSomethingWithString(str);
              match (obj)
{
  | str isstring =>  DoSomethingWithString(str);
  | _ => () // ничего не делаем
}

Паттерн «is» похоже по смыслу на аналогичный оператор C# (который также присутствует в Nemerle).

Оператор «as» часто неверно используется C#-программистами. Его использование без последующей проверки может привести к появлению трудных в обнаружении ошибок. Многим нравится, как выглядит использование этого оператора и он частенько применяется (особенно не опытными программистами) как замена для оператора приведения типов. Какое-то время такой код может вести себя нормально (не вызвать проблем), но в последствии, в результате рефакторинга тип может измениться и оператор «as» начнет возвращать «null». «null» может быть сохраняет в поле или переменной, что приведет к появлению исключения. Причем исключение это возникнет не в месте реальной ошибки, а совсем в другом месте. Это усложнит ее обнаружение и исправление.

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

Кроме того, сопоставление с образцом (т.е. оператор match) позволяет указать несколько образцов «is» подряд (с разными типами), что намного проще читать и поддерживать, нежели цепочку вложенных объявлений переменных и операторов if, как это происходит в C#-коде.

if без else

В C# if является так называемым statement-ом (корректного русского перевода, по-видимому, не существует). При этом if можно применять как с else, так и без него. Возможность применять в C# if как c else, так и без него приводит к неоднозначности. Парсер C# решает ее по принципу "else относится к ближайшему if". В Nemerle if является выражением (expression) и может применяться внутри других выражений. Это делает недопустимым наличие неоднозначности. Для устранения неоднозначности в Nemerle было принято решение заменить «if без else» на отдельный макрос when:

C#

Nemerle

              var result1 = condition ? 42 : 0;

int result2 = 0;

if (condition)
  result = 42;

int result2;

if (condition)
  result = 42;
else
  result = 0;
              def result1 = if (condition) 42 else 0;

mutable result2 = 0;

when (condition)
  result = 42;

mutable result2;

if (condition)
  result = 42;
else
  result = 0;

Кроме того в Nemerle можно использовать void-литерал () для явной декларации отсутствия вычисления:

        mutable result2 = 0;

if (condition)
  result = 42;
else
  (); // указываем, что вычисление не придвидится!
СОВЕТ

Кстати, void-литерал можно использовать в целях отладки. В приведенном выше примере можно поставить точку останова на void-литерал.

Объявление и инициализация массивов

Объявление массивов и их литералов в Nemerle сильно отличается от такового в C#. Этому способствуют следующие факторы:

1. В Nemerle, кроме встроенной поддержки массивов (как в C#), также встроена поддержка списков. Список – list[T] – это не изменяемый однонаправленный связанный список.

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

3. В Nemerle объявление литералов делается похожим на вызов конструктора. Это делает язык интуитивно понятнее.

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

C#

Nemerle

              var ary = newint[1];
ary[0] = 42;

var ary = newint[] { 42 };
var ary = new [] { 42 };

// явное указание типа переменнойint[] ary = new [] { 42 };

// массив массивов (вложенный массив)int[][] ary = new[] { new[] { 42 } };
var ary = new[] { new[] { 42 }, new[] { 33, 1 } };

// многомерные массивыvar ary = new[,] { { 42, 1 }, { 33, 2 } };
Console.WriteLine(ary[0, 0]);
              def ary = array(1);
ary[0] = 42; // тип выводится из использованияdef ary = array[1];


// явное указание типа переменнойdef ary : array[int] = array[1];

// массив массивов (вложенный массив)def ary : array[array[int]] = array[array[42]];
def ary = array[array[42], array[33, 1]];

// многомерные массивыdef ary = array.[2][[42]];
def ary = array.[2][[42, 1], [33, 2]];

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

        def ary = [1];

// явное указание типа переменнойdef ary : list[int] = [1];

// список списков (вложенный список)def ary : list[list[int]] = [[42]];
def ary = [[42], [33, 1]];

Создание объектов (конструкторы)

Nemerle поощряет использование функционального стиля программирования внутри кода методов. Для этого стиля характерно создание иерархий объектов, отражающих предметную область. Чтобы сделать синтаксис создания объектов более компактным, в Nemerle было решено отказаться от оператора «new». Конструктор в Nemerle рассматривается как функция, принимающая ноль или более параметров и возвращающая объект некоторого типа. Другими словами, как в C#, но без «new»:

C#

Nemerle

              var xml =
  new XDocument(
    new XDeclaration("1.0", "UTF-8", "yes"),
      new XElement("people",
        new XElement("idperson",
          new XAttribute("id", 1),
          new XAttribute("year", 2004),
          new XAttribute("salary", "100"))));

thrownew System.ArgumentException("foo");
              def xml = 
  XDocument(
    XDeclaration("1.0", "UTF-8", "yes"),
      XElement("people",
        XElement("idperson",
          XAttribute("id", 1),
          XAttribute("year", 2004),
          XAttribute("salary", "100"))));

throw System.ArgumentException("foo")

Такое решение также позволяет использовать конструктор там, где требуется функция (или делегат).

Описание параметров типов

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

C#

Nemerle

              interface ISomeInterface<in TIn, out TOut>
{
  TOut Method<T>(TIn value1, T value2);
}
              interface ISomeInterface[-TIn, +TOut]
{
  Method[T](value1 : TIn, value2 : T) : TOut;
}

Определение ко/контрвариантности в параметрах типов появилось в Nemerle значительно раньше нежели в C# (Nemerle поддерживает ко/контрвариантность со второго .NET Framework). Посему было принято взять синтаксис из языков, уже поддерживающих ко/контрвариантность. Выбор был остановлен на нотации «+» / «-», так как она отражает ограничение, накладываемое на тип. «+T» означает, что тип должен быть T или его наследником, а «-T» означает, что тип должен быть T или его предком.

Присвоения и операции инкремента / декремента

В Nemerle операция присвоения и операции инкремента / декремента имеют тип void. Это не позволяет использовать их в C-стиле – по несколько операций присвоения или инкремента / декремента в одном выражении. Это сделано потому, что подобное использование чревато ошибками и зачастую усложняет чтение кода.

C#

Nemerle

expr1 = expr2 = expr3


int y = x++;


int y = ++x;
expr2 = expr3;
expr1 = expr3;

def y = x;
x++;

x++;
def y = x;

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

Заключение

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

Список литературы

  1. http://nemerle.org
  2. http://nemerle.org/wiki/CsharpDiff


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