UPDATE: интересно продолжение этой статьи? Читайте: “Контракты vs. Монады?”.
Вступление
В моих черновиках уже больше года лежит статья, в которой я хотел рассказать о проблеме разыменовывания пустых ссылок (null reference dereferencing), с подходами в разных языках и платформах. Но поскольку у меня все никак не доходили руки, а в комментариях к прошлой статье ("Интервью с Бертраном Мейером") была затронута эта тема в контексте языка C#, то я решил к ней все-таки вернуться. Пусть получилось не столь фундаментально как я хотел изначально, но букв и так получилось довольно много.
Ошибка на миллиард долларов?
В марте 2009-го года сэр Тони Хоар (C.A.R. Hoare) выступил на конференции Qcon в Лондоне с докладом на тему "Нулевые ссылки: ошибка на миллиард долларов" (Null References: The Billion Dollar Mistake), в котором признался, что считает изобретение нулевых указателей одной из главных своих ошибок, стоившей индустрии миллиарды долларов.
“Я называю это своей ошибкой на миллиард долларов. Речь идет о изобретении нулевых ссылок (null reference) в 1965 году. В то время, я проектировал комплексную систему типов для ссылок в объекто-ориентированном языке программирования ALGOL W. Я хотел гарантировать, что любое использование всех ссылок будет абсолютно безопасным, с автоматической проверкой этого компилятором. Но я не смог устоять перед соблазном добавить нулевую ссылку (null reference), поскольку реализовать это было столь легко. Это решение привело к бесчисленному количеству ошибок, дыр в безопасности и падений систем, что привело, наверное, к миллиардным убыткам за последние 40 лет. В последнее время, ряд анализаторов, таких как PREfix и PREfast корпорации Microsoft были использованы для проверки валидности ссылок и выдачи предупреждений в случае, если они могли быть равными null. Совсем недавно появился ряд языков программирования, таких как Spec# с поддержкой ненулевых ссылок (non-null references). Это именно то решение, которое я отверг в 1965-м.”
Сегодня уже поздно говорить, чтоб бы было, если бы Хоар когда-то принял иное решение и более разумно посмотреть, как эти проблемы решаются в разных языках и, в частности, в языке C#.
Void Safety в Eiffel (с примерами на C#)
Одним из первых на выступление Тони Хоара отреагировал Бертран Мейер, гуру ООП и автор языка Eiffel. Предложенное Мейером решение заключается в разделении переменных всех ссылочных типов на две категории: на переменные, допускающие null (nullable references или detach references в терминах Eiffel) и на переменные, не допускающие null (not-nullable references или attach references в терминах Eiffel).
При этом по умолчанию, все переменные стали относится именно к non-nullable категории! Причина такого решения в том, что на самом деле, в подавляющем большинстве случаев нам нужны именно non-nullable ссылки, а nullable-ссылки являются исключением.
С таким разделением компилятор Eiffel может помочь программисту в обеспечении "Void Safety": он свободно позволяет обращаться к любым членам not-nullable ссылок и требует определенного "обряда" для потенциально «нулевых» ссылок. Для этого используется несколько паттернов и синтаксических конструкций, но идея, думаю, понятна. Если переменная может быть null, то для вызова метода x.Foo() вначале требуется проверить, что x != null.
При этом, чтобы решить проблему с многопоточностью (дабы избавиться от гонок), вводится специальная синтаксическая конструкция, следующего вида:
if attached t as l then
l.f -- здесь обеспечивается Void safety.
end
Если провести параллель между языком Eiffel и C#, то выглядело бы это примерно так. Все переменные ссылочного типа превратились бы в not-nullable переменные, что требовало бы их инициализации в месте объявления валидным (not null) объектом. А доступ к любым nullable переменным требовал бы какой-то магии:
// Где-то в идеальном мире
// s – not-nullable переменная
public void Foo(string s)
{
// Никакие проверки, контркты, атрибуты не нужны
Console.WriteLine(s.Length);
}
// str – nullable (detached) переменная.
//string! аналогичен типу Option<string>
public void Boo(string! str)
{
// Ошибка компиляции!
// Нельзя обращаться к членам "detached" строки!
// Console.WriteLine(str.Length);
str.IfAttached((string s) => Console.WriteLine(s));
// Или
if (str != null)
Console.WriteLine(str.Length);
}
public void Doo(string! str)
{
Contract.Requires(str != null);
// Наличие предусловия позволяет безопасным образом
// обращаться к объекте string через ссылку str!
Console.WriteLine(str.Length);
}
Такие изменения в языке, а также в стандартной библиотеке Eiffel позволили гарантировать отсутствие разыменовывания нулевых ссылок. Но даже с небольшим комьюнити, для внедрения этих изменений потребовалось несколько лет. Поэтому неудивительно, что нам не стоит ждать аналогичных изменений в таком языке как C#.
Более того, реалии таковы, что нам вряд ли стоит ждать появления not-nullable ссылочных типов (о чем не так давно поведал Эрик Липперт в своей статье "C#: Non-nullable Reference Types"), поэтому нам приходится изобретать различного вида велосипеды. Но прежде чем переходить к этим самым велосипедам, интересно посмотреть на подход другого языка платформы .NET – F#.
ПРИМЕЧАНИЕ
Подробнее о Void Safety в Eiffel можно почитать в статье Бертрана Мейера "Avoid a Void: The eradication of null dereferencing", а также в замечательной статье Жени Охотникова "Void safety в языке Eiffel".
Void Safety в F#
Большинство функциональных языков программирования (включая F#) решают проблему разыменовывания нулевых ссылок уже довольно давно, причем способом аналогичным рассмотренному ранее. Так, типы, объявленные в F# по умолчанию не могут быть null (нужно использовать специальный атрибут AllowNullLiteral, да и в этом случае это возможно не всегда). В F# разрешается использование литерала null, но лишь для взаимодействия со сторонним кодом, не столь продвинутым в вопросах Void Safety, написанном на таких языках как C# или VB (хотя и в этом случае иногда могут быть проблемы, см. "F# null trick").
Для указания "отсутствующего" значения в F# вместо null принято использовать специальный тип – Option<T>. Это позволяет четко указать в сигнатуре метода, когда ожидается nullable аргумент или результат, а когда результат не должен быть null:
// Результат не может быть null
let findRecord (id: int) : Record =
...
// Результат тоже не может быть null,
// но результат завернут в Option<Record> и может отсутствовать
let tryFindRecord (id: int) : Option<Record> =
...
Так, в первом случае возвращаемое значение обязательно (и при его отсутствии будет сгенерировано исключение), а во втором случае результат может отсутствовать.
ПРИМЕЧАНИЕ
Именно этой идиоме следует стандартная библиотека F#. Так, вместо пары методов из LINQ: Single/SingleOrDefault в F# используются методы find/tryFind модулей Seq, List и других.
Поскольку "необязательные" значения может участвовать в целой цепочке событий, любой функциональный язык программирования поддерживает специальный "паттерн", который позволяет "протаскивать" значение через некоторую цепочку операций. Данный паттерн носит название "монада" и является фундаментальным в некоторых функциональных языках программирования. Я не хочу останавливаться на абстрактном описании, поскольку для этого уже есть ряд отличных источников (см. дополнительные источники в конце статьи).
Идея же достаточно простая: нам нужен простой способ работать с "завернутыми" значениями, такими как Option<T>, аналогично тому, как бы мы работали с самим типом T напрямую. Проще всего это показать на примере:
Предположим, у нас есть цепочка операций:
- Попытаться найти сотрудника (tryFindEmployee), если он найден, то
- взять свойство Department, если свойство не пустое, то
- взять свойство BaseSalary, если свойство не пустое, то
- вычислить размер заработной платы (computeSalary).
- если один из этапов возвращает не содержит результата (None в случае F#), то результат должен отсутствовать (должен быть равен None), в противном случае мы получим вычисленный результат.
Это можно сделать большим набором if-ов, а можно использовать Option.bind:
let salary =
tryFindEmployee 42
|> Option.bind(fun e -> e.Department)
|> Option.bind(fun d -> d.BaseSalary)
|> Option.bind computeSalary
Основная идея метода bind очень простая: если Option<T> содержит значение, тогда метод bind вызывает переданный делегат с "распакованным" значением, в противном случае – просто возвращается None (отсутствие значение в типе Option). При этом делегат, переданный в метод Bind также может вернуть значение, завернутое в Option или None, если значение не получено. Такой подход позволяет легко создавать цепочки операций, не заморачиваясь кучей проверок на null на каждом этапе.
Void Safety в C#
Поскольку популярность функциональных языков существенно возросла, то не удивительно, что многие паттерны функционального программирования начали перекачивать в такие исходно объектно-ориентированные языки, как C# (авторы C# серьезно думают о добавлении в одной из новых версий Pattern Matching-а!). Поэтому не удивительно, что для решения проблемы "нулевых ссылок" очень часто начали использоваться идиомы, заимствованные из функциональных языков.
"Монада" Maybe
Одна из причин появления этой статьи заключается в попытке выяснить, насколько полезно/разумно использовать тип Maybe<T> (аналогичный типу Option<T> из F#) в языке C#. В комментариях к предыдущей заметке было высказано мнение о пользе этого подхода, но хотелось бы рассмотреть "за" и "против" более подробно.
> Используйте монадки, и не нужно париться с null. Накладывайте рестрикшен через тип.
Действительно, строгая типизация и выражение намерений через систему типов – это лучший способ для разработчика передать свои намерения. Так, не зная существующих идиом платформы .NET, очень сложно понять, в чем же разница между методами First и FirstOrDefault в LINQ, или как понять, что будет делать метод GetEmployeeById в случае отсутствия данных по сотруднику: вернет null или сгенерирует исключение? В первом случае суффикс "Default" намекает, что в случае отсутствия элемента мы получим "значение по умолчанию" (т.е. null), но что будет во втором случае – не ясно.
В этом плане подход, принятый в F# выглядит намного более приятным, ведь там методы отличаются не только именем, но и возвращаемым значением: метод find возвращает T, а tryFind возвращает Option<T>. Так почему бы не пользоваться этой же идиомой в языке C#?
Главная причина, останавливающая от повсеместного использования Option<T> в C# заключается в отсутствии повсеместного использования Option<T> в C#. Звучт бредово, но все так и есть: использование Option<T> не является стандартной идиомой для библиотек на языке C#, а значит использование своего "велосипедного" Option<T> не принесет существенной пользы.
Теперь стоит понять, а стоит ли овчинка выделки и будет ли полезным использование Option<T> лишь в своем проекте.
Обеспечивает ли Maybe<T> Void Safety?
Особенность использования Maybe заключается в том, что это лишь смягчает, но не решает полностью проблему "разыменовывания нулевых указателей". Проблема в том, что нам все равно нужен способ сказать в коде, что данная переменная типа Maybe<T> должна содержать значение в определенный момент времени. Нам не только нужен безопасный способ обработать оба возможных варианта значения переменной Maybe, но и сказать о том, что один из них в данный момент не возможен.
Давайте рассмотрим такой пример. При реализации "R# Contract Extensions" мне понадобился класс для добавления предусловий/постусловий в абстрактные методы, и методы интерфейсов. Для тех, кто не очень в курсе библиотеки Code Contracts, контракты для абстрактных методов и интерфейсов задаются отдельным классом, который помечается специальным атрибутом – ContractClassFor:
[ContractClass(typeof (AbstractClassContract))]
public abstract class AbstractClass
{
public abstract void Foo(string str);
}
// Отдельный класс с контрактами абстрактного класса
[ContractClassFor(typeof (AbstractClass))]
abstract class AbstractClassContract : AbstractClass
{
public override void Foo(string str)
{
Contract.Requires(str != null);
throw new System.NotImplementedException();
}
}
Процесс добавления контракта для абстрактного метода AbstractClass.Foo такой:
- Получить "контрактный метод" текущего метода (в нашем случае нужно найти "дескриптор" метода AbstractClassContract.Foo).
- Если такого метода еще нет (или нет класса AbstractClassContract), сгенерировать класс контракта с этим методом.
- Получить "контрактный метод" снова (теперь мы точно должны получить "дескриптор" метода AbstractClassContract.Foo).
- Добавить предусловие/постусловие в "контрактный метод".
В результате метод добавления предусловия для абстрактного метод выглядит так (см. ComboRequiresContextAction метод ExecutePsiTransaction):
var contractFunction = GetContractFunction();
if (contractFunction == null)
{
AddContractClass();
contractFunction = GetContractFunction();
Contract.Assert(contractFunction != null);
}
...
[CanBeNull, System.Diagnostics.Contracts.Pure]
private ICSharpFunctionDeclaration GetContractFunction()
{
return _availability.SelectedFunction.GetContractFunction();
}
Метод GetContractFunction возвращает метод класса-контракта для абстрактного класса или интерфейса (в нашем примере, для метода AbstractClass.Foo этот метод вернет AbstractClassContract.Foo, если контрактный класс существует, или null, если класса-контракта еще нет).
Как бы изменился этот метод, если бы я использовал Maybe<T> или Option<T>? Я бы убрал атрибут CanBeNull и поменял тип возвращаемого значения. Но вопрос в том, как бы использование Maybe мне помогло бы выразить, что "постусловие" этого закрытого метода меняется в зависимости от контекста? Так, при первом вызове этого метода вполне нормально, что контрактного метода еще нет и возвращаемое значение равно null (или None, в случае "монадического" решения). Однако после вызова AddContractClass (т.е. после добавления класса-контракта) возвращаемое значение точно должно быть и метод GetContractFunction обязательно должен вернуть "непустое" значение! Я никак не могу отразить это в системе типов языка C# или F#, поскольку лишь я знаю ожидаемое поведение. (Напомню, что контракты выступают в роли спецификации и задают ожидаемое поведение, отклонение от которого означает наличие багов в реализации. Так и в этом случае, если второй вызов GetContractFunction вернул null, то это значит, что в консерватории что-то сломалось и нужно лезть в код и его чинить).
Этим маленьким примером я хотел показать, что не все проблемы с разыменовыванием нулевых указателей можно решить с помощью класса Maybe, а контракты и аннотации (такие как CanBeNull) вполне может поднять «описательность» методов не меняя тип возвращаемого значения.
Использование "неявной" монады Maybe
Как уже было сказано выше, польза от Maybe будет лишь в том случае, если он будет использован всеми типами приложения. Это значит, что если ваш метод TryFind возвращает Maybe<Person>, то нужно, чтобы все "nullable" свойства класса Person тоже возвращали Maybe<T>.
Добиться повсеместного использования Maybe в любом серьезном приложении, написанном на C#, весьма проблематично, поскольку все равно останется масса "внешнего" кода, который не знает об этой идиоме. Так почему бы не воспользоваться этой же идеей, но без «заворачивания» всех типов в Maybe<T>?
Так, класс Maybe<T> (или Option<T>) является своего рода оболочкой вокруг некоторого значения, со вспомогательным методом Bind (название метода является стандартным для паттерна монада), который "разворачивает коверт" и достает из него реальное значение и передает для обработки. При этом становится очень легко строить pipe line, который мы видели в разделе, посвященном языку F#.
Но почему бы не рассматривать любую ссылку, как некую "оболочку" вокруг объекта, для которого мы добавим метод расширения с тем же смыслом, что и метод Bind класса Option? В результате, мы создадим метод, который будет "разворачивать" нашу ссылку и вызывать метод Bind (который мы переименуем в With) лишь тогда, когда текущее значение не равно null:
public static U With<T, U>(this T callSite, Func<T, U> selector) where T : class
{
Contract.Requires(selector != null);
if (callSite == null)
return default(U);
return selector(callSite);
}
В этом случае, мы сможем использовать достаточно компактный синтаксис для обхода целого дерева, каждый узел которого может вернут null. И тогда, если все "узлы" этого дерева будут не null, мы получим непустой результат, если же на одном из этапов, значение будет отсутствовать, то результат всего выражения будет null.
С методом With наш пример, рассмотренный в предыдущем разделе будет выглядеть так:
var salary = Repository.GetEmployee(42)
.With(employee => employee.Department)
.With(department => department.BaseSalary)
.With(baseSalary => ComputeSalary(baseSalary));
В некоторых случаях такой простой "велосипед" может в разы сократить размер кода и здорово улучшить читабельность, особенно при работе с "глубокими" графами объектов, большинство узлов которого может содержать null. Примером такого "глубокого" графа является API Roslyn-а, а также ReSharper SDK. Именно поэтому в своем плагине для ReSharper-а я очень часто использую подобный код:
[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;
}
Данный тип "расковыривает" выражение вызова метода и достает тип, на котором этот вызов происходит. При этом может возникнуть вопрос с отладкой, когда результат равен null и не понятно, что же конкретно пошло не так:
В этом плане очень полезна новая фича в VS2013 под названием Autos, которая показывает, на каком из этапов мы получили null:
(В данном случае мы видим, что Resolve вернул не нулевой элемент, но DeclaredElement этого значения равен null.)
Null propagating operator ?. в C# 6.0
Поскольку проблема обработки сложных графов объектов с потенциально отсутствующими значениями, столь распространена, разработчики C# решили добавить в C# 6.0 специальный синтаксис в виде оператора "?." (null propagating operator). Основная его идея аналогична методу Bind класса Option<T> и приведенному выше методу расширения With.
Следующие три примера эквивалентны:
var customerName1 = GetCustomer() ?.Name;
var customerName2 = GetCustomer().With(x => x.Name);
var tempCustomer = GetCustomer();
string customerName3 = tempCustomer != null ? tempCustomer.Name : null;
ПРИМЕЧАНИЕ
Null propagating operator не может заменить приведенные ранее методы расширения With, поскольку справа от оператора ?. может быть лишь обращение к членам текущего объекта, а произвольное выражения использовать нельзя.
Так, в случае с With мы можем сделать следующее:
var result = GetCustomer().With(x => x.Name).With(x => string.Intern(x));
В случае с “?.” нам все же понадобится временная переменная.
Заключение
Так как же можно обеспечить Void Safety в языке C#? Обеспечить полностью? Никак. Если же мы хотим уменьшить эту проблему к минимуму, то тут есть варианты.
Мне правда нравится идея явного использования типа Maybe (или Option) для получения более декларативного дизайна, но меня напрягает неуниверсальность этого подхода: для собственного кода этот подход работает "на ура", но все равно будет непоследовательным, поскольку сторонний код этой идиомой не пользуется.
Для обработки сложных графов объектов мне нравится подход с методами расширения типа With/Return для ссылочных типов + контракты для декларативного описания наличия или отсутствия значения. Мне вообще кажется, что аннотации типа CanBeNull + контракты с соответствующей поддержкой со стороны среды разработки (как в плане редактирования, так и просмотра) могут существенно упростить понимание кода и проблема сейчас именно в отсутствии такой поддержки.
После выхода C# 6.0 и появления оператора “?.” в ряде случаев можно будет отказаться от методов расширения With/Return, но иногда такие велосипеды все равно будут нужны из-за ограничений оператора “?.”.
К сожалению, без полноценных not-nullable типов обеспечить Void Safety полностью просто невозможно. Поэтому сейчас нам остается использовать разные велосипеды и надеяться на усовершенствование средств разработки, которые упростят работу с контрактами и nullable-типами.
Дополнительные ссылки
- Интервью с Бертраном Мейером
- Null Reference: The Billion Dollar Mistake – выступление Тони Хоара на QCon 2009
- Bertrand Meyer. Avoid a Void: The eradication of null dereferencing – отличная статья Мейера о Void Safety в Eiffel.
- Евгений Охотников. Void safety в языке Eiffel – описание Void Safety в Eiffel на русском языке.
- Eric Lippert. "C#: Non-nullable Reference Types" – сказ о том, почему нам не стоит ждать nullable reference типов в языке C#.
- Functors, applicatives, and monads in pictures – отличная статья с графическим объяснением, что такое монада.
- Eric Lippert. Monads, part one – первая статья отличной серии постов Эрика Липперта о монадах. Одно из лучших описаний для C# разработчика
- F# for fun and profit: The “Computation Expressions” series – цикл статей о Computation Expressions в F#. Не о монадах напрямую, но это все равно лучшее описание принципов, которые лежат в основе монад. Если имеете представление об F#, то эта серия – лучший способ разобраться в монадах.
- Обсуждение null propagating operator на roslyn.codeplex.com
- R# Contract Extension – R# плагин для упрощения работы с контрактами
- Thinking Functionally in C# with monads.net
З.Ы. Понравился пост? Поделись с друзьями! Вам не сложно, а мне приятно;)
https://www.nuget.org/packages/Maybe
ОтветитьУдалитьЧтоб було
УдалитьИ от сергуна еще https://www.nuget.org/packages/Monads/
УдалитьСпасибо за ссылки!
УдалитьProgramming Stuff: Борьба С "Нулевыми" Ссылками В C >>>>> Download Now
Удалить>>>>> Download Full
Programming Stuff: Борьба С "Нулевыми" Ссылками В C >>>>> Download LINK
>>>>> Download Now
Programming Stuff: Борьба С "Нулевыми" Ссылками В C >>>>> Download Full
>>>>> Download LINK vA
Жизнь — боль, и язык с null'ами болен ими же самими. Тоже боремся, но все кончается «монадами»-расширениями, которыми и не все пользоваться-то хотят, да и не всегда к месту. Увы.
ОтветитьУдалитьЕсть такое дело!
УдалитьКстати, в аннонсированном вчера чудо языке от Appke - Swift - есть поддержка not-nullable сылок. Так что, видимо, индустрия таки движется в правильном направлении
"для собствненого кода".
ОтветитьУдалитьСпасибо за статью, Сергей.
Спасибо!
УдалитьПоправил очепятку.
Вообще-то Functor Maybe и Applicative Maybe обеспечивает всё необходимое не затрагивая тему монад. ((+1) <$> Nothing) == Nothing, ((+1) <$> Just 2) == Just 3. Monad Maybe позволяет просто использовать нотацию "do" в Haskell, которая в случае
ОтветитьУдалитьdo { x <- foo; y <- bar; return x*y; } :: Maybe Int
делает что-то вроде (*) <$> foo <*> bar .
А с использованием Applicative Monad можно писать (Nothing <|> Just 4) == Just 4
По поводу обёрток типов... Можно ввести value-тип Strict<T> и для With иметь overload который не будет проверять на null. Хотя, сомневаюсь это ускорит что-то. Думаю компилятор вставит свою проверку чтобы кинуть NullPointerException даже если это никогда не произойдёт.
В Haskell очень популярно вводить обёртки которые добавляют семантику через тип и для этого есть специальный способ объявления newtype который может легко наследовать type-class'ы которые есть у оборачиваемого типа (NewtypeDeriving).
Там не Applicative Monad а Alternative Maybe
УдалитьНу вот, пришел Коля и сломал всем мозг:))
Удалить>>>Null propagating operator не может заменить приведенные ранее методы расширения When, поскольку справа от оператора ?. может быть лишь обращение к членам текущего объекта, а произвольное выражения использовать нельзя.
ОтветитьУдалить>>>Так, в случае с When мы можем сделать следующее:
Наверно, оба раза имелось в виду не When, а With?
Спасибо! Уже поправил!
УдалитьЭтот комментарий был удален автором.
ОтветитьУдалить1) Твой пример с контрактами не сильно подходящий, он не высосан с пальца, но он очень спецыфичный к твоей задаче...
ОтветитьУдалитьНа самом деле ты вводишь сложность с твоим: "постусловие этого закрытого метода меняется в зависимости от контекста?". Мне твой солюшен попахивает "bool hell". Ведь если ты мыслишь в терминах типов то по сути ты должен оперировать типами, а не состоянием в зависимости от какого - то там пост кондишена. Для примера: Maybe монаду ты можешь заимплиментить на bool флажках (isSome, isNone) с использованием basic contrflow конструкций (if, else) в императивном стиле... Но так же ты можешь опиратся на контейнерные типы Just : Maybe, и Nothing : Maybe - улавливаешь разницу?
[DebuggerStepThrough]
private class Nothing : Maybe
{
public override bool HasValue
{
get { return false; }
}
public override TValue Value { get { throw new InvalidOperationException(); } }
public override Maybe Map(Func map)
{
return new Nothing();
}
public override Maybe FlatMap(Func> flatMap)
{
return new Nothing();
}
}
С таким решением ты мыслишь проджекшинами выстраивая четкий воркфлов - это очень правильно. Неправильно - это когда ты оперируешь всяким хитрым bool флажкам, какому-то неявному состоянию - ты только усложняешь свое решение. Также замечу что чаще всего приходится видеть такой код у людей которые не интересуются функциональщиной или не до конца её поняли. Но есть исключения, например перфоменс.
"Я никак не могу отразить это в системе типов языка C# или F#, поскольку лишь я знаю ожидаемое поведение."
- Я не сильно понял как ты это отразил с контрактами?
Если ты имеешь это ввиду:
contractFunction = GetContractFunction();
Contract.Assert(contractFunction != null);
То чем это отличается от if (contractFunction.isNone).
Мне кажется что твою задачу можно решить по другому.. По сути тебе нужно ретурнуть два разных типа и все. Есть только прямой флов и ты не можешь с него выпрыгнуть рание не пройдя его полностью. Как ты это сделаешь (добавишь еще один интерфейс который будет возвращать NotNull тип и просунешь его дальше, что б твои клиенты работали только с ним, а не с Maybe.... заиспользуешь композицию... способов туча)... Поинт тут в том что ты не должен опиратся на семантику кода, а говорить все через типы.... Это уже сугубо твой выбор...
2) Еще раз по поводу контрактов: то что я(и многие из моих френдов) вижу - очень страшно.. на 3 строчки кода я должен добавить просто пачку метаданных... и кому это понравится?? а поддержка, а реффакторинг??? Зачем так жостко захламлять код? Для C# это очень не универсальный инструмент....
3) "Как уже было сказано выше, польза от Maybe будет лишь в том случае, если он будет использован всеми типами приложения. Это значит, что если ваш метод TryFind возвращает Maybe, то нужно, чтобы все "nullable" свойства класса Person тоже возвращали Maybe." - да так и есть... Хотел добавить что TryFind ваще не должен применятся когда у тебя есть Maybe. когда у тебя есть хоть минимальная причина получить null то лучше сразу возвращать Maybe First() и не добавлять FirstOrDefault();
4) "Добиться повсеместного использования Maybe в любом серьезном приложении, написанном на C#, весьма проблематично, поскольку все равно останется масса "внешнего" кода, который не знает об этой идиоме. Так почему бы не воспользоваться этой же идеей, но без «заворачивания» всех типов в Maybe?"
ОтветитьУдалить- да ну? да ну зачем ты так говоришь? какое заворачивание всех типов?(может у тебя что-то с дизайном что тебе приходится заворачивать все типы в maybe). Я не люблю когда оперируют абстракциями.. давай вот просто посмотрим реальную модель кода (Person кого хочешь) и ты увидешь что там этих Maybe не так много как ты это хочешь преподнести.... Понятно что есть исключения, например у меня не так давно было много интенсивного WinAPI, у меня был для этого WinAPiService который помагал мне работать с хендлами и тд.. и например у него был метод Maybe GetProcessByName(string name); и да мне приходилось чекать результат на IsSome, но у меня при этом Process оставался чистым.. если я его получил (он не IsNone) то значит что все в порядке.. и дальше я с ним работаю без всяких чеков на null... и также на практике, у тебя чаще будет ситуации когда или целый обджек Null или NotNull.
Я не вижу примущества использования контрактов в C# для проверок на null(там пачка проблем выползает которые неявные). Еще раз, мы на проекте используем монады и у нас нет проблем.
@Antya: ты не хочешь замутить бэттл на Kiev ALT.NET по этому поводу?
ОтветитьУдалитьМы тут будем переходить по частностям обсуждения, теряя основную мысль, поэтому особенно цепляться к заявлениям в комментариях я не хочу. Я готов прокомментить твою обдуманную статью по этому поводу, но сраться в комментах на комменты смысла не вижу:)
@Sergey Teplyakov по поводу батла даже незнаю... Мне кажется, что лучше просто попробовать решить задачку.... просто смоделировав реаьную ситуацию... и сравнить код....
ОтветитьУдалить"Я готов прокомментить твою обдуманную статью по этому поводу, но сраться в комментах на комменты смысла не вижу:)"
- это тоже правильно, но мне кажется что все сведется к определенной задачке... так как в коментах ты будешь пытатся решить задачку которую я опишу в посте....
Не совсем. Вот я, например, не вполне пойму, с чем именно ты не согласен. Ты реагируешь на конкретные моменты, но я не понял, с чем ты не согласен по сути. Кажется, ты не согласен с тем, что контракты помогают бороться с проблемой нулевых ссылок, и если это так, то я готов описать это подробнее (кстати, я хочу напомнить, что в я ведь использую микс из подходов: у меня есть монадические расширения и есть контракты;)).
УдалитьНу твою статью я не буду просто комментить, я ведь напишу еще одну статью, в которой постараюсь разложить по полочкам, с чем я согласен, а с чем - нет.
Если таки интересно, я готов продолжить общение и в комментах, но, как правило, это будет существенно менее интерактивным и более трудоемким с точки зрения донесения мысли.
Ну и можно выбрать такой подход: одна ветка комментов - одна мысль. Так у нас будет шанс не запутаться в мыслях и таки почерпнуть максимум пользы из нашего общения;)
> Мне кажется, что лучше просто попробовать решить задачку.... просто смоделировав реаьную ситуацию... и сравнить код....
Я готов на это:) Но тут есть проблема: будет сложно найти такую задачу, в предметной области которой мы одинаково хорошо разбираемся:) А именно от этого будет строиться дизайн решения.
В статье я привел несколько примеров из своего плагина, в котором проблема нулевых ссылок очень актуальна. В обычной бизнес задаче для меня эта проблема вообще практически не возникала, поскольку я в своем коде очень и очень редко возвращаю null-ы, а значит любой из предложенных подходов будет абсолютно нормальным и не покажет реальной разницы))
Если же взять такие вещи, как Розлин или R# API, в котором очень ветвистые графы объектов, и многие узлы этих графов могут отсутствовать, много полиморфных узлов с заранее не понятными типами, то там разница будет. Но, как ты правильно заметил, это не вполне типовая задача (раз), и я с этим доменом немного поработал, а ты, скорее всего, нет (два). Это делает ее использование невозможной.
В общем, я готов к любому раскладу:)
С null вообще не надо бороться - это нормальное значение. Бороться надо с расхлябанностью кода. А так же писать наиболее гибкий код, который не падает при null параметрах. Скажем, если сделать string.Substring принимающим ТОЛЬКО не-null, то прогеры охренеют от проверок перед КАЖДЫМ вызовом substr. Не юзеры метода должны приседать вокруг автора, а автор метода должен быть максимально "лояльным" к аргументам.
ОтветитьУдалитьProgramming Stuff: Борьба С "Нулевыми" Ссылками В C >>>>> Download Now
ОтветитьУдалить>>>>> Download Full
Programming Stuff: Борьба С "Нулевыми" Ссылками В C >>>>> Download LINK
>>>>> Download Now
Programming Stuff: Борьба С "Нулевыми" Ссылками В C >>>>> Download Full
>>>>> Download LINK Ag