Модераторы: Partizan, gambit

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Nemerle, цикл статей 
:(
    Опции темы
Exception
  Дата 21.9.2006, 17:43 (ссылка) |    (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Участник Клуба
Сообщений: 4525
Регистрация: 26.12.2004

Репутация: 29
Всего: 186



Этот цикл статей посвящён языку программирования Nemerle под платформу .NET.

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

К слову, Nemerle весьма похож на C# и является лёгким в изучении.

Итак, начать этот цикл я хочу с перевода статей обучающего цикла Nemerle for OOP Programmers Course. Внимание: перевод местами будет не точно совпадать с оригиналом.

Для начала, давайте чуточку лучше определимся, что же такое есть Nemerle. Nemerle - это язык программирования под .NET, я бы сказал, что это усовершенствованный C#. Это функциональный язык, как и, к примеру, LISP, Haskell или Python. С другой стороны, он поддерживает и императивный стиль программирования (C#, C++, Java, etc). Что ж, начнём!


Nemerle for OOP Programmers Week 0

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




Значения и переменные

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

Код

def name : type = value



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

Код

mutable name : type = value



Здесь наличие инициализатора необязательно – прим. перев.


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


Автоопределение типов


Наверняка, вы удивились, а зачем мы используем специальное ключевое слово для определения значений и переменных, а не пишем попросту type name;, как это делали в C-подобных языках? Ответ прост – во многих случаях тип можно опустить, потому что его вычислит компилятор. Вместо

Код

def x : int = 42;


можно написать:

Код

def x = 42;


Здесь нет ограничений только на литералы, а потому можно написать что-нибудь вроде: 

Код

mutable y = null;
// делаем что-либо
y = SomeComplexObject ();



Компилятор всё равно определит тип переменной. Прим. перев.: на самом деле движок для автоопределения типа (type inference engine) является одной из наиглавнейших частей компилятора и умеет очень-очень многое. 


Методы


Как вы, конечно же, знаете, метод (или функция) – это некая последовательность операторов с осмысленным именем. Метод может возвращать значение – итоговый результат вычисления, которое в нём проводилось. Также метод может принимать некоторые параметры. Общий синтаксис для определения методов выглядит так:

Код

name ( parameter1 : type, parameter2 : type ... ) : return_type


Как вы можете видеть, единственное отличие от C#, C++ или Java заключается в том, что типы параметров стоят после их имён, а не перед ними. Это всего лишь дело вкуса; в Visual Basic, например, типы тоже располагаются после имён.

ПРИМЕЧАНИЕ: автоопределение типа недоступно для параметров и возвращаемых методами значений потому тогда интерфейсы классов могут внезапно измениться, и уже работающий код, использующий эти классы, перестанет работать. Но, всё же, мы планируем ввести поддержку автоопределения типов для закрытых (private) методов. 

Возвращаемые значения

Как и другие функциональные языки, Nemerle не использует ключевое слово return или для указания возвращаемого методом значения. Это отлично вписывается в общую картину в Nemerle: в нём нет операторов gotobreaklabel, или каких-нибудь других, нарушающих нормальный ход выполнения программы (вообще-то, некоторые макросы, расширяющие язык, позволяют ввести их, но пока что это нас не должно волновать).
Тогда возникает вопрос: если нет никакого ключевого слова, как же компилятор определит, какое значение возвращать? Ответ очень прост: возвращается значение последнего вычисленного выражения. Простейший пример: 

Код

add (number1 : int, number2 : int) : int
{
    number1 + number2
}



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


Примитивные типы


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

Код

Короткое имя    Полное имя    Описание
sbyte    System.SByte    Знаковое 8-битное целое число.
short    System.Int16    Знаковое 16-битное целое число.
int    System.Int32    Знаковое 32-битное целое число.
long    System.Int64    Знаковое 64-битное целое число.
byte    System.Byte    Безнаковое 8-битное целое число.
ushort    System.UInt16    Безнаковое 16-битное целое число.
uint    System.UInt32    Безнаковое 32-битное целое число.
ulong    System.UInt64    Безнаковое 64-битное целое число.
string    System.String    Неизменяемая строка символов.
char    System.Char    Символ, внутренне представляемый a 16-битным целым числом.
void    System.Void    Тип, который может иметь лишь одно значение, называемое () в Nemerle. Используется, если метод ничего не возвращает.
float    System.Single    32-битное число IEEE с плавающей запятой.
double    System.Double    64-битное число IEEE с плавающей запятой.
decimal    System.Decimal    Тип, используемый для представления денежных сумм.




Во время изучения данного курса вам, скорее всего, не пригодятся никакие примитивные типы кроме intstringchar и void, так что не утруждайте себя запоминанием всей таблицы :-) 
Полное имя – это имя, используемое в библиотеке типов (BCL). В коде проще использовать короткое имя, хотя использование полного законом не карается. Они полностью взаимозаменяемы.
Nemerle основыван на платформе Microsoft .NET Framework (или её бесплатной реализации – Mono). Таким образом, эти типы существуют не только в Nemerle, но и, в общем-то, в любом языке под .NET Framework. Если вы раньше программировали на C#, скорее всего вы уже неплохо знаете базовые библиотеки типов. Что ж, пожалуй, начнём с нескольких практических примеров.
Простые примеры
Скажем, наша задача – спросить имя пользователя и высветить приветствие. Код такой программы приведён ниже: 

Код

def name = System.Console.ReadLine ();
System.Console.WriteLine ("Привет, " + name);



Вы можете сохранить эти две строчки в файл, скажем, greeting.n. Скомпилируйте его командой 

Код

 ncc greeting.n


и запустите (через командную строку Windows):

Код

 out


(или через терминальное окно в Linux с установленным mono): 

Код

 mono out.exe


В Nemerle для выполнения кода необязательно оборачивать его в какие-нибудь классы или методы.
Есть и другой способ задать точку входа – необходимо написать статический  метод Main в каком-нибудь классе (как в C# - прим. перев.). Тогда пример выглядел бы так: 

Код

class Greeter
{
  public static Main () : void
  {
    def name = System.Console.ReadLine ();
    System.Console.WriteLine ("Привет, " + name);
  }
}


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

Код

class Asker
{
  public static Run () : string
  {
    System.Console.ReadLine ()
  }
}
 
def name = Asker.Run ();
System.Console.WriteLine ("Привет, " + name);



Операторы сравнения


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

Код

==    Равно
!=    Не равно
>    Больше
>=    Больше или равно
<    Меньше
<=    Меньше или равно


ПРИМЕЧАНИЕ: некоторые языки, такие как Visual Basic или Pascal, используют оператор = как логический оператор сравнения. Этот оператор используется как оператор присваивания в Nemerle, а потому его использование в сравнении является ошибкой.

Результаты сравнений могут быть сохранены в значениях или переменных типа bool:

Код

def number = int.Parse (System.Console.ReadLine ());
def isEven = (number % 2) == 0;
System.Console.Write ("Число чётно:");
System.Console.Write (isEven);



Этот код покажет True или False в зависимости от введённого пользователем числа.


Объединение операторов сравнения


Иногда необходимо одновременно использовать более чем одно условие. Это реализуется следующими операторами:

Код

! c    Отрицание, возвращает true, если c было false, и false, если c было true.
c1 && c2    «И», проверяет c1, и если оно равно false, возвращает false, иначе возвращает c2.
c1 || c2    «ИЛИ», проверяет c1, и если оно равно true, возвращает true, иначе возвращает c2. 


Оба оператора – && и|| – вычисляются только если это необходимо, то есть, следующий код:

Код

when (foo != null && foo.Length > 0)
  System.Console.WriteLine (foo);


никогда не вызовет NullReferenceException, поскольку foo.Length не станет вычисляться, если foo == null
Так же ведут себя и другие C-подобные языки.


Использование условий


Раз мы научились проверять условия, пора уже и научиться выполнять определённый кусок кода в зависимости от них. Вот три основные условные конструкции: 

Код

when (condition)
  code


when выполняет код только когда условие имеет значение true.

Код

unless (condition)
  code


unless выполняет код только когда условие имеет значение false.

Код

if (condition)
  code1
else
  code2


if..else выполняет первый блок кода, если значение условия –  true, а второй – если результат равен false. В большинстве языков семейства C нет конструкций when и unless, место которых занимает if без блока else. В Nemerle каждый if  должен иметь блок else.

Код

Код

unless (something)
  DoSomethingElse ()


абсолютно эквивалентен

Код

when (!something)
  DoSomethingElse ()


Использовать конструкцию unless или нет – это дело вкуса: используйте то, что больше нравится. Здесь нет никаких ограничений. 
В Nemerle условные операторы также могут использоваться как функции (как и в других функциональных языках), так что тип возвращаемого значения каждой ветви должен быть одинаковым. Впрочем, эти конструкции в основном используются тем же образом, что и их собратья из C# или Java (тип обеих ветвей – void), так что никаких проблем не возникнет, если вы опустите финальную ;. Операторы when/unless транслируются в оператор if...else, в качестве результата else возвращающий ().

Блоки кода

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


Установка Nemerle


Установите последнюю стабильную версию Nemerle на Linux, Windows, MacOS или какую-либо другую по вашему выбору (чую нутром, всё-таки лучше ставить на Windows :-) – прим. перев.).  Если вы используете Linux или MacOS, необходимо сначала поставить Mono. Если вы используете Windows, необходимо скачать и установить Microsoft .NET Framework 2.0. 
На странице Step by step guide to get Nemerle compiler running описаны все детали установки, как для MS .NET, так и для Mono.
Для подсветки кода Nemerle можно использовать различные редакторы кода (например, SciTE, VIM, XEmacs, Kate, GEdit, mcedit). Вы можете найти необходимые конфигурационные файлы в репозитории SVN. Прим. перев.: на самом деле сейчас идёт разработка плагина к Visual Studio, но он пока что далёк от совершенства. Лично я использую SciTE и доволен.


Следующая статья расскажет об объектно-ориентированном программировании в Nemerle.

Присоединённый файл ( Кол-во скачиваний: 18 )
Присоединённый файл  Nemerle_for_OOP_Programmers_Week_0__translated_.doc 93,50 Kb
PM   Вверх
nikitao
Дата 21.9.2006, 20:22 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Кот-программист
***


Профиль
Группа: Завсегдатай
Сообщений: 1206
Регистрация: 30.8.2005
Где: Спб

Репутация: 4
Всего: 26



Жду продолжения  smile 


--------------------
Жизнь - печальная штука.
PM MAIL ICQ Skype GTalk   Вверх
Cr@$h
Дата 21.9.2006, 22:41 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Исследователь
***


Профиль
Группа: Участник Клуба
Сообщений: 1693
Регистрация: 3.4.2005
Где: Санкт-Петербург, Россия

Репутация: 1
Всего: 41



Exception, молодец ++. К слову о функциональности Python: он поддерживает функциональный стиль. Интересное обсуждение и ссылки на статьи ведём в теме О функциональном программировании на Python.
Я не стал бы таки бросаться прямо на Nemerle. В C# 3.0 ввели ряд функциональных средств. Но то, что язык завоёвывает популярность, это факт. Ещё раз спасибо.
PM MAIL ICQ   Вверх
mr.DUDA
Дата 21.9.2006, 22:45 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


3D-маньяк
****


Профиль
Группа: Экс. модератор
Сообщений: 8244
Регистрация: 27.7.2003
Где: город-герой Минск

Репутация: 110
Всего: 232



Модератор: на всякий случай, зафиксировал тему smile


--------------------
user posted image
PM MAIL WWW   Вверх
Exception
Дата 22.9.2006, 08:22 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Участник Клуба
Сообщений: 4525
Регистрация: 26.12.2004

Репутация: 29
Всего: 186



Нет-нет, вы так на меня косо не смотрите, о функциональных средствах в С# 3.0 я прекрасно знаю smile . Кто там переводил LINQ Overview? Только вся эта красота элементарно реализуется в Nemerle, а type inference в С# вообще слабенький. Функциональность LINQ, по сути, легко реализовать синтаксическими макросами. Одна из самых сильных сторон Nemerle - это pattern-matching, а в С#...
PM   Вверх
Exception
Дата 18.10.2006, 16:40 (ссылка) |    (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Участник Клуба
Сообщений: 4525
Регистрация: 26.12.2004

Репутация: 29
Всего: 186



Прошу прощения за вынужденную задержку в переводе.

Nemerle for OOP Programmers Week 1

В этом уроке мы научимся основам объектно-ориентированного программирования на языке Nemerle. Основные техники практически не отличаются от тех, что предоставляют языки C#, Java или C++, но имеются и некоторые отличия.

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

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


В Nemerle классы объявляются ключевым словом class. Простейший случай его употребления:

Код
class MyClass
{
  // "внутренности" класса
}


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

Модификаторы

Существует много модификаторов. Скорее всего, в ходе изучения вам понадобятся только основные: publicstatic и mutable.

Всё же, для начала мы обсудим все модификаторы доступа. Они позволяют указать уровень доступа или, другими словами, из каких классов к этой сущности можно обратиться. Модификаторы доступа могут применяться ко всем без исключения членам классов. По умолчанию всегда используется privatepublic и internal могут применяться и к классам (которые по умолчанию internal).
  •  public - доступ к члену открыт для любых классов
  •  internal - только классы из той же сборки имеют доступ к члену 
  •  private - член закрыт от других классов 
  •  protected - доступ к члену имеет этот класс и его потомки 
  • protected internal - protected или internal - доступ к члену открыт наследникам, а также типам из той же сборки
Другие модификаторы изменяют семантику, поведение сущности, к которой они применяются:
  •  static - перед полем означает, что оно общее между всеми экземплярами данного класса. Перед методами и свойствами static означает, что скрытый аргумент this коду не передаётся, и метод может оперировать только статическими данными и методами класса. 
  • mutable - этот модификатор применяется только к полям и означает, что поле может быть изменено вне конструктора. В C# и Java нет подобного модификатора; по умолчанию в этих языках все переменные и поля изменяемые. В противоположность Nemerle, в C# имеется модификатор readonly, заставляющий поле быть неизменным.
  •  volatile - означает, что поле всегда должно читаться и записываться прямо в память (минуя кэширование при JIT-компиляции). Этот модификатор в основном используется для многопоточных приложений.
  •  extern - этим модификатором помечаются методы с установленным атрибутом DllImport, позволяющим вызывать неуправляемый код из кода. Мы не будем касаться этой темы в нашем курсе, так что, если эта тема вас заинтересует, вы можете поискать в интернете информацию о PInvoke.
  •  partial - применяется к типам и означает, что его реализация разбивается по нескольким файлам. Вы просто объявляете классы с одинаковым именем и модификатором partial, а компилятор соединяет их в один.
Следующие модификаторы используются для управления виртуальными методами и сокрытием имён. Пока что мы рассмотрим лишь модификатор new, поскольку остальные более подробно будут обсуждаться в разделе о вызове виртуальных функций.
  •  new - может применяться ко всем сущностям кроме вложенных типов и означает, что он "скрывает" член с тем же именем в базовом классе. 
  •  virtual
     
  •  override
  •  abstract
  •  sealed
Немного об изменяемости

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

Статические методы или методы экземпляра?

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

Пользовательские атрибуты

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

Код
[MyCustomAttribute(7, "foobar")]
public class FooBar
{
  [OtherCustomAttribute]
  SomeMethod () : void {}
}


Пользовательские атрибуты на самом деле являются наследниками класса System.Attribute. Вы можете объявить свои собственные атрибуты, которых нет в стандартной библиотеке.
Дополнительную информацию об атрибутах, их создании и использовании можно найти в соответствующей статье в MSDN. Хотя примеры в ней написаны на C#, это не играет практически никакой роли.
Некоторые специальные атрибуты в Nemerle вызывают выполнение синтаксических макросов - дополнений к компилятору. Чуточку позже мы научимся автоматически генерировать с помощью макросов конструкторы и методы-аксессоры. 

Методы

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

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

Поля

Поля в Nemerle ничем не отличаются от таковых в C#/Java/C++, кроме своей неизменности: если поле не объявить с модификатором mutable, его значение можно будет изменить только в конструкторе класса (если поле статическое, то в статическом конструкторе). Всё это очень похоже на действие модификаторов
readonly из C# и final 
из Java.
Ещё можно поместить присваиваемое значение прямо в объявление поля, при этом код присваивания будет добавлен в конструктор экземпляра класса.
Поля объявляются таким синтаксисом:

Код
modifiers name : type;


или с инициализатором:

Код
modifiers name : type = expression;


Модификаторы являются необязательными, по умолчанию все поля закрыты от внешних классов. Соответственно, если опустить инициализатор, полям присваиваются значения, такие как 0, 0.0
или null для ссылочных типов.

На самом деле, даже если вы поставите выражение-инициализатор, всё равно можно наблюдать присутствие значения по умолчанию, например

Код
class Test
{
  static public x : int = y + 1;
  static public y : int = x + 2;
}
System.Console.WriteLine (Test.x);
System.Console.WriteLine (Test.y);


выводит 1 и 3, потому что в то время, когда происходит инициализация x, значение y всё ещё равно нулю. Как вы можете видеть, инициализация проходит сверху вниз.

Конструкторы экземпляра

В отличие от таких языков, как C#/Java/C++, имя метода-конструктора постоянно и называется this.
Пример:

Код
class Point
{
  public x : float;
  public y : float;
  public this (ini_x : float, ini_y : float)
  {
    x = ini_x;
    y = ini_y;
  }
}


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

Код
class Point
{
  public x : float;
  public y : float;
  public this (x : float, y : float)
  {
    this.x = x;
    this.y = y;
  }
}


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

Код
def the_point = Point (3.14, -3.14);


В Nemerle нет ключевого слова new, в отличие от C# или Java.

Макрос Record


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

Код
[Record]
class Point
{
  public x : float;
  public y : float;
}


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


Конструктор по умолчанию


Если конструктора не предоставлено, компилятор генерирует открытый, пустой конструктор по умолчанию, не принимающий параметров. Поэтому следующий кусок кода абсолютно правилен:
Код
class MyClass {}

def c = MyClass ();


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

Код
class Marvin
{
  // при инициализации мы придаём ему положительное направление
  mutable attitude : int = 100;
  mutable x : int;
  mutable y : int;
  mutable direction : int;
  
  public this (ini_x : int, ini_y : int)
  {
    x = ini_x;
    y = ini_y;
    // направляемся на север
    direction = 0;
  }
}


Этот пример абсолютно эквивалентен следующему:

Код
class Marvin

  mutable attitude : int; // ИЗМЕНЕНО!
  mutable x : int;
  mutable y : int;
  mutable direction : int;
  
  public this (ini_x : int, ini_y : int)
  {
    attitude = 100; // ИЗМЕНЕНО
    x = ini_x;
    y = ini_y;
    // ИЗМЕНЕНО
  }
}


Как видно, мы переместили код инициализации поля, обозначающего направление, в конструктор класса. Именно это и делает компилятор, когда встречает поле с инициализатором. Мы также убрали строку direction = 0, поскольку 0 - значение по умолчанию для типа int и будет присвоено переменной в любом случае.

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

Теперь мы можем немного усложнить поведение Марвина. Начнём, пожалуй, с метода Turn:

Код
public Turn (delta : int) : void
{
  // нужно защитить Марвина
  // от слишком больших поворотов!
  if (delta < -90 || delta > 90)
    System.Console.WriteLine ($"Не могу выполнить поворот на $delta градусов. Уменьшите угол поворота!");
  else
  {
    // повороты заставляют робота изнашиваться
    attitude -= 5;
    direction += delta;
    // вызываем ПО робота
  }
}


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

Код
    System.Console.WriteLine ($"Не могу выполнить поворот на $(delta / 180 * 3.1415) радиан. Уменьшите угол поворота!");


Это работает практически точно так же, как и в PHP, perl или Bourne Shell, но выражения проверяются на корректность ещё на стадии компиляции: при использовании в выражении не определённой в коде переменной произойдёт ошибка компиляции. В других языках в этом случае обычно во время выполнения возвращается пустая строка.

.NET Framework имеет собственный способ подстановки строк. Набрав директиву {n} внутри строки, вы тем самым подставите на её место (n+2)-ой параметр. Один из методов, позволяющий проделывать подобные фокусы - это Console.WriteLine().

Код
def addition = string.Format ("{0} + {1} = {2}", n1, n2, n1 + n2);


Как вы можете видеть, в нумерации параметров элементарно запутаться, так что лучше используйте $ :-)

Немного о циклах

Теперь мы можем добавить ещё один метод:

Код
public TurnBack () : void
{
  for (mutable i = 0; i < 2; ++i)
    Turn (90);
}


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

Так что насчёт реализации поворота без ограничения?

Код
public TurnAny (mutable delta : int) : void
{
  while (delta != 0)
    if (delta > 90) {
      Turn (90);
      delta -= 90;
    } else if (delta > 0) {
      Turn (delta);
      delta = 0;
    } else if (delta < -90) {
      Turn (-90);
      delta += 90;
    } else {
      Turn (delta);
      delta = 0;
    }
}


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

Подробнее о классах


Статические конструкторы

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

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

Статические конструкторы должны быть закрытыми (private), не должны иметь параметров и... должны быть статическими (static). Например:

Код
using System.Console;
 
class Hello
{
  static this ()
  {
    WriteLine ("static ctor");
  }
 
  static public Run () : void
  {
    WriteLine ("Run");
  }
}
 
WriteLine ("hello");
Hello.Run ();
WriteLine ("bye");
 
/* Выводит:
hello
static ctor
Run
bye
*/


Свойства

Свойства предоставляют поддержку методов-аксессоров get/set (часто используемых в C++ и Java) на уровне языка. Большинство языков для .NET (включая C#) поддерживают концепцию свойств. Свойство с именем Foo и типом int на самом деле компилируется в два метода: get_Foo() : int и set_Foo (value : int). Вместо того, чтобы писать так:

Код
def my_value = some_object.get_Foo ();
some_object.set_Foo (42);


мы пишем:

Код
def my_value = some_object.Foo;
some_object.Foo = 42;


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

Код
using System.Console;
 
class MyClass
{
  mutable m_foo : int;
  
  public Foo : int
  {
    get
    { 
      WriteLine ("get_Foo вызван");
      m_foo
    }
    set
    {
      when (value > 3)
        WriteLine ("значение больше, чем 3");
      m_foo = value;
    }
  }
}
 
def c = MyClass ();
c.Foo = 1;
WriteLine ($ "c.Foo = $(c.Foo)");
c.Foo += 42;
WriteLine ($ "c.Foo = $(c.Foo)");
 
/* Выводит:
get_Foo вызван
c.Foo = 1
get_Foo вызван
значение больше, чем 3
get_Foo вызван
c.Foo = 43
*/


Обратите внимание, что += преобразуется в c.Foo = c.Foo + 42, благодаря чему get_Foo вызывается перед установкой значения.

Также заметьте, что метод set принимает неявный параметр с именем value.

Вы можете опустить часть get или set (но не обе). Если опустить set, свойству нельзя будет присвоить значение. Опустив get, вы не сможете читать значение свойства.

Макрос [Accessor]

Если свойство только и делает, что возвращает значение некоторого поля, имеет смысл воспользоваться макросом [Accessor], который определён в пространстве имён Nemerle.Utility. Макрос следует прикрепить к полю и, если ему не передавать никаких параметров, он сгенерирует открытый get-метод для этого поля:

Код
using Nemerle.Utility;
 
class MyClass
{
  [Accessor]
  foo_bar : string = "qux";
}
 
def c = MyClass ();
System.Console.WriteLine (c.FooBar);
 
/* Выводит: qux */


Вы также можете определить другое имя для аксессора, сгенерировать set-метод или изменить модификатор доступа. Дополнительную информацию о макросе [Accessor] можно найти в документации.

Наследование и виртуальные вызовы

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

Например, мы можем определить класс под названием Robot и два класа-наследника: Marvin и R2D2.

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

Код
class Robot
{
  public virtual SelfDestruct () : void
  {
    // вызов ПО робота
  }
}
 
class R2D2 : Robot
{
  public override SelfDestruct () : void
  { 
    System.Console.WriteLine ("отмена");
  }
}
 
class Marvin : Robot
{
 // ничего не переопределяем
}


Как вы можете видеть, двоеточие ([b:[/b])используется для обозначения базового класса (в Java для этих целей используют ключевое слово extends). Методы должны быть помечены как виртуальные (virtual), иначе их нельзя будет переопределить (в отличие от методов в Java). Вдобавок, переопределяющие методы должны быть помечены модификатором override (в отличие от методов в C++).

Абстрактные методы

Методы также бывают абстрактными (abstract). В таком случае не нужно предоставлять вовсе никакого тела метода. Это значит, что метод должен быть переопределён классом-наследником в любом случае. Класс является абстрактным, если он содержит хотя бы один абстрактный метод или не переопределяет хотя бы один абстрактный метод базового класса. Экзепляры абстрактных классов не могут быть созданы.

Пример:

Код
abstract class Robot
{
  abstract SelfDestruct () : void;
}
 
class R2D2 : Robot
{
  public override SelfDestruct () : void
  { 
    System.Console.WriteLine ("отмена");
  }
}
 
class Marvin : Robot
{
  public override SelfDestruct () : void
  { 
    System.Console.WriteLine ("саморазрушение через 5 секунд...");
  }
}


Модули

Ещё один часто встречающийся паттерн, используемый в некоторых классах C# (например, в Console) - это когда класс не предоставляет открытых конструкторов, а все  его члены - статические. Это предотвращает создание экзепляра объекта и бывает полезно для объектов, которые и существуют-то в одном-единственном экземпляре (например, консоль или файловая система в UNIX/Linux).

В Nemerle этот паттерн реализуется с помощью специальных элементов языка - модулей (привет, VB .NET!). Определение модуля ничем не отличается от определения класса, но к каждому члену модуля автоматически добавляется модификатор static.

Например:

Код
module Operations
{
  public Add (n1 : int, n2 : int) : int {
    n1 + n2
  }
 
  public Substract (n1 : int, n2 : int) : int {
    n1 - n2
  }
}
 
// Обращение к членам модуля производится только через его имя
System.Console.WriteLine (Operations.Add (1, 2));


Преобразование типов

Иногда бывает необходимо преобразовать объект из одного типа в другой. Например, вам может быть нужно преобразовать int в double или класс к классу-предку. В Nemerle есть два опреатора преобразования типов (или опреторов приведения типов, как их любят называть шарписты). Выбор оператора обусловливается тем, нужно ли проверять тип при компиляции или во время выполнения.
  •  Приведение типа ( :> ): этот оператор проверяет возможность приведение типа во время выполнения. Это особенно полезно, если вы получаете ссылку на экземпляр базового класса от некоторого метода и свойства и хотите привести его к дочернему классу. Однако, поскольку у класса может быть более чем один наследник, правильность преобразования не может быть проверена компилятором, и во время выполнения может быть выброшено исключение InvalidCastException.
  •  Строгое приведение типа ( : ): этот оператор можно использоватьтолько если успешность приведения типа можно установить на этапе компиляции. Например, это приведение short к int или класса к своему предку.
Оба оператора используются в коде по-одинаковому. Сначала вы пишете объект, который хотите конвертировать, затем оператор и результирующий тип.

Код
def memoryStream = System.IO.MemoryStream ();
def stream = memoryStream : System.IO.Stream; // MemoryStream - подкласс Stream
 
def n1 = int.Parse (System.Console.ReadLine());
def n2 = int.Parse (System.Console.ReadLine());
def resultAsShort = (n1 + n2) :> short; // Мы не знаем, "уложится" ли n1 + n2 в short, а потому используем :>


Это сообщение отредактировал(а) Exception - 19.10.2006, 11:46
PM   Вверх
Exception
Дата 19.10.2006, 11:57 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Участник Клуба
Сообщений: 4525
Регистрация: 26.12.2004

Репутация: 29
Всего: 186



Nemerle for OOP Programmers Week 1

Вступление к функиональному программированию

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

Существуют чисто функциональные языки (например, Haskell - прим. перев.), которые запрещают любые изменения состояния объектов (другими словами, все объекты и их поля неизменяемы). Многие функциональные языки являются ленивыми (lazy), то есть, значения параметров, переданных функции, не вычисляются до тех пор, пока не понадобятся (вам это может показаться оптимизацией, пока вы не попробуете Haskell :-)

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

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

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

Напомню, базовые идеи ФП в Nemerle таковы:
  •  Вы можете сделать структуры данных неизменяемыми (причём по умолчанию они такие и есть; чтобы сделать поле изменемым, к нему следует применить специальный модификатор [color=blue]mutable[/color).
  •  Вы можете сделать неизменяемыми локальные переменные (def короче, чем mutable, не так ли? :-)
  •  Вы всегда можете использовать функции в качестве значений, как и другие ФЯ.
Что же, похоже, вам становится несколько скучно, так что глянем на пару примеров.

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

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

Код
class LocalFunctionExample
{
  public static Main () : void
  {
    def say_hello () : void {
      System.Console.WriteLine ("Hello!");
    }
 
    say_hello ();
    say_hello ();
  }
}


Вероятно, вам не покажется большим сюрпризом, что этот код дважды выведет слово "Hello!" на экран. Локальная функция say_hello определена внутри Main и потому на неё можно ссылаться только из неё (это ведь локальная функция, верно?).

Вы даже можете опустить объявление Main, как мы видели раньше:

Код
def say_hello () : void
{
  System.Console.WriteLine ("Hello!");
}
 
say_hello ();
say_hello ();


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

Код
def say_hello ()
{
  System.Console.WriteLine ("Hello!");
}
 
say_hello ();
say_hello ();


Передача функций в качестве параметров

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

Код
def run_twice (f)
{
  f ();
  f ();
}


Мы не указали тип f, потому что система автоопределния типа сделает это за нас. Nemerle видит, что f используется в теле run_twice в качестве функции.

Теперь мы можем определить функцию say_hello и передать её run_twice:

Код
def say_hello ()
{
  System.Console.WriteLine ("Hello!");
}
 
run_twice (say_hello);



Лексическая видимость

Локальные функции могут использовать все идентификаторы, определённые в родительской функции. Например:

Код
def hello = "Hello!";
def say_hello ()
{
  System.Console.WriteLine (hello);
}
 
say_hello ();
say_hello ();


Когда локальная функция ссылается на некоторый идентификатор, она использует ближайший сверху (в тексте программы). Это свойство называется лексической видимостью. В данном случае, когда say_hello обращается к переменной hello, она использует ближайшее объявление выше (в данном случае, "Hello!"). Обратите внимание, что другие идентификаторы hello могут быть объявлены в других контекстах и использоваться без конфликтов. Правила лексической видимости разделяют их.




Это сообщение отредактировал(а) Exception - 19.10.2006, 12:09
PM   Вверх
Exception
Дата 19.10.2006, 12:15 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Участник Клуба
Сообщений: 4525
Регистрация: 26.12.2004

Репутация: 29
Всего: 186



Списки

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

Списки состоят из элементов. Каждая элемент содержит в себе хранимое значение (head) и ссылку на другой элемент списка (tail). Существует также специальный пустой элемент Nil. Элементы списка не могут быть изменены после своего создания. С одной стороны, это значит, что нельзя изменить порядок следования элементов в списке и их значения после создания этого списка, но с другой - вы можете быть уверены, что список не может быть случайно изменён.

Хотя списки не могут быть изменены с момента создания, у них может быть один "хвост" (tail):

Код
      42
      |
      V
 10-->34-->27-->nil


Здесь списки [10, 34, 27] и [42, 34, 27] имеют общую часть-список [34, 27]. Поскольку списки неизменяемы, это не приносит никакого вреда. Все списки в Nemerle имеют общий нулевой список (список, содержащий один элемент - Nil - прим. перев.)

Списки конструируются с помощью оператора ::, в то время, как [] представляет собой пустой список. Таким образом, 1 :: [] означает, что мы хотим создать список с элементом, содержащим значение 1 в качестве "головы" (head) списка и пустым списком в качестве "хвоста". Другими словами, так мы создаём список, содержащий один элемент - число 1. Теперь мы можем создать и список, состоящий из двух элементов:

Код
def l1 = 1 :: [];
def l2 = 42 :: l1;


Здесь мы объединяем цепочкой элементы списков l1 и l2, дабы создать более длинный список. Разумеется, список можно было объявить и одним выражением: 42 :: 1 :: []. Если вам не нравятся все эти двоеточия, существует упрощённая нотация: [42, 1]. Короткая форма предпочтительна для использования в выражении сопоставления с образцом.

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

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

Код
match (some_value)
{
  | pattern1 => expr1
  | pattern2 => expr2
  ...
}


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

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

Образец подстановки

Образец подстановки записывается как _ и походит для любого значения, включая null (не путайте с пустым элементом списка - Nil). Этот образец используется как ветвь default конструкции switch в C-подобных языках.

Образцы-литералы

Любой литерал (например, 42, 15, "foobar" или null) - вполне корректный образец. Это позволяет использовать match в качестве switch.

Код
match (some_number)
{
  | 1 => "one"
  | 2 => "two"
  | 3 => "three"
  | _ => "a lot"
}


Образцы списков

При сопоставлении списков доступны и конструктор ::, и синтаксис [1, 2]:

Код
match (some_list)
{
  | [42, 42] => "два элемента 42"
  | [42, _] => "42 на первой позиции двухэлементного списка"
  | [_, 42] => "42 на второй позиции двухэлементного списка"
  | 42 :: _ => "42 на первой позиции"
  | _ :: 42 :: _ => "42 на второй позиции"
  | [] => "список пуст!"
  | _ => "другой список"
}


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

Образцы-переменные

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

Например, функция, перечисляющая все элементы списка, выглядит так:

Код
using System.Console;
 
def display (l) {
  match (l)
  {
    | head :: tail =>
      Write ($ "$head, ");
      display (tail)
    | [] =>
      WriteLine ("end")
  }
}
 
display ([1, 2, 3])
 
// Выводит: 1, 2, 3, end


Функция display получает список в качестве аргумента и сопоставляет его с образцом head :: tail. Если сопоставляемый список не пуст, значение первого элемента ("головы") можно получить через идентификатор head, а оставшийся список (второй элемент и далее - прим. перев.) доступен через обращение к tail. Функция выводит значение первого элемента ("головы") и рекурсивно вызывает себя, чтобы вывести остаток списка.

Когда функция достигает конца списка, происходит сопоставление с пустым [], выводится слово "end", и функция завершает выполнение.

Простые действия над списками

Нам известны два основных инструмента для манипуляции списками - конструирование списков и сопоставление с образцом. Настало время рассмотреть несколько примеров.

Фильтрация

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

Например:

Код
def is_even (x) { x % 2 == 0 }
System.Console.WriteLine (filter ([1, 2, 3, 4], is_even));


должен вывести [2, 4].

В Nemerle все основные структуры данных переопределяют метод ToString(), возвращая своё содержимое в читаемой для человека форме, а поэтому WriteLine() нормально выводит список.

Возвращаясь к коду функции filter:

Код
def filter (l, f) {
  match (l) {
    | x :: xs =>
      def xs' = filter (xs, f);
      if (f (x)) x :: xs'
      else xs'
    | [] => []
  }
}


Сначала функция производит операцию сопоставления с образцом для своего аргумента. В случае, если список не пуст, "голова" привязывается к x, а "хвост" - к xs. Для обозначения "головы" часто используется имя x или y, а для "хвоста" - xs или ys.

Далее функция рекурсивно вызывает сама себя, передавая остаток списка, и результат сохраняется в переменной xs'. В Nemerle апостроф (') является допустимой частью идентификатора. Трансформацию x часть называют x' (x-prime, x-штрих), это обозначение пришло из математики.

В конце, в зависимости от значения, возвращённого f (x), функция либо добавляет элемент x к результату, либо пропускает его. Если вы ищете опретор return, то его вы не найдёте; как мы говорили ранее, функция возвращает последнее вычисленное значение, то есть, либо x :: xs', либо xs'.

Разумеется, если список пуст, функция вернёт также пустой список.

Заметка: анонимные функции

Тестируя приведённую выше функцию, вероятно, вам хотелось бы иметь возможность делать фильтрацию по различным выражениям "на лету". По сути, вызов функции filter можно переписать так:

Код
System.Console.WriteLine (filter ([1, 2, 3, 4], fun (x) { x % 2 == 0 }));


Если мы используем локальную функцию только однажды, её можно определить как анонимную.

Выражение

Код
fun (some_parms) { some_expression }


эквивалентно

Код
{
  def tmp (some_parms) { some_expression }
  tmp
}


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

Первые N элементов

Функция first_n возвращает первые N элемнтов переданного ей списка.

Код
def first_n (l, n) {
  if (n == 0)
    []
  else
    match (l)
    {
      | x :: xs => x :: first_n (xs, n - 1)
      | [] => throw System.ArgumentException ("Список слишком короткий")
    }
}
 
System.Console.WriteLine (first_n (["foo", "bar", "baz"], 2))
 
// Выводит: ["foo", "bar"]


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

Ещё одна новая вещь в этом коде - это выброс исключения. Оператор throw выбрасывает исключение-аргумент. System.ArgumentException - это класс из BCL. Обработка исключений будет обсуждаться позднее (вероятно, в следующей части)

Типы и обобщения

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

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

Список является параметризованным (в терминологии .NET, он является generic-типом, или обобщением), а поэтому список целых чисел имеет тип list [int], а список строк - list [string]. Обратите внимание, что для передачи параметров типа используются квадратные скобки ([]). В C++, Java и C# используются угловые скобки (<>) для этих целей.

BCL и стандартная библиотека Nemerle предоставляют некоторые generic-классы, например, Nemerle.Collections.Hashtable [K, V] (изменяемая коллекция для хранения пар K и V, наследующая от System.Collections.Generic.Dictionary [K, V]).

Другой важный пример параметризованных типов - это, безусловно, массивы. Массив - это тип, похожий на список (вероятно, каждый из вас с ними знаком :-) - прим. перев.). Синтаксис для объявления массивов - array [type], где type - это тип элементов массива.

Типы функций

Типом функции, принимающей аргумент типа string и возвращающей int (как, например, метод int.Parse()), является string -> int. Сивол -> называют стрелкой. Если функция ничего не возвращает, мы используем тип void. Например, метод System.Threading.Thread.Sleep имеет тип int -> void. Аналогично, если функция не принимает аргументов, мы пишем void перед стрелкой. Таким образом, тип функции Main - void -> void.

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

Зная, как пишутся типы функций, мы можем написать полностью типизированную версию filter:

Код
def filter (l : list [int], f : int -> bool) : list [int] {
  match (l)
  {
    | x :: xs =>
      def xs' = filter (xs, f);
      if (f (x)) x :: xs'
      else xs'
    | [] => []
  }
}


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

Что ж, а почему функция оперирует значениями типа int? Потому что мы используем её со списком целых чисел. "Замечательно, - ответите вы, - но зачем ограничиваться списком целых чисел?" Как вы верно заметили, структура функции должна работать, скажем, и для списка строк, и для списка объектов FooBar - просто нужен способ уменьшить ограничения. Это приводит нас к идее обобщённых функций.


Обобщённые функции

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

Код
def funname[gentype0] (parmlist) : funtype {body} 


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

С этими знаниями, давайте напишем обобщённую функцию filter:

Код
using System.Console;
 
def filter[T] (l : list [T], f : T -> bool) : list [T]
{
  match (l)
  {
    | x :: xs =>
      def xs' = filter (xs, f);
      if (f (x)) x :: xs'
      else xs'
    | [] => []
  }
}
 
WriteLine (filter ([1, 2, 3, 4], fun (x) { x % 2 == 0 }));
WriteLine (filter (["foo", "bar", "foox"], fun (x) { x.StartsWith ("foo") }));
 
/* Выводит:
[2, 4]
["foo", "foox"]
*/


Новое обобщённое определение функции filter понимается так: для любого типа T, первый параметр имеет тип list [T], второй - T -> bool, а возвращаемый тип - list [T]. Это позволяет нам фильтровать как список чисел, так и список строк. Однако, тип принимаемого функцией аргумента и тип элементов списка должны совпадать (попробуйте ей передать лямбда-выражение, проверяющее чётность числа, и список строк). Поэтому мы используем один и тот же параметр типа T как для списка, так и для аргумента функции-предиката.

Используя тот же синтаксис, вы можете параметризовать следующие сущности:
  •  class classname[gentype0] {body}
  •  public methodname[gentype0] (parmlist) : methodtype {body}
  •  variant varname[gentype0] {body} (мы подробно рассмотрим варианты в следующей части)
Поскольку параметры типа используют короткий синтаксис списка, мы также можем определять много таких параметров:

Код
class classname[gentype0, gentype1, ... , gentypeN] {body} 


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

Заметка: имена параметров типа

Существует соглашение (взятое из ML) называть параметры типа 'a'b'c..., то есть альфа, бета, гамма,... Поскольку Nemerle поддерживает присутствие апострофов в идентификаторов, вы можете ему следовать.

Соглашение, пришедшее из C++, предписывает называть параметры типов буквами (TK), в то время, как C# обычно использует значимые имена вкупе с префиксом T (что-то вроде TElementName). Стандартная библиотека использует соглашение, пришедшее из ML.

Это сообщение отредактировал(а) Exception - 19.10.2006, 12:28
PM   Вверх
Любитель
Дата 7.2.2007, 18:42 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Программист-романтик
****


Профиль
Группа: Комодератор
Сообщений: 3645
Регистрация: 21.5.2005
Где: Воронеж

Репутация: 11
Всего: 92



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

Заодно пару незначительных замечаний:
1. Почему-то получилось дважды Week 1. в оригинале глава про списки, локальные функции и пр. - Week 2  smile 
2. 
Цитата(Exception @  21.9.2006,  17:43 Найти цитируемый пост)
Nemerle основыван на платформе Microsoft .NET Framework (или её бесплатной реализации – Mono)

  Всё же MS .Net Framework (в том числе и целиком SDK) тоже бесплатна. Не оупенсорсная, онли виндовая, но бесплатная.
3. Насчёт упражнений - я, конечно, не знаю но мне кажется, что их можно было бы написать. В Week 0, 1 они, конечно (учитывая "for OOP programmers") смешные, а вот функции для Week 2 я лично с удовольствием написал. Мелкие, но приятные. Кстати, насчёт свое реализации list_n так и не уверен (насчёт её рациональности). Я пока не стали писать (упражнения) - решил спросить.

Упражнения для Week 2
В качестве введения в функциональное программирование (ФП) напишите короткие функции обработки списков. Функции должны выбрасывать исключение при получении некоректных данных. Например, System.ArgumentException. Также не забудьте написать пару тестов для каждой функции.

1: вспомогательные функции
  • head[T] (l: list[T]): T - возвращает первый элемент списка.
  • tail[T] (l: list[T]): list[T] - возвращает список без первого элемента.
  • length[T] (l: list[T]): int - возвращает количество элементов списка.
  • max (l: list[int]): int - возвращает максимальный элемент целочисленного списка.
  • last[T] (l: list[T]): T - возвращает последний элемент списка.
  • last_n[T] (l: list[T], n: int): list[T] - возвращает n последних элементов списка.
  • nth[T] (l: list[T], n: int): T - возвращает n-ный элемент списка.
  • append[T] (l1: list[T], l2: list[T]): list[T] - возвращает объединение списков l1 и l2 (перегруженный оператор + не юзаем ;) ).
  • range (frpm: int, to: int): list[int] - возвращает список целых чисел от from до to (включительно).
2: итерирование
  • iter[T] (l: list[T], f: T -> void): void - применяет функцию f ко всем элементам списка.
  • map[A, B] (l: list[A], f: A -> B): list[ B ] - применяет f ко всем элементам списка и возвращает список результатов.
  • forall[T] (l: list[T], p: T -> bool): bool - возвращает true, если предикат p верен дял всех элементов списка.
  • find[T] (l: list[T], p: T -> bool): T - возвращает первый элемент списка, для которого условие p верно.
3: объединение
Напишите функцию flatten[T]: (l: list [list [T]]): list [T]. Результат функции - объединение полученных списков вместе. Например:
Код

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

должно вывести: [1, 2, 3, 4, 5].

Nemerle for OOP Programmers Week 3

Кортежи

Кортежи в Nemerle аналогичны оным в математике или в SQL. Это неизменяемая, возможно, разнотипная, последовательность объектов. Наиболее типичные примеры кортежей - это пары и тройки: 

Код

def our_pair = (1, 2);
def our_triple = (1, 12, -10);


В кортеже может быть до 9 элементов.

Можно создавать кортежи из разнотипных элементов:

Код

def another_pair = ("foo", 3.0);


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

Кортежи в качестве образца

Наиболее частоиспользуемый способ декомпозиции кортежа - это его использование в качестве образца:

Код

def divmod (x, y)
{
    def div = x / y; 
    def mod = x % y;
    (div, mod) // результат - кортеж
}

match (divmod (142, 13))
{
    | (d, m) =>
      System.Console.WriteLine ($ "div=$d, mod=$m");
}

// выводит: div=10, mod=12 


Одновариантные match имеют в Nemerle специальный, более короткий синтаксис:

Код

def (d, m) = divmod (142, 13);
System.Console.WriteLine ($ "div=$d, mod=$m");


Функцию divmod тоже можно укоротить:

Код

def divmod (x, y)
{
    (x / y, x % y)
}


Индексный доступ к элементам кортежа

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

Код

def divmod (x, y)
{
    ( x / y, x % y)
}

def r = divmod (142, 13);
System.Console.WriteLine ($ "div=$(r[0]), mod=$(r[1])");


Как и для массивов, нумерация идёт с нуля. 

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

Код

def pair = (2, "foo");
def idx = int.Parse(System.Console.ReadLine());
def element = pair[idx]; // ошибка 


В этом примере в зависимости от пользовательского ввода тип element может оказаться как string, так и int. Таким образом, компилятору приходится "останавиться" на типе object. Разработчики Nemerle сочли это не очень логичным (с чем я согласен). 

Также заметьте, что так как кортежи неизменяемы - вы не сможете изменить значения элементов кортежей:

Код

def p = (3, 4);
p[0] = 17; // ошибка


Тип кортежей

Тип кортежей образуется с помощью звёздочки (*) и типов элементов. Например, кортеж ("foo", 42, 3.1415) имеет тип string * int * double.

Полностью типизированная версия функции divmod будет выглядеть так: 

Код

static divmod(x: int, y: int): int * int
{
    (x / y, x % y)
}


Кортежи и параметры функций

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

Код

def print_two_ints (x, y)
{
  System.Console.WriteLine ($ "first=$x, second=$y");
}

def divmod (x, y) 
{
  (x / y, x % y)
}
print_two_ints (divmod (143, 77))


Присваивание кортежам

В Nemerle есть множественное присваивание - для этого используются псевдокортежи. Пример:

Код

mutable x = 17, y = 32;
(x, y) = (y, x); // обмен x и y 
(x, y) = (y + 12, 2 * x); // или взаимное изменение значений


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

Вариантный тип 

Вариантный тип - это форма представления данных нескольких фиксированных видов. Простейший пример вариантного типа - перечисления в C (и многих других языках).

Код

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


В Nemerle вы также можете объявлять перечисления в стиле C#, но об этом мы поговорим позже. А сейчас посмотрите на простейший вариантный тип:

Код

variant Color 
{
    | Red
    | Yellow
    | Green
}


Вариантный тип позволяет хранить какие-либо дополнительные данные для конкретного варианта:

Код

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


То есть, если цвет не является ни красным, ни желтым, ни зелёным, то мы можем записать его в RGB-форме. Вы можете создавать объекты вариантного типа, как и любые другие объекты - с помощью конструктора. Все вариантные типы неявно объявляют макрос [Record]. О нём мы говорили раньше, он добавляет конструктор, присваивающий значения всем полям: 

Код

def blue = Color.Different (0, 0, 1);
def red = Color.Red ();


С использованием классического ООП моделировать поведение вариантных типов можно наследованием:

Код
 
// C#
class Color
{
    class Red: Color {}
    class Green: Color {}
    class Yellow: Color {}
    class Different: Color
    {
        public float red;
        public float green;
        public float blue; 
        
        public Different (float red, float green, float blue)
        {
            this.red = red;
            this.green = green;
            this.blue = blue;
        }
    }
}
 

Как вы заметили, с вариантным типом это сделать намного проще. smile

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

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

Код

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

def string_of_color (color) 
{
    match (color)
    {
        | Color.Red => "red"
        | Color.Yellow => "yellow"
        | Color.Green => "green"
        | Color.Different (red = r, green = g, blue = b) => 
          $ "rgb($r, $g, $b)"
    }
}

System.Console.WriteLine (string_of_color (Color.Red ()));
System.Console.WriteLine (string_of_color (Color.Different (1, 1, 0)));

/*
    Выводит: 
        red
        rgb(1, 1, 0)
*/


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

Код

| Color.Different (r, g, b) =>


Представление деревьев в виде вариантных типов

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

Сейчас мы определим бинарное дерево целых чисел:

Код

variant Tree
{
    | Node
      {
        left: Tree;
        elem: int;
        right: Tree;
      }
    | Null
    
    public override ToString(): string
    {
        match (this)
        {
            | Tree.Node(l, e, r) => $ "($l $e $r)"
            | Tree.Null => "."
        }
    }
}


Таким образом, узел дерева либо пуст, либо представляет из себя данные и два поддерева. Мы также преопределяем метод ToString, чтобы макрос $ и метод WriteLine могли с ними работать (реализация метода ToString по умолчанию просто возвращает название типа: " Tree.Node" или "Tree.Null").

Теперь мы можем определить метод для вставки элемента в дерево. Он должен быть размещён в типе Tree:

Код

public Insert (e: int): Tree
{
    match (this) 
    {
        | Tree.Node (l, cur, r) =>
          if (e < cur)
            Tree.Node (l.insert (e), cur, r)
          else if (e > cur)
            Tree.Node (l, cur, r.insert (e))
          else 
            this
        | Tree.Null =>
          Tree.Node (Tree.Null (), e, Tree.Null ())
    }
}


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

Метод Contains проверяет наличие элемента в дереве:

Код

public Contains (e: int): bool
{
    match (this)
    {
        | Tree.Node (l, cur, _) when e < cur =>
          l.Contains (e)
          
        | Tree.Node (_, cur, r) when e > cur =>
          r.Contains (e)
        
        | Tree.Node => true
        | Tree.Null => false
    }
}


Этот метод работает подобно Insert: если искомый элемент меньше текущего, его поиск продолжается в левом поддереве, а если больше - в правом. 

Здесь используется одна новинка - конструкция when в образце. Любой образец может иметь дополнительное условие. Сперва проверяется соответствие самому образцу, а затем, в случае совпадения, проверяется истинность условия. 

Разумеется, также можно было использовать и обычную конструкцию if.

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

Код

// начинаем с пустого дерева
def t = Tree.Null ();
// добавляем несколько элементов 
def t = t.Insert(13).Insert(34).Insert(23);
// выведем дерево на консоль
System.Console.WriteLine (t);
// и информацию о наличии элементов
System.Console.WriteLine (t.Contains (13));
System.Console.WriteLine (t.Contains (42));

/* Результат:
(. 13 ((. 23 .) 34 .))
True
False
*/


XML-деревья

Как вы заметили, бинарные деревья не особо интересны, так что мы перейдём к хранению данных XML. smile 
К тому же, XML, пожалуй, практичнее (реализацией бинарных деревьев вам едва ли придётся заниматься).

Код

variant Node
{
    | Text
      {
        value: string;
      }
    | Element 
      {
        name: string;
        children: list[Node];
      }
}


Этот вариантный тип определяет простейшее представление XML-дерева. XML-узел - это текстовый узел с некоторым текстом внутри или узел-элемент с именем и (необязательно) дочерними узлами. Дочерние узлы представляются как список (list). 

Например, следующая XML-структура:

Код

<tree>
  <branch>
    <leaf/>
  </branch>
  <branch>
    Foo
  </branch>
</tree>


будет представлена как:

Код

Node.Element ("tree",
    [Node.Element ("branch", [Node.Element ("leaf", [])]),
     Node.Element ("branch", [Node.Text ("Foo")])]); 


Конечно, XML сам по себе является форматом данных, но обработка данных непосредственно в XML вряд ли удобна. Хотелось бы использовать различные внутренние форматы и использовать XML только для сериализации данных. 

Вот пример работы с XML-структурой:

Код

variant RefrigeratorContent
{
    | Beer { name: string; volume: float; }
    | Chips { weight: int; }
    | Ketchup
    
    public static FromXml (node: Node): RefrigeratorContent 
    {
        match (node)
        {
        | Node.Element ("ketchup", []) =>
          RefrigeratorContent.Ketchup ()
        
        | Node.Element ("beer", 
            [ Node.Element ("name", [Node.Text (name)]),
             Node.Element ("volume", [Node.Text (volume)])]) =>
          RefrigeratorContent.Beer (name, float.Parse (volume))
        
      | Node.Element ("chips",
            [Node.Element ("weight", [Node.Text (weight)])]) =>
        RefrigeratorContent.Chips (int.Parse (weight))
        
      | _ =>
        throw System.ArgumentException ()
    }
}


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

В примере выше очень часто пришлось писать имена RefrigeratorContent и Node. На самом деле они оба в большинстве случаев могут быть опущены по различным причинам.

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

Node во конструкции match, потому что компилятор "видит", что тип node - Node и "догадывается", что, к примеру, тип Element является сокращением для полного имени Node.Element

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

Код

variant RefrigeratorContent
{
    | Beer { name: string; volume: float; }
    | Chips { weight: int; }
    | Ketchup
    
    public static FromXml (node: Node): RefrigeratorContent 
    {
        match (node)
        {
        | Element ("ketchup", []) => Ketchup ()
        
        | Element ("beer", [Element ("name", [Text (name)]),
                            Element ("volume", [Text (volume)])]) => 
          Beer (name, float.Parse (volume))
        
      | Element ("chips", [Element ("weight", [Text (weight)])]) =>
        Chips (int.Parse (weight))
        
      | _ => 
        throw System.ArgumentException ()
    }
}


Раз у нас есть, что положить в холодильник, неплохо бы иметь и его самого smile

Код

[Record]
class Refrigerator
{
    public minimal_temperature: float; 
    public content: list [RefrigeratorContent];
    
    public static FromXml (node: Node): Refrigerator
    {
        match (node)
        {
            | Element ("refrigerator",
                Element ("minimal_temperature", [Text (min_temp)]) :: content) => 
              def parse_content (lst)
              {
                  match (lst)
                  {
                      | x :: xs =>
                        RefrigeratorContent.FromXml (x) :: parse_content (xs) 
                      | [] => []
                  }
              }
              RefrigeratorContent (float.Parse (min_temp), parse_content (content));
            
            | _ =>
              throw System.ArgumentException ("node")
        }
    }
}


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

Код

RefrigeratorContent (float.Parse (min_temp), content.Map (RefrigeratorContent.FromXml));


Пропуск match

Как вы заметили, при программировании на Nemerle многие функции содержат только конструкцию match. Nemerle представляет возможность опустить match в таких случаях. Если тело вашей функции начинается с |, то неявно подразумевается наличие строчки match (single_param) или (если в функции больше одного параметра) match ((param1, param2, ...)). В последнем случае вы можете использовать образцы-кортежи. 

Теперь последний пример можно переписать так:

Код

public static FromXml (node: Node): Refrigerator
{
    | Element ("refrigerator"
        Element ("minimal_temperature", [Text (min_temp)]) :: content) => 
      Refrigerator (float.Parse (min_temp), content.Map (RefrigeratorContent.FromXml))
    | _ =>
      throw System.ArgumentException ("node")
}


Упражнения

1: Tree.Iter
Добавьте в вариантный тип Tree метод Iter (f : int -> void) : void для обхода дерева слева направо (вначале посещается левое поддерево, затем вызывается f для текущего узла, затем посещается правое поддерево).

2: XML парсинг
Напишите функцию, читающую заданный XML-файл и записывающую его содерживое в вариантный тип Node, описанный выше. Затем напишите функцию записи ваших данных в формат вроде:
Код

(tree
(branch
(leaf)
)
(branch
($text "Foo")
)
)

Затем вы можете реализовать отступы при выводе:
Код

(tree
   (branch
      (leaf))
   (branch
      ($text "Foo")))

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

<?xml version='1.0' encoding='utf-8' ?>
<refrigerator>
  <minimal-temperature>-3.0</minimal-temperature>
  <beer>
    <name>Hyneken</name>
    <volume>0.6</volume>
  </beer>
  <beer>
    <name>Bydweisser</name>
    <volume>0.5</volume>
  </beer>
  <beer>
    <name>Plsner</name>
    <volume>0.5</volume>
  </beer>
  <chips>
    <weight>500</weight>
  </chips>
  <ketchup/>
</refrigerator>

Внимание: вам не надо писать собственный XML парсер. Используйте средства System.Xml. Для связи с с библиотекой System.Xml компилируйте с ключом -r:System.Xml. Например:
Код

ncc -r:System.Xml MyProgram.n


Это сообщение отредактировал(а) Любитель - 12.2.2007, 08:54


--------------------
PM MAIL ICQ Skype   Вверх
Exception
Дата 8.2.2007, 16:24 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Участник Клуба
Сообщений: 4525
Регистрация: 26.12.2004

Репутация: 29
Всего: 186



Цитата(Любитель @  7.2.2007,  19:42 Найти цитируемый пост)
Так как здесь давно ничего не прибавлялось, я решил, что никто не обидится, если я продолжу перевод (в первую очеред вопрос к Exception).


Наоборот, огромное спасибо! Я уж было забросил.. smile

Цитата(Любитель @  7.2.2007,  19:42 Найти цитируемый пост)
Насчёт упражнений


Я думаю, что они чересчур элементарны. Хотя, на списки, вроде, уже неплохие. Выложи, если хочешь smile .
PM   Вверх
Любитель
Дата 8.2.2007, 23:07 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Программист-романтик
****


Профиль
Группа: Комодератор
Сообщений: 3645
Регистрация: 21.5.2005
Где: Воронеж

Репутация: 11
Всего: 92



Договорились. Чуть позже выложу.

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


--------------------
PM MAIL ICQ Skype   Вверх
Void
Дата 8.2.2007, 23:44 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


λcat.lolcat
****


Профиль
Группа: Участник Клуба
Сообщений: 2206
Регистрация: 16.11.2004
Где: Zürich

Репутация: 25
Всего: 173



Любитель, опиши синтаксис по образу и подобию предложений по OCaml и Fortran, дай знать об этом администрации и может быть, когда-нибудь…

Если на Винграде подсветка Немерле появится раньше, чем на RSDN, это будет весьма неожиданный поворот, так что дерзай smile


--------------------
“Coming back to where you started is not the same as never leaving.” — Terry Pratchett
PM MAIL WWW GTalk   Вверх
Любитель
Дата 9.2.2007, 21:23 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Программист-романтик
****


Профиль
Группа: Комодератор
Сообщений: 3645
Регистрация: 21.5.2005
Где: Воронеж

Репутация: 11
Всего: 92



Код пишу далее не как C# (csharp), а как nemerle - с надеждой на возможную будущую подсветку.

Nemerle for OOP Programmers Week 4

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

Это последняя глава, посвящённая ФП в этом курсе.

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

Образцы с проверкой типа

Образцы с проверкой типа проверяют полученное значение на соответствие определённому типу, аналогично is в C# и Java. Синтаксически это выглядит так:
Код

using System.Console;

def check (o : object)
{
    match (o)
    {
         | i is int => WriteLine ($ "an int $(i * 2) / 2!");
         | s is string => WriteLine ($ "a string: $s!");
         | _ => WriteLine ("something else");
    }
}

check (21);
check ("foo");
check (3.0);

/* Output:
an int 42 / 2!
a string: foo!
something else
*/

В этой конструкции match вы видите как i используется как тип int, а s - как string. Образец is проверяет, если сопоставляемое значение (o типа object) является одним из представленных типов, связывая при этом значение с определённым идентификатором. Идентификатор получает статический тип для каждой ветки match-а. Компилятор неявно преобразует значение к типу сопоставленного идентификатора. Так типичный код на C#:
Код

if (x is Foo)
{
    Foo y = (Foo)x;
    // ... use y ...
}
else
{
    // ... 
}

или:
Код

Foo y = x as Foo;
if (y != null)
{
    // ... use y
}
else
{
    // ...
}

будет выглядеть в Nemerle как:
Код

match (x)
{
    | y is Foo => /* ... use x ... */
    | _ => /* ... */
}

Подобный код используется, когда вам надо привести объект к типу одного из наследников (или к классу, реализующему некоторый интерфейс).

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

Образец-запись

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

class MyClass
{
    public foo : int;
    public bar : string;
}

def c = MyClass();
match (c)
{
    | (foo = 0, bar = null) => System.Console.WriteLine("fine");
    | _ => System.Console.WriteLine("oops");
}

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

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

def check_class(c)
{
    match (c)
    {
        | MyClass where (foo = 0) => true
        | _ => false
    }
}

def result = check_class (MyClass());

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

class MyClass
{
    [Accessor]
    public foo : int;
    public bar : string;
}

def c = MyClass();
match (c)
{
    | (foo = 0, bar = null) => System.Console.WriteLine("fine");
    | _ => System.Console.WriteLine("oops");
}

Заметьте, что когда вы пишите образец-кортеж, он автоматически переводится в образец-запись.

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

match (construct_list())
{
   | [1, _] as l => handle_one_something(l);
   | _ => {}
}

Более интересный пример:
Код

variant Foo
{
    | A { x: int; mutable y: string; }
    | B
}

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

Здесь компилятор знает, что статический тип - Foo, поэтому возможно изменение значение поля y.

[size=7]Образцы с указанием типа[/szie]

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

def foo (l)
{
   | (s : string) :: _ => s[0]
   | [] => '?'
}

assert (foo (["foo", "bar"]) == 'f');

Здесь мы указываем компилятору, что тип s (первого элемента списка) - это string, таким образом мы можем обратиться к первому символу строки с помощью индексированию (в текущей версии Nemerle автоопределение при данном вызове не срабатывает  smile ).

Макрос assert проверяет выполнение заданного условия, в противном случае будет выброшено исключение.

Этот пример может быть переписан в таком виде:
Код

def foo (l : list[string])
{
   | (s : string) :: _ => s[0]
   | [] => '?'
}

assert (foo (["foo", "bar"]) == 'f');

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

Альтернативы

Вы можете комбинировать несколько образцов в одной ветке:
Код

match (my_list)
{
   | [1]
   | [1, 2] => WriteLine ("one or one, two");

   | [x]
   | [7, x] => WriteLine ($ "$x or seven, $x");

   | _ => WriteLine ("something else");
}

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

   | [7] // здесь должен быть x
   | [7, x] => WriteLine ($ "$x or seven, $x"); // или не быть здесь

Для преодоления этого ограничения используются образцы с with.

Использование with

При использовании групп альтернативных образцов иногда необходимо, например, использование в некотором меньшем образце переменной x, а в другом, большем - x и y. Как таковое это невозможно (так как в коде, выполняемом при успешном сопоставлении будет использоваться переменная y, не определённая для одного из альтернативных вариантов).

with разрешает эту проблему, указывая значения по умолчанию для y:
Код

def foo (_)
{
    | [x] with y = 3
    | [x, y] => x * y
    | _ => 42
}

assert (foo (3) == 9);
assert (foo (3, 4) == 12);
assert (foo (3, 4, 5) == 42);

Возможность сопоставления списков непостоянной длины с использованием with позволяет составлять более комплексные образцы.

Это сообщение отредактировал(а) Любитель - 12.2.2007, 08:56


--------------------
PM MAIL ICQ Skype   Вверх
Любитель
Дата 10.2.2007, 23:27 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Программист-романтик
****


Профиль
Группа: Комодератор
Сообщений: 3645
Регистрация: 21.5.2005
Где: Воронеж

Репутация: 11
Всего: 92



Приёмы ФП

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

Рекурсия или циклы

Цикл while:
Код

while (true)
   // некоторые дествия

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

def my_loop()
{
   // некоторые действия
   my_loop();
}

my_loop();


Этот код вызывает функцию my_loop, которая вначале выполняет тело цикла, а затем вызывает саму себя (опять выполняя тело цикла и т. д.). Как вы видите, результат аналогичен циклу выше.

Если у цикла есть условие, то он также может быть переписан:
Код

while (some_condition)
   // некотрые действия

получим:
Код

def my_loop()
{
   when (some_condition)
   {
       // некоторые действия
       my_loop();
   }
}

my_loop();


Производительность этих двух способов будет одинакова.  smile Компилятор Nemerle замечает, что после вызова my_loop внутри самой my_loop, больше ничего не делается, поэтому он заменит вызов на простой переход (на уровне IL - прим. перев.). Более того, он замечает, что функция вызывается только однажды (извне самой функции), таким образом происходит встраивание, то есть замена вызова функции на простую подстановку кода этой функции.

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

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

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

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

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

Переворачивание списка

Код

def rev (l, acc = [])
{
   match (l)
   {
       | x :: xs => rev(xs, x :: acc)
       | [] => acc
   }
}

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

// Вывод: [3, 2, 1]


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

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

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

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

Длина списка

Аккумуляторами могут быть не только списки:
Код

def length(l, acc = 0)
{
   match (l)
   {
        | _ :: xs => length(xs, acc + 1)
        | [] => acc
   }
}

System.Console.WriteLine (length ([4, 44, 22]));

// Вывод: 3


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

Fold-функции

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

List.FoldLeft ['a, 'b] (l : list ['a], ini : 'b, f : 'a -> 'b): 'b


FoldLeft получает список из типа 'a, начальное значение типа 'b и функцию f, получающую из значения типа 'a значение типа 'b. FoldLeft возвращает значение типа 'b.

Выражение List.FoldLeft ([x1, x2, ..., xN], ini, f) эквивалентно вызову f (xN, f (... f (x2, f (x1, ini)) ...). Возможно будет проще посмотреть на примерный код:
Код

def fold (l, acc, f)
{
   match (l)
   {
      | x :: xs => 
        fold (xs, f (x, acc), f)
      | [] => acc
   }
}


Таким образом обе функции выше можно значительно укоротить:
Код

def rev (l) { List.FoldLeft (l, [], fun (x, xs) { x :: xs }) }
def length (l) { List.FoleLeft (l, 0, fun (_, acc) { 1 + acc }) }


Также вы можете вызвать метод (нестатический) из списка:
Код

def rev (l) { l.FoldLeft ([], fun (x, xs) { x :: xs }) }
def length (l) { l.FoleLeft (0, fun (_, acc) { 1 + acc }) }


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

Переопределение имён

В Week 2 мы написали такой код:
Код

def filter (l, f)
{
    match (l)
    {
         | x :: xs =>
           def xs' = filter (xs, f);
           if (f (x)) x :: xs'
           else xs'
         | [] => [] 
    }
}


Здесь мы используем xs' как имя для результата фильтрования xs. Но мы также можем повторно использовать xs:
Код

def xs = filter (xs, f);


В императивном программировании это бы выглядило как переприсваивание, так как в этом же блоке уже была объявлена xs. Но в Nemerle это несколько другое.

Вы можете переопределить имя с другим типом:
Код

def x = [1, 2];
def x = "foo";

это вполне корректно. Хотя вы можете это делать и с mutable-переменными, в этом случае вы получите предупреждение компилятора.

Достаточно типичен код вроде:
Код

def result = 2 * 2;
def result = $ "the result is $result";

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

Это сообщение отредактировал(а) Любитель - 10.2.2007, 23:28


--------------------
PM MAIL ICQ Skype   Вверх
Любитель
Дата 12.2.2007, 01:34 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Программист-романтик
****


Профиль
Группа: Комодератор
Сообщений: 3645
Регистрация: 21.5.2005
Где: Воронеж

Репутация: 11
Всего: 92



Параметрический полиморфизм aka дженерики

Обобщённые (дженерик) типы (и неизменяемые коллекции)

Обобщённые типы позволяют определять строготипизированные контейнеры со значениями произвольного типа. Пример такого контйнера - массив: тип, сохраняемых в массиве элементов строго совпадает с типом массива. Аналогично списки содержат элементы только типа списка.

Ограничение типа списка позволяет улучшить статическую типобезопасность кода. Вместо использование списка, хранящего неопределённые object, вы можете ограничить свой список одним типом, написав list [int]. Это предотваращет добавление в список, скажем, строк и гарантирует, что элемент, прочитанный из списка - это целое число. Это избавляет от лишних преобразований в рантайме. Конечно, бывают случаи, когда нужно хранить и целые числа, и строки в одном списке, для этого вы можете использовать list [object].

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

Давайте посмотрим на тривиальную реализацию множества:
Код

class Set ['a]
{
    mutable storage : list ['a] = [];
    public Add (e : 'a) : void
    {
        when (! Contains (e))
            storage ::= e;
    }
    public Contains (e : 'a) : bool
    {
        storage.Contains (e);
    }
}

def s1 = Set();
s1.Add(3);
s1.Add(42);
assert (s1.Contains(3));
// s1.Add("foo"); // здесь ошибка!
def s2 = Set();
s2.Add("foo");
assert (s2.Contains("foo"));


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

Параметр обощённого класса 'a используется для определения типа спика storage и параметров методов.

Мы можем добавлять элементы в множество и проверять, что они уже в нём содержатся. Тот факт, что множество изменяет само себя, не очень хорош для стиля ФП. Сделаем другую реализацию:
Код

class Set ['a]
{
    storage : list ['a] = [];

    public this()
    {
        this([]);
    }

    this (s : list ['a])
    {
        storage = s;
    }

    public Add (e : 'a) : Set ['a]
    {
        if (Contains(e)) this
        else Set (e :: storage)
    }

    public Contains (e : 'a) : bool
    {
        storage.Contains(e)
    }
}

def s1 = Set();
def s1 = s1.Add(3).Add(42);
assert (s1.Contains(3));
def s2 = Set().Add("foo");
s2.Add("foo");
assert (s2.Contains("foo"));


Некоторые особенности этого класса:
  •  Публичный конструктор используется для создания экземпляров класса извне его самого.
  •  Приватный конструктор, принимающий список, используется в методе Add для возврата множества из новых элементов.
  •  внутрення ссылка this также используется в методе Add дял возврата самого себя, если добавляемый элемент уже в списке.
Строка def s1 = s1.Add(3).Add(42); также может вызвать вопросы. Она эквивалентна такому коду:
Код

def s1 = s1.Add(3);
def s1 = s1.Add(42);


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

Обобщённые (дженерик) методы

Вы уже видели как определяются обобщённые методы:
Код

public Length['a] (l : list['a], acc = 0) : int
{
    match (l)
    {
        | _ :: xs => Length(xs, acc + 1)
        | [] => acc
    }
}


Вы должны указать, какие обощённые типы вы собираетесь использовать. Так следующий код некорректен:
Код

class C
{
    public foo (x : list [T]) : void { }
}


А следующий - нормальный:
Код

class C
{
    public foo[T] (x : list [T]) : void {}
}


Спецификация обобщений (дженериков)

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

Для конструкторов

Это самый очевидный случай. Если у вас есть класс множества, описанный выше, и вы хотите создать множество из целых чисел, это можно сделать так:
Код

def s = Set.[int]();
s.Add("foo"); // ошибка!
def s2 = Set.[int]();
s2.Add(3); // OK


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

Для методов

Это достаточно просто - когда методы имеют дженерик-параметры, вы можете явно задать их:
Код

def l1 = Length.[string] (["foo", "bar"]); // OK
def l2 = Length.[string] ([1, 2, 3]); // ошибка


Для статических методов обощённых типов

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

class Foo['a]
{
    static public bar() : void
    {
        System.Console.WriteLine(typeof('a));
    }
}


Если вы попытаетесь вызвать Foo.bar(), компилятор не сможет ничего сказать о типе 'aFoo.bar.[int](), так как метод bar не обобщённый. Вы должны написать Foo[int].bar, также вы можете написать Foo.[int].bar(). Таким образом точка опциональна (но только в этом случае, разработчики Nemerle обещают поддержку и для других ситуаций).

Ограничение на тип

Для некоторых проектов, таких как MVC (model-view-controller) фреймворки, иногда нужно, чтобы обобщённые параметры удовлетворяли определённому интерфейсу. Мы рассмотрим эту возможность достаточно подробно, така как, вероятно, для большинства читателей она будет новинкой.

Например, элементы хранимые в упорядоченном бинарном дереве, должны быть доступны для сравнения. Таким образом, вначале мы определим интерфейс IComparable, а затем обощённый вариантный тип Tree. При этом Tree требует, чтобы параметр реализовывал нужный интерфейс.
Код

interface IComparable ['a]
{
    CompareTo (elem : 'a) : int;
}

variant Tree ['a]
    where 'a : IComparable['a]
{
    | Node
    {
        left : Tree ['a];
        elem : 'a;
        right : Tree ['a];
    }
    | Tip
}


В нашем объявлении вариантного типа мы добавили параметр where дял ограничения допустимых типов в качестве 'a. Такое сужение типа переменной, когда тип переменной может появляться в сужающем типе, называется в теории типов F-bounded полиморфизмом.

Заметим, что на самом деле в стандартной библиотеке уже есть интерфейс IComparable.

Так как мы уверены, что все элементы в дереве реализуют интерфейс IComparable, мы можем использовать метод CompareTo для вставки элементов в дерево:
Код

module TreeOperations
{
    public Insert['a] (t : Tree['a], e : 'a) : Tree['a]
        where 'a : IComparable['a]
    {
        match (t)
        {
            | Node (l, c, r) =>
              if (e.CompareTo(c) < 0)
                  Node(Insert(l, e), c, r)
              else if (e.CompareTo(c) > 0)
                  Node(l, c, Insert(r, e))
              else
                  Node(l, e, r)
            | Tip =>
              Node(Tip(), e, Tip())
        }
    }
}


Сейчас люди, знакомые с C# или Java, вероятно, спросят, почему бы не написать просто:
Код

interface IComaprable
{
    CompareTo (elem : IComparable) : int;
}

variant Tree
{
    | Node
      {
          left : Tree;
          elem : IComparable;
          right : Tree;
      }
    | Tip
}


Но это лишь полурешение. Чаще всего в дереве необходимо хранить элементы определённого типа, например, строки. Мы не хотим хранить в одном дереве и строки, и числа по очень простой причине: мы не сможем их сравниать между собой (более менее разумным способом). Такой дизайн лишает нас возможности быть уверенными, что мы храним в дереве именно элементы определённого типа. В методе CompareTo требуется повышающее преобразование для сравнения. При этом при попытке сравнения двух объектов разных типов мы получим исключение в рантайме. При извлечении элементов из дерева нам также придётся использовать повышающее преобразование. Это ещё одно потенциальное место для рантаймовых ошибок.

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

Для лучшего понимания этой проблемы посмотрите пример:
Код

interface IFrobincatable
{
    Frobnicate (x : int) : void;
}

class C1 : IFrobincatable
{
    public this() {}
    public Frobnicate(_ : int) : void {}
}

class C2 : IFrobincatable
{
    public this() {}
    public Frobnicate(_ : int) : void {}
}

module M
{
    f1['a] (o : 'a) : 'a
        where 'a : IFrobincatable
    {
        o.Frobnicate(3);
        o
    }

    f2 (o : IFrobincatable) : IFrobincatable
    {
        o.Frobnicate(3);
        C1()
    }

    Main() : void
    {
        def x1 = f1 (C1()); // x1 : C1
        def x2 = f1 (C2()); // x2 : C2
        def x3 = f2 (C1()); // x3 : IFrobincatable
        def x4 = f2 (C2()); // x4 : IFrobincatable
    }
}


В функции Main вызов f1 возвращает значение, переданное как параметр. f2 же этого не гарантирует: в строке def x4 = f2 (C2()); было передано значение типа C2, а возвращённое - C1. Хотя f2 нормально компилируется, конечные пользователи f2 могут получить рантаймовые ошибки, потому что их допущения о типе поддерживаются некорректно.

Упражнения

1

Напишите функциональную версию класса Set, использующую несбалансированные бинарные деревья, рассмотренные в прошлой главе. Используйте интерфейс System.IComparable [T].

2

Напишите следующие функции с использованием хвостовой рекурсии:
  • Sum (l : list [int]) : int - возвращает сумму элементов списка.
  • RevMap ['a, 'b] (l : list ['a], f : 'a -> 'b) : list ['b] - применяет функцию f ко всем элементам списка и возвращает писок результатов в обратном порядке.
  • RevFilter ['a] (l : list ['a], f : 'a -> bool) : list ['a] - аналогично функции Filter, но результирующий список должен быть в обратном порядке.
  • Map ['a, 'a] (l : list ['a], f : 'a -> 'b) : list ['b] - классическая Map-функция, но использующая RevMap и Rev.

3

Перепишите функции из второго упражнения (кроме последней) через FoldLeft.

Это сообщение отредактировал(а) Любитель - 12.2.2007, 13:12


--------------------
PM MAIL ICQ Skype   Вверх
Google
  Дата 18.11.2018, 23:23 (ссылка)  





  Вверх
Ответ в темуСоздание новой темы Создание опроса
Прежде чем создать тему, посмотрите сюда:
mr.DUDA
THandle

Используйте теги [code=csharp][/code] для подсветки кода. Используйтe чекбокс "транслит" если у Вас нет русских шрифтов.
Что делать если Вам помогли, но отблагодарить помощника плюсом в репутацию Вы не можете(не хватает сообщений)? Пишите сюда, или отправляйте репорт. Поставим :)
Так же не забывайте отмечать свой вопрос решенным, если он таковым является :)


Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, mr.DUDA, THandle.

 
0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Общие вопросы по .NET и C# | Следующая тема »


 




[ Время генерации скрипта: 0.1646 ]   [ Использовано запросов: 21 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.