Комментарии 361
Это все звучит очень круто. Но вам не кажется, что за стремлением к высокому искусству несколько теряется смысл программирования?
Когда-то изобрели процедурный подход. Жить стало круче, стали плодить процедуры из всего, чего можно, где можно было обойтись прямым кодом: вместо a = a + 2 стали писать Add(a, 2). Что улучшает читаемость и поддерживаемость кода.
Потом стали работать с ООП, и все завертелось ещё круче — все должно быть объектами! Теперь мы можем писать a.Add(2), что ещё лучше улучшает читаемость и поддерживаемость!
Но это мало, далее пошла мода на интерфейсы. Стоит ведь предусмотреть, что 2 — это не совсем два, а добавить — это не всегда сложить. Реализуем интерфейс IAdd! Что улучшит читаемость и поддерживаемость кода, само собой, а также сделает его ну ОЧЕНЬ гибким.
Тут уже наворотили столько, что чтобы не изобретать велосипед, улучшить читаемость и поддерживаемость, надо воспользоваться соответствующим паттерном — желательно с древнеримским названием типа MVXXML. Каждому интерфейсу по контроллеру, каждому контроллеру по интерфейсу! Теперь код мало того, что необходимо поддерживать — это ещё должна делать толпа джуниоров, пара сениоров и главный архитектор.
Но это мало. Это, знаете ли, не Bleeding Edge! Заверните, пожалуйста, в функциональное программирование, нарежьте монадами по 100гр кусочек и подайте под мелко нашинкованными лямбдами. Выглядит вкусно? Что, добавить 2 к переменной a? Это прошлый век! Как вы можете оскорблять высокую кухню самой постановкой такой задачи!
Идите к этим, как их, низкоуровневым! Пусть выдадут вам
add ax, 2! А у нас — искусство!
ООП не о том, что мы прибавляем 2, а о том, зачем мы это делаем. (Поэтому никакого .add там не будет).
Ну вот вы шутите, а многие за чистую монету такое принимают. И даже так пишут :-)
Для математики ООП вообще не нужно. ООП удобно для моделирования предметной области на языке этой самой предметной области. ФП удобно для реализации алгоритмов и всяких цепочек взаимодействий-преобразований. Никто не запрещает совмещать :-)
Или еще можно сказать, что инструментами ООП удобно описывать "что надо сделать", а инструментами ФП — "как мы это делаем".
Вообще-то строго наоборот: алгоритмы на чистых функциональных языках описываются плохо, а потому средствами ФП удобнее описывать что надо сделать.
А вот "как" описывается, в основном, структурным программированием, которое не относится ни к ООП, ни к ФП.
Да, поясню по поводу ООП. ООП может использоваться для написания как императивного кода — в таком случае его скрещивают со структурным программированием, так и для написания декларативного — в таком случае к нему часто добавляют элементы функционального. Проще всего показать это на примере языка C#.
Императивный подход:
IEnumerable<int> SomeMethod(IEnumerable<Foo> foos)
{
foreach (var foo in foos)
{
if (foo.x > 42)
{
foreach (var bar in foo.Bars)
{
yield return foo.y + bar.z;
}
}
}
}
Декларативный подход:
IEnumerable<int> SomeMethod(IEnumerable<Foo> foos) =>
from foo in foos
where foo.x > 42
from bar in foo.Bars
select foo.y + bar.z;
Эквивалентный код:
IEnumerable<int> SomeMethod(IEnumerable<Foo> foos) => foos
.Where(foo => foo.x > 42)
.SelectMany(foo => foo.Bars.Select(bar => foo.y + bar.z));
В первом случае используется структурное программирование, во втором случае угадывается монада List. Но оба подхода активно используют ООП и его паттерны (первый код неявно использует паттерн "итератор", второй к нему добавляет "цепочку ответственности").
А, ну это смотря что считать ООП.
Я вот не считаю, что если я написал class ImmutableCollection<T>
с методами map и filter, то это ООП. :-)
Если это простой класс не привязанный ни к какой иерархии — то тут конечно же от ООП будет только обертка. А вот если ваш ImmutableCollection реализует хотя бы интерфейс IEnumerable (C#) или Iterable (Java) — то это уже типичный механизм ООП.
С того, что тут задействуются два кита ООП — наследование и полиморфизм (ну, второе еще не задействуется — но я все же предполагаю что написанный код кто-то использует).
Кроме того, тут используется паттерн из мира ООП ("итератор"), который запрещен в мире функционального программирования, поскольку построен вокруг изменяемого состояния — так что как только вы его реализовали, ваш код перестает быть чистым функциональным.
Это как вы определили, что там используется итератор? Из кода это никак не следует.
> С того, что тут задействуются два кита ООП — наследование и полиморфизм
Интерфейсы не наследуются, а реализуются. И если интерфейс в стиле тайпклассов хаскеля, не предполагает полиморфизма подтипов (по крайней мере вне костылей из existential types)? Будет уже не ООП?
Это как вы определили, что там используется итератор? Из кода это никак не следует.
Следует. Интерфейсы IEnumerable и Iterable — это стандартные реализации паттерна "итератор".
Интерфейсы не наследуются, а реализуются.
Это всего лишь особенность конкретного языка.
И если интерфейс в стиле тайпклассов хаскеля, не предполагает полиморфизма подтипов (по крайней мере вне костылей из existential types)? Будет уже не ООП?
Мы все еще говорим о C# и Java или обсуждаем какой-то неопределенный язык программирования?
Разумеется, в том же Хаскеле классы не являются признаком ООП по построению. Но Хаскель — чистый функциональный язык, а я говорю про мультипарадигменный C#.
То есть если я поменяю IEnumerable на List, код чудесным образом перестанет быть ООП?
> Это всего лишь особенность конкретного языка.
Это особенность интерфейсов. Интерфейсы нельзя наследовать, потому что интерфейс — это контракт. Что значит «наследовать контракт»?
> Мы все еще говорим о C# и Java или обсуждаем какой-то неопределенный язык программирования?
Какая разница, на каком? У вас что, _один и тот же_ код то функциональный, то ООП в зависимости от того, какой язык?
В хаскеле нету классов
Это особенность интерфейсов. Интерфейсы нельзя наследовать, потому что интерфейс — это контракт. Что значит «наследовать контракт»?
Когда придумывались классические "три кита" ООП, интерфейсов/контрактов не было, отсюда и некоторое несоответствие в определениях. Технически интерфейс мало отличается от абстрактного класса без полей.
Применительно к интерфейсам в C#, при реализации интерфейса в некотором смысле наследуются все методы-расширения, определенные для этого интерфейса. Например, любой класс, реализующий IEnumerable<T>
, автоматически получает методы-расширения Where, Select, SelectMany, Join и прочие.
То есть если я поменяю IEnumerable на List, код чудесным образом перестанет быть ООП?
Мои ответы надо рассматривать в контексте тех комментариев, которые я писал ранее и тех вопросов, на которые я отвечал.
Напомню, началась эта ветка с моего ответа на вот этот комментарий:
Я вот не считаю, что если я написал class ImmutableCollection<T>
с методами map и filter, то это ООП.
Если вы считаете что слово class
автоматически означает ООП — то лучше расскажите об этом symbix, а не мне. Если вы так не считаете — то я не понимаю какой тезис вы пытаетесь отстоять в этом споре.
И? Это вы к чему? Как это меняет тот факт, что интерфейс — это контракт?
> Если вы так не считаете — то я не понимаю какой тезис вы пытаетесь отстоять в этом споре.
Мой тезис — интерфейсы не имеют никакого отношения к ООП. Они вообще не привязаны к какой-либо парадигме и существуют хоть в ООП, хоть в ФП, хоть в процедурном программировании (интерфейс модуля, например).
Вы опять путаете общее понятие и элемент языка.
Интерфейс как элемент языка очень похож на абстрактный класс без полей.
Давайте по порядку. Есть интерфейсы, интерфейсы — это контракты (абстрактные классы, конечно же, тоже). Идея накладывать контракты на программные сущности — не является чем-то специфичным для ООП. Это моя точка зрения. Можете свою полностью сформулировать, потому что мне совершенно непонятно, к чему вы ведете.
Я видел программы на C#, в которых все методы были статические и находились в классе Program, а остальные классы были без конструкторов и с публичными полями. Это было в чистом виде структурное программирование на C#.
Но если не брать такие крайние случаи — то да, почти любая программа на C# использует парадигму ООП. В этом нет ничего удивительного — все же это основная парадигма языка.
Но ведь _класс_, да еще и статический — это со всей очевидностью ООП-шная конструкция. Как и методы. Ровно в той же степени, как интерфейсы или абстрактные классы. Разве из этого не следует сразу сделать вывод, что это код в ООП-стиле? И не-ООП код, получается, на c# вообще не написать?
Когда мы приводим объект к типу `IEnumerable`, например, `void Foo(IEnumerable obj)`, то интерфейс неотличим от абстрактного класса.
Когда же мы накладываем ограничение на тип, например `void Foo(T obj) where T: IEnumerable`, интерфейс является контрактом.
Всё остальное типа особенности множественного наследования здесь не имеет значения.
Что значит «наследовать контракт»?
Можно сделать более специфичный контракт который унаследует поддерживаемые операции у другого контракта и добавит свои.
Кстати, каждому классу концептуально соотвествует свой наиболее специфичный контракт. Типа "Поток-в-памяти это такой поток вообще, который обязуется еще и хранить то, что в него запихали в ОЗУ и предоставлять потом по требованию"
Костыли — в качестве эмуляции подтипирования. Потому что подтипирование в хаскеле считается ненужным.
> Что вы называете подтипами?
Не совсем понимаю вопроса. То же, что и все? Есть системы типов с подтипированием (например, lambda<:), у них есть семантика.
data IShow = forall a. (Show a) => IShow a
тогда любой инстанс IShow ведет себя так же, как в ООП ведет себя класс, реализующий интерфейс IShow. Отличие только в наличии обертки, но на самом деле чисто с формальной точки зрения запись data IShow = forall a. (Show a) => a тоже валидна, просто в хаскеле так нельзя (вроде бы нельзя, по крайней мере, без каких-то хитрых расширений).
Любой инстанс Show, конечно же.
Но ведь сабтайпинг именно так и работает: «перед нами объект, про который мы можем сказать, что мы с ним можем делать все, что и с объектом, подтипом которого он является». В данном случае любой инстанс Show ведет себя как подтип IShow (если без боксинга).
Тем, что foo можно применять только к одному конкретному типу a, а foo :: IShow => smth к любому «подтипу» IShow (т.к. а там нет).
> Но вы с ним не можете делать ничего другого.
Как и в случае подтипирования — если вы написали ф-ю, которая работает с данным интерфейсом, вы не можете делать ничего, кроме операций, определенных в данном интерфейсе.
Ну уж нет. Итератор в ООП — это общий паттерн для организации обхода произвольных коллекций, а head/tail-список — это конкретная реализация.
Аналогом паттерна "Итератор" в Хаскеле можно назвать классы Traversable и Foldable, но никак не список.
… вот мы и пришли к вопросу "что же такое ООП".
Ну, от того, что реализован интерфейс, тоже пока еще ничего не случится. ООП появится там, где будет someMethod(Iterable foo)
. :-)
Это не алгоритмы. Алгоритм — это по определению последовательность действий, что для функциональных языков едва ли не запретное слово :-)
Функциональное выражение алгоритма на деревьях — это скорее схема такого алгоритма чем сам алгоритм.
Ничем не хуже. Просто take 5 . sortBy (comparing fst)
— декларативное выражение того что мы хотим получить в результате, а приведенный вами фрагмент кода на C++ — императивный алгоритм получения.
Код перестает быть императивным когда из него пропадают явные указания на промежуточные состояния процесса решения задачи.
Так, в вашем примере после исполнения std::sort
контейнер vec
оказывается в состоянии "отсортирован, но может содержать более 5 элементов". Это промежуточное состояние явно требуется кодом — но оно не требуется постановкой задачи и никоим образом из нее не следует!
В то же время в варианте take 5 . sortBy (comparing fst)
состояние "отсортирован, но может содержать более 5 элементов" имеет не входной список и не выходной — а некоторый промежуточный, не имеющий даже имени. Он полностью скрыт реализацией.
Тем не менее, если сделать библиотеку на плюсах и написать что-то типа x = take(5) | sortBy(comparing(fst)) | x
или x.transform(take(5) | sortBy(comparing(fst)))
— то эта команда, хоть и написана в декларативном стиле, скорее всего будет являться частью какого-то императивного алгоритма.
"Функциональным" же код делает применение паттернов и подходов функционального программирования, сам по себе функциональный код может быть как декларативным, так и императивным.
А параллельный алгоритм обычно можно рассматривать как несколько таких последовательностей.
Вы, наверное, имеете в виду линейный порядок.
Ну, да, наверное, скорее "выражение алгоритма". Я несколько неудачно выразился, а 0xd34df00d правильно понял, что я имел ввиду.
Ну и, разумеется, стоит понимать, что разнообразные функции «add» — это всего лишь максимально простой пример для демонстрации паттерна, а не реальный юзкейс. Разумный программист и без того понимает, что забивать гвозди микроскопом — не лучшая идея. А неразумный найдет способ сделать чушь и без функционального программирования.
Ага, особенно "практичен" вот этот кусок кода:
let printList anAction aList =
for i in aList do
anAction i
Был обычный алгоритм, который делал вполне конкретную вещь. Из него вынесли наружу всю конкретику, оставив тривиальный код. Но ведь [1..10]
и printLn
на самом деле никуда не делись! Они просто перешли к вызывающему коду, теперь каждый кто вызывает printList
должен указывать еще и эти [1..10]
и printLn
.
В итоге уровень абстракции от такого преобразования на самом деле уменьшился, а не увеличился.
Напомнило https://xkcd.com/1790/
Что самое веселое, даже после такого преобразования код все еще не абстрагирован от всего. К примеру, для асинхронных операций код будет работать некорректно — запустит их параллельно вместо последовательного выполнения. О ужас! Нам срочно нужна операция foldM
!
PS в целом пост мне понравился, но конкретно этот пример вызвал возмущение своей бессмысленностью.
Кстати, называть функцию `printList` после такого преобразования уже некорректно, правильнее `visitList`. А `printList` — результат частичного применения `visitList` с параметром `printLn`.
Они просто перешли к вызывающему коду, теперь каждый кто вызывает printList должен указывать еще и эти [1..10] и printLn.
Вероятно, потому, что функция теперь называется неправильно. Она не печаетает а применяет действие к списку поэтому ее надо назвать по другому
В итоге уровень абстракции от такого преобразования на самом деле уменьшился, а не увеличился.
Функция скрывает, как именно происходит перебор — например значение i уже наружи не видно и не надо о нем заботиться
К примеру, для асинхронных операций код будет работать некорректно
Асинхронные операции это синхронные операции по созданию тасков. Так что для них код скорее всего даже не скомпилируется — будет ожидать функцию не возвращающую значения а на вход подадут нечто, возвращающее таск
Функция скрывает, как именно происходит перебор — например значение i уже наружи не видно и не надо о нем заботиться
Но раньше-то она скрывала список и действия!
Асинхронные операции это синхронные операции по созданию тасков. Так что для них код скорее всего даже не скомпилируется
Хорошо, что ошибочный код не скомпилируется. Но плохо что он при этом не будет работать.
Но раньше-то она скрывала список и действия!
Это уже не она. Никто не мешает оставить старую, просто абстрагировать перебор оттуда
let printList = List.iter printItem
Хорошо, что ошибочный код не скомпилируется. Но плохо что он при этом не будет работать.
Если надо последовательно объединить асинхронные функции просто воспользуйтесь другой функцией
Никто не мешает оставить старую, просто абстрагировать перебор оттуда
Автору статьи тоже не помешало бы так сделать :-)
Если надо последовательно объединить асинхронные функции просто воспользуйтесь другой функцией
Но зачем в таком случае абстракция над перебором списка, если она не позволяет нам повторно использовать код?
Но зачем в таком случае абстракция над перебором списка, если она не позволяет нам повторно использовать код?
Она нам позволяет повторно использовать код. Но не весь. Какого поведения вы хотите от нее? Чтобы выполняла параллельно? Чтобы ждала выполнения каждого шага? Это и надо специфицировать. То есть передавать ей функцию, которая не возвращает task, а что-то делает.
Точно такой же бред можно сочинить и в обратную сторону:
— Функции высшего порядка / Класс
— Каррирование / Класс
— Композиция функций / Класс
— Предикат / Класс
— Функтор / Класс
— Лифт / Класс
— Моноид / Класс
— Монада / Класс
И так далее из списка "жаргона ФП"
Паттерны — это просто примеры некоторых общепринятых подходов, при помощи которых можно решать задачи. И таких не существует разве что в тех парадигмах, которыми никто не пользуется.
Особо смешно, что сперва автор приводит эту картинку, которая показывает, как все просто в ФП решается просто функциями, а потом зачем-то пишет статью с кучей практик (их можно назвать даже паттернами, лол). Странно, что у него мозг не взрывается от такого диссонанса. Шапку в мороз носите и выбирайте теплую, а не красивую.
Ну вот даже это что — «просто функция»? Или, все-таки, «паттерн»?
что делать с пустыми списками? Что делать, если у нас нечетное количество элементов? Правильно, добавить в список нейтральный элемент
И да, на счет КДПВ. Я, конечно, могу ошибаться, но, насколько я знаю, в ФП, Лисп — это просто баловство вроде JS в сравнении с Haskell, потому мне непонятно, почему он улетел так высоко.
А вот этот код но уже с обработкой ошибок:
На самом деле, данный код совсем не соответствует семантике приведенного в пример «ужасного ООП с проверкой ошибок».
В ООП, каждая ошибка выдает какой-то свой результат, в ФП варианте – все ошибки просто игнорируются. Если добавить проверку ошибок как в ООП, то будет не на много лучше чем в ООП, на который автор жаловался.
ФП вариант с проверкой ошибок соответствует ООП варианту без проверки, где каждый метод может кинуть RuntimeException в случае ошибки.
ФП вариант с проверкой ошибок соответствует ООП варианту без проверки, где каждый метод может кинуть RuntimeException в случае ошибки.
Здесь я с вами согласен, но только для однопоточного кода. С TPL вариант с exception'ами уже не такой хороший. Кроме этого, нужно гарантировать, что в программе только один catch, иначе высок риск «проглатывания» ошибок.
TPLЭто уже смесь фп и ооп подходов и является нетрадиционным. Потому этот кейс не стоит брать во внимание – там другие правила.
Кроме этого, нужно гарантировать, что в программе только один catch, иначе высок риск «проглатывания» ошибок.Не обязательно.
Эксепшены должны ловиться теми, кто их должен обрабатывать. Под обработкой так же подразумевается возможность обернуть в другой эксепшн и кинуть дальше.
Это все тот же ".catch()" в промисах, но в промисах он менее гибкий.
Потому этот кейс не стоит брать во внимание – там другие правила
Если вас кейс на данный момент не слишком волнует, это не значит, что его не стоит брать во внимание. Вот был у вас однопоточный код, стал он выполняться двое суток. Надо параллелить, а у вас внутри везде
throw
. Весь код на помойку, переписываем на TPL с нуля? Весело, но дорого.2. Теперь конкретно ваш пример:
Вот был у вас однопоточный код, стал он выполняться двое суток. Надо параллелить, а у вас внутри везде throw.Это самое плохое решение в данном случае.
Сегодня Вы распараллелили код и получили профит, но проблема так и осталась – вы ее просто замаскировали и Вы с нею столкнетесь через время в еще больших масштабах.
Такие проблемы не появляются на ровном месте, а если вы с нею столкнулись, то у вас проблемы гораздо большего масштаба, чем «у нас в коде везде throw».
Такие проблемы не появляются на ровном месте, а если вы с нею столкнулись, то у вас проблемы гораздо большего масштаба, чем «у нас в коде везде throw».Случай из жизни. Жили были программисты. Совместили они логику валидации и прасинга документов. И выбрасывали они исключения для валидации. И нормально все было, пока не стали присылать сводные xls-файлы на несколько десятков миллионов строк. И, нет-нет, нельзя клиенту объяснить, что обмен xls-файлами такого размера не самое оптимальное решение.
Ждать сутки для разбора этого файла последовательно — не вариант, тем более, что железо на проде позволяет и не все строки файлов связаны.
Map / Reduce
— прям то, что нужно. В итоге не контролируемые побочные эффекты (функции валидации, дергающие БД и выбрасывающие исключения) значительно затруднили мне тот самый Map / Reduce
.У
Either
есть сильные и слабые стороны. Вы продолжаете настаивать на том, что есть один православный способ на все случаи жизни, а все остальное к дело не относится и вообще другой случай. У автора пример тоже максимально простой, чтобы не пугать монадами и прочими эндофункторами. В ASP.NET MVC с обработкой ошибок действительно хорошо — можно обработать все декларативно. Но вы точно уверены, что у вас все исключения должны возвращать код 500? 401, 412, 422, не? Посмотрите доклад про rop целиком, прежде чем судить. Там есть много здравых мыслей.А еще вместо Either можно взять аналог Validation
Вот статья. А вот реализация на TS
Ммм, а должна? Это ж аппликатив, аккумулирующий все ошибки в полугруппе "слева"?
Так ведь Writer просто собирает эффекты в моноид слева, по-прежнему предоставляя доступ к значению.
Validation же — своего рода Either, но со сбором всех ошибок в полугруппу (причем удобно в NonEmptyArray) без доступа к значению при провале (так как его, значения, нет). Поэтому, например, в scala cats Validated описывается как способ параллельной валидации, а Either — последовательной.
Если вы про early return для map функтора, то там да, все ок — failure-ветка "закорачивается".
PS. Вы можете меня поправить, я пока разбираюсь со всем этим добром :)
Без примеров кода сложно.
Примеры там на гитхабе есть
Как я могу вызвать bar, если foo мне вернула ошибку, и никакого b нет?
Никак, так как при попытке определить bind/flatMap
ломается ap
и перестает аггрегировать ошибки. Так что только через ap
и с одинаковым типом значения. Но, вроде, можно считерить и ненадолго перегнать в Either. В cats даже есть хелпер withEither
В итоге не контролируемые побочные эффекты (функции валидации, дергающие БД и выбрасывающие исключения) значительно затруднили мне тот самый Map / Reduce.
Что-то мне кажется, что дерганье БД было намного большей проблемой чем исключения.
ИМХО так и было, из личного опыта буквально на днях, с учётом ограничений xls на 1е6 строк в листе, не оптимизированный код грузит полный лист в БД как есть примерно за минуту(а вот если построчно вставлять теже самые пол миллиона, то выходит только на вставку в одной транзакции около получаса), дальнейшая обработка инструментами Oracle это никак не часы, видать просто при разработке использовались тестовые наборы по 100 строк на файл, а потом поставили на prod и внедрили.
Спроектированная система была не предназначена для такого – это архитектурная проблема.
Решение архитектурных проблем – это всегда больно. Даже если бы у Вас не было бы throw, но была бы логика хождения в базу и изменения какого-то общего состояния. Вы бы мучались не меньше. Но в этом случае вы бы проклинали «Stateful». Но вы упускаете тот момент, что Вы переделывали Архитектуру уже работающего приложения.
Вы переделывали самолет в вертолет во время полета, а жалуетесь на то, что заклепки хуже болтов.
Если бы валидация была чистой функцией и если бы ее потрудились выделить в отдельный класс, а необходимые данные загружались бы из бд на основе спецификаций через QueryObject, инжектируемого через интерфейсную ссылку с помощью IOC-контейнера, то мне бы осталось только немного перекомпоновать код и распараллелить. Но этого не случилось.
В данном случае есть «правильный» вариант (болты). Нужно было их сразу ставить. Была бы дополнительная сложность, кривая обучения, но я заметил, что по примеру джуниоры копипастят код разного качества примерно с одной эффективностью. Лучше учить копипастить сразу хороший:)
Я строго на стороне дяди Боба: ФП и ООП — инструменты, не заменяющие, а дополняющие друг-друга. Полезно знать и применять при необходимости приемы и той и другой.
На мой взгляд ФП поднимает ряд вопросов, традиционно игнорируемых ООП-сообществом: формальное доказательство корректности программ (вместо решения задачи численным методом: юнит-тестами), точное определение возможных случаев и реакций на них. С помощью
Either
можно перенести проверку на этап компиляции с рантайма. Как известно, чем раньше найдена ошибка, тем дешевле ее поправить. Практическая применимость и tool-support таких концепций — отдельный вопрос. Я не считаю, что IO — супер-элегантное решение проблемы ввода-вывода в хаскеле: «посмотрите это выглядит как императивный код, но это не он»:)
А что не так в TPL с исключениями?
Вообще монады vs исключения vs коды возврата тема, мягко говоря, спорная:)
Хорошо, а в чем особенность отслеживания exit points в многопоточном коде?
try
{
var a = someMethodThrowingArgumentException(123);
var b = someMethodThrowingInvalidOperationException(a);
var c = someMethodThrowingGenericException(b);
return c.ToString();
}
catch(ArgumentException)
{
retrun "ArgumentException happened";
}
catch(InvalidOperationException)
{
retrun "InvalidOperationException happened";
}
catch(Exception)
{
retrun "Ooops!";
}
С TPL:
someTask.ContinueWith(a => {
try
{
someSyncMethod(a);
someAsyncMethod(a).ContunueWith(b => {
try
{
// ну и так далее
}
catch(InvalidOperaionException)
{
// только для синхронного метода
return "InvalidOperationException happened";
}
})
}
catch(ArgumentException)
{
return "ArgumentException happend"
}
})
Да, есть async/await. Но код с async/await только выглядит императивным, а компилируется в нечто другое, работающее с помощью SyncronizationContext и по духу ничем не отличается от монад. Т.е. в чисто-императивном коде без «примочек» придется писать try/catch в каждом
ContinueWith
. Код становится менее читаемым, exit points «размазываются» по разным callback'ам и их приходится «собирать». Основная проблема такая.Код с async/await ничем не отличается от кода с Task.ContunueWith, а просто явялется синтаксическим сахаром. SynchronizationContext используется и там, и там.
Т.е. в чисто-императивном коде без «примочек» придется писать try/catch в каждом ContinueWith.
А разве в момент получения результата всей цепочки (т.е. var t = a.ContinueWith().ContinueWith().ContinueWith(); t.Wait();
) вы не получите AggregateException
?
Thread
напрямую. Да, сильно сложнее с TPL не становится. Я говорю о том, что с TPL появляются дополнительные нюансы.var t = a.ContinueWith().ContinueWith().ContinueWith(); t.Wait();
Этот код не имеет практического применения. Какой смысл использовать асинхронную модель, чтобы потом принудительно ее «синхронизировать» с помощью
t.Wait()
?Я имел в виду следующие особенности:
- Fire & Forget: не все разработчики одинаково
полезныответственно относятся к обработке исключений. Кто-то просто напишет так и ошибка будет потеряна.
Task.Run(() => { // ... throw new Exception("please help me") }; // Fire & Forget
- Про
AggregateException.Flatten
дляTaskCreationOptions.AttachedToParent
нужно знать, иначе можно потерять часть исключений.
void Handle(AggregateException ex) { foreach (var exception in ex.Flatten().InnerExceptions) { Console.WriteLine(exception.Message); } }
- Возможность обработать исключение с помощью
TaskContinuationOptions.OnlyOnFaulted
, а неtry/catch
— еще один exit point. - Можно потерять одно из исключений при
использовании Task.WhenAll
(да, они ССЗБ). - UnobservedTaskException — настраиваемое поведение
- Wrapping / Unwrapping
AggregateException
при использованииawait
илиt.Wait()
— нужно внимательно следить, что мы ловим вtry/catch
.
Возможно, для кого-то эти аргументы покажутся несущественными. Для меня это дополнительная сложность с которой нужно работать.
Спасибо за комментарий. Пока отвечал понял, что зря вообще докопался до TPL. Мне больше не понравилась идея строить на исключениях control-flow на самом деле, а TPL как-то под «замес» попал:)
Этот код не имеет практического применения.
Не имеет. Это просто иллюстрация для того, чтобы было понятно, где именно вылетит AggregateException
.
Я имел в виду следующие особенности:
Знаете, когда у вас код заведомо многопоточный (например, веб-приложение), это все такие мелочи… TPL это, по крайней мере, упрощает и нормализует.
Мне больше не понравилась идея строить на исключениях control-flow на самом деле,
А не надо строить control flow на исключениях, они не для этого.
Это в maybe. Если в either, то не будут игнорироваться.
ps: можете привести пример правильного кода, который будет обрабатывать ошибки точно так же, как это делается в ООП варианте? Будет интересно посмотреть, на сколько он будет лучше смотреться.
Там ведь код на картинке будет тот же самый. Просто в другой монаде (она там, вообще говоря, не указана, просто мы из контекста знаем, что автор говорил о Maybe).
В ООП версии но ставил некрасивые обработчики, чтобы показать всю уродливость, хотя так никто не делает.
А в ФП версии кода, он даже не добавил никаких обработчиков, мол «вот смотрите как красиво!».
Если бы автор привел пример, где каждая ветка, которая может пойти не так выдавала бы свое сообщение об ошибке, это выглядело бы уже не так красочно, как он показывал ;-)
Maybe
и Either
. Кстати, Скотт не кисло глумится над тем, как обычно объясняют монады апологеты ФП и какое впечатление это производит.Под «игнорируются» я имел ввиду, что в данном коде нету явных обработчиков ошибок. Они будут где-то снаружи (или внутри).
После ошибки в первой функции мы «перейдем на красные пути» и выйдем из функции с результатом, который говорит про ошибку – я прав?
И тут вопрос, который я изначально подымал: Кто обрабатывает эти ошибки?
Судя по коду есть 2 варианта:
1. ошибки обрабатываются внутри каждой функции, которую вызывает updateCustomerWithErrorHandling;
2. ошибка обрабатывается снаружи updateCustomerWithErrorHandling.
В обоих случаях – мы получаем несоответствие между двумя примерами кода.
Так эти ветки внутри функций за |>
Функции выдают ошибку. В коде с монадами нам не надо при этом создавать ветки в вызывающем коде, а в коде с ООП — нужно было бы.
Так, может быть, тогда и [кажущаяся] разница в красоте ООП- и ФП-решений связана исключительно с тем, в каком месте размещается проверка и возврат сообщения об ошибке?
Плюс — всегда можно упороться трансформерами и засовывать туда на call site дополнительную логику, не меняя сам код. Но я лично не ярый сторонник подобного.
Эдак можно и про вариант с исключениями сказать что он лучше монад тем что в нем нет монад :-)
Монада — это тип данных, то есть нечто существующее в рантайме. Чем же в таком случае отличаются монады и костыли рантайма? Почему раскрутка стека — костыль рантайма, а целый лишний тип данных — нет?
По поводу типизации — вон в Java есть checked exceptions. Все как вы написали — информация содержится в типе, компилятор гарантирует что исключение будет поймано. И даже под свои нужны исключения прекрасно допиливаются.
Монады — это то, как мы используем некий набор функций. Способ. Мы можем этот способ инкапсулировать в тип данных, но это совсем не обязательно. И этот способ мы можем реализовать сами на основе базовых объектов, функций, и поправить его как угодно. Никакой поддержки со стороны компилятора или рантайма при этом не требуется. Исключения, в противовес, это особый объект, который дан нам разработчиком языка, вещь в себе.
> По поводу типизации — вон в Java есть checked exceptions.
И они по определенным причинам не взлетели.
И они по определенным причинам не взлетели.
И эти причины заключаются в том, что на самом деле никому не нужна столь строгая типизация. Тем не менее, если конкретно вам она нужна — возможность ее использовать есть.
Я могу написать ф-ю, которая принимает лямбду с заданными checked exceptions? Нет.
Проблема у checked exceptions не с тем, что они, с-но, checked exceptions, а с тем, что их хреново реализовали в джаве. Можно реализовать по-человечески — но штука в том, что в этом случае оно будет только названием отличаться от встроенной в язык монады Either. Точнее — это _и будет_ монада Either!
Так это ж не лямбда, а чит чистой воды
Может, потому-что это не лямбда, а объект с методом call
?
И в чем же принципиальная разница?
Java довольно многословна, это факт. Но чем кроме 4 букв и 1 точки этот объект отличается от лямбды?
Странно, что вы тогда не apply
использовали. Судя по докам, так оно и реализуется.
Э, вы вообще о чем?
Ну, запись x -> x + 2
это же тоже самое, что
new Function<Number, Number>() {
@Override
public Number apply(x: Number) {
return x + 2;
}
}
Не? Я не пишу на джаве
О том речь и шла — в джаве checked exceptions реализованы крайне неудобно. Потому не взлетели. А если сделать так, чтобы взлетели, то оно будет как монада :)
Напомню: исходно обсуждался ООП-подход против функционального. Исключения относятся к первому, лямбды — ко второму. ООП возможно вовсе без лямбд.
Да, "классическое" ООП многословно, это его недостаток, который никто не скрывает — и именно потому в такие классические ООП-языки как C++, Java и C# активно добавляют элементы ФП (те же лямбды). Но к исключениям против монад это отношения не имеет.
Да это просто пример. У checked exceptions много разных проблем из-за врожденной кривизны.
Да это просто пример. У checked exceptions много разных проблем из-за врожденной кривизны.
Аргументируйте, пожалуйста.
Я не спорю, в рамках ФП все является либо функцией, либо значением, либо и тем и другим — и исключения тут никаким боком не нужны.
Но я спрашивал про недостатки checked exceptions в рамках подхода ООП. Где нет никаких функций, возвращающих функции.
1. В случае ООП мы работаем со стейтом, вместо того, чтобы прокидывать аргументы явно
2. Проверяемые исключения не являются first class citizen
В итоге на практике сильно ограничивается возможность композиции методов с проверяемыми исключениями. В случае монад мы можем писать как угодно методы без исключений, а потом на самом верху лифтануть в функтор по нужному аргументу. При этом весь внутренний код о наличии дополнительной обработки сверху не в курсе. В случае с исключениями они по стеку протекают от внутреннего метода до внешних — везде «заражая» сигнатуры, которые при изменениях постоянно надо поправлять или перебрасывать исключения. В итоге на практике вместо того, чтобы инструментом пользоваться, его стремятся сломать и обойти, потмоу что чересчур много геморроя. Если бы ООП код писался в ФП стиле, с явной передачей, то этой проблемы бы не было, т.к. логику можно было бы точно так же выделить, а потом пробрасывать аргументы сверху.
Ну не, не будет оно точно как Either никогда. Во-первых, есть уже Either, в javaslang. Попробуйте сделать пару-тройку уровней вызовов функций, каждый из которых возвращает свои ошибки (ну там условно — сетевой/сокеты, http, прикладной), и дотащите ошибки уровня сокетов до приложения в виде Either. Это будет похоже, но это тоже будет кучка геморроя — просто другого.
Я готов согласиться, что Either в javaslang тоже реализовали криво, но лично не вижу способа сделать сильно прямее.
Пожалуйста, прочитайте ветку обсуждения сначала, а не с середины. Тут уже давно обсуждается совсем другой код.
Автор исходного сравнения соорудил соломенное чучело и разбил его в пух и прах. Ветка началась с сообщения о том, что можно в рамках подхода ООП написать код без разбросанных по коду проверок (с помощью исключений).
Дальше начался холивар "исключения — это плохо, но мы никому не расскажем почему именно".
Ну, я упростил пересказ. Полная ветка выглядит пока что именно так: исключения — плохо, потому что их нет в сигнатуре, а checked exceptions — еще хуже, но почему мы не расскажем.
по взгляду на функцию нельзя понять, что она возвращает
Вон ниже про ФП такой диалог:
— Вот только эксепшены не объявлены в сигнатуре функции, и о наличии «красных путей» можно только догадываться.
— Это было бы аргументом если сторонники ФП брали за правило всегда объявлять сигнатуры своих функций. Вот только…
— В статически-типизированных все хорошо с сигнатурами, просто их можно опускать, потому что вывод типов работает лучше
Так в чем разница? В том же C# точно так же на этапе компиляции можно узнать обо всех потенциальных ошибках.
В том же C# точно так же на этапе компиляции можно узнать обо всех потенциальных ошибках.
Только для известных реализаций, не для интерфейсов. К сожалению. И не для переданных делегатов.
(Я, правда, не говорю, что это обязательно нужно. Просто указываю ряд ограничений.)
В коде с монадами нам не надо при этом создавать ветки в вызывающем коде, а в коде с ООП — нужно было бы.Вообще-то нет, не обязательно.
Метод, который выполняет какое-то действие кидает эксепшн (переходит на «красные пути»), который прерывает дальнейшее выполнение кода.
Функция validateRequest явно не занимается формированием ответа на, допустим, http запрос, значит она отдает ответ, который просто переводит исполнение на «красные пути», и где-то тааам, в конце кто-то примет этот ответ и сделает из него ответ на запрос.
в случае с эксепшеном:… и кто-то перехватит эксепшн и сделает из него ответ на запрос.
– суть та же, но способ реализации другой.
Вопрос: почему автор не использовал эксепшены и намеренно изуродовал ООП код? =)
Метод, который выполняет какое-то действие кидает эксепшн (переходит на «красные пути»), который прерывает дальнейшее выполнение кода.
Вот только эксепшены не объявлены в сигнатуре функции, и о наличии "красных путей" можно только догадываться.
Это было бы аргументом если сторонники ФП брали за правило всегда объявлять сигнатуры своих функций. Вот только ...
… ни одна функция из определенных в обсуждаемом тексте в примерах на F# не имеет явной сигнатуры! Тут не то что о наличии "красных путей", тут в принципе о типах аргументов и возвращаемых значений надо догадываться!
И самое интересное — это таки работает. А значит, "нужно догадываться" еще не означает "трудно разобраться". И с исключениями можно поступить так же.
Тем более, для сложных случаев есть документация.
А еще в Java есть checked exceptions.
… ни одна функция из определенных в обсуждаемом тексте не имеет явной сигнатуры! Тут не то что о наличии «красных путей», тут в принципе о типах аргументов и возвращаемых значений надо догадываться!
В статически-типизированных все хорошо с сигнатурами, просто их можно опускать, потому что вывод типов работает лучше. Но можете и объявлять везде.
Почему для аргументов и возвращаемых значений считается нормальным использовать "вывод типов", а для исключений всем достань и полож сигнатуру?
А как вы (окей, без checked exceptions) поймете, свалится ли у вас функция в рантайме, если в ее типе нет на это указания?
Обычно мне этого знать просто не нужно: я пишу код, который будет корректно работать при любом развитии событий, и это совсем не сложно.
Но если мне нужно узнать про конкретную функцию — а посмотрю ее исходный код (если она моя) или документацию (если она сторонняя).
Обычно мне этого знать просто не нужно: я пишу код, который будет корректно работать при любом развитии событий, и это совсем не сложно.
Сложно, несложно, это уже человеческий фактор.
Но если мне нужно узнать про конкретную функцию — а посмотрю ее исходный код (если она моя) или документацию (если она сторонняя).
Ну а можно этого не делать и положиться на компилятор.
Явной не имеет, но по факту сигнатура есть. Вы увидите ее, если наведете мышкой на ф-ю в ИДЕ, ну и компилятор вам в любом случае напомнит, если вы вдруг не обработаете ошибку.
А явная сигнатура кому нужна-то, по факту?
А сигнатура потому-что выводится. И если у вас у значения вывелся тип Option<number>
, вам компилятор не даст работать с ним как с number.
Ну и не все на джаве пишут.
В общем я сильно порадовался названию статьи и полностью разочарован её содержимым. Буду ждать следующую попытку от адептов ФП…
В ФП нет паттернов, так что вопрос смысла не имеет.
Так почему паттернов ФП нет? там все задачи уникальные? Или может в ФП не принято решать одну и туже задачу как все и надо обязательно сделать свой велосипед (сомневаюсь, но вдруг)? Или какие-то другие причины?
Мне правда интересно
Раздел про монады — это ж самые, что ни на есть, паттерны. Далеко не все, конечно, статья бедновата даже по меркам введения.
Нет, потому что решения тривиальны (например, паттерн «стратегия» — просто ф-я, и паттерн «фабрика» — тоже просто функция, и эти функции ничем друг от друга не отличаются, глядя на них вы даже не сможете сказать, какой там паттерн). Основная причина того, что паттерны в ООП существуют — это низкая гибкость применяемых в ООП методов. Ну трудно выражать какие-то вещи через обвязку из наследования, интерфейсов, вот этого вот всего, потому и появляются всякие хитрые схемы, предназначенные для того, чтобы каким-то единым образом описывать вещи, которые сложно описываются. В ФП же гибкость, наоборот, высокая (возможно даже, что слишком высокая), там нет смысла описывать реализацию паттернов в силу их очевидности. На паттерны могут тянуть монады/стрелки/етц., да рекурсивных схемы и их обвязка — но эти вещи слишком абстрактны и не имеют конкретного назначения (в то время как паттерны, вроде, должны иметь).
Если какой-то подход имеет строго выраженную идею, структуру и применяется для достижения конкретных целей – это паттерн.
Каррирование, Промисы, Коллбеки – это все паттерны.
Я и говорю, в этом понимании в ФП нету паттернов.
> Каррирование, Промисы, Коллбеки – это все паттерны.
Нет, каррирование и коллбеки — не паттерны. Так как у них нет назначения. То самое упомянутое вами «применяется для достижения конкретных целей». Не существует никаких конкретных целей для применения каррирования в общем. Так же как нету их для применения, например, монад в общем. Правильнее называть это каким-то другим словам, например — «концепция».
Я и говорю, в этом понимании в ФП нету паттернов.
О! А вот и целевая аудитория статьи, я выше о них смеялся.
Если в какой-нибудь парадигме нету паттернов, то это значит что она настолько ненужна в практической разработке, что под нее даже не пишут никакую теорию. Вы считаете, что это об ФП?
Под ФП есть огромное количество теории. Просто паттернов в этой теории нету. Там есть монадные трансформеры, стрелки, а также зигохистоморфные препроморфизмы с пределами и копределами. А паттернов — вот нет, не завезли :)
И, да, эту теорию можно применять на практике. Например, благодаря этой теории я знаю, что могу использовать синтаксис генераторов в js с любой монадой, в которой fmap применяет свой аргумент не более раза :)
Давайте определимся с терминологией, чтобы друг друга понимать. Я под «паттерном» понимаю нечто, что, в частности, имеет четкое назначение. Это соответствует общепринятому смыслу, вкладываемому в термин «паттерн». В этом смысле паттернов в ФП нету, потому как все конструкции слишком универсальны. Если же под «паттерном» подразумеваются какие-то «классические» приемы, которые туда-сюда ходят и применяется в том или ином контексте — то такое, безусловно, есть.
Давайте определимся с терминологией
Давайте. По вашему, паттерны ФП внезапно перестают быть паттернами в какой-то момент универсальности? А насколько именно они должны быть «универсальными», чтобы перестать быть «паттерном»? «зигохистоморфный препроморфизм» — уже не такой и универсальный, он уже стал паттерном? А паттерн «Интерфейс» внезапно перестает быть паттерном? И все просто потому вы хотите, чтобы в священном ФП не было того же, что есть в отврательном ООП?
По моему (и это, повторюсь, не только по моему, это общепринятый взгляд на вещи) сущность перестает быть паттерном, если у нее нет конкретного, четко формулируемого назначения.
> А паттерн «Интерфейс» внезапно перестает быть паттерном?
Там же у вас в первых же строчках на вики написано конкретное назначение паттерна — обеспечить программисту простой или более программно-специфический способ доступа к другим классам. Сможете сходным образом описать назначение монад?
> И все просто потому вы хотите
Я ничего не хочу. Мне лично без разницы, как и что вы будете называть. Кажется, я достаточно ясно раскрыл свою точку зрения, а какими конкретно словами она выражается — для меня совершенно несущественно. Если фп-конструкции удовлетворяют вашему пониманию термина «паттерн» — ну так пусть удовлетворяют, я не против. Сущность объекта не зависит от того, как этот объект называют, пусть хоть «тирьямпампация» :)
Именно так. У _конкретной_ монады назначение есть. У монады в общем (какой-то, неизвестно какой) — назначения нет. Это слишком общий объект, сказать про что-то «оно монада» — практически, ничего полезного не сказать. Что с этим чем-то будут делать — вывести уж точно нельзя.
> У объекта первого класса
Монада как бы не особо объект первого класса да и вообще не программный объект. Ее программным объектом, конечно, можно сделать (запаковать нужные ф-и в класс, например), но этого не требуется. Слово «конструкция» на мой взгляд наиболее точно отражает положение вещей. Есть некая конструкция (которая может быть реализована как угодно), и если она обладает определенными особенностями, то это — монада.
Монада Maybe, монада Option — это паттерны? Они четко соответствуют вашему определению: «нечто, что, в частности, имеет четкое назначение»
Да, конкретные монады (из классических, то есть у которых есть какая-то общепринятая семантика, как у той же Maybe), наверное, можно считать паттернами.
Однако, почему тогда не считать паттерном в ООП какой-то _конкретный_ класс? Так ведь не поступают, по каким-то причинам.
Джентльмены, откройте глаза!
Эта серия лекция популяризировала паттерн Railway [Oriented] Programming.
Гугль выдает: Результатов: примерно 15 600 000 (0,53 сек.)
Не все 15 миллионов по делу. Большая часть результатов относится к каким-то железнодорожным делам. Если искать то же в кавычках, то результатов становится примерно в тысячу раз меньше. Думаю, вы преувеличиваете известность этого словосочетания.
а. Обсуждение дизайна котлина
б. Неправильно употребенное по отношению к котлиновским проверкам на нулл
в. Какой джаваскриптов reactive framework
lmgtfy.com/?q=Railway+oriented+programming
Первую «железнодорожную» ссылку я вижу на 6-ой странице
Я под «паттерном» понимаю нечто, что, в частности, имеет четкое назначение.
Ну вот например fold. Это на мой вкус ровно такая же типовая композиция из нескольких функций, какая обычно имеет место в паттернах ООП. Только там объекты. Конструкция, не являющаяся базовой, но тем не менее повторяющаяся, имеющая определенное назначение (хотя и очень широкое).
Вот например какие паттерны мне надо применить при проектирование GUI библиотеки?Рекомендация «используйте react+redux» не подойдет? Функциональное программирование на UI.
Это были бы принципы функционального программирования, если бы на этом подходе строился поток управления в целом. Но 90% логики остается в грязных функциях с сайд-эффектами, а в редьюсерах — мелочь. Редакс ведь не про редьюсеры, редьюсеры можно исопльзовать и без редакса. Он про стор (который работает грязно) и про action creators и их middleware (которые работают грязно). В редаксе используются чистые функции? Используются, с этим не поспоришь. Но они в той же мере абсолютно везде используются. Важно — как используются. Если подход основан на использовании чистых ф-й — можно говорить об ФП. Если подход основан на использовании грязных функций (как в случае редакса) и там где-то применяются чистые функции — то тут об ФП говорить рановато.
Абрамов несколько иного мнения 29:00.
о! Группи Абрамова! Если Дан что-то сказал — надо отключить свой мозг и принять за истину! Думать не нужно, нужно помнить заветы Абрамова.
У Редакса есть элементы ФП. Код там процедурный. Диспатч — процедурный. В языке С sqrt — тоже чистая функция, которая не изменяет свой аргумент, но от этого весь язык не становится функциональным
«react+redux» не имеет никакого отношения к ФП.
Вы и другие участники ветки уже признали, что отношение есть: react / redux используют элементы ФП. Очевидно, что в них много побочных эффектов, потому что в web-ориентированном UI по-другому быть вообще не может.
В JS идеи начали просачиваться в том числе из-за проблем с data-flow и callback hell. Оказалось, что есть продолжения и с ними все попроще. А вот монады в JS пока никому не нужны, потому что никто целей чистоты, как в хаскеле не ставит.
Есть проблема — асинхронный код, который есть в 90% приложений и необходимость его как-то поддерживать и развивать. Есть решение — промисы. Копнули чуть дальше, оказывается еще и чистые функции есть. Подход прагматичный и инструментальный.
Рекомендация «используйте react+redux» не подойдет? Функциональное программирование на UI.
Это не «функциональное программирование», пусть и с элементами.
Я сослался на мейнтейнера
Который отличный маркетолог, эвангелист, но плохой источник истины.
Я никогда не высмеивал Абрамова. Я высмеиваю только его фанаток-программистов, которые заменяют свой мозг его речами.
И, кстати, так проигнорировали главный вопрос. Потому повторюсь:
вы признаете, что были неправы?
«react+redux» не имеет никакого отношения к ФПи
У Редакса есть элементы ФП. Код там процедурный. Диспатч — процедурный. В языке С sqrt — тоже чистая функция, которая не изменяет свой аргумент, но от этого весь язык не становится функциональным
Спорите со мной уже вы про Абрамова. А оригинальный вопрос был:
Вот например какие паттерны мне надо применить при проектирование GUI библиотеки?
React и Redux — это примеры применения элементов ФП в UI-библиотеках. Это максимально точный ответ. В комментариях мне пишут, что «react и redux не имеют отношения» а потом «содержат элементы».
Содержать элементы уже != иметь отношение?
Очевидно, что react и redux — не хаскель. Это никто утверждать не будет. Кстати, если перейти на ссылку с видео то там он утверждает ровно тоже самое «redux использует идеи функционального программирования. Они не новые».
Суть вопроса «какие есть примеры ФП в UI» уже утратила актуальность. В ветке мы обсуждаем недобросовестный маркетинг и девочек.
вы признаете, что были неправы?
В чем конкретно я не прав? Может и признаю, пока не понял.
Рекомендация «используйте react+redux» не подойдет? Функциональное программирование на UI.
Но это, как мы уже выяснили, не функциональное, а так, моментами использует функциональное.
Вопрос был про «паттерны создания UI на ФП», а вы в ответ привели процедурную либу с некоторыми функциональными элементами и еще ошибочно назвали ее функциональной.
Другой момент — был задан вопрос «какие есть практики создания UI на ФП». Все, что прилетело в ответ — «вот используйте библиотеку» (это просто фейспалм). Данное грустное обстоятельство указывает на слабую теоретическую базу.
Вы делаете pr на хейте Абрамова
И да, это лол. Я не выступаю на конференциях, где кого-то хейтю вместо выступления по теме, не рисую глуповатые сравнительнительные таблицы с хейтом, не пишу статьи, которые начинаю с хейта. А комментарии на хабре слишком быстро забываются, чтобы считаться пиаром.
Как вообще можно проектировать на ФП, вот в чём вопрос.
Тут спрашивают: «как писать поддерживаемые программы на фп», на что получают от вас ответ из разряда: «ну пишите как-то, чтобы оно скомпилировалось».
Не удивительно, что с такими советами никто не пишет на ФП ничего сложного и выходящего за простую обработку данных. Ну, например, игры, редакторы музыки, изображений и видео. Все годами пишется на императивной технике и никто не выбрасывает и не возвращается в Хаскель, лол.
Возможно, потому что в императивной технике люди хотя бы думают, как сделать результат поддерживаемым, а в фп единственный совет — «Пишете сигнатуры функций, добиваетесь, чтобы тайпчекалось — всё, спроектировали». Супер!
«как вообще можно проектировать на ФП».
Ну смотрите — на вопрос «как вообще можно управлять самолетом?» можно просто ответить: «берете штурвал и двигаете им, пока не прилетите на место», а можно дать ссылку на пару талмудов об управлении самолетом.
Никогда не приходилось поддерживать неподдерживаемый код, где синглтон на визиторе и фабрикой с инверсией зависимостей погоняет?
Конечно стыкался с таким. Со всяким стыкался. Но все-равно, у тебя есть куча хорошей теории о проектировании крупных систем, откуда можно почерпнуть множество разномастного опыта. Да, некоторые люди будут иметь культ карго, некоторые вообще все неправильно поймут. Да и без какого-либо опыта сложно понять всю эту теорию. Но с ФП — тебя просто садят в горящий самолет и кричат: «лети».
пишете сначала модуль ...
Вы же понимаете, что скорее описываете «как писать», а не «как проектировать»?
пишете сначала модуль, у него продумываете...
Уже не функциональный подход.
Функции в ФП декларируются как стейтлесс, поэтому их можно звать из любого места в любое время. Смысла делить проект на модули, кроме как для оптимизации загрузки программы, нету.
Собственно-говоря — так оно и выглядит в реале, но хотелось верить, что есть кто-то, кто знает как проектировать функционально.
Что-же касается Хэшмапы.
Тебе не нужна Хэшмапа, но ты этого не понимаешь!
Тебе нужен контейнер для неких данных.
Возвращаясь к вопросу проектирования — для каких данных тебе нужен контейнер, как эти данные связаны с другими данными, какое место занимают эти данные во множестве других данных. На эти вопросы ответа нет, потому, что данные ты не формализовал и не обобщил. А из этого ответа, через долгое (или недолгое, как повезёт) исходит решение, какой конкретно контейнер ты должен применить в данном случае.
Модули — это не ответ. Модульность даёт некоторое смысловое разбиение предметной области на группы. Ещё большее и удачное разбиение (Анализ) даёт ОО. Но мы говорим о ФП, причём в теоретическом смысле, не вдаваясь в подробности.
Модуль — это прежде всего способ организации кода. Модули не имеют отношения к используемой парадигме.
Функции в ФП декларируются как стейтлесс, поэтому их можно звать из любого места в любое время.
Ээээ, что? Сокрытие информации никто не отменял, оно не только к состоянию относится.
Есть функция, которая принимает нечто и нечто другое выдаёт.
Функция — это тоже информация.
Не может, и что? К тому, что функция — это информация, и иногда нуждается в сокрытии, это отношения не имеет.
Из одних существительных – тоже.
Более забавная статья есть на хабре:
habrahabr.ru/post/161885
Только она больше вопросов порождает, чем отвечает на вопрос «как проектировать на ФЯ».
…
Реальные задачи немного другие.
Вот например, постановка задачи про «Грабить Корованы» — до хэшмапы там как до эльфов лесом.
Если проектировать «Корованы» с Функциональным подходом, то мы быстренько наталкиваемся на такую проблему, что нам тяжело описать «Корован» и очень легко описать операцию «Грабить».
Почему же тяжело? Прикидываем данные, которые должны описывать караван, и пишем соответствующий тип.
Первый это Неограбленный_Корован, а второй это Ограбленный_Корован.
И нет никакого жёстко заданного «типа».
Э, правда? Из какого же определения ФП это следует?
Как это нет? Есть, конечно. Как же компилятор типы проверяет, если их нет? :)
> В ФП у тебя минимум два «Корована»!
Берете Clean (это такой чисто функциональный ЯП), объявляется тип вашего каравана uniqueness и будет только один караван! Чудо! :)
type Caravan = {
gold: number
};
const rob = (caravan: Caravan): Caravan => ({
gold: 0
});
Парам парам пам. Caravan
— жестко заданный тип.
del
Она не содержит скобок из-за того, что скобки там ничего не значат, в силу правоассоциативности "->".
Поэтому скажем большое спасибо создателям зоопарка, но заклеймим религиозных фанатиков, сбивающих с толку юных программистов.
Если говорить про метафоры, то вот еще проскальзывает в статье избитая тема: «функция в ФП- это объект первого порядка». Ну на практике (такой же, как с вишней-черешней) это не совсем тот «первый порядок», который имелся в виду, когда Чёрч придумывал лямбда-исчисление. Вот что, например, имеется в виду под «настоящим первым порядком» с практической точки зрения: habrahabr.ru/post/322052 —
True = t => f => t
False = t => f => f
И дальше все остальное через это выводится.
А то, что коллбэки можно передавать в параметрах или что функции комбинировать, так это как черешня-вишня: вроде бы и то же, а вроде бы и вообще нет.
(apple -> banana) (banana -> cherry)
apple, banana, cherry — это разные сущности. У них абсолютно разные аттрибуты, параметры и инварианты, но — функциональщики с лёгкостью необыкновенной преобразовывают одно в другое. При этом они даже не могут объяснить (или не удосуживаются), что это за преобразование такое (apple->banana), что за этим стоит из предметной области.
getCustomerFromDatabase — в функциональщине вообще невалидная функция (по названию). Потому, что нету ни Кастомера ни Датабазы.
Потому что основа функционального программирования — это stateless-операции над аргументами функции. Когда мы добавляем внешний изменяемый state в качестве, например, базы данных, то теряется идемпотентность функций, и результат композиции становится непредсказуемым. Чтобы жестко зафиксировать последовательный порядок применений функций, требуется использовать специальную монаду IO, с которой программирование фактически становится императивным. Кроме того, результат вычисления функции — это уже не просто Response, а еще и измененный State. То есть красивая идеальная схема HttpResponse = WebApplication(HttpRequest) уже не работает, и проблема здесь будет не в getCustomerFromDatabase(), а в updateCustomerInDatabase().
Поэтому для data-driven приложений функциональное программирование не лучший выбор.
Чтобы жестко зафиксировать последовательный порядок применений функций, требуется использовать специальную монаду IO, с которой программирование фактически становится императивным.Все-таки точнее сказать: «код выглядит как императивный». IO позволяет отделить вычисления от исполнения.
Так вот, этот самый Эрланг изначально противоречит «основе функционального программирования» — а именно в модульной структуре. Если наши функции такие стейтлесс, то нахрена ограничивать доступ к функциям в модуле с помощью -export([blablafun/1]).
Ну ладно… Ты не замечаешь, что в ответе ты применил Структуру Данных, которой быть не должно? То есть идеальная схема — это:
{http_response, _bla, _bla ,_bla, _bla} = handle({http_request,_bla,_bla,_bla,_bla}).
… и эти оба тупла — это совсем не структуры данных…
А до кастомеров в датабазах и о последовательности исполнения мы ещё и близко не добрались.
Это тоже слишком сложно. Просто используйте неизменяемые коллекции, функции без побочных эффектов и ориентируйтесь на данные, а не паттерны.
Просто используйте неизменяемые коллекции
Всегда и везде и память ваша закончится:) Кроме шуток, R# перестал работать в VS2015 как раз из-за неизменяемых коллекций в Roslyn, которые выжрали всю оперативку. Анализаторы Roslyn выключить нельзя. Пришлось JetBrains запилить Rider.
Судя по картинке, Лисп вообще вне конкуренции. Почему же речь идет идет за F#, который двумя этажами ниже? И как лисперы живут без типов, монад?
монада — это всего лишь моноид в категории эндофункторовНе один год не мог понять, что такое монада, а после этой фразы резко понял.
Большое спасибо!
Функции с одинаковым типом входного и выходного значения являются моноидами
Странное утверждение. Функция из А в А — это бинарное отношение на множестве А, то есть, подмножество декартова произведения (АхА). Моноид — полугруппа с единицей. Может, имелось в виду, что множетво всех таких фнкций образует моноид относительно операции композиции?
Т.е абсолютно адекватный материалу.
Рекомендую!
И далеко не все у него просто. Когда он объясняет различие аппликатива и монад, если не seasoned haskeler/MLer — мозг приходится на повышенную тактовую частоту переключать.
Кстати, «Railway oriented programming» (который является ни чем иным как паттерном(!)) — имеет реализации (или подражания) почти для каждого значимого языка.
Я полагал, что если даже первоисточник кто-то не читал, то уже с последствиями его — обязательно должен был столкнуться.
Ну, а если вам так нравится удивляться тому что уже вошло в повсеместную практику — то может быть вам в новинку будет лекция Норвига 98-го года norvig.com/design-patterns/design-patterns.pdf
Рекомендую!
Академический ФП — академический и его надо оставить академикам. Но идеи из ФП вкупе с последними версиями популярных языков очень полезны. Вот пример java8 + spring после двух лет на скале. Ничего особо хитрого, но джавист так не напишет:
long unsentEmailId = transactionTemplate.execute(status ->
unsentEmailRepository.save(
new UnsentEmail()
.to(to)
.subject(subject)
.body(body)
.lastSent(new Date())
).getId()
);
Я про то же — "совсем ФП" беспощаден, медленнен и бесполезен. И не очень хорошо работает с J2EE.
«Совсем ФП» медленнен (по крайней мере, когда я на нем пишу), потому что немалая часть времени исполнения уходит в манипулирование иммутабельными контейнерами и структурами. Более или менее любой код на ФП можно переписать в «в основном процедурном» стиле (лишь с небольшими элементами ФП, типа как на c++ с использованием алгоритмов STL, но с малым количеством классов) так, чтобы он работал точно таким же образом. А потом можно заменить иммутабельные структуры и контейнеры на мутабельные, что, очевидно, лишь улучшит производительность.
ФП бесполезен в том смысле, что пока что не принес лично мне пользы (помимо разминки для мозгов и положительных эмоций, связанных с математической красотой), и в ближайшем будущем вряд ли принесет — я просто не вижу, какие проекты мне было бы эффективнее разрабатывать на функциональном языке.
Собственно говоря, я вполне допускаю, что «совсем ФП» для меня медленнен лишь потому, что у меня не так уж много в нем опыта — вон, если на каком-нибудь python писать начать, в первый день тоже все будет работать как Energizer — до десяти раз дольше. А вторая проблема — то, что я не вижу, где можно применить функциональную парадигму, собственно, и является основной причиной малого количества опыта, и, вполне возможно, вызывается всего лишь узостью моего мышления.
Насколько я понимаю, вы — достаточно опытный ФП-разработчик. Пожалуйста, посоветуйте, в проектах какого типа я, как разработчик, могу получить ощутимые плюсы от использования ФП (не сейчас, а хотя бы в будущем, через несколько лет опыта)? Это сразу отменило бы все три пункта, по крайней мере, лично для меня.
Спасибо, что так хорошо расписали :-) По моему опыту, полный отказ от мутабельности в угоду математической чистоте — плохая идея, т.к. компиляторов, способых качественно реализовать эту математическую чистоту, нет.
Теперь о пользе:
- иммутабельные структуры данных значительно упрощают синхронизацию в многопоточном окружении. Вместо мьютексов и т.п. часто можно обойтись CAS над одной переменной
- передавать функции в качестве аргументов другие функции — удобная вещь, как для декомпозиции кода так и для тестирования.
- map, flatMap и filter над коллекциями — очень полезные операции, позволяющие изящно выразить большинство распространенных преобразований коллекций
- концепция "всё является выражением" в языке программирования позволяет писать более качественный код. К примеру, if в современных языках это expression, а не statement.
- pattern matching — тоже очень удачная находка из мира ФП. switch на стероидах.
- добавление к четырем китам ООП (наследование, полиморфизм, инкапсуляция, агрегация) композиции (операция соединения двух объектов, возвращающая третий объект того же типа) позволяет многие вещи делать проще и изящнее.
- монада Option вместо null — must use в 21 веке, кроме самого оптимизированного кода
- монада Either позволяет писать безопасный код, где компилятор проверит, что программист обработал все ошибки
- монада Future/Promise — новый стандарт для асинхронного кода.
Это не ФП медленен, это компиляторы не-ФП языков плохи в оптимизации идеоматических для ФП конструкций.
А потом можно заменить иммутабельные структуры и контейнеры на мутабельные, что, очевидно, лишь улучшит производительность.Я имел в виду, что «можно заменить» не может ухудшить производительность — где улучшит, заменяем (и нередко получаем очень большой буст), где не улучшит — оставляем как есть.
Неочевидно
В любом случае, б-г с ней, с производительностью, я о ней написал лишь потому, что о ней писали выше. В моей практике обычно несравненно большее значение имеют скорость разработки, предсказуемость скорости разработки, читабельность и поддерживаемость кода (в том числе и с использованием всяких средств автоматизации и для человека, не знакомого с языком). Будь Haskell (или любой другой ФП-язык) хоть в десять раз медленнее на всех задачах, если он позволяет улучшить эти параметры, то я его самый преданный фанат. А сервера найти можно.
А какого типа проекты вы обычно разрабатываете?Обычно всякие вычисления в глубоком бекенде. Предметные области разные были за годы работы — поиск в интернете, ортодонтия, финансы, криптовалюты.
Смогу лучше ответить после вашего ответа на предыдущий вопрос, ибо, возможно, у меня очень нерепрезентативная выборка моих проектовПроблема в том, что сейчас я не вижу вообще никаких проектов, которые бы выиграли из-за применения существенного количества ФП (грубо говоря, большего его количества, чем то, что доступно в c++ из коробки). Но я предполагаю, что я их не вижу не потому, что их нет, а потому, что у меня недостаточно знаний или опыта в этой сфере.
Поэтому примеры совершенно любых проектов, которые, на ваш взгляд, лучше пишутся в ФП, будут очень кстати. А дальше я уже смогу над этими примерами думать, возможно, что-то реализовать как pet project, возможно, где-то в основной деятельности найти аналогии, возможно, еще что-то.
Как же тут не вспомнить канонiчное:
type Equality s t a b = forall p f. p a (f b) -> p s (f t)
hackage.haskell.org/package/tweak-0.1.0.1/docs/Control-Lens-Type.html
задокументировано на 11 из 10 :trollface.jpeg:
Дык о том-то и речь, что в хаскеле любят бессмысленные однобуквенные сокращения, что сразу «самодокументируемость типов» снижает до минимума. Прочитать-то не сложно, сложно понять какая семантика у этих буковок.
> Почему a и s, а не a и a', скажем?
А что, замена s на a' тут что-то существенно улучшает? :)
> Впрочем, я ни разу за свою практику не встречался с задачей, где были бы нужны линзочки
Да тут дело не в линзах, это хорошая демонстрация неких свойств кода на хаскеле в общем :)
Ну вот в рассматриваемом случае — не функтор.
> Третье перечитывание
Вообще, тот факт, что необходимость трех (ТРЕХ! даже не двух, Карл!) прочтений для парсинга «самодокументированной» строчки кода вас не смущает — показателен сам по себе :)
> Если же мы говорим о буковках для переменных, то \x y -> x == y не сильно хуже bool operator==(T left, T right).
Конечно не хуже, вот только в сколько-нибудь более сложных случаях пишут полные имена. Как пример — тип для Select в C#:
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector);
А что, для метафункции, в отличии от функции, понятных имен для переменных не надо? Хотя, погодите! Ведь хаскелисты и для обычных ф-й хреначат x, a, b, c, а когда не хватает — то x', a', b', c' :)
Другая деформация — нелюбовь к скобкам. Это прям отдельное направление спецолимпиады в хаскеле — взять понятный код со скобками и переписать его в нечитабельную дичь без скобок, зато с $,. и прочими радостями. И в одну строчку :)
> Ну ещё бы, с таким количеством синтаксического шума :)
Где там синтаксический шум?
Что ясно?
> Ну так шум же!
Так в том и дело, что не шум, а необходимая часть кода. В которую хаскель просто не может в силу очень плохо спроектированного синтаксиса.
> Ну так шум же! Как по мне, первый вариант сильно лучше второго:
Первый вариант плох сам по себе, а второй плох из-за врожденной кривинзы хаскеля, ничего более. По-этому _приходится_ писать говнокод вроде первого.
> В угловых скобках. Сравните с
От угловых скобок шума на порядок меньше, чем от кучи ->, которые еще и не слишком просто распарсить правильно в длинной строке, это во-первых. Во-вторых — в варианте хаскеля нету имен переменных, попробуйте записать то же самое, но с именами (к слову, отсутствие имен переменных у деклараций — еще один крест на «самодокументируемости» типов).
> Хотя каноничный хаскелист выбрал бы имена src, col, res вместо source, collection, result, например.
Во-первых, каноничный хаскелист бы выбрал a,b,c. Во-вторых — даже так показательно. Зачем писать col если можно написать collection? В чем вообще смысл этого сокращения? Зачем искусственно занижать читабельность кода?
При этом вы сами, небось, предпочитаете писать
items.Where(x => x.Price > 10).Count()
вместо
Count(Where(items, x => x.Price > 10))
да?
Конечно, ведь первую запись можно на несколько строчек по точкам разбить.
Теоретически — да, но на практике этого не делают.
> Что именно там необходимо?
Угловые скобки, запятые.
> То есть, код на хаскеле писать нельзя вообще, раз он не может в необходимые части?
Так они не считаются необходимыми и код пишется без них.
> Синтаксиса в хаскеле, кстати, предельно мало, и это очень удобно.
Что? Несколько десятков контролирующих инструкций со своей ассоциативностью и приоритетами — это «мало синтаксиса»? Из неэзотерических языков — хаскель язык с_самым_ перегруженным синтаксисом.
> Ну да, Func<TSource, IEnumerable> гораздо лучше и читабельнее (source -> t collection).
Во-первых, на хаскеле не пишут (source -> t collection), там будет (a -> b c). Во-вторых, если уж делать аналог, то (source -> IEnumerable collection), в-третьих, в случае с несколькими аргументами сложных типов — да, читабельнее.
> Никто не запрещает ставить переносы строк:
Вы сейчас рассуждаете о теоретической возможности. А я о том, как пишется реальный код. Чего кто запрещает или нет — не важно, важно что в реальности происходит с кодом, понимаете? Как выглядит идиоматический код на языке, каков общепринятый стиль.
> То же самое вряд ли получится, декларация типа и переменных таки разнесены.
Я и говорю, что не получится. И это плохо, т.к. основная часть «документации» из декларации типа сразу теряется.
> А каноничный шарпист бы выбрал синглтон фабрики декораторов селекторов. Будем биться стереотипами?
Вот вы хороший тезис привели. Теперь обратите внимание, что синглтоны фабрики декораторов селекторов считаются в шарпе раком, с которым борются, который режут на ревью. А a,b,c — это норма, скорее на ревью скажут: «зачем это ты написал тип source, пиши a». В этом и есть проблема. На хаскеле _можно_ (хоть и неудобно) писать с длинными именами, многострочно и т.п. Можно выработать какие-то соглашения, и код не будет превращаться в канонiчный хаскиарт. Но так не делают (не делают по бОльшей части, то есть это не считается какой-то общей нормой в комьюнити, какие-то отдельные специалисты, может, и делают).
> А почему вы пишете collection, а не itemsCollection, dataSetCollection, collectionWithResultsFromTheDatabaseOrOtherLinqQueryBackend?
Наверное, вы ожидали, что я скажу: «потому что оно понятно из контекста»? А вот и нет! Потому что это не важно. Не важно, откуда к вам пришла эта коллекция в данном участке кода, потом и не требуется писать, что она FromTheOtherDatabase. А вот понять, что это collection а не какой-то col — вполне себе важно, раз мы будем с ним работать как с коллекцией.
Так еще раз, у нас цепочка:
1. Нужна длинная строчка (как выше, если строчку записать полноценно, то она будет длиннее 80 символов, как в c#)
2. Но писать длинные строчки в хаскеле неудобно
3. По-этому пишут коротко => снижается качество кода.
> Зачем там угловые скобки-то?
Чтобы отделить типы от имен. В хаскеле этой проблемы нет, потому что там нету имен, что плохо (уже обсуждалось выше).
> А на сишарпе не пишут без фабрик синглтонов визиторов декораторов.
Конечно, пишут. Код без синглтонов и фабрик на шарпе — это норма. А на хаскеле норма a -> b -> c.
> Нет. Traversable t => source -> t collection
Ну вы t сократили, тогда Traversable traversable => source -> traversable collection. Видите как вы упорно сопротивляетесь «удлинению»?
> Почему? Вам же важен сам тип, а не то, под каким именем он используется в функции.
Конечно же мне важно, под каким именем он используется. Как это не важно? Имя описывает функцию. Назначение, в смысле
> Вы придумываете какой-то идеальный мир, где шарписты в белом, а хаскелисты пишут одни лишь однобуквенные фибоначчи и факториалы.
Я описываю вполне реальный мир. И в этом реальном мире синглтоны абстрактных фабрик — известный стереотип, повод посмеяться, на практике же — code smell. А в хаскеле _повсюду_ сокращения и попытки уложиться «в одну строчку и без скобочек», ну зайдите на hackage.
> А вдруг это не коллекция данных, а коллекция картин художника? Или музыкальная коллекция?
А нам это важно? Если нет — то и не требуются эти данные. Если же где-то дальше в работе важно, что это коллекция именно коллекция картин — тогда и стоит удлинить имя.
С нормальными именами для типов, с именами аргументов
> В чём неудобство?
Язык не поддерживает переноса.
> Не разбивают на две или несколько функций, не выносят отдельные элементы композиции в let/where-блоки, а пишут коротко.
Вы забываете, что выделение ф-й/блоков — не бесплатно, само по себе оно сильно снижает качество кода. Когда вам приходится выделять ф-и не потому, что это требуется семантически, а для того, чтобы компенсировать недостатки ЯП — это плохо. У меня на этот счет есть такое правило — если за 3 секунды не получается придумать ф-и имя, которое бы полностью и исчерпывающе описывало назначение ф-и для человека незнакомого с контекстом, то эту ф-ю выделять нельзя, т.к. код с ее выделением сильно усложнится, а профитов — нет.
> Я как-то всю жизнь думал, что в языках с сиподобным синтаксисом для этого используют пробелы
a b c d e f — вот, разделил пробелами. Где тут имена, а где типы? Вы просто попробуйте записать конструкцию C# без угловых скобок и поймете, зачем они нужны.
> А вы тоже вместо i пишете index?
Мы это уже обсуждали. Писать i в качестве индекса — общепринятое соглашение, которому все следуют. t в качестве traversable — не соглашение, ему не следуют. Не всегда traversable сокращаются как t, не всегда t означает traversable.
> Для библиотечного кода и факториалов (а также изложений конструктивных доказательств в виде кода) — пожалуй.
И поскольку никакого кода, кроме библиотечного пронаблюдать нельзя, то и опровергнуть ваше утверждение невозможно.
> Как люди пишут на языке?
Именно это. А что еще имеет смысл обсуждать?
> Или вы библиотечные операторы (всякие там $, ., <*>, >>=) тоже считаете захардкоженными в синтаксис языка?
Никто не говорил о «захардкоженности», речь шла об управляющих конструкциях. Если управляющая конструкция реализована как ф-я (или как макрос), то она управляющей конструкцией быть не перестает. И претензия не в самом факте ее наличия ав получающемся ascii-арте вместо синтаксиса. Лисп такого недостатка лишен, там как раз пишут вменяемые имена, особенно если мы о scheme-derived ветви :)
А вы в циклах в императивных языках пишете i или index?
Лучше вообще избегать индексов в циклах.
Ну вот, например, изменить значения массива:
void Modify<T>(this IList<T> data, Func<T, T> func)
{
for (int i = 0; i < data.Count; i++)
data[i] = func(data[i]);
}
Что тут лучше: i
или index
? Или нормально, что используется T
вместо TItem
?
void Modify<T>(this IList<T> data, Func<T, T> func)
{
... // Some code
for (int i = 0; i < data.Count; i++)
data[i] = func(data[i]);
... // Some code
}
void MeaningfulMethod<TMeaningfulType>(this IList<TMeaningfulType> meaningfulList, Func<TMeaningfulType,TMeaningfulType> meaningfulDelegate)
{
...// Some meaningful code
for (int meaningfulIndex = 0; meaningfulIndex <meaningfulList.Count; meaningfulIndex ++)
meaningfulList[meaningfulIndex] = meaningfulDelegate(meaningfulList[meaningfulIndex]);
...// Some more meaningful code
}
Вот вам кусок реального кода:
public void Truncate(int Count)
{
if (Count >= array.Count)
return;
for (int i = Count; i < array.Count; i++)
{
TValue value = array[i];
if (value != null)
{
value.SetUpdated(value);
value.Rollback();
value.SetParent(null);
array[i] = null;
}
}
Truncate();
}
Впрочем, да, в проекте больше нет мест, где нельзя обойтись без индекса.
как ваш код разорвало
Нелегко жить в вакууме.
читать же неудобно.
Не знаю, что хуже: многословные названия, из-за которых строчка занимает всю ширину 21"—монитора, или практика запихивать все самые тривиальные действия в функции с односложным названием.
Ну если с вашей точки зрения приведённая мной функция выполняет тривиальное действие, и от неё нужно отказаться в пользу вставки её содержимого в вызвающий код, то смысла продолжать спор не вижу.
Почему если Count больше или равно количеству элементов в массиве, происходит просто возврат, а не, к примеру, выбрасывается исключение?
Наверное, потому, что функция называется Truncate
? И обычная логика работы этой операции — обрезать лишнее.
Почему массив называется array, но не передаётся параметром?
Потому что это метод класса WatchableArray
, имеющего поле array
и generic параметр TValue
.
Что за тип TValue?
А вот это уже не имеет значения. Да, у него есть constraints, но если его назвать TTrackableAndWatchableValue
вместо просто TValue
, то на читаемости кода это скажется отрицательно за счёт увеличения объёма кода и тавтологии (WatchableArray<TTrackableAndWatchableValue>).
Truncate(Count) забивает элементы null, а вызываемая в ней приватная функция Truncate() уже корректирует размер массива.
Это вы, кстати, хороший пример привели. Если обратиться к официальному кодстайлу, то там будет указано, что все имена типов должны быть с префиксом T. Отсюда тривиально следует факт — у вас может быть лишь _один единственный_ однобуквенный тип, и это сам T :)
По-этому нормально писать T вместо TItem, потому что это одно и тоже по общему соглашению. А вот если у вас несколько типов — вам придется, по кодстайлу, дать им более значащие название.
Иногда это T1 и T2 — всяко лучше, чем TFirstArgument и TSecondArgument.
В случаях, когда у аргументов нет семантики — пусть будет T1 и T2. Когда мы пишем на шарпе, сам факт отсутствия «звучащих» имен и говорит, что семантики не стоит никакой за ними, иначе — перед нами плохой код. В хаскеле же, еще раз, f s t a b — это _норма_. В этом проблема. Речь не о том, что в принципе можно или нельзя написать, речь о том, что считается либо не считается нормальными, общепринятыми практиками.
Ну вы не сравнивайте несравнимое. i, j, k — это стандартные общепринятые сокращения для индексов, которые понятны всем. И если вы так обзовете не-индекс, то вам зададут недоуменный вопрос на ревью. С другой же стороны, a,b,c,f,a',b',c',f',x' и прочая дичь — это просто буквы, которые никакого смысла не несут. И, нет, не надо про «f — функтор», или «m — монада», потому что очень часто они используются не так и это считается нормальным (в отличии от i,j,k).
Просто хаскелисты фигачат говнокод, потому что в хаскеле очень кривой синтаксис. Он проектировался для написания коротких формул-однострочников и не подходит для general-purpose вещей, многострочных код и код с длинными именами на хаскеле выглядит откровенно плохо, вот и извращаются кто во что горазд. Ну и плюс — это круто же! В одну строчку и без скобок!
поэтому многострочный код бьют на функции.
Ничего хорошего в этом не вижу. Точно так же, как в ООП порой навешивают абстрактных фабрик и стратегий в любом коде больше одного экрана.
Вполне допускаю, что это действительно так. Но тогда вы — исключение.
> Люди, радующиеся тому, что круто, на любом языке наговнокодят.
Только в случае мейнстримных языков их бьют по рукам, а в случае хаскеля — их говнокод считается нормой. Более того — язык стимулирует на такой говнокод. В итоге вместо отрицательного стимула «наговнокодил — получи палкой по хребту» мы имеем положительный стимул «наговнокодил — молодец, возьми пирожок!»
> Во-вторых, имена переменных (а их и нет) в хаскеле и в ФП вообще важны чуть меньше, чем в императивном программировании. Довольно часто Ъ-хаскелист в библиотечном коде, где никакой предметной области ещё толком нет
Это не так, в функциональном языке имена переменных важны не меньше. А то, что хаскелисты любят писать максимально абстрактный код, забывая о том, что любая абстракция — сама по себе вредна, это еще одна проблема. И, опять же, в мейнстримных языках за говнокод с лишними абстракциями вам надают по рогам, в хаскеле — погладят по голове и дадут конфетку.
> Код с длинными именами выглядит тоже вполне нормально.
Видимо, именно по-этому вы выше вместо collections все-таки написали cols? Я, к слову, сперва подумал, что это сокращение от columns, и некоторое время недоумевал, при чем там columns? Только потом дошло.
Не подскажете ссылку на крупное опнсорс приложение на хаскеле? Не компилятор, не математика, не библиотека, а классическое приложение с базой данных, DAO, логикой и GUI?
А какое отношение паттерн Builder, сугубо ООП-шный, имеет к ФП?
www.reddit.com/r/haskell/comments/6ck72h/functional_design_and_architecture
«Паттерны» функционального программирования