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

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

а можно все-так ещё раз просуммировать решаемые проблемы? не совсем понятна важность 10 страниц текста
При образовании нового типа M<A> возникают проблемы с использованием старых функций
1) A -> B 
2) (A,B) -> C
Так же у нас получается новый тип функций A -> M<B> у которых есть проблемы с 
3)Применением
4)Композицией между собой
5)Композицией со старыми функциями
А с практической точки зрения?
Пример с GetDiscount не практический? Можно городить кучу ifов, а можно использовать абстракции. Да на C# выглядит страшно, так как он не совсем подходит под такой стиль.
int discount = DiscountService.GetDiscount(userId.Value, productId.Value)
Что произойдет вот в этом месте? внутри userId.Value будет null или выбросится исключение на моменте получения userId?
Какой конкретно профит? Потому и говорю, приведите полноценный пример, иначе нихрена не понятно.
DiscountService.GetDiscount(userId.Value, productId.Value)
Этот код я привел для простоты, конечно же он не корректный, так как в случае если один из них Nothing то Value == 0. Правильное решение дальше по тексту

Func<int,int,int> f = DiscountService.GetDiscount;
var discount = f.Curry().Pure().Apply(userId).Apply(productId);

Получается более прозрачная запись, так как нам вообще не приходится задумываться есть там что то в userId и productId или нет.
Да, я уже дочитал дальше. Если честно стиль изложения у вас слишком «матанистый». Хорошо, что я знаком уже с этой темой, но реально понять, о чем идет речь сложно сходу. И кстати, я бы назвал метод не Pure, а Maybe, для понятности:)
НЛО прилетело и опубликовало эту надпись здесь
Вот, я помнил, что где-то уже про это читал, спасибо:)
Кстати да, это мой пост, есть еще тут, отличается от того что приведено тут в статье тем, что подход реально можно использовать, т.е. брать существующие API где null используется вроде аналога F#‘ного Nothing и делать проверки с этим допущением.

Вообще переписывать все как показано в статье я бы не рекомендовал вообще т.к. непонятно, какие конкретно задачи это решает. Одно дело убрать проверки на null – в этом смысле подойдет мой метод. А каррирование например? Каррирование и функциональная композиция на практике нужны мало где – например редко где нужно делать вызовы вроде f(g(h(x))) а даже если нужно, это не так страшно. Я много пишу на F# но даже там операторы << и >> у меня не возтребованы, хотя конечно |> я пользуюсь т.к. удобно (но не критично).
О как, спасибо за статьи, читаю постоянно:)
А, так Monads это ваша библиотечка?)
Что? Какая библиотечка? Не знаю о чем вы
Вот эта. Но она действительно не ваша.
Да, это не мое. На практике монады — очень индивидуальной явление. Каждый допиливает реализацию под свои нужды. Не думаю что можно создать что-то обобщенное. Например многие люди несогласны с моим подходом к именованию методов и возможно они правы.
Ценность монад и прочего — в том, что они обобщают кучу вещей: nullable, списки, парсеры, исключения, цепочки колбеков, всякие недетерминированные вычисления, и много-много еще чего.

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

В C# есть LINQ, который выполняет все три перечисленные функции, и очень сильно похож на монады.

К сожалению, C# зарелизился не успев быть вылизанным, многие косяки уже не исправить. В частности в C# есть null, а мог бы быть Nulllable<> и для ссылочных типов. Теперь у нас есть зоопарк из null и Nullable<>, статья добаляет еще и третью сущность, и через это примеры статьи выглядят неудачно и непрактично. Если бы у нас был бы один Nullable<>, то к гадалке не ходи — был бы LINQ2Nullable и все бы юзали и были рады. Ну увы.

Вот хороший пример того, как можно понимание монад совокупить с LINQ и получить годный, практичный результат: code.google.com/p/sprache/
В частности в C# есть null, а мог бы быть Nulllable<> и для ссылочных типов.

Только это не в C#, а в BCL.
а зачем образовывать новый тип? похоже на решение проблемы введением дополнительного абстрактоного слоя.

а теперь вернемся на землю. у меня два контр-аргумента:
1) если c# — то там есть Nullable. Но даже при его использовании вылезает пункт 2
2) допустим где-то значение Maybe пришло в Nothing. Нет гарантии, что логическая цепочка из композиций сработает верно. А в случае null — мы получим NRE. И это будет именно exception — исключительная ситуация. И если это будет NRE — то об этом нужно логировать/сообщить/пойти альтернативным путем. В случае Maybe — это происходит за счет Maybe.HasValue, что и продемонстрировано в примерах. В итоге — разницы никакое

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

Отчет весьма прост: свойство Value у Maybe вызываться не должно. Ни когда. Его вообще быть не должно.
Это промах автора, что рассказывать про Maybe он начинает с метода, превращающего Maybe в кривой null.

Если не пользоваться Value, а обращаться к значению только через Map, FlatMap и т.д., то NRE исчезает и на смену ему ни что не приходит (см мой пример).
конечно не приходит. и в простых случаях (типа калькулятора — а все примеры на этом и основаны) этого достаточно. то есть в цепочечном вызове это можно использовать для короткой записи.

поясню, почему медитация академическая:
— не везде типы ссылочные.
— не так как хотелось бы исчезает NRE — я не вижу его cause. вернулся null или Nothing — хорошо. а ответа на вопрос «почему?» — нет.

прямо как в rails — «пишем быстро». особенно доставило в статье на gotdotnet абзац про «я сначала пишу как обычно, отлаживаю, а потом переписываю через монаду»

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

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

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

А писать сначала обычно. а потом через монаду — это какое-то странное эстетство. На scala я пишу сразу по человечески — так гораздо удобнее. Да и на C# я пишу сразу LINQ, а не сначала foreach, а потом LINQ.
«Ссылочность не важна: Nullable — struct» не про то была фраза.

представьте что пишется какой-то контракт, самый простой.

class Calculator {
public int add(int a, int b) { return a + b; }
}

автор предлагает мне заменить это на:

class Calculator {
public Maybe Add(Maybe a, Maybe b) { return a.HasValue && b.HasValue? a.Value + b.Value: Maybe.Nothing; }
}

таким образом я вношу в контракт три дополнительные степени свободы — каждый из трех актеров
приобретает характеристику HasValue — и после вызова я должен это учесть и проверить.

это то самое упрощение о котором говорит автор или нет?

ps. а апдейт к статье вообще страшный. вы будете пользоваться приложением, которое на любое нарушение преусловий будет говорить — «не шмогла»? а вы будете снова и снова вбивать один и тот же код продукта, не подозревая, что он уже не валиден.
Автор как раз на оборот не предлагает ничего менять, а говорит, что вот у вас появился новый тип Maybe<T> и остались старые функции А->B
соответственно ваш Add остается таким как бы, (int,int) -> int но его нельзя просто так использовать с новым типом, поэтому то и нужны доп телодвижения.
извиняюсь, source для кода не сработал…
Я правильно понимаю, что это некая страшная эмуляция «nullable data type» при отсутствии ее поддержки языком программирования? То есть то же самое на питоне:

userId = UserService.GetUserId( email ) # Может быть None
productId = ProductService.GetProductId( productCode ) # Может быть None
discount = DiscountService.GetDiscount( userId, productId) # Не нужно ".value", переживает None.
if discount is None :
  # Тут бизнес логика "не найдено".


BTW, в начале статьи вы написали фрагмент кода:

int discount = DiscountService.GetDiscount(userId.Value, productId.Value) // не корректное решение


Что в нем значит комментарий «не корректное решение»? Что это некий неправильный фрагмент кода, который не показывает практическое применение монады? Или он показывает практическое прменение монады для написания неправильного кода? Или что? Запутали старика :(.
С питоном тут сравнивать не стоит просто по тому, что в статье подразумевается статическая типизация и защита от NRE во время компиляции.

А некорректность решения в том, что оно обходит защиту от NRE. Вызов Value почти всегда вреден ибо опасен.
С динамическими тут вообще сложно сравнивать, так как там можно делать все что пожелаешь.
Но представьте если бы у вас DiscountService.GetDiscount на входе требовал что бы оба параметра были не None.
Заменил некорректное, на классическое.
С динамическими тут вообще сложно сравнивать, так как там можно делать все что пожелаешь.


Не обижайтесь, я просто для примера взял первый попавшийся язык с поддержкой nullable data type. Могу то же самое переписать на java, ruby, objective-c. Могу на C# — но я на нем давно не писал, боюсь ляпну что-нибудь, а народ как набежит с LINQ, замыканиями и пятой версией — и все O_O.

Но представьте если бы у вас DiscountService.GetDiscount на входе требовал что бы оба параметра были не None.


Представил:

if userId is not None and productId is not None :
  discount = DiscountService.GetDiscount( userId, productId)


Все еще непонятно, чем представленное отличается от эмуляции Nullable Data Type (BTW, так все равно писать нельзя — но это отдельный вопрос, который в рамках обсуждения этой статьи неуместен).
Вот что бы так не писать, я в самом последнем примере показал второе решение
discount = DiscountService.GetDiscount.Curry().Pure().Apply( userId).Apply(productId)
Которое мне кажется более прозрачным из за отсутствия if.
Maybe<int> discount = DiscountService.GetDiscount.Curry().Pure().Apply(userId).Apply(productId);


Фееричный, нечетаемый звиздец? применить карирование, создание функтора и тапнуть полученного монстрика два раза только для того, чтобы не вызвать метод с неверным аргументом? Чем это лучше кучки if'ов, кроме того что на поддержку данного кода придется разработчика искать в три раза дольше и в два раза дороже?
(userId |@| productId) {DiscountService.GetDiscount(_, _)}


Так лучше?

Или может так:

for {
  u <- userId
  p <- productId
} yield DiscountService.GetDiscount(u, p)
for {
  u <- userId
  p <- productId
} yield DiscountService.GetDiscount(u, p)


Извиняюсь за необразованность — а это вообще какой язык программирования и какое отношение это имеет к обсуждаемой эмуляции монады?
Scala

Приведен как пример ФП языка с более понятным чем у хаскеля синтаксисом.

К эмуляции подобного поведения на C# относится только как иллюстрация, что есть куда стремиться.

Скорее это иллюстрация того, что при хорошей реализации данный подход не многословен и семантичен.
Скорее это иллюстрация того, что при хорошей реализации данный подход не многословен и семантичен.


Это вы сейчас про скалу, или про то «функтор().частично().тапнуть().тупнуть().наизнанку().застрелиться()» которое мы выше на C# обсуждали? :)
функтор().частично().тапнуть().тупнуть().наизнанку().застрелиться()

И это вместо

productId.With(p => userId.With(u => DiscountService.GetDiscount(u, p)));
Вот это не совсем то, что я бы назвал «хорошей реализацией», но, как уже писал автор, C# не очень-то располагает к подобным фокусам.
for в скале, как и LINQ в .net — синтаксис для чего-то типа монад.
Проблема не в идеи, а в том что язык не очень подходит для такого стиля. Вот как выглядит на хаскеле
getDiscount <$> userId <*> productId
А на objective-c вообще можно вызвать метод для null и получать в результате null :) Я правильно понимаю, что на практике использовать монады без поддержки языка не получится благодаря нечитаемому звездицу, который в результате вырисовывается?
Скорее всего да. Например, хоть в скале и есть библиотека scalaz но насколько я понимаю большинство от нее плюются, данные концепции должны быть у истоков языка, как например в хаскеле. Если интересно, можете почитать мою старую статью
habrahabr.ru/post/112464/
там используется для монад сахар от linq.
Я читал. Но в данный момент меня эта статья больше интересует. Потому как одно дело «абстрактное множество, для пар элементов которорго мы задаем кортежи аппликативных бульбуляторов», а другое дело — некое практическое применение на языке программирования общего назначения. Но, увы, не взлетело :(.
Хаскель вполне язык общего назначения.
По вашей ссылке есть другие ФП, в которых есть практическое применение всех описаных практик.
Я знаю :).
Только я подозреваю что не у одного из них из коробки нет монад.
Мотря что вы под ними подразумеваете.

Там есть scala.
Разве в Scala в дефолтной поставке есть функторы монады категории?
Если нет в дефолтной, то есть в scalaz, но я бы не сказал, что scalaz так уж необходима.
some general-purpose languages:
И это я тоже знаю :).
Сам тип Maybe или его аналоги нужен для избавления от NullReferenceException.
При помощи подобных конструкций ответственность перекладывается на компилятор.

Автор забыл упомянуть, что все далее верно не только для Maybe, но и для любого другого контейнера (всевозможные коллекции). Это позволяет единообразно работать со всеми контейнерами.

В остальной части статьи описывается создание инструментов для удобной работы с Maybe. К сожалению не упомянуто, что чтобы все это имело смысл точно такие же инструменты должны существовать и для остальных контейнеров. Так же не упомянуты некоторые другие базовые инструменты, такие как перевод M<M> -> M.

К сожалению не показаны примеры использования, иллюстрирующие превосходство такого подхода над другими.
Код съелся:
M<M<T>> -> M<T>
В заключении я как раз упомянул про List<T>. В C# наличие инструментов для других сущностный особой пользы не принесет, так как они будут не связаны, как, опять же, я написал в заключении. Не увидел смысла упоминать join, помоем достаточно bind.
А какой пример бы привели вы на C#? Мне показалось, что раз я описываю проблему и ее решение, то человек уже сам при желании может сравнить как бы он решил ее старыми способами.
Под примером я подразумевал сравнение подходов.
Это савнение должно быть простым, но при этом достаточно жизненным.

Постараюсь воспроизвести для C# класический пример для scala с получением первой сестры прабабушки по отцовской линии:

Null:
public Person GetFathersMothersSister(Person person)
{
	if (person == null)
		return null;
	var father = person.GetFather();
	if (father == null)
		return null;
	var fathersMother = father.GetMother();
	if (fathersMother == null)
		return null;
	return fathersMother.GetSisters().FirstOrDefault();
}


Maybe:
public Maybe<Person> GetFathersMothersSister(Maybe<Person> person)
{
	return
		person
			.FlatMap(p => p.GetFather())
			.FlatMap(f => f.GetMother())
			.FlatMap(m => m.GetSisters().FirstMaybe());
}
Неужели вот такая запись:

f = length . filter (>3) . map (+1) . skip 3

считается логичной и читабельной?

Я это прочел как «взять некую длину (список чего-то?), отфильтровать, увеличить и пропустить», а не так, как вы это описали.
Ну, эта запись соответствует принятой в математике:
(f ∘ g)(x) = f(g(x))
=>
(length ∘ (filter (>3)) ∘ (map (+1)) ∘ (skip 3)) x = length((filter (>3))((map (+1))((skip 3)(x))))
Хотя мне больше нравится синтаксис OCaml/F# для этого:
Seq.length << (Seq.filter ((>) 3)) << (Seq.map ((+) 1)) << (Seq.skip 3)
или, эквивалентно:
(Seq.skip 3) >> (Seq.map ((+) 1)) >> (Seq.filter ((>) 3)) >> Seq.length
Дело привычки. На F# пишут так
skip 3 |> map (+1) |> filter (>3) |> length
Этот вариант наверное покажется более читабельным из-за того, что практически у всех современных языков операции над объектами идут через точку x.DoSomething()
Это pipelining, а не композиция. Его обычно используют так:
someList 
|> Seq.skip 3
|> Seq.map ((+) 1)
|> Seq.filter ((>) 3)
|> Seq.length

Разницу легко понять, если взглянуть на типы:
> (|>);;
val it : ('a -> ('a -> 'b) -> 'b)
> (>>);;
val it : (('a -> 'b) -> ('b -> 'c) -> 'a -> 'c)

Ну и да, на самом деле версия с композицией не скомпилируется, если дальше мы где-то не используем полученную функцию, из-за value restriction, но это уже совсем другая история...)
Да, подзабыл F#.
Наверное вам больше подойдет scala:
numbers.drop(3).map{_+1}.filter{_>3}.length
Примерно то же самое реализованов LINQ.

В примере автора все становится понятнее если заменить точки на пары скобочек.
Не валиднный, но более понятный код: length ( filter (>3, map (+1, skip 3 ))). То есть исходный код надо читать справа на лево.
НЛО прилетело и опубликовало эту надпись здесь
«для решения вполне реальных проблем, притом существующих не только в haskell»

Но мы вам про них не скажем. Зачем?

Вы не поняли описание проблем по ходу статьи?
Для людей, привыкших к null и считающих его естественным, Maybe выглядит просто как многословный аналог null.

Действительно очень сложно понять пока не начнешь этим постоянно пользоваться.

Тем, кто понимает, этастатья уже полезна не будет.
А остальным надо очень наглядно объяснить в чем проблема и почему это решение.
Поддерживаю. Вот я пока морально не готов изучить хаскель — из статьи понял только что сделали кривую эмуляцию «nullable data type», походя допустив несколько грубейших архитектурных ошибок О_О. Зачем? Почему? Кому все это надо?

А вот знал бы я хаскель — саразу бы все понял. Наверное.
Если хаскель для вас перебор, как, например, для меня, то возможно стоит обратить внимание на scala — там все проще, но при этом поддерживается полноценное ФП.
Да он не то чтобы перебор — у меня по работе мало алгоритмических задач, и тратить достаточно много времени на изучение теорикрафта, который заведомо не будет использоваться не очень хочется. Вот наберется естественным образом некая практическая база используемых подходов функционального программирования — тогда можно будет и изучить. Для подведения теоретической базы под практику. А без практики это будет всего лишь двадцать первый язык программирования который я «как бы знаю» и который мертвым грузом осядет в конспектах :).
Честно говоря — нет. Пришлось пойти по ссылке на другую статью и потратить минут 15, чтобы понять, что речь идет о полях-объектах, которые не инициализированы.

Дальше опять стало трудно понимать, поскольку неясно, что мешает проинициализировать эти поля.

Программирую на Delphi, не смог в голове построить аналог проблемы.

У подобного подхода есть множество плюсов.

Самый наглядный — это возможность наконец исправить «ошибку стоимостью в миллиард долларов». Полностью избавиться от самого понятия null.

При написании программы вы в любом месте знаете получите вы A или Maybe. И компилятор не позволит вам обращаться с Maybe как с A.

Другой плюс — это большая семантичность кода. Вместо получения кучи промежуточных значений, многие из которых не имеют самостоятельного смысла, вы пользуетесь копозицией функций. Вы избавляетесь от множества промежуточных точек выхода вида «if (a == null) return null». Одна точка выхода — это всегда лучше.

Ну и кроме прочего избавляясь от явного состояния мы становимся на шаг ближе к immutable подходу, что очень хорошо для многопоточного программирования, да и вообще для простоты понимания кода.
Я не очень понимаю, что значит «избавиться от null».

Если мне нужен объект, а вместо него вижу null, то надо бросать исключение.

Разве нет?

Нет.

Избавиться от null означает избавиться от ситуации «Если мне нужен объект, а вместо него вижу null».
Подробнее:

Null ипользуется для ситуации «нету». Как значение необязательного параметра функции, как не заданные данные, как отсутствие искомого в тех случаях, когда это не является ошибкой.

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

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

Или я чего-то не понимаю?
Уберите из Maybe метод Value и вы сможете получить доступ к содержимому только через безопасные методы вроде Map.

Метод Value — единственный опасный, но им пользоваться не надо.
И кто гарантирует, что есть все нужные мне методы?

Вот у меня есть метод (в интерфейсе, а значит, c неизменняемой сигнатурой) string -> string. С оговоркой, что если на входе null, то возвращаем null.

Как с помощью вашей реализации мне гарантировать такое поведение?
String -> String
это одно

а если с оговоркой то:
String -> Maybe String
или
Maybe String -> Maybe String
Нет-нет, именно string -> string. В терминах C#. Интерфейс именно такой, и именно фиксированный. Внутри можно заворачивать что угодно во что угодно, но внешний интерфейс менять нельзя.
Тут я не советчик.
Кроме haskell и erlang языков не знаю.
Вот беда в том, что пост-то на C# (и про C#-реализацию), а половина комментаторов рассуждает в терминах чистых функциональных языков.
Erlang далеко не чистый функциональный язык,

А для C# наверно речь идет про Nullable Type, как намекает msdn.
Неправильно намекает. Nullable of T — это другое.
Хотя при строгой типизации, сигнатура функции с оговорками мне вообще не понятна.

А зря, потому что контракт функции всегда сложнее, чем можно выразить в системе типов. Это порождает контракты и аннотации контрактов.
QuickCheck для haskell, но там не может быть типа с оговорками.
У erlang сложнее.

При этом haskell
позволяет спрототипировать приложение не написав ни одной реализации функции.

Классическая разработка от полного к частному.
Примеры можете посмотреть в ссылке выше.
Какой ссылке?
тут
Игра 15

Это называется «не написав ни одной реализации»?
Вначале описали типы данных.
потом написали названия функций и их типы (из каких типов какие получаем)
реализация функций undefined
когда по типам программа корректна, только тогда начали реализацию логики.

По сути, прототип корректной программы не содержал ни одной реализации логики.

Или что то не так?
Прототип — это когда что-то работает (не путать с дизайном).

(мы тут не рассматриваем UI-прототипирование, о нем речь не идет)
Пусть будет дизайн.
Только в примере мы получаем дизайн программы, и знаем что она будет работать.
Единственное что нам нужно, дописать реализацию функций с известной сигнатурой.

Ради интереса нарыл реализацию на java
подобие
На мой взгляд логику работы понять весьма проблематично.
Хотя каждому свое.
Только в примере мы получаем дизайн программы, и знаем что она будет работать.

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

И кстати всегда есть выбор:
Prelude> take 0 undefined [] Prelude> take 0 $! undefined *** Exception: Prelude.undefined
первый вариант — lazy
второй — strict
Сорри парсер.
Prelude> take 0 undefined [] 
Prelude> take 0 $! undefined 
*** Exception: Prelude.undefined 

первый вариант — lazy
второй — strict
Если программа проходит согласование по типам, она работоспособна при правильной реализации.

«Работоспособна» — это «компилируется и выполняется, не выдавая ошибок»? Это уже давно никого не интересует. Я считаю работоспособной ту программу, которая корректно реализует бизнес-требования. А тут, если мы говорим о хоть сколько-нибудь сложной программе, то есть ровно два варианта подобной ситуации.

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

Примеры нужны? (признаюсь честно, пример первого мне написать слабо)
Легко добиться того, чтобы программа компилировалась.
Сложнее добиться чтобы она без ошибок выполнялась.

Определенные подходы позволяют приблизиться к тому, что если программа без ошибок компилируется, то она без ошибок выполняется. Это очень упрощает жизнь.
В хаскеле данные подходы принудительны. В scala — рекомендуемы и легко выполнимы.

Корректно спроектировать типы проще ибо это декларативное описание. И компилятор тут проверяет корректность в определенной стапени.

Полностью зашить бизнеслогику в типы невозможно, но при различных подходах и инструментах к этому можно приблизиться в различной степени.
В хаскеле и других ФП языках это проще.
Ну касательно (a)
Если типы не корректно спроектированы
компилятор не пропустит.
Если корректно, но есть ошибки в логике, ну чудес не бывает, голова нам дана не орехи колоть.
насчет (b)
От логических ошибок страховки не бывает.
Если нужно делить на 5 а поделили на 4 это косяк программиста а не инструмента.
А если компилятор пропустил деление на ноль, и об этом узнали после релиза,
я склонен считать что с инструментом есть некие проблемы.
Вообще серебряной пули не бывает.
Каждый использует наиболее удобный для себя инструмент.
Кому то удобно плодить if else
кому то использовать Maybe.

Кто то не может осилить haskell, кто то java.
Дело не в инструменте, дело в головах и руках.
А если компилятор пропустил деление на ноль, и об этом узнали после релиза, я склонен считать что с инструментом есть некие проблемы.

Видимо, слово «тестирование» успешно забыто.

Не говоря уже о том, что я что-то не знаю компиляторов (разве что Eiffel), которые защищают от ошибок деления на ноль в случае деления на внешний аргумент.
Это должно быть на уровне идеологии.
То есть если метод возвращает string, то этот string не null. Иначе он должен возвращать Maybe.

В хаскеле это поддерживается полноценно (там вообще нет null).
В scala все почти везде поддерживается. Проблемы только в Java-библиотеках, но над ними пишут обертки либо просто оборачивают специальной функцией каждый получаемый результат.
В котлине пошли еще дальше и там все, что пришло из java автоматически обернуто если нет атрибута NotNull.
Ну то есть никак.

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

В частности, к монадам относятся:
IO (монада строго последовательных вычислений): стратегия связывания — «сначала первое вычисление, затем второе»;

По сути:
любой код на любом языке програмирования как минимум использует монаду IO (в терминах haskell)

Смеяться можно всем миром.
Мне это напоминает «everything is an object».

Ну да, если у вас есть молоток…
) Ну да.
только роль объекта играет лямбда.
Через нее реализуются все остальные кирпичики программ в функциональной парадигме.

А если вы хотите использовать сопоставление с образцом (которого в C# нет), то там, где оно есть, обычно есть и проверка на то, что все возможности учтены.
Для тех кто не знаком с C#, ключевое слово this позволяет использовать функцию через точку после объекта

Дальше даже читать не стал. Это настолько неграмотно с точки зрения C#, что слов нет.

Ну и да, при наличии работающего <a href=«nuget.org/packages/Monads>Monads это все ни о чем.

Maybe discount = f.Curry().Pure().Apply(userId).Apply(productId);
Извините, но ничего декларативного в этом коде нет. Он вообще не показывает бизнес-намерение кода.
Для любителей практики, абстрактный вебсервис

Без Maybe:

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

если корректно -> смотрим урл -> если нет обработчика выход

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

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

если результат есть -> рисуем и отправляем ответ

Без Maybe куча if then else

C Maybe:
получить запрос >>= 
        проверить корректность >>= 
        проверить url >>= 
        проверить пользователя >>= 
        выбрать результат из базы >>= 
        нарисовать и отправить ответ



если на каком нибудь этапе, обработка провалится, результат будет Nothing или кому более привычный null
если на каком нибудь этапе, обработка провалится, результат будет Nothing или кому более привычный null

А должен быть exception (в абстрактном вебсервисе). Потому что fail early.
Exception в многопоточном коде не всегда удобен.
Здесь тоже выполнение прерывается на первой же неудачной операции, но менее многословно и позволяет вместо броска исключения вернуть результат неудачной проверки, например.
Exception в многопоточном коде не всегда удобен.

Абстрактный веб-сервис однопоточен (точнее, каждый его вызов).

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

А зачем это делать тем же способом, которым возвращается результат удачного выполнения? Честное слово, исключения придумали не просто так.
Зучем придумали исключения — отдельный вопрос. Но логику работы программы они ломают. И на исклбючениях строить бизнеслогику — зло.

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

Каким образом?

И на исклбючениях строить бизнеслогику — зло.

Это не бизнес-логика. Бизнес-логика в вашем примере в двух последних шагах (найти данные и вернуть результат). Все, что до этого — не бизнес.

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

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

Против самой монады Maybe я ничего не имею, я и сам ее радостно использую.
Nothing это и есть exception для чистых вычислений (для данного примера).

В реальном коде там еще будет State монада, чтоб получить правильный код ответа сервера (404, 503 и тд)
Но при этом нарисованный код останется таким же.
Nothing это и есть exception для чистых вычислений (для данного примера).

А как отличать «не вернули результат» от «ошибка»?

(если что, null — это именно «нет результата»)
Храним состояние в State монаде, и ответ берем оттуда.
А зачем изобретать велосипеды? Особенно в веб-сервисах, где давно все придумано?
Это не велосипеды, это функциональная парадигма.
И веб на ней также окучивается как на императивных языках, и зачастую сильно проще.

И справка из википедии:
Lisp — 1958
Erlang — 1986
ML — 1979
Haskell — в конце 80
Если нет желания использовать State и Maybe,
можно взять Data.Either
data Either a b = Left a | Right b

Это тоже монада и вычисления связываются точно также.
Только если будет ошибка она у вас будет Left «что случилось»
если все ок, то Right ответ сервера

То есть пример становится уже другим.
Кстати нет.
Типы данных используются другие.
И внутренняя реализация.
А пример остается валидным.
У меня просто есть устойчивое ощущение, что для этой монады придется писать другие реализации в пайплайне.
Как раз нет.
С непривычки это может запутать еще сильнее, но обычно стараются писать функции, работающие с монадами так, чтобы работа с различными монадами была как можно более однородна.
я подозреваю, что none, в каком бы месте он не случился, пролетит оставшуюся часть пайплайна крайне быстро, так что вряд ли это как-то скажется.
Только вот вызывающий код не поймет, что произошло.
это да, но думаю, для этого уже тоже придумали какую-нибудь монаду))
А нафига? Чем Exception-то не угодил?
Мне кажется, это вопрос отношения к exception. Если рассматривать его возникновение как нормальный flow для программы, то все в порядке, но многие, в том числе и «классики», считают, что exception как следует из названия, ситуация исключительная, и исключения не должны применяться в целях обеспечения обычной работы приложения. Не могу сказать, что один из подходов однозначно лучше, но мне больше по душе второй.
Понятие «исключительно» и «нормально» — относительны. И я склонен считать, что для абстрактного вебсервиса, отдающего данные, ошибка авторизации пользователя — исключительная ситуация (как бы часто она не проиходила).
Как раз в случае с ФП без броска исключения мало того, что вызывающий код гарантировано поймет что произошло, так еще и компилятор позаботится чтоб вызывающий код не забыл обработать все возможные результаты, а не только валидный. Ну хотя бы просто переправиви невалидный выше.
Потребителем «абстрактного веб-сервиса» не обязательно является функциональная программа.
И этой абстрактной программе проще будет обработать Either или аналогичный ему тип, чем принять исключение.
Вы в курсе существования WS-Fault?
И потом первая залетевшая ворона разрушит цивилизацию. ))
Exception должен быть только в том случае, если мы используем исключения для обработки ожидаемых ошибок (неавторизованность пользователя — это ведь ожидаемая ошибка?). Учитывая, что никто из популярных языков программирования / фреймворков так не делает — в общем случае, на практике, нам будет очень тяжко — придется каждый метод проверять — а действительно ли он в случае ожидаемой ошибки кинет исключение нужного типа? Не получится ли, что мы там вместо «not authorized» словим «out of memory» и продолжим работу, вместо того чтобы упасть?
если мы используем исключения для обработки ожидаемых ошибок (неавторизованность пользователя — это ведь ожидаемая ошибка?)

Внутри метода «получи данные»? Нет, не ожидаемая. Мы предполагаем, что пользователь пришел с правильным контекстом.

а действительно ли он в случае ожидаемой ошибки кинет исключение нужного типа?

А альтернатива какая? Получать null и пытаться угадать, там нет данных, или у нас не авторизовался пользователь?
Внутри метода «получи данные»? Нет, не ожидаемая. Мы предполагаем, что пользователь пришел с правильным контекстом.


В таком случае нам не нужно в конце получать null и продолжать работу — нам нужно падать с логом в тот момент, как оказалось что пользователь пришел с неправильным контекстом. Если мы ожидаем что контекст правильный, а контекст неправильный — значит у нас бага в логике программы и дальше мы работать не хотим, надо ползти к разработчику и ремонтироваться? Ну или перезапускаться :).

А альтернатива какая? Получать null и пытаться угадать, там нет данных, или у нас не авторизовался пользователь?


Альтернатива чему?
В таком случае нам не нужно в конце получать null и продолжать работу — нам нужно падать с логом в тот момент, как оказалось что пользователь пришел с неправильным контекстом.

Именно. И самый простой способ для этого — exception (ну да, а еще это неплохо бы сделать аспектами, если это возможно, но это тема другого разговора).

Альтернатива чему?

Тройке «результат — null если данных нет (ожидаемая ситуация) — exception если нарушен контракт (неверная авторизация, нет обработчика, что-то еще)».
Да, вообщем-то, нет никакой альтернативы ^_^. Все так делают последние лет… много. И вроде как все работает. У меня, по крайней мере :).
Или в точечной нотации:

вебсервис = нарисовать и отправить ответ . выбрать результат из базы . проверить пользователя . проверить url . проверить корректность
И вы считаете, что это читаемый код? И давно у нас текст читается с конца?
Кому-то привычно.
Если вам непривычно читать с конца, то вам будет удобнее пользоваться другими ФП языками, например scala. Суть подхода от этого не меняется.
Это математическая нотация.
Не нравится можете использовать другой синтаксис.
В некоторых случаях такая запись очень удобна.
Возможно, но, имхо, когда речь идет именно о последовательности действий (как в вашем примере), то читать/писать логичнее слева направо, как собственно и будет записываться этот код. Сомневаюсь что лишняя инверсия-другая в голове дает какой-то профит.
НЛО прилетело и опубликовало эту надпись здесь
С точки зрения задачи (алгоритма) это именно последовательность.
НЛО прилетело и опубликовало эту надпись здесь
null в C# был добавлен в спешке. Я видел интервью где главный по C# говорит что вместо null надо было бы сделать чтобы Nullable<> работал и для reference-типов. И чтобы по-умолчанию все reference-типы были бы не-Nullable. Имели бы кучу плюшек от этого. И тогда LINQ можно было бы реализовать для Nullable, получив аналог монады Maybe с нормальным синтаксисом.

Эх… Если бы, да кабы…
null вообще был «добавлен в спешке». billion-dollar mistake

А C# с первой версии развился очень сильно и изначально там не было ни намека на LINQ. Там и лямбд-то не было. Там даже генериков не было изначально — откуда там взяться Nullable<>.

Разработчики языка проделали с первой версии огромную и весьма хорошую работу. Но отказ от null уже не возможен, к сожалению.
Для Nullable<> нужны были только дженерики. Без остального прожить было можно, а новые фичи потом удачно бы дополнили работу с Nullable<>. Но дженерики они не успевали. Как по мне — не стоило релизить первую версию C# вообще, слишком уж многое от этого печально в C#.

Да, теперь уже не поправить.

Я вообще это все к тому, что Maybe монада — неудачный пример в контексте .net-а, ее просто не сделать нормально поверх этого зоопарка.
Отличная статья, впервые на русском читаю так грамотно изложенное понимание того зачем все эти Монады, Стрелки и тп нужны. Браво автору!
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории