Залежно набрав Haskell, зараз?
Хаскелл - це в деякій мірі залежно набрана мова. Існує поняття даних на рівні типу, тепер більш розумно набране завдяки DataKinds
, і є деякі засоби ( GADTs
) дати представлення даних про рівень виконання даних про рівень часу. Отже, значення матеріалів, які виконуються під час виконання, ефективно відображаються у типах. Це означає, що мова має бути залежною від типу.
Прості типи даних просуваються до рівня виду, так що значення, які вони містять, можна використовувати в типах. Звідси архетипний приклад
data Nat = Z | S Nat
data Vec :: Nat -> * -> * where
VNil :: Vec Z x
VCons :: x -> Vec n x -> Vec (S n) x
стає можливим, а разом з ним і такими визначеннями, як
vApply :: Vec n (s -> t) -> Vec n s -> Vec n t
vApply VNil VNil = VNil
vApply (VCons f fs) (VCons s ss) = VCons (f s) (vApply fs ss)
що приємно. Зауважте, що довжина n
- це чисто статична річ у цій функції, забезпечуючи те, що вхідні та вихідні вектори мають однакову довжину, хоча ця довжина не грає жодної ролі у виконанні
vApply
. Навпаки, це набагато складніше (тобто неможливо) реалізувати функцію , яка робить n
копії даність x
(що було б , pure
щоб vApply
«s <*>
)
vReplicate :: x -> Vec n x
тому що важливо знати, скільки копій зробити під час виконання. Введіть одиночні.
data Natty :: Nat -> * where
Zy :: Natty Z
Sy :: Natty n -> Natty (S n)
Для будь-якого рекламного типу ми можемо побудувати сімейство однотонних, індексоване над промоційним типом, населене дублікатами його значень під час виконання. Natty n
- це тип копій, що виконуються під час виконання, на рівні типу n
:: Nat
. Зараз ми можемо писати
vReplicate :: Natty n -> x -> Vec n x
vReplicate Zy x = VNil
vReplicate (Sy n) x = VCons x (vReplicate n x)
Таким чином, у вас є значення рівня типу, підключене до значення часу виконання: перевірка копії часу виконання уточнює статичні знання про значення рівня типу. Незважаючи на те, що терміни та типи розділені, ми можемо працювати залежно типовим способом, використовуючи однотонну конструкцію як різновид епоксидної смоли, створюючи зв’язки між фазами. Це довгий шлях від дозволу довільних виразів запуску у типах, але це не нічого.
Що неприємно? Що не вистачає?
Давайте трохи тиснемо на цю технологію і подивимося, що починає коливатися. Ми можемо зрозуміти, що одиночні кнопки повинні бути керовані трохи неявніше
class Nattily (n :: Nat) where
natty :: Natty n
instance Nattily Z where
natty = Zy
instance Nattily n => Nattily (S n) where
natty = Sy natty
дозволяючи нам писати, скажімо,
instance Nattily n => Applicative (Vec n) where
pure = vReplicate natty
(<*>) = vApply
Це працює, але це означає, що наш оригінальний Nat
тип породив три екземпляри: вид, сімейство одиноких та клас одиночки. У нас є досить незграбний процес обміну явними Natty n
значеннями та Nattily n
словниками. Більше того, Natty
це не так Nat
: ми маємо якусь залежність від значень часу виконання, але не від типу, про який ми вперше думали. Жодна повністю набрана мова не ускладнює залежні типи!
Тим часом, хоча Nat
можна просунути, Vec
не можна. Не можна індексувати за індексованим типом. Повна мова залежно набраних мов не накладає такого обмеження, і в моїй кар’єрі як залежно набраний показ я навчився включати в свої розмови приклади двошарової індексації, просто щоб навчити людей, які зробили одношарову індексацію важко, але можливо не очікувати, що я складусь як будинок карт. В чому проблема? Рівність. GADT працюють, перекладаючи обмеження, які ви досягаєте неявно, коли ви надаєте конструктору певний тип повернення в явні рівняння. Подобається це.
data Vec (n :: Nat) (x :: *)
= n ~ Z => VNil
| forall m. n ~ S m => VCons x (Vec m x)
У кожному з наших двох рівнянь обидві сторони мають вид Nat
.
Тепер спробуйте той же переклад для чогось, що індексується над векторами.
data InVec :: x -> Vec n x -> * where
Here :: InVec z (VCons z zs)
After :: InVec z ys -> InVec z (VCons y ys)
стає
data InVec (a :: x) (as :: Vec n x)
= forall m z (zs :: Vec x m). (n ~ S m, as ~ VCons z zs) => Here
| forall m y z (ys :: Vec x m). (n ~ S m, as ~ VCons y ys) => After (InVec z ys)
і тепер ми формуємо рівнянні обмеження між as :: Vec n x
і
VCons z zs :: Vec (S m) x
там, де обидві сторони мають синтаксично чіткі (але, можливо, рівні) види. Ядро GHC наразі не обладнане для такої концепції!
Чого ще не вистачає? Ну, більшість Haskell відсутня на рівні типу. Мова термінів, які ви можете рекламувати, має лише змінні та не-GADT конструктори. Після того, як у вас є ці type family
пристрої, ви зможете писати програми на рівні типу: деякі з них можуть бути цілком подібними до функцій, які ви могли б вважати написанням на рівні терміна (наприклад, оснащення Nat
додатком, щоб ви могли дати хороший тип для додавання Vec
) , але це просто збіг!
Ще одна річ, на практиці відсутня - це бібліотека, яка використовує наші нові здібності для індексації типів за значеннями. Що робити Functor
та Monad
стати у цьому відважному новому світі? Я думаю про це, але є ще багато чого зробити.
Запуск програм типу рівня
Haskell, як і більшість залежних мов програмування, має дві
оперативні семантики. Ось так працює система запуску програм (лише закриті вирази, після стирання типу, сильно оптимізована), а потім є спосіб, за допомогою якого програма-контролер запускає програми (ваші сімейства типів, ваш "клас типу Prolog", з відкритими виразами). Для Haskell ви зазвичай не змішуєте їх, оскільки програми, що виконуються, є різними мовами. Мови залежно набраних типів мають окремі моделі виконання та статичні варіанти виконання для однієї мови програм, але не хвилюйтесь, модель запуску все ще дозволяє вводити стирання і, дійсно, доказ стирання: саме це видобуток Coqмеханізм дає вам; це принаймні те, що робить компілятор Едвіна Брейді (хоча Едвін стирає непотрібно дублювані значення, а також типи та докази). Розрізнення фаз більше не може бути розрізненням синтаксичної категорії
, але воно живе і здорово.
Залежно набрані мови, будучи тотальними, дозволяють шпигуню запускати програми, вільні від страху нічого гіршого, ніж довгого очікування. Оскільки Haskell набирає більш залежний характер, ми стикаємося з питанням, якою повинна бути його статична модель виконання? Одним із підходів може бути обмеження статичного виконання загальними функціями, що дасть нам таку ж свободу запуску, але може змусити нас робити розрізнення (принаймні для коду рівня типу) між даними та кодами, щоб ми могли сказати, чи потрібно примусове припинення або продуктивність. Але це не єдиний підхід. Ми вільні вибирати набагато слабку модель виконання, яка не хоче запускати програми, ціною створення меншої кількості рівнянь виходить лише шляхом обчислення. По суті, саме це і робить GHC. Правила набору тексту для ядра GHC не згадують про запуск
програми, але лише для перевірки доказів рівнянь. При перекладі на ядро вирішувач обмежень GHC намагається запустити ваші програми на рівні типу, створюючи трохи сріблястого сліду доказів того, що даний вираз дорівнює його нормальній формі. Цей метод отримання доказів є трохи непередбачуваним і неминуче незавершеним: наприклад, бореться із сором'язливою страшною рекурсією, наприклад, і це, мабуть, мудро. Одне, про що нам не потрібно хвилюватися, - це виконання IO
обчислень у контролері типу: пам’ятайте, що шпигун машин не повинен надавати
launchMissiles
того ж значення, що і система виконання часу!
Культура Хіндлі-Мілнера
Система типу Хіндлі-Мілнера досягає по-справжньому приголомшливого збігу чотирьох чітких розрізнень, з нещасливим культурним побічним ефектом, що багато людей не бачать розрізнення між відмінностями і вважають, що збіг неминучий! Про що я говорю?
- терміни проти типів
- явно написані речі проти неявно написані речі
- присутність під час запуску проти стирання перед часом виконання
- не залежна абстракція від залежної кількісної оцінки
Ми звикли писати терміни і залишати типи для висновку ..., а потім стиратися. Ми звикли кількісно оцінювати змінні типу з відповідним типом абстракцій та застосувань, що відбувається безшумно та статично.
Вам не доведеться занадто віддалятися від ванілі Хіндлі-Мілнер, перш ніж ці відмінності вийдуть у відповідність, і це не погано . Для початку ми можемо мати більше цікавих типів, якщо ми готові написати їх у кількох місцях. Тим часом нам не потрібно писати словники класу типів, коли ми використовуємо перевантажені функції, але ці словники, безумовно, присутні (або вбудовані) під час виконання. У залежно введених мовах ми очікуємо, що під час виконання буде видалено більше, ніж просто типи, але (як це стосується класів типів), що деякі неявно виведені значення не будуть стерті. Наприклад, vReplicate
числовий аргумент часто можна порівняти з типу потрібного вектора, але нам все одно потрібно знати це під час виконання.
Який вибір мовного дизайну слід переглянути, оскільки ці збіги вже не мають значення? Наприклад, чи правильно, що Haskell не забезпечує способу forall x. t
явного визначення кількісного показника? Якщо машинка для введення тексту не може здогадатися x
, уніфікуючи t
, у нас немає іншого способу сказати, щоx
має бути.
В більш широкому сенсі, ми не можемо трактувати "умовивід" типу як монолітну концепцію, про яку ми маємо все або нічого. Для початку нам потрібно розділити аспект "узагальнення" (правило Мілнера "нехай"), яке в значній мірі покладається на обмеження того, які типи існують, щоб забезпечити, що дурна машина може здогадатися про неї, з аспекту "спеціалізації" (мілнера "var "правило), яке настільки ж ефективно, як і ваш вирішувач обмежень. Ми можемо очікувати, що типи вищого рівня стануть важче зробити, але інформацію про внутрішній тип залишатимуться досить простою.
Наступні кроки щодо Haskell
Ми бачимо, що рівень і вид зростають дуже схожими (і вони вже поділяють внутрішнє представництво в GHC). Ми також можемо їх об'єднати. Було б весело взяти, * :: *
якщо ми можемо: ми втратили
логічну здоровість давно, коли нам дозволили знизу, але
звуковість типу зазвичай є слабшою вимогою. Ми повинні перевірити. Якщо у нас повинні бути різні типи, види та ін. Рівні, ми можемо принаймні переконатися, що все на рівні типу та вище завжди можна рекламувати. Було б чудово просто повторно використовувати поліморфізм, який ми вже маємо для типів, а не переосмислювати поліморфізм на такому рівні.
Ми повинні спростити та узагальнити діючу систему обмежень, дозволяючи гетерогенні рівняння, a ~ b
де види a
та
b
не є синтаксично однаковими (але можуть бути доведені рівними). Це стара методика (в моїй тезі минулого століття), яка набагато легше справляється із залежністю. Ми зможемо висловити обмеження на вирази в GADT, і таким чином зняти обмеження щодо того, що можна рекламувати.
Ми повинні виключити необхідність будівництва одноплідних шляхом введення залежного типу функції, pi x :: s -> t
. Функція з таким типом може бути явно застосована до будь-якого виразу типу, s
яке живе в перетині мов типу та терміна (так, змінні, конструктори, з якими більше пізніше). Відповідна лямбда та додаток не буде стерто під час виконання, тому ми зможемо написати
vReplicate :: pi n :: Nat -> x -> Vec n x
vReplicate Z x = VNil
vReplicate (S n) x = VCons x (vReplicate n x)
без заміни Nat
на Natty
. Домен pi
може бути будь-якого рекламного типу, тому, якщо можна просувати GADT, ми можемо записувати залежні послідовності кількісних показників (або "телескопи", як їх називав Де Брюййн)
pi n :: Nat -> pi xs :: Vec n x -> ...
будь-якої довжини нам потрібно.
Сенс цих кроків полягає у усуненні складності , працюючи безпосередньо з більш загальними інструментами, замість того, щоб робити слабкі інструменти та незграбні кодування. Нинішня часткова виплата робить вигоди залежних типів від Haskell дорожчими, ніж потрібно.
Занадто складно?
Залежні типи нервують багатьох людей. Вони нервують мене, але мені подобається нервувати, або принаймні мені важко не нервувати. Але це не допомагає, що навколо цієї теми досить туман невігластва. Дещо це пов’язано з тим, що всім нам ще багато чому навчитися. Але, як відомо, прихильники менш радикальних підходів закладають страх перед залежними типами, не переконуючись, що факти цілком пов'язані з ними. Я не буду називати імена. Ці міфи "нерозбірливі перевірки типу", "Тюрінг незавершений", "відсутність фазового розрізнення", "відсутність стирання типу", "докази скрізь" тощо.
Звичайно, це не так, що програми, що набираються залежно, завжди повинні бути доведені правильними. Можна покращити основну гігієну своїх програм, застосовуючи додаткові інваріанти у типах, не переходячи до повних специфікацій. Невеликі кроки в цьому напрямку нерідко призводять до набагато сильніших гарантій з невеликими або відсутніми додатковими зобов'язаннями щодо доказування. Це неправда, що залежно введені програми неминуче наповнені доказами, адже я зазвичай приймаю будь-які докази в своєму коді як підказку під питання моїх визначень .
Бо, як і при будь-якому збільшенні артикуляції, ми можемо вільно говорити про недобрі речі, а також про справедливі. Наприклад, існує безліч крихких способів визначення дерев бінарного пошуку, але це не означає, що це не гарний спосіб . Важливо не припускати, що поганого досвіду не можна покращити, навіть якщо воно заполонить его, щоб визнати це. Розробка залежних визначень - це нова майстерність, яка вимагає навчання, а те, що програміст Haskell, не робить вас автоматично експертом! І навіть якщо деякі програми є невірними, чому б ви заперечували іншим свободу бути справедливою?
Навіщо все-таки турбуватися з Haskell?
Мені дуже подобаються залежні типи, але більшість моїх хакерських проектів все ще перебувають у Haskell. Чому? У Haskell є типи класів. У Haskell є корисні бібліотеки. Haskell має працездатний (хоча і далеко не ідеальний) режим програмування з ефектами. Haskell має промисловий компілятор міцності. Мови залежно введеного типу знаходяться на набагато більш ранній стадії у зростанні спільноти та інфраструктури, але ми потрапимо туди, з реальним поколінням зрушень у тому, що можливо, наприклад, шляхом метапрограмування та типових даних даних. Але вам просто потрібно озирнутися, що роблять люди в результаті кроків Хаскелла до залежних типів, щоб побачити, що є багато користі, яка також може бути отримана шляхом висування сучасного покоління мов.