LINUX.ORG.RU

Сценарии использования алгебраических типов на практике

 ,


2

7

Для чего вы наиболее часто применяете алгебраические типы? С т.з. плюсовика, я в целом понимаю, что они делают, но не вижу, как бы их полезно применять. Допустим, если бы алгебраические типы были в С++ (полагаю что-то подобное можно на шаблонах сделать), для чего бы их использовать, с учётом того, что существует плюсовое ООП и если встроенное в язык ООП позволяет довольно лёгко решить задачу, то с помощью такого ООП и лучше её решать.

★★★★★

Последнее исправление: next_time (всего исправлений: 1)

Ответ на: комментарий от dvetutnev

для это существут программирование вообще

next_time ★★★★★
() автор топика

В языках с встроенным синтаксисом для ADT идут и операции над ними. В с и с++ примерным аналогом был бы безопасный switch на union type.

anonymous
()
Ответ на: комментарий от anonymous

безопасный switch на union type.

это динамика, а в динамике использовать свитч в 99% случаев - моветон, вместо него следует использовать наследование и виртуальные функции

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

next_time ★★★★★
() автор топика
Ответ на: комментарий от next_time

Я рассматриваю ADT и в том числе как данные. Виртуальные функции имеют один интерфейс для всех подтипов. Это не всегда удобно.

Динамика, которая позволяет получить информацию о точном типе это dynamic_cast.

В статике вообще не важно какой формат хранения данных. Но compile-time switch по типам бы не помешал :)

anonymous
()
Ответ на: комментарий от anonymous

Но compile-time switch по типам бы не помешал

if constexpr

anonymous
()

В плюсах раскрыть всю их прелесть не выйдет.

Deleted
()
Ответ на: комментарий от anonymous

Это не всегда удобно.

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

в плюсоподобном ООП (С++, жаба, шарп) пишется много подготовительного мусора типа общий предок, куча классов, виртуальные ф-ции в каждый, но зато в прикладном коде уже видно просто x->calc(y), условно говоря

Но compile-time switch по типам бы не помешал :)

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

next_time ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Динамика, которая позволяет получить информацию о точном типе это dynamic_cast.

за всю жизнь ни разу не видел, чтобы кто-то использовал dynamic_cast вне уродливых костылей

в теории, иногда эти костыли бывают к месту: могу понять, если человеку лень расписывать ради одной строчки ООП-стайл код, но на практике, такой ситуации, почему-то, никогда не встречается

next_time ★★★★★
() автор топика
Ответ на: комментарий от RazrFalcon

в сишке - c натяжкой, да, в плюсах - либо бросается исключение, либо вызывается сигнал (qt)

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

next_time ★★★★★
() автор топика
Ответ на: комментарий от RazrFalcon

да вот и странно что его никто не использует и что тогда этот тег вообще существует

next_time ★★★★★
() автор топика
Ответ на: комментарий от next_time

вызывается сигнал (qt)

Шта?

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

RazrFalcon ★★★★★
()
Ответ на: комментарий от RazrFalcon

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

да, именно для этого придумали исключения

next_time ★★★★★
() автор топика
Ответ на: комментарий от RazrFalcon

Шта?

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

next_time ★★★★★
() автор топика
Ответ на: комментарий от next_time

в плюсах - либо бросается исключение

У исключений есть свои минусы. По крайней мере у исключений в C++ (и питоне тоже, например). Первая проблема - невозможность жёстко ограничить типы исключений, выбрасываемых конкретной функцией. В стандарте одно время была определена возможность указывать спецификацию исключений, но это не прижилось и сейчас deprecated. Вторая проблема (частично является следствием первой) - невозможность во время компиляции проверить, что ты действительно обработал все возможные случаи в catch. Лично видел в суровом (а так же очень большом и очень давно развивающемся) проприетарном проекте, частично написанном на плюсах, кучу конструкций вида

try {
// Делаем что-то страшное и очень сложное
} catch (const my::super::mega::ExceptionHierarchyBase &exc) {
// Обрабатываем ошибки _нашего_ кода.
} catch (const std::exception &exc) {
// Кто-то забыл обернуть стандартные исключения в наш класс. Или в какой-то сторонней либе случилось что-то странное. Ну бывает, чо.
} catch (const other::lib::Exception &exc) {
// В другой сторонней либе случилась хрень, но мы такое уже видели и в принципе готовы.
} catch (...) {
// Вот такая хрень тоже иногда происходит.
}
Тот код должен был работать не смотря ни на что долго и без возможности обслуживания/обновления. Поэтому везде где только можно на всякий случай вручную проверялось всё что можно, а так же вообще всё по максимуму писалось в логи. Если бы была возможность на этапе компиляции проверять, что все возможные типы ошибок обработаны, то кучу кода можно было бы сократить или написать намного красивее (изолировав работу с «посторонними» исключениями в отдельных модулях, например).

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

Не хватает. Это одна из причин, по которой исключения появились. Вторая - желание объединить ошибки в иерархию.

ИМХО, Result<> в расте - шикарнейший способ обработки ошибок. С одной стороны, можно передавать кучу инфы об ошибке (как в исключениях). С другой стороны - компилятор не даст тебе ошибку случайно заигнорить. Ещё - не получится перепутать ошибку с валидным возвращаемым значением, что иногда бывает в C при невнимательном чтении манов.

Deleted
()
Ответ на: комментарий от RazrFalcon

в самом куте обработка ошибок на самом минимальном уровне

например, виджеты (в основном) вообще ошибки не обрабатывают, т.к. на логическом уровне они там невозможны

next_time ★★★★★
() автор топика
Ответ на: комментарий от Deleted

ИМХО, Result<> в расте - шикарнейший способ обработки ошибок. С одной стороны, можно передавать кучу инфы об ошибке (как в исключениях). С другой стороны - компилятор не даст тебе ошибку случайно заигнорить. Ещё - не получится перепутать ошибку с валидным возвращаемым значением, что иногда бывает в C при невнимательном чтении манов.

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

Да, я в курсе про error_chaining и подобные библиотеки.

hateyoufeel ★★★★★
()
Ответ на: комментарий от Deleted

Это одна из причин, по которой исключения появились.

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

next_time ★★★★★
() автор топика
Ответ на: комментарий от Deleted

class ProxyPattern : public ForeignClass
{
public:

  bool write_tolog =true;
  void read()
  { try
    {
      ForeignClass::read();
    }
    catch(ForeignE& e)
    {
       if(write_tolog) log(e.what());
       throw MyException(e); 
    }
  }
}

И далее, работаем только с классом MyException, с универсальной иерархией. Причём, работаем так:

ErrorContext context;
try{
context.set("bla-bla");
ProxyPattern proxy;
proxy.read();
}
catch(MyException e)
{
  e.set_context(context);
  e.log();
}

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

next_time ★★★★★
() автор топика
Ответ на: комментарий от next_time

Несколько замечаний:

1) прокси реализуется для всех используемых классов, всех библиотек, но во избежание чрезмерного гемороя, кроме, возможно, 1-3 наиболее часто используемых библиотек

2) прокси позволяет сделать для каждого класса экзепшена унифицированный запись в лог, так как каждый класс выдаёт ошибку в своём формате, превращание вывода какого-нибудь std::exception().what() в пригодную для записи в лог форму. Поэтому, прокси-класс нужен всё равно

3) прокси позволяет добавлять дополнительную логику и игнорировать неинтересные ошибки, такие как выхлоп экзепшенов из std::string().substr() например. Поэтому, прокси-класс нужен всё равно

4) позволяет обрабатывать ошибки в стиле С как исключения

5) позволяет анализировать выходные данные используемых библиотек и централизованно кидать ошибки

6) позволяет централизованно добавлять другой необходимый функционал, несвязанный с обработкой ошибок, если требуется

7) вписывает иерархию сторонних классов в иерархию классов приложения

8) решает проблему зоопарка стилей наименования классов и функций

9) позволяет решать прочие проблемы, для которых изначально придуман прокси-паттерн

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

next_time ★★★★★
() автор топика
Последнее исправление: next_time (всего исправлений: 1)
Ответ на: комментарий от next_time

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

Капитан, вы?

Deleted
()
Ответ на: комментарий от next_time

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

Deleted
()
Ответ на: комментарий от next_time

1) прокси реализуется для всех используемых классов, всех библиотек, но во избежание чрезмерного гемороя, кроме, возможно, 1-3 наиболее часто используемых библиотек

Осталось придумать где взять столько человеко*лет =). К тому же, проблему это никак не решает: у тебя всё равно не будет никаких проверок во время компиляции. И отсутствие достоверной информации о том, откуда какие исключения в принципе могут прилететь.

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

Ты предлагаешь писать код, который на 95% будет состоять из классов-обёрток. Не надо так делать. Такой подход приводит к:

  • куче новых багов;
  • сильному уменьшению читабельности;
  • и увеличению сложности отладки.
Deleted
()
Ответ на: комментарий от Deleted

Сценарии использования алгебраических типов на практике (комментарий)

так что прокси всё равно нужен

для _каждой_ функции ForeignClass

нет, так как не всякий метод кидает экзепшены, в контейнерах std::vector, std::forward_list и т.п. нужно буквально только один метод для каждого обернуть (если вообще заморачиваться)

То есть по факту увеличил количество бессмысленного кода на порядок.

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

next_time ★★★★★
() автор топика
Ответ на: комментарий от Deleted

И отсутствие достоверной информации о том, откуда какие исключения в принципе могут прилететь

из документации, или грепом по исходникам

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

next_time ★★★★★
() автор топика
Ответ на: комментарий от Deleted

состоять из классов-обёрток.

прокси-паттерн не просто так придумали

куче новых багов; сильному уменьшению читабельности; и увеличению сложности отладки.

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

а багов в таких проектах было как и везде

next_time ★★★★★
() автор топика
Ответ на: комментарий от Deleted

Осталось придумать где взять столько человеко*лет =).

пффф, 90%+ времени программиста уходит на продумывание кода, по статистике

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

next_time ★★★★★
() автор топика
Ответ на: комментарий от next_time

нет, так как не всякий метод кидает экзепшены, в контейнерах std::vector, std::forward_list и т.п. нужно буквально только один метод для каждого обернуть (если вообще заморачиваться)

Любой метод, не отмеченный как noexcept, будет выкидывать исключения, причём неожиданного для тебя типа.

в контейнерах std::vector, std::forward_list и т.п. нужно буквально только один метод для каждого обернуть (если вообще заморачиваться)

Речь не про просто использование std. Там всё просто - ровно один тип исключений. Сложность увеличивается уже когда ты используешь стороннюю либу, использующую std, и не всегда заворачивающую std::exception в свой собственный класс. Количество таких «наслоений» может увеличиваться произвольно.

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

Я просто недостаточно детально описал. Кучи кетчей в том коде были в разных «главных циклах» и подобных сущностях, где была возможность в случае возникновения непредвиденной херни отдельные компоненты пересоздать/переинициализировать/рестартануть/etc. (причём иногда вместе с соответствующей аппаратной частью). И вот конкретно в том случае твой подход сделал бы реально хуже. А растовский подход с Result<> уменьшил бы количество кода и дал бы гарантию, что в случае изменения типов ошибок в используемых либах, всё просто бы перестало компилироваться.

Deleted
()
Ответ на: комментарий от Deleted

К тому же, проблему это никак не решает: у тебя всё равно не будет никаких проверок во время компиляции.

Решает же, теперь вместо зоопарка исключений, есть список разнотипных исключений, включённых в единую иерархию. А там уже достаточно сделать

catch(ParentOfMyException e) //ловим объект корневого класса иерархии исключений

чтобы все возможные исключения обработать

next_time ★★★★★
() автор топика
Последнее исправление: next_time (всего исправлений: 1)
Ответ на: комментарий от next_time

Ох... Ещё раз: ты by design не знаешь что могут выкинуть либы, которые ты используешь, потому что они проприетарные и написаны индусами. Твой идеализированный подход работает только только если весь код подконтролен лично тебе, либо небольшой сплочённой команде, а все использованные либы открытые, плюс написаны качественно и хорошо документированы (не знаю примеров кроме libstdc++ и может быть буста). А если код писался десять лет до тебя и будет писаться ещё десятки лет после, да ещё и кучей разных людей (часть которых могут быть просто некомпетентны), раскиданных по всему миру, то всё становится очень печально. Да, можно брать отдельные компоненты и переписывать их нормально с нуля (это в общем то и делается периодически). Но со временем разработчики сменяют друг друга и даже изначально идеальная архитектура обрастает говном и всё опять становится плохо. И в плюсах мало встроенных механизмов, которые могли бы препятствовать таким процессам. Основная претензия именно к этому.

Deleted
()
Ответ на: комментарий от Deleted

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

Deleted
()
Ответ на: комментарий от Deleted

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

Эм, нет. Для шаблонов эта логика не работает. Шаблоны не объявляются noexcept, потому что аргумент шаблона может кидать произвольное исключения.

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

Я просто недостаточно детально описал. Кучи кетчей в том коде были в разных «главных циклах» и подобных сущностях, где была возможность в случае возникновения непредвиденной херни отдельные компоненты пересоздать/переинициализировать/рестартануть/etc. (причём иногда вместе с соответствующей аппаратной частью).

Да нет, я прекрасно понял, я такого хлама понавидался уже. Подход с прокси прекрасно позволяет пересоздать/рестартануть, что хочешь

next_time ★★★★★
() автор топика
Ответ на: комментарий от next_time

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

Вот! Вот это и есть самое плохое в плюсовых исключениях.

Deleted
()
Ответ на: комментарий от next_time

Эм, нет. Для шаблонов эта логика не работает. Шаблоны не объявляются noexcept, потому что аргумент шаблона может кидать произвольное исключения.

При чём тут вообще шаблоны?

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

Как ты не понимаешь, что перетаскивание catch (...) из одной части говнокода («главный цикл») в другую («прокси») исходную проблему (невозможность проверки во время компиляции) не решает вообще никак?

Deleted
()
Ответ на: комментарий от Deleted

Ох... Ещё раз: ты by design не знаешь что могут выкинуть либы, которые ты используешь, потому что они проприетарные и написаны индусами.

Ох, ещё раз: компилятор ВНЕЗАПНО, тоже. Более того, даже если вы прочитаете доки к своей индусской либе, и узнаете, что func кидает std::exception, то

try{
 func();
}
catch(std::exception& e)
{
 ...
}

Ничего не поймает, в общем случае. И алгебраические типы (если б и были) тоже не помогут.

если весь код подконтролен лично тебе, либо небольшой сплочённой команде, а все использованные либы открытые

а иначе исключения вообще нормально не отлавливаются, иначе в принципе кроме catch(...) вариантов нет.

и хорошо документированы

нет, нужна нормальная документация, как код без неё писать

А если код писался десять лет до тебя и будет писаться ещё десятки лет после, да ещё и кучей разных людей

если вы плохо разбираетесь в коде проекта, ничего годного вы не напишете

next_time ★★★★★
() автор топика
Ответ на: комментарий от Deleted

При чём тут вообще шаблоны?

самым прямым образом vector<T>::push_back() кидает исключения T, а не собственные

next_time ★★★★★
() автор топика
Ответ на: комментарий от Deleted

перетаскивание catch (...) из одной части говнокода («главный цикл») в другую («прокси») исходную проблему (невозможность проверки во время компиляции)

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

next_time ★★★★★
() автор топика
Ответ на: комментарий от next_time

И алгебраические типы (если б и были) тоже не помогут.

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

нет, нужна нормальная документация, как код без неё писать

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

если вы плохо разбираетесь в коде проекта, ничего годного вы не напишете

Как ты предлагаешь разбираться в коде сторонних плохо документированных либ, исходников которых у тебя нет?

В общем ты просто ни разу не видел такое дерьмо, которое видел я 8).

Deleted
()
Ответ на: комментарий от Deleted

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

Что-то похожее кстати можно изобразить на boost::variant и аналогах, но:

  • бойлерплейтного кода будет сильно больше, чем на ЯП, в которых подобный способ обработки ошибок является основным;
  • это будет иметь смысл, только если вообще весь код и все либы используют тот же подход (иначе придётся городить всё те же прокси);
  • всё равно останутся способы отстрелить себе ногу.
Deleted
()
Ответ на: комментарий от Deleted

Тут на железо то внутренняя документация соответствует действительности

вот это-то, как раз, нормально

next_time ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.