Nullable/Optional
От: NeoCode  
Дата: 10.12.14 10:13
Оценка:
Предположим, что есть некоторый "опциональный" тип, то есть такой, который может кроме своих основных значений принимать еще и недействительное значение none. Для краткости пусть он обозначается в стиле c# nullable: T?

Рассмотрим вот такой код.
T? x = T(100,"hello"); // некая инициализация опционального типа, x != none
T y;   // обычный (не опциональный) объект
y = x;


в последней строчке присваивание неопциональной переменной опционального значения.
Приходят в голову три варианта, как обрабатывать такое в языке программирования.
1. запретить. Всегда требовать явное преобразование типов (например с помощью методов isNone() и value() ). Наиболее "правильный" способ, но требующий больше всего писанины.
2. разрешить, но если x == none, то выбрасывать исключение
3. разрешить, но в случае если x == none, вычисление всего выражения (до ближайшей точки с запятой) пропускается (в данном случае y останется неинициализированной, но это наиболее простой случай)

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

Интересно вообще обсудить это. Какие решения в каких языках применяются? Что предпочтительнее? Возможны ли еще варианты такого преобразования?
Re: Nullable/Optional
От: Sinix  
Дата: 10.12.14 10:21
Оценка:
Здравствуйте, NeoCode, Вы писали:

NC>в последней строчке присваивание неопциональной переменной опционального значения.

NC>Приходят в голову три варианта, как обрабатывать такое в языке программирования.

А зачем что-то изобретать, когда все грабли уже исхожены?
т.е.
4. Требовать явного приведения "T y = (T)x", не выходит — исключение. Синтаксический сахар типа as, match, ??, ?. — по вкусу.

UPD Про возражение "больше писанины". Введение nullable предполагает, что _все_ типы по умолчанию становятся не-nullable (вопрос про new object[1][0] пока оставим в сторонке), т.е. проверки понадобятся только если api не гарантирует наличие результата. Всякие TryGetValue()/TryParse() как пример.
Отредактировано 10.12.2014 10:24 Sinix . Предыдущая версия .
Re: Nullable/Optional
От: Klapaucius  
Дата: 10.12.14 14:03
Оценка: 42 (3)
Здравствуйте, NeoCode, Вы писали:

NC>1. запретить. Всегда требовать явное преобразование типов (например с помощью методов isNone() и value() ). Наиболее "правильный" способ, но требующий больше всего писанины.


Ну так нужно решать проблему с писаниной.

NC>Интересно вообще обсудить это. Какие решения в каких языках применяются? Что предпочтительнее? Возможны ли еще варианты такого преобразования?


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

Вот как это все работает в библиотеке lens

Разбираем, допустим, xml и что-то делаем с результатом:
> parseLBS "<b><a>42</a></b>" ^. root . entire . el "a" . text . to T.reverse
"24"

Тут практически на каждом этапе может вернуться Nothing: например, может не быть a или в нем текста и т.д.
Также, может вернуться несколько значений
> parseLBS "<b><a>42</a><a>24</a></b>" ^.. root . entire . el "a" . text . to T.reverse
["24","42"]

но это отдельная история
Это можно обработать всеми указанными вами способами без изменения конвейера, заменой одного оператора.
(^?) просто протягивает опциональное значение
> parseLBS "<b><a>42</a></b>" ^? root . entire . el "a" . text . to T.reverse
Just "24"
> parseLBS "<b>42</b>" ^? root . entire . el "a" . text . to T.reverse
Nothing -- нет <a>

В случае множества ответов вернется первый найденный.
(^.) попытается избавится от обертки, свернуть ее с помощью класса Monoid, в котором определена операция объединения элементов и пустой элемент. Для строк такой класс имплементирован в виде конкатенации и пустой строки соответственно:
> parseLBS "<b><a>42</a></b>" ^. root . entire . el "a" . text . to T.reverse  
"24"                                                                             
> parseLBS "<b>42</b>" ^. root . entire . el "a" . text . to T.reverse         
"" -- известно, что возвращать в случае Nothing

Если же такое значение по умолчанию не определено (как для чисел, например, где есть разные осмысленные значения по умолчанию для разных вариантов объединения — 0 и 1) — будет ошибка компиляции.
> parseLBS "<b><a>42</a></b>" ^. root . entire . el "a" . text . to T.unpack . to read :: Int

<interactive>:13:39:
    No instance for (Monoid Int) arising from a use of `entire'
    Possible fix: add an instance declaration for (Monoid Int)

Мы, правда, можем сделать Monoid из чего угодно, с помощью free monoid — списка.
Для возвращения списка есть оператор (^..)
> parseLBS "<b><a>42</a></b>" ^.. root . entire . el "a" . text . to T.reverse
["24"]
> parseLBS "<b>42</b>" ^.. root . entire . el "a" . text . to T.reverse
[]
> parseLBS "<b><a>42</a><a>24</a></b>" ^.. root . entire . el "a" . text . to T.reverse
["24","42"]

Ну и мы можем развернуть результат и выбросить исключение, если разворачивать нечего с помощью оператора (^?!)
> parseLBS "<b><a>42</a></b>" ^?! root . entire . el "a" . text . to T.reverse
"24"
> parseLBS "<b>42</b>" ^?! root . entire . el "a" . text . to T.reverse
"*** Exception: (^?!): empty Fold
'You may call it "nonsense" if you like, but I'VE heard nonsense, compared with which that would be as sensible as a dictionary!' (c) Lewis Carroll
Re: Nullable/Optional
От: 0BD11A0D  
Дата: 15.12.14 14:35
Оценка:
Здравствуйте, NeoCode, Вы писали:

NC>Предположим, что есть некоторый "опциональный" тип, то есть такой, который может кроме своих основных значений принимать еще и недействительное значение none. Для краткости пусть он обозначается в стиле c# nullable: T?


NC>Рассмотрим вот такой код.

NC>
NC>T? x = T(100,"hello"); // некая инициализация опционального типа, x != none
NC>T y;   // обычный (не опциональный) объект
NC>y = x;
NC>


NC>в последней строчке присваивание неопциональной переменной опционального значения.

NC>Приходят в голову три варианта, как обрабатывать такое в языке программирования.
NC>1. запретить. Всегда требовать явное преобразование типов (например с помощью методов isNone() и value() ). Наиболее "правильный" способ, но требующий больше всего писанины.
NC>2. разрешить, но если x == none, то выбрасывать исключение
NC>3. разрешить, но в случае если x == none, вычисление всего выражения (до ближайшей точки с запятой) пропускается (в данном случае y останется неинициализированной, но это наиболее простой случай)

NC>Понятно что можно сочетать все три решения с помощью специальных операторов. Например, по умолчанию запрещать явное преобразование, а с помощью специальных операторов включать второй или третий способ развертки.


1. Сделать библиотечный обобщенный класс, сахар ? оставить.
2. В классе сделать оператор каста к T, который кидает исключение при null.
3. Сделать этот оператор имплиситным. В колстеке при исключении появится лишний уровень, но глаза ломать об return (SearchResult) _SearchResult; не придется.
4. ...
5. Любовь прикладных программистов!
Re: Еще по Nullable/Optional
От: NeoCode  
Дата: 18.12.14 20:45
Оценка:
Еще один вопрос — по реализации.
Допустим, язык программирования для ссылочных типов будет поддерживать опциональность естественным путем — для любого ссылочного типа ссылка на NULL это None.

Для типов, представленных по значению, естественно должна поддерживаться внешняя реализация optional<T> — полностью включающая поля класса T и дополнительное поле тега, которое равно None или Some. Эта реализация должна быть полностью совместима со ссылками на null (в плане сравнения объектов, поведения, преобразования одного представления в другое и т.п.)

Но потенциально есть еще одна возможность. Можно добавить в язык программирования поддержку "none-конструктора". Это специальный конструктор объекта, который конструирует объект с внутренним состоянием, которое для этого класса объектов считается невалидным.
С одной стороны вроде удобно (не нужно дополнительных оберток, памяти на них), но с другой — непонятно во что это выльется. Ведь можно в программе явно (без none-конструктора) создать такой объект простым присваиванием полей. И как с ним работать дальше? По идее, с ним нельзя работать дальше, т.к. формально он равен "none", и нужно сразу же выбрасывать исключение.
Также, если объект держал какие-то ресурсы, и вдруг "случайно" стал none, ресурсы остались неосвобожденными и пропали? В общем фигня получается.
Отредактировано 18.12.2014 20:48 NeoCode . Предыдущая версия .
Re[2]: Еще по Nullable/Optional
От: D. Mon Великобритания http://thedeemon.livejournal.com
Дата: 19.12.14 11:00
Оценка:
Здравствуйте, NeoCode, Вы писали:

NC>Но потенциально есть еще одна возможность. Можно добавить в язык программирования поддержку "none-конструктора". Это специальный конструктор объекта, который конструирует объект с внутренним состоянием, которое для этого класса объектов считается невалидным.


Это ж
http://en.wikipedia.org/wiki/Null_Object_pattern
Re: Nullable/Optional
От: vsb Казахстан  
Дата: 24.12.14 21:52
Оценка:
Здравствуйте, NeoCode, Вы писали:

NC>Предположим, что есть некоторый "опциональный" тип, то есть такой, который может кроме своих основных значений принимать еще и недействительное значение none. Для краткости пусть он обозначается в стиле c# nullable: T?


NC>Рассмотрим вот такой код.

NC>
NC>T? x = T(100,"hello"); // некая инициализация опционального типа, x != none
NC>T y;   // обычный (не опциональный) объект
NC>y = x;
NC>


NC>в последней строчке присваивание неопциональной переменной опционального значения.

NC>Приходят в голову три варианта, как обрабатывать такое в языке программирования.
NC>1. запретить. Всегда требовать явное преобразование типов (например с помощью методов isNone() и value() ). Наиболее "правильный" способ, но требующий больше всего писанины.
NC>2. разрешить, но если x == none, то выбрасывать исключение
NC>3. разрешить, но в случае если x == none, вычисление всего выражения (до ближайшей точки с запятой) пропускается (в данном случае y останется неинициализированной, но это наиболее простой случай)

NC>Понятно что можно сочетать все три решения с помощью специальных операторов. Например, по умолчанию запрещать явное преобразование, а с помощью специальных операторов включать второй или третий способ развертки.


NC>Интересно вообще обсудить это. Какие решения в каких языках применяются? Что предпочтительнее? Возможны ли еще варианты такого преобразования?


1. Java (до 8). Есть примитивный класс int. Есть класс Integer, который может принимать значения int-а, а также значение null. Присваивание возможно, вылетит NullPointerException.

2. Java после 8. Появился тип Optional. Какого-либо сахара для него не предусмотрено, поэтому и проблемы не возникает Как по-мне, самый нормальный способ.

3. Swift. Есть для опциональных типа. T? и T!. Первый примерно как в Java сделан отдельным классом, неявных приведений нет, нужно явно писать x!. Вылетит ошибка при "разыменовании" пустого значения. Второй не требует разыменования, работает, как обычное значение, но кинет ошибку, если там будет пустое значение. Введён, по-моему, больше как костыльный тип, я, по крайней мере, ещё не видел, где его применять не для обхода архитектурных проблем. В общем явно не образец для подражания. В Swft есть возможность писать x?.some, который вычислится в x.some для ненулевого значения и в null для нулевого. Может быть удобно в некоторых случаях (аналог функционального map).

Моё мнение — среднее между запретить и разрешить. Смысл такой. Если есть проверка в коде, то значение уже не считается опциональным и с ним можно работать, как с обычным. Пока проверки не было – нельзя. Например
T? x = getX()
// T z = x; // не скомпилируется
// T z = x.get // кинет исключение, если значение пусто
if (x.isEmpty) {
    print("x is empty")
    return
}
T y = x; // тут компилятор точно знает, что x не empty

такой подход легко реализуется в компиляторе, правильный код выглядит симпатично и удобно, забытые проверки сразу подсвечиваются при компиляции.
Re[2]: Еще по Nullable/Optional
От: vsb Казахстан  
Дата: 24.12.14 22:04
Оценка:
Здравствуйте, NeoCode, Вы писали:

NC>Еще один вопрос — по реализации.

NC>Допустим, язык программирования для ссылочных типов будет поддерживать опциональность естественным путем — для любого ссылочного типа ссылка на NULL это None.

NC>Для типов, представленных по значению, естественно должна поддерживаться внешняя реализация optional<T> — полностью включающая поля класса T и дополнительное поле тега, которое равно None или Some. Эта реализация должна быть полностью совместима со ссылками на null (в плане сравнения объектов, поведения, преобразования одного представления в другое и т.п.)


NC>Но потенциально есть еще одна возможность. Можно добавить в язык программирования поддержку "none-конструктора". Это специальный конструктор объекта, который конструирует объект с внутренним состоянием, которое для этого класса объектов считается невалидным.


null значение даёт удобную возможность не ставить проверки на каждый чих. При разыменовании нулевого указателя процессор сгенерирует прерывание и программа получит сигнал от ОС (вроде так всё происходит). В общем runtime без лишних инструкций получает проверки в нужных местах. Другие варианты будут требовать проверок во всех сомнительных местах и это может быть хуже по производительности.

А так вариант вполне рабочий. float/double неиспользуемый найти можно, там есть "особые" значения. unsigned int – 2^32-1. signed int – -2^31. Разве что может быть непривычно, что эти значения непредставимы. Придётся отрабатывать много всяких угловых случаев, обзятально делать ошибки при переполнении (впрочем это к лучшему). Значение меньше 4 байтов можно просто представлять 4-мя байтами и всё, там вообще проблем нет. Но куча лишних инструкций будет, либо хитрые оптимизации, всё равно не всегда работающие.

NC>С одной стороны вроде удобно (не нужно дополнительных оберток, памяти на них), но с другой — непонятно во что это выльется. Ведь можно в программе явно (без none-конструктора) создать такой объект простым присваиванием полей. И как с ним работать дальше? По идее, с ним нельзя работать дальше, т.к. формально он равен "none", и нужно сразу же выбрасывать исключение.


Тут всё просто. Говорим, что значения 2^32-1 у нас нет и всё тут. Кто-то сделал (2^32-2) + 1? Просто кидаем integer overflow exception и всё. Главное это определять побыстрее и подешевле.

NC>Также, если объект держал какие-то ресурсы, и вдруг "случайно" стал none, ресурсы остались неосвобожденными и пропали? В общем фигня получается.


Этот момент я не очень понял. Чем это отличается от указателя, который держал какие то ресурсы и стал null? Что вообще значит случайно?
Отредактировано 24.12.2014 22:06 vsb . Предыдущая версия . Еще …
Отредактировано 24.12.2014 22:05 vsb . Предыдущая версия .
Re[3]: Еще по Nullable/Optional
От: NeoCode  
Дата: 25.12.14 06:40
Оценка:
Здравствуйте, vsb, Вы писали:

NC>>Также, если объект держал какие-то ресурсы, и вдруг "случайно" стал none, ресурсы остались неосвобожденными и пропали? В общем фигня получается.

vsb>Этот момент я не очень понял. Чем это отличается от указателя, который держал какие то ресурсы и стал null? Что вообще значит случайно?

Например, у нас есть класс. Ну, скажем, для представления картинки. Там есть ширина, высота, информация о формате картинки и указатель на память с пикселами. Допустим, информация о формате — это перечисление enum Fmt { UNDEFINED, MONOCHROME, GRAYSCALE, TRUECOLOR }; допустим, глупый программист решил использовать значение UNDEFINED как признак того что объект картинки невалиден, то есть равен None.
Допустим, он загрузил картинку из файла. Объект владеет ресурсом — памятью с пикселами. Затем он случайно поставил значение формата в UNDEFINED.
Если язык поддерживает "интрузивную нуллабельность" — то такой объект моментально станет равным None, несмотря на то что там внутри неосвобожденный ресурс. Вот что я имел в виду.
(я не знаю языков с поддержкой такого на языковом уровне, возможно поэтому их и нет что это может привести к таким казусам)
Re[4]: Еще по Nullable/Optional
От: vsb Казахстан  
Дата: 25.12.14 06:45
Оценка:
Здравствуйте, NeoCode, Вы писали:

NC>>>Также, если объект держал какие-то ресурсы, и вдруг "случайно" стал none, ресурсы остались неосвобожденными и пропали? В общем фигня получается.

vsb>>Этот момент я не очень понял. Чем это отличается от указателя, который держал какие то ресурсы и стал null? Что вообще значит случайно?

NC>Например, у нас есть класс. Ну, скажем, для представления картинки. Там есть ширина, высота, информация о формате картинки и указатель на память с пикселами. Допустим, информация о формате — это перечисление enum Fmt { UNDEFINED, MONOCHROME, GRAYSCALE, TRUECOLOR }; допустим, глупый программист решил использовать значение UNDEFINED как признак того что объект картинки невалиден, то есть равен None.

NC>Допустим, он загрузил картинку из файла. Объект владеет ресурсом — памятью с пикселами. Затем он случайно поставил значение формата в UNDEFINED.
NC>Если язык поддерживает "интрузивную нуллабельность" — то такой объект моментально станет равным None, несмотря на то что там внутри неосвобожденный ресурс. Вот что я имел в виду.
NC>(я не знаю языков с поддержкой такого на языковом уровне, возможно поэтому их и нет что это может привести к таким казусам)

Ну это типичный случай потерянного указателя, как мне кажется. Язык с GC соберёт мусор, язык с ARC в момент выставления UNDEFINED уменьшит счётчики ссылок на все указатели внутри объекта, освободит память, обнулит указатели. Какой-нибудь С++ может вызвать деструкторы для членов, наверное. Если управление памятью исключительно ручное, то память утечёт, да.
Re[5]: Еще по Nullable/Optional
От: NeoCode  
Дата: 25.12.14 07:15
Оценка:
Здравствуйте, vsb, Вы писали:

vsb>Ну это типичный случай потерянного указателя, как мне кажется. Язык с GC соберёт мусор, язык с ARC в момент выставления UNDEFINED уменьшит счётчики ссылок на все указатели внутри объекта, освободит память, обнулит указатели. Какой-нибудь С++ может вызвать деструкторы для членов, наверное. Если управление памятью исключительно ручное, то память утечёт, да.


Получается так, что при каждом изменении поля с форматом нужно проверять, не стал ли объект в целом равен none. А если указатель на это поле оказался где-то вне класса и оттуда его изменили?
В общем, непонятно, стоит ли такую возможность разрешать, и если да то в каких границах.
Re[6]: Еще по Nullable/Optional
От: vsb Казахстан  
Дата: 25.12.14 07:27
Оценка:
Здравствуйте, NeoCode, Вы писали:
\
vsb>>Ну это типичный случай потерянного указателя, как мне кажется. Язык с GC соберёт мусор, язык с ARC в момент выставления UNDEFINED уменьшит счётчики ссылок на все указатели внутри объекта, освободит память, обнулит указатели. Какой-нибудь С++ может вызвать деструкторы для членов, наверное. Если управление памятью исключительно ручное, то память утечёт, да.

NC>Получается так, что при каждом изменении поля с форматом нужно проверять, не стал ли объект в целом равен none. А если указатель на это поле оказался где-то вне класса и оттуда его изменили?


Если у нас сборщик мусора, то не нужно каждый раз проверять. Сборщик мусора должен быть умным и при обходе графа объектов может проверить всё, что ему надо. Если нет сборщика мусора, то надо проверять, да. Указатель на это поле выдавать какой-нибудь хитрый, наверное.

NC>В общем, непонятно, стоит ли такую возможность разрешать, и если да то в каких границах.


Честно говоря я смысла в ней не вижу. Накладные расходы на лишнее поле невелики, чтобы так за них переживать. А если надо оптимизировать, пользователь и руками может сделать своё "nullable" значение. Вопросов у такого подхода больше, чем ответов.
Re: Nullable/Optional
От: dimgel Россия https://github.com/dimgel
Дата: 29.12.14 01:16
Оценка:
Здравствуйте, NeoCode, Вы писали:

NC>1. запретить. Всегда требовать явное преобразование типов (например с помощью методов isNone() и value() ). Наиболее "правильный" способ, но требующий больше всего писанины.


Плюсую здесь.

NC>2. разрешить, но если x == none, то выбрасывать исключение


Никакого смысла: статическая типизация пролетает, а в рантайме NPE и так вылетит.

NC>3. разрешить, но в случае если x == none, вычисление всего выражения (до ближайшей точки с запятой) пропускается (в данном случае y останется неинициализированной, но это наиболее простой случай)


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