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

в 9:26, , рубрики: php, ооп

Некоторое время назад назад Энтони Феррара выразил мысли по поводу будущего 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.

Автор: iGusev

Источник

Поделиться

* - обязательные к заполнению поля