Опицональные типы (Optional, Maybe)
это типы которые могут принимать все значения некоторого типа T ( Some<T> ), а также специальное значение None, означающее "отсутствие" данных вообще или нечто подобное.
Несколько вопросов
1. Является ли нулевая ссылка на объект эквивалентом None? Можно ли сказать что нуллабельные ссылочные типы это полноценная реализация опциональных типов?
2. Если у объекта некоторого класса имеется свое собственное, определенное программистом "недействительное" состояние, то может ли это быть то же самое что и None? Кажется что это разные вещи, по крайней мере в сравнении с "нулевой ссылкой": в случае нулевой ссылки объекта просто нет, поэтому невозможно обратиться ни к каким полям и т.д.; в случае пользовательского недействительного состояния недействительность чисто логическая, при этом память под объект есть, поля есть и к ним можно обратиться, можно перевести объект в действительное состояние вызвав какой-то метод и т.д.
Но в то же время такое "пользовательское недействительное состояние" ближе к классической реализации Optional<T>, где для хранения признака None просто заводится булевский флаг, при этом память под объект Some<T> физически также присутствует.
3. В частности, для чисел с плавающей точкой, является ли NaN эквивалентом None? Или это логически разные понятия и их не следует смешивать? Если разные то почему?
4. Какой физический смысл у многократного закручивания опционала в опционал? Optional<Optional<T>>, Optional<Optional<Optional<T>>> и т.п.? Где такое может встретиться и для чего?
Здравствуйте, x-code, Вы писали:
XC>3. В частности, для чисел с плавающей точкой, является ли NaN эквивалентом None? Или это логически разные понятия и их не следует смешивать? Если разные то почему?
Ну, учитывая, что signaling NaN отличается от quiet NaN, тебе потребуется два разных None
А ещё у них мантисса содержит интересные признаки о происхождении.
Как там у Лема — драконы не существуют, но при этом они бывают отрицательные, мнимые и нулевые.
XC>4. Какой физический смысл у многократного закручивания опционала в опционал? Optional<Optional<T>>, Optional<Optional<Optional<T>>> и т.п.? Где такое может встретиться и для чего?
Здравствуйте, netch80, Вы писали:
N>Здравствуйте, x-code, Вы писали:
XC>>4. Какой физический смысл у многократного закручивания опционала в опционал? Optional<Optional<T>>, Optional<Optional<Optional<T>>> и т.п.? Где такое может встретиться и для чего?
N>Сомневаюсь, что он зачем-то такой нужен.
Допустим, есть некий сервис, возвращающий Optional<T>. И есть некая обертка, обращающаяся к этому сервису. Возможна ситуация, когда вызывающему коду важно, на каком уровне произошел отказ, получен ли отказ от самого сервиса, или от обертки. Следует ли делать повторные обращения через какое-то время?
Высосал из пальца.
Здравствуйте, x-code, Вы писали:
XC>1. Является ли нулевая ссылка на объект эквивалентом None? Можно ли сказать что нуллабельные ссылочные типы это полноценная реализация опциональных типов?
На самом деле у опциональных типов есть одна киллер фича по сравнению с просто null. Я долго не мог понять, какого хрена ввели такой тип, если один фиг делаешь get и потом проверяешь на тип. А потом прочитал и понял. Над опциональным типом определены операции map и flatMap. Соответственно ты можешь написать что то вроде:
myOrganisationList.flatMap(getPersons)
.flatMap(getOrganisations)
.flatMap(getName);
И таким образом ты уберешь явные проверки на null.
Хотя ИМХО вот такой монадический стиль один черт является костылем, и гораздо лучше, если на уровне языка есть нормальная поддержка работы с null, например через оператор ?. . Ну и not null типы. В этом случае код становится гораздо более читаемый. А эти все монады — это на деле просто костыль для решения проблем, которые в новых языках можно решать и без монад.
Здравствуйте, samius, Вы писали:
XC>>>4. Какой физический смысл у многократного закручивания опционала в опционал? Optional<Optional<T>>, Optional<Optional<Optional<T>>> и т.п.? Где такое может встретиться и для чего?
N>>Сомневаюсь, что он зачем-то такой нужен. S>Допустим, есть некий сервис, возвращающий Optional<T>. И есть некая обертка, обращающаяся к этому сервису. Возможна ситуация, когда вызывающему коду важно, на каком уровне произошел отказ, получен ли отказ от самого сервиса, или от обертки. Следует ли делать повторные обращения через какое-то время? S>Высосал из пальца.
Мнэээ... 7 раз обернуть в optional — делать повторную попытку через 7 секунд, так? А потом дойдём до 500 обёрток?
Здравствуйте, netch80, Вы писали:
N>Здравствуйте, samius, Вы писали:
S>>Допустим, есть некий сервис, возвращающий Optional<T>. И есть некая обертка, обращающаяся к этому сервису. Возможна ситуация, когда вызывающему коду важно, на каком уровне произошел отказ, получен ли отказ от самого сервиса, или от обертки. Следует ли делать повторные обращения через какое-то время? S>>Высосал из пальца.
N>Мнэээ... 7 раз обернуть в optional — делать повторную попытку через 7 секунд, так? А потом дойдём до 500 обёрток?
Можно, конечно, сделать полиморфню функцию, которая будет ждать 500 сек, приняв 500-кратную обертку на входе, и даже заставить выводить тип компилятор. Проблема в том, что бы объявить функцию, которая возвращает в качестве результата и 7и кратную обертку и 500 кратную тоже. Можно и ее решить, спрятав n-кратную обертку за полиморфно-рекурсивным типом, как у Окасаки. (Кстати, в реализации полиморфно-рекурсивных структур данных рекурсивный Optional<T> может занять достойное место)
Но я вообще не предлагал прятать количественные характеристики в n-кратных обертках, где maxN определяется во время выполнения. Хоть это и возможно, но не очень удобно по сравнению с обычным представлением чисел.
Я немного о другом. При прохождении через слои системы, каждый слой может добавлять к ответу свою опциональность. По аналогии с исключениями, но с той разницей, что исключительной ситуации не возникло. Для одного слоя семантика Optional может означать временную недоступность, для другого — несоответствие бизнес-правилам. Тогда ответ со вложенными Optional<T> однозначно определяет уровень, на котором произошел отказ. Не утверждаю что это правильный подход и что он лучше, чем то, что можно организовать на исключениях или Either.
Здравствуйте, samius, Вы писали:
S>Можно, конечно, сделать полиморфню функцию, которая будет ждать 500 сек, приняв 500-кратную обертку на входе, и даже заставить выводить тип компилятор. Проблема в том, что бы объявить функцию, которая возвращает в качестве результата и 7и кратную обертку и 500 кратную тоже. Можно и ее решить, спрятав n-кратную обертку за полиморфно-рекурсивным типом, как у Окасаки. (Кстати, в реализации полиморфно-рекурсивных структур данных рекурсивный Optional<T> может занять достойное место)
Как пример красивого, но совершенно бесполезного решения? Ну может быть...
S>Я немного о другом. При прохождении через слои системы, каждый слой может добавлять к ответу свою опциональность. По аналогии с исключениями, но с той разницей, что исключительной ситуации не возникло. Для одного слоя семантика Optional может означать временную недоступность, для другого — несоответствие бизнес-правилам. Тогда ответ со вложенными Optional<T> однозначно определяет уровень, на котором произошел отказ. Не утверждаю что это правильный подход и что он лучше, чем то, что можно организовать на исключениях или Either.
Вот именно. Я приводил пример с sNaN против qNaN. Или ещё пример: в системе мониторинга, которую я когда-то делал, отсутствие значение датчика имело варианты: датчика нет, ответ не получен, явный отказ датчика вернуть значение.
Чтобы это нормально передать, нужен не Optional сам по себе, а что-то из следующих вариантов:
* Erlang: {ok, значение} | {error, ошибка} (где "ошибка" — данное неограничиваемого типа и значения, описывающее проблему)
* Если делать на Optional, то это будет что-то в духе
причём в идеале один и только один из них может иметь реальное значение; как это устроить реально в конкретном языке — уже будет зависеть от него средств. Вот на качественное теоретическое обеспечение этого ограничения — можно и потратиться.
Что-то вспомнился Паскаль с его "записями с вариантами". Ведь было уже такое:
type response = record
case resp_type: (ok, err) of
ok: (result: ResultType);
err: (error: ErrorType);
end;
Здравствуйте, x-code, Вы писали:
XC>4. Какой физический смысл у многократного закручивания опционала в опционал? Optional<Optional<T>>, Optional<Optional<Optional<T>>> и т.п.? Где такое может встретиться и для чего?
Вот реальный пример для Optional<Optional<T>>. Игровой сервер рассылает клиентам информацию об интересующих их юнитах (например, о выделенной цели, о членах сквада, просто о всех видимых соседях и т.п.). В протокольном IDL эта информация описывается рекордом с кучей опциональных значений, например:
record UnitUpdate
{
Optional<float> hp;
Optional<int> level;
Optional<ClanID> clan;
}
Опциональность нужна, чтобы пересылать только изменившиеся значения, а не каждый раз весь рекорд целиком.
А теперь представим, что игрок может в любой момент выйти из клана, и всех подписчиков нужно об этом уведомить, но как? Самое простое:
Optional<Optional<ClanID>> clan;
В этом случае наличие значения clan означает, что информация передана клиенту, наличие значения clan.value означает, что пришел новый клан игрока, а его отсутствие — что игрок больше не состоит в клане.
Ну и при попытке сгенерировать для приведенного рекорда адекватный код для таргет-языка, не имеющего опциональных типов, сразу становится ясен ответ на этот вопрос: XC> 1. Является ли нулевая ссылка на объект эквивалентом None? Можно ли сказать что нуллабельные ссылочные типы это полноценная реализация опциональных типов?
Здравствуйте, netch80, Вы писали:
N>Здравствуйте, samius, Вы писали:
S>>Я немного о другом. При прохождении через слои системы, каждый слой может добавлять к ответу свою опциональность. По аналогии с исключениями, но с той разницей, что исключительной ситуации не возникло. Для одного слоя семантика Optional может означать временную недоступность, для другого — несоответствие бизнес-правилам. Тогда ответ со вложенными Optional<T> однозначно определяет уровень, на котором произошел отказ. Не утверждаю что это правильный подход и что он лучше, чем то, что можно организовать на исключениях или Either.
N>Вот именно. Я приводил пример с sNaN против qNaN. Или ещё пример: в системе мониторинга, которую я когда-то делал, отсутствие значение датчика имело варианты: датчика нет, ответ не получен, явный отказ датчика вернуть значение. N>Чтобы это нормально передать, нужен не Optional сам по себе, а что-то из следующих вариантов:
N>* Erlang: {ok, значение} | {error, ошибка} (где "ошибка" — данное неограничиваемого типа и значения, описывающее проблему)
N>* Если делать на Optional, то это будет что-то в духе
N>
N>причём в идеале один и только один из них может иметь реальное значение; как это устроить реально в конкретном языке — уже будет зависеть от него средств. Вот на качественное теоретическое обеспечение этого ограничения — можно и потратиться.
Я ведь упомянул Either. Он именно такой — либо одно, либо другое. По сути это расширенный Option, который обладает записью о причине отсутствия значения. Его так же можно применять рекурсивно.
Здравствуйте, x-code, Вы писали:
XC>1. Является ли нулевая ссылка на объект эквивалентом None? Можно ли сказать что нуллабельные ссылочные типы это полноценная реализация опциональных типов?
XC>2. Если у объекта некоторого класса имеется свое собственное, определенное программистом "недействительное" состояние, то может ли это быть то же самое что и None? Кажется что это разные вещи, по крайней мере в сравнении с "нулевой ссылкой": в случае нулевой ссылки объекта просто нет, поэтому невозможно обратиться ни к каким полям и т.д.; в случае пользовательского недействительного состояния недействительность чисто логическая, при этом память под объект есть, поля есть и к ним можно обратиться, можно перевести объект в действительное состояние вызвав какой-то метод и т.д.
XC>Но в то же время такое "пользовательское недействительное состояние" ближе к классической реализации Optional<T>, где для хранения признака None просто заводится булевский флаг, при этом память под объект Some<T> физически также присутствует.
XC>3. В частности, для чисел с плавающей точкой, является ли NaN эквивалентом None? Или это логически разные понятия и их не следует смешивать? Если разные то почему?
Все пункты являются. Но есть важное отличие. У нулябельной ссылки есть "неявный оператор приведения" к целевому типу, который кидает исключение (или валит программу) в случае NULL-а, в большинстве ЯП. То же про float и пользовательский тип. Это большой недостаток. Если он исправлен (например в Kotlin так), тогда вообще проблем нет. Разве что желательны какие-то утилитные методы над этим нулябельным типом вроде map, чтобы меньше писать. Т.е. смысл опциональных типов в проверке компилятором, чтобы баги были на уровне системы типов, а если компилятор проверил программу и ошибок не нашёл, значит этих багов там нет.
XC>4. Какой физический смысл у многократного закручивания опционала в опционал? Optional<Optional<T>>, Optional<Optional<Optional<T>>> и т.п.? Где такое может встретиться и для чего?
Some(X), None1, None2, ..., т.е. перечисление с кучей альтернатив. На мой взгляд это признак говнокода, нужно вводить свой тип. Это как возвращать int[] из двух элементов, вместо создания типа Point.
Здравствуйте, samius, Вы писали:
S>Я ведь упомянул Either.
Без URL на спеку или просто описание словами — цена такому упоминанию ноль целых фиг десятых, потому что по такому распространённому слову ничего не гуглится.
S> Он именно такой — либо одно, либо другое. По сути это расширенный Option, который обладает записью о причине отсутствия значения.
Ну тогда это как раз, что нужно в нормальном рабочем варианте.
S> Его так же можно применять рекурсивно.
Хм, это выглядит немного более осмысленным, чем рекурсивный Optional, но всё равно странно.
_>А теперь представим, что игрок может в любой момент выйти из клана, и всех подписчиков нужно об этом уведомить, но как? Самое простое:
_>Optional<Optional<ClanID>> clan;
_>В этом случае наличие значения clan означает, что информация передана клиенту, наличие значения clan.value означает, что пришел новый клан игрока, а его отсутствие — что игрок больше не состоит в клане.
Логика туманная и крайне неудобная для запоминания. Рекламируемый тут по соседству Either выглядит всё-таки более сопровождаемым.
Здравствуйте, netch80, Вы писали: _>>Optional<Optional<ClanID>> clan; _>>В этом случае наличие значения clan означает, что информация передана клиенту, наличие значения clan.value означает, что пришел новый клан игрока, а его отсутствие — что игрок больше не состоит в клане. N>Логика туманная и крайне неудобная для запоминания. Рекламируемый тут по соседству Either выглядит всё-таки более сопровождаемым.
Я не очень понял, чем здесь поможет Either, к тому же в мэйнстрим языках с Either еще хуже чем с Optional.
Что до туманности и сопровождаемости, попробую переубедить с помощью реального кода использования.
Вот как в реальности выглядит код использования unit_update на Эрланговском сервере:
% Вызывается при изменении HP
unit_update:set(State, hp, HP)
Таких вызовов за одну транзацию (обработку одного сообщения) может быть несколько (так как могут произойти изменения нескольких полей), соответственно все они буферизуются, и только в случае успешного завершения транзакции итоговый пакет будет отправлен клиентам.
В итоге пользователи фрэймворка усваивают очень простой паттерн: для обновления клиентской информации нужно вызвать unit_update:set для соответствующего поля. Тогда следующий код выглядит для них естественным, и они ожидают, что он будет работать:
%% при вступлении в клан
unit_update:set(State, clan, Clan)
%% при выходе из клана
unit_update:set(State, clan, undefined)
Какие тут могут быть альтернативы?
Немного дикости
%% при вступлении в клан
unit_update:set(State, clan, {some, Clan},
%% при выходе из клана
unit_update:set(State, clan, none)
с точностью до именования тэгов. Для пользователя это менее удобно и более запутано.
Другие опробованные на практике варианты выглядят еще более неожиданно:
unit_update:set(State, is_in_clan, false)
А это и вовсе дико
% Clan - числовой Id
ClanValue = case Clan of undefined -> 0; _ -> Clan end,
unit_update:set(State, clan, ClanValue)
А вот примерный код клиента:
public int HP { get; private set; }
public int? Clan { get; private set; }
void Update(UnitUpdate update)
{
if (update.HP.HasValue)
this.HP = update.HP.Value;
if (update.Clan.HasValue)
this.Clan = update.Clan.Value;
}
Вот разве это туманно и неудобно для запоминания? Единственная беда — на C# так нельзя, во всяком случае без кастомных опциональных типов.
Здравствуйте, x-code, Вы писали:
XC>4. Какой физический смысл у многократного закручивания опционала в опционал? Optional<Optional<T>>, Optional<Optional<Optional<T>>> и т.п.? Где такое может встретиться и для чего?
Маловероятно, что такое может понадобиться. Например, в парсинге вложенных элементов XML, JSON, etc. При этом некоторые элементы могут отсутствовать.
Здравствуйте, netch80, Вы писали:
XC>>4. Какой физический смысл у многократного закручивания опционала в опционал? Optional<Optional<T>>, Optional<Optional<Optional<T>>> и т.п.? Где такое может встретиться и для чего? N>Сомневаюсь, что он зачем-то такой нужен.
Очень нужен.
Без него обобщённый код работать не будет.
Например:
class Hashtable<Tkey, TValue>
{
public Option<TValue> Get(TKey key)
{
...
}
}
Теперь если у нас есть обобщённый код который работает с хештаблицей и в него передадут Hashtable<int, Option<string>> то без этой фичи код не будет знать есть элемент или в таблице лежит None.
Из-за этого алгоритм может сломаться.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, netch80, Вы писали:
N>Здравствуйте, samius, Вы писали:
N>Без URL на спеку или просто описание словами — цена такому упоминанию ноль целых фиг десятых, потому что по такому распространённому слову ничего не гуглится.
В сочетании с "data type" отлично гуглится. Впрочем, извиняюсь, что не дал ссылку. Например, тут (перед Either идет описание Maybe — оно как раз Optional<T>).
S>> Он именно такой — либо одно, либо другое. По сути это расширенный Option, который обладает записью о причине отсутствия значения.
N>Ну тогда это как раз, что нужно в нормальном рабочем варианте.
S>> Его так же можно применять рекурсивно.
N>Хм, это выглядит немного более осмысленным, чем рекурсивный Optional, но всё равно странно.
Странно, что список списков никого не удивляет.
Здравствуйте, vsb, Вы писали:
vsb>Все пункты являются. Но есть важное отличие. У нулябельной ссылки есть "неявный оператор приведения" к целевому типу, который кидает исключение (или валит программу) в случае NULL-а, в большинстве ЯП. То же про float и пользовательский тип. Это большой недостаток. Если он исправлен (например в Kotlin так), тогда вообще проблем нет.
А можете рассказать поподробнее, что там в Kotlin?
Здравствуйте, x-code, Вы писали:
vsb>>Все пункты являются. Но есть важное отличие. У нулябельной ссылки есть "неявный оператор приведения" к целевому типу, который кидает исключение (или валит программу) в случае NULL-а, в большинстве ЯП. То же про float и пользовательский тип. Это большой недостаток. Если он исправлен (например в Kotlin так), тогда вообще проблем нет.
XC>А можете рассказать поподробнее, что там в Kotlin?
Optional на уровне языка и реализован через ссылочный тип, т.е. максимально эффективно. Есть немного "синтаксического сахара", позволяющего со всем этим работать удобно.
var s1: String? // "?" означает, что s1 имеет nullable тип, т.е. может принимать строки или null
s1 = null
s1 = "something"var s2: String // null принимать не может
s2 = s1 // ошибка
s2 = s1!! // так можно, в случае null будет выброшено исключениеif (s1 == null) {
s2 = "empty"
} else {
s2 = s1
} // так тоже можно, компилятор отслеживает проверки и внутри else будет знать, что s1 гарантированно не null, поэтому его тип внутри этого блока меняется, собственно это и позволяет писать нормальный код без проблем
s2 = s1 ?: "empty"// то же самое, но с более удобным синтаксисом
val s1Length: Int? = s1?.length // ?. проверяет s1 на null и выполняет функцию, если не null либо возвращает null
val s2LengthOrZero: Int = s1?.length ?: 0 // комбинируя предыдущие два пункта
Здравствуйте, WolfHound, Вы писали:
WH>Здравствуйте, netch80, Вы писали:
XC>>>4. Какой физический смысл у многократного закручивания опционала в опционал? Optional<Optional<T>>, Optional<Optional<Optional<T>>> и т.п.? Где такое может встретиться и для чего? N>>Сомневаюсь, что он зачем-то такой нужен. WH>Очень нужен. WH>Без него обобщённый код работать не будет. WH>Например:
Ну, для ссылочных типов такую же семантику умудрились сделать без накручивания вложенностей определений.
Просто есть специальное значение, приводимое (сравнимое) с произвольным типом — null.
Для алг. типов данных — тоже. Правда, эти типы данных всегда обрабатываются как ссылочные, даже если не ссылочные. ))
WH>Теперь если у нас есть обобщённый код который работает с хештаблицей и в него передадут Hashtable<int, Option<string>>
Если string — это ссылочный тип, то Option<string> не имеет смысла, можно пользоваться сразу string.
WH>то без этой фичи код не будет знать есть элемент или в таблице лежит None.
Это всё рассуждения в пользу бедных, потому что прикладная семантика может быть обслужена сугубо прикладными же полями, а не "системными". И даже должна, потому что булевских флагов, связанных со значениям, может быть более одного.
А в общем случае достаточно интерпретировать null как "нет значения".
Тебе не надоело позориться? Опять же бред написал.
V>Ну, для ссылочных типов такую же семантику умудрились сделать без накручивания вложенностей определений. V>Просто есть специальное значение, приводимое (сравнимое) с произвольным типом — null.
Это аналог Optional<T>.
У Optional<Optional<T>> ДВА различных null'а. У Optional<Optional<Optional<T>>> три.
V>Для алг. типов данных — тоже. Правда, эти типы данных всегда обрабатываются как ссылочные, даже если не ссылочные. ))
Бред.
V>Если string — это ссылочный тип, то Option<string> не имеет смысла, можно пользоваться сразу string.
Не во всех языках есть null. Ибо null зло.
V>Это всё рассуждения в пользу бедных, потому что прикладная семантика может быть обслужена сугубо прикладными же полями, а не "системными". И даже должна, потому что булевских флагов, связанных со значениям, может быть более одного.
Опять чистейший бред.
WH>>то без этой фичи код не будет знать есть элемент или в таблице лежит None. V>А в общем случае достаточно интерпретировать null как "нет значения".
Не достаточно.
class Hashtable<Tkey, TValue>
{
public Option<TValue> Get(TKey key)
{
...
}
}
class CachedFactory<Tkey, TValue>
{
private Hashtable<Tkey, TValue> _cache = new Hashtable<Tkey, TValue>();
private Tkey -> TValue _factory;
public CachedFactory(Tkey -> TValue factory)
{
_factory = factory;
}
public TValue Get(TKey key)
{
if (_cache.Get(key) is Some(value))
return value;
else
{
var value = _factory(key);
_cache.Add(key, value);
return value;
}
}
}
Если сюда засунуть тяжёлую функцию (а CachedFactory именно для таких функций и нужно) которая возвращает Option<что-что> то в твоём варианте алгоритм не сможет определить была вызвана функция или нет.
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн