Как стать автором
Обновить

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

НЛО прилетело и опубликовало эту надпись здесь
<sarcasm_mode>Останется только добавить mixed, который позволит использовать любой тип</sarcasm_mode>

А потом и вовсе отказаться от статической типизации. И так по кругу.

Согласен. Не вижу смысла его вводить после введения строгой типизации. А вы проголосовали в pull-request?
НЛО прилетело и опубликовало эту надпись здесь

Соглашусь с Вами, что некачественный код можно написать тысяча и одним способом и сейчас. Но вот предложение тоже не поддерживаю. Когда язык обязывает указывать строго один тип (в крайнем случае nullable), вам так или иначе придется преобразование типов выносить либо наверх, либо в другой метод, тем самым упрощая этот конкретный метод, избавляя его от необходимости обрабатывать несколько типов. В других строго-типизированных языках такие проблемы решаются перегрузкой, когда для разного набора аргументов (в том числе других типов) определяется другой одноименный метод, реализующий требуемую логику именно для такого набора. Вычитать такой код будет куда проще, чем когда все в одной куче. И уж если PHP идет в сторону более строгой типизации, то было бы логичнее придерживаться более строгого подхода и в данном случае тоже, и вполне достаточно того, что можно типы не указывать вообще. ИМХО, естественно.

НЛО прилетело и опубликовало эту надпись здесь

Так может декомпозировать и эти ситуации?:) Моя идея в том, что если язык движется в сторону строгости, то и разработчики должны писать более строгий код и стараться избегать ситуаций, где допустимы несколько типов одного аргумента или возвращаемых значений. Там где "капец как надо" или для легаси останется возможность тип не указывать вообще. А принятие этого предложения фактически легализует возможность юзать несколько типов везде где надо и не надо, что точно не приведет к росту качества php-кода.

НЛО прилетело и опубликовало эту надпись здесь

В этом случае нужно в phpDoc описать в каких случаях у вас будет false. Кроме этого, чтобы получить информацию о том, почему же все таки false, пользователю метода нужно будет догадаться вызвать json_last_error, а если вы вдруг решите метод поменять и парсить json как-то по другому? Здесь как раз таки намного разумнее кинуть исключение, если string не может быть возвращен, с описанием собственно почему, а пользователю уже решать, что с этим делать.

НЛО прилетело и опубликовало эту надпись здесь
Представьте, что это метод, который принимает или объект или массив для инициализации объекта и возвращает строку или false

Так проблема то собственно не в json, а в том, что вы должны


  1. Четко документировать в каких случаях может быть false (то есть от phpdoc мы не избавимся)
  2. Предоставить механизм получения детальной информации, что пошло не так

И это все решается как раз таки механизмом исключений.


Пример, когда возвращается либо результат, либо false, в принципе очень не удачен. Для того же json_decode в 7.3 ввели опцию JSON_THROW_ON_ERROR, чтобы в случае ошибки не false возвращать, а исключение кидать. И в целом этот подход считается одним из самых не удачных во встроенных функциях php

НЛО прилетело и опубликовало эту надпись здесь
Я, например, скажу, что это плохой код. Если вам надо обрабатывать массив — делайте метод который принимает массив.

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


Очевидно что, для MyClassInterface и для array логика обработки разная либо нам нужно привести аргументы к одному типу, тогда ваш метод будет выглядеть примерно так


public function decode(array|MyClassInterface $item): string
{
    if (is_array($item)) {
        // do some logic for array or cast type to MyClassInterface
        return ...;
    }

    // do some logic for MyClassInterface
    return ...;
}

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


В классических языках со строгой типизацией это бы решилось простой перегрузкой и код превратился бы в что-то типо того


public function decode(array $item): string
{
    // do some logic for array or cast type and call decode with MyClassInterface argument
    return ...;
}

public function decode(MyClassInterface $item): string
{
    // do some logic for MyClassInterface
    return ...;
}

И таким образом компилятор/интерпретатор бы сам решал, какой метод использовать в зависимости от аргумента. К сожалению, в php такой код работать не будет, но мы можем реализовать его немного по другому:


public function decodeArray(array $item): string
{
    // do some logic for array or cast type and call decode with MyClassInterface argument
    return ...;
}

public function decodeMyClass(MyClassInterface $item): string
{
    // do some logic for MyClassInterface
    return ...;
}

Таким образом, если нам необходимо будет реализовать decode для чего-то еще нам нужно всего лишь добавить еще один метод (а не модифицировать старый Open/Closed из SOLID) и мы гарантированно не сломаем то, что уже работает.


Либо, если array можно представить в виде MyClassInterface, то реализовать MyArrayClass, который бы создавался из массива и имплементировал MyClassInterface, и у нас бы остался decode с одним типом аргумента. Ну и конечно наоборот, когда MyClassInterface можно представить в виде массива…


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

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

Так пусть возвращаемым типом будет ResponseInterface, а SuccessResponse и ErrorResponse будут его имплементировать. И фич никаких не нужно.

Именно так я и делаю. Я просто привёл более-менее валидный кейс. Сам я тоже против такой фичи.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Хорошо, не буду цепляться.
Данный пример мне очень сильно напоминает WordPress, с их WP_Post|Int|Null и пачкой проверок внутри. В связи с этим и хочется найти практическое применение UT, а не примеры в вакууме. Сам я пока не могу придумать, где и как такое применять.
НЛО прилетело и опубликовало эту надпись здесь
Здесь как раз таки намного разумнее кинуть исключение, если string не может быть возвращен, с описанием собственно почему, а пользователю уже решать, что с этим делать.

С Union Types можно это исключение просто вернуть. Его и сейчас можно вернуть, только обрабатывать это неудобно, и контролировать сложнее, надо не использовать типизацию вообще.

Такой метод не должен возвращать false
Он должен или возвращать строку если все получилось, или кидать исключение, если что то пошло не так.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
с версии 7.3 он уже может кидать исключение при указании JSON_THROW_ON_ERROR
Ну и в целом стандартная либа php явно не образец для подражания в плане качественого API
НЛО прилетело и опубликовало эту надпись здесь

Пример json_encode как раз показывает, что такой подход не многим нравится. Если бы такое поведением было приемлемым, то его бы не меняли.

Поддерживаю. Хотя в некоторых ситуациях использую ?Type, чтобы вернуть null.
НЛО прилетело и опубликовало эту надпись здесь

Еще парочка примеров:


$user = getUser();

function getUser(string|int $id): User|null
{
    ...
}

$node = parseNode($str);

function parseNode(): NodeType1|NodeType2|NodeType3
{
    ...
}

Переписал на текущем синтаксисе. И зачем тут Union?


$user = getUser((string) $id);

function getUser(string $id): ?User
{
    ...
}

$node = parseNode($str);

function parseNode(): NodeInterface
{
    ...
}
И зачем тут Union?

Так ?User это и есть Union, просто частный случай. Зачем оставлять только частный случай, если можно сделать универсально?


NodeInterface

Это не то же самое. Они все должны теперь этот интерфейс откуда-то импортировать и добавить в объявление класса, даже если parseNode никаких функций не вызывает. Это повышает взаимозависимости. Я бы даже сказал, интерфейс в данном случае это имитация множественных типов, за счет того что от него могут разные типы наследоваться. Интерфейсы без методов это вот как раз такая штука. А если скажем есть еще один тип, который этот интерфейс реализует, но возвращать в этом месте программы его не надо, то все еще сложнее становится.

Так ?User это и есть Union

NullPointer? Объект в php — указатель. Указатель может принимать значение null. Тут нет таких противоречий и этот тип уже есть в языке.


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

Ой какая каша у вас в знаниях…


  1. Классы не наследуются от интерфейсов.
  2. Интерфейсы позволяют унифицировать поведение, если у вас есть какие-то классы обладающие одинаковым api, коду уже будет не важно в большинстве случаев какая конкретно у вас реализация.
  3. Интерфейсы уменьшают взаимозависимости т.к. скрывают детали реализации.

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

Объект в php — указатель. Указатель может принимать значение null

php -r "class C{} var_dump(gettype(new C()), gettype(null));"
Command line code:1:
string(6) "object"
Command line code:1:
string(4) "NULL"

В PHP это union type. Это же написано и в обсуждаемом RFC.


Ой какая каша у вас в знаниях…
Классы не наследуются от интерфейсов.

Знаете, я как-то рассчитывал, что с учетом темы дискуссии вы не будете придираться к этой формулировке, тем более что далее у меня есть фраза "А если скажем есть еще один тип, который этот интерфейс реализует".


В контексте проверки типов это ничем не отличается от наследования, instanceof вернет true в обоих случаях.


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

Да, я в курсе. И как мой комментарий этому противоречит?


Какое поведение задает интерфейс без методов? Поискал в проекте на Symfony, аж 70 штук нашлось. Один из них даже связан с деревом нод Symfony\Component\Config\Definition\Builder\NodeParentInterface.


Интерфейсы уменьшают взаимозависимости т.к. скрывают детали реализации.

Покажите пожалуйста, где тут детали реализации, если SomeType это класс, и почему их нет, если SomeType интерфейс?


function f(SomeType $v) {
    $v->doSomething($arg1);
    $v->doSomethingElse($arg2);
}

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


Нам достаточно будет накинуть интерфейс с методом, условно, toLog

Ну накиньте на какой-нибудь класс из папки vendor. Получится?)


Или вы предлагаете и в логгере написать Union со всеми классами которые этот метод имплементируют?

Я предложил конкретный пример с parseNode(). Это не значит, что так нужно делать везде.

Знаете, я как-то рассчитывал, что с учетом темы дискуссии вы не будете придираться к этой формулировке

Прошу прощения, это было лишним.


Какое поведение задает интерфейс без методов

Это маркер, но мы скорее говорим о непустых интерфейсах


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

Тогда вам и обрабатывать их придётся совершенно раздельно, иначе остаётся только надежда, что у них не изменится поведение.
А как это поведение тестировать вообще не представляю


Ну накиньте на какой-нибудь класс из папки vendor. Получится?)

Разумеется получится. Адаптеры, прокси, декораторы на выбор


Я предложил конкретный пример с parseNode()

Вы указали абстрактный пример с parseNode()

Это маркер, но мы скорее говорим о непустых интерфейсах

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


Тогда вам и обрабатывать их придётся совершенно раздельно

Ну да, поэтому также нужна конструкция для удобной обработки на вызывающей стороне. Но это не главное, они даже могут иметь один интерфейс. Запись с union types задает ограничение. Допустим, у нас есть еще один тип NodeType4, но возвращать его в этой части программы не надо. В вашем варианте есть возможность ошибки.


Разумеется получится. Адаптеры, прокси, декораторы на выбор

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


Вы указали абстрактный пример с parseNode()

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

НЛО прилетело и опубликовало эту надпись здесь
Бывают ситуации, когда нужно вернуть/передать какой-то тип вместе с другим

Возможно Вы имеете в виду «вернуть несколько аргументов из метода» — так для этого Union Types не нужен — с этим вполне справляется и массив (как минимум):
return [$val1, $val2];

В C# для этого завезли кортежи, в Java можно часто встретить что-то типа:
class Pair<T, U> { ... }

Что к слову тоже не является примером отличного кода — ведь как так? Вы не знаете какой тип Вы должны вернуть и прибегаете к обобщенному коду?
(например, ?string)

Ну так это влияние других ЯП e.g., Java/C#, что к слову не самое топовое решение, почитайте про null pointer hell.
Еще, к примеру, много встроенных функций языка возвращают комбинированные типы.

А вот это вообще отдельная песня. SPL уж точно не является примером для подражания. Нашумевшая, хоть уже и несколько устаревшая статья «PHP — fractal of bad design» является отличной иллюстрацией моих слов.
Насколько будет проще, когда не нужно будет писать PHPDoc комменты с пояснением, что за тип будет у переменной.

Я возможно скажу что-то новое для Вас, но PHPDoc комменты нужны скорее для того, чтобы понять зачем «интерфейсу» метода нужны те или иные параметры. Типы PHPDoc'a уже не так нужны, если Вы не работаете со старым legacy кодом конечно.
Некачественный код получается не тогда, когда в языке есть возможность, а тогда, когда эти возможности используют бездумно. И сейчас язык вполне себе помогает писать некачественный код, даже без Union Types.

Согласен, вот только сама возможность использовать Union Types не будет помагать с решением этой проблемы ничем — только усугублять.
НЛО прилетело и опубликовало эту надпись здесь
Тем не менее, к сожалению, я вынужден ней пользоватся.

К сожалению, мы все вынуждены ей пользоваться. Но Вы ведь понимаете, что union types не сделает SPL лучше?
Как говорили выше — куда лучше выбрасывать ошибку в непонятной ситуации, а не false.
Ну и как бы там ни было — ни того ни другого скорее всего в SPL не будет, твит на тему.

PHPDoc комменты нужны также для автокомплита в IDE, например где то в теле метода я получил обьект, но у метода не было return-type и IDE не знает что это, ну я и пишу

Опять таки, ну и чем Вам здесь поможет union types?

Как бы то ни было, я просто хочу подрезюмировать свое отношение к данному предложению — строго отрицательное. И вот почему: жизнь это не упростит аж никак (ну хочется прогеру поговнокодить, вернуть несколько различных типов — никто же не заставляет писать m():types), а вот потенциальных неприятных вещей добавит уж точно.
НЛО прилетело и опубликовало эту надпись здесь
Согласен. Тем более что вряд ли будет разрешено использовать мульти-объекты: getModel(): Post|Category
НЛО прилетело и опубликовало эту надпись здесь
Понимаю. Но тогда для IDE будет путаница какой класс использовать в данном случае в качестве подсказчика.
Кажется, я чего-то не понимаю – что делаете без UnionTypes? Вот два примера:

class User
{
    public function getFriends() : Collection|User[] { /* ... */ }
}


class Like
{
    public function getTarget() : Post|Comment { /* ... */ }
}


Контекст примеров: какой-нибудь типичный CRUD на Symfony+Doctrine. Сейчас это решается вообще отсутствием возвращаемого типа и описанием в DocBlock.
Решить, конечно, можно.. но геморроя больше чем пользы.
В первом случае можно решить создав дофига типизированных коллекций, а в геттерах ещё и каждый раз в них оборачивать доктринские (она вставит в аттрибут всё-равно свою коллекцию) – уж не перебор ли?

Во втором можно разделить Like на CommentLike и PostLike, но я пока, видимо, не сталкивался с задачами, где лишние сущности имеют смысл – «общего лайка» было достаточно, да и код копипастить не нужно.


будет помогать писать некачественный код

А PHP и так сейчас помогает писать любой говнокод просто не используя типы. Но это ведь не значит что надо вдруг в обязательном порядке всех заставлять явно их указывать?
НЛО прилетело и опубликовало эту надпись здесь
Про первый спасибо, видимо, невнимательно прочитал и понял RFC неверно, печально, ждём.
Про второй – догдался сам когда уже написал сообщение. Наличие интерфейса типа `LikeableInterface` подойдёт в качестве нормального кода?

выходят фичи, которые помогают писать хороший код

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

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

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

Привет, еще из, надеюсь, кому-то полезного: мы делаем PHP-митапы в Самаре 28-го сентября и Ульяновске 19-го октября — приходите или приезжайте, если где-то рядом)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации