Ось як це робить 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 неймовірно сувора. Якби мені довелося підсумувати центральну філософію дизайну типової системи Хаскелла, я б сказав: "Зробіть якомога більше речей, помиляйтесь під час компіляції, щоб якомога менше помилялось під час виконання". Ніяких неявних перетворень взагалі немає (хочете популяризувати Int
a 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 для великого добра! , один з найкращих мовних навчальних посібників в Інтернеті, який я коли-небудь читав. Сподіваємось, ви побачите ту саму красу на цій мові, яку я і роблю.