Pull to refresh

Comments 48

А как решается вопрос с деконструкцией enum'а? Например, если я сделал


match token {
   case Identifier(public string $identifier) => { identifier='foo'}
}

(простите за смесь rust и php), то что в token? Он поменялся или нет? У Rust'а за счёт понимания ownership точно понятно на этапе компиляции что произошло с декоструированным enum'ом (был он деконструирован или кусочек его был mutably borrowed).


Мне кажется, пытаться принести в другие языки 'match как в Rust' без 'ownership как в Rust' чревато феерическими багами и wtf'ами.

Ну наверно там где-нибудь добавят амперсанд, чтобы показать, что значение передалось по ссылке. Не знаю, посмотрим

В RFC предлагается копия, не ссылка.
Мне кажется match будет работать без проблем — gc в наличии и модель памяти не такая кривая как в JS

Если копия, то будет проблема с обновлением части enum'а.


Вот, посмотрите на Rust:


match foo{
   Some(ref mut x) => {*x=3;},
   _ => {}
}

Если будет copy, то оригинальный foo поменять нельзя будет, половина пользы rust'а пропадёт.

Вы подаете ручную обработку ошибок как плюс, но это не плюс, а минус языков Го и Раст. Из-за этого приходится писать больше рутинного кода. Если у нас 10 функций вызывают друг друга, то нам в каждой приходится писать if с проверкой, а не вернула ли функция ошибку. Ведь в большинстве случаев эти ошибки никак особенно не обрабатываются, а просто показывается страница с сообщением об ошибке.


С исключениями код становится гораздо проще и линейней. Сравните:


// Без исключений, с монадами, в стиле Го и Раст
$a = f1();
if (!$a) {
    return false;
}

$b = f2($a);
if (!$b) {
    return false;
}

... и так далее

// С исключениями, в стиле PHP 
return f4(f3(f2(f1())));

В редких случаях, когда требуется как-то особенно обрабатывать ошибки, описываемая в статье фича может пригодиться. Но в 90% случаев гораздо лучше использовать обычное исключение, которое вылетает наверх завершает программу и показывает страницу с сообщением об ошибке. Хотя сами алгебраические типы вещь хорошая, бежать и заменять ими исключения — верх глупости.


А вы в статье почему-то описываете исключения как что-то плохое, а ручную обработку ошибок как что-то хорошее, хотя это не так. Исключения как раз появились как решение проблемы написания рутинного кода обработки ошибок. В PHP они есть и этим он лучше, чем Го или Раст.

Я ничего не подаю, как хорошо или плохо, я описал как сделано в этих языках. Подправлю текст, чтобы это было более явно.

По поводу вашего примера, в Rust есть синтаксический сахар для этого.

Можно поставить знак? после вызова функции, и это означает следующее: если вернулась ошибка, то автоматически сделай return, включающий эту ошибку.

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

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

Вы всё перепутали, это совершенно не стиль Rust. В расте такой пример будет выглядеть примерно так:


// Алиас для типа, чтобы пример покороче был
type MyResult<T> = Result<T, Box<dyn std::error::Error>>;

fn f1() -> MyResult<i32> { Ok(2 + 2) }
fn f2(a: i32) -> MyResult<i32> { Ok(a * 2) }
fn f3(a: i32) -> MyResult<i32> { Ok(a * 5) }

fn main() -> MyResult<()> {
    // Без исключений, в стиле Rust и линейно
    let result = f3(f2(f1()?)?)?;

    println!("{}", result);
    Ok(())
}
 let result = f3(f2(f1()?)?)?;
Может я чего-то не понимаю, но мне всегда казалось, что try-catch — это устоявшийся синтаксический сахар для примерно того-же самого, только в таких языках такой порядок исполнения — это дефолтное поведение, а отлов исключения — отдельная конструкция, а не наоборот.
нет, растовый сахар работает в пределах одной функции. У которой должен явно прописан тип возврата, включающий наличие ошибки. Т.е. всё явно, но при этом не перегружено ифами

Важное отличие исключений в том, что они могут возникнуть абсолютно в любом месте кода неожиданно для программиста. А чтобы в языках вроде Rust функция могла выдать ошибку, она должна явно объявить это в своей сигнатуре. Обработку исключения можно забыть и не написать try-catch, а обработку ошибки в Rust забыть нельзя (программа не скомпилируется из-за несовпадения типов).


Касательно try-catch — ключевое слово try в Rust нынче зарезервированное, может в будущем когда-нибудь и появится что-то более похожее на привычный try-catch. Но сути это не изменит — обработка ошибок всё равно ручная (хоть и с сахаром) и её невозможно забыть, ибо не скомпилируется. Я считаю это важным плюсом перед исключениями.


(Впрочем, функция всё ещё может вызвать panic и обрушить программу, но это не является нормальным или ожидаемым ходом работы)

Просто посмотрите разницу:


let result = f3(f2(f1()?))?;

и


let result = f3(f2(f1()?)?)?;

Например, в первом варианте видно что функция f2 всегда возвращает результат и никогда — ошибку, во втором — каждая функция может вернуть ошибку.


А с исключениями они выглядят одинаково:


let result = f3(f2(f1()));

По коду не сказать, может быть ошибка или нет, если не заглядывать рекурсивно в кажую функцию

А с исключениями они выглядят одинаково:
Как по мне, это скорее плюс.

Когда у вас внезапно в продакшне код начал падать из-за того что функция кторая раньше всегда возвращала ОК иногда начала падать vs получить во время компиляции ошибку про то же амое — это плюс? Ну, как скажете.

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


И тут с исключениями начинаются пляски с бубном. Спасибо, не надо.

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


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

В теории все хорошо, на практике даже парсинг строки в число является исключением в почти всех языках с этими самыми исключениями, хотя на самом деле это настолько часто не требует выбрасывать исключения, что в том же шарпе каждую такую функцию продублировали TryParse которая возвращает bool'ом удалось спарсить или нет — это ещё более "деревенский" подход, чем АДТ.


Наконец можно вспомнить про такие прекрасные вещи как StopIteration exception который просто сигнализирует что цикл стоит остановить. Невыполнение условия продолжения цикла — не такая уж исключительная ситуация, согласитесь?

На практике проблем тоже нет. Если строка с числом приходит из ненадёжного источника, например, ввод пользователя — то очевидно нужно проверять корректность информации, Try, или монады. Если же строка с числом приходит из источника, который должен быть корректным по определению (по контракту АПИ, например), то мы используем выброс исключение, так как нарушение контракта — исключение.


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


StopIteration exception выглядят как неправильное применение исключений.


Я боюсь, что проблема исключений вовсе не в том, что они плохой инструмент. А в том, что люди не научились применять их правильно. Что делают в таком случае инженеры? Пытаются оградить разработчиков от выстрела в ногу. Бог с вами, не умеете работать с исключениями, тогда возитесь с монадами. И 50 тыс. статей какие монады хорошие, а исключения плохие.


Классика :)

На практике проблем тоже нет. Если строка с числом приходит из ненадёжного источника, например, ввод пользователя — то очевидно нужно проверять корректность информации, Try, или монады. Если же строка с числом приходит из источника, который должен быть корректным по определению (по контракту АПИ, например), то мы используем выброс исключение, так как нарушение контракта — исключение.

Библиотечная функция парсинга числа в строку вашего любимого языка к сожалению не понимает, из какого источника ей пришла строка. Скажу больше того, это не её зона ответственности: SRP, всед ела. Она должна просто распарсить строку или выдать результат что парсинг не получился и по какой причине. Сделать это в виде Either куда логичнее, чем бросать эксепшн. Хотя бы потому, что при желании эксепшн я могу легко получить вызовом одной функции типа myParseResult.unwrap(), а вот завернуть эксепшн в монаду — это трайкетчи, штрафы по перфомансу и так далее.


Я боюсь, что проблема исключений вовсе не в том, что они плохой инструмент. А в том, что люди не научились применять их правильно.

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

UFO just landed and posted this here
Библиотечная функция парсинга числа в строку вашего любимого языка к сожалению не понимает, из какого источника ей пришла строка. Скажу больше того, это не её зона ответственности: SRP, всед ела. Она должна просто распарсить строку или выдать результат что парсинг не получился и по какой причине. Сделать это в виде Either куда логичнее, чем бросать эксепшн. Хотя бы потому, что при желании эксепшн я могу легко получить вызовом одной функции типа myParseResult.unwrap(), а вот завернуть эксепшн в монаду — это трайкетчи, штрафы по перфомансу и так далее.

Забавно, что всё сказанное вами про исключения — верно, и у меня нет противоречий. Однако это только одна сторона медали. В средненьком бизнесовом приложений тысячи различных методов, слоёв. А используется библиотечных методов и того больше. Проверки или unwrap на каждом шагу, это головняк. В то время, как эксепшен вываливается наверх, где красиво пишется в лог, указывая модуль, файл, и строку кода где произошла беда. И в большинстве случаев такие ошибки являются либо логическими, либо системными, т.е. лечить толстым слоем проверок это удорожание разработки на порядки.


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


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

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


Любой инструмент можно использовать неправильно. Единственный путь обезопасить людей от ошибок, это вообще запретить им что-то делать :)

Мне кажется, это не нужно. То же самое можно сделать уже имеющимися инструментами. Можно возвращать объект, например какой-нибудь Payload — что-то вроде DTO для параметров.
То же самое можно сделать уже имеющимися инструментами.
В каком-то сферическом вакууме это и на массивах можно было сделать. Вопрос тут про типо-безопасность на уровне языка.
Хотя я согласен, что натягивать сюда энумы — это перебор, Named Union Types был бы намного проще (простые Union Types там уже есть).
я исхожу из того, что можно сделать класс, комплексную структуру, где можно хранить что-угодно, ошибки, возвращаемые данные, для функции и метода сделать type hinting для возвращаемого значения в виде этого класса.

иногда так и делаю, особенно когда нужно вернуть что-то сложное из функции, в Golang это реализовано нативно и удобнее. Это особенно удобно при работе с внешними АПИ, там идея с Payload-ами ложится хорошо

На практике (без относительно ЯП), переход от исключений к монаде на реальном проекте вместо профита принёс головной боли, количество кода выросло, читабельность кода сильно снизилась. При этом пришлось вводить дополнительные регламенты к написанию кода.


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

(без относительно ЯП)

Вот как раз ЯП очень важен, ибо синтаксический сахар.


читабельность кода сильно снизилась

Ну вот я выше в комментах пример написал:


let result = f3(f2(f1()?)?)?;

Или можно его на несколько строчек разбить:


let a = f1()?;
let b = f2(a)?;
let result = f3(b)?;

Считаете, такая обработка ошибок не читабельна?

По-моему это одно и тоже. Результат функции f1() кладётся в стек, а вы во втором случае выделили это явно, в локальную переменную. Ничего не поменялось, только букв больше стало.

Ну так упомянутая вами читабельность кода какая для вас тут?

Я просто не совсем понимаю как это связано с исключениями :) null-coalescing прекрасный оператор, переоценить его сложно.


ИМХО читабельность разбитого на нескольких строчках кода — зачастую лучше. Но это на мой субъективный взгляд. Чем старше становлюсь, тем меньше мне хочется экономить на буквах. А когда был молод, считал высшим пилотажем запихнуть в одну строку как можно больше логики :)

А к null-coalescing это не имеет отношения, в этом примере показана ручная обработка ошибок через эти самые монады (точнее, их местном аналоге в Rust).


Ну так как вам такая читабельность кода обработки ошибок без использования исключений?

А где здесь обработка ошибок? Я её не наблюдаю. Какие ошибки тут обрабатываются? Какая логика обработки?


Насчёт исключений я вот что скажу. Данный код на любом шаге может выбросить исключение и оно 100% будет обработано. При чём при обработке ошибки на самом верхнем уровне я увижу: саму ошибку, файл и строка кода, где эта ошибка произошла. Более того я могу расширить эту обработку не вмешиваясь в программный код модулей.


Это практично. Это удобно. Это приносит пользу всю мою практику.

Какая логика обработки?

Если вызванная функция возвратила ошибку, то пробросить эту ошибку выше и завершить выполнение текущей функции. Достаточная обработка для 99% ситуаций, а дальше вышестоящий код пусть сам разбирается.


оно 100% будет обработано.

Это относится и к монадам — код, в котором не будет обработки, тупо не скомпилируется из-за несоответствия типов. Компилятор гарантирует, что любая ошибка всегда будет обработана.


А вот в случае с исключениями вы как раз можете забыть обработку: случайно не написали в нужном месте try/catch — и ошибка пошла выше по стеку, разрушая всю программу в хлам. С монадами такое невозможно, ибо не скомпилируется.


При чём при обработке ошибки на самом верхнем уровне я увижу: саму ошибку, файл и строка кода, где эта ошибка произошла.

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


Более того я могу расширить эту обработку не вмешиваясь в программный код модулей.

Вам с монадами кто-то мешает это делать?


Это практично. Это удобно. Это приносит пользу всю мою практику.

Всё то же самое можно сказать и про монады.




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

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

Монады хороши когда у вас в языке есть сахар в виде do чтобы ими удобно пользоваться. А когда его нет — то нет.


Я вот монадки очень люблю, но в шарпе ими не пользуюсь. Почему? Потому что писать лапшу из Map/MapErr желания ноль. LINQ не сильно спасает ситуацию т.к. стейтменты в нем использовать нельзя. Можно было бы авейты попробовать взять, но тогда возникает проблема трансформеров типа Task<Either<Error, Result>> — это уже не выразить по-человечески никак.

PHP усложняется. Сложно представить что будет в итоге с динамическим языком имеющим под капотом возможности уровня Явы или Раста…

Я считаю если усложнять язык, то следует так же переводить его к строгой типизации.

Мне уже сложно определить как среагирует то или иное условие, учитывая что в разных версиях оно меняется. Приходится плодить скобки чтоб наверняка все работало как задумывалось. А сколько ещё будет неявным мест?!

ИМХО конечно…
какие скобки? Что вы имеете в виду?
Тернарные операторы. Порядок выполнения отличается от принятых в других языках.
Сложно запомнить и чтоб все гарантированно работало ставлю скобки.
В какой-то из версий предлагали привести к общепринятому порядку. Тут опять же свою предохранительную роль сыграют скобки. Безопаснее будет мигрировать между версиями.

И что?
Что типизацию не вводят?
Хочешь пишешь тип, хочешь не пишешь.
Если ты создаешь здоровенный проект и не используешь определенные типы, то это не PHP виноват.
Анекдот: "-Ма-ам, а почему все дети едят сладкую вату, а я обычную?.. -Не знаю, сын. Не знаю!"
Тоже самое и с PHP, кто мешает писать типы?

Есть легаси и пакеты написанные не мной.

Я несколько раз (2 раза) пытался внести на обсуждение в PHP,. Сейчас <?= //Комментарий?> вызывает ошибку. Хотел чтобы не вызывало ошибку. Но из-за незнания Английского, я не смог даже с переводчиком внести на обсуждение эту возможность.
Может если Вам это нравится Вы поможете на голосование это отправить?
Ребят поддержите пожалуйста.

Простите, а почему это не должно вызывать ошибку?

<?= подразумевает, что будет какое-то значение для вывода. У вас же пустота.

Это всё равно, что вместо print $x написать просто print //комментарий

предлагается также включить его в PHP 8.1.

Это пока черновик, то есть он даже не обсуждался. Думаю в 8.1 маловероятно.

там внизу написано
Proposed PHP Version(s)
php 8.1

Это понятно, просто черновиков много https://wiki.php.net/rfc#in_draft. Этот RFC официально еще не предложили. Вот когда станет "under discussion", тогда значит рассматривается.


Ну то есть не стоит делать какие-то выводы, когда документ в статусе черновика. И даже обсуждать его немного некорректно.


Вот кусок моей переписки с одним из участников core-команды:


The "Request for comments" part of an RFC only applies once it has
actually been published. While it is in draft, and the author is still
working on it, it's usually trivially easy to shit on the RFC and
point out all the problems with it, which is hugely discouraging to
RFC authors.
я ж так и написал в статье, что этот RFC предлагается к включению 8.1. Т.е. никто не обещал, что оно будет и всё такое.

В целом на что я ориентировался:

1) Этот черновик является частью wiki.php.net/rfc/adts. Другую его часть (enumerations) уже приняли на голосовании.

2) Версия этого черновика 1.0. А не 0.xxx, как другие. Т.е. он всё же не совсем сырой.

3) В своей статье я никак не обсирал этот RFC, а даже скорее наоборот. Я не знал (это нигде не написано), что обсуждать черновики некорректно. Они же опубликованы в публичном пространстве.

Могу дописать в начале статьи жирным «Это только черновик, который еще не был официально предложен»

Думаю, предупреждение добавить было бы хорошо. За статьи по инамам и эту спасибо!

Идея крутая. Но она не укладывается в парадигму Типизации.
Ваши идеи, аналогичны статическим методам.
class Item{
public static function Method($number){return $number + 123;}
}

Теперь мы в коде можем сравнивать в условии на ложность.
if(Item::Methos(111) == 234){
...
}

В коде мы видим аналогичное использование как у Вас в описании. Но тут так же нет типизации как и у Вас.
Основная концепция идеи Enums это типизация группы из простых значений.
Как Ваше предложение типизирует?
Sign up to leave a comment.

Articles