Страницы

среда, 30 июля 2014 г.

Контракты vs. Монады?

DISCLAIMER: чтобы было легче понять, о чем пойдет речь в этой заметке, стоит прочитать заметку "Борьба с "нулевыми" ссылками в C#".

Q: Если бы в BCL был тип Optional<T> или Nullable<T> для ссылочных типов, ты бы его использовал в своем коде?

A: Да, конечно!

Q: Значит, тогда ты бы отказался и от контрактов?

A: …

Примерно такой диалог произошел у меня после прошедшей вчера встречи Kiev ALT.NET, посвященной вопросам борьбы с нулевыми ссылками в C# (вот презентация с выступления, а вот – код).

У этого вопроса есть один простой ответ, и звучит он так: «Нет, я не откажусь от контрактов!», но поскольку подобная форма ответа не слишком информативна, то я готов объяснить свою точку зрения более подробно.

Три уровня обороны

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

  1. Система типов
  2. Контракты
  3. Юнит-тесты

Чем "выше" линия обороны, тем раньше она срабатывает и "докладывает" нам о возможной проблеме в коде. Если мы что-то можем выразить с помощью системы типов, то именно с ее помощью мы должны выражать свои намерения. Именно поэтому в С++ обязательные входные значения передаются по константной ссылке, необязательные входные значения – по константному указателю и т.п.

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

API класса/метода должен быть таким, чтобы его легко было пользовать правильно и сложно использовать неправильно!

Но что, если мы не можем выразить что-то через систему типов (ибо она недостаточно выразительна)? Например, в языке C# мы не можем выразить, что аргумент метода ссылочного типа является обязательным, поскольку любой ссылочный тип может принимать значение null. Иногда, когда это возможно, мы можем заменить его на тип-значение (value type) и уйти от проблемы, но далеко не всегда это возможно (ну и вы же помните о проблеме изменяемых значимых типах, правда?)

Когда система типов бессильна, нам на помощь приходят контракты: инструмент, который помогает выразить наши намерения в более явном виде. Как выразить с помощью системы типов, что метод Add интерфейса ICollection<T> может не добавлять еще один элемент, а метод Add любой реализации класса IList<T> обязана это сделать? (Помните обсуждение этого вопроса в статье "Принцип замещения Лисков и контракты"?).

Контракт располагается в самом методе и формально является частью его "сигнатуры", что делает его (метод) более описательным. При этом некоторые нарушения контрактов могут детектиться на этапе компиляции (с помощью статик-чекера, или инструментов, таких как R#), а некоторые – на этапе исполнения.

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

Контракты по своей природе тоже не могут выразить все тонкости ожидаемого поведения. Когда их выразительности становится недостаточно, нам на помощь приходит следующая линия обороны – юнит-тесты. Так, например, в контрактах невозможно выразить, что третьим числом Фибоначчи будет 2, или что при вызове метода Save вью-модели будет вызван метод SaveEmployee нашего сервиса.

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

ПРИМЕЧАНИЕ
Более детальное сравнение контрактов и юнит-тестов можно найти в статье "Контракты, состояние и юнит-тесты".

А как насчет Option<T>?

Чем полезен тип Option<T>, Maybe<T> или Nullable<T>, в качестве обертки над ссылочными типами?

Он полезен тем, что с его помощью мы выражаем четкие намерения метода вернуть (или получить) необязательное значение. Дальше уже второй вопрос, как его обработать, с помощью модного "монадического" синтаксиса, с помощью "сопоставления с образцом" (a.k.a. pattern-matching-а) или с помощью простого if-else).

Хорошо. Но какую проблему решают эти типы, со всеми этими Bind-ам, Apply-ами и Return-ами? (это я про составляющие паттерна "монада").

ПРИМЕЧАНИЕ
Если вас пугает это страшное слово, то зря. Просто посвятите сегодня один вечер, и прочтите отличное введение по этой теме от Эрика Липперта – Monads, part one (и дальше по ссылкам!).

Напомню, что у нас есть две задачи: во-первых, мы хотим упростить control flow и избавиться от множества вложенных проверок на null, поскольку не хотим видеть код вроде этого:

[CanBeNull]
public static IClrTypeName GetCallSiteTypeNaive(IInvocationExpression
invocationExpression)
{
   
Contract.Requires(invocationExpression != null
);
   
if (invocationExpression.InvokedExpression != null
)
    {
       
var referenceExpression = invocationExpression.InvokedExpression as IReferenceExpression
;
       
if (referenceExpression != null
)
        {
           
var resolved = referenceExpression.Reference.
Resolve();
           
if (resolved.DeclaredElement != null
)
            {
               
var declared = resolved.DeclaredElement as IClrDeclaredElement
;
               
if (declared != null
)
                {
                   
var containingType = declared.
GetContainingType();
                   
if (containingType != null
)
                    {
                       
return containingType.
GetClrName();
                    }
                }
            }
        }
    }
   
return null;
}

Нужно сказать, что подобные примеры не должны встречаться слишком часто, ведь такой код полностью нарушает закон Деметры. Но поскольку в реальном мире такой API все же встречается, поэтому мы хотим как-то уменьшить боль, и избавиться от вложенных if-statmeents. Один из способов решения, воспользоваться классом Option<T> или методами расширения, рассмотренными ранее:

[CanBeNull]
public static IClrTypeName GetCallSiteType(this IInvocationExpression
invocationExpression)
{
   
Contract.Requires(invocationExpression != null
);

   
var type =
invocationExpression
       
.With(x => x.
InvokedExpression)
       
.With(x => x as IReferenceExpression
)
       
.With(x => x.
Reference)
       
.With(x => x.
Resolve())
       
.With(x => x.
DeclaredElement)
       
.With(x => x as IClrDeclaredElement
)
       
.With(x => x.
GetContainingType())
       
.Return(x => x.
GetClrName());
   
return type;
}

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

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

Давайте рассмотрим такой пример. Допустим, нам нужно найти сотрудника по имени:

Option<Employee> FindByName(string name) {...}

Помогает ли Option<T> в этом случае? Частично. С возвращаемым значением код становится более самоописательным, но как мы можем выразить "обязательность" аргумента? Есть мнение, что в случае передачи в качестве имени null мы должны вернуть None, но насколько это разумно? Как нам понять, что означает этот None? Отсутствие такого сотрудника с заданным именем или баг в коде, который подсунул невалидные входные данные?

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

Давайте я приведу другой пример. Насколько будет разумным, если метод Enumerable.FirstOrDefault возвращал бы null, при вызове на null-коллекции, или при передаче null в качестве предиката?

IEnumerable<Employee> sequence = null;
var result = sequence.FirstOrDefault((Func<Employee, bool>)null);
Debug.Assert(result == null);
// Hello, rude world!
Очевидно, что этот код неверный, и было бы печально, если бы реализация метода FirstOrDefault прятала бы ошибку, путем возвращения null при передаче этому методу ошибочных входных данных. (Кстати, обратите внимание, что при замене возвращаемого значения с Employee на Option<Employee> наша логика не изменится. null не является валидным входным значением, поэтому нельзя возвращать None в этом случае).

Наши инструменты не должны скрывать ошибки, а должны следовать идиоме Fail Fast и бросать исключения, или прерывать исполнение как можно раньше (помните, нарушение контрактов может вести себя аналогично нарушению Debug.Assert!).

Почему нужно следовать идиоме Fail Fast? Чтобы избавиться от того самого эффекта бабочки, когда изменение в одной части системы проявится на несколько слоев ниже, или на несколько модулей правее, от места возникновения ошибки! (Мы просто будем сидеть и думать, а какого лешего при обработке этого запроса мы сохранили null в этом поле базы данных?)

Контракты в ФП мире

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

clip_image002

И звучит она так: любое выполнение команды С начинается в состоянии, где утверждение P истинно, завершится и в заключительном состоянии будет истинным утверждение Q. При этом C – это операция (или команда), P – предусловие, а Q – постусловие.

ПРИМЕЧАНИЕ
Подробности см. в первой статье по контрактам: Проектирование по контракту. Корректность ПО.

Звучит мудрено, но по сути, все довольно просто. Давайте в качестве примера рассмотрим метод Enumerable.Single:

public static class Enumerble
{
   
public static T Single<T>(this IEnumerable<T> source,
Func<T, bool> predicate)
    { }
}

В данном случае P (предусловие): source != null, predicate != null, source.Contains(predicate).
С (команда): Enumerable.Single
Q (постусловие): элемент из source, удовлетворяющий предикату!

Т.е. если мы вызовем метод Single на коллекции, не равной null, с предикатом не равным null, то метод вернет значение из последовательности, удовлетворяющему этому предикату. При этом искомый элемент обязательно должны быть в коллекции, в противном случае прилетит исключение InvalidOperationException (так что по сути, тип возвращаемого значения является не T, а non-nullable T).

Если же предусловие P не выполняется, то никакие обязательства на метод С не возлагаются, и он перестает быть должен вызывающему коду хоть что-то.

Обратите внимание, что тройка Хоара вообще никакого отношения к ООП не имеет. Более того, весь LINQ – это воплощение функционального стиля программирования, описать контракты которого существенно проще, чем, например, для ОО-классов коллекций.

В недавнем интервью Бертран Мейер как раз подчеркивал важность ФП-техник для обеспечения корректности кода. Посудите сами, какой контракт проще проанализировать: контракт метода с массой побочных эффектов или контракт ФП-метода без побочных эффектов? Контракт чистой функции очевиден: все что идет ей на вход является частью предусловия, а постусловие выражается в качестве возвращаемого значения! Контракт экземплярного метода с четырьмя изменяемыми полями потенциально содержит 4 входных аргумента и 4 выходных аргумента.

И в этом же плане неизменяемые типы являются отличным инструментом любого ОО разработчика. Контракт неизменяемого типа проще, поскольку единственным местом проверки контракта является конструктор. При этом предусловие конструктора автоматом становится инвариантом класса, ведь все, что истинно в конструкторе останется истинным на протяжении всего времени жизни объекта!

Так что насчет Контракты vs. Монады?

Да, кажется я несколько отвлекся от исходной темы. Так вот, проясню в качестве заключения: всяческие монады, и методы расширения для упрощения обхода графов объектов упрощают control flow, но они не имеют никакого отношения к тому, что делают контракты!

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

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

Дополнительные ссылки

З.Ы. Понравился пост? Поделись с друзьями! Вам не сложно, а мне приятно;)

39 комментариев:

  1. Дякую Сергію, повністю згоден. Класна стаття і виступ вчора!

    ОтветитьУдалить
    Ответы
    1. Олексей, спасибо! Ждем оппонентов))

      Удалить
    2. Так а чему тут оппонировать? Польностью согласен :) Будешь в Редмонде, попроси их, пожалуйста, сделать Contracts побыстрее и я с него не слазить не буду ;)

      Удалить
    3. "Будешь в Редмонде, попроси их, пожалуйста, сделать Contracts побыстрее и я с него не слазить не буду ;)"

      ;-) LOL простите - не мог пройти мимо :-)

      Удалить
    4. Женя, судя по чатику Kiev ALT.NET-а, не все согласны с нашей с тобой позицией:)

      > Будешь в Редмонде, попроси их, пожалуйста, сделать Contracts побыстрее и я с него не слазить не буду ;)

      Обязательно займусь этим.

      Если серьезно, то есть мысль пропихнуть non-nullable reference типы на уровне IDE. А вдруг выйдет:)

      Удалить
  2. Для того, что бы имело смысл учить новые инструменты, которые ту тут пропагандируешь, нужно разрабатывать достаточно сложные системы. В таких случаях оправданно изучить чуть больш чем нужно для решения текущей задачи. Или нужно платить за разработку и поддержку продукта самому, тогда, внезапно, формальное обоснование подходов позволяет экономить приличную копейку.
    Мне видиться, что нужно работать над расширением аудитории, нужно показать, что мы выигрываем он того, что разобрались с понятием "Система типов", "Контракты", "Строгая семантика", а сейчас самый частый вопрос "а зачем всё это наворачивать?". Зарплату платят не монадически :)

    ОтветитьУдалить
    Ответы
    1. Гриша, если ты это ремарку сделал из-за моего вчерашнего "наезда", то я тебя расстрою - я прекрасно понимаю, что ты вчера пытался объяснить. Что пюр фанкшены это круто, и как просто понимать код, когда всё зависимости локализованы и нет глобального доступа и побочныъ эффектов, и всё такоё - просто пример был не "ахти". Я не знаю, знаком ли ты с CQRS, но я уже года 4-ре, разрабатываю в этом стиле, что позволяет мне использовать все прелести ФП без впадания в cargo cult, и при этом одновременно использовать все сильные стороны ООП. Оченб рекомендую ознакомиться, ибо то, что ты показывал очень схоже на недоделанный CQRS по Грегу Янгу.

      Удалить
    2. В отсуствие изменяемого состояниея, выгода от применения CQRS от меня ускользает. Да и как его можно применить если единственный способ изменить значение это вернуть его.
      А пример мой нужен был для того, что бы показать, что объектное поведение это не обязательно экземпляр класса с набором методов и изменяемых полей, а вполне достаточно нескольких чистых функций.

      Удалить
    3. Гриша, по поводу исходного коммента:
      > Мне видиться, что нужно работать над расширением аудитории, нужно показать, что мы выигрываем он того, что разобрались с понятием "Система типов", "Контракты", "Строгая семантика", а сейчас самый частый вопрос "а зачем всё это наворачивать?". Зарплату платят не монадически :)

      Да, такая проблема и правда есть, причем не только ведь у нас. Эта проблема есть во всех софтовых конторах по всему миру. Даже в МС-е, Гугле, Фейсбуке средний уровень команд не вполне дотягивает до всего этого (именно средний), не говоря уже за наш аутсорс или Нью Йоркскую банковскую сферу, где на качество кода вообще кладется огромный болт.

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

      Удалить
    4. И мне странно, что их не тянут. Код написанный разработчиком который понимает зачем соблюдать семантику написания, и максимольно выносит код в чистые функции поддерживать гораздо дешевле. И на мой взгляд именно компании которые в итоге владеют кодом заинтереосваны в практиках которые удешевляют поддержку продукта.
      Разработчику не платят за самосовершенствовани, а значит мотивация есть только у самых непрактичных разработчиков :) Можно ведь было провести вечер с пивом, а не в циклуме :)
      Если копнуть дальше, то компании аутсорсеру тоже плевать на лёгкость поддерживания кода. Качественный код нужен только заказчику, но он как правило об этом не знает и оценить качество не может, а потому и не платит за него.
      Вот если появится оценка качества кода и это станет критерием при выборе подрядчика то сразу появиться спрос на людей понимающих, что система типов это тоже язык программирования, контракты нужно описывать вне функции, а отсутвие контроля за соблюдением семантики со стороны языка это не повод писать как угодно.

      Удалить
  3. Кстати, тройка Хоара будет еще проще, если всегда следовать принципу "the object should be always in a valid state" из DDD. Там получается следующее:

    1) Проверка, что можем перейти в следующее (валидное) состояниее. Прекондишен (или Guard Clause)
    2) Собственно реакция на команду - изменение состояния.
    3) Объект переходит в следующее валидное (читай детерминированное) состояние. Постусловие

    То есть становится практически двойкой :)

    P.S. А не валидные состояния - моделируются специальным образом. Не правда, ли, очень похоже на то, что предлагает тот самый Скотт Влашин ;) https://vimeo.com/97507575

    ОтветитьУдалить
    Ответы
    1. Правда, Скотт несколько уходит в фанатизм и начинает моделировать динамическое различие в поведение через статические типы, что приводят лишь к Type Explosion и замусориванию Ubiquitous Language :( Так что тут главное не перестараться ...

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

      Вот, например, цитата из первой статьи Скотта:

      > This will deliberately be a very simple data-flow oriented recipe with no special tricks or advanced techniques. But if you are just getting started, I think it is useful to have some straightforward steps you can follow to get a predictable result. I don't claim that this is the one true way of doing this. Different scenarios will need different recipes, and of course, as you get more expert, you may find this recipe too simplistic and limited.

      А это значит две вещи: во-первых, пример рассмотрен для определенного типа приложения, а во-вторых, Скотт не отрицает других подходов.

      Удалить
  4. "Контракт неизменяемого типа проще, поскольку единственным местом проверки контракта является конструктор"

    В общем случае это не так:
    1. у неизменяемого типа могут быть еще и методы (понятно, что эти методы никак не изменяют состояние экземпляра типа, в котором они объявлены), принимающие входные параметры (привет, предусловия);
    2. постусловия никуда не делись.

    Так что эту фразу я бы точно изменил, чтобы никого не вводить в заблуждение :)

    ОтветитьУдалить
    Ответы
    1. Владимир, для неизменяемого класса любому методу достаточно добавить лишь одно постусловие: Contract.Ensures(Contract.Result() != null); и все.
      Остальные кейсы будут покрыты предусловием контракта. Иногда постусловие может быть строже, но в большинстве случаев достаточно будет использовать предусловие конструктора в качестве постусловие метода-"мутатора".

      Удалить
  5. Общая идея, поданная в статье выглядит правильно, однако есть существенное недопонимание того, зачем нужны монады.

    Цель монад - контроль за эффектами. Монада - это аппликативный полиморфный контейнер, оборачивающий не ссылочные типы, а вычисление. Результатом использования монад является pure код, который существенно легче отлаживать и поддерживать.

    Вторым поинтом, который непременно стоит отметить это то, что исключение нарушает referential transparency. Соответственно, то, что будет выброшено как failure case of Contract.Requires, является нарушением RT. Это является очень серьезным поводом предпочитать проверку корректности за счет системы типов использованию контрактов.

    В заключение отмечу: несмотря на то, что контракты и монады нельзя сравнивать, на мой взгляд, они состоят в разных "лагерях подходов" к разработке. Их нельзя назвать противоборствующими, равно как и FP vs OOP (думаю почти все согласятся, что такая постановка вопроса абсолютно бессмысленна).

    ОтветитьУдалить
    Ответы
    1. Всеволод, спасибо за комментарий.
      Один вопрос: вы говорите за монады в общем случае или в контексте конкретного *нефункционального* языка программирования? ;)

      Удалить
    2. А что вы подразумеваете под "нефункциональным" языком программирования?

      Удалить
    3. Отвечать вопросом на вопрос не есть гуд:)
      Но я говорю за C#, поскольку все обсуждение в статье ведется не в общем случае, а именно в контексте языка C#, и только.

      Удалить
    4. Если честно, у меня нет цели "непременно доказать свою правоту", стремление быть всегда правым есть признаком вульгарности ума, как говорил дедушка Камю. Моя цель - интересная дискуссия. Поэтому мой встречный вопрос был уточняющим, дабы убедится, что у нас одинаковое понимание "функционального языка".

      Отвечая на ваш вопрос, скажу, что монады как вычислительная абстракция работает на C# также, как и "в общем случае"

      Удалить
    5. Ок, я просто не был уверен в контексте исходного вопроса. А интересно подискутировать я готов.

      Итак, мы говорим, не о сферических конях, а об использовании всего этого добра в конкретном языке - в C#.

      Тогда вот несколько моментов:

      1. Как мы можем обеспечить pure код в не pure языке, как C#? Ведь мы не знаем, является ли лямбда pure или нет. Так что о какой referencial transparency мы можем говорить? Как мы гарантируем, что из-за бага или неосторожности не будет вызван метод с дополнительными побочными эффектами?
      Тот же F# тоже не pure language, но там нужны дополнительные усилия, чтобы нарушить чистоту, а в C# нужны очень серьезные усилия, чтобы ее обеспечить!

      2. Что делать с non-nullable типами и как их обеспечить в языке C#? Забить на них, и на null возвращать None?

      Удалить
    6. 1. Вне всяких сомнений, этот вопрос очень актуален. Приведу несколько поинтов для дальнейшей дискуссии:
      - до тех пор, пока чистота не обеспечивается компилятором, ни один язык не может ее гарантировать
      - мы никак не можем гарантировать, что из-за бага или неосторожности, не будет вызван метод с дополнительными сайд-эффектами.

      Отвечая на ваш первый вопрос - это сугубо вопрос дизайна вашего приложения. Вы можете использовать или не использовать глобальный стейт, вы можете писать или не писать чистые функции. Монады - это не более чем уровень абстракции над вашими вычислениями. Если вы хотите использовать монады, вам скорее всего придется исповедовать определенный стиль программирования.
      Основной изъян в логике выглядит так: берется язык программирования, который заявлен как гибридный, и на котором уже десять лет пишут в императивном стиле. В качестве примеров берется код, написанный в императивном стиле и к нему пытаются применить элементы функционального дизайна. О Боги - это не работает! А значит на языке невозможно использовать функциональные абстракции? Really?

      А что если действовать иначе? Что если взять задачу и реализовать ее без использования глобального состояния? Используя чистые функции там, где это возможно? Возможно ли такое на C#? Более чем. Создаст ли это дополнительные трудности? Нет, такой код легче читать, его значительно легче отлаживать, особенно в многопоточной среде.

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

      Резюмируя скажу - любой инструмент нужно правильно использовать. Монады в императивном дизайне годятся разве что вложенность сокращать.

      2. Отвечая на второй вопрос - зачем вам их обеспечивать? Ваше основное опасение, насколько я понимаю, выглядит так - SomeMethod[T](null) : Maybe[T]? Встречный вопрос - а в рамках функционального дизайна приложения - как вообще вы можете получить null в параметр?

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

      Удалить
    7. Небольшое исправление - *что если вместо maybe передадут null*

      Удалить

    8. Всеволод, давайте разделим обсуждение на два. Я начну со второго.
      Мир не идеален и я хочу бороться с багами в коде. Для этого я использую контракты, при этом генерируется исключение, причем как можно раньше.
      Вы можете привести код того, как именно будет генерироваться исключение при передаче null в монаду? Будет ли это if (arg == null) throw new ArgumentNullException()? Или какая-то иная форма проверки ... внимание ... предусловия!

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

      Вот и выходит, что контракты в C# очень даже нормально используются с подобными "монадическими" расширениями или классами, типа Option.

      Но

      Удалить
    9. В пред. комменте завершающее "Но" лишнее. И я отвечу на ваш вопрос о том, как может прилететь null: null может прилететь в случае ошибки, от которых мы хотим избавиться.

      Удалить
    10. Давайте :)

      Наверное проблема в том, что мои аргументы недостаточно очевидны. Мы с вами согласны в том, что монады и контракты - это вещи не одного порядка и скорее всего сравнивать их сложно. Весь мой предыдущий комментарий я говорил о том, что монады более чем применимы в C# и вопрос не в самих монадах, а в подходах, которых вы придерживаетесь, когда пишете ваш код, а монады - это лишь инструмент абстракции одного из них, который нынче принято называть функциональным. Вы боретесь с ошибками в коде с помощью контрактов. Супер. Однако исключения нарушают RT. Вы прекрасно можете жить и без этого, однако, я думаю очевидным является тот факт, что придерживаться RT = упростить себе жизнь. Почему бы вам не обернуть вычисление, внутри которого вы верифицируете ваши параметры в Either или Try? В результате contract failure вы получите левую проекцию с meaningful payload, который впоследствии обработаете так, как вам это нужно.

      Вы очень широко употребляете термин "ошибка". Давайте уточним причину - у вас есть совершенно конкретный метод someMethod(SomeType t). Приведите конкрентый пример, почему в параметр может прилететь null? Понимаю что их сотни - приведите любой, который вам по душе.


      Удалить
    11. Причина по которой прилетит null - простая: баг в коде:

      var st = GetSomeType();
      someMethod(st); //Нет проверки, что st - не null

      SomeTime GetSomeType()
      {
      // много сложных букав с nullable реф. типами, в результате чего
      return null;
      }

      Удалить
    12. Отлично, а почему бы вам не сделать Maybe GetSomeType() - и внутри не использовать монаду мейби в явном виде или monadic comprehension?

      Удалить
    13. Потому что GetSomeType() должен вертать non-nullable reference type, но проблема в том, что в системе типов языка C# я этого выразить не могу.

      Удалить
    14. Почему? Почему нельзя обернуть результирующее значение в контейнер?

      Удалить
    15. В какой? Добавить еще и NotNullable?

      Удалить
    16. Вопрос выглядит следующим образом: почему нельзя сделать
      Maybe[SomeType] GetSomeType() вместо SomeType GetSomeType?

      Удалить
    17. Всеволод, мне нужен метод, который не может вернуть null.
      Maybe[SomeType] GetSomeType() говорит о прямопротивоположном.

      А почему я хочу именно non-nullable? потому что этого я хочу в подавляющем большинстве случаев. Еще раз повторю: я хочу смоделировать non-nullable поля, возвращаемые значения и аргумены, поскольку необязательные поля, возвращаемые значения и аргументы нужны существенно реже.

      Удалить
    18. Сергей, похоже мы говорим о принцпиально разных вещах :)

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

      Спасибо за любопытный диспут :)

      Удалить
    19. Всеволод, спасибо.

      Хотя я так и не понял, как мне выразить non-nullable reference тип с помощью монад.

      Удалить
  6. Этот комментарий был удален автором.

    ОтветитьУдалить
  7. Сергей, а вы используете код https://gist.github.com/SergeyTeplyakov/a1fbd8b2bb192009b650 в продуктовом коде? Или это эксперименты, которые в прод не идут?

    ОтветитьУдалить
    Ответы
    1. Александр, я привел этот в качестве троллизма подхода с использованием NoThrow, так что ничего такого в продуктовом коде нет. В своем R# плагине я использую методы расширения (implicit monadic types) + контракты.

      Удалить