Методы в примитивных типах PHP

Автор оригинала: Nikita Popov
  • Перевод
Некоторое время назад назад Энтони Феррара выразил мысли по поводу будущего PHP. Соглашусь с большинством его взглядов, но не со всеми. В статье я остановлюсь на одном конкретном аспекте: преобразования примитивных типов данных, таких как строки или массивы, в “псевдо-объекты”, позволяя выполнять в них вызовы методов.

Начнем с нескольких примеров:

$str = "test foo bar";
$str->length();      // == strlen($str)        == 12
$str->indexOf("foo") // == strpos($str, "foo") == 5
$str->split(" ")     // == explode(" ", $str)  == ["test", "foo", "bar"]
$str->slice(4, 3)    // == substr($str, 4, 3)  == "foo"

$array = ["test", "foo", "bar"];
$array->length()       // == count($array)             == 3
$array->join(" ")      // == implode(" ", $array)      == "test foo bar"
$array->slice(1, 2)    // == array_slice($array, 1, 2) == ["foo", "bar"]
$array->flip()         // == array_flip($array)        == ["test" => 0, "foo" => 1, "bar" => 2]

Здесь $str — это обычная строка и $array является простым массивом — они не объекты. Мы просто даем им немного объектного поведения, позволяя вызывать в них методы.

Обратите внимание, такое поведение совсем не за горами. Это уже не сон, кое-что уже существует прямо сейчас. PHP расширение scalar objects позволяет определить методы для примитивных типов.



Введение поддержки вызова методов в примитивных типах имеет ряд преимуществ, которые я рассмотрю далее:

Возможность очистить API


Вероятно, наиболее распространенной жалобой тех, кто хоть что-то слышал о PHP, является непоследовательное и непонятное именование функций в стандартной библиотеке, а также в равной степени непоследовательный и непонятный порядок параметров. Типичные примеры:

// различная концепция именования
strpos
str_replace

// совершенно непонятные имена
strcspn                  // STRing Complement SPaN
strpbrk                  // STRing Pointer BReaK

// инвертированный порядок параметров
strpos($haystack, $needle)
array_search($needle, $haystack)

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

Логичный выход — просто добавить в PHP6 огромное количество алиасов для функций, которые будут унифицировать имена и параметры вызова. Мы будем иметь string\\pos(), string\\replace(), string\\complement_span() или что-то вроде того. Лично для меня (и, кажется, многие php-src разработчиков придерживаются схожего мнения) это не имеет особого смысла. Нынешние имена функций глубоко укоренились в мышечной памяти любого PHP-программиста и делать несколько тривиальных косметических изменений, кажется, просто незачем.

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

  • Я бы очень хотел, чтобы методы $string->split($delimiter) и $array->join($delimiter), являющиеся общепринятыми, имели нормальные для этих функций названия (в отличие от explode и implode). С другой стороны, очень неудобно иметь string\\split($delimiter) функцию при том, что уже существует функция str_split, которая делает совершенно другое (преобразует строку в массив).
  • Я бы хотел, чтобы новый API использовал исключения для отчетов об ошибках, как в OO API, в котором это уже воспринимается как данность, так и в переименованном процедурном API. Однако такой подход идет против текущего соглашения, в котором сказано, что все процедурные функции должны использовать warning для обработки ошибок. Конечно же это не высечено на камне, но я не хотел бы осознанно начинать холивар ;)


Моя главная цель в OO API для примитивных типов: начать с чистого листа, что позволит нам реализовать комплекс правильно разработанных решений. Но, конечно, это не единственное преимущество такого шага. ОО синтаксис предлагает ряд дополнительных преимуществ, которые будут рассмотрены ниже.

Улучшение читаемости


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

$output = array_map(function($value) {
    return $value * 42;
}, array_filter($input, function($value) {
    return $value > 10;
});

На первый взгляд не ясно, что вызвал array_map и к чему обратился array_filter? В каком порядке они вызвались? Переменная $input спрятана где-то в середине между двумя замыканиями, вызовы функций пишутся в обратном порядке, от того как они применяются на самом деле. Теперь тот же пример с использованием ОО синтаксиса:

$output = $input->filter(function($value) {
    return $value > 10;
})->map(function($value) {
    return $value * 42;
});

Я полагаю, что в этом случае порядок действий (сначала фильтр, потом маппинг) и исходный массив $input показаны более очевидно.

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

substr(strtr(rtrim($className, '_'), '\\', '_'), 15);

В этом случае ряд дополнительных параметров '_'), '\\\\', '_'), 15 совершенно сбивает с толку, трудно связать подставляемые значения с соответствующими вызовами функций. Сравните с этой версией:

$className->trimRight('_')->replace('\\', '_')->slice(15);

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

Еще одним бонусом, который получается из такого синтаксиса, является отсутствие проблемы «needle/haystack». В то время как алиасы позволяют нам устранить это путем введения соглашения об именовании, в ОО API подобной проблемы просто не существует:

$string->contains($otherString)
$array->contains($someValue)

$string->indexOf($otherString)
$array->indexOf($someValue)

Не может быть никакой путаницы относительно того какая часть выполняет какую роль.

Полиморфизм


PHP в данный момент предоставляет интерфейс Countable, который может быть реализован в классах, для того, чтобы настроить вывод count($obj). Зачем все это нужно? Из-за того, что мы не имеем полиморфизма функций. Тем не менее, у нас есть полиморфизм методов.

Если массивы реализуют $array->count() как (псевдо-)метод, на уровне кода можно будет не волноваться, что $array — это массив. Это может быть реализовано в любом другом объекте с помощью метода count(). В принципе, мы получаем такое же поведение, как при использовании Countable, только без необходимости в каких-либо манипуляциях.

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

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

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

Нестрогая типизация


Цитата из блога Энтони:
[C]каляры не являются объектами, но, что более важно, они не могут быть любыми типами. PHP зависит от системы типизации, которая искренне верит, что строки это целые числа. Много гибкости системы основано на том, что любой скалярный тип может быть преобразован в любой другой с легкостью. [...]

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


Чтобы проиллюстрировать эту проблему, рассмотрим следующий пример:

$num = 123456789;
$sumOfDigits = array_sum(str_split($num));

Здесь $num обрабатывается как строка из цифр, которые разделены с помощью str_split, а затем суммируются с помощью array_sum. Теперь попробуйте сделать то же самое с использованием методов:

$num = 123456789;
$sumOfDigits = $num->chunk()->sum();

Метод chunk(), который находится в string, вызывается из number. Что происходит? Энтони предлагает одно из решений:

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

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

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


Помимо объектов, PHP имеет следующие типы переменных:

null
bool
int
float
string
array
resource

Теперь давайте подумаем, что из списка на самом деле может иметь значимые методы: Сразу же можно убрать resource (legacy type) и посмотреть на остальных. Null и bool, очевидно, не нуждаются в методах, если вы не хотите придумывать мерзости типа $bool->invert().

Подавляющее большинство математических функций не слишком хорошо выглядят в качестве методов. Рассмотрим:

log($n)        $n->log()
sqrt($n)       $n->sqrt()
acosh($n)      $n->acosh()

Надеюсь, что вы согласны с тем, что математические функции для чтения гораздо приятнее в текущем обозначении. Есть, конечно, несколько методов, которые разумно было бы отнести в тип number. Например, $num->format(10) читается довольно красиво. Подробнее об этом. Нет никакой реальной необходимости в OO number API, так как в нем мало функций, которые можно включить. Кроме того, нынешний математический API не так проблематичен в плане именования в соответствии с математическими операциями, имена довольно стандартизированы.

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

Хотя очень часто используется преставление строк в качестве целых чисел, например, приходящих по HTTP или из БД, обратное не верно: очень редко, чтобы нужно было использовать целое число в виде строки. Следующий код будет запутает меня:

strpos(54321, 32, 1);

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

$num = 123456789;
$sumOfDigits = ((string) $num)->chunk()->sum();

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

С массивами ситуация еще проще: не имеет смысла применять операции для работы с массивами с тем, что не является массивом.

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

Но это не значит, что никакой проблемы здесь нет вообще. Из-за неправильного дизайна функций, может иногда случаться, что неожиданный тип пробирается в код. Например, substr($str, strlen($str)), кто-то очень «мудрый» решил возвращать bool(false) вместо string(0) "". Однако, такой вопрос касается только substr. С методами API он никак не связан, так что вы не столкнетесь с этим

Семантика передачи объекта


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

function change($arg) {
    echo $arg->length(); // $arg выглядит как объект
    $arg[0] = 'x';       // а теперь нет :3
}

$str = 'foo';
change($str); // $str остается прежним

$array = ['f', 'o', 'o'];
change($array); // $array остается прежним

Можно было бы изменить действие семантики. В моих глазах передачи больших структур, таких как массивы, по значению — довольно плохая идея, в первую очередь, предпочтительнее было бы, чтобы они передавались пообъектно. Тем не менее, получилась бы довольно большая дыра в обратной совместимости при смене подхода, по крайней мере, мне так кажется, я не выполнял тесты, чтобы определить фактическое воздействие такого изменения. Для строк, с другой стороны, передача в качестве объекта будет иметь катастрофические последствия, если мы заставим строки быть полностью неизменными. Лично я считаю текущий подход, позволяющий изменить конкретный символ в строке в любой момент, очень удобным (попробуйте сделать тоже самое в Python).

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

Проблему также можно расширить и на другие объектно-связанные функции. Например, вы могли бы писать что-то вроде $string instanceof string чтобы явно определять строка это или настоящий объект. У меня нет уверенность в том, как далеко все это должно зайти. Лучше строго придерживаться всех методов и явно упоминать, что это не реальные объекты. В этом случае получится хорошая поддержка фич ОО системы. Надо будет еще подумать над этим.

Текущее состояние


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

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

Я создал проект scalar objects, который реализован как расширение PHP. Он позволяет регистрировать класс, который будет обрабатывать вызовы метода для соответствующего примитивного типа. Пример:

class StringHandler {
    public function length() {
        return strlen($this);
    }

    public function contains($str) {
        return false !== strpos($this, $str);
    }
}

register_primitive_type_handler('string', 'StringHandler');

$str = "foo bar baz";
var_dump($str->length());          // int(11)
var_dump($str->contains("bar"));   // bool(true)
var_dump($str->contains("hello")); // bool(false)

Сейчас начата работа над string handler, включающий API спецификацию, но я так и не закончил проект. Надеюсь, найду мотивацию, чтобы когда-нибудь продолжить развивать эту идею. Уже существует ряд проектов, работающих на подобных API.

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

Подробнее
Реклама

Комментарии 35

    +5
    В моих глазах передачи больших структур, таких как массивы, по значению — довольно плохая идея


    А как же copy on write? Не холивара ради, просто вы предлагаете некий проект по улучшению языка, при этом не особо разбираясь в его работе?
      +9
      А как же copy on write? Не холивара ради, просто вы предлагаете некий проект по улучшению языка, при этом не особо разбираясь в его работе?


      Что за странная ныныче мода,
      на хабре спорить с переводом )
        +7
        А как же copy on write? Не холивара ради, просто вы предлагаете некий проект по улучшению языка, при этом не особо разбираясь в его работе?

        Небольшая справка: Автор статьи, Никита Попов, является котрибьютором PHP 5.5 фич, таких как генераторы и сопрограммы.

        Думаю, что он все-таки знает о чем говорит.
          +3
          …а также таких фич как variadic functions и много чего ещё
          +2
          А почему вы считаете, что проблема в производительности?
          Проблема в удобстве: передавая в функцию массив и изменяя его, мы копируем его, а не изменяем внешний. Это поведение сильно отличается от ожидаемого: того, к которому привыкли в других языках программрования.
            +2
            Это зависит от языка, на самом деле.
          +5
          В последнее время какие-то постоянные тёрки между контрибьюторами — я всё жду, когда у кого-то лопнет терпение, они форкнутся и начнут свой проект. Но опубликованный timeline на 7-ую версию вроде всех устраивает: видимо форк откладывается пока :)
            +3
            HACK + HHVM?
              0
              HACK не взлетит по следующим причинам (лично моё мнение):
              1) Не исправляет проблемы внутри АПИ
              2) Добавляет свои совершенно отвратительные конструкции, вроде "==>"
              3) Строгая типизация иногда зло (в мире веб разработки намного лучше чувствуют себя ruby, python, php, js, coffee и прочие, нежели TypeScript, Dart, C++, Haxe, и т.д… Но есть исключения, вроде .NET и Java, которые живут только благодаря тому, что это языки, принятые в основном в корпоративном секторе).
                +1
                В Python, Ruby и Java как раз таки строгая (сильная) типизация в отличии от PHP.
                  +1
                  Конечно же я попутал со статической типизацией, просьба простить.
                  +1
                  В Dart опциональная типизация. Он себя отлично в вебе чувствует. Просто не сильно популярен.
                    +1
                    Под «лучше чувствует» я и подразумевал как раз популярность. Но Dart не плох, не спорю, жаль только не исправляет некоторых недостатков JS (вроде инкапсуляции), и является вполне самостоятельным яп со своим апи, в отличии от TypeScript =(

                    Вообще моя мечта — это заменить js простым java-style языком со слабой динамической типизацией (т.е. оставить такую же типизацию), как в JS, но убрать всякие undefined, NaN, void(0) и прочие — оставить только null.
                      +1
                      Единственная причина по которой дарту никогда не стать мэйнстримом это то, что остальные разработчики браузеров отказались имплементить или использовать Dart в своих браузерах. С учетом того что ES6 уже не за горами (с учетом компиляции через traceur можно хоть сейчас писать на нем) то будущее Dart может быть светлым только на сервере.

                      Что до статической типизации — я не вижу в этом никакой проблемы. Проблемы возникают тогда, когда язык имеет статическую типизацию, не умеет сам вычислять тип исходя из контекста и у него нету возможности использовать полиморфизм параметров. Вот тогда все плохо, да. А так возьмите какой D или Go… Да даже тот же c++. это раньше там надо было писать что-то типа MySuperCollection list = some.getList(); Сейчас же можно написать auto list = some.getList() а компилятор сам все типы выведет. Зато у нас есть возможности провести нормальный статический анализ кода и выявить возможные проблемы быстро и просто. Вопрос реализации по сути.
                        0
                        ES6 хоть и добавляет всякие классы, корутины, let'ы и прочее-прочее, но не исправляет самой сути языка, т.е. просто выполняет роль Coffee и подсахаривает тот страх, что находится внутри самого ядра, увы.

                        К слову, из перечисленного выше — Haxe вполне себе такой язык со статической типизацией, который я бы хотел видеть вместо JS — очень приятный, советую как-нибудь посмотреть. Он даже собираться в JS умет… =)
                        +1
                        не исправляет некоторых недостатков JS (вроде инкапсуляции)

                        Исправляет. Не настолько по́лно как в других ЯП, в которых модификаторов доступа как минимум 3 штуки, но всё же. Стоит добавить "_" префиксом к любому идентификатору и он становится приватным для своей библиотеки.

                        и является вполне самостоятельным яп со своим апи, в отличии от TypeScript =(

                        Для меня это только плюс. На сервере всё работает шустро без каких-либо конвертаций в другие ЯП, а на клиенте пока что просто конвертируем в JS.

                        Dart много перенял от Java и других ЯП и он весьма не плохо спроектирован. Чего только стоит запрет на сравнение переменных разных типов, неочевидностью которых «славится» JS.
                  +1
                  Между ними, как мне видится, нет тёрок. Товарищи просто рассуждают на заданную тему.
                    +2
                    Вы про какую-то конкретную тему сейчас говорите?
                    Летом были достаточно серьезные трения по дальнейшему развитию: тогда как раз обсуждались следующая мажорная версия, phpng. Были громкие «уходы» (Anthony, Sara). Моё субъективное мнение, что обстановка накалялась достаточно серьезно
                      +1
                      Я только про эту статью и про этих людей.
                    +2
                    Кстати, по поводу тёрок: blog.ircmaxell.com/2014/10/an-open-letter-to-php-fig.html
                  • НЛО прилетело и опубликовало эту надпись здесь
                      +1
                      Нормальное именование сделать нельзя из-за потери обратной совместимости, которую очень бояться потерять. Пример с перекидыванием функций в пространства уже было (см. начало статьи, начиная с «Логичный выход — просто добавить в PHP6 огромное количество алиасов для функций») ну и прочее. А «объектизация» — это попытка убить двух зайцев одновременно — переписать std апи и сделать его унифицированным, и добавить очень красивую и любимую «хотелку».

                      К слову, вполне не обязательно делать все примитивы объектами, можно просто на этапе парсинга сопоставлять $some->length() функции mb_strlen($some), например составив какую-то такую карту алиасов: 'length' => ['mb_strlen' => 1], которая будет обозначать, что для метода length надо использовать функцию mb_strlen, где ссылка на $this (т.е. ссылка на примитив с которым происходят операции) будет передаваться в качестве первого аргумента. Ну или что-то подобное.
                        +1
                        Без строгой типизации (хотябы в виде типизации параметров, в таком случае можно анализировать и переодически заменять, это можно использовать для оптимизация, пример) не получится сопоставить $some->length() функции mb_strlen($some), так как php не типизированный, никто не знает каких типов будет эта переменна, так что в случае с php примитивами — это утиная типизация, мы просто знает что у переменной должен быть метод и вызываем его
                          0
                          Достаточно того, что php знает какой тип у переменной в тот момент времени, когда мы вызываем у неё length() и сам сопоставляет с оригинальной функцией. Этот пример (и соответственно комментарий) был рассчитан только на то, что бы показать, что реализовать ОО интерфейс у примитивов можно вполне без серьёзных затрат по оперативной памяти и процессорному времени.
                      +1
                      Такая фича создает опасность конфликта имен, когда например 2 библиотеки добавляют метод к типу с одним и тем же названием. И еще в добавок к этому, сложно найти в коде весь набор методов типа, если их будет несколько, особенно когда каждая библиотека будет добавлять что-то свое. И есть большая вероятность, что вызовы в таком стиле по производительности будут проигрывать обычным функциям в 2-10 раз.
                        +1
                        1) Существует так же опасность, что любая глобальная функция будет конфликтовать.
                        2) Ничего не мешает просто запретить добавлять методы в примитивы, а предоставлять просто готовый набор.
                        3) Есть пространства имён: use Some\Any\StringType; для импорта методов из Some\Any\StringType;
                        4) Про производительность я уже упоминал выше: habrahabr.ru/post/240561/#comment_8069813
                          0
                          4) Это не решение, т.к. переменная может быть и реальным объектом. Будет осуществляться поиск функции и разруливание типов, когда обычный вызов функции или статичного метода может быть вставлен в код напрямую (уже вместе с адресом функции).

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

                          А запись типа $a->cos() довольно сомнительна.
                            0
                            Ну так и предполагается не трогать мат. функции, а поменять только работу со строками и массивами
                              –1
                              4) На этот вопрос я тоже ответил: habrahabr.ru/post/240561/#comment_8070047
                              Пых внутри знает какого типа эта переменная на тот момент и сам выберет нужную функцию.

                              А фича решает простую проблему — каждый примитив останется примитивом, просто с наличием сопоставлений метода сабжа с существующей функцией, а не полноценным объектом с методами.
                                0
                                Дело в том, что он знает о типе в момент выполнения, а в байткод можно вставить прямой адрес вызываемой функции в момент компиляции и JIT'ить его или вызывать более оптимально, чем через поиск по хеш таблице.
                                  –1
                                  А можно вставить один из вариантов, либо метод объекта (если есть), либо алиас примитива при нахождении соответсвий — это может и помедленнее прямого адреса, но значительно быстрее поиска по всему хешу ;)
                          –6
                          Честно говоря, я думаю не приживется. PHP программисты любят решать многочисленные проблемы, которые можно было бы исправить уже давным давно. лютое ИМХО в минутку ненависти к PHP
                            +2
                            Так долго ненавидеть php можно только с условием что где то глубоко внутри. где то там… вы подавляете в себе желание вернуться к php)
                              0
                              Латентный php-шник? Новый термин в психиатрии?
                                0
                                У меня была идея свести программистов с психотерапевтом. Однако.)) Как-то не так много желающих побеседовать о не привычно забытом: хороших интересных беседах о смыслах жизни)

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

                          Самое читаемое