Ось як це робить Haskell: (не зовсім протилежний твердженням Ліпперта, оскільки Haskell не є об'єктно-орієнтованою мовою).
ПОПЕРЕДЖЕННЯ: відповідь з довгого вітром від серйозного фанату Haskell попереду.
TL; DR
Цей приклад точно ілюструє, чим відрізняється Haskell від C #. Замість делегування логістики побудови конструкції конструктору, вона повинна оброблятися в навколишньому коді. Немає можливості для значення null (або Nothingв Haskell) з'являтись там, де ми очікуємо ненульового значення, оскільки нульові значення можуть виникати лише в межах спеціальних типів обгортки, Maybeякі не є взаємозамінними з / безпосередньо перетворюються на звичайні, неконвертовані мінливі типи. Для того, щоб використовувати значення, яке робиться нульовим, обернувши його в a Maybe, ми повинні спочатку дістати значення, використовуючи відповідність шаблону, що змушує нас перенаправляти контрольний потік у гілку, де ми точно знаємо, що у нас є ненулеве значення.
Тому:
чи можемо ми завжди знати, що нерегульована посилання ніколи за жодних обставин не виявляється недійсною?
Так. Intі Maybe Intє двома абсолютно окремими типами. Знаходження Nothingв рівнині Intбуло б порівняно з пошуком рядка "fish" в an Int32.
А як щодо конструктора об'єкта з ненульовим полем опорного типу?
Не проблема: конструктори значень у Haskell нічого не можуть зробити, окрім як взяти задані їм значення, так і скласти їх. Вся логіка ініціалізації відбувається до виклику конструктора.
А як щодо фіналізатора такого об’єкта, де об’єкт доопрацьовується, оскільки код, який повинен був заповнити посилання, кинув виняток?
У Haskell немає фіналізаторів, тому я не можу реально вирішити це питання. Однак моя перша відповідь все ще залишається.
Повний відповідь :
Haskell не має нуля, і використовує Maybeтип даних для представлення зведених змін. Можливо, тип алгабраїчних даних визначений так:
data Maybe a = Just a | Nothing
Для тих, хто вам не знайомий з Haskell, читайте це як "A Maybe- це Nothingабо a Just a". Конкретно:
Maybe- конструктор типів : його можна розглядати (неправильно) як загальний клас (де a- змінна типу). Аналогія C # є class Maybe<a>{}.
Justє конструктором значень : це функція, яка приймає один аргумент типу aі повертає значення типу, Maybe aяке містить значення. Тож код x = Just 17аналогічний int? x = 17;.
Nothingє іншим конструктором значень, але він не бере ніяких аргументів і Maybeповернутий не має значення, окрім "Нічого". x = Nothingє аналогом int? x = null;(якщо припустити, що ми обмежили своє aіснування в Haskell Int, що можна зробити, написавши x = Nothing :: Maybe Int).
Тепер, коли основи Maybeтипу не виходять з ладу, як Haskell уникає питань, обговорених у питанні ОП?
Ну, Haskell насправді відрізняється від більшості обговорюваних мов, тому я розпочну з пояснення кількох основних мовних принципів.
По-перше, у Haskell все незмінне . Все. Імена стосуються значень, а не місць пам'яті, де можна зберігати значення (одне лише це величезне джерело усунення помилок). В відміну від C #, де оголошення змінної і присвоювання дві окремі операції, в значеннях Haskell створюються шляхом визначення їх значення (наприклад x = 15, y = "quux", z = Nothing), який ніколи не може змінитися. Тому кодуємо як:
ReferenceType x;
Не можливо в Хаскелл. Немає проблем з ініціалізацією значень, nullоскільки все має бути явно ініціалізовано до значення, щоб воно існувало.
По-друге, Хаскелл - не об’єктно-орієнтована мова : це суто функціональна мова, тому об’єктів у строгому значенні цього слова немає. Натомість є просто функції (конструктори значень), які беруть свої аргументи і повертають об'єднану структуру.
Далі, абсолютно немає імперативного коду стилю. Під цим я маю на увазі, що більшість мов слідують такому шаблону:
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 неймовірно сувора. Якби мені довелося підсумувати центральну філософію дизайну типової системи Хаскелла, я б сказав: "Зробіть якомога більше речей, помиляйтесь під час компіляції, щоб якомога менше помилялось під час виконання". Ніяких неявних перетворень взагалі немає (хочете популяризувати Inta Double? Використовуйте fromIntegralфункцію). Єдине, що, можливо, має недійсне значення, яке виникає під час виконання, - це використання Prelude.undefined(яке, мабуть, просто має бути там і неможливо видалити ).
Зважаючи на все це, давайте подивимось на "зламаний" приклад Амона і спробуємо повторно висловити цей код у 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 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 для великого добра! , один з найкращих мовних навчальних посібників в Інтернеті, який я коли-небудь читав. Сподіваємось, ви побачите ту саму красу на цій мові, яку я і роблю.