Вот как это делает Haskell: (не совсем противоречит заявлениям Липперта, поскольку Haskell не является объектно-ориентированным языком).
ПРЕДУПРЕЖДЕНИЕ: длинный обдуманный ответ от серьезного фаната Haskell впереди.
TL; DR
Этот пример точно показывает, насколько Haskell отличается от C #. Вместо того, чтобы делегировать логистику построения конструкции конструктору, она должна обрабатываться в окружающем коде. Нулевое значение (или Nothingв Haskell) не может появиться там, где мы ожидаем ненулевое значение, потому что нулевые значения могут встречаться только в специальных вызываемых типах-обертках, Maybeкоторые не взаимозаменяемы с / напрямую преобразуются в обычные, не обнуляемые типы. Чтобы использовать значение, которое можно обнулять, заключив его в a Maybe, мы должны сначала извлечь значение, используя сопоставление с образцом, что заставляет нас перенаправить поток управления в ветвь, где мы точно знаем, что у нас есть ненулевое значение.
Следовательно:
можем ли мы всегда знать, что необнуляемая ссылка ни при каких обстоятельствах не считается недействительной?
Да. Intи Maybe Intдва совершенно разных типа. Нахождение Nothingна равнине Intбыло бы сравнимо с нахождением строки «рыба» в Int32.
Как насчет конструктора объекта с ненулевым полем ссылочного типа?
Не проблема: конструкторы значений в Haskell ничего не могут сделать, кроме как взять значения, которые они дали, и сложить их вместе. Вся логика инициализации происходит до вызова конструктора.
Как насчет финализатора такого объекта, где объект завершен, потому что код, который должен был заполнить ссылку, вызвал исключение?
В Хаскеле нет финализаторов, поэтому я не могу решить эту проблему. Мой первый ответ все еще остается, однако.
Полный ответ :
Haskell не имеет значения NULL и использует Maybeтип данных для представления значений NULL. Может быть, это тип данных algabraic, определенный следующим образом:
data Maybe a = Just a | Nothing
Для тех из вас знакомы с Haskell, читать это как « Maybeэто либо Nothingили Just a». В частности:
Maybeявляется конструктором типа : его можно (неправильно) рассматривать как универсальный класс (где a- переменная типа). Аналогия с C # есть class Maybe<a>{}.
Justявляется конструктором значения : это функция, которая принимает один аргумент типа aи возвращает значение типа, Maybe aкоторое содержит значение. Таким образом, код x = Just 17аналогичен int? x = 17;.
Nothingэто другой конструктор значений, но он не принимает аргументов, а Maybeвозвращаемое не имеет значения, кроме «Nothing». x = Nothingявляется аналогом int? x = null;(предполагая, что мы ограничены aв Haskell Int, что можно сделать, написав x = Nothing :: Maybe Int).
Теперь, когда основы этого Maybeтипа находятся вне пути, как Haskell избегает вопросов, обсуждаемых в вопросе OP?
Что ж, Haskell действительно отличается от большинства языков, которые обсуждались до сих пор, поэтому я начну с объяснения нескольких основных принципов языка.
Во-первых, в Хаскеле все неизменно . Все. Имена относятся к значениям, а не к областям памяти, в которых можно хранить значения (это само по себе является огромным источником устранения ошибок). В отличие от C #, где объявление переменной и присваивание две отдельные операции, в Haskell значения создаются путем определения их стоимости (например x = 15, y = "quux", z = Nothing), который никогда не может изменить. Следовательно, код вроде:
ReferenceType x;
В Хаскеле это невозможно. Нет проблем с инициализацией значений, nullпотому что все должно быть явно инициализировано значением, чтобы оно существовало.
Во-вторых, Haskell не является объектно-ориентированным языком : это чисто функциональный язык, поэтому в строгом смысле этого слова нет объектов. Вместо этого есть просто функции (конструкторы значений), которые принимают свои аргументы и возвращают объединенную структуру.
Далее нет абсолютно никакого императивного кода стиля. Под этим я подразумеваю, что большинство языков следуют шаблону примерно так:
do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
do thing 6
return thing 7
Поведение программы выражается в виде серии инструкций. В объектно-ориентированных языках объявления классов и функций также играют огромную роль в потоке программы, но, по сути, «мясо» выполнения программы принимает форму последовательности инструкций, которые должны быть выполнены.
В Хаскеле это невозможно. Вместо этого выполнение программы полностью определяется цепочечными функциями. Даже императивно-выглядящая doнотация является просто синтаксическим сахаром для передачи анонимных функций >>=оператору. Все функции имеют вид:
<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression
Где body-expressionможет быть что-либо, что оценивается в значение. Очевидно, что доступно больше синтаксических функций, но главное - полное отсутствие последовательностей операторов.
Наконец, и, возможно, самое главное, система типов Хаскелла невероятно строгая. Если бы мне пришлось суммировать основную философию проектирования системы типов Haskell, я бы сказал: «Сделайте так, чтобы как можно больше вещей не работало во время компиляции, чтобы как можно меньше работало во время выполнения». Не существует никаких неявных преобразований (хотите продвигать Intк a Double? Используйте fromIntegralфункцию). Единственное, что может иметь недопустимое значение во время выполнения, - это использовать Prelude.undefined(который, очевидно, просто должен быть там и его невозможно удалить ).
Имея все это в виду, давайте посмотрим на «сломанный» пример amon и попытаемся повторно выразить этот код в Haskell. Во-первых, объявление данных (с использованием синтаксиса записи для именованных полей):
data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar }
( fooи barдействительно являются функциями доступа к анонимным полям здесь, а не к фактическим полям, но мы можем игнорировать эту деталь).
Конструктор NotSoBrokenзначения не способен предпринимать какие-либо действия, кроме взятия a Fooи a Bar(которые не могут быть обнуляемыми), и создавать NotSoBrokenих из них. Там нет места, чтобы поставить императивный код или даже вручную назначить поля. Вся логика инициализации должна происходить в другом месте, скорее всего, в специальной фабричной функции.
В приведенном примере конструкция Brokenвсегда терпит неудачу. Нет никакого способа сломать NotSoBrokenконструктор значений подобным образом (просто некуда писать код), но мы можем создать фабричную функцию, которая будет аналогично дефектной.
makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing
(первая строка - объявление сигнатуры типа: makeNotSoBrokenпринимает аргументы a Fooи a Barи выдает a Maybe NotSoBroken).
Тип возвращаемого значения должен быть, Maybe NotSoBrokenа не просто NotSoBrokenпотому, что мы указали его для оценки Nothing, который является конструктором значения Maybe. Типы просто не будут выстраиваться, если мы напишем что-то другое.
Помимо того, что эта функция абсолютно бессмысленна, эта функция даже не выполняет своего реального предназначения, как мы увидим, когда попробуем ее использовать. Давайте создадим функцию с именем, useNotSoBrokenкоторая ожидает NotSoBrokenв качестве аргумента:
useNotSoBroken :: NotSoBroken -> Whatever
( useNotSoBrokenпринимает NotSoBrokenв качестве аргумента a и выдает a Whatever).
И используйте это так:
useNotSoBroken (makeNotSoBroken)
В большинстве языков такое поведение может вызвать исключение нулевого указателя. В Haskell типы не совпадают: makeNotSoBrokenвозвращает a Maybe NotSoBroken, но useNotSoBrokenожидает a NotSoBroken. Эти типы не являются взаимозаменяемыми, и код не компилируется.
Чтобы обойти это, мы можем использовать caseоператор для ветвления на основе структуры Maybeзначения (используя функцию, называемую сопоставлением с шаблоном ):
case makeNotSoBroken of
Nothing -> --handle situation here
(Just x) -> useNotSoBroken x
Очевидно, этот фрагмент должен быть помещен в некоторый контекст для фактической компиляции, но он демонстрирует основы того, как Haskell обрабатывает обнуляемые значения. Вот пошаговое объяснение приведенного выше кода:
- Сначала
makeNotSoBrokenоценивается, что гарантированно выдает значение типа Maybe NotSoBroken.
caseЗаявление проверяет структуру этого значения.
- Если значение равно
Nothing, код «обрабатывать ситуацию здесь» оценивается.
- Если вместо этого значение совпадает со
Justзначением, выполняется другая ветвь. Обратите внимание, что соответствующее предложение одновременно идентифицирует значение как Justконструкцию и привязывает его внутреннее NotSoBrokenполе к имени (в данном случае, x). xзатем можно использовать как нормальное NotSoBrokenзначение, которое есть.
Таким образом, сопоставление с образцом обеспечивает мощное средство для обеспечения безопасности типов, поскольку структура объекта неразрывно связана с ветвлением управления.
Я надеюсь, что это было понятное объяснение. Если это не имеет смысла, прыгайте в Learn You A Haskell For Great Good! , один из лучших онлайн языковых уроков, которые я когда-либо читал. Надеюсь, вы увидите ту же красоту на этом языке, что и я.