Язык Nemerle

Автор: Владислав Юрьевич Чистяков
The RSDN Group

Источник: RSDN Magazine #2-2009
Опубликовано: 24.09.2009
Версия текста: 1.0
Отвлеченное введение
О языке
Почему Nemerle?
Как использовать язык?
Инсталлятор
ncc.exe
Упрощенное введение
Первый пример – «Hello, World!»
Пространства имен и типы
Арифметика
Локальные функции
Встроенные типы данных
Вывод типов и их явное описание
Макросы
Промежуточный итог
Ссылки

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

Отвлеченное введение

На данную работу меня вдохновила книга Кернигана и Ричи – «Язык С». Много лет назад я учился программировать по этой книге. Мне очень понравилась концепция изложения использованная в этой книге. В этой книге, вместо того чтобы взять одну тему и разобрать ее по косточкам, попутно заостряя внимание на мелких деталях, давался минимум, необходимый человеку, чтобы начать программировать на «C», а затем приоткрывался аспект за аспектом. Причем изложение давалось не на абстрактных примерах, которыми так увлекаются проповедники функционального программирования, а на примерах простых, но все же из этой жизни. Понимая, что прыгнуть выше Кернигана и Ричи очень тяжело, я решил не соревноваться с ними в креативности, а просто повторить их методологию, но с расчетом на Nemerle.

Nemerle – это совсем другой язык. Он многим отличается от C, но это никак не мешает применить тот же подход, и даже те же примеры, что и в знаменитой книге «Язык С». Надеюсь, что у меня что-то получилось :).

О языке

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

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

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

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

Почему Nemerle?

«Зачем изучать именно этот язык, если вокруг есть масса других (в том числе и для платформы .Net)?» - можете спросить вы. Ответ очень прост. Это один из самых лучших и интересных языков программирование, созданных на сегодняшний день, который легко и приятно изучать. Могу сказать без сомнения, что это лучший язык, созданный для платформы .Net на сегодняшний день. Этот язык поддерживает практически все возможности, которые имеются в других языках программирования (пожалуй, кроме логического программирования, в основном представленное языком Prolog), так что изучив этот язык, вам будет проще освоить другие языки.

СОВЕТ

Кстати, в чем я искренне убежден, так это в том, что хороший программист должен знать не менее 3 языков программирования. Так что если перед вами стоит выбор, какой язык изучать, то изучите любой, а потом изучите еще парочку ;). Причем желательно, чтобы языки максимально отличались друг от друга. Я бы посоветовал познакомиться со следующими языками: Nemerle, Haskell, C++, Prolog, Erlang и Ruby. После этого большинство из остальных языков покажутся вам диалектами оных.

Конечно есть концепции не вошедшие в Nemerle. Например, «зависимые типы» (совершенно новая раработка доступная только в эсперементальных языках вроде agda2 или continuation (доступные в ряде языков). Но это уже не так много. К тому же некоторые возможности (например, те же continuation) можно реализовать в виде макросов Nemerle.

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

1. Nemerle является одним из самых современных и мощных языков программирования, доступных на сегодня.

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

3. Nemerle довольно прост в изучении, но по сравнению с не менее простыми языками программирования вроде Ruby или Python он является статически типизированным. Это с одной стороны, позволяет получать весьма быстрый исполнимый код (сравнимый по быстродействию с C/C++ и не уступающий C#), а с другой – выявлять многие ошибки еще до запуска программы.

4. Nemerle, хотя и поддерживает такие интересные парадигмы, как ФП (функциональная парадигма) и МП (метапрограммирование), не требует кардинальной перестройки сознания (как Haskell или LISP). При этом после освоения Nemerle данные языки осваиваются намного проще. Причем тут нет никакой магии. Просто Nemerle не жертвует привычными вещами, чтобы упростить работу разработчикам языка.

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

6. Большая часть операторов языка – это макросы, написанные на самом языке. База языка очень небольшая.

7. Язык поддерживает вывод типов, что позволяет в 99.9% случаев не указывать типы внутри кода. Это существенно облегчает процесс обучения.

8. Наличие интеллектуальной IDE (модуля расширения для Microsoft Visual Studio). Поддержка IDE позволяет резко упростить процесс написания и чтения (понимания) кода.

9. Nemerle полностью совместим с C# и VB на уровне библиотек. Все компоненты и библиотеки, доступные в .Net, можно использовать из Nemerle. Более того, если в публичном интерфейсе не используются вариантные типы данных, библиотеку Nemerle можно использовать из C# или VB без переделок и каких-то мучений, как это часто бывает с языками, адаптируемыми к платформе, а не разрабатываемыми для нее.

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

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

12. Nemerle – это расширяемый язык, в котором вы можете воплотить свои, казалось бы, самые нереальные фантазии.

13. Nemerle – новаторский, но отнюдь не экспериментальный язык. Он очень практичен.

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

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

Как использовать язык?

Самым удобным способом писать на Nemerle является использование «Интеграции с Microsoft Visual Studio» (далее VS). Однако VS, как и любая IDE (интегрированная среда разработки), скрывает большинство особенностей работы с проектом, которые следует знать новичку. Поэтому в начале я буду использовать компилятор командной строки.

Инсталлятор

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

Если вы используете инсталлятор, то лучше всего располагать файлы в предложенном по умолчанию каталоге (%ProgramFiles%\Nemerle).

ncc.exe

Компилятор командной строки Nemerle называется ncc.exe. По умолчанию он находится в %ProgramFiles%\Nemerle (например, на моей машине это «c:\Program Files\Nemerle»).

Там же находятся библиотеки, относящиеся к «Интеграции с Microsoft VS» и библиотеке времени выполнения Nemerle.dll, которая требуется для запуска программ, получаемых с помощью компилятора Nemerle. Для упрощения использования ncc добавьте путь %ProgramFiles%\Nemerle в переменную среды окружения PATH (о том, как это сделать, см. здесь). Тогда вы сможете вызывать ncc из любого каталога на вашей машине.

Ncc принимает в качестве аргументов список файлов, которые требуется скомпилировать, и набор ключей. Ключи нужно предварять символом «-» (минус) или «/» (обратный слеш). Полный список ключей компиляции можно получить, введя в командной строке

ncc -h

У ncc есть много ключей, но для нас сейчас важны только следующие:

Ключ Описание
-no-color Подавляет вывод специальных эскейп-символов, используемых для раскраски выводимых сообщений в Linux. Под Windows, если не установлена специальная поддержка, эти эскейп-символы замусоривают вывод компилятора. Так что если у вас нет этой поддержки, всегда указывайте –no-color в командной строке.
-out:STRING Позволяет задать имя выходного файла (т.е. исполняемого exe-файла или dll). Имя файла задается вместо «STRING». Если не указать данный ключ, то компилятор всегда будет генерировать исполняемый файл с именем «out.exe».
-reference:STRING Подключение внешних библиотек (называемых сборками в .Net).
-target:STRING Тип выходного файла: exe (исполняемый файл), library (библиотека)
-nostdmacros Не подключать макросы из стандартной библиотеки Nemerle
-macros:STRING Подключить сборку, содержащую макросы

Упрощенное введение

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

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

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

Первый пример – «Hello, World!»

Как метко подмечено в книге «Язык C» (которая в немалой степени вдохновила меня на данную работу и послужила прототипом), «Единственный способ освоить новый язык программирования – писать на нем программы. Первая программа, которая должна быть написана, одна для всех языков: напечатать предложение "Hello, World!"».

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

        using System.Console;

WriteLine("Hello, World!");

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

ncc -no-color HelloWorld.n -out:HelloWorld.exe

И вызвать получившуюся программу «HelloWorld.exe».

ПРИМЕЧАНИЕ

Если вы пользуетесь Mono (независимой реализацией .Net, поддерживаемой компанией Novell и работающей под Windows и Linux), то для запуска программы вам потребуется ввести в командную строку «mono HelloWorld.exe».

Если вы нигде не ошибетесь, то после запуска «HelloWorld.exe» на консоль (т.е. на экран компьютера) будет выведено:

Hello, World!


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

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

Функции

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

ПРИМЕЧАНИЕ

Попробуйте заменить имя функции с WriteLine на Write и посмотрите на то, как изменится вывод на консоль.

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

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

Функции и другие действия можно выполнять последовательно. При этом отдельные действия (выражения или вызовы функций) нужно отделять знаком «;» (точка с запятой). Это поведение аналогично тому, как это было принято в языке Pascal. Впрочем, знак «;» можно просто ставить в конце каждого отдельного действия (как это принято в C-подобных языках, таких как C#, Java и C++).

Следующий пример:

          using System.Console;

WriteLine("Hello, World!");
WriteLine("Привет, Мир!");
WriteLine("Мир! Привет!");

выведет:

Hello, World!
Привет, Мир!
Мир! Привет!

Последнюю точку с запятой можно смело не указывать.

СОВЕТ

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

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

          using System.Console;

Write("Hello");
Write(", World");
WriteLine("!");

выведет, как и исходный пример, строку:

Hello, World!

Строки и строковые литералы

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

Для представления заранее известного (статического) текста используются строковые литералы (или, как еще их иногда называют, «строковые константы»). Nemerle поддерживает три типа строковых литералов, но на данный момент нам будет достаточно одного типа – обычного строкового литерала (regular string literals).

Обычный строковый литерал – это просто строка символов, которая заключается в пару двойных кавычек (то есть в пару символов «"»). Такие строки должны располагаться на одной строке (не могут содержать символ новой строки) и не могут содержать в себе символ двойной кавычки «"». Чтобы вписать эти символы в строку, нужно воспользоваться так называемой эскейп-нотацией. Это мудреное словосочетание означает, что перед такими символами нужно поставить символ «\» (экскейп-символ). Естественно, что сам символ «\» тоже не может встречаться в строке самостоятельно (так как компилятор воспримет его как управляющую конструкцию), так что если вам нужно написать в строке этот символ, то его нужно удвоить.

Этот тип строкового литерала часто называют C-строкой (си-строкой), так как впервые он появился в языке «C».

Остальные виды строковых литералов вы освоите позже. Если они вас интересуют уже сейчас, обратитесь к подразделу «Строковые литералы» из раздела «Расширенное описание».

Пространства имен и типы

Думаю, что в этом примере ясно все, за исключением строки:

        using System.Console;

Если вы удалите эту строку, то компилятор выдаст сообщение об ошибке:

HelloWorld.n:3:1:3:10: error: unbound name `WriteLine'

Это означат, что компилятор не нашел имя функции.

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

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

Функции могут объявляться только в типах (таких, как модуль) и внутри других функций. Типы могут объявляться в пространствах имен, в других типах или в глобальном пространстве имен (безымянном). Например, функция WriteLine объявлена в модуле Console, который объявлен в пространстве имен System. Модуль Console объединяет все функции, связанные с консольным вводом/выводом, а System объединяет все системные типы (то есть базовые для системы). Например, тип «строка» также объявлен в пространстве имен System.

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

Строка «using System.Console;», так сказать, открывает тип «System.Console». После этого содержимое данного типа можно использовать напрямую. В принципе, и не открывая типы и/или пространства имен можно дотянуться до нужных функций. Наш исходный пример можно переписать следующим образом:

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

Такой способ задания имен называется полностью квалифицированным.

СОВЕТ

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

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

        using System;

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

Два последних варианта выведут на консоль то же самое, что и самый первый вариант.

Арифметика

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

Например, чтобы пересчитать (по формуле «C = 5 / 9 * (F - 32)») 40 градусов по Фаренгейту в эквивалент по шкале Цельсия можно написать следующую программу:

        using System.Console;

WriteLine(5.0 / 9.0 * (40.0 - 32.0));

Поместите ее в файл f2c.n, скомпилируйте его и запустите на исполнение (это вы уже должны уметь).

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

4,44444444444444

Странным здесь выглядит то, что к числам дописаны «.0». Это необходимо потому, что в Nemerle, как и во всех C-подобных языках, целые числа и числа с плавающей точкой (или запятой, как ее принято называть в России) записываются по-разному. Более того! Деление и умножение с ними также выполняется по-разному. При делении целого числа на целое же число в результате также получается целое число. Дробная часть при этом отбрасывается. Так что если попытаться поделить 5 / 9:

        using System.Console;

WriteLine(5 / 9);

то мы получим не 0.555555, а 0!

ПРИМЕЧАНИЕ

Это не вполне интуитивное поведение объясняется тем, что арифметика в Nemerle, как и в его древнем предке языке C, привязана к архитектуре процессоров, на которой исполняется программа. Современные процессоры имеют отдельные вычислительные устройства (АЛУ, Арифмети́ческо-логи́ческое устро́йство) для целочисленных вычислений и отдельное (часто называемое сопроцессором) для операций над числами с плавающей точкой.

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

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

Если же хотя бы один из аргументов (операндов) выражения будет числом с плавающей точкой, то второй операнд операции деления будет автоматически приведен к типу другого операнда. Другими словами, будет произведено неявное приведение типов к большему типу (типу, который может содержать значения из большего диапазона). Приведение типов констант осуществляет компилятор. Так что в итоге мы получаем максимально эффективный код. Таким образом, благодаря автоматическому приведению типов, вместо выражения «5.0 / 9.0 * (40.0 - 32.0)» мы могли бы написать «5.0 / 9 * (40 - 32)» или «5 / 9.0 * (40 - 32)». Все остальные типы компилятор привел бы сам. Но чем более явно выражено намерение, тем меньше места остается для домыслов. А чем меньше домыслов (при прочих равных), тем проще программисту понять, что написано в коде, а стало быть, тем сложнее сделать ошибку. Кстати, если бы мы написали «5 / 9 * (40.0 - 32)», то компилятор привел бы к типу числа с плавающей точкой уже результат целочисленного деления, что в итого дало бы неверный (для нас) результат.

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

Функции дают удобный способ заключения некоторой части вычислений в «черный ящик», который в дальнейшем можно использовать, не интересуясь его внутренним содержанием. Использование функций в Nemerle (да и в остальных языках) является одним из важнейших механизмов, позволяющих справляться с потенциальной сложностью больших программ. Если функции организованы должным образом, то можно игнорировать, как делается работа; достаточно знания того, что делается. Nemerle недаром называется функциональным языком. В нем использование функций доведено до совершенства. Функции не только легко и удобно создавать (как в C), но ими еще точно так же легко манипулировать. Их можно передавать в другие функции, хранить в переменных и даже динамически создавать новые функции из нескольких других. Вам будут часто встречаться функции длиной всего в несколько строчек, вызываемые только один раз. Они создаются, чтобы прояснять некоторую часть программы. Ведь даже само имя функции может сказать о многом. В сочетании же со списком параметров функция является незаменимым источником информации о программе.

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

        using System.Console;

def fahrenheitToCelsius(fahrenheit)
{
  5.0 / 9 * (fahrenheit - 32)
}

WriteLine(fahrenheitToCelsius(40));
ПРЕДУПРЕЖДЕНИЕ

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

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

4,44444444444444

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

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

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

f2c.n:4:3:4:6: error: parse error near double literal: expected `{' 
at the beginning of function body

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

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

Выведем информацию таким образом:

Fahrenheit:   40 Celsius:   4,4

Следующий пример делает это:

        using System.Console;

def fahrenheitToCelsius(fahrenheit)
{
  5.0 / 9 * (fahrenheit - 32)
}

WriteLine("Fahrenheit: {0, 4} Celsius: {1, 5:##0.0}", 40, 
  fahrenheitToCelsius(40));

Немного о форматированном выводе

Как видите, вместо «{0, 4}» выводится «40», а вместо «{1, 5:##0.0}» «4,4», причем числа выравниваются вправо. Это называется форматированным выводом. Каждая пара фигурных скобок внутри строки описывает заполнитель (placeholder), вместо которого подставляется значение параметров функции WriteLine (идущих за строкой формата). Значение какого параметра будет подставлено вместо заполнителя, определяется первым числом. «0» означает, что будет использовано значение второго параметра (первого, идущего за строкой формата), «1» – что третьего и т.д. Следующие поля заполнителя – необязательные, но нам они нужны. После запятой указывается выравнивание поля. Не заполненные значениями символы заменяются пробелами в количестве, указанном после запятой. Двоеточие начинает раздел, предназначенный для строки формата (так же необязательный раздел). Я не буду подробно описывать всевозможные форматы, так как их довольно много. Все, что касается формата, вы сможете прочесть в MSDN (общей документации компании Microsoft) в примечании к описанию метода string.Format. Скажу только, что формат второго заполнителя позволяет отбросить ненужные нам знаки после точки и вывести всего один знак.

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

В Nemerle (да и в .Net в целом) принято начинать нумерацию с нуля. Обратите на это внимание!

Усложним задачу. Вместо одного значения выведем 16, с шагом в «20» градусов:

Fahrenheit:    0 Celsius: -17,8
Fahrenheit:   20 Celsius:  -6,7
Fahrenheit:   40 Celsius:   4,4
Fahrenheit:   60 Celsius:  15,6
Fahrenheit:   80 Celsius:  26,7
Fahrenheit:  100 Celsius:  37,8
Fahrenheit:  120 Celsius:  48,9
Fahrenheit:  140 Celsius:  60,0
Fahrenheit:  160 Celsius:  71,1
Fahrenheit:  180 Celsius:  82,2
Fahrenheit:  200 Celsius:  93,3
Fahrenheit:  220 Celsius: 104,4
Fahrenheit:  240 Celsius: 115,6
Fahrenheit:  260 Celsius: 126,7
Fahrenheit:  280 Celsius: 137,8
Fahrenheit:  300 Celsius: 148,9

Конечно, можно просто шестнадцать раз скопировать строку:

WriteLine("Fahrenheit: {0, 4} Celsius: {1, 5:##0.0}", 40, 
  fahrenheitToCelsius(40));

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

        using System.Console;

def fahrenheitToCelsius(fahrenheit)
{
  5.0 / 9 * (fahrenheit - 32)
}

def loop(fahrenheit)
{
  WriteLine("Fahrenheit: {0, 4} Celsius: {1, 5:##0.0}", 
    fahrenheit, fahrenheitToCelsius(fahrenheit));

  loop(fahrenheit + 20);
}

loop(0);

Обратите внимание на выделение. Это рекурсивный вызов функции (т.е. вызов функцией самой себя).

ПРИМЕЧАНИЕ

Работает рекурсивный вызов очень просто. Когда управление доходит до места рекурсивного вызова, то функция вызывается еще раз, но с новыми значениями параметров (в нашей программе значение единственного параметра «fahrenheit» увеличивается на 20). Далее процесс повторяется (в нашем примере новое значение снова выводится на консоль, и снова происходит рекурсивный вызов). Переполнения стека (т.е. области памяти, где размещаются значения аргументов функций и адреса возврата) при этом не происходит вследствие оптимизации компилятором Nemerle хвостовой рекурсии.

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

Наверно, многие из вас уже догадались, что данный код выведет не совсем то, что было запланировано. Более того, данный код попросту не завершится! Вам придется нажать Ctrl+C, чтобы прервать работу программы. Иначе она еще долго будет прокручивать консольное окно, выводя все новые и новые значения.

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

Если вы знаете какие-то другие языки программирования, то наверняка догадались, что таким средством обычно является оператор if/else. Все правильно, но в Nemerle такого оператора также нет. :)

Зато в Nemerle есть оператор match. Это так называемый оператор сопоставления с образцом. Он позволяет сравнить значение с одним или несколькими образцами и выполнить некоторые действия, если некоторый образец совпал с проверяемым значением.

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

        using System.Console;

def fahrenheitToCelsius(fahrenheit)
{
  5.0 / 9 * (fahrenheit - 32)
}

def loop(fahrenheit)
{
  WriteLine("Fahrenheit: {0, 4} Celsius: {1, 5:##0.0}", 
    fahrenheit, fahrenheitToCelsius(fahrenheit));

  match (fahrenheit)
  {
    | 300 => ()
    | _   => loop(fahrenheit + 20);
  }
}

loop(0);

Суть оператора match очень проста. В качестве единственного параметра ему передается некоторое значение, а в теле (состоящем из блока) размещается список образцов. Каждый образец начинается со знака «|» и состоит из одного выражения, которое и является образцом. Далее может идти еще один образец или «=>». За «=>» идут выражения/действия (одно или более) которое вычисляется, если один из образцов, перечисленных перед «=>», совпал с переданным значением (со значением в скобках, идущих сразу за ключевым словом «match»).

В нашей программе в качестве сопоставляемого значения используется текущее значение параметра fahrenheit, а в качестве образцов – выражение «300», и выражение «_». Подчеркивание – «_» – это особая штука в Nemerle. Она называется подстановочным символом (wildcard). Когда wildcard используется в качестве образца, то оно сопоставляется с любым значением. Таким образом, если первым в списке образцов поставить образец «| _ », то всегда будет сопоставляться только он, а остальные ветви никогда не получат управления (никогда не будут вычислены). Но компилятор Nemerle не так глуп, чтобы допустить такую ситуацию. Компилятор поймет, что все образцы, кроме первого, не имеют смысла, и выдаст предупреждение об этом. Если же такой образец расположен в конце списка образцов, то он будет использоваться, если никакие другие образцы не совпали (не сопоставились) со значением. Таким образом, порядок следования образцов в операторе match может быть важен (о случаях, когда он не важен, мы поговорим позднее).

В нашем случае образец «_» – это как раз то, что нужно! Ведь если значение fahrenheit не равно тремстам, то нам нужно рекурсивно вызвать текущую функцию еще раз. Если же значение равно тремстам, то следует остановить рекурсию.

ПРИМЕЧАНИЕ

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

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

К счастью, есть выражение «()» которое означает «ничто». Это выражение также не осуществляет никаких действий. Вписав его после «=>», мы говорим компилятору, что намеренно хотим, чтобы компилятор ничего не сделал. Кроме того, компилятор делает вывод о том, что результат оператора match также является «ничем», а так как оператор match является последним оператором функции, то и возвращаемым значением функции тоже будет «ничто». Это как раз то, что нам нужно, так как функция «loop» и не должна ничего возвращать. Ее задача – вывод информации на консоль.

ПРИМЕЧАНИЕ

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

Задача выполнена, но есть нюансы. Код нашей функции loop рассчитывает на то, что в качестве параметра этой функции будет передано верное значение (лежащее в диапазоне между 0 и 300). Если мы ошибемся и, например, передадим в loop не «0», а, скажем, «400», то наша функцию снова никогда не завершится. Как же исправить ситуацию?

Для этого можно воспользоваться операторами сравнения «больше»/«меньше» и еще одним оператором match:

        using System.Console;

def fahrenheitToCelsius(fahrenheit)
{
  5.0 / 9 * (fahrenheit - 32)
}

def loop(fahrenheit)
{
  WriteLine("Fahrenheit: {0, 4} Celsius: {1, 5:##0.0}", 
    fahrenheit, fahrenheitToCelsius(fahrenheit));

  match (match (fahrenheit >= 0) { | true  => fahrenheit < 300 | false => false })
  {
    | true  => loop(fahrenheit + 20);
    | false => ()
  }
}

loop(400);

Разберем вложенный оператор match:

        match (fahrenheit >= 0) { | true  => fahrenheit < 300 | false => false }

СОВЕТ

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

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

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

Вторая особенность – это использование операторов «>=» и «<» – это «операторы сравнения» (изучите таблицу, приведенную ниже, чтобы понять, как работают операторы сравнения).

Оператор сравнения Название Примеры (после «=>» указывается результат)
< «меньше» 1 < 2 => true
1 < 1 => false
2 < 1 => false
<= «меньше или равно» 1 <= 2 => true
1 <= 1 => true
2 <= 1 => false
> «больше» 1 > 2 => false
1 > 1 => false
2 > 1 => true
== «равно» 1 == 2 => false
1 == 1 => true
2 == 1 => false
>= «больше или равно» 1 >= 2 => false
1 >= 1 => true
2 >= 1 => true
!= «не равно» 1 != 2 => true
1 != 1 => false
2 != 1 => true

Если fahrenheit больше или равен нулю, оператор сравнения «>=» возвращает значение «истина» (true), которое сопоставляется с образцом «| true», что приводит к выполнению выражения «fahrenheit < 300». Оно, в свою очередь, становится равным true, если fahrenheit меньше трехсот, и false в обратном случае. Если же значение fahrenheit меньше нуля, то оператор «>=» вернет значение false, которое сопоставится с образцом «| false». В этом случае возвращаемым значением оператора match будет «false» (так как за «=>» у этого вхождения match идет выражение «false»).

Таким образом, если значение fahrenheit больше или равно нулю и меньше трехсот, match вернет true, иначе он вернет false.

Кому-то данное выражение может показаться громоздким. Охотно соглашусь с этим мнением! Как сейчас любят говорить представители молодежи – «слишком много букв». Ведь нам всего лишь нужно было связать два оператора сравнения союзом «и». В нашем коде было логично использовать оператор, аналогичный этому союзу – принимающий два логических значения и возвращающий true, если оба значения равны true, и false в противном случае. Такой оператор – «&&» – встроен во все C-подобные языки (C, C#, Java и C++). Но в Nemerle его нет. Сравните:

        match (fahrenheit >= 0)
{
  | true  => fahrenheit < 300
  | false => false
}

и

fahrenheit >= 0 && fahrenheit < 300

Несомненно, код Nemerle более громоздкий, а стало быть, менее выразительный!

Однако есть и преимущество. В языке меньше конструкций, а значит, его проще понять и запомнить.

ПРИМЕЧАНИЕ

Никлуас Вирт, создатель знаменитого языка программирования Pascal (а так же ряда других менее знаменитых языков) и вообще знаковая фигура в IT, считает, что простота – главное достоинство языка программирования с точки зрения обучения.

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

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

    | true  => loop(fahrenheit + 20);

внешнего оператора match. Это приводит к рекурсивному вызову функции loop.

Если же значение вложенного оператора – false, то выполняется вхождение:

    | false => ()

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

Таким образом, ошибка в аргументе (значение 400 вместо нуля) не приведет к зацикливанию. Вместо этого просто один раз будет выдано значение:

Fahrenheit  400 Celsius 204,4

Обратите также внимание на то, что в коде не использован образец «_», и на то, что изменился тип образца. В прошлом варианте образцом служило целочисленное значение. А в этом – булево (логическое). Это еще один встроенный тип данных Nemerle (и заодно .Net). Он может принимать только два значения true и false. Данный тип несовместим ни с какими другими, кроме себя. Так что если попытаться передать в оператор match выражение, тип которого не булев, например:

...
  match (fahrenheit)
  {
    | true  => loop(fahrenheit + 20);
    | false => ()
  }
...

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

f2c.n:15:7:15:11: error: expected bool, got int+ in matched value: 
System.Boolean is not a subtype of System.Int32 [simple require]

(в переводе: ожидается булево значение, а получено целое число)

ПРИМЕЧАНИЕ

Задания:

1. Перепишите код функции loop так, чтобы в случае передачи значения, лежащего вне диапазона 0 .. 300, на консоль выдавалось сообщение об ошибке.

2. Перепишите код функции loop так, чтобы эта функция принимала два параметра: начальное и конечное значение диапазона, проверяла бы, что первое меньше второго более чем на 20, и только в этом случае выводила список значений для заданного диапазона.

3. Напишите программу печати таблицы соответствий градусов Цельсия градусам Фаренгейта (т.е. обратную таблицу).

Встроенные типы данных

В сообщении компилятора из предыдущего раздела:

f2c.n:15:7:15:11: error: expected bool, got int+ in matched value: 
System.Boolean is not a subtype of System.Int32 [simple require]

есть слова «bool», «System.Boolean», «int» и «System.Int32». Если до этого вы не программировали на статически типизированных языках, то сообщаю вам, что это типы данных. Откуда они взялись? Их вывел компилятор. Компилятор Nemerle довольно умен, чтобы угадывать тип параметров (и переменных, о которых речь пойдет дальше) на основании того, чем эти параметры инициализируются, и даже от того, в каком контексте они используются. Компилятор видит, что параметр fahrenheit подставляется в оператор match, все вхождения в котором имеют булев тип (булев тип в Nemerle имеет имя «bool»). Также он видит, что параметр складывается с «20», и делает вывод, что параметр также должен быть целочисленным значением (32-битное целочисленное значение в Nemerle имеет тип «int»). Компилятор пытается унифицировать эти типы, но так как это не удается, он выдает сообщение об ошибке, гласящее, что «ожидается bool, а получен int+».

Про «+» я все расскажу позже, когда дойдет время. А сейчас я лучше поясню, что такое System.Boolean, System.Int32 и откуда они здесь взялись. System.Boolean и System.Int32 – это название типов «bool» и «int» соответственно, принятые в .Net. Точнее сказать, что в Nemerle для типов System.Boolean и System.Int32 созданы соответствующие псевдонимы. Так что это одни и те же типы, просто какая-то часть компилятора использует имена псевдонимов, а какая-то – имена типов, принятых в .Net.

Вот список базовых типов .Net и их Nemerle-псевдонимы:

Тип .Net Alias Nemerle Описание
System.Byte; byte беззнаковое целое, 8 бит
System.SByte; sbyte целое со знаком, 8 бит
System.Int16; short целое со знаком, 16 бит
System.UInt16; ushort беззнаковое целое, 16 бит
System.Int32; int целое со знаком, 32 бита
System.UInt32; uint Беззнаковое целое, 32 бита
System.Int64; long целое со знаком, 64 бита
System.UInt64; ulong беззнаковое целое, 64 бита
System.Single; float Число с плавающей точкой, 32 бита
System.Double; double Число с плавающей точкой, 64 бита
System.Decimal; decimal Число с плавающей точкой очень большой точности
System.String; string Строка
System.Object; object Объект, к данному типу можно привести почти любой тип
System.Boolean; bool булев тип (true, false)
System.Char; char Символ. 16-битное целое, представляющее символы строк.
System.Void void Отсутствие типа. Или «никакой тип»

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

ПРИМЕЧАНИЕ

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

Тип void – это особый тип, который, по существу, типом и не является. Он используется, когда требуется показать, что функция или выражение не возвращает никакого значения, или как уже говорилось выше, возвращает «ничто». Этот тип не может быть назначен параметрам или переменным. Однако выражение может иметь тип void. Приведенное в предыдущем разделе выражение () как раз и имеет этот тип. Кроме того, тип void имеет и функция loop.

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

Сейчас важно понимать, что у любого значения в Nemerle есть тип.

Вывод типов и их явное описание

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

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

        using System.Console;

def fahrenheitToCelsius(fahrenheit : int) : double
{
  5.0 / 9 * (fahrenheit - 32)
}

def loop(fahrenheit : int) : void
{
  WriteLine("Fahrenheit: {0, 4} Celsius: {1, 5:##0.0}", 
    fahrenheit, fahrenheitToCelsius(fahrenheit));

  match (match (fahrenheit >= 0)
         {
           | true  => fahrenheit < 300
           | false => false
         })
  {
    | true  => loop(fahrenheit + 20);
    | false => ()
  }
}

loop(0);
СОВЕТ

Обратите внимание. Вложенный оператор match отформатирован иначе. Но это все то же выражение. Изменилось только его форматирование!

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

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

        def fahrenheitToCelsius(fahrenheit : int) : double
{
  (5 : double) / 9 * (fahrenheit - 32)
}

Синтаксис уточнения типа (а именно так называется данная операция) является «выражение : тип».

ПРИМЕЧАНИЕ

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

СОВЕТ

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

ПРИМЕЧАНИЕ

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

Макросы

Первый макрос

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

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

ncc -nostdmacros -no-color f2c.n -out: f2c.exe
СОВЕТ

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

@echo off
ncc -nostdmacros -no-color %1.n -out:%1.exe
if %errorlevel% == 0 %1.exe

Программа перестала работать корректно? Не пугайтесь. Всему виной приоритеты операторов, которые размещаются в стандартной библиотеке. Чтобы исправить ошибку, всего лишь нужно окружить скобками выражение «5.0 / 9»:

(5.0 / 9) * (fahrenheit - 32)

Сделайте это и попытайтесь скомпилировать код еще раз. Получилось? Тогда двинемся дальше.

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

Макрос, реализующий оператор «&&», имеющийся в C-подобных языках, будет выглядеть так:

          macro @&& (e1, e2) 
{
  <[ 
    match ($e1)
 {
      | true  => $e2
      | false => false
    }
  ]>
}

Поместите этот код в файл MyMacroses.n и скомпилируйте его в библиотеку. Командная строка для этого должна быть примерно такой:

ncc -no-color -target:library MyMacroses.n -ref:Nemerle.Compiler.dll -out:MyMacroses.dll

Что же изменилось в командной строке, за исключением имени исходного файла (MyMacroses.n)?

Во-первых, добавился ключ «-target:library». Он приказывает компилятору генерировать библиотеку, а не исполняемый файл. Во-вторых, изменилось расширение выходного файла (на dll, что расшифровывается как Dynamic Link Library, т.е. динамически подключаемая библиотека). И, в-третьих, был добавлен ключ «-ref:Nemerle.Compiler.dll» которые приказывает компилятору добавить ссылку на библиотеку «Nemerle.Compiler.dll». Nemerle.Compiler.dll – это библиотека, в которой реализован компилятор Nemerle. Утилита ncc.exe, которой мы все это время пользовались для сборки наших программ, на самом деле всего лишь предоставляет интерфейс командной строки. Вся же логика компилятора реализована в Nemerle.Compiler.dll, которую и использует ncc.exe.

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

ПРИМЕЧАНИЕ

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

Итак, если вы сделали все верно, то после выполнения приведенной выше команды в текущем каталоге появится файл MyMacroses.dll. Это сборка, содержащая наш первый макрос.

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

Для этого в командную строку, собирающую эту программу, нужно добавить ключ «-macros:MyMacroses.dll». В итоге командная строка должна выглядеть примерно так:

ncc -nostdmacros -no-color f2c.n -macros:MyMacroses.dll -out: f2c.exe
СОВЕТ

Опять же имеет смысл создать командный файл, который будет собирать MyMacroses.dll, затем, в случае успеха, собирать программу, и случае успешной сборки программы запускать ее на выполнение. Вот готовый командный файл «c.cmd», делающий это:

          @echo off
set PATH=%ProgramFiles%\Nemerle;%PATH%

rem %1 - имя файла программы
rem %2 - имя файла библиотеки маросов
if "%1" == "" goto usage
if "%2" == "" goto usage

rem Компилируем библиотеку макросов
ncc -no-color -target:library %2.n -ref:Nemerle.Compiler.dll -out:%2.dll
rem Завершаем пакетный файл в случае неудачи компиляции
if NOT %errorlevel% == 0 exit /B %errorlevel%

rem Компилируем программу
ncc -nostdmacros -no-color %1.n  -macros:%2.dll -out:%1.exe
rem Завершаем пакетный файл в случае неудачи компиляции
if NOT %errorlevel% == 0 exit /B %errorlevel%


rem Запускаем программу на исполнение
%1.exe
rem Завершаем командный файл
exit /B 0

rem Вывод описания использования командного файла
:usage
echo usage: c.cmd ProgramFileName MacrosesFileName
СОВЕТ

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

c f2c MyMacroses

Теперь мы можем воспользоваться макросом «&&» в нашей программе. Заменим вложенный match следующим образом:

          using System.Console;

def fahrenheitToCelsius(fahrenheit)
{
  (5.0 / 9) * (fahrenheit - 32)
}

def loop(fahrenheit)
{
  WriteLine("Fahrenheit: {0, 4} Celsius: {1, 5:##0.0}", 
    fahrenheit, fahrenheitToCelsius(fahrenheit));

  match (fahrenheit >= 0 && fahrenheit < 300)
  {
    | true  => loop(fahrenheit + 20);
    | false => ()
  }
}

loop(0);

Теперь скомпилируем программу. Не забудьте перед этим скомпилировать макро-сборку, в которой находится код макроса «&&». Сборка MyMacroses.dll должна находиться в том же каталоге, что и исходный файл f2c.n. Проще всего для этого воспользоваться приведенным выше командным файлом.

К сожалению, во время компиляции вы получите сообщение об ошибке:

f2c.n:13:10:13:59: error: in argument #1 of <.s, needed a int, got bool-: 
System.Boolean is not a subtype of System.Int32 [simple require]

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

          match ((fahrenheit >= 0) && (fahrenheit < 300))

Если теперь скомпилировать пример и запустить его на выполнение, то все будет работать как надо.

Проблему с приоритетами можно решить, добавив в начало файла файл MyMacroses.n следующие строки:

          using Nemerle.Internal;

[assembly: OperatorAttribute("Nemerle.Core", "*",  false, 260, 261)]
[assembly: OperatorAttribute("Nemerle.Core", "/",  false, 260, 261)]
[assembly: OperatorAttribute("Nemerle.Core", "+",  false, 240, 241)]
[assembly: OperatorAttribute("Nemerle.Core", "-",  false, 240, 241)]
[assembly: OperatorAttribute("Nemerle.Core", "<",  false, 210, 211)]
[assembly: OperatorAttribute("Nemerle.Core", ">",  false, 210, 211)]
[assembly: OperatorAttribute("Nemerle.Core", "<=", false, 210, 211)]
[assembly: OperatorAttribute("Nemerle.Core", ">=", false, 210, 211)]
[assembly: OperatorAttribute("Nemerle.Core", "==", false, 165, 166)]
[assembly: OperatorAttribute("Nemerle.Core", "!=", false, 165, 166)]
[assembly: OperatorAttribute("Nemerle.Core", "&&", false, 160, 161)]
[assembly: OperatorAttribute("Nemerle.Core", "||", false, 150, 151)]

macro @&& (e1, e2) 
{
  <[ 
    match ($e1)
 {
      | true  => $e2
      | false => false
    }
  ]>
} 

Это описание приоритетов операторов, задаваемое с помощью глобального атрибута OperatorAttribute. Об атрибутах речь пойдет в следующих главах. Сейчас это не важно. Сейчас важно понимать, что означают параметры этого атрибута. Первый параметр задает пространство имен, в котором объявлены операторы. "Nemerle.Core" – это системное пространство имен, которое считается открытым по умолчанию (т.е. оно открыто автоматически, и его не надо открывать вручную). Второй параметр – это имя оператора. Третий сообщает, является ли оператор унарным (true) или бинарным (false). Унарный оператор имеет один аргумент. Например, если нам нужно изменить знак числа, то мы пишем «-x». Минус в данном случае – это унарный оператор. А если мы из одного числа вычитаем другое («2 – 1»), то применяем бинарный оператор. Следующие два параметра задают приоритет операторов (справа и слева соответственно). Чем ниже число, тем ниже приоритет. Так, для оператора «||» мы задали самый низкий приоритет (150), а для «*» и «/» – самый высокий. При этом у знаков «*» и «/» приоритет одинаковый.

ПРИМЕЧАНИЕ

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

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

Разбор первого макроса

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

          macro @&& (e1, e2) 
{
  <[ 
    match ($e1)
 {
      | true  => $e2
      | false => false
    }
  ]>
}

Что это такое – макрос? Из чего же он состоит? И что он делает?

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

Единственное внешнее отличие макроса от функции заключается в том, что он предваряется ключевым словом «macro».

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

В нашем случае тело макроса состоит из выражения:

  <[ 
    match ($e1)
 {
      | true  => $e2
      | false => false
    }
  ]>

В этом выражении заключена определенная магия! Дело в том, что код (в данном случае оператор match), заключенный в скобки вида «<[ ]>» рассматривается компилятором не как текст программы, который нужно преобразовать в исполняемый код, а как некий шаблон фрагмента кода, который требуется преобразовать в специальное внутреннее представление, используемое в компиляторе. Такое представление называется AST (Abstract Syntax Tree, Абстрактное синтаксическое дерево). Именно такой тип имеют параметры макроса и именно такой тип нужно возвратить из макроса.

Такой шаблон в Nemerle называется квази-цитатой. С цитатой, думаю, все понятно. Мы как бы цитируем код. А что значит приставка «квази»? Вы, наверно, обратили внимание, что внутри приведенной выше цитаты есть выражения «$e1» и «$e2». Это ссылки на соответствующие параметры макроса («e1» и «e2»). Знак «$» нужен, чтобы указать компилятору, что это не код внутри цитаты, а именно ссылки на выражения, объявленные вне цитаты. Эти выражения подставляются в цитату (в том месте, где на них ссылаются) и тем самым формируют результирующий код.

В коде программы в макрос передаются два выражения:

fahrenheit >= 0 

и

fahrenheit < 300

Они подставляются вместо «$e1» и «$e2», и в результате формируется код:

          match (fahrenheit >= 0)
 {
      | true  => fahrenheit < 300
      | false => false
    }

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

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

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

Внимательный читатель мог заметить, что осталось два неясных момента:

1. Что означает знак «@» перед «&&»?

2. Как компилятор догадывается, что макрос описывает оператор?

Знак «@» – это специальный символ, используемый в языке для указания того, что некоторое имя является просто обычным именем (идентификатором), а не специальным именем (именем оператора или ключевым словом).

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

ПРИМЕЧАНИЕ

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

Что касается операторов то, во-первых, компилятор рассматривает все последовательности символов состоящие из символов: '=', '<', '>', '@', '^', '&', '-', '+', '|', '*', '/', '$', '%', '!', '?', '~', '.', ':' и '#' именами операторов. А во-вторых, компилятор позволяет использовать макросы, имеющие один или два параметра, как операторы. Таким образом, можно создать операторы с буквенными именами (например, «and» и «or»).

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

...
  match (@&&(fahrenheit >= 0,  fahrenheit < 300))
...

Компилятор понял бы нас.

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

Понимайте макросы правильно!

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

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

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

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

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

ПРИМЕЧАНИЕ

Задания:

1. Напишите макрос «||», реализующий операцию «логическое или».

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

3. Добавьте в код макроса вывод текстовых сообщений на консоль, но в этот раз поместите их внутрь квази-цитаты. Посмотрите, в какой момент и сколько раз они выводятся.

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

Промежуточный итог

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

Ссылки


Эта статья опубликована в журнале RSDN Magazine #2-2009. Информацию о журнале можно найти здесь