Option<T>
От: Qbit86 Кипр
Дата: 01.07.15 07:35
Оценка:
Добрый день!

Какая реализация типа option считается более или менее общеиспользуемой в современном Дотнете? (Option из F# не предлагать.) По тегам optional и maybe гуглится десяток пакетов, но они вроде какие-то доморощенные. Требуется, чтобы тип был значимым и реализовывал IEnumerable<T>; опционально «паттерн матчинг» и обвязка для TryGet-паттерна.
Глаза у меня добрые, но рубашка — смирительная!
Re: Option<T>
От: Sinix  
Дата: 01.07.15 07:58
Оценка: 1 (1)
Здравствуйте, Qbit86, Вы писали:

Q>Добрый день!


Q>Какая реализация типа option считается более или менее общеиспользуемой в современном Дотнете?


Никакой. Обвязка [Null/NotNull]-атрибутами и проверка самописным правилом при компиляции.
Можно поспрашивать на форуме JetPrains — есть ли у них решения для проверки при компиляции. Если нет — можно попробовать https://github.com/mattwar/nullaby (ранний прототип, толком не вылизывался).
Если хочется пособирать грабли — можно попробовать CodeContracts. nullable он как раз здорово ловит, остальное -как повезёт.

Все альтернативные варианты приводят к очень хорошему проседанию производительности и куче лишних проверок по всему коду.

В любом из вариантов — решение не получится на халяву. Из опыта, поддержание качественного аннотированного кода с требованием проверки во время компиляции обходится в несколько раз дороже, чем банальная разметка ассертами вида "Code.NotNull(someArg, "someArg")".
Re[2]: Аннотации
От: Qbit86 Кипр
Дата: 01.07.15 08:11
Оценка:
Здравствуйте, Sinix, Вы писали:

Во-первых, CodeContracts, постпроцессинг и Roslyn — это всё для белых людей; увы, в моей текущей реальности есть лишь Unity3D и древний глючной Моно с не менее архаичным компилятором.
Во-вторых, compile time мне не нужен. Мне нужно возвращать из функций типа Find() или Parse() опциональные значения, отсутствие которых в рантайме вполне нормально.
Глаза у меня добрые, но рубашка — смирительная!
Re[3]: Аннотации
От: Sinix  
Дата: 01.07.15 08:20
Оценка:
Здравствуйте, Qbit86, Вы писали:

Q>Во-первых, CodeContracts, постпроцессинг и Roslyn — это всё для белых людей; увы, в моей текущей реальности есть лишь Unity3D и древний глючной Моно с не менее архаичным компилятором.

Упс

Q>Во-вторых, compile time мне не нужен. Мне нужно возвращать из функций типа Find() или Parse() опциональные значения, отсутствие которых в рантайме вполне нормально.


А. В этих случаях или стандартный "bool Try(out result)", или Try-метод, который возвращает null result при неудаче. И ассерты на входные параметры (при желании — результат проверять тоже). Самый дешёвый способ. Тем более что в релизных сборках ассерты можно не включать.
Re[4]: bool Try(out result)
От: Qbit86 Кипр
Дата: 01.07.15 08:51
Оценка:
Здравствуйте, 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 — это вполне самостоятельная концепция, ортогональная контрактам/аннотациям времени компиляции.
Глаза у меня добрые, но рубашка — смирительная!
Re: Option<T>
От: xy012111  
Дата: 01.07.15 09:29
Оценка:
Здравствуйте, Qbit86, Вы писали:

Q>Какая реализация типа option считается более или менее общеиспользуемой в современном Дотнете? … Требуется, чтобы тип …реализовывал IEnumerable<T>…


IEnumerable<T>? или IEquatable<>? Зачем нужен IEnumerable<T> опшион?
Re[2]: IEnumerable<T>
От: Qbit86 Кипр
Дата: 01.07.15 09:39
Оценка: +1
Здравствуйте, xy012111, Вы писали:

X>IEnumerable<T>? или IEquatable<>?


IEquatable<> само собой.

X>Зачем нужен IEnumerable<T> опшион?


Выше ответил. Опцион можно (и нужно) рассматривать как коллекцию из одного элемента или пустую. Например, мне надо извлечь из словаря int по string'овому ключу, и если этот элемент там есть, и он больше 1729, то передать его куда-то дальше:
Option<Foo> foo = myDict.TryGet("someKey").Where(i => i > 1729).Select(i => new Foo(i)).ToOption();

На каком-то этапе у меня может получится пустая коллекция — не беда, произойдёт «None propagation» до самого верха.
Глаза у меня добрые, но рубашка — смирительная!
Re[5]: bool Try(out result)
От: Sinix  
Дата: 01.07.15 09:55
Оценка:
Здравствуйте, 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.
Re[6]: bool Try(out result)
От: Qbit86 Кипр
Дата: 01.07.15 10:21
Оценка: 20 (1)
Здравствуйте, Sinix, Вы писали:

S>Тогда или c#6 с ?. (в mono тож должно появиться)


Увы, из-за каких-то юридических ньюансов Юнитеки не могут обновить (модифицированную) Моно в своей поставке Unity3D начиная с версии, когда Мигель де Икаса изменил лицензию.
В какой-то степени оно всё CIL-совместимо и теоретически можно писать нескриптовый код в 2015 Студии на новом C# 6 под старый .NET 3.5, но там свои органичения и подводные грабли с AOT на iOS.

S>См пример выше. На практике это безумно неудобно.


Привёл пример ниже, имхо, вполне удобно.

S>В реальном коде проще гаранитровать ассертами, что null-ов не будет


Как же не будет, если могут быть в рантайме?
Глаза у меня добрые, но рубашка — смирительная!
Re[3]: IEnumerable<T>
От: Sinclair Россия https://github.com/evilguest/
Дата: 01.07.15 10:40
Оценка: +1
Здравствуйте, Qbit86, Вы писали:
Q>Выше ответил. Опцион можно (и нужно) рассматривать как коллекцию из одного элемента или пустую. Например, мне надо извлечь из словаря 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)
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[4]: IEnumerable<T>
От: Qbit86 Кипр
Дата: 01.07.15 10:51
Оценка:
Здравствуйте, 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>.
Глаза у меня добрые, но рубашка — смирительная!
Re[5]: IEnumerable<T>
От: Sinix  
Дата: 01.07.15 11:05
Оценка: 7 (1)
Здравствуйте, Qbit86, Вы писали:

Q> Но, по-моему, Option<T> всё-таки является IEnumerable<T>.

Это из серии "всё является object, но по факту почему-то передают конкретные типы"

Раз уж завели отдельный тип с своим контрактом (к примеру IEnumerator.Current у вас не может быть null), то проще объявить его явно, чем надеяться, что никто не передаст Select(x=>default(T)).
Re[3]: Аннотации
От: alexzz  
Дата: 01.07.15 13:10
Оценка:
Здравствуйте, Qbit86, Вы писали:

Q>Во-первых, CodeContracts, постпроцессинг и Roslyn — это всё для белых людей; увы, в моей текущей реальности есть лишь Unity3D и древний глючной Моно с не менее архаичным компилятором.

Q>Во-вторых, compile time мне не нужен. Мне нужно возвращать из функций типа Find() или Parse() опциональные значения, отсутствие которых в рантайме вполне нормально.

У меня есть куча функций, который ищут пересечения между прямыми, лучами, отрезками и плоскостями. Я для возвращаемых значений использую Nullable<T>. Получается намного НАМНОГО удобнее, чем "bool Try(out result)".
Re[4]: Try-Parse pattern
От: Qbit86 Кипр
Дата: 01.07.15 13:16
Оценка:
Здравствуйте, alexzz, Вы писали:

A>Получается намного НАМНОГО удобнее, чем "bool Try(out result)".


Так я о том и говорю, что Try-Parse pattern из FDG ужасен.
Глаза у меня добрые, но рубашка — смирительная!
Re[5]: Try-Parse pattern
От: Sinix  
Дата: 01.07.15 13:38
Оценка: :)
Здравствуйте, Qbit86, Вы писали:


Q>Так я о том и говорю, что Try-Parse pattern из FDG ужасен.


Проблема не в инструменте, проблема в том, что вы его не по назначению используете.
Try-parse не заточен под кривой дизайн в стиле "цепочка операций, каждая может вернуть результат "нишмогла"". Для инфраструктуры это _очень_ плохой подход по очевидным причинам.

С появлением null propagating operator по тем же причинам код превратится из "очень плохой" просто в "плохой", но делу это не сильно поможет.

P.S. Для полной красоты не хватает только declaration expressions, но это уже к шестому шарпу.

P.P.S. Пост от alexzz, кстати, идеальный пример такого неправильного подхода. Для интервалов вместо nullable надо использовать свою обёртку, иначе две трети сценариев отпадает.
Самое простое: как отличить -∞ от +∞? Что насчёт замкнутых/открытых интервалов (wiki)? Пустое множество?
ну и тд.
Отредактировано 01.07.2015 13:45 Sinix . Предыдущая версия .
Re[6]: Haskell, F#, Scala
От: Qbit86 Кипр
Дата: 01.07.15 13:47
Оценка: +3
Здравствуйте, Sinix, Вы писали:

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

S>Try-parse не заточен под кривой дизайн в стиле "цепочка операций, каждая может вернуть результат "нишмогла"". Для инфраструктуры это _очень_ плохой подход по очевидным причинам.

В инфраструктуре стандартных библиотек нормальных языков типа Haskell, F#, Scala в try-get-сценарях как раз используется именно Option (Maybe). Например, лукап в хэш-таблице или поиск в коллекции. Просто разработчики .NET Framework не придумали сразу этот примитив (ограничившись ущербным Nullable<T>), вот API в Dictionary<K, V> и выглядит так криво. Но это не норма, пойми.
Глаза у меня добрые, но рубашка — смирительная!
Re[7]: bool Try(out result)
От: alexzz  
Дата: 01.07.15 14:14
Оценка: 69 (3)
Здравствуйте, Qbit86, Вы писали:

Q>В какой-то степени оно всё CIL-совместимо и теоретически можно писать нескриптовый код в 2015 Студии на новом C# 6 под старый .NET 3.5, но там свои ограничения и подводные грабли с AOT на iOS.


Я приделал к Unity два компилятора C# 6: майкрософтовский и из поставки Mono 4.0.0.

http://forum.unity3d.com/threads/c-6-0.314297/#post-2108999
http://www.gamedev.ru/flame/forum/?id=151586&amp;page=32#m472

Также приспособил библиотеку AsyncBridge.Net35.dll, теперь могу использовать async/await прямо в скриптах вместо сопрограмм.

Не знаю, можно ли этим пользоваться в продакшене, пока играюсь, смотрю, какие вылезут косяки. В редакторе, в билдах под Windows и на Андроиде у меня заработали все фичи C# 6. Т.е. вообще всё, кроме dynamic, естественно. Под WebGL IL2CPP не смог скомпилировать фильтры исключений и библиотека AsyncBridge ему не понравилась, т.е. недоступны async/await и атрибуты caller information.

iOS у меня нет, поэтому проверить AOT не могу, хотя было бы интересно. Вот тут чувак пишет, что iOS поддерживается.


Между компиляторами Roslyn и Mono нашёл только одно отличие. Моновский не понимает, что у bar должен быть тип Nullable<int>.
var foo = new[] { 1, 2, 3 };
var bar = foo?[0];

Компилятор из нового Mono 4.0.1 пока не проверял.
Re[8]: bool Try(out result)
От: Qbit86 Кипр
Дата: 01.07.15 14:19
Оценка:
Здравствуйте, alexzz, Вы писали:

A>Я приделал к Unity два компилятора C# 6: майкрософтовский и из поставки Mono 4.0.0.

A>http://forum.unity3d.com/threads/c-6-0.314297/#post-2108999

Да, читал эту ветку, и даже отписался там :)

A>Также приспособил библиотеку AsyncBridge.Net35.dll, теперь могу использовать async/await прямо в скриптах вместо сопрограмм... Не знаю, можно ли этим пользоваться в продакшене, пока играюсь, смотрю, какие вылезут косяки.


У меня в своё время бэкпорт TPL (и AsyncBridge?) не заработал на iOS. На Android вроде работало, но нам нужны все мобильные платформы.
Глаза у меня добрые, но рубашка — смирительная!
Re[7]: Haskell, F#, Scala
От: Sinix  
Дата: 01.07.15 14:26
Оценка:
Здравствуйте, Qbit86, Вы писали:


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)
{
  // ...
}
?
Re[6]: Try-Parse pattern
От: alexzz  
Дата: 01.07.15 14:30
Оценка: 40 (1)
Здравствуйте, Sinix, Вы писали:

S>P.P.S. Пост от alexzz, кстати, идеальный пример такого неправильного подхода. Для интервалов вместо nullable надо использовать свою обёртку, иначе две трети сценариев отпадает.


У меня практическая задача. Такие сценарии в ней отсекаются заранее.