Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
 iakovlev.org 
 Languages
 С
 GNU С Library 
 Qt 
 STL 
 Threads 
 C++ 
 Samples 
 stanford.edu 
 ANSI C
 Libs
 LD
 Socket
 Pusher
 Pipes
 Encryption
 Plugin
 Inter-Process
 Errors
 Deep C Secrets
 C + UNIX
 Linked Lists / Trees
 Asm
 Perl
 Python
 Shell
 Erlang
 Go
 Rust
 Алгоритмы
NEWS
Последние статьи :
  Тренажёр 16.01   
  Эльбрус 05.12   
  Алгоритмы 12.04   
  Rust 07.11   
  Go 25.12   
  EXT4 10.11   
  FS benchmark 15.09   
  Сетунь 23.07   
  Trees 25.06   
  Apache 03.02   
 
TOP 20
 MINIX...3057 
 Solaris...2933 
 LD...2906 
 Linux Kernel 2.6...2470 
 William Gropp...2182 
 Rodriguez 6...2016 
 C++ Templates 3...1945 
 Trees...1938 
 Kamran Husain...1866 
 Secure Programming for Li...1792 
 Максвелл 5...1710 
 DevFS...1694 
 Part 3...1684 
 Stein-MacEachern-> Час...1632 
 Go Web ...1627 
 Ethreal 4...1619 
 Стивенс 9...1607 
 Arrays...1607 
 Максвелл 1...1593 
 FAQ...1539 
 
  01.01.2024 : 3621733 посещений 

iakovlev.org

Программирование в Erlang

case

В Erlang есть три вида условных выражений. Первый вид - выбор уравнения в определении функции при сопоставлении значения с образцами аргументов функции. Второй вид - case-выражения, их выполнение похоже на выполнение уравнений в определении функции. Третий вид - это if-выражения, которые можно представить как упрощённую версию case-выражений. Общий вид case-выражений:
 case conditional-expression of
   Patternl -> expressionl, expression2, ..;
   Pattern2 -> expressionl, expression2, ..;
   ...
   Patternn - > expressionl, expression!, ..
 end
В следующем примере в case-выражении вычисляется, принадлежность ли атом foo списку List. Вычисления производятся с помощью стандартной функции member из библиотеки lists . Если это так, возвращается атом ok, если нет, возвращается кортеж {error, unknown_element}:
 case lists:member(foo,List) of
   true -> ok;
   false -> {error, unknown_element}
 end
Case-выражения всегда возвращают значение, и вы запросто можете связать его с переменной. Если в case-выражении сопоставление с каждым из уравнений пройдёт безуспешно, программа остановится с ошибкой.

if

If-выражения похожи на case-выражения без выражения-условия и ключевого слова of:
 if
   Guardl -> expressionll, expressionl2, ...;
   Guard2 -> expression21, expression22, ...;
   ...
   Guardn -> expressionnl, expressionn2, ...
 end
В качестве результата if-выражение возвращает значение последнего выражения, вычисленного в теле этого уравнения. В следующем примере для переменной X проверяется содержит, ли она значе­ ние, меньшее, большее или равное единице. В следующем примере определяется четность числа двумя способами - с помощью операторов if и case:
 if					 case X rem 2 of
   X rem 2 == 1 -> odd;                        1 -> odd;
   X rem 2 == 0 -> even;                       2 -> even
 end					 end

Контролеры (guards)

Контролеры представляют собой дополнительные ограничения, которые могут быть применены в функциональных уравнениях либо case. Вы можете использовать контролеры и при определении функции, в их заголовках, где они следуют за ключевым словом when. Либо же вы можете их использовать в любом месте программы, где можно использовать выражение. И если контролеры используются как выражение, то они вычисляются до одного из атомов: true или false. Считается, что если вычисление контролера привело к true, то его вычисление прошло успешно. В противном случае - неудачным.

Примеры контролеров:

 f(X,Y) when is_integer(X), X > Y, Y < 6 -> ...
Эта запись означает, что "когда Х это целое число и Х больше чем У и У меньше чем 6, то ...". Запятая, разделяющая отдельные проверки в контролере означает логическое "И". Точка с запятой (;) означает логическое "ИЛИ".

Пример:

 my_add(X,Y) when no t ( ( ( X>Y ) or not ( is_ atom ( X ))) and (is_atom(Y) or (X==3.4))) -> X+Y.
Контролеры могут состоять из сложных сочетаний предикатов, но не могут включать функции, определённые пользователем. Предыдущий пример можно записать и так:
 my_add2(X,Y) when not(X>Y) , is_atom(X) ; not(is_atom(Y)) , X=/=3.4 -> X+Y.

Встроенные функции

Встроенные функции (BIF - built-in function) написаны в основном на С. Они являются частью виртуальной машины (ВМ). Изначально предполагалось, что эти функции будут включены только в модуль erlang. Но позже для повышения эффективности эти функции проникли и в другие модули. Среди них модули ets и lists. Большинство BIF являются частью языка Erlang, но есть и такие, что зависят от конкретной версии ВМ или даже реализации ВМ для данной операционной системы. Стандартные встроенные функции импортируются автоматически, и вам не надо выписывать имя модуля перед именем такой функции.

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

 hd         - Возвращает первый элемент списка.
 tl         - Возвращает элементы списка, оставшиеся после изъятия первого элемента.
 length     - Возвращает длину списка,
 size       - Возвращает число элементов в кортеже.
 element    - Возвращает n-ый элемент кортежа.
 setelement - Обновляет элемент кортежа и возвращает новый кортеж,
 appendelement - Добавляет элемент в конец кортежа.
Примеры:

 > List = [one , two , three, four , five].
 [one,two,three,four,five]
 > hd(List).
 one
 > tl(List).
 [two,three,four,five]
 > length(List).
 5
 > hd(tl(List)).
 two
 > Tuple = {1,2,3,4,5}.
 {1,2,3,4,5}
 > tuple size(Tuple).
 5
 > element(2,Tuple).
 2
 > setelement(3, Tuple, three).
 {1,2,three,4,5}
 > erlang:appendelement(Tuple, 6 ) .
 {1,2,3,4,5,6}
Функция apply принимает три аргумента: имя модуля, имя экспортируемой функции и список аргументов. Эта функция возвращает в качестве результата результат применения указанной в первом и втором аргументах функции к аргументам, переданным третьим аргументом. Функция apply может динамически определять, какую функцию вызвать, что лежит в основе обобщённых (generic) алгоритмов:
 > Module = examples.
 examples
 > Function = even.
 even
 > Arguments = [10].
 10
 > apply(Module,Function,Arguments).
 true
Функция date возвращает текущий календарный день в формате {Год, Месяц, День}, а функция time возвращает текущее время в виде кортежа {Час, Минута, Секунда}, функция now возвращает кортеж {Мегасекунды, Секунды, Микросекунды}, сколько времени прошло с полуночи первого января 1970 года. Функция now для каждого узла Erlang вернёт уникальное значение, даже если она будет вызвана несколько раз в микросекунду, её можно использовать в качестве уникального идентификатора.

Ввод и вывод производятся с помощью функций из модуля io. Операции для работы с файлами определены в модуле file. Для чтения со стандартного ввода существует функция io:get line/1, принимающая на вход строку или атом, которые будут показаны в качестве приглашения:

 1> io:get_line( "line>" ) .
 line>blablabla .
 "blablabla, \n"
Также можно считать определённое число символов:
 2> io :get_chars("tell me>",2).
 tell me> er
 "er"
Наиболее полезной является функция io:read/l, читающая терм Erlang со стандартного ввода:
 > io: read ("ok, then»").
 ok, then»atom.
 {ok,atom}
 > io: read ("ok, then»").
 ok, then»{2,tue,{mon,"weds"}}.
 {ok,{2,tue,{mon,"weds"}}}
 > io: read ("ok, then»").
 ok, then»2+3.
 {error , { 1 . erl_parse , " bad term"}}
Вывод в Erlang производится с помощью функции io:write, которая распечатывает терм Erlang. Но чаще всего пользуются функцией io:format, которая выполняет форматированный вывод. Функция io:format принимает следующие аргументы:
 • строку (или двоичные данные), определяющую формат вывода;
 • список значений для печати.
Управляющая последовательность начинается с символа тильда (~), и в простейшем случае за ним следует один символ:
 > List = [ 2 , 3 , math:pi()].
 [2,3,3.141592653589793]
 > Sum = lists:sum(List).
 8.141592653589793
 > io:format("Hello, world!~n",[]).
 Hello, world!
 ok
 > io:format("the sum of ~w is ~w.~n", [[2,3,4],lists:sum([2,3,4])]).
 the sum of [2,3,4] is 9.
 ok
 > io:format("the sum of ~w is ~w.~n", [List,Sum]).
 the sum of [2,3,3.141592653589793] is 8.141592653589793.
 ok
 > io:format("the sum of ~W is ~w.~n", [List,3,Sum]).
 the sum of [2,3j ... 3 is 8.141592653589793.
 ok
 7> io:format("the sum of ~W is ~f.~n", [List,3,Sum]).
 the sum of [2,3|...] is 8.141593.
 ok

Рекурсия

Задача: имеется список. Прибавим ко всем элементам списка единицу. Прийдется создать новый список, куда будет записан новый результат. Функцию назовем bump. Исходный список может быть пуст - в этом случае получаем:

 bump([]) -> [] ;
Если список содержит хотя бы один элемент, разделим его на голову и хвост и создадим новый список, голова которого будет содержать сумму единицы с голо­ вой исходного списка:
 bump([Head | Tail]) -> [Head + 1 | bump(Tail).
Решение заключается в рекурсивном вызове функции на оставшейся части списка. Т.е. функция имеет окончательный вид:
 Ьшпр(П) -> [ ] ;
 bump([Head | Tail]) -> [Head + 1 | bump(Tail)].
Мы вызываем ту же функцию, используя в ней те же переменные. Переменные уникальны для каждого вызова, и на каждой итерации они считаются несвязанными. Для каждого вызова функции в стеке вызовов создаётся новый фрэйм, в котором хранится информация о том, в какое место нужно вернуть результат функции, параметры функции и локальные переменные. Это важный аспект реализации вычислений в Erlang, несмотря на то что вы не сталкиваетесь с ним напрямую. Мы к нему ещё вернёмся, когда речь пойдёт о хвостовой рекурсии.

Рассмотрим более сложный пример. Вычислим среднее арифметическое списка чисел. Назовём функцию average. Что такое среднее арифметическое? Это есть сумма всех элементов списка, разделённая на его длину. Мы можем определить average следующим образом:

 average(List) -> sum(List) / len(List).
Нужно определить 2 дополнительных функции - sum и len. Окончательный вариант:
 average(List) -> sum(List) / len(List).
 sum([]) -> 0;
 sum([Head | Tail]) -> Head + sum(Tail).
 len([]) -> 0;
 len([_ | Tail]) -> 1 + len(Tail).
Мы специально использовали функцию len вместо стандартной length, чтобы показать, как можно переопределить функцию.

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

 even([]) -> [ ] ;
 even([Head | Tail]) when Head rem 2 == 0 -> [Head | even(Tail)];
 even([_ | Tail]) -> even(Tail).

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

 member(_, [ ] ) -> false ;
 member(H, [H | ]) -> true;
 member(H, [ | Т]) -> member(H, T).
Теперь рассмотрим хвостовую рекурсию. Вернемся к старом примеру с суммой списка:
 sum([]) -> 0;
 sum([Head | Tail]) -> Head + sum(Tail).
Здесь мы видим пример прямой рекурсии. Эту функцию можно определить по-другому, добавив дополнительный параметр для самой суммы. В этом случае сумма будет выглядеть так:
 sum_acc([],Sum) -> Sum;
 sum_acc([Head|Tail], Sum) -> sum_acc(Tail, Head+Sum).
Эта рекурсия называется хвостовой, потому что ее тело состоит из вызова себя самой.

Перепишем функцию bump, которая прибавляет к каждому элементу списка единицу, с помощью хвостовой рекурсии. Разница в том, что результирующий список нужно перевернуть:

 bump(L) -> bump_acc(L, [ ] ) .
 bump_acc([], Асc) -> reverse(Acc);
 bump_acc([H | T ] , Acc) -> bump_acc(T, [H + 1 | Acc]).
А теперь сравните два алгоритма работы функции bump - старый с прямой рекурсией и новый с хвостовой. Старый:
 bump([1,2,3]) => [1+1 | bump([2,3])
 bump([2,3])   => [2+1 | bump([3])
 bump([3])     => [3+1 | bump([])
 bump([]) => []
 [4 | []]
 [3 | [4]] => [3,4]
 [2 | [3,4]] => [2,3,4]
Новый:
 bump([1, 2, 3])
 => bump_acc([1, 2, 3 ] , [ ] )
 => bump_acc([2, 3 ] , [2])
 => bump_acc([3], [3, 2])
 => bump_acc([], [4, 3, 2])
 => reverse([4, 3, 2])
 => [2, 3, 4]
Т.е. мы видим, что в первом случае мы 2 раза проходим по списку.

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

   int sum(int boundary) {
     int i, sum = 0;
     for(i = 1 ; i <= boundary; i++)
 	sum += i;
     return sum;
   }
Аналогичная функция на эрланге:
   sum(Boundary) -> sum_acc(l, Boundary, 0 ) .
   sum_acc(Index, Boundary, Sum) when Index =< Boundary ->
 	sum_acc(Index + 1, Boundary, Sum + Index);
   sum_acc(_I, B, Sum) -> Sum.
Рекурсивный вызов соответствует телу for-цикла.

Обработка ошибок

Основные типы ошибок:
   function clause
   case_clause
   if_clause
   badmatch
   badarg
   undef
   badarith
Обрабатываются ошибки стандартным образом - с помощью try... catch
   try Exprs of
     Patternl [when Guardl] ->
 	ExpressionBodyl;
     ...	
     PatternN [when GuardN] ->
 	ExpressionBodyN
   catch
     [Classl: ]Excepti.onalPatternl
 	[when ExceptionGuardSeq 1] ->
 	    ExceptionBodyl;
     ...
     [ ClassN: ] ExceptionatPatternN
 	[when ExceptionGuardSeqN ]ExceptioriBodyN
   end
Пример использования:
  Х=2.
  try (X=3) of
     Val -> {normal, Val}
  catch
     _:_ -> 43
  end.
Здесь все образцы ошибок из всех классов (соответствует образцу _:_ ) отображаются в число 43, которое возвращается в качестве результата. В следующем варианте мы получим более конкретную ошибку:
  try (X=3) of
   Val -> {normal, Val}
  catch
   error:Error -> {error , Error }
 4> end.
 
 > {error,{badmatch,3}}
Функция throw позволяет нам возвращать ошибки собственного типа и обрабатывать их внутри t гу ... catch-выражения:
  try (throw(nonnormalreturn)) of
     Val -> {normal, Val}
  catch
     throw:Error -> {throw, Error}
  end.
 
 > {throw, non normal return}

Модули

Список наиболее важных стандартных библиотечных модулей эрланга:
   array
   calendar
   diсt
   erlang
   file
   filename
   io
   lists
   math
   random
   string
   timer

Примеры

Написать функцию sum2, которая по двум целым числам N и М, при N =< М, возвращает сумму всех чисел из интервала от N до М:
 sum2(Min, Max) -> sum2(Min, Min, Max, 0 ) .
 sum2(Index, Min, Max, Sum) when (Index =< Max) and  (Index >= Min) -> 
       sum2(Index + 1, Min, Max, Sum + Index);
 sum2(_I, _Min, _Max, Sum) -> Sum.
 
 
Написать программу, которая распечатывает числа в диапазоне от Min до Max:
 n_print(Min, Max) -> n_print(Min, Min, Max) .
 n_print(Index, Min, Max) when (Index =< Max) and  (Index >= Min) -> 
   io:format("~p ~n",[Index]),
   n_print(Index + 1, Min, Max);
 n_print(_I, _Min, _Max) -> 0.
Для предыдущей задачи - дополнительное условие: распечатывать только четные числа:
 n_print(Min, Max) -> n_print(Min, Min, Max) .
 n_print(Index, Min, Max) when (Index =< Max) and  (Index >= Min) -> 
   if Index rem 2 == 0 ->  io:format("~p ~n",[Index]);
      Index rem 2 == 1 ->  false
   end,  
   n_print(Index + 1, Min, Max);
 n_print(_I, _Min, _Max) -> 0.
Функция, которая принимает на вход список целых чисел и одно целое число, а возвращает список всех элементов списка, которые меньше либо равны числу, переданному вторым аргументом - привожу три решения:
 Вариант 1:
 filter([H|T], N) when H>N -> filter(T, N);
 filter([H|T], N) -> [H|filter(T, N)];
 filter([], _) -> [].
 
 Вариант 2:
 filter([], _Max, Result) ->  Result;
 filter([Head | Tail], Max, Result) when Head =< Max ->  
     filter(Tail, Max, Result ++ [Head]);
 filter([_Head | Tail], Max, Result) -> filter(Tail, Max, Result).
 filter(List, Max) -> filter(List, Max, []).
 
 Вариант 3:
 filter([], _, Acc) -> lists:reverse(Acc);
 filter([H|T], X, Acc) when H > X -> filter(T,X,Acc);
 filter([H|T], X, Acc) -> filter(T,X, [H|Acc]).
 filter(L,X) -> filter(L,X,[]).
Функция, которая преобразует список плоских списков в один список, соединяя все списки-элементы:
 Вариант 1:
 concatenate(L) ->
   concatenate(L, []).
 concatenate([], Result) ->
   Result;
 concatenate([Head | Tail], Result) ->
   concatenate(Tail, Result ++ Head).    
Функция, которая преобразует список списков произвольного уровня вложенности в один список, соединяя все списки-элементы привожу два решения :
 Вариант 1:
 flatten(List) ->
   flatten(List, []).
 flatten([], Result) ->
   Result;
 flatten([Head | Tail], Result) ->
   flatten(Tail, Result ++ flatten(Head));
 flatten(Value, Result) ->
   Result ++ [Value].
 
 Вариант 2:  
 flatten([   ]  ) -> [];
 flatten([_|_]=L) -> flatten(L, []).
 flatten([[_|_]=H | T], Tail) -> flatten(H, flatten(T, Tail));
 flatten([[   ]   | T], Tail) ->            flatten(T, Tail) ;
 flatten([      H | T], Tail) ->        [H| flatten(T, Tail)];
 flatten([           ], Tail) ->                       Tail.  
Переходим к сортировке. Реализуем следующие алгоритмы сортировки списков:
Быстрая сортировка Голова списка считается опорным элементом. Затем список разделяется на два: из тех элементов, что меньше опорного элемента, и остальных. Далее эти списки сортируются так же, как и исходный список, и объединяются с опорным элементом между ними.
Сортировка слиянием Список разбивается на два с примерно одинаковым числом элементов в каждом их них. После чего каждый из списков сортируется отдельно, и затем они сливаются в один список.
Сначала быстрая сортировка:
 Вариант 1:
 quicksort([]) -> [];
 quicksort([P|T]) ->
   [L, H] = split(P, T, [], []),
   quicksort(L) ++ [P] ++ quicksort(H).
 split(P, [F|T], L, H) when F < P -> split(P, T, [F|L], H);
 split(P, [F|T], L, H) -> split(P, T, L, [F|H]);
 split(_, [], L, H) -> [L, H].
   
 Вариант 2:  
 quicksort2([]) ->    [];
 quicksort2([Pivot | Rest]) ->
     quicksort2([L || L <- Rest, L < Pivot]) ++ [Pivot] ++ quicksort2([R || R <- Rest, R >= Pivot]).
Теперь сортировка слиянием:
 Вариант 1:  
 mergesort(L) ->
     {L1B, L2B} = lists:split(length(L) div 2, L),
     L1 = quicksort(L1B),
     L2 = quicksort(L2B),
     merge(L1, L2).
 
 merge(L1, []) -> L1;
 merge([], L2) -> L2;
 merge([H1 | T1], [H2 | T2]) when H1 =< H2 -> [H1 | merge(T1, [H2 | T2])];
 merge([H1 | T1], [H2 | T2]) -> [H2 | merge([H1 | T1], T2)].  
   
 Вариант 2:  
 mergesort2([]) -> [];
 mergesort2(L) ->
   [R] = merge2([[X]||X<-L]),  R.
 
 merge2([]) -> [];
 merge2([A, B|T]) -> merge2([merge2(A, B)|merge2(T)]);
 merge2([A]) -> [A].
 merge2([A|TA], [B|_] = LB) when A [A|merge2(TA, LB)];
 merge2([_|_] = LA, [B|TB]) -> [B|merge2(LA, TB)];
 merge2([], LB) -> LB;
 merge2(LA, []) -> LA.
Прочитать файл построчно и сохранить строки в списке:
 read_file(F) ->
   {ok, Data} = file:read_file(F),
   rawdoc(binary_to_list(Data)).
 
 rawdoc([]) -> [];
 rawdoc(Data) -> rawdoc(Data, []).
 
 rawdoc([], Line) ->
   [lists:reverse(Line)];
 rawdoc([$\n|Rest], Line) ->     
   [lists:reverse(Line)|rawdoc(Rest)];
 rawdoc([C|Rest], Line) ->
   rawdoc(Rest, [C|Line]).
Если вы хотите запустить исходник не из интерпретатора, а напрямую из командной строки, делается это так: напишем следующий код и сохранима его в файле с именем sample.erl:
 -module(sample).
 -export([main/1]).
 
 main([A]) ->
   I = list_to_integer(atom_to_list(A)),
   F = fac(I),
   io:format("factorial ~w = ~w~n" ,[I, F]),
   init:stop().
 
 fac(0) -> 1;
 fac(N) -> N*fac(N-1).
В нем мы определили функцию для нахождения факториала, а так же главную функцию main, которая будет читать параметр, переданный из командной строки - в данном случае это будет 25 - и передавать его в функцию для вычисления факториала. Для сборки и выполнения этой программы нужно набрать две команды в командной строке:
 erlc sample.erl
 erl -noshell -s sample main 25
Пакетная компиляция достигается в эрланге с помощью make-файлов. Ниже представлен шаблон для такого make-файла. MODS - список всех модулей, которые надо скомпилировать. Каждый модуль из строки MODS будет скомпилирован командой erlc Mod.erl . Некоторые модули - такие, как special - будут подвергнуты специальной обработке. В этом make-файле есть несколько целей - all, compile, special.beam .
 # leave these lines alone
 .SUFFIXES: .erl .beam .yrl
 
 .erl.beam:
 	erlc -W $<
 
 .yrl.erl:
 	erlc -W $<
 
 ERL = erl -boot start_clean 
 
 # список модулей для компиляции
 
 # Edit the lines below
 MODS = module1 module2 \
        module3 ... special1 ...\
        ...
        moduleN
 
 # The first target in any makefile is the default target.
 # If you just type "make" then "make all" is assumed (because
 #   "all" is the first target in this makefile)
 
 all: compile
 
 compile: ${MODS:%=%.beam} subdirs
 	
 ## special compilation requirements are added here
 
 special1.beam: special1.erl    
 	${ERL} -Dflag1 -W0 special1.erl
 
 ## run an application from the makefile
 
 application1: compile
 	${ERL} -pa Dir1  -s application1 start Arg1 Arg2 
 
 # the subdirs target compiles any code in 
 # sub-directories
 
 subdirs:
 	cd dir1; make
 	cd dir2; make
 	...
 
 # remove all the code
 
 clean:	
 	rm -rf *.beam erl_crash.dump
 	cd dir1; make clean
 	cd dir2; make clean  
Для того, чтобы запустить это make-файл, нужно выполнить команду, где Target - необязательный параметр. По умолчанию это all.
 > make[Target]
 
Еще короче make-файл можно записать так:
 .SUFFIXES: .erl .beam
 .erl.beam:
     erlc -W $<
 ERL = erl -boot start_clean
 MODS = module1 module2 module3
 all: compile
     ${ERL} -pa '/home/joe/.../this/dir' -s module1 start
 compile: 
     ${MODS:%=%.beam}
 clean:
     rm -rf *.beam erl_crash.dump

Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье