Совсем не обязательно пользоваться sbt. Я пока от него тоже отказался, т.к. он ещё сырой и в принципе не такой уж интуитивный. Зато для сборки scala-проектов вполне подходит gradle. В интернете давно описаны стандартные задачи для gradle, которые по умолчанию доступны в sbt, типа запуска scalatest или консоли в контексте проекта.
Ещё одно важное отличие — поддержка персистентности. Т.е. если мы используем данную структуру для реализации иммутабельной структуры данных, то при необходимости создать копию с одним измененным элементом, мы можем не копировать всю структуру, а скопировать только один листовой узел с 32 элементами, что значительно экономит память в функциональных языках.
Да много чем плохи. Начнем с того, что исключение и ошибка — это две разные ситуации со всеми вытекающими.
Ошибка может прерывать выполнение только текущего блока и являться штатной ситуацией в блоке выше по стеку. При этом нам совсем не нужно может быть получать получать стек вызовов и тратить на это ресурсы. Если забыть обработать нужным образом ошибку, то программа в идеале не должна компилироваться, а не падать в рантайме с возможным повреждением открытых ресурсов и т.д.
А исключение, опять же в идеале, никто перехватывать не должен. Если оно возникло, то нужно доработать код.
Проблема сейчас в C#, такая же как и в java — отсутствие краткого синтаксиса для работы в функциональном стиле. Поэтому получается достаточно ущербный код, типа приведенного в статье. Но сама идея верна и при правильной реализации увеличивает производительность, безопасность и читаемость кода.
Я к похожим выводам в статье для scala прихожу habrahabr.ru/post/262971, где пытаюсь сравнивать исключения и монады для обработки ошибок. Правда, боюсь, рассуждения там могут быть немного сумбурными.
По крайней мере в scala для вектора используется AMT, а не HAMT. А вообще несколько не хватает примеров использования подобных структур. Как удачных, так и неудачных (что не менее важно). Потому что, если смотреть формально на тот же вектор, то, допустим время модификации элемента O(1) (ну т.е. создание нового вектора на основе старого), но на практике ~32 раза медленнее чем в обычном массиве. И хотя иммутабельность хорошо, но при частых модификациях врядли удасться себе такое позволить.
Если у кого-то не отображаются какие-то символы юникода, то пишите в ЛС или на e-mail из профиля. А я пока по возможности буду заменять на картинки. Выяснилось, что у некоторых пользователей с этим проблемы.
Ну в принципе да. Сама идея явного описания ошибок в заголовке мне нравится. Но с текущей реализацией в Java работать просто невозможно, т.к. на 10 строк бизнесс-логики будет 50 строк обработки исключений.
Есть ряд причин, по которым из двух зол (исключения и монады), я бы всегда выбирал монады.
При работе с исключениями в текущем их виде мне не нравятся следующие вещи:
— по заголовку функции может быть не понятно, что функция кидает исключение;
— если ты его не отловил, то компилятор ничего не скажет;
— если ты его не отловил, то его действие распространяется на вышестоящие функции и может привести к некорректному закрытия каких-то ресурсов и повреждению данных;
— если же ты его всё-таки отлавливаешь, то часто тонешь в избыточном синтаксисе try...catch (да и throw new многовато для такой фундаментальной операции, как возврат ошибки)
Монады частично решают эти проблемы:
— из заголовка явно видно, что функция может завершиться с ошибкой;
— есть оператор for, который позволяет устранить избыточность синтаксиса;
— если вышестоящая функция явно не рассчитана на работу с ошибками нижестоящей функции, то это вызовет ошибку компиляции в большинстве случаев;
— действие монад всегда локально и не затрагивает вышестоящие функции;
Ясно. Я тоже по началу пытался рекурсию для циклов использовать. Проблема в том, что порой список параметров раздувает на пару строк, что совсем не есть удобно. Кроме того, надо понимать, что @tailcall рекурсия разворачивается в обычный while. Т.е. большинство проблем while вылезут также и в этой рекурсии.
Кстати, на счет goto. Это как говорить, что атомная бомба абсолютное зло. Но на деле она зло, только когда взрывается рядом с тобой. Я это к тому, что ряд сценариев использования goto действительно приводит к спагетиобразному коду, в котором не разберешься, но такие частные случаи как break и continue вряд ли можно в этом обвинить. И есть ряд вполне безопасных сценариев для их использования.
По идее так сделать можно. Здесь скорее вопрос идеологии, состоящий в том, что изменяемые данные за пределами какой-то очень локальной области кода сами по себе уже являются злом. Затрудняют отладку, увеличивают вероятность ошибки. Здесь же предлагается передавать их через всю программу и модифицировать во многих местах, что является, так сказать, дурным тоном. Ну и кроме того, не совсем красиво выглядит тот факт, что поля, которые уже были в исключении и отвечали за обработку сообщения, так никуда и не делись. И теперь у нас есть несколько полей, отвечающих за одно и тоже, но часть из них ничего не делает.
А как вы справляетесь с циклами? Для каждого цикла создаете рекурсивную функцию с @tailcall оптимизацией?
>Если хочется собирать кучу ошибок, то есть стейт монады. Как-то так.
Можете привести какой-нибудь пример использования? Я о них мало знаю.
>Исключения в бизнес-логике — дурной тон, особенно в ФП. Вы таким образом поток выполнения прерываете ненормальным путем.
Я с этим согласен. Поэтому у себя их использую локально и ограниченно, только для транспортных целей. В этом случае обычно видно, что ничего критического не прерывается.
В том и суть, что для упрощения надо сделать метод, и даже не один. Кроме того в Res к верхнему уровню мы имеем одно исключение чисто для стека вызовов и список сообщений об ошибках, которые могут задаваться не только строками, но и произвольными типами. При использовании Try к верхнему уровню мы имеем в лучшем случае список исключений каждое со своим стеком или одно с нерелевантным стеком.
Ну и на счет того, что всё кидается в контексте. А что будет если случайно контекст забыть. В случае с Try во многих случаях компилятор ничего не скажет, в случае с Res это будет явная ошибка компиляции. Поэтому я считаю важным отделить работу с ошибками, которые можно обработать, от исключений, которые никто, по хорошему, ловить не должен.
Ну опять же: указать своё сообщение прямо на месте нельзя; необходимо создавать целое новое исключение, что во-вервых длиннее, во-вторых без доп. телодвижений теряется часть стека вызовов, а в-третьих требует больше памяти и процессорного времени. Кроме того, работа с исключениями всегда характерна тем, что их можно пропустить там, где не ожидаешь.
Это не совсем верно будет. В данном примере сам класс F1 может хранить любые значения и в других частях программы они допустимы. Но вот в этой функции преобразования есть свои локальные ограничения.
Ошибка может прерывать выполнение только текущего блока и являться штатной ситуацией в блоке выше по стеку. При этом нам совсем не нужно может быть получать получать стек вызовов и тратить на это ресурсы. Если забыть обработать нужным образом ошибку, то программа в идеале не должна компилироваться, а не падать в рантайме с возможным повреждением открытых ресурсов и т.д.
А исключение, опять же в идеале, никто перехватывать не должен. Если оно возникло, то нужно доработать код.
Проблема сейчас в C#, такая же как и в java — отсутствие краткого синтаксиса для работы в функциональном стиле. Поэтому получается достаточно ущербный код, типа приведенного в статье. Но сама идея верна и при правильной реализации увеличивает производительность, безопасность и читаемость кода.
Я к похожим выводам в статье для scala прихожу habrahabr.ru/post/262971, где пытаюсь сравнивать исключения и монады для обработки ошибок. Правда, боюсь, рассуждения там могут быть немного сумбурными.
При работе с исключениями в текущем их виде мне не нравятся следующие вещи:
— по заголовку функции может быть не понятно, что функция кидает исключение;
— если ты его не отловил, то компилятор ничего не скажет;
— если ты его не отловил, то его действие распространяется на вышестоящие функции и может привести к некорректному закрытия каких-то ресурсов и повреждению данных;
— если же ты его всё-таки отлавливаешь, то часто тонешь в избыточном синтаксисе try...catch (да и throw new многовато для такой фундаментальной операции, как возврат ошибки)
Монады частично решают эти проблемы:
— из заголовка явно видно, что функция может завершиться с ошибкой;
— есть оператор for, который позволяет устранить избыточность синтаксиса;
— если вышестоящая функция явно не рассчитана на работу с ошибками нижестоящей функции, то это вызовет ошибку компиляции в большинстве случаев;
— действие монад всегда локально и не затрагивает вышестоящие функции;
habrahabr.ru/post/262971
Кстати, на счет goto. Это как говорить, что атомная бомба абсолютное зло. Но на деле она зло, только когда взрывается рядом с тобой. Я это к тому, что ряд сценариев использования goto действительно приводит к спагетиобразному коду, в котором не разберешься, но такие частные случаи как break и continue вряд ли можно в этом обвинить. И есть ряд вполне безопасных сценариев для их использования.
>Если хочется собирать кучу ошибок, то есть стейт монады. Как-то так.
Можете привести какой-нибудь пример использования? Я о них мало знаю.
>Исключения в бизнес-логике — дурной тон, особенно в ФП. Вы таким образом поток выполнения прерываете ненормальным путем.
Я с этим согласен. Поэтому у себя их использую локально и ограниченно, только для транспортных целей. В этом случае обычно видно, что ничего критического не прерывается.
Ну и на счет того, что всё кидается в контексте. А что будет если случайно контекст забыть. В случае с Try во многих случаях компилятор ничего не скажет, в случае с Res это будет явная ошибка компиляции. Поэтому я считаю важным отделить работу с ошибками, которые можно обработать, от исключений, которые никто, по хорошему, ловить не должен.