Null ошибка на миллиард долларов

Время на прочтение
5 мин

Количество просмотров 123K

Ошибка дизайна

Именно так и никак иначе: null в C# — однозначно ошибочное решение, бездумно скопированное из более ранних языков.

  1. Самое страшное: в качестве значения любого ссылочного типа может использоваться универсальный предатель — null, на которого никак не среагирует компилятор. Зато во время исполнения легко получить нож в спину — NullReferenceException. Обрабатывать это исключение бесполезно: оно означает безусловную ошибку в коде.
  2. Перец на рану: сбой (NRE при попытке разыменования) может находится очень далеко от дефекта (использование null там, где ждут полноценный объект).
  3. Упитанный пушной зверек: null неизлечим — никакие будущие нововведения в платформе и языке не избавят нас от прокаженного унаследованного кода, который физически невозможно перестать использовать.

Этот ящик Пандоры был открыт еще при создании языка ALGOL W великим Хоаром, который позднее назвал собственную идею ошибкой на миллиард долларов.

Лучшая историческая альтернатива

Разумеется, она была, причем очевидная по современным меркам

  1. Унифицированный Nullable для значимых и ссылочных типов.
  2. Разыменование Nullable только через специальные операторы (тернарный — ?:, Элвиса — ?., coalesce — ??), предусматривающие обязательную обработку обоих вариантов (наличие или отсутствие объекта) без выбрасывания исключений.
  3. Примеры:
    object o = new object(); // ссылочный тип - корректная инициализация
    object o = null; // ссылочный тип - ошибка компиляции, так как null недопустим
    object? n = new object; // nullable тип - корректная инициализация
    object? n = null; // nullable тип - корректная инициализация
    object o = n; // ссылочный тип - ошибка компиляции, типы object и object? несовместимы
    object o = n ?? new object(); // разыменование с fallback значением (coalesce), дополнительное значение будет вычислено только если n != null
    Type t = n ? value.GetType() : typeof(object); // специальный тернарный оператор - value означает значение n, если оно не null
    Type? t = n ? value.GetType(); // бинарная форма оператора ? - возвращает null, если первый операнд null, иначе вычисляет второй операнд и возвращает его, завернутого в nullable
  4. В этом случае NRE отсутствует по определению: возможность присвоить или передать null определяется типом значения, конвертация с выбросом исключения отсутствует.

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

Лекарства для текущей реальности

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

  1. Явные проверки на null в операторе if. Очень прямолинейный способ с массой серьезных недостатков.

    1. Гигантская масса шумового кода, единственное назначение которого — выбросить исключение поближе к месту предательства.
    2. Основной сценарий, загроможденный проверками, читается плохо
    3. Требуемую проверку легко пропустить или полениться написать
    4. Проверки можно добавлять отнюдь не везде (например, это нельзя сделать для автосвойств)
    5. Проверки не бесплатны во время выполнения.

  2. Атрибут NotNull. Немного упрощает использование явных проверок

    1. Позволяет использовать статический анализ
    2. Поддерживается R#
    3. Требует добавления изрядного количества скорее вредного, чем бесполезного кода: в львиной доле вариантов использования null недопустим, а значит атрибут придется добавлять буквально везде.

  3. Паттерн проектирования Null object. Очень хороший способ, но с ограниченной сферой применения.

    1. Позволяет не использовать проверок на null там, где существует эквивалент нуля в виде объекта: пустой IEnumerable, пустой массив, пустая строка, ордер с нулевой суммой и т.п. Самое впечатляющее применение — автоматическая реализация интерфейсов в мок-библиотеках.
    2. Бесполезен в остальных ситуация: как только вам потребовалось отличать в коде нулевой объект от остальных — вы имеете эквивалент null вместо null object, что является уже двойным предательством: неполноценный объект, который даже NRE не выбрасывает.

  4. Конвенция о возврате живых объектов по умолчанию. Очень просто и эффективно.

    1. Любой метод или свойство, для которых явно не заявлена возможность возвращать null, должны всегда предоставлять полноценный объект. Для поддержания достаточно выработки хорошей привычки, например, посредством ревью кода.

    2. Разработчики сторонних библиотек ничего про ваше соглашение не знают
    3. Нарушения соглашения выявить непросто.

  5. Конвенция о стандартных способах явно указать что свойство или метод может вернуть null: например, префикс Try или суффикс OrDefault в имени метода. Органичное дополнение к возврату полноценных объектов по умолчанию. Достоинства и недостатки те же.

  6. Атрибут CanBeNull. Добрый антипод-близнец атрибута NotNull.

    1. Поддерживается R#
    2. Позволяет помечать явно опасные места, вместо массовой разметки по площадям как NotNull
    3. Неудобен в случае когда null возвращается часто.

  7. Операторы C# (тернарный, Элвиса, coalesce)

    1. Позволяют элегантно и лаконично организовать проверку и обработку null значений без потери прозрачности основного сценария обработки.
    2. Практически не упрощают выброс ArgumentException при передаче null в качестве значения NotNull параметра.
    3. Покрывают лишь некоторую часть вариантов использования.
    4. Остальные недостатки те же, что и у проверок в лоб.

  8. Тип Optional. Позволяет явно поддержать отсутствие объекта.

    1. Можно полностью исключить NRE
    2. Можно гарантировать наличие обработки обоих основных вариантов на этапе компиляции.
    3. Против легаси этот вариант немного помогает, вернее, помогает немного.
    4. Во время исполнения помимо дополнительных инструкций добавляется еще и memory traffic

  9. Монада Maybe. LINQ для удобной обработки случаев как наличия, так и отсутствия объекта.

    1. Сочетает элегантность кода с полнотой покрытия вариантов использования.
    2. В сочетании с типом Optional дает кумулятивный эффект.
    3. Отладка затруднена, так как с точки зрения отладчика вся цепочка вызовов является одной строкой.
    4. Легаси по-прежнему остается ахиллесовой пятой.

  10. Программирование по контракту.

    1. В теории почти идеал, на практике все гораздо печальнее.
    2. Библиотека Code Contracts скорее мертва, чем жива.
    3. Очень сильное замедление сборки, вплоть до невозможности использовать в цикле редактирование-компиляция-отладка.

  11. Пакет Fody/NullGuard. Автоматические проверки на null на стероидах.

    1. Проверяется все: передача параметров, запись, чтение и возврат значений, даже автосвойства.
    2. Никакого оверхеда в исходном коде
    3. Никаких случайных пропусков проверок
    4. Поддержка атрибута AllowNull — с одной стороны это очень хорошо, а с другой — аналогичный атрибут у решарпера другой.
    5. С библиотеками, агрессивно использующими null, требуется довольно много ручной работы по добавлению атрибутов AllowNull
    6. Поддержка отключения проверки для отдельных классов и целых сборок
    7. Используется вплетение кода после компиляции, но время сборки растет умеренно.
    8. Сами проверки работают только во время выполнения.
    9. Гарантируется выброс исключения максимально близко к дефекту (возврату null туда, где ожидается реальный объект).
    10. Тотальность проверок помогает даже при работе с легаси, позволяя как можно быстрее обнаружить, пометить и обезвредить даже null, полученный из чужого кода.
    11. Если отсутствие объекта допустимо — NullGuard сможет помочь только при попытках передать его куда не следует.
    12. Вычистив дефекты в тестовой версии, можно собрать промышленную из тех же исходников с отключенными проверками, получив нулевую стоимость во время выполнения при гарантии сохранения всей прочей логики.

  12. Ссылочные типы без возможности присвоения null (если добавят в одну из будущих версий C#)

    1. Проверки во время компиляции.
    2. Можно полностью ликвидировать NRE в новом коде.
    3. В реальности не реализовано, надеюсь, что только пока
    4. Единообразия со значимыми типами не будет.
    5. Легаси достанет и здесь.

Итоги

Буду краток — все выводы в таблице:

Настоятельная рекомендация Антипаттерн На ваш вкус и потребности
4, 5, 7, 11, 12 (когда и если будет реализовано) 1, 2 3, 6, 8, 9, 10

На предвосхищение ООП через 20 лет не претендую, но дополнениям и критике буду очень рад.

Обновление

добавил примеры кода к утопической альтернативе.

В мире Javascript и как с этим работать

Какие ошибки в мире программного обеспечения обходятся в миллиарды долларов?

  • По словам Тони Хоара, ошибка 2000 года, класс ошибок, связанных с хранением и форматированием данных календаря, обойдется чуть менее чем в 4 миллиарда долларов.
  • CodeRed Virus, компьютерный червь, внедрившийся в компании по всему миру, вывел из строя все сети. Прерывание бизнеса и всего обычного банковского дела обошлось мировой экономике в 4 миллиарда долларов.
  • Null – ошибочное изобретение британского ученого-компьютерщика Тони Хоара (наиболее известного благодаря своему алгоритму быстрой сортировки) в 1964 году, который изобрел нулевые ссылки. как его «ошибка на миллиард долларов».

Кто придумал «обнулить» ошибку на миллиард долларов и почему?

Я называю это своей ошибкой на миллиард долларов. Это было изобретение нулевой ссылки в 1965 году. В то время я разрабатывал первую всеобъемлющую систему типов для ссылок в объектно-ориентированном языке (ALGOL W). Моя цель состояла в том, чтобы гарантировать, что любое использование ссылок должно быть абсолютно безопасным, с автоматической проверкой компилятором. Но я не мог устоять перед искушением добавить нулевую ссылку просто потому, что это было так легко реализовать. Это привело к бесчисленным ошибкам, уязвимостям и системным сбоям, которые, вероятно, причинили миллиарды долларов боли и ущерба за последние сорок лет. — Тони Хоар

Ссылка на нулевой указатель может быть плохой идеей. Сравнивая ссылку нулевого указателя с неразборчивым в связях прелюбодеем, он заметил, что нулевое присвоение для каждого холостяка, представленного в объектной структуре, «будет выглядеть полиаморно женатым на одном и том же человеке Null. — Эдсгар Джикстра

Что такое ошибка на миллиард долларов в контексте мира Javascript?

У нас есть два кандидата в мире Javascript, подпадающие под эту категорию:

1⃣️ Нет

Значение null записывается литералом: null. null не является идентификатором свойства глобального объекта, как может быть undefined. Вместо этого null выражает отсутствие идентификации, указывая на то, что переменная не указывает ни на какой объект. В API-интерфейсах null часто извлекается в месте, где объект можно ожидать, но объект не является релевантным.

2⃣️ Не определено

undefined — это свойство глобального объекта. То есть это переменная в глобальной области видимости. Начальное значение undefined — это примитивное значение undefined.

примитивные типы данных Javascript (ES2020),

  1. Boolean
  2. Null
  3. Undefined
  4. Number
  5. String
  6. BigInt
  7. Symbol

Null и Undefined в Javascript называются «нулевыми» (ложными) значениями.

Ложные значения: Undefined, null, 0, NaN, empty string‘’, false

Нулевой или неопределенный

Несмотря на то, что поведение обоих значений является ложным, если кто-то думает Null vs Undefined как Declared vs Undeclared, это не совсем так!

Undefined может быть как объявленным, так и необъявленным.

А как насчет Null?

У Null есть свои проблемы, с которыми нужно разобраться… отлично! Давайте посмотрим на это,

Ну, typeof null == “object” — это ошибка 25-летней давности, начиная с первой версии Javascript.

В первой версии JavaScript значения хранились в 32-битных единицах, которые состояли из небольшого тега типа (1–3 бита) и фактических данных значения. Теги типа хранились в младших битах единиц. Их было пятеро:

  • 000: object. Данные являются ссылкой на объект.
  • 001: int. Данные представляют собой 31-битное целое число со знаком.
  • 010: double. Данные являются ссылкой на двойное число с плавающей запятой.
  • 100: string. Данные являются ссылкой на строку.
  • 110: boolean. Данные являются логическими.

Из исходного кода jsapi.h, (ссылка)

#define JSVAL_OBJECT      0x0     /* untagged reference to object */
#define JSVAL_INT         0x1     /* tagged 31-bit integer value */
#define JSVAL_DOUBLE      0x2     /* tagged reference to double */
#define JSVAL_STRING      0x4     /* tagged reference to string */
#define JSVAL_BOOLEAN     0x6     /* tagged boolean value */

Два значения были особенными:

  • undefined(JSVAL_VOID), представляет собой целое минус (-) JSVAL_INT_POW2 (30), то есть число вне целочисленного диапазона
  • null(JSVAL_NULL) — это указатель NULL машинного кода, тег типа объекта плюс ссылка, равная нулю(OBJECT_TO_JSVAL(0)).
#define JSVAL_VOID              INT_TO_JSVAL(0 - JSVAL_INT_POW2(30))
#define JSVAL_NULL              OBJECT_TO_JSVAL(0)
#define JSVAL_ZERO              INT_TO_JSVAL(0)
#define JSVAL_ONE               INT_TO_JSVAL(1)
#define JSVAL_FALSE             BOOLEAN_TO_JSVAL(JS_FALSE)
#define JSVAL_TRUE              BOOLEAN_TO_JSVAL(JS_TRUE)

Теперь, когда рассмотрел его тег типа, а тег типа сказал объект. («источник»)

  1. Строка № 10, сначала проверяет, является ли значение v undefined(VOID).
  2. Следующая проверка в строке № 12 проверяет наличие объекта JSVAL_IS_OBJECT,
  3. Кроме того, вызывает функциональный класс (строка № 18, 19).
  4. И, следовательно, оценивается как Object
  5. Впоследствии есть проверки на число, строку и логическое значение, даже не проверка на Null

Возможно, это одна из причин, по которой первая версия JavaScript была завершена за 10 дней. Позже, продолжал жить с этой проблемой, а не исправлять из-за ее тесно связанной логики в исходном исходном коде. Исправление может привести к поломке многих вещей в коде.

Работа с Null и Undefined

Начиная с ES2020, у нас есть лучший способ обработки значений Nullish в Javascript. Для текущих проектов того же можно добиться с помощью Babel.js и/или Typescript.

  • Необязательная цепочка (?.)

Также известен как безопасная оценка или оператор безопасности.

Длинные цепочки обращений к свойствам в Javascript приводят к ошибкам, вызывающим сбои, поскольку можно получить null или undefined (“nullish” values). Проверка наличия свойства в глубоко вложенной структуре — утомительная задача, например, рассмотрим ответ API погоды,

Чтобы получить данные о значении «Гроза», используются три подхода:

Теперь из ES2020 или TypeScript 3.7 или @babel/plugin-proposal-optional-chaining поддерживает необязательную цепочку, где можно написать так:

  • Нулевое объединение (??)

Оператор Nullish Coalescing (??) действует очень похоже на оператор ||, за исключением того, что мы используем не ложные значения, а nullish, что означает, что значение строго равно null или undefined.

Поддерживается с ES2020, Typescript 3.7 и @babel/plugin-proposal-nullish-coalescing-operator

Избегайте Null всеми возможными способами

Шаблон NullObject

❌ НЕПРАВИЛЬНО

✅ ВПРАВО, Шаблон NullObject

Заключительные слова

(любезно предоставлено: Максмиллиано Контьери)

Программисты используют Null как разные флаги. Он может намекать на отсутствие, неопределенность, значение, ошибку или ложное значение (значение “Nullish”). Множественная семантика приводит к ошибкам связывания.

Проблемы

  • Связь между вызывающими и отправляющими
  • Несоответствие между вызывающими и отправляющими
  • Если/переключатель/случай загрязняет окружающую среду
  • Null не полиморфен реальным объектам, поэтому NullPointerException (TypeError: null or undefined has no properties)
  • Null не существует в реальном мире. Таким образом, нарушается принцип биекции

Решения

  • Избегайте нуля
  • Использовать Шаблон нулевого объекта
  • Используйте необязательно

Исключения

  • API, базы данных, внешние системы, где существует NULL

Поддержка Линтера

Добавьте no-null и no-undef к вашему .eslintrc

Звук Нулевая Безопасность

В современных языках введена Sound Null Safetyили иначе известная как Void Safety для более безопасного и удобного кода, что означает, что по умолчанию язык предполагает, что переменные являются ненулевыми значениями, если явно не указано иное. .

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

Например, Dart, Swift и другие следуют Sound Null Safety.

использованная литература

UPDATE: интересно продолжение этой статьи? Читайте: “Контракты vs. Монады?”.

Вступление

В моих черновиках уже больше года лежит статья, в которой я хотел рассказать о проблеме разыменовывания пустых ссылок (null reference dereferencing), с подходами в разных языках и платформах. Но поскольку у меня все никак не доходили руки, а в комментариях к прошлой статье («Интервью с Бертраном Мейером») была затронута эта тема в контексте языка C#, то я решил к ней все-таки вернуться. Пусть получилось не столь фундаментально как я хотел изначально, но букв и так получилось довольно много.

Ошибка на миллиард долларов?

В марте 2009-го года сэр Тони Хоар (C.A.R. Hoare) выступил на конференции Qcon в Лондоне с докладом на тему «Нулевые ссылки: ошибка на миллиард долларов» (Null References: The Billion Dollar Mistake), в котором признался, что считает изобретение нулевых указателей одной из главных своих ошибок, стоившей индустрии миллиарды долларов.

“Я называю это своей ошибкой на миллиард долларов. Речь идет о изобретении нулевых ссылок (null reference) в 1965 году. В то время, я проектировал комплексную систему типов для ссылок в объекто-ориентированном языке программирования ALGOL W. Я хотел гарантировать, что любое использование всех ссылок будет абсолютно безопасным, с автоматической проверкой этого компилятором. Но я не смог устоять перед соблазном добавить нулевую ссылку (null reference), поскольку реализовать это было столь легко. Это решение привело к бесчисленному количеству ошибок, дыр в безопасности и падений систем, что привело, наверное, к миллиардным убыткам за последние 40 лет. В последнее время, ряд анализаторов, таких как PREfix и PREfast корпорации Microsoft были использованы для проверки валидности ссылок и выдачи предупреждений в случае, если они могли быть равными null. Совсем недавно появился ряд языков программирования, таких как Spec# с поддержкой ненулевых ссылок (non-null references). Это именно то решение, которое я отверг в 1965-м.”

Сегодня уже поздно говорить, чтоб бы было, если бы Хоар когда-то принял иное решение и более разумно посмотреть, как эти проблемы решаются в разных языках и, в частности, в языке C#.

Void Safety в Eiffel (с примерами на C#)

Одним из первых на выступление Тони Хоара отреагировал Бертран Мейер, гуру ООП и автор языка Eiffel. Предложенное Мейером решение заключается в разделении переменных всех ссылочных типов на две категории: на переменные, допускающие null (nullable references или detach references в терминах Eiffel) и на переменные, не допускающие null (not-nullable references или attach references в терминах Eiffel).

При этом по умолчанию, все переменные стали относится именно к non-nullable категории! Причина такого решения в том, что на самом деле, в подавляющем большинстве случаев нам нужны именно non-nullable ссылки, а nullable-ссылки являются исключением.

С таким разделением компилятор Eiffel может помочь программисту в обеспечении «Void Safety»: он свободно позволяет обращаться к любым членам not-nullable ссылок и требует определенного «обряда» для потенциально «нулевых» ссылок. Для этого используется несколько паттернов и синтаксических конструкций, но идея, думаю, понятна. Если переменная может быть null, то для вызова метода x.Foo() вначале требуется проверить, что x != null.

При этом, чтобы решить проблему с многопоточностью (дабы избавиться от гонок), вводится специальная синтаксическая конструкция, следующего вида:

if attached t as l then
  l.f — здесь обеспечивается Void safety.
end

Если провести параллель между языком Eiffel и C#, то выглядело бы это примерно так. Все переменные ссылочного типа превратились бы в not-nullable переменные, что требовало бы их инициализации в месте объявления валидным (not null) объектом. А доступ к любым nullable переменным требовал бы какой-то магии:

// Где-то в идеальном мире
// s – not-nullable переменная

public void Foo(string
s)
{
   
// Никакие проверки, контркты, атрибуты не нужны
    Console.WriteLine(s.
Length);
}

// str – nullable (detached) переменная.
//string! аналогичен типу Option<string>

public void Boo(string
! str)
{
   
// Ошибка компиляции!
    // Нельзя обращаться к членам "detached" строки!
    // Console.WriteLine(str.Length);
    str.IfAttached((string s) => Console.
WriteLine(s));
   
// Или
    if (str != null
)
       
Console.WriteLine(str.
Length);
}

public void Doo(string
! str)
{
   
Contract.Requires(str != null
);
   
// Наличие предусловия позволяет безопасным образом
    // обращаться к объекте string через ссылку str!
    Console.WriteLine(str.Length);
}

Такие изменения в языке, а также в стандартной библиотеке Eiffel позволили гарантировать отсутствие разыменовывания нулевых ссылок. Но даже с небольшим комьюнити, для внедрения этих изменений потребовалось несколько лет. Поэтому неудивительно, что нам не стоит ждать аналогичных изменений в таком языке как C#.

Более того, реалии таковы, что нам вряд ли стоит ждать появления not-nullable ссылочных типов (о чем не так давно поведал Эрик Липперт в своей статье «C#: Non-nullable Reference Types»), поэтому нам приходится изобретать различного вида велосипеды. Но прежде чем переходить к этим самым велосипедам, интересно посмотреть на подход другого языка платформы .NET – F#.

ПРИМЕЧАНИЕ
Подробнее о Void Safety в Eiffel можно почитать в статье Бертрана Мейера «Avoid a Void: The eradication of null dereferencing», а также в замечательной статье Жени Охотникова «Void safety в языке Eiffel».

Void Safety в F#

Большинство функциональных языков программирования (включая F#) решают проблему разыменовывания нулевых ссылок уже довольно давно, причем способом аналогичным рассмотренному ранее. Так, типы, объявленные в F# по умолчанию не могут быть null (нужно использовать специальный атрибут AllowNullLiteral, да и в этом случае это возможно не всегда). В F# разрешается использование литерала null, но лишь для взаимодействия со сторонним кодом, не столь продвинутым в вопросах Void Safety, написанном на таких языках как C# или VB (хотя и в этом случае иногда могут быть проблемы, см. «F# null trick»).

Для указания «отсутствующего» значения в F# вместо null принято использовать специальный тип – Option<T>. Это позволяет четко указать в сигнатуре метода, когда ожидается nullable аргумент или результат, а когда результат не должен быть null:

// Результат не может быть null
let findRecord (id: int) : Record =
    ...


// Результат тоже не может быть null,
// но результат завернут в Option<Record> и может отсутствовать

let tryFindRecord (id: int) : Option<Record> =
    ...

Так, в первом случае возвращаемое значение обязательно (и при его отсутствии будет сгенерировано исключение), а во втором случае результат может отсутствовать.

ПРИМЕЧАНИЕ
Именно этой идиоме следует стандартная библиотека F#. Так, вместо пары методов из LINQ: Single/SingleOrDefault в F# используются методы find/tryFind модулей Seq, List и других.

Поскольку «необязательные» значения может участвовать в целой цепочке событий, любой функциональный язык программирования поддерживает специальный «паттерн», который позволяет «протаскивать» значение через некоторую цепочку операций. Данный паттерн носит название «монада» и является фундаментальным в некоторых функциональных языках программирования. Я не хочу останавливаться на абстрактном описании, поскольку для этого уже есть ряд отличных источников (см. дополнительные источники в конце статьи).

Идея же достаточно простая: нам нужен простой способ работать с «завернутыми» значениями, такими как Option<T>, аналогично тому, как бы мы работали с самим типом T напрямую. Проще всего это показать на примере:

Предположим, у нас есть цепочка операций:

  1. Попытаться найти сотрудника (tryFindEmployee), если он найден, то
  2. взять свойство Department, если свойство не пустое, то
  3. взять свойство BaseSalary, если свойство не пустое, то
  4. вычислить размер заработной платы (computeSalary).
  5. если один из этапов возвращает не содержит результата (None в случае F#), то результат должен отсутствовать (должен быть равен None), в противном случае мы получим вычисленный результат.

Это можно сделать большим набором if-ов, а можно использовать Option.bind:

let salary = 
    tryFindEmployee
42
 
   
|> Option.bind(fun e -> e.
Department)
   
|> Option.bind(fun d -> d.
BaseSalary)
   
|> Option.bind computeSalary

Основная идея метода bind очень простая: если Option<T> содержит значение, тогда метод bind вызывает переданный делегат с «распакованным» значением, в противном случае – просто возвращается None (отсутствие значение в типе Option). При этом делегат, переданный в метод Bind также может вернуть значение, завернутое в Option или None, если значение не получено. Такой подход позволяет легко создавать цепочки операций, не заморачиваясь кучей проверок на null на каждом этапе.

Void Safety в C#

Поскольку популярность функциональных языков существенно возросла, то не удивительно, что многие паттерны функционального программирования начали перекачивать в такие исходно объектно-ориентированные языки, как C# (авторы C# серьезно думают о добавлении в одной из новых версий Pattern Matching-а!). Поэтому не удивительно, что для решения проблемы «нулевых ссылок» очень часто начали использоваться идиомы, заимствованные из функциональных языков.

«Монада» Maybe

Одна из причин появления этой статьи заключается в попытке выяснить, насколько полезно/разумно использовать тип Maybe<T> (аналогичный типу Option<T> из F#) в языке C#. В комментариях к предыдущей заметке было высказано мнение о пользе этого подхода, но хотелось бы рассмотреть «за» и «против» более подробно.

> Используйте монадки, и не нужно париться с null. Накладывайте рестрикшен через тип.

Действительно, строгая типизация и выражение намерений через систему типов – это лучший способ для разработчика передать свои намерения. Так, не зная существующих идиом платформы .NET, очень сложно понять, в чем же разница между методами First и FirstOrDefault в LINQ, или как понять, что будет делать метод GetEmployeeById в случае отсутствия данных по сотруднику: вернет null или сгенерирует исключение? В первом случае суффикс «Default» намекает, что в случае отсутствия элемента мы получим «значение по умолчанию» (т.е. null), но что будет во втором случае – не ясно.

В этом плане подход, принятый в F# выглядит намного более приятным, ведь там методы отличаются не только именем, но и возвращаемым значением: метод find возвращает T, а tryFind возвращает Option<T>. Так почему бы не пользоваться этой же идиомой в языке C#?

Главная причина, останавливающая от повсеместного использования Option<T> в C# заключается в отсутствии повсеместного использования Option<T> в C#. Звучт бредово, но все так и есть: использование Option<T> не является стандартной идиомой для библиотек на языке C#, а значит использование своего «велосипедного» Option<T> не принесет существенной пользы.

Теперь стоит понять, а стоит ли овчинка выделки и будет ли полезным использование Option<T> лишь в своем проекте.

Обеспечивает ли Maybe<T> Void Safety?

Особенность использования Maybe заключается в том, что это лишь смягчает, но не решает полностью проблему «разыменовывания нулевых указателей». Проблема в том, что нам все равно нужен способ сказать в коде, что данная переменная типа Maybe<T> должна содержать значение в определенный момент времени. Нам не только нужен безопасный способ обработать оба возможных варианта значения переменной Maybe, но и сказать о том, что один из них в данный момент не возможен.

Давайте рассмотрим такой пример. При реализации «R# Contract Extensions» мне понадобился класс для добавления предусловий/постусловий в абстрактные методы, и методы интерфейсов. Для тех, кто не очень в курсе библиотеки Code Contracts, контракты для абстрактных методов и интерфейсов задаются отдельным классом, который помечается специальным атрибутом – ContractClassFor:

[ContractClass(typeof (AbstractClassContract))]
public abstract class AbstractClass
{
   
public abstract void Foo(string
str);
}

// Отдельный класс с контрактами абстрактного класса
[ContractClassFor(typeof (AbstractClass))]
abstract class AbstractClassContract : AbstractClass
{
   
public override void Foo(string
str)
    {
       
Contract.Requires(str != null
);
       
throw new System.NotImplementedException();
    }
}

Процесс добавления контракта для абстрактного метода AbstractClass.Foo такой:

  1. Получить «контрактный метод» текущего метода (в нашем случае нужно найти «дескриптор» метода AbstractClassContract.Foo).
  2. Если такого метода еще нет (или нет класса AbstractClassContract), сгенерировать класс контракта с этим методом.
  3. Получить «контрактный метод» снова (теперь мы точно должны получить «дескриптор» метода AbstractClassContract.Foo).
  4. Добавить предусловие/постусловие в «контрактный метод».

В результате метод добавления предусловия для абстрактного метод выглядит так (см. ComboRequiresContextAction метод ExecutePsiTransaction):

var contractFunction = GetContractFunction();
if (contractFunction == null
)
{

    AddContractClass();

   
contractFunction = GetContractFunction();
    Contract.Assert(contractFunction != null
);
}


...

[CanBeNull, System.Diagnostics.Contracts.Pure]
private ICSharpFunctionDeclaration 
GetContractFunction()
{

    return _availability.SelectedFunction.GetContractFunction();
}

Метод GetContractFunction возвращает метод класса-контракта для абстрактного класса или интерфейса (в нашем примере, для метода AbstractClass.Foo этот метод вернет AbstractClassContract.Foo, если контрактный класс существует, или null, если класса-контракта еще нет).

Как бы изменился этот метод, если бы я использовал Maybe<T> или Option<T>? Я бы убрал атрибут CanBeNull и поменял тип возвращаемого значения. Но вопрос в том, как бы использование Maybe мне помогло бы выразить, что «постусловие» этого закрытого метода меняется в зависимости от контекста? Так, при первом вызове этого метода вполне нормально, что контрактного метода еще нет и возвращаемое значение равно null (или None, в случае «монадического» решения). Однако после вызова AddContractClass (т.е. после добавления класса-контракта) возвращаемое значение точно должно быть и метод GetContractFunction обязательно должен вернуть «непустое» значение! Я никак не могу отразить это в системе типов языка C# или F#, поскольку лишь я знаю ожидаемое поведение. (Напомню, что контракты выступают в роли спецификации и задают ожидаемое поведение, отклонение от которого означает наличие багов в реализации. Так и в этом случае, если второй вызов GetContractFunction вернул null, то это значит, что в консерватории что-то сломалось и нужно лезть в код и его чинить).

Этим маленьким примером я хотел показать, что не все проблемы с разыменовыванием нулевых указателей можно решить с помощью класса Maybe, а контракты и аннотации (такие как CanBeNull) вполне может поднять «описательность» методов не меняя тип возвращаемого значения.

Использование «неявной» монады Maybe

Как уже было сказано выше, польза от Maybe будет лишь в том случае, если он будет использован всеми типами приложения. Это значит, что если ваш метод TryFind возвращает Maybe<Person>, то нужно, чтобы все «nullable» свойства класса Person тоже возвращали Maybe<T>.

Добиться повсеместного использования Maybe в любом серьезном приложении, написанном на C#, весьма проблематично, поскольку все равно останется масса «внешнего» кода, который не знает об этой идиоме. Так почему бы не воспользоваться этой же идеей, но без «заворачивания» всех типов в Maybe<T>?

Так, класс Maybe<T> (или Option<T>) является своего рода оболочкой вокруг некоторого значения, со вспомогательным методом Bind (название метода является стандартным для паттерна монада), который «разворачивает коверт» и достает из него реальное значение и передает для обработки. При этом становится очень легко строить pipe line, который мы видели в разделе, посвященном языку F#.

Но почему бы не рассматривать любую ссылку, как некую «оболочку» вокруг объекта, для которого мы добавим метод расширения с тем же смыслом, что и метод Bind класса Option? В результате, мы создадим метод, который будет «разворачивать» нашу ссылку и вызывать метод Bind (который мы переименуем в With) лишь тогда, когда текущее значение не равно null:

public static U With<T, U>(this T callSite, Func<T, U> selector) where T : class
{
    Contract.Requires(selector != null);

   
if (callSite == null)
        return default(U);

   
return selector(callSite);
}

В этом случае, мы сможем использовать достаточно компактный синтаксис для обхода целого дерева, каждый узел которого может вернут null. И тогда, если все «узлы» этого дерева будут не null, мы получим непустой результат, если же на одном из этапов, значение будет отсутствовать, то результат всего выражения будет null.

С методом With наш пример, рассмотренный в предыдущем разделе будет выглядеть так:

var salary = Repository.GetEmployee(42)
   
.With(employee => employee.
Department)
   
.With(department => department.
BaseSalary)
   
.With(baseSalary => ComputeSalary(baseSalary));

В некоторых случаях такой простой «велосипед» может в разы сократить размер кода и здорово улучшить читабельность, особенно при работе с «глубокими» графами объектов, большинство узлов которого может содержать null. Примером такого «глубокого» графа является API Roslyn-а, а также ReSharper SDK. Именно поэтому в своем плагине для ReSharper-а я очень часто использую подобный код:

[CanBeNull]
public static IClrTypeName GetCallSiteType(this IInvocationExpression 
invocationExpression)
{

    Contract.Requires(invocationExpression != null);

   
var type = invocationExpression
        .With(x => x.InvokedExpression)
        .With(x => x as IReferenceExpression)
        .With(x => x.Reference)
        .With(x => x.Resolve())
        .With(x => x.DeclaredElement)
        .With(x => x as IClrDeclaredElement)
        .With(x => x.GetContainingType())
        .Return(x => x.GetClrName());
    return type;
}

Данный тип «расковыривает» выражение вызова метода и достает тип, на котором этот вызов происходит. При этом может возникнуть вопрос с отладкой, когда результат равен null и не понятно, что же конкретно пошло не так:

clip_image002

В этом плане очень полезна новая фича в VS2013 под названием Autos, которая показывает, на каком из этапов мы получили null:

clip_image004

(В данном случае мы видим, что Resolve вернул не нулевой элемент, но DeclaredElement этого значения равен null.)

Null propagating operator ?. в C# 6.0

Поскольку проблема обработки сложных графов объектов с потенциально отсутствующими значениями, столь распространена, разработчики C# решили добавить в C# 6.0 специальный синтаксис в виде оператора «?.» (null propagating operator). Основная его идея аналогична методу Bind класса Option<T> и приведенному выше методу расширения With.

Следующие три примера эквивалентны:

var customerName1 = GetCustomer() ?.Name;
var customerName2 = GetCustomer().With(x => x.
Name);

var tempCustomer = GetCustomer();
string customerName3 = tempCustomer != null ? tempCustomer.Name : null;

ПРИМЕЧАНИЕ
Null propagating operator не может заменить приведенные ранее методы расширения With, поскольку справа от оператора ?. может быть лишь обращение к членам текущего объекта, а произвольное выражения использовать нельзя.
Так, в случае с With мы можем сделать следующее:

var result = GetCustomer().With(x => x.Name).With(x => string.Intern(x));

В случае с “?.” нам все же понадобится временная переменная.

Заключение

Так как же можно обеспечить Void Safety в языке C#? Обеспечить полностью? Никак. Если же мы хотим уменьшить эту проблему к минимуму, то тут есть варианты.

Мне правда нравится идея явного использования типа Maybe (или Option) для получения более декларативного дизайна, но меня напрягает неуниверсальность этого подхода: для собственного кода этот подход работает «на ура», но все равно будет непоследовательным, поскольку сторонний код этой идиомой не пользуется.

Для обработки сложных графов объектов мне нравится подход с методами расширения типа With/Return для ссылочных типов + контракты для декларативного описания наличия или отсутствия значения. Мне вообще кажется, что аннотации типа CanBeNull + контракты с соответствующей поддержкой со стороны среды разработки (как в плане редактирования, так и просмотра) могут существенно упростить понимание кода и проблема сейчас именно в отсутствии такой поддержки.

После выхода C# 6.0 и появления оператора “?.” в ряде случаев можно будет отказаться от методов расширения With/Return, но иногда такие велосипеды все равно будут нужны из-за ограничений оператора “?.”.

К сожалению, без полноценных not-nullable типов обеспечить Void Safety полностью просто невозможно. Поэтому сейчас нам остается использовать разные велосипеды и надеяться на усовершенствование средств разработки, которые упростят работу с контрактами и nullable-типами.

Дополнительные ссылки

  • Интервью с Бертраном Мейером
  • Null Reference: The Billion Dollar Mistake – выступление Тони Хоара на QCon 2009
  • Bertrand Meyer. Avoid a Void: The eradication of null dereferencing – отличная статья Мейера о Void Safety в Eiffel.
  • Евгений Охотников. Void safety в языке Eiffel – описание Void Safety в Eiffel на русском языке.
  • Eric Lippert. «C#: Non-nullable Reference Types» – сказ о том, почему нам не стоит ждать nullable reference типов в языке C#.
  • Functors, applicatives, and monads in pictures – отличная статья с графическим объяснением, что такое монада.
  • Eric Lippert. Monads, part one – первая статья отличной серии постов Эрика Липперта о монадах. Одно из лучших описаний для C# разработчика
  • F# for fun and profit: The “Computation Expressions” series – цикл статей о Computation Expressions в F#. Не о монадах напрямую, но это все равно лучшее описание принципов, которые лежат в основе монад. Если имеете представление об F#, то эта серия – лучший способ разобраться в монадах.
  • Обсуждение null propagating operator на roslyn.codeplex.com
  • R# Contract Extension – R# плагин для упрощения работы с контрактами
  • Thinking Functionally in C# with monads.net

З.Ы. Понравился пост? Поделись с друзьями! Вам не сложно, а мне приятно;)

InfoQ Homepage

Presentations

Null References: The Billion Dollar Mistake

Summary

Tony Hoare introduced Null references in ALGOL W back in 1965 «simply because it was so easy to implement», says Mr. Hoare. He talks about that decision considering it «my billion-dollar mistake».

Bio

Sir Charles Antony Richard Hoare, commonly known as Tony Hoare, is a British computer scientist, probably best known for the development in 1960, at age 26, of Quicksort. He also developed Hoare logic, the formal language Communicating Sequential Processes (CSP), and inspired the Occam programming language.

About the conference

QCon is a conference that is organized by the community, for the community.The result is a high quality conference experience where a tremendous amount of attention and investment has gone into having the best content on the most important topics presented by the leaders in our community. QCon is designed with the technical depth and enterprise focus of interest to technical team leads, architects, and project managers.

INFOQ EVENTS

  • QCon SF image
    October 2-6, In-Person or Video-Only pass to recordings

    QCon San Francisco 2023

    Make the right decisions by uncovering how senior software developers at early adopter companies are adopting emerging trends. Register now!

  • October 19th, 2023, 1 PM EDT

    How To Build Payment Systems That Scale to Infinity

    Presented by: Domenic Ravita — Principal Product Evangelist and Alex Lunev — Senior Director, Engineering

Key Takeaways

  • Null references have historically been a bad idea
  • Early compilers provided opt-out switches for run-time checks, at the expense of correctness
  • Programming language designers should be responsible for the errors in programs written in that language
  • Customer requests and markets may not ask for what’s good for them; they may need regulation to build the market
  • If the billion dollar mistake was the null pointer, the C gets function is a multi-billion dollar mistake that created the opportunity for malware and viruses to thrive

Show notes

  • 00:45 Thesis: historically, null references have been a bad idea.
  • 02:15 Null references were created in 1964 — how much have they cost? Less or more than a billion dollars?
  • 03:20 Whilst we don’t know, the amount is probably in the order of an (American) billion — more than a tenth of a billon, less than ten billion.

History of programming languages

  • 03:35 A little on the history of the idea. Tony started as a programmer with Elliot’s [Ed: Elliot Brothers, London Ltd] in 1960, and was asked to design a new programming language.
  • 04:10 In the library was a 23-page booklet entitled «Report on the international language ALGOL60»;, edited by Peter Naur.
  • 04:30 Used as a basis for the new language, but left out the complicated parts such as «if»; and «then»;.
  • 05:00 Most software was still written in machine code (including the complier).
  • 05:25 Most assembly was simple enough to understand that when it went wrong, it could be diagnosed by following through to find out what the fault was.

Towards a high level language

  • 05:40 Using a high level language meant you couldn’t step through the machine code.
  • 05:50 The Elliot’s machine had 4096 locations, with a length of 4 7/8 bytes long (39 bits), although other machines had different sizes (IBM’s had 36 bits.
  • 06:30 To shield customers from implementation details, customers were told the errors in terms of the high level programming language, instead of a hexadecimal core dump.
  • 07:10 In order to implement error messages, an array had a check to verify whether its reference was in the bounds.
  • 08:00 Adding checks to arrays added space and time to the program; on Tony’s first machine it ran at less than 2k operations per second (500 micro seconds per operation, and two such tests for each array bounds).
  • 08:40 No undetected array errors, and customers didn’t know they could trade off safety for speed.
  • 09:30 The Java language has, after 30 years, decided to replicate the decision to bounds checking arrays. [Ed: other languages, like Python, handle this as well].

Record oriented programming

  • 10:20 Introduced the concept of an object, which could be referred to with a pointer.
  • 10:30 With pointers, it is possible to wreak havoc with the program you are trying to test [Ed: this is the single biggest cause of security failures in modern day code].
  • 10:55 If a floating point value or integer is used as a pointer accidentally, and the value it is pointing to is updated, then it will just as likely update the program which may then crash or cause problems now or in the future. [Ed: these days, virtual memory and page mapping takes away some of the problems about editing program code, but these weren’t present in the computers of that era.]
  • 12:00 As a given, when invoking a function with a pointer required the type of the pointer to be declared.
  • 13:30 The type of the program can be compile time checked from the static types.
  • 13:45 Many years later Tony discovered that some of these ideas had been integrated for the first time, although previous examples came from both Doug Rossier’s Plex and Simula.

Records avoid subscript errors

  • 14:35 The great thing about record handling is that you don’t need to have a subscript error, because you cannot construct a pointer that points to something that doesn’t exist, and a whole set of errors cannot occur and do not need to be checked at run-time.
  • 15:50 Later, we asked the customers whether they wanted the option to be able turn off the type checking in production. It’s a bit like wearing a life jacket when you are practicing drills, but then taking it off the ship was sinking. The customers decided to not switch off the type checking.
  • 17:00 We produced a compiler that would translate Fortran programs to Algol programs. It was a disaster, and no Fortran user would use it.
  • 18:00 The reason that they couldn’t use it was because they couldn’t use any of their programs. Within a few milliseconds of running it would come up with a subscript error. The error wasn’t wanted as they just wanted the code to run.

Type checking as standard

  • 19:00 Things have changed a bit — mainstream programming languages like Java now have subscript checking as standard, type-checked object oriented programming.
  • 19:30 And then I went and invented the null pointer. You either have to check every reference, or you risk disaster.
  • 19:45 Fortran programmers preferred to risk disaster; in fact, experience disaster, rather than check subscripts.
  • 20:00 I didn’t know it a the time, but my friend Edsger Dijkstra thought the null reference was a bad idea. He said:
  • 20:20 «If you have a null reference, then every bachelor who you represent in your object structure will seem to be married polyamocursly to the same person Null».
  • 20:55 It brings back the same question whether you want to run your code quickly (without checks) or safely (with checks).

Disjoint unions and discrimination test

  • 21:10 I did know there was a solution based on the idea of discrimination of objects belong to a disjoint union class; that is, two sets in which there are no members in common. For example a Vehicle class that has subtypes Car and Bus; the Car may have a luggage carrying capacity property while the Bus has a person carrying capacity. You would then have a discrimination test and do different operations based on whether it was a Bus or a Car.
  • 23:40 The size of the program grows with the number of discrimination clauses and number of types. This allows null to be represented as a different class, which can then be passed in to functions.
  • 24:30 The types of the pointer could then be implemented as a union of either a pointer to the null type, or a pointer to the type.
  • 25:20 This leads to implementation problems; what happens if you assume that a pointer is a Bus but change that pointer to a Car instead?
  • 25:55 One of the things you want is to be able to know in a high level language is that when it is created, all of its data structure is initialised. In this case, a null reference can be used to indicate that the data is missing or not known at this time. In fact, it’s the only thing that can be assigned if you have a pointer to a particular type.
  • 26:35 If you don’t want to use null, you have to implement a sublanguage for representing how to initialise objects of the right type. If the data structure is a tree-based representation, this is achievable if you create the leaves first because they can be fully created.
  • 27:10 It isn’t possible to create a cyclic structure using this technique; if there’s a cycle in the data structure you can start with a null pointer and then assign it once the rest of the cycle has been completed.

Introducing null

  • 27:40 This led me to suggest that the null value is a member of every type, and a null check is required on every use of that reference variable, and it may be perhaps a billion dollar mistake.
  • 28:00 Modern languages such as C# or Spec# and even Java are introducing the idea of non-null reference parameters, and compile time checking which verifies that they cannot possibly have null values.
  • 28:50 The issues of overloading and inheritance make it a lot more difficult to do these when null references were originally created.
  • 29:20 The movement must have been made based on the fact that null references were an expensive mistake.

Programming languages should be responsible for their users

  • 30:20 A programming language designer should be responsible for the mistakes made by programmers using the language. It is a serious activity; not one that should be given to programmers with 9 months experience with assembly; they should have a strong scientific basis, a good deal of ingenuity and invention and control of detail, and a clear objective that the programs written by people using the language would be correct. free of obvious errors and free of syntactical traps.
  • 31:40 This was the idea that led me to the idea of using proof and formal verification of programs as logical and mathematical models, is a method of conducting research into the design of good programming languages. I wasn’t too optimistic in 1969 would actually be using proofs to guarantee correctness of programs.
  • 32:20 By looking at the programming language and whether programs written would be possible to prove the programs written in the language gives an objective measure of how easy it would be to verify the program later. If the understanding of applying a rule locally has to depend on global knowledge of the program then you haven’t done a good job in creating the programming language, and you don’t need your customers to tell you that.
  • 33:30 In fact customers don’t tell you — it’s very easy to persuade your customers that anything that goes wrong is their fault rather than yours.
  • 33:40 I rejected that — programming language design is a serious scientific engineering activity, and we should begin to take responsibility for the mistakes that our users make.

Designing for safety

  • 33:55 It’s beginning to happen again — the Java programming language and its successors have all used avoidance of error as one of the criteria in the detail ed design of new features of the language, and I’m delighted to give them a great deal of credit for that — but it is only one criteria, and it is only one.
  • 34:35 The most important criteria is backwards compatibility of everything that has gone before, with the millions or billions lines of code that have been written.
  • 34:55 Every commercial language has to make concessions for commercial and historical reasons; but gradually, ideas change, programmers get more interested in provable correctness; production techniques, languages, checkers, analytic tools, test case generators and so on that are going to help them get their programs correct.

Safe at any speed?

  • 35:40 The analogy that I draw is with agricultural pollution and vehicle security. When Ralph Nader first started publishing «Unsafe at any speed», what he was saying had no connection with the marketplace — customers were not asking for reliability or safety as one of their vehicles.
  • 36:20 But gradually, customers started to demand reliability and safety, with the aid of law making and legal constraints requiring basic levels of safety to be included in every vehicle sold.
  • 36:50 There is a possibility that the marketplace will move the reliability of programs and the language in which they&’re expressed.
  • 37:15 For many professional engineers, they do have ideals and do pursue them in preference to not pursuing them whenever the opportunity arises. The commercial imperative that requires greater attention paid to the formal correctness of the programs is the virus.
  • 37:50 The virus (or malware, or worm) does dreadful things by reaching the parts of the program that it doesn’t usually reach. It is no longer applicable to test the cases that are likely to arise, the virus will attack the places that are not likely to arise, and so need just the same level of testing.
  • 38:35 It forces you to get the while program correct, not just the ones that will be used by customers, the code that will be used by viruses needs to be checked too.
  • 38:45 And that can’t be done by testing, it has to be done by analysis.
  • 38:55 Analysis of the source code, type-checking techniques are the simplest, but more sophisticated reasoning techniques are being used to high volume code to check that it doesn’t contain any naughty things like null reference dereferencing.

Introduction of the virus

  • 39:30 So if I am responsible for a billion dollar mistake; and I bring it up because other designers are much more responsible.
  • 39:40 The designers of C — one can definitely quantify. The buffer overflow is a direct result of the C language gets fnction that doesn’t check the bounds of the string input. That allowed the early viruses to get in by overwriting the return values of the code.
  • 40:10 These simple viruses taught the world how to write malware. Without this very simple entry point, it is quite possible that nobody would ever have thought to look for the more subtle kind of thing which are now being exploited every day by people who are now motivated, skilled, and whose profession and income it is to write botware, malware.
  • 40:45 If it hadn’t been for the gets routine in C, we might have had no malware.
  • 40:55 Now one virus — the CodeRed virus — was estimated to have cost the world economy 4 billion dollars, because it brought down all the networks, and the interruption to business and all the ordinary banking, other business was estimated to cost that amount. There was another one later as well.
  • 41:30 And that was more than the Millennium bug, which was estimated a little less than 4 billion dollars.

Companies mentioned

  • Elliot Brothers (London) Ltd

People mentioned

  • Peter Naur
  • Doug Rossier
  • Edsger Dijkstra
  • Ralph Nader

Languages mentioned

  • Algol60
  • Occam
  • Plex
  • Simula
  • Fortran
  • C#
  • Spec#
  • C

Products mentioned

  • ACM Turing Award speech

See more presentations with show notes

Recorded at:

Aug 25, 2009

Joker burning a huge pile of money

Tony Hoare, the creator of NULL, now refers to NULL as The Billion Dollar Mistake. Even though NULL Reference Exceptions continue to haunt our code to this day, we still choose to continue using it.

And for some reason JavaScript decided to double down on the problems with null by also creating undefined.

Today I would like to demonstrate a solution to this problem with the Maybe.

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years. — Tony Hoare

Do not underestimate the problems of NULL

Before you have even finish reading this article… I can already sense it, your desire to hit PAGE DOWN, rush straight to the comment section and blast out a «but NULL is never a problem for ME». But please pause, slow down, read and contemplate.

8 of 10 errors from Top 10 JavaScript errors from 1000+ projects (and how to avoid them) are null and undefined problems. Eight. Out. Of. Ten.

To underestimate NULL is to be defeated by NULL.

Null Guards

Because of the problems null brings with it, we have to constantly guard our code from it. Unguarded code might look something like this:

const toUpper = string => string.toUpperCase()

This code is susceptible to NULL Reference Exceptions.

toUpper(null) //=> ​​Cannot read property 'toUpperCase' of null​​

So we are forced to guard against null.

const toUpper = string => {
  if (string != null) {
//    --------------
//                   \
//                    null guard
    return string.toUpperCase()
  }
}

But this quickly becomes verbose as everywhere that may encounter null has to be guarded.

const toUpper = string => {
  if (string != null) {
//    --------------
//                   \
//                    duplication
    return string.toUpperCase()
  }
}

const toLower = string => {
  if (string != null) {
//    --------------
//                   \
//                    duplication
    return string.toLowerCase()
  }
}

const trim = string => {
  if (string != null) {
//    --------------
//                   \
//                    duplication
    return string.trim()
  }
}

If we think about a values as having a one-to-many relationship with code that may access it, then it makes more sense to place the guards on the one and not on the many.

Nullable Types

The .NET Framework 2.0 introduced Nullable Types into the .NET language. This new Nullable value, could be set to null without the reference being null. This meant if x was a Nullable Type, you could still do things like x.HasValue and x.Value without getting a NullReferenceException.

int? x = null
if (x.HasValue)
{
    Console.WriteLine($"x is {x.Value}")
}
else
{
    Console.WriteLine("x does not have a value")
}

The Maybe

The Maybe is similar to a Nullable Type. The variable will always have a value, and that value might represent a null, but it will never be set to null.

For these examples, I’ll be using the Maybe from MojiScript. (Also checkout monet and Sanctuary, Folktale for other Maybes). Use the following import:

import { fromNullable } from "mojiscript/type/Maybe"

The Maybe is a union type of either a Just or a Nothing. Just contains a value and Nothing is well… nothing.

But now the value is all wrapped up inside of the Maybe. To access the value of a Maybe, you would have touse a map function. Fun to Google: map is what makes the Maybe type a Functor.

If you are getting that feeling that you have seen this somewhere before that is because this exactly how a Promise works. The difference is Promise uses then and Maybe uses Map.

const promise = Promise.resolve(888)
const maybe = Just(888)

promise.then(double)
maybe.map(double)

Same same but different.

const toUpper = string => string.toUpperCase()

Just("abc").map(toUpper) //=> Just ('ABC')
Nothing.map(toUpper) //=> Nothing

Notice how in both cases above, the toUpper function no longer throws an Error. That is because we are no longer calling toUpper directly with a String, but instead mapping it with our Maybe.

If we convert all types within our application to use a Maybe, then all null guards are no longer necessary.

The null is now guarded in a single place, in the Maybe type, instead of being sprinkled throughout the application, wherever the value might be accessed.

The Maybe is a guard on the one instead of the many!

Neo vs many agent Smiths

Getting in and out of Maybes

But what about the times when we are not in control of the code, when we must send or receive a null value? Some examples might be 3rd party libraries that will return a null or libraries that will require passing null as an argument.

In these cases, we can convert a null value to a Maybe using fromNullable and we can convert back to a nullable value using fromMaybe.

import { fromMaybe, fromNullable } from "mojiscript/type/Maybe"

// converting nullable values to a Maybe
fromNullable(undefined) //=> Nothing
fromNullable(null) //=> Nothing
fromNullable(123) //=> Just (123)
fromNullable("abc") //=> Just ("abc")

// converting Maybe to a nullable type
fromMaybe(Just("abc")) //=> 'abc'
fromMaybe(Nothing) //=> null

You could als guard a single function like this:

const toUpper = string =>
  fromNullable(string).map(s => s.toUpperCase()).value

But that is a little verbose and it’s much better to expand the safety of the Maybe type to the entire application. Put the guards in place at the gateways in and out of your application, not individual functions.

One example could be using a Maybe in your Redux.

// username is a Maybe, initially set to Nothing.
const initalState = {
  username: Nothing
}

// your reducer is the gateway that ensures the value will always be a maybe.
const reducer = (state = initialState, { type, value }) =>
  type === 'SET_USERNAME'
    ? { ...state, username: fromNullable(value) }
    : state

// somewhere in your render
render() {
  const userBlock = this.props.username.map(username => <h1>{username}</h1>)
  const noUserBlock = <div>Anonymous</div>

  return (
    <div>
    {fromMaybe (noUserBlock) (userBlock)}
    </div>
  )
}

JavaScript Type Coercion

MojiScript’s Maybe can use JavaScript’s implicit and explicit coercion to it’s advantage.

Maybe can be implicity coerced into a String.

// coercing to a String
console.log("a" + Just("b") + "c") //=> 'abc'
console.log("a" + Nothing + "c") //=> 'ac'

Maybe can be explicity coerced into a Number.

Number(Just(888)) //=> 888
Number(Nothing) //=> 0

Maybe can even be stringified.

const data = {
  id: Nothing,
  name: Just("Joel")
}

JSON.stringify(data)
//=> {"id":null,"name":"Joel"}

Accessing Nested Objects

Let’s take a look at the common task of accessing nested objects.

We’ll use these objects. One is lacking an address, which can yield nulls. Gross.

const user1 = {
  id: 100,
  address: {
    address1: "123 Fake st",
    state: "CA"
  }
}

const user2 = {
  id: 101
}

These are common ways to access nested objects.

user1.address.state //=> 'CA'
user2.address.state //=> Error: Cannot read property 'state' of undefined

// short circuit
user2 && user2.address && user2.address.state //=> undefined

// Oliver Steel's Nested Object Pattern
((user2||{}).address||{}).state //=> undefined

Prettier seems to hate both of those techniques, turning them into unreadable junk.

Now let’s try accessing nested objects with a Maybe.

import { fromNullable } from 'mojiscript/type/Maybe'

const prop = prop => obj =>
  fromNullable(obj).flatMap(o => fromNullable(o[prop]))

Just(user1)
  .flatMap(prop('address))
  .flatMap(prop('state)) //=> Just ("CA")

Just(user2)
  .flatMap(prop('address))
  .flatMap(prop('address)) //=> Nothing

A lot of this boiler plate can be reduced with some helper methods.

import pathOr from 'mojiscript/object/PathOr'
import { fromNullable } from 'mojiscript/type/Maybe'

const getStateFromUser = obj =>
  fromNullable(pathOr (null) ([ 'address', 'state' ]) (obj))

Just(user1).map(getStateFromUser) //=> Just ("CA")
Just(user2).map(getStateFromUser) //=> Nothing

Decoupled map function

A Map can also be decoupled from Maybe. There are many libs that have a map function, like Ramda, but I’ll be using the one from MojiScript for this example.

import map from 'mojiscript/list/map'

const toUpper = string => string.toUpperCase()

Just("abc").map(toUpper) //=> Just ('ABC')
Nothing.map(toUpper) //=> Nothing
import map from 'mojiscript/list/map'

const toUpper = string => string.toUpperCase()

map (toUpper) (Just ("abc")) //=> Just ('ABC')
map (toUpper) (Nothing) //=> Nothing

This was getting far too big for this section, so it has been broken out into it’s own article here: An introduction to MojiScript’s enhanced map

Heavy Lifting

Lifting is a technique to apply Applicatives to a function. In English that means we can use «normal» functions with our Maybes. Fun to Google: ap is what makes the Maybe type an Applicative.

This code will use liftA2, A for Applicative and 2 for the number of arguments in the function.

import liftA2 from "mojiscript/function/liftA2"
import Just from "mojiscript/type/Just"
import Nothing from "mojiscript/type/Nothing"

const add = x => y => x + y
const ladd = liftA2 (add)

add (123) (765) //=> 888

ladd (Just (123)) (Just (765)) //=> Just (888)
ladd (Nothing) (Just (765)) //=> Nothing
ladd (Just (123)) (Nothing) //=> Nothing

Some things to notice:

  • The function add is curried. You can use any curry function to do this for you.
  • add consists of 2 parameters. If it was 3, we would use liftA3.
  • All arguments must be a Just, otherwise Nothing is returned.

So now we do not have to modify our functions to understand the Maybe type, we can use map and also lift to apply the function to our Maybes.

Continue Learning: Functors, Applicatives, And Monads In Pictures does an incredible job of explaining this and more!

Maybe Function Decorator

There are times when you would like to guard a single function against NULL. That is where the maybe Function Decorator comes in handy.

const maybe = func => (...args) =>
  !args.length || args.some(x => x == null)
    ? null
    : func(...args)

Guard your functions against null with the maybe function decorator:

const toUpper = string => string.toUpperCase()
const maybeToUpper = maybe(toUpper)
maybeToUpper("abc") //=> 'ABC'
maybeToUpper(null) //=> null

Can also be written like this:

const toUpper = maybe(string => string.toUpperCase())

Learn more about Function Decorators:

  • Function decorators: Transforming callbacks into promises and back again
  • Functional JavaScript: Function Decorators Part 2

TC39 Optional Chaining for JavaScript

This is a good time to mention the TC39 Optional Chaining Proposal that is currently in Stage 1.

Optional Chaining will allow you to guard against null with a shorter syntax.

// without Optional Chaining
const toUpper = string => string && string.toUpperCase()

// with Optional Chaining
const toUpper = string => string?.toUpperCase()

Even with Optional Chaining, the guards are still on the many and not the one, but at least the syntax is short.

Wisdoms

  • To underestimate NULL is to be defeated by NULL.
  • 8 out of the 10 top 10 errors are NULL and undefined errors.
  • If we think about a values as having a one-to-many relationship with code that may access it, then it makes more sense to place the guards on the one and not on the many.
  • It is possible to completely eliminate an entire class of bugs (NULL Reference Exceptions) by eliminating null.
  • Having NULL Reference Exceptions in your code is a choice.

End

Have questions or comments? I’d love to hear them!

Hop over to the MojiScript Discord chat and say hi!

This turned out a little longer than I originally thought it would. But this is a subject that is hard to sum up into a single article.

You can also use the Maybe with MojiScript’s map. Read more about how awesome MojiScript’s map is here…

My articles are very Functional JavaScript heavy, if you need more FP, follow me here, or on Twitter @joelnet!

Cheers!

Понравилась статья? Поделить с друзьями:
  • Nucleus coop ошибка
  • Ntvdm exe ошибка приложения
  • Ntvdm exe аппаратная ошибка
  • No such file or directory git bash ошибка
  • No such file or directory arduino ошибка