Вот как это делает 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! , один из лучших онлайн языковых уроков, которые я когда-либо читал. Надеюсь, вы увидите ту же красоту на этом языке, что и я.