Какая реализация типа option считается более или менее общеиспользуемой в современном Дотнете? (Option из F# не предлагать.) По тегам optional и maybe гуглится десяток пакетов, но они вроде какие-то доморощенные. Требуется, чтобы тип был значимым и реализовывал IEnumerable<T>; опционально «паттерн матчинг» и обвязка для TryGet-паттерна.
Здравствуйте, Qbit86, Вы писали:
Q>Добрый день!
Q>Какая реализация типа option считается более или менее общеиспользуемой в современном Дотнете?
Никакой. Обвязка [Null/NotNull]-атрибутами и проверка самописным правилом при компиляции.
Можно поспрашивать на форуме JetPrains — есть ли у них решения для проверки при компиляции. Если нет — можно попробовать https://github.com/mattwar/nullaby (ранний прототип, толком не вылизывался).
Если хочется пособирать грабли — можно попробовать CodeContracts. nullable он как раз здорово ловит, остальное -как повезёт.
Все альтернативные варианты приводят к очень хорошему проседанию производительности и куче лишних проверок по всему коду.
В любом из вариантов — решение не получится на халяву. Из опыта, поддержание качественного аннотированного кода с требованием проверки во время компиляции обходится в несколько раз дороже, чем банальная разметка ассертами вида "Code.NotNull(someArg, "someArg")".
Во-первых, CodeContracts, постпроцессинг и Roslyn — это всё для белых людей; увы, в моей текущей реальности есть лишь Unity3D и древний глючной Моно с не менее архаичным компилятором.
Во-вторых, compile time мне не нужен. Мне нужно возвращать из функций типа Find() или Parse() опциональные значения, отсутствие которых в рантайме вполне нормально.
Здравствуйте, Qbit86, Вы писали:
Q>Во-первых, CodeContracts, постпроцессинг и Roslyn — это всё для белых людей; увы, в моей текущей реальности есть лишь Unity3D и древний глючной Моно с не менее архаичным компилятором.
Упс
Q>Во-вторых, compile time мне не нужен. Мне нужно возвращать из функций типа Find() или Parse() опциональные значения, отсутствие которых в рантайме вполне нормально.
А. В этих случаях или стандартный "bool Try(out result)", или Try-метод, который возвращает null result при неудаче. И ассерты на входные параметры (при желании — результат проверять тоже). Самый дешёвый способ. Тем более что в релизных сборках ассерты можно не включать.
Здравствуйте, Sinix, Вы писали:
Q>>Во-вторых, compile time мне не нужен. Мне нужно возвращать из функций типа Find() или Parse() опциональные значения, отсутствие которых в рантайме вполне нормально.
S>А. В этих случаях или стандартный "bool Try(out result)"
Нет, как раз от этой лапши я и пытаюсь уйти. Она не chain'ится, загрязняет скоуп именами и прочим синтаксическим мусором.
S>или Try-метод, который возвращает null result при неудаче.
Оно должно работать и для значимых типов, плюс нужны «null propagation» и тому подобные монадические штуки: flatten, map, filter. Для себя во внутренних сигнатурах или полях класса я могу использовать «option для бедных» в виде коллекции (возвращать из функций пустой|одноэлементный массив или yield'ить результат), к чему уже можно будет применить Linq; и в целом защититься от NullReferenceException, делая «разыменование» монады явным.
Реализовать option самостоятельно не так уж сложно, но ведь хочется вместо велосипеда в «публичных» API использовать хорошо спроектированный и проверенный примитив из авторитетного источника.
S>И ассерты на входные параметры (при желании — результат проверять тоже). Самый дешёвый способ. Тем более что в релизных сборках ассерты можно не включать.
Проверка входных параметров на null и прочие предусловия через старомодные исключения меня не напрягают. В любом случае, Option/Maybe — это вполне самостоятельная концепция, ортогональная контрактам/аннотациям времени компиляции.
Здравствуйте, Qbit86, Вы писали:
Q>Какая реализация типа option считается более или менее общеиспользуемой в современном Дотнете? … Требуется, чтобы тип …реализовывал IEnumerable<T>…
IEnumerable<T>? или IEquatable<>? Зачем нужен IEnumerable<T> опшион?
Опцион можно (и нужно) рассматривать как коллекцию из одного элемента или пустую. Например, мне надо извлечь из словаря int по string'овому ключу, и если этот элемент там есть, и он больше 1729, то передать его куда-то дальше:
Option<Foo> foo = myDict.TryGet("someKey").Where(i => i > 1729).Select(i => new Foo(i)).ToOption();
На каком-то этапе у меня может получится пустая коллекция — не беда, произойдёт «None propagation» до самого верха.
Здравствуйте, Qbit86, Вы писали:
S>>А. В этих случаях или стандартный "bool Try(out result)" Q>Нет, как раз от этой лапши я и пытаюсь уйти. Она не chain'ится, загрязняет скоуп именами и прочим синтаксическим мусором.
Тогда или c#6 с ?. (в mono тож должно появиться), или chaining с лямбдами, который кстати не требует Option.
что-то типа "Value.With(s => s.SomeValue)", или совершенно шизофреничный
public static class NullableExtensions
{
public static T2 Select<T1, T2>(this T1 p, Func<T1, T2> c) where T2 : class
{
return p == null ? null : c(p);
}
public static T2? Select<T1, T2>(this T1 p, Func<T1, T2?> c) where T2 : struct
{
return p == null ? null : c(p);
}
}
// ...int? p = null;
var d = from p2 in p select"!";
Console.WriteLine(d ?? "null"); // null
* При желании входной и выходной типы в NullableExtensions превращаются в Option<>.
Производительность и количество лишних аллокаций (для замыканий) соответствующая
Q>Оно должно работать и для значимых типов, плюс нужны «null propagation» и тому подобные монадические штуки: flatten, map, filter.
См пример выше. На практике это безумно неудобно.
Q>Реализовать option самостоятельно не так уж сложно, но ведь хочется вместо велосипеда в «публичных» API использовать хорошо спроектированный и проверенный примитив из авторитетного источника.
Не будет такого. Потому что количество писанины не намного уступает коду без хелперов, производительность падает значительно, а ?. operator самому не добавить. В реальном коде проще гаранитровать ассертами, что null-ов не будет, чем строить по всему коду nullable chains.
Здравствуйте, Sinix, Вы писали:
S>Тогда или c#6 с ?. (в mono тож должно появиться)
Увы, из-за каких-то юридических ньюансов Юнитеки не могут обновить (модифицированную) Моно в своей поставке Unity3D начиная с версии, когда Мигель де Икаса изменил лицензию.
В какой-то степени оно всё CIL-совместимо и теоретически можно писать нескриптовый код в 2015 Студии на новом C# 6 под старый .NET 3.5, но там свои органичения и подводные грабли с AOT на iOS.
S>См пример выше. На практике это безумно неудобно.
Опцион можно (и нужно) рассматривать как коллекцию из одного элемента или пустую. Например, мне надо извлечь из словаря int по string'овому ключу, и если этот элемент там есть, и он больше 1729, то передать его куда-то дальше: Q>
Q>Option<Foo> foo = myDict.TryGet("someKey").Where(i => i > 1729).Select(i => new Foo(i)).ToOption();
Q>
Q>На каком-то этапе у меня может получится пустая коллекция — не беда, произойдёт «None propagation» до самого верха.
Ну, тут IEnumerable как бы и не нужен. Достаточно написать екстеншны для Where и Select, и можно пользоваться query comprehensions.
Типа
var foo = from i in myDict.TryGet("someKey") where i > 1729 select new Foo(i)
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Ну, тут IEnumerable как бы и не нужен. Достаточно написать екстеншны для Where и Select, и можно пользоваться query comprehensions. S>Типа S>
S>var foo = from i in myDict.TryGet("someKey") where i > 1729 select new Foo(i)
S>
Из тех реализаций, что я видел, кто-то реализует IEnumerable<T>, кто-то независимые экстеншены (ещё нужен bind aka flatMap aka SelectMany). В последнем случае к тому же не теряется тип Option<T> (не нужен ToOption() в конце). Но, по-моему, Option<T> всё-таки является IEnumerable<T>.
Здравствуйте, Qbit86, Вы писали:
Q> Но, по-моему, Option<T> всё-таки является IEnumerable<T>.
Это из серии "всё является object, но по факту почему-то передают конкретные типы"
Раз уж завели отдельный тип с своим контрактом (к примеру IEnumerator.Current у вас не может быть null), то проще объявить его явно, чем надеяться, что никто не передаст Select(x=>default(T)).
Здравствуйте, Qbit86, Вы писали:
Q>Во-первых, CodeContracts, постпроцессинг и Roslyn — это всё для белых людей; увы, в моей текущей реальности есть лишь Unity3D и древний глючной Моно с не менее архаичным компилятором. Q>Во-вторых, compile time мне не нужен. Мне нужно возвращать из функций типа Find() или Parse() опциональные значения, отсутствие которых в рантайме вполне нормально.
У меня есть куча функций, который ищут пересечения между прямыми, лучами, отрезками и плоскостями. Я для возвращаемых значений использую Nullable<T>. Получается намного НАМНОГО удобнее, чем "bool Try(out result)".
Проблема не в инструменте, проблема в том, что вы его не по назначению используете.
Try-parse не заточен под кривой дизайн в стиле "цепочка операций, каждая может вернуть результат "нишмогла"". Для инфраструктуры это _очень_ плохой подход по очевидным причинам.
С появлением null propagating operator по тем же причинам код превратится из "очень плохой" просто в "плохой", но делу это не сильно поможет.
P.S. Для полной красоты не хватает только declaration expressions, но это уже к шестому шарпу.
P.P.S. Пост от alexzz, кстати, идеальный пример такого неправильного подхода. Для интервалов вместо nullable надо использовать свою обёртку, иначе две трети сценариев отпадает.
Самое простое: как отличить -∞ от +∞? Что насчёт замкнутых/открытых интервалов (wiki)? Пустое множество?
ну и тд.
Здравствуйте, Sinix, Вы писали:
S>Проблема не в инструменте, проблема в том, что вы его не по назначению используете. S>Try-parse не заточен под кривой дизайн в стиле "цепочка операций, каждая может вернуть результат "нишмогла"". Для инфраструктуры это _очень_ плохой подход по очевидным причинам.
В инфраструктуре стандартных библиотек нормальных языков типа Haskell, F#, Scala в try-get-сценарях как раз используется именно Option (Maybe). Например, лукап в хэш-таблице или поиск в коллекции. Просто разработчики .NET Framework не придумали сразу этот примитив (ограничившись ущербным Nullable<T>), вот API в Dictionary<K, V> и выглядит так криво. Но это не норма, пойми.
Здравствуйте, Qbit86, Вы писали:
Q>В какой-то степени оно всё CIL-совместимо и теоретически можно писать нескриптовый код в 2015 Студии на новом C# 6 под старый .NET 3.5, но там свои ограничения и подводные грабли с AOT на iOS.
Я приделал к Unity два компилятора C# 6: майкрософтовский и из поставки Mono 4.0.0.
Также приспособил библиотеку AsyncBridge.Net35.dll, теперь могу использовать async/await прямо в скриптах вместо сопрограмм.
Не знаю, можно ли этим пользоваться в продакшене, пока играюсь, смотрю, какие вылезут косяки. В редакторе, в билдах под Windows и на Андроиде у меня заработали все фичи C# 6. Т.е. вообще всё, кроме dynamic, естественно. Под WebGL IL2CPP не смог скомпилировать фильтры исключений и библиотека AsyncBridge ему не понравилась, т.е. недоступны async/await и атрибуты caller information.
iOS у меня нет, поэтому проверить AOT не могу, хотя было бы интересно. Вот тут чувак пишет, что iOS поддерживается.
Между компиляторами Roslyn и Mono нашёл только одно отличие. Моновский не понимает, что у bar должен быть тип Nullable<int>.
Да, читал эту ветку, и даже отписался там :)
A>Также приспособил библиотеку AsyncBridge.Net35.dll, теперь могу использовать async/await прямо в скриптах вместо сопрограмм... Не знаю, можно ли этим пользоваться в продакшене, пока играюсь, смотрю, какие вылезут косяки.
У меня в своё время бэкпорт TPL (и AsyncBridge?) не заработал на iOS. На Android вроде работало, но нам нужны все мобильные платформы.
S>>Try-parse не заточен под кривой дизайн в стиле "цепочка операций, каждая может вернуть результат "нишмогла"". Для инфраструктуры это _очень_ плохой подход по очевидным причинам.
Q>В инфраструктуре стандартных библиотек нормальных языков типа Haskell, F#, Scala в try-get-сценарях как раз используется именно Option (Maybe). Например, лукап в хэш-таблице или поиск в коллекции. Просто разработчики .NET Framework не придумали сразу этот примитив (ограничившись ущербным Nullable<T>), вот API в Dictionary<K, V> и выглядит так криво. Но это не норма, пойми.
Нет. Во-первых, Option<T> надо вводить одновременно с non-nullable ref. Это на мой взгляд очевидно, объяснять не надо?
Во-вторых — попробуй придумать для стандартной инфраструктуры BCL сценарий, в котором эта цепочка будет полезна. Если поднапрячься, то пара, возможно и найдётся. Только протаскивать Option придётся по всему остальному коду, который на пару порядков пообъёмней будет.
Не, серьёзно, смысл переписывать половину API вместо очевидного
if (dict.TryGetValue(key, out var value)
{
// ...
}
Здравствуйте, Sinix, Вы писали:
S>P.P.S. Пост от alexzz, кстати, идеальный пример такого неправильного подхода. Для интервалов вместо nullable надо использовать свою обёртку, иначе две трети сценариев отпадает.
У меня практическая задача. Такие сценарии в ней отсекаются заранее.