PascalABC.NET - новые возможности (2015-2016)

Кому адресована данная статья

Данная статья ориентирована на тех, кто хотел бы получить быстрое представление о возможностях PascalABC.NET. В статье иногда приводятся некоторые общеизвестные факты по Delphi/Pascal, ориентированные на тех, кто мало знаком с этими языками.

Почему создан PascalABC.NET

Язык Паскаль традиционно использовался и используется в российском образовании для обучения начинающих (преимущественно школьников). Наиболее известная реализация — Borland Pascal и впоследствии Delphi — стали стандартом де-факто этого языка. Но время шло, мир менялся, появлялись и развивались новые языки, и к настоящему моменту язык Паскаль в его исходном воплощении потерял былую привлекательность. Свободная реализация — Free Pascal — уже длительное время практически не развивает язык и содержит катастрофически устаревшую консольную IDE, ориентированную на MS DOS.

Проект PascalABC.NET, начатый 10 лет назад в 2005 году, как раз и был ориентирован на то, чтобы внедрить в Паскаль современные возможности, появившиеся на тот момент в платформе .NET, и предоставить простую удобную современную IDE.

Современное состояние PascalABC.NET

Что такое PascalABC.NET сегодня?

PascalABC.NET — развивающийся язык, ориентирующийся на mainstream-языки такие как C#, Java, C++, Python.

Это язык, имеющий основой синтаксис Паскаля, но содержащий внутри большинство возможностей современных языков программирования. Если говорить коротко, то PascalABC.NET — это C# с паскалевским синтаксисом или ответвление Delphi в сторону .NET.

Где можно использовать PascalABC.NET? Простая IDE с мощными возможностями (система Intellisense, отладчик) позволяет рекомендовать его использовать в образовании — студентов и школьников старших классов. Благодаря компактности среды и мощности языка PascalABC.NET рекомендуется для академических и научных целей.

PascalABC.NET — это единственная российская разработка, содержащая компилятор промышленного языка, разработанный с нуля.

PascalABC.NET — это ряд расширений в исследовательских целях, не входящих в основную версию.

PascalABC.NET — это свободная разработка, позволяющая изучать код и разрабатывать собственные улучшения.

Базовые возможности

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

  • Операторы += и *=
  • Внутриблочные переменные
  • Инициализация при описании
  • Автоопределение типа
  • Описание переменной цикла в заголовке цикла

Рассмотрим простейшую программу вычисления an!

begin
var n := ReadInteger('Введите n: ');
var a := ReadReal('Введите a: ');
var p: real := 1;
for var i:=1 to n do
p *= a;
writeln(p);
end.

Здесь используются стандартные функции ReadInteger и ReadReal, выводящие перед вводом приглашение к вводу, описание переменных внутри блока, описание переменной прямо в заголовке цикла for, оператор *= и автоопределение типа переменной при описании по типу присваиваемого значения. Например, функция ReadReal возвращает real, поэтому переменная a получит тип real.

Описание переменных до begin используется редко — в основном, для переменных, используемых в течение всей программы/подпрограммы. Это сильно снижает захламление раздела описаний, характерное для Pascal-программ.

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

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

Для форматированного вывода можно используовать writelnFormat:

  writelnFormat('{0} в степени {1} = {2}',a,n,p);

Для разделения элементов вывода пробелами рекомендуется использовать стандартную процедуру Print (в стиле Python)

  Println(a,n,p);

Замер времени работы

Для замера времени работы участка программы используются стандартные функции Milliseconds, MillisecondsDelta. Milliseconds возвращает количество миллисекунд с начала работы программы, MillisecondsDelta - количество миллисекунд с начала работы программы или с момента последнего вызова Milliseconds или MillisecondsDelta:

function Sum2(n: integer): real;
begin
Result := 0;
for var i:=1 to n do
for var j:=1 to n do
Result += 1/i/j;
end;

begin
Println(Sum2(5000),Milliseconds/1000);
Println(Sum2(10000),MillisecondsDelta/1000)
end.

Case по строкам

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

begin
var Country := ReadString;
write('Столица: ');
case Country of
'Россия': writeln('Москва');
'Франция': writeln('Париж');
'Италия': writeln('Рим');
'Германия': writeln('Берлин');
else writeln('Нет в базе данных');
end;
end.

##Тип BigInteger Тип BigInteger является стандартным:

begin
var p: BigInteger := 1;
for var i:=2 to 100 do
p *= i;
writeln('100!=',p)
end.

Функции

Result

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

function fact(n: integer): integer;
begin
Result := 1;
for var i:=1 to n do
Result *= i;
end;

Этот синтаксис появился еще в Delphi. Синтаксис старого Паскаля с присваиванием имени функции категорически не рекомендуется.

Короткие определения функций

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

function Sqr3(x: integer) := x*x*x;

function CircleLen(r: real): real := 2 * Pi * r;

function Hypot(a,b: real) := sqrt(a*a + b*b);

function Len(x1,y1,x2,y2: real) := Hypot(x2-x1,y2-y1);

function DigitCount(x: integer) := abs(x).ToString.Length;

begin
writeln(Hypot(3,4));
writeln(DigitCount(-1234));
end.

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

Особенно хороши такие короткие функции как методы классов:

type Vec2 = auto class
x,y: real;
function Len := sqrt(x*x + y*y);
end;

begin
var a := new Vec2(3,4);
Println(a,a.Len);
end.

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

Записи

В PascalABC.NET можно создавать записи «на лету» с помощью функции Rec. Поля записи, возвращаемой функцией Rec, именуются последовательно: Item1, Item2 и т.д (по-существу Rec возвращает Tuple).

begin
var p := Rec('Петрова',18);
Println(p, p.Item1, p.Item2);
end.

Имена полей Item1, Item2 менее удобны, зато код пишется быстрее, чем такой:

type Pupil = record
name: string;
age: integer;
end;

var p: Pupil;

begin
p.name := 'Петрова';
p.age := 18;
Println(p.name,p.age);
Println(p);
end.

В последнем случае запись будет выведена в виде

(Петрова,18)

Таким образом, процедуры write/writeln, Print/Println при выводе переменной типа запись выводят в круглых скобках все её поля. Данное поведение отсутствует во всех остальных “стандартных” Паскалях.

Методы внутри записей

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

type Pupil = record
name: string;
age: integer;
procedure Init(name: string; age: integer);
begin
Self.name := name;
Self.age := age;
end;
end;

begin
var p: Pupil;
p.Init('Петрова',18);
end.

Как в Delphi и C++, есть возможность в интерфейсе класса объявлять лишь заголовки методов, а реализацию давать позже.

Массивы

В PascalABC.NET имеется два типа массивов — статические в стиле старого Паскаля и динамические:

var
a: array [1..10] of integer;
b: array of integer;

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

Инициализация

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

  • С помощью функции SetLength
begin
var a: array of integer;
var m: array [,] of integer;
var n := ReadInteger;
SetLength(a,n);
SetLength(m,n,n);
end.
  • С помощью синтаксиса new T[] в стиле C#
begin
var n := ReadInteger;
var a: array of integer := new integer[n];
var m: array [,] of integer := new integer[n,n];
end.

Поскольку работает выведение типа, то предыдущий код проще записать так:

begin
var n := ReadInteger;
var a := new integer[n];
var m := new integer[n,n];
end.
  • Одномерные — с помощью стандартных функций Arr…
begin
var a := Arr(2,3,5); // Заполнение целыми значениями
var cc := Arr('a','e','i','o','u','y'); // Заполнение символьными значениями
var b := ArrFill(0.0,10); // Заполнение десятью вещественными нулями
var b := ArrFill(10, i->i*i); // Заполнение десятью значениями по заданной формуле
var c := ArrRandom(10); // Заполнение десятью случайными целыми
var cr := ArrRandomReal(10,1,10); // Заполнение десятью случайными вещественными в диапазоне от 1 до 10
var a1 := ReadArrInteger(10); // Заполнение десятью вводимыми с клавиатуры целыми
end.

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

Цикл по динамическому массиву

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

  foreach var x in a do
Print(x);

Вывод динамического массива

Следующий код

begin
var a := Arr(2,3,5); // Заполнение целыми значениями
writeln(a);
var m := MatrixRandom(2,3);
writeln(m);
end.

выведет динамические массивы в виде

[2,3,5]
[[36,12,35],[84,37,38]]

Таким образом, процедуры write/writeln, Print/Println выводят структурированные значения всех составных типы данных. Данное поведение отсутствует во всех остальных “стандартных” Паскалях.

Сочетание функций Arr и Rec

Сочетая стандартные функции Arr и Rec, можно получать необходимые составные данные “на лету”. Например, для создания массива записей, заполненного заданными значениями, проще всего написать:

begin
var a := Arr(Rec('Иванов',18),Rec('Петрова',20),Rec('Попов',19));
writeln(a);
end.

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

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

begin
var a := Arr(Arr(1,2,3,4),Arr(5,6),Arr(7,8,9));
writeln(a);
end.

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

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

begin
var a := Rec('Попова',1,9,Arr(5,4,5,3));
writeln(a);
end.

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

Очень просто передать динамический массив в функцию, поскольку он помнит свой размер. Столь же просто вернуть массив из функции.

function SquareElems(a: array of integer): array of integer;
begin
SetLength(Result,a.Length);
for var i:=0 to a.Length-1 do
Result[i] := sqr(a[i]);
end;

Стандартные подпрограммы для динамических массивов

Для динамических массивов имеется несколько стандартных подпрограмм:

Sort(a)
Reverse(a)
Copy(a) // функция, возвращающая клон массива

Методы динамических массивов

В динамических массивах сосредоточено огромное число методов. Здесь мы приведем только самые простые:

a.Println // выводит элементы, разделяя их пробелом. Возвращает исходный массив
a.Println(',') // выводит элементы, разделяя их запятой
a.Sorted // возвращает отсортированный массив
a.Min
a.Max
a.Sum
a.Average

Последние 4 метода работают, разумеется, только для числовых массивов.

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

a.Println.Sorted.Println

При работе с последовательностями мы ещё столкнемся с подобными цепочечными записями.

Символы

Символы в .NET — двухбайтовые в кодировке Unicode. Поскольку в “стандартном” Паскале символы char - однобайтовые, то для совместимости функции Ord и Chr работают только с кодами символов в диапазоне 0..255 в кодировке Windows. Кроме того, для символов с кодами 128-255 преобразования символ <-> код происходят страшно долго.

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

Строки

Строки, как и в Delphi, — длинные: строка потенциально может занимать 2 Гб.

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

Например, в коде

begin
var s := 'Hello world';
s[6] := ','
end.

в результате выполнения

  s[6] := ','

строка ‘Hello world’ разрезается на части ‘Hello’ ‘world’, затем происходит конкатенация ‘Hello’ + ‘,’ + ‘world’ и после этого результат присваивается строке s. Попросту говоря, время изменения только одного символа строки пропорционально длине строки.

Что делать? Есть 2 пути: изменить алгоритмы обработки строк (например, формировать новую строку) либо использовать стандартный класс StringBuilder, который представляет собой как раз изменяемую строку:

begin
var s := 'Hello world';
var sb := new StringBuilder(s);
sb[5] := ','; // быстрая операция
writeln(sb);
end.

Другая проблема реализации строк состояла в том, что строки в .NET индексируются с нуля, а в Паскале — с 1. Она была решена так: строки индексируются с 1 (есть директива компиляции {$string_nullbased+}, включающая индексацию строк с нуля), но во всех методах строк предполагается, что строки индексируются с 0:

begin
var s := 'ABCNet';
Insert('.',s,4); // индексация с 1
writeln(s);
s := 'ABCNet';
s := s.Insert(3,'.'); // индексация с 0
writeln(s);
end.

Цикл по строке

Цикл по строке делается одним из двух способов.

Способ 1:

begin
var s := 'ABCNet';
for var i:=1 to s.Length do
Print(s[i]);
end.

Способ 2:

begin
var s := 'ABCNet';
foreach var c in s do
Print(c);
end.

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

Операции над строками

Помимо операции + слияния строк в PascalABC.NET определен ряд операций, облегчающих работу со строками:

begin
s := ''+12345; // число преобразуется в строку
writeln('a'*10); // строка повторяется 10 раз
s := s * 2; // '1234512345'
end.

Стандартные подпрограммы работы со строками

Наиболее известные стандартные подпрограммы работы со строками — это Pos, Insert, Delete, Copy, Length. Они использовались еще в Turbo Pascal и считаются золотым стандартом. Они хороши тем, что в случае выхода за границы индекса не выбрасывют исключение.

К другим стандартным подпрограммам относятся пришедшие из Delphi:

LowerCase(s)
UpperCase(s)
StringOfChar(ch,n)
ReverseString(s)
LeftStr(s)
RightStr(s)
Trim(s)

Методы строк

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

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

begin
var s := 'красный зеленый синий';
var ss := s.ToWords;
Sort(ss);
writeln(ss);
end.

В результате выполнения данной программы получим [зеленый,красный,синий]

Назад соединить массив элементов в строку можно с помощью метода JoinIntoString:

s := ss.JoinIntoString('-');
writeln(s);

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

Если строка содержит только целые или только вещественные числа, разделенные пробелом, то для преобразования соответственно в массив целых (вещественных) используются s.ToIntegers и s.ToReals. Не забываем, что по умолчанию десятичным разделителем считается точка.

Преобразования строка-число

В PascalABC.NET — множество способов преобразования строка–число

Из числа в строку:

var i: integer := 123;
var s := i.ToString;
s := IntToStr(i);
s := '' + i;
var r: real := 123.4;
s := FloatToStr(r);
s := '' + r;

Из строки в число:

var s := '123';
var i := StrToInt(s); // может возникнуть исключение
i := integer.Parse(s); // может возникнуть исключение
i := s.ToInteger; // может возникнуть исключение
var b := integer.TryParse(s,i); // исключение не возникает, b=false если преобразование невозможно
b := TryStrToInt(s,i); // исключение не возникает, b=false если преобразование невозможно
// и аналогично для других числовых типов

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

В PascalABC.NET появился новый тип — последовательность. Последовательность имеет тип sequence of T и представляет собой набор элементов, которые можно перебирать от первого до последнего. В частности, все массивы являются последовательностями, поэтому все методы для последовательностей годятся также и для массивов.

Синонимом sequence of T в .NET является тип IEnumerable<T>.

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

Функции для генерации последовательностей

Для генерации последовательностей используются стандартные функции Seq… и Range. Начнем с самой простой функции Seq перечисления элементов последовательности (это аналог функции Arr для динамических массивов):

begin
var q: sequence of integer;
q := Seq(1,3,5);
Println(q);
end.

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

[1,3,5]

Вывести последовательность можно и с помощью метода Print:

begin
var q := Seq(1,3,5);
q.Println;
end.

Для индивидуальной обработки элементов последовательности используется цикл foreach:

begin
var q := Seq(1,3,5);
foreach var x in q do
Print(x);
end.

Случайная последовательность задается функцией SeqRandom или SeqRandomReal:

begin
var q := SeqRandomReal(20);
q.Println;
end.

Среди модификаций Seq есть также SeqWhile и SeqFill.

Среди других способов формирования последовательности - функция Range:

begin
var q := Range(3,10); // последовательность 3 4 5 6 7 8 9 10
q.Println;
q := Range(1,10,2); // последовательность с шагом 2: 1 3 5 7 9
q.Println;
end.

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

begin
var q := Range(1,integer.MaxValue);
end.

не вызовет переполнение памяти.

С этой точки зрения конструкцию Range можно считать заменой цикла for. Сравните:

begin
Range(1,10).Println;
Range('a','z').Println;

for var i:=1 to 10 do
Print(i);

for var c:='a' to 'z' do
Print(c);
end.

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

В PascalABC.NET определены операции + и * для последовательностей.

Операция + конкатенирует две последовательности или последовательность и элемент. Операция sq * n повторяет последовательность sq n раз.

begin
var q := Range(1,10)+Range(20,30);
var q1 := Range(1,10)+777;
var q2 := Range(1,5) * 3;
Println(q);
Println(q1);
Println(q2);
end.

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

[1,2,3,4,5,6,7,8,9,10,20,21,22,23,24,25,26,27,28,29,30] 
[1,2,3,4,5,6,7,8,9,10,777]
[1,2,3,4,5,1,2,3,4,5,1,2,3,4,5]

##Лямбда-выражения

Лямбда-выражения активно внедряются в mainstream языки программирования. В C# они появились раньше, в C++ и Java — относительно недавно. Лямбда-выражения это способ создать описание простой функции “на лету” — прямо в коде программы. Они значительно сокращают код, повышают читаемость и позволяют не перемещаться по коду туда-сюда от описания функции к ее вызову.

Лямбда-выражения имеют в PascalABC.NET так называемый процедурный тип (название историческое).

Рассмотрим пример:

begin
var f: real->real := x->x*sin(x);
writeln(f(1));
end.

Здесь real->real — тип лямбда-выражения, описывающий функцию с параметром типа real и возвращающую значение типа real. Переменная этого типа — процедурная переменная, которой можно присваивать такие функции.Выражение x->x*sin(x) — и есть лямбда-выражение, “на лету” генерирующее функцию.

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

function lambda1(x: real): real;
begin
Result := x->x*sin(x)
end;

begin
var f: real->real := lambda1;
writeln(f(1));
end.

Здесь необходимо понимать, что в записи x->x*sin(x) тип x и тип возвращаемого значения автовыводятся по типу real->real переменной, которой мы присваиваем данное лямбда-выражение. Это — автовывод типов “в обратную сторону”. Такой автовывод, в частности, не позволяет записать

  var f := x->x*sin(x);

Здесь типы не выведутся. Такая запись может быть допустима в языках с динамической типизацией, но в PascalABC.NET — языке со статической типизацией — это ошибочно.

Тип real->real имеет синоним в .NET — это тип Func<real,real>, а также синоним в Delphi — function(x: real) real. В PascalABC.NET все эти три описания эквивалентны:

begin
var f: real->real;
var f1: Func<real,real>
var f2: function(x: real) real;
end.

Рассмотрим более сложные лямбда-выражения:

() -> 1
function -> 1
(x,y) -> x*y
(x,y: integer) -> x*y
(x,y: integer): integer -> x*y
(x: integer; y: integer) -> x*y
(x,y: integer) -> begin var a := x*y; Result := a*a end
(x,y: integer) -> begin var a := x*y; Result := a*a end
function (x,y) -> x*y
function (x,y: integer) -> x*y
function (x,y: integer): integer -> x*y
function (x,y: integer): integer -> begin var a := x*y; Result := a*a end
procedure -> begin write(1); write(2) end
procedure (x: integer; s: string)-> begin write(x,s) end

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

Если функция описывается не одним выражением, а алгоритмом, то ее тело представляет составной оператор begin-end с присваиванием Result.

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

Рассмотрим более сложные типы процедурных переменных в синтаксисе ->:

() -> T;        // функция без параметров, возвращающая T
T1 -> T; // функция c параметром T1, возвращающая T
(T1,T2) -> T // функция c параметрами T1 и T2, возвращающая T
(T1,T2,T3) -> T // функция c параметрами T1, T2 и T3, возвращающая T
и т.д.
() -> (); // процедура без параметров
T1 -> (); // процедура c параметром T1
(T1,T2) -> T // процедура c параметрами T1 и T2
(T1,T2,T3) -> T // процедура c параметрами T1, T2 и T3

Синтаксис -> не позволяет использовать типы, состоящие не из одного имени, например List -> List, поэтому в этих случаях приходится прибегать к типам Func<List, List> или function(l: List): List. Для подробностей следует смотреть [справку по описанию языка](http://pascalabc.net/downloads/pabcnethelp/)

Лямбда-выражения могут захватывать переменные из внешнего контекста:

begin
var a := 5;
var f: integer->integer := x->x*a;
writeln(f(2));
a := 10;
writeln(f(2));
end.

Захват всегда осуществляется по ссылке, поэтому вывод программы будет следующим:

10
20

Методы последовательностей, использующие лямбда-выражения

Методы последовательностей технически в .NET реализованы как методы расширения типа IEnumerable<T> и доступны для всех типов, которые являются разновидностями последовательностей: array of T, List<T>, HashSet<T> и т.д. Соответствующая технология в .NET называется LINQ to Objects.

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

Рассмотрим распространенные методы последовательностей Println (вывод), Select (проекция), Where (фильтрация) и OrderBy (сортировка):

begin
var a := Seq(1,9,2,8,3,7,4,6,5);
a.Println;
var b := a.Where(x -> x mod 2 <> 0);
b.Println;
var c := b.Select(x->x*x);
c.Println;
var d := c.OrderBy(x->x);
d.Println;
end.

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

1 9 2 8 3 7 4 6 5
1 9 3 7 5
1 81 9 49 25
1 9 25 49 81

Where фильтрует последовательность, оставляя только нечетные элементы (x->x mod 2 <> 0), Select проектирует элементы на квадраты (x->x*x), OrderBy сортирует элементы по ключу (в данном случае ключом является сам элемент: x->x). Каждый метод возвращает измененную последовательность.

Этот код можно записать в одну строку, используя тот факт, что все методы возвращают последовательность sequence of integer:

begin
var q := Seq(1,9,2,8,3,7,4,6,5);
q.Println.Where(x->x mod 2 <> 0).Println.Select(x->x*x).Println.OrderBy(x->x).Println;
end.

Примеры, иллюстрирующие использование методов последовательностей:

Пример 1. Сумма квадратов нечетных, меньших 100

begin
var sum := Range(1,100,2).Select(i -> i*i).Sum;
write(sum);
end.

Пример 2. Вывод всех простых чисел, меньших 1000

function IsPrime(x: integer): boolean;
begin
Result := Range(2,Round(sqrt(x)))
.All(i->x mod i <> 0)
end;

begin
Range(2,1000).Where(IsPrime).Print;
end.

Решение можно записать и в одну строку — с вложеными лямбдами:

begin
Range(2,1000).Where(x -> Range(2,Round(sqrt(x))).All(i->x mod i <> 0))Print;
end.

Пример 3. Таблица квадратов

begin
Range(1.0,2.0,10).Select(x->Rec(x,x*x)).Println(NewLine);
end.

Здесь при выводе элементы последовательности разделяются символом NewLine перехода на новую строку.

Вывод:

(1,1)
(1.1,1.21)
(1.2,1.44)
(1.3,1.69)
(1.4,1.96)
(1.5,2.25)
(1.6,2.56)
(1.7,2.89)
(1.8,3.24)
(1.9,3.61)
(2,4)

Пример 4. Быстрая сортировка

function QuickSort(a: sequence of integer): sequence of integer;
begin
if a.Count = 0 then
Result := a
else
begin
var head := a.First();
var tail := a.Skip(1);
Result := QuickSort(tail.Where(x->x<=head)) +
head +
QuickSort(tail.Where(x->x>head));
end;
end;

begin
var a := ArrRandom(20);
a.Println;
QuickSort(a).Println;
end.

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

Можно записать основной алгоритм и в одну строку:

function QuickSort(a: sequence of integer): sequence of integer;
begin
Result := a.Count = 0 ? a : QuickSort(a.Skip(1).Where(x->x<=a.First())) + a.First() + QuickSort(a.Skip(1).Where(x->x>a.First()));
end;

Только понятней он от этого не станет :(

Методы генерации последовательностей, использующие лямбда-выражения

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

begin
SeqGen(1,x->x+2,10).Println;
SeqGen(1,x->x*2,10).Println;
SeqGen(1,1,(x,y)->x+y,10).Println; // генерация чисел Фибоначчи
SeqWhile(1,x->x*3,x->x<1000).Println;
SeqFill(10,x->0.1*x+1).Println;
end.

Вывод:

begin
1 3 5 7 9 11 13 15 17 19
1 2 4 8 16 32 64 128 256 512
1 1 2 3 5 8 13 21 34 55
1 3 9 27 81 243 729
1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2
end.

Файлы

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

Сравним решение задачи о выводе содержимого файла на экран в старом и новом стиле.

Старый стиль:

var 
f: Text;
s: string;
begin
Assign(f,'13_Files1.pas');
Reset(f);
while not eof(f) do
begin
readln(f,s);
writeln(s);
end;
Close(f);
end.

Новый стиль:

begin
var f := OpenRead('13_Files2.pas');
while not f.Eof do
begin
var s := f.ReadlnString;
writeln(s);
end;
f.Close;
end.

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

В PascalABC.NET имеется стандартная функция ReadLines, позволяющая превратить текстовый файл в последовательность строк. Поскольку последовательности имеют огромное количество методов, решение задач на текстовые файлы значительно упрощается.

Вот пример, иллюстрирующий считывание строк из файла и вывод их на экран:

begin
foreach var s in ReadLines('a.pas') do
writeln(s);
end.

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

Решение без цикла foreach, использующее метод Print, еще короче:

begin
ReadLines('a.pas').Print(NewLine);
end.

Здесь при выводе элементы последовательности строк разделяются символом NewLine перехода на новую строку.

Приведем более сложный пример, в котором дан файл freqs.txt, фрагмент которого приводится ниже:

135 6.67 агроном noun
136 1.29 агути noun
137 32.14 ад noun
138 5.51 адаптация noun
139 2.20 адаптироваться verb
140 1.16 адвентист noun
141 33.05 адвокат noun
142 1.04 адвокатский adj
...

Пусть надо отобрать в этом файле только глаголы — слова, помеченные идентификатором verb.

Вот вариант решения с foreach:

begin
foreach var s in ReadLines('freqs.txt') do
begin
var ss := s.ToWords;
if ss[3] = 'verb' then
writeln(ss[2]);
end;
end.

Вот — вариант решения с лямбда-выражениями и методами последовательностей:

begin
ReadLines('freqs.txt').Select(s->s.ToWords())
.Where(ss->ss[3]='verb').Select(ss->ss[2]).Println
end.

Он легко читается по цепочке — как и записан:

  • Считать строки из файла ‘freqs.txt’
  • Разбить их на слова
  • Отфильтровать только глаголы
  • Взять эти глаголы
  • Вывести их

Приведем решение еще одной задачи для иллюстрации стиля программирования с помощью методов последовательностей. Эта задача была взята из электронного задачника Programming Taskbook, который входит в поставку системы программирования PascalABC.NET:

LinqObj4°. Исходная последовательность содержит сведения о клиентах фитнес-центра. 
Каждый элемент последовательности включает следующие поля:

<Год> <Номер месяца> <Продолжительность занятий (в часах)> <Код клиента>

Все данные целочисленные. Значение года лежит в диапазоне от 2000 до 2010, 
код клиента — в диапазоне 10−99, продолжительность занятий — в диапазоне 1−30. 
Для каждого клиента, присутствующего в исходных данных, определить 
суммарную продолжительность занятий в течение всех лет (вначале выводить 
суммарную продолжительность, затем код клиента). Сведения о каждом клиенте 
выводить на новой строке и упорядочивать по убыванию суммарной продолжительности, 
а при их равенстве — по возрастанию кода клиента. 

Вот ее решение:

uses PT4, PT4Linq;

begin
Task('LinqObj4');
var r := ReadLines(ReadString)
.Select(e ->
begin
var s := e.ToWords();
result := new class (
hours := StrToInt(s[2]),
code := StrToInt(s[3])
);
end)
.GroupBy(e -> e.code,
(k, ee) -> new class(k, sum := ee.Sum(c -> c.hours) ))
.OrderByDescending(e -> e.sum).ThenBy(e -> e.k)
.Select(e -> e.sum + ' ' + e.k);
WriteLines(ReadString, r);
end.

Решение также простое — считываем строку, разбиваем ее на слова, формируем объект безымянного класса с полями hours и code, группируем по code, одновременно проектируя группу записей с одним кодом на одну запись, где вычислена сумма часов для данного клиента, сортируем как сказано в условии задачи, после чего форматируем вывод и выводим.

Указатели

Указатели в PascalABC.NET реализованы для совместимости. Однако, в .NET — ссылочная модель объектов, основанная на управляемой памяти и сборке мусора, поэтому указатели в стиле

type 
Point = record
x,y: integer;
end;

begin
var p: ^Point;
New(p);
p^.x := 5;
p^.y := 6;
Dispose(p);
end.

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

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

type
Node = auto class
x: integer;
next: Node;
end;

begin
var p := new Node(5, new Node(7, new Node(6, nil)));
writeln(p);
end.

Здесь p является ссылкой на объект класса Node, память под который выделяется конструктором. Для того чтобы очистить память, занимаемую таким списком, достаточно выполнить

p := nil;

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

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

(5,(7,(6,nil))) 

Модули

В PascalABC.NET всегда имеется главная программа, к которой подключаются модули:

uses AUnit,BUnit;
begin
x := 1;
AUnit.x := 1;
end.

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

Если в AUnit и BUnit описана глобальная переменная x, то данный код откомпилируется. Переменная x в присваивании x := 1; будет вначале искаться в основной программе, затем в модуле BUnit и затем в модуле AUnit.

Сокращенный синтаксис модулей

В PascalABC.NET наряду с синтаксисом модулей в стиле

Unit AUnit;

interface

function Add(a,b: integer): integer;

implementation

function Add(a,b: integer): integer;
begin
Result := a + b;
end;

end.

принят сокращенный синтаксис — без разделения на интерфейс и реализацию.

Unit AUnit;

function Add(a,b: integer) := a + b;

end.

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

Библиотеки dll

Управляемые библиотеки dll, которые может использовать любой код на другом .NET-языке, могут быть созданы в PascalABC.NET следующим образом:

library ALibrary;

function Add(a,b: integer) := a + b;

end.

Имеется также полный синтаксис библиотек — с interface и implementation — как у модулей.

Подключение такой dll в программе на PascalABC.NET осуществляется с помощью директивы компилятора reference:

{$reference ALibrary.dll}

begin
write(Add(2,3));
end.

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

В IDE PascalABC.NET имеется возможность создавать проекты — в этом случае подключаемые dll можно указывать в папке References проекта.

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

Например, для использования ALibrary.dll в коде на C# потребуется написать следующий код:

using System;

class Program
{
public static void Main()
{
var res = ALibrary.ALibrary.Add(2, 3);
Console.WriteLine(res);
}
}

Здесь в записи ALibrary.ALibrary.Add первый ALibrary — это пространство имен библиотеки, второй ALibrary — статический класс, содержащий все глобальные переменные и подпрограммы в качестве статических полей и методов, Add — статический метод класса Add

##Использование классов из системных библиотек .NET

Если в программе на PascalABC.NET необходимо использовать класс .NET, находящийся в некотором именованном пространстве имен, следует либо указывать полное имя:

begin
System.Console.WriteLine('Hello');
end.

либо подключать пространство имен System в секции uses:

uses System;
begin
Console.WriteLine('Hello');
end.

Таким образом, в секции uses используется своеобразное кровосмешение модулей языка Pascal и пространств имен .NET.

##Стандартные классы коллекций

Стандартные классы коллекций List<T>, LinkedList<T>, HashSet<T>, Dictionary<K,V> и сопровождающие их интерфейсы находятся в пространстве имен System.Collections.Generic. Однако, ввиду частого использования все они переопределены в стандартном модкле PABCSystem, подключаемом к любой программе (модуле). Поэтому указывать пространство имен при их использовании не требуется:

begin
var l := new List<integer>;
l.Add(3); l.Add(2); l.Add(5);
Println(l);

var h := new HashSet<integer>;
h.Add(3); h.Add(2); h.Add(5);
Println(h);

var d := new Dictionary<string,integer>;
d['крокодил'] := 2;
d['какаду'] := 5;
d['бегемот'] := 3;
Println(d);
end.

Вывод: [3,2,5] {3,2,5} {(крокодил,2),(какаду,5),(бегемот,3)}

Таким образом, Print/write выводит элементы множества и словаря в фигурных скобках, элементы массива и последовательности — в квадратных, элементы записи — в круглых.

Заключение

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

Читатель найдет полное описание языка PascalABC.NET на сайте http://pascalabc.net и в справочной системе инсталлировнного продукта.