Почему у большинства языков программирования есть специальное ключевое слово или синтаксис для объявления функций? [закрыто]


39

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

Например в Python:

x = 2
y = addOne(x)
def addOne(number): 
  return number + 1

Почему бы нет:

x = 2
y = addOne(x)
addOne = (number) => 
  return number + 1

Точно так же на языке как Java:

int x = 2;
int y = addOne(x);

int addOne(int x) {
  return x + 1;
}

Почему бы нет:

int x = 2;
int y = addOne(x);
(int => int) addOne = (x) => {
  return x + 1;
}

Этот синтаксис кажется более естественным способом объявления чего-либо (будь то функция или переменная) и одного ключевого слова меньше, как defили functionв некоторых языках. И, IMO, он более последовательный (я смотрю там же, чтобы понять тип переменной или функции) и, вероятно, делает синтаксический анализатор / грамматику немного проще для написания.

Я знаю, что очень немногие языки используют эту идею (CoffeeScript, Haskell), но большинство распространенных языков имеют специальный синтаксис для функций (Java, C ++, Python, JavaScript, C #, PHP, Ruby).

Даже в Scala, которая поддерживает оба способа (и имеет вывод типов), чаще пишут:

def addOne(x: Int) = x + 1

Скорее, чем:

val addOne = (x: Int) => x + 1

IMO, по крайней мере в Scala, это, пожалуй, самая легко понятная версия, но за этой идиомой редко следуют:

val x: Int = 1
val y: Int = addOne(x)
val addOne: (Int => Int) = x => x + 1

Я работаю над своим собственным игрушечным языком, и мне интересно, есть ли какие-нибудь подводные камни, если я создаю свой язык таким образом, и есть ли какие-либо исторические или технические причины, по которым этот шаблон широко не используется?


9
Причина скорее всего историческая. Кто бы это ни делал первым, он делал это так, а все остальные копировали. Но я сомневаюсь, что мы можем знать наверняка.

29
Я думаю, это потому, что функция или метод просто не просто другая именованная сущность . В функциональных языках они есть (вы можете передавать их и т. Д.), Но в других языках (например, Java) функция или метод являются чем-то совершенно отличным от простой переменной и не могут рассматриваться как таковые (я допускаю, что Java-8 ослабляет это оператор), поэтому имеет смысл определять функции / методы по-разному, поскольку они ведут себя по- разному.
11684

15
Я не согласен с тем, что ваше предложение является более естественным способом объявления функций. Мне действительно не нравится синтаксис Coffeescript, и частично это связано с синтаксисом объявления функций. Дело в том, что если он не сломан, не чините его. Написание «def» или «function» не имеет большого значения и является более очевидным с первого взгляда по сравнению с некоторыми причудливыми Perl-подобными символами, которые с легкомысленными или усталыми глазами легко могут быть приняты за что-то другое. На личном замечании я также думаю, что предложенный вами синтаксис в примерах выглядит намного хуже, чем текущий синтаксис.
Рой

22
Как «=>» не является «специальным ключевым словом или синтаксисом для объявления функции»?
TML

11
Я не знаю о вас, но для меня (int => int) addOne = (x) => {это гораздо более "особенный" и "сложный", чем int addOne(int) {...
Богдан Александру

Ответы:


48

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

И в этих языках функции не просто еще одно значение:

  • В C ++, C # и Java вы можете перегружать функции: у вас может быть две функции с одинаковым именем, но разной сигнатурой.
  • В C, C ++, C # и Java вы можете иметь значения, которые представляют функции, но указатели функций, функторы, делегаты и функциональные интерфейсы отличаются от самих функций. Частично причина в том, что большинство из них на самом деле не просто функции, они являются функцией вместе с некоторым (изменяемым) состоянием.
  • Переменные изменчивы по умолчанию (вы должны использовать const, readonlyили finalзапретить мутации), но функции не могут быть переназначены.
  • С более технической точки зрения код (который состоит из функций) и данные являются отдельными. Как правило, они занимают разные части памяти, и к ним обращаются по-разному: код загружается один раз, а затем только выполняется (но не читается или записывается), тогда как данные часто постоянно выделяются и освобождаются и записываются и читаются, но никогда не выполняются.

    И поскольку C должен был быть «близок к металлу», имеет смысл отразить это различие и в синтаксисе языка.

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


12
C # имел анонимные функции - которые являются просто лямбдами без вывода типов и неуклюжим синтаксисом - с 2005 года.
Эрик Липперт,

2
«С более технической точки зрения код (который состоит из функций) и данные являются отдельными», - некоторые языки, наиболее заведомо LISP, не проводят такое разделение и на самом деле не обрабатывают код и данные слишком по-разному. (LISP - самый известный пример, но есть множество других языков, таких как REBOL, которые делают это)
Бенджамин Грюнбаум,

1
@ BenjaminGruenbaum Я говорил о скомпилированном коде в памяти, а не об уровне языка.
svick

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

@PatrickHughes Да, я упоминаю их во втором пункте. Но указатели на функции сильно отличаются от функций.
свик

59

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

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

Это действительно та же самая причина, по которой у нас есть циклы while и for, switch и if else и т. Д., Хотя все они в конечном итоге сводятся к инструкции сравнения и перехода. Причина в том, что он существует для блага людей, которые поддерживают и понимают код.

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


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

29
«Это действительно та же самая причина, по которой у нас есть циклы while и for, switch и if else и т. Д., Хотя все они в конечном итоге сводятся к инструкции сравнения и перехода ». +1 к этому. Если бы все программисты были довольны машинным языком, нам не понадобились бы все эти модные языки программирования (высокий или низкий уровень). Но правда в том, что у всех разный уровень комфорта в том, как близко к машине они хотят написать свой код. Как только вы найдете свой уровень, вам просто нужно придерживаться синтаксиса, который вам дан (или написать свои собственные спецификации языка и компилятор, если вы действительно обеспокоены).
Хоки,

12
+1. Более краткая версия может быть такой: «Почему все естественные языки отличают существительные от глаголов?»
СМ

2
«Непостижимая капля символов хороша для интерпретации машиной, но для людей было бы почти невозможно понять и поддержать». Какая необдуманная квалификация для такого скромного синтаксического изменения, как предложено в вопросе. Один быстро адаптируется к синтаксису. Это предложение является гораздо менее радикальным отклонением от соглашения, чем синтаксис вызова метода OO object.method (args) в свое время, однако последний не оказался почти невозможным для понимания и поддержки людьми. Несмотря на то, что в большинстве языков ОО методы не должны рассматриваться как хранимые в экземплярах классов.
Марк ван Леувен

4
@MarcvanLeeuwen. Ваш комментарий верен и имеет смысл, но некоторая путаница вызвана тем, как оригинальное сообщение отредактировано. На самом деле есть 2 вопроса: (i) Почему это так? и (ii) почему бы не так? , Ответ whatsisnameбыл больше адресован первому пункту (и предупредил о некоторой опасности удаления этих отказоустойчивых), в то время как ваш комментарий больше связан со второй частью вопроса. Действительно, можно изменить этот синтаксис (и, как вы описали, это уже было сделано много раз ...), но он не подойдет всем (так как упс не подходит всем тоже
Hoki

10

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

имя типа функции = ( список параметров ) тип результата : тело ;

Конкретно ваш пример будет читать

PROC (INT)INT add one = (INT n) INT: n+1;

Признание избыточности в том, что начальный тип может быть считан из RHS декларации, и, поскольку тип функции всегда начинается с PROC, это может (и обычно) заключаться в

PROC add one = (INT n) INT: n+1;

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

PROC (INT)INT func var := (INT n) INT: n+1;
PROC func var := (INT n) INT: n+1;

Однако в этом случае обе формы фактически являются аббревиатурами; поскольку идентификатор func varобозначает ссылку на локально сгенерированную функцию, полностью развернутая форма будет

REF PROC (INT)INT func var = LOC PROC (INT)INT := (INT n) INT: n+1;

К этой конкретной синтаксической форме легко привыкнуть, но она явно не имела большого числа последователей в других языках программирования. Даже функциональные языки программирования , такие как Haskell предпочитают стиль f n = n+1с = следующим списком параметров. Я думаю, причина в основном психологическая; В конце концов, даже математики не часто предпочитают, как и я, f = nn + 1, а не f ( n ) = n + 1.

Кстати, в приведенном выше обсуждении подчеркивается одно важное различие между переменными и функциями: определения функций обычно связывают имя с одним конкретным значением функции, которое не может быть изменено позднее, тогда как определения переменных обычно вводят идентификатор с начальным значением, но тот, который может измениться позже. (Это не абсолютное правило; функциональные переменные и не-функциональные константы встречаются в большинстве языков.) Кроме того, в скомпилированных языках значение, определенное в определении функции, обычно является константой времени компиляции, так что вызовы функции могут быть скомпилирован с использованием фиксированного адреса в коде. В C / C ++ это даже требование; эквивалент Алгола 68

PROC (REAL) REAL f = IF mood=sunny THEN sin ELSE cos FI;

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


1
«Математики не предпочитают f = nn + 1 над f ( n ) = n + 1» ... Не говоря уже о физиках, которые любят писать только f = n + 1 ...
leftaroundabout

1
@ leftaroundabout, потому что писать ⟼ - это боль. Честно.
Пьер Арло

8

Вы упомянули Java и Scala в качестве примеров. Однако вы упустили важный факт: это не функции, а методы. Методы и функции принципиально разные. Функции являются объектами, методы принадлежат объектам.

В Scala, которая имеет как функции, так и методы, есть следующие различия между методами и функциями:

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

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


2
«Функции - это объекты, методы принадлежат объектам». Это должно применяться ко всем языкам (например, C ++)?
Свик

9
«методы могут иметь параметры под именами, функции не могут» - это не принципиальное различие между методами и функциями, это просто причуда Scala. То же самое с большинством (все?) Других.
user253751

1
Методы - это просто особый вид функций. -1. Обратите внимание, что Java (и, соответственно, Scala) не имеет никаких других функций. Это «функции» - это объекты с одним методом (в общем случае функции могут не быть объектами или даже значениями первого класса; зависит ли это от языка).
Ян Худек,

@JanHudec: Семантически, у Java есть и функции и методы; он использует терминологию «статические методы» при обращении к функциям, но такая терминология не стирает семантического различия.
суперкат

3

Причины, которые я могу придумать:

  • Компилятору легче знать, что мы объявляем.
  • Для нас важно знать (тривиальным образом), является ли это функцией или переменной. Функции обычно являются черными ящиками, и мы не заботимся об их внутренней реализации. Мне не нравится вывод типов в возвращаемых типах в Scala, потому что я считаю, что проще использовать функцию с возвращаемым типом: часто это единственная предоставляемая документация.
  • И наиболее важным является следующая стратегия толпы , которая используется при разработке языков программирования. C ++ был создан для кражи программистов на C, а Java был разработан таким образом, чтобы не пугать программистов на C ++, а C # для привлечения программистов на Java. Даже C #, который я считаю очень современным языком с потрясающей командой, скопировал некоторые ошибки из Java или даже из C.

2
«… И C # для привлечения Java-программистов» - это сомнительно, C # гораздо больше похож на C ++, чем Java. и иногда это выглядит так, как будто сделано намеренно несовместимым с Java (без подстановочных знаков, без внутренних классов, без ковариации возвращаемого типа…)
Sarge Borsch

1
@SargeBorsch Я не думаю, что это сделано намеренно несовместимо с Java, я уверен, что команда C # просто пыталась сделать это правильно или сделать это лучше, как бы вы ни хотели взглянуть на это. Microsoft уже написала свою собственную Java & JVM и получила иск. Поэтому команда C #, вероятно, была очень заинтересована в затмении Java. Это, безусловно, несовместимо, и лучше для этого. Java начиналась с некоторых базовых ограничений, и я рад, что команда C # решила, например, по-разному делать дженерики (видимые в системе типов, а не только в сахарах).
Коденхайм

2

Если вы не хотите пытаться редактировать исходный код на машине, которая сильно ограничена в ОЗУ, или минимизировать время на считывание его с дискеты, что не так с использованием ключевых слов?

Конечно, читать приятнее, x=y+zчем это store the value of y plus z into x, но это не значит, что знаки препинания по своей природе «лучше», чем ключевые слова. Если переменные i, jи kесть Integer, и xесть Real, рассмотрим следующие строки в Паскале:

k := i div j;
x := i/j;

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

Хотя есть несколько контекстов, в которых может быть полезно сделать определение функции кратким (например, лямбда-выражение, которое используется как часть другого выражения), функции обычно должны выделяться и быть легко визуально распознаваемыми как функции. В то время как возможно было бы сделать различие намного более тонким и использовать только знаки препинания, какой смысл? Говоря Function Foo(A,B: Integer; C: Real): Stringпроясняет, как называется функция, какие параметры она ожидает и что она возвращает. Может быть, можно было бы сократить его на шесть или семь символов, заменив их Functionнекоторыми знаками препинания, но что из этого получится?

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


3
Я не думаю, что этот вопрос о краткости. Тем более что, например, void f() {}на самом деле короче, чем лямбда-эквивалент в C ++ ( auto f = [](){};), C # ( Action f = () => {};) и Java ( Runnable f = () -> {};). Краткость лямбд проистекает из логического вывода и опущения return, но я не думаю, что это связано с тем, что задают эти вопросы.
svick

2

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

На языках наследия ML или Miranda, OTOH, вы определяете функции большую часть времени. Посмотрите на некоторый код на Haskell, например. Буквально это в основном последовательность определений функций, многие из которых имеют локальные функции и локальные функции этих локальных функций. Следовательно, забавное ключевое слово в Haskell было бы ошибкой так же, как и требование оператора вызова на императивном языке для начала с assign . Назначение причин, вероятно, является наиболее частым утверждением.


1

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

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

Тем не менее, вы должны заметить, что в большинстве языков defсинтаксис -style (то есть традиционный синтаксис) ведет себя иначе, чем стандартное присвоение переменных.

  • В семействе Cи C++функции обычно не рассматриваются как «объекты», то есть фрагменты типизированных данных, которые нужно скопировать и поместить в стек, и еще много чего. (Да, у вас могут быть указатели на функции, но они по-прежнему указывают на исполняемый код, а не на «данные» в обычном смысле.)
  • В большинстве ОО-языков есть специальная обработка для методов (т.е. функций-членов); то есть они не просто функции, объявленные внутри области определения класса. Наиболее важным отличием является то, что объект, для которого вызывается метод, обычно передается методу в качестве неявного первого параметра. Python делает это явно с self(который, кстати, на самом деле не является ключевым словом; вы можете сделать любой допустимый идентификатор первым аргументом метода).

Вам нужно подумать, точно ли ваш новый синтаксис (и, надеюсь, интуитивно) отражает то, что фактически делает компилятор или интерпретатор. Это может помочь понять, скажем, разницу между лямбдами и методами в Ruby; это даст вам представление о том, чем ваша парадигма «функции-просто-данные» отличается от типичной ОО / процедурной парадигмы.


1

Для некоторых языков функции не являются значениями. На таком языке сказать, что

val f = << i : int  ... >> ;

это определение функции, тогда как

val a = 1 ;

объявляет константу, сбивает с толку, потому что вы используете один синтаксис для обозначения двух вещей.

Другие языки, такие как ML, Haskell и Scheme, рассматривают функции как значения 1-го класса, но предоставляют пользователю специальный синтаксис для объявления констант, имеющих значение функции. * Они применяют правило «использование сокращает форму». Т.е. если конструкция является как общей, так и многословной, вы должны дать пользователю сокращение. Нелегко давать пользователю два разных синтаксиса, которые означают одно и то же; иногда элегантность должна быть принесена в жертву полезности.

Если на вашем языке функции относятся к 1-му классу, то почему бы не попытаться найти синтаксис, достаточно краткий, чтобы вы не испытали искушения найти синтаксический сахар?

-- Редактировать --

Еще одна проблема, которую еще никто не затронул, - это рекурсия. Если вы позволите

{ 
    val f = << i : int ... g(i-1) ... >> ;
    val g = << j : int ... f(i-1) ... >> ;
    f(x)
}

и вы позволяете

{
    val a = 42 ;
    val b = a + 1 ;
    a
} ,

из этого следует, что вы должны позволить

{
    val a = b + 1 ; 
    val b = a - 1 ;
    a
} ?

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

- Конец редактирования -

* Можно утверждать, что Haskell не входит в этот список. Он предоставляет два способа объявления функции, но оба являются, в некотором смысле, обобщением синтаксиса для объявления констант других типов


0

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

В вашем случае, функция с 4 переменными будет:

(int, long, double, String => int) addOne = (x, y, z, s) => {
  return x + 1;
}

Когда я смотрю на заголовок функции и вижу (x, y, z, s), но я не знаю типы этих переменных. Если я хочу узнать тип, zкоторый является третьим параметром, мне придется посмотреть на начало функции и начать считать 1, 2, 3, а затем увидеть, что тип является double. По-прежнему я смотрю прямо и вижу double z.


3
Конечно, есть такая замечательная вещь, как вывод типов, которая позволяет вам писать что-то вроде var addOne = (int x, long y, double z, String s) => { x + 1 }не-дебильного статически типизированного языка по вашему выбору (примеры: C #, C ++, Scala). Для этого вполне достаточно даже очень ограниченного локального вывода типа, используемого C #. Таким образом, этот ответ просто критикует определенный синтаксис, который в первую очередь сомнителен и фактически нигде не используется (хотя у синтаксиса Haskell есть очень похожая проблема).
Амон

4
@amon Его ответ может быть критикой синтаксиса, но он также указывает на то, что целевой синтаксис в вопросе не может быть «естественным» для всех.

1
@amon Полезность вывода типов - не удивительная вещь для всех. Если вы читаете код чаще, чем пишете код, то вывод типа является плохим, потому что вы должны выводить в голове тип при чтении кода; и намного быстрее читать тип, чем думать о том, какой тип используется.
m3th0dman

1
Критика кажется мне обоснованной. Для Java OP, кажется, думает, что [input, output, name, input] является более простой / понятной функцией def, чем просто [output, name, input]? Как утверждает @ m3th0dman, с более длинными подписями «более чистый» подход ОП становится очень затянутым.
Майкл

0

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

У Haskell есть специальная модель, в которой нет различия между оценкой и объявлением, поэтому нет необходимости в специальном ключевом слове.


2
Но некоторый синтаксис для лямбда-функций также решает эту проблему, так почему бы не использовать это?
svick

0

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

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

Рассмотрим отладку какого-либо модуля ядра в Java, где у каждого объекта есть операция toString (). Хотя можно ожидать, что метод toString () должен восстановить объект, ему может потребоваться разобрать и собрать объект, чтобы преобразовать его значение в объект String. Если вы пытаетесь отладить методы, которые toString () будет вызывать (в сценарии с крючками и шаблонами), чтобы выполнить свою работу, и случайно выделить объект в окне переменных большинства IDE, это может привести к сбою отладчика. Это связано с тем, что среда IDE попытается выполнить toString () для объекта, который вызывает сам код, который находится в процессе отладки. Ни одно примитивное значение никогда не будет таким дерьмом, потому что семантическое значение примитивных значений определяется языком, а не программистом.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.