Здравствуйте, _Claus_, Вы писали:
_C_>собственно интуитивно ожидается, что должен работать и возвращать Value, однако же нет. _C_>недоработка или что-то другое?
?. защищает доступ к членам. Откровенно говоря я не предполагал, что кто-то будет использовать его с option[T] для обхода None.
С другой стороны ?? родственный оператор и он работает с option[T]. Так что даже не знаю.
К тому же option[T] — это сам по себе ссылочный тип и члены возвращающие этот тип могут содержать null. Так что если изменить его поведение, то могут быть проблемы в других местах.
В общем, хотелось бы услышать другие мнения. Нужно ли прикручивать поддержку option[T] к "?.", и что при этом делать с проверкой на null самого члена типа option[T]?
ЗЫ
Чем больше думаю над option[T], тем больше понимаю, что надо было его структурой делать.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, _Claus_, Вы писали:
_C_>собственно интуитивно ожидается, что должен работать и возвращать Value, однако же нет. _C_>недоработка или что-то другое?
Да. Забыл добавить. Ты можешь использовать метод WithDefault. Он делает ровно что тебе нужно.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
VD>В общем, хотелось бы услышать другие мнения. Нужно ли прикручивать поддержку option[T] к "?.", и что при этом делать с проверкой на null самого члена типа option[T]?
VD>ЗЫ
VD>Чем больше думаю над option[T], тем больше понимаю, что надо было его структурой делать.
хотел вот именно это все сразу написать, но потом подумал, что это как бы очевидно и так, и не стоит.
но ессно option[T] должна быть структурой по реализации, даже если ее удобно показывать как вариант.
иначе накладные расходы и странности в использовании.
соответственно и ?. должен вести себя предсказуемо.
Здравствуйте, catbert, Вы писали:
C>Здравствуйте, hardcase, Вы писали:
H>>Это несовсем верно. У ?. ленивая семантика, тогда как у WithDefault — энергичная.
C>Это как? Если я передам a?.b в метод, оно не будет вычисляться пока аргумент не понадобится?
Речь идет не о передачи значения всего выражения, но о вычислении правого аргумента. Праый аргумент операторов ?. и ?? вычисляется лишь при условии.
Здравствуйте, _Claus_, Вы писали:
_C_>но ессно option[T] должна быть структурой по реализации, даже если ее удобно показывать как вариант.
По моему надо с любым вариантом сделать что то подобное -- запретить любым вариантам принимать значение null.
В хаскеле, например, Алгебраические типы (читай варианты) по поределению не могут быть null, собственно и понятия такого как null там нет.
Но у нас есть пережиток .NET'a как NULL.
Ну вообще исключать null из языка -- гемор в использовании, но в случаи с вариантами null не нужен. Например как list может быть null? по логике он должен быть либо [], либо [a1..an]. То же можно сказать про любой другой вариант, там где нужно неопределенное значение можно использовать option.
Поясню по другому: где видим класса -- понимаем что это может быть, либо null, либо экземпляр. В случае с вариантами -- подразумеваем, что это один из вариантов(извините за тавтологию)
Здравствуйте, BogdanMart, Вы писали:
BM>Ну вообще исключать null из языка -- гемор в использовании
Если оборачивать все либы в опции, то гемор, но внешние либы не гарантируют что поля не будут null, даже если это означает ошибку либы. Но лепить визде опшины тоже не дело.
То есть в кошерном коде null не будет по определению(либо варианты, либо свой класс с объявленным контрактом). Ну а там где иидет взаимодейтсвие сс внешним кодом могут быть нулы.
То есть объявить святотатство класса notnull
BM>По моему надо с любым вариантом сделать что то подобное -- запретить любым вариантам принимать значение null.
Наделе можно так сделать:
[NotNull]
class A{};
class B:A{};
class C
{
this()
{
a = A();
} // Error: nonnul immutable field a' must be initializedpublic a:A;
public a':A;
public mutable a'':A; // Error: public fieelds can't expose non-null typesprivate mutable a''':A = A(); // All ok, check for non-null contract will be emited by compiler on use casespublic mutable a'''':A = A(); // Error: public fieelds can't expose non-null typesinternal setA(A aa):void
{
a''' = aa; // поскольку метод internal , то мы уверены что здесь нету нулл'а
}
public setA2(A aa):void// поскольку метод public , то если аа == Null -- бросаем ArgumentException
{
a''' = aa;
}
}
это создаст следующий код(то что не скомпилится выбросил):
[NotNull]
class A{};
[NotNull]
class B:A{};
class C
{
this()
{
a = A();
}
public a:A;
private mutable a''':A = A();
internal setA(A aa):void
{
a''' = aa;
}
internal setA_Internal_N_35483(A aa):void
{
a''' = aa;
}
public setA2(A aa):void
{
if (aa == null)
throw ArgumentException(...)
else
setA_Internal_N_35483(aa)
}
}
// ну и напоследок сладость для доступа к такому методу из другой сборки написанной на немерлеpublic NonNullInteropHelper_Ϡ
{
public static C__setA_Internal_Ϡ_35483(C c, A aa):void// иероглиф, чтобы ненароком с C# не вызвали
{
c.setA_Internal_N_35483(aa)
}
}
можно вообще какой то непечатаемый символ ( обфускатор использует символ конца строки в имени метода,так как CLR просто транслирует Unicode, то ему по фиг, а ни один компилер такое не распарсит
Вопрос целесообразно ли делать такой трюк, чтобы обойти проверку на Null при вызове из другой либы на Nemerle? думаю не очень, не такие большие накладные расходы.
Но если прикрутить нативную поддержку компилятором и других контрактов, то тогда есть смысл.
Здравствуйте, VladD2, Вы писали: VD>В дотнете это физически невозможно. Например, null-ссылка может быть получена внутри массива или внутри структуры созданной дефолтным конструктором.
Да с массивом и структурой действительно лажа..
Но подобную штуку видел в С++/CIL
struct A
{
int i;
String^ s; //Error can't use managed pointer inside unmanged structchar Cs[20];
}
struct val B
{
int i;
String^ s; //All okchar Cs[20]; //Critical warning -- mixed types not allowed
// светится как ошибка, но прагмой специальной можно отключить, но тогда эта струкутра не будет CLS Compilant
}
Так что для таких типов можно запретить массивы/структы.
Ты часто видел массивы варантов? а не листы, например.
можно:
вставлять проверки на Null и исключение по несоотвтесвию контарктов в точах соприкосовения NonNull типов и внешних дотнетовских тпов
запретить использовать наши типы с внешними, небезопасными
какой то свой, "безоасный" тип структа, как в С++/CLI
Здравствуйте, _Claus_, Вы писали:
_C_>но ессно option[T] должна быть структурой по реализации, даже если ее удобно показывать как вариант.
Можно ввести понятие варианта "по умолчанию" для вариантных типов, это может быть любой не гиперболизированный вариант указанный разработчиком.
Например для option это None а для list[T] это Nil итд.
И когда идет матч по варианту, если видим null обрабытваем как вариант по умолчанию.
(подводный камень -- если есть какие то методы)
Здравствуйте, _Claus_, Вы писали:
_C_>но ессно option[T] должна быть структурой по реализации, даже если ее удобно показывать как вариант. _C_>иначе накладные расходы и странности в использовании.
Заинтересовался вопросом накладных расходов на класс по сравнению со структурой ещё из прошлого обсуждения в какой-то другой теме.
В некоторых модулях в рабочем проекте (правда на C#) активно используется самодельный Option вида:
public abstract class Option<T>: IEnumerable<T> {
public abstract bool HasValue { get; }
public abstract T Value { get; }
}
public sealed class Some<T>: Option<T> {
...
}
public sealed class None<T>: Option<T> {
...
}
Из любопытства проверил следующие варианты:
1. Option в реализации, приведённой выше
2. Option без наследования (то есть пустое поле под Value будет даже у None)
3. Option в виде структуры
4. Алгоритм вообще без Option
Дело было недели 3 назад, поэтому конкретных цифр, к сожалению, не осталось.
В целом, первые три варианта отличались совсем незначительно и по большей части разница была только в объёме используемой памяти и количестве сборок мусора.
Можно даже сказать, что вариант со структурами был несколько медленнее с точки зрения затраченного времени, но эффективнее в плане нагрузки на память и сборщик.
Вариант без Option'ов работал где-то процентов на 30-40 быстрее, чем с оными, но код при этом стал раза в 3 страшнее.
Здравствуйте, Uriel, Вы писали:
U>Дело было недели 3 назад, поэтому конкретных цифр, к сожалению, не осталось. U>В целом, первые три варианта отличались совсем незначительно и по большей части разница была только в объёме используемой памяти и количестве сборок мусора. U>Можно даже сказать, что вариант со структурами был несколько медленнее с точки зрения затраченного времени, но эффективнее в плане нагрузки на память и сборщик. U>Вариант без Option'ов работал где-то процентов на 30-40 быстрее, чем с оными, но код при этом стал раза в 3 страшнее.
Talk is cheap
Было бы интересно посмотеть на бенчмарки и реализацию структуры. Особенно меня смущает реализация IEnumerable<T>.
Здравствуйте, hardcase, Вы писали:
H>Talk is cheap H>Было бы интересно посмотеть на бенчмарки и реализацию структуры. Особенно меня смущает реализация IEnumerable<T>.
Да я понимаю, просто привычка поганая есть удалять всё после того, как оно уже не актуально.
Вечером гляну, может быть остались исходники, с которыми я баловался.
А IEnumerable<T> навеяно реализацией Option'ов в Scala, где их можно пользовать в конструкции for.
Ну и к тому же очень удобно иногда впихивать операции над Option'ами в портянку вызовов LINQ, a-la:
public Option<T> Foo(Bar input);
IEnumerable<T> foo = SomeCollection.SelectMany(_ => Foo(_));
Здравствуйте, Uriel, Вы писали:
U>А IEnumerable<T> навеяно реализацией Option'ов в Scala, где их можно пользовать в конструкции for. U>Ну и к тому же очень удобно иногда впихивать операции над Option'ами в портянку вызовов LINQ, a-la: U>
Не лучше ли такой метод использовать:
IEnumerable<TResult> Choose<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Option<TResult>> selector) { return source.Select(selector).Where(Option.IsSame); }
?
Ну и вообще в качестве примера http://msdn.microsoft.com/en-us/library/ee370544.aspx
Здравствуйте, Uriel, Вы писали:
U>В целом, первые три варианта отличались совсем незначительно и по большей части разница была только в объёме используемой памяти и количестве сборок мусора.
Конечно, сперва нужно смотреть на код. Так ничего сказать нельзя.
Но "отличались совсем незначительно" скорее всего является следствием синтетичности тесто. По видимому в этих тестах объем работы с option был незначителен по сравнению с основном объемом работы.
Но бывают и другие ситуации. Например, в компиляторе немерла option используется очень плотно. При этом каждый раз создаются новые экзепляры. Уверен, что замена его на структуру заметно повысит производительность.
U>Можно даже сказать, что вариант со структурами был несколько медленнее с точки зрения затраченного времени, но эффективнее в плане нагрузки на память и сборщик.
Это зависит от особенностей применения. Если будет создаваться большое количество экземпляров option-а, то структура должна быть значительно шустрее. Плюс на скорость влияет и способ ее возврата. К сожалению CLR генерирует медленный код когда из метода возвращается структура размер которой больше размера указателя.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, hardcase, Вы писали:
VD>>Да. Забыл добавить. Ты можешь использовать метод WithDefault. Он делает ровно что тебе нужно.
H>Это несовсем верно. У ?. ленивая семантика, тогда как у WithDefault — энергичная.
"ленивая" тут не совсем точный термин. Но я понял твою мысль. WithDefault больше похож на ??. Это да.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Вообщем добрался до тех исходников и прогнал тесты ещё раз.
Во-первых, что именно тестировалось. В рабочем проекте есть самодельный механизм сериализации, который, помимо всего прочего, до определённых пределов позволяет контролировать версионность хранимых структур, а также вырезает из результирующего XML элементы и целые поддеревья, если в них записаны default'ные значения. К сожалению, выложить сюда рабочий тест не получится, так как этот механизм сильно завязан на внутренние особенности проекта и я с трудом представляю, как можно отцепить всё это в один небольшой бинарник.
Зато в качестве бонуса я получил модульные и интеграционные тесты, которые позволили убедиться, что после всех манипуляций с Option'ами, код оставался рабочим.
Тем не менее, реализацию Option<T> привести проблем не составляет ("лишние" методы опущены):
public abstract class Option<T>: IEnumerable<T> {
public abstract bool HasValue { get; }
public abstract T Value { get; }
#region foreach support
IEnumerator<T> IEnumerable<T>.GetEnumerator() {
if (HasValue)
yield return Value;
else
yield break;
}
IEnumerator IEnumerable.GetEnumerator() {
return (this as IEnumerable<T>).GetEnumerator();
}
#endregion
public Option<V> Map<V>(Func<T, V> func, Func<V, bool> somePredicate) {
if (HasValue)
return func(Value).ToOption(somePredicate);
else
return Option<V>.None;
}
public Option<V> Map<V>(Func<T, V> func) {
return Map(func, Option<V>.DefaultSomePredicate);
}
...
public static Option<T> None {
get { return _noneInstance; }
}
public static Option<T> Some(T value) {
return new Some<T>(value);
}
internal static readonly Func<T, bool> DefaultSomePredicate = value => value != null;
private static readonly Option<T> _noneInstance = new None<T>();
}
public sealed class Some<T>: Option<T> {
public override bool HasValue {
get { return true; }
}
public override T Value {
get { return _value; }
}
internal Some(T value) {
_value = value;
}
private T _value;
}
public sealed class None<T>: Option<T> {
public override bool HasValue {
get { return false; }
}
public override T Value {
get { throw new NoValueException(); }
}
internal None() {
}
}
public static class OptionHelpers {
public static Option<T> ToOption<T>(this T value, Func<T, bool> somePredicate) {
return somePredicate(value) ? Option<T>.Some(value) : Option<T>.None;
}
public static Option<T> ToOption<T>(this T value) {
return ToOption(value, Option<T>.DefaultSomePredicate);
}
}
Насчёт реализации IEnumerable<T> я уже писал выше. Почему DefaultSomePredicate объявлен так хитро — скажу ниже.
В целом эта штука бывает удобна, когда при работе со внешним кодом нужно по-особенному интерпретировать семантику отсутствия значения.
Например, для строки можно одним махом сказать
str.ToOption(_ => !string.IsNullOrEmpty(_));
Во-вторых, как тестировалось.
Для тестов специально был подготовлен относительно толстый (в рамках условий исходной задачи) XML-файл размером 9 метров.
Затем этот файл десериализовывался в объект и сериализовывался обратно. Данная операция для "весомости" повторялась 3 раза.
Для такого цикла замерялось количество затраченного времени, разница в потреблении оперативки до и после, а также количество запусков сборщика мусора на объектах 0-ого поколения. Сразу скажу, что в деталях работы .NET слишком уж сильно не разбираюсь, поэтому где-то мог допсутить оплошность.
В итоге получилось что-то в этом духе:
private static Results RunSerializationCycle() {
var memBefore = GC.GetTotalMemory(true);
var gccBefore = GC.CollectionCount(0);
var watch = Stopwatch.StartNew();
var times = 3;
for (var i = 0; i < times; ++i) {
var element = _serialization.Deserialize(_xml, typeof(Модель));
var xml = _serialization.Serialize(element);
}
var ms = watch.ElapsedMilliseconds;
var gccAfter = GC.CollectionCount(0);
var memAfter = GC.GetTotalMemory(false);
return new Results(ms, memAfter - memBefore, gccAfter - gccBefore);
}
Данный цикл прогонялся требуемое число раз (я решил делать 5) и результаты банально усреднялись. Кроме этого, перед запуском основных циклов прогонялся так называемый "разогревочный" цикл, который должен был взять на себя все затраты по JIT-компиляции, заполнению внутренних кэшей проекта данными, etc.
В-третьих, что менялось.
Первый тест проводился с той реализацией Option, которая приведена в самом начале поста.
Вторая попытка заключалась в устранении наследования и использовании одного единственного класса. По сути, получилось вот что:
public class Option<T>: IEnumerable<T> {
public bool HasValue { get; private set; }
public T Value {
get {
if (!HasValue)
throw new NoValueException();
return _value;
}
}
...
private Option(T value) {
_value = value;
HasValue = true;
}
private Option() {
HasValue = false;
}
}
Третий заход непосредственно гонялся на структурах.
public struct Option<T>: IEnumerable<T> {
public bool HasValue { get; private set; }
public T Value {
get {
if (!HasValue)
throw new NoValueException();
return _value;
}
}
private Option(T value): this() {
_value = value;
HasValue = true;
}
private Option(int dummy): this() {
HasValue = false;
}
}
Пока писал сюда, подумал, что структуры со свойствами и реализующие интерфейсы — это, наверное, не самое приятное зрелище, но я старался по-минимуму ломать существующий код проекта.
Четвёртый вариант подразумевал переписывание алгоритма таким образом, чтобы Option'ы там вообще не использовались.
В-четвёртых, пара лирических отступлений. VD>> Но "отличались совсем незначительно" скорее всего является следствием синтетичности тесто. По видимому в этих тестах объем работы с option был незначителен по сравнению с основном объемом работы.
Честно говоря, не знаю, как можно достоверно замерить "значительность" работы с Option'ами, но кое-какие цифры всё же приведу.
За один "цикл" проверки создавалось 4.26 миллиона Some и 5.51 миллион None. По результатам профайлинга, количество вызовов методов класса Option сопоставимо только с запросами данных из кэшей.
Ну и обещанный выше финт с DefaultSomePredicate. Признаться, после первого прогона тестов был очень удивлён разницей во времени работы алгоритмов с использованием Option и без него. Разница там была за 50% (в первом посте ошибся немного, прошу прощения ). Причём профайлер говорил, что очень много времени убивается в методе ToOption, но непонятно, где именно. Путём экспериментов с кодом понял, что засада в преобразовании группы методов в объект Func. Если посмотреть на сгенерированный IL, то окажется, что каждого вызова ToOption(value, Option<T>.DefaultSomePredicate) из переданного метода создаётся новый объект типа Func. После этого, оверхэд от использования Option'ов сократился с почти 60% до чуть менее, чем 14%.
Как можно увидеть, оверхэд действительно не такой уж и большой, равно как и разница между конкретными реализациями.
Единственное, что хоть как-то отличается — это нагрузка на память и сборщик мусора. Но даже здесь я не вижу у структур какого-то принципиального преимущества.
Если народ скажет, что тесты отстой и ничего не доказывают — с удовольствием попробую покарёжить реализацию ещё немного.
Здравствуйте, Jack128, Вы писали:
J>Здравствуйте, Uriel, Вы писали:
U>>А IEnumerable<T> навеяно реализацией Option'ов в Scala, где их можно пользовать в конструкции for. U>>Ну и к тому же очень удобно иногда впихивать операции над Option'ами в портянку вызовов LINQ, a-la: U>>
J>Не лучше ли такой метод использовать: J>IEnumerable<TResult> Choose<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Option<TResult>> selector) { return source.Select(selector).Where(Option.IsSame); } J>? J>Ну и вообще в качестве примера http://msdn.microsoft.com/en-us/library/ee370544.aspx
Можно и такой. Не думаю, что это принципиально что-то изменит. По большому счёту, вся эта пляска с IEnumerable<T> была только ради возможности использования Option'ов в foreach. Которая, в свою очередь, осталась привычкой после изучения Scala. Все остальные прелести LINQ'а уже чуть позже нашлись.
Здравствуйте, Uriel, Вы писали:
U>Как можно увидеть, оверхэд действительно не такой уж и большой, равно как и разница между конкретными реализациями. U>Единственное, что хоть как-то отличается — это нагрузка на память и сборщик мусора. Но даже здесь я не вижу у структур какого-то принципиального преимущества. U>Если народ скажет, что тесты отстой и ничего не доказывают — с удовольствием попробую покарёжить реализацию ещё немного.
Судя по результатам и по рассказу у тебя основной алгоритм занимается массивным выделением памяти. На его фоне оверхэд от опшонов уже мало заметен. Но бывает и другая ситуация. Например, вычислительный алгоритм. Память в нем выделяется редко, а вычислений много. В нем все может быть совсем по другому.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.