Чи є причина мати нижній тип мови програмування?


49

Нижній тип - це конструкція, головним чином з'являється в математичній теорії типів. Його ще називають порожнім типом. Це тип, який не має значень, але є підтипом усіх типів.

Якщо тип повернення функції - це нижній тип, це означає, що він не повертається. Період. Можливо, він зациклюється назавжди, або, можливо, це викидає виняток.

Який сенс мати цей дивний тип мови програмування? Це не так часто, але він присутній у деяких, таких як Скала та Лісп.


2
@SargeBorsch: ти впевнений у цьому? Звичайно, не можна в C чітко визначити voidдані ...
Basile Starynkevitch

3
@BasileStarynkevitch немає значень типу void, і тип одиниці повинен мати одне значення. Крім того, як ви вказали, ви навіть не можете оголосити значення типу void, це означає, що це навіть не тип, а лише особливий кутовий регістр у мові.
Sarge Borsch

2
Так, C у цьому химерний, особливо в тому, як записуються типи вказівників та функцій. Але voidв Java майже те саме: насправді це тип і не може мати значення.
Сардж Борщ

3
У семантиці мов із типом нижнього типу вважається, що тип нижнього не має значень, а скоріше має одне значення, нижнє значення, що представляє собою обчислення, яке ніколи не завершується (як правило). Оскільки нижнє значення - це значення кожного типу, нижній тип може бути підтипом кожного типу.
Теодор Норвелл

4
@BasileStarynkevitch Common Lisp має нульовий тип, який не має значень. Він також має нульовий тип, який має лише одне значення, символ nil(aka, ()), який є одиничним типом.
Джошуа Тейлор

Відповіді:


33

Я візьму простий приклад: C ++ проти Rust.

Ось функція, яка використовується для кидання винятку в C ++ 11:

[[noreturn]] void ThrowException(char const* message,
                                 char const* file,
                                 int line,
                                 char const* function);

І ось такий еквівалент у Іржі:

fn formatted_panic(message: &str, file: &str, line: isize, function: &str) -> !;

Що стосується суто синтаксичного питання, конструкція Іржі є більш розумною. Зауважте, що конструкція C ++ визначає тип повернення, хоча він також визначає, що він не збирається повертатись. Це трохи дивно.

У стандартній примітці синтаксис C ++ з'явився лише з C ++ 11 (він був встановлений на вершині), але різні компілятори протягом деякого часу надавали різні розширення, так що інструменти аналізу сторонніх сторін повинні були запрограмуватися для розпізнавання різних способів цей атрибут можна було записати. Його стандартизація, очевидно, є вищою.


Тепер, як на користь?

Те, що функція не повертається, може бути корисною для:

  • оптимізація: після цього можна обрізати будь-який код (він не повернеться), немає необхідності зберігати регістри (тому що відновити їх не потрібно), ...
  • статичний аналіз: він виключає ряд потенційних шляхів виконання
  • ремонтопридатність: (див. статичний аналіз, але людиною)

6
voidу вашому прикладі C ++ визначає (частина) тип функції, а не тип повернення. Це обмежує значення, яким функція дозволена return; все, що може перетворитись на недійсне (що є нічого). Якщо функція returns, за нею не повинно слідувати значення. Повний тип функції void () (char const*, char const*, int, char const *). + 1 для використання char constзамість const char:-)
Очищення

4
Це не означає, що має більше сенсу мати нижній тип, але просто має сенс анотувати функції щодо того, повертаються вони чи ні як частина мови. Насправді, оскільки функції не можуть повернутися через різні причини, схоже, краще кодувати причину якось замість того, щоб використовувати термін «загальний вигляд», подібний до порівняно недавньої концепції анотування функцій на основі їх побічних ефектів.
GregRos

2
Насправді, є причина зробити "не повертається" і "має тип повернення X" незалежним: зворотна сумісність для вашого власного коду, оскільки умова виклику може залежати від типу повернення.
Дедуплікатор

це [[noreturn]] частина синтаксису чи додавання функціональності?
Заїбіс

1
[продовження] Загалом, я просто скажу, що обговорення переваг of має визначити, що кваліфікується як реалізація of; і я не думаю, що система типів, яка не має ( a → ⊥) ≤ ( ab ), є корисною реалізацією of. Тож у цьому сенсі ABI SysV x86-64 C (серед інших) просто не дозволяє реалізувати ⊥.
Олексій Шпілкін

26

Відповідь Карла хороша. Ось додаткове використання, яке, я думаю, ніхто ще не згадував. Тип

if E then A else B

повинен бути тип, який включає всі значення у типі Aта всі значення у типі B. Якщо тип Bє Nothing, то типом ifвиразу може бути тип A. Я часто оголошую рутину

def unreachable( s:String ) : Nothing = throw new AssertionError("Unreachable "+s) 

сказати, що не очікується досягнення коду. Оскільки його тип є Nothing, unreachable(s)тепер його можна використовувати будь-який ifабо (частіше), switchне впливаючи на тип результату. Наприклад

 val colour : Colour := switch state of
         BLACK_TO_MOVE: BLACK
         WHITE_TO_MOVE: WHITE
         default: unreachable("Bad state")

У Scala такий тип Нічого.

Іншим випадком використання Nothing(як згадується у відповіді Карла) є Список [Нічого] - це список списків, кожен з членів яких має тип Нічого. Таким чином, це може бути тип порожнього списку.

Ключова властивість , Nothingщо робить ці випадки використання роботи НЕ що не має значення --although в Scala, наприклад, він є нічим values-- це те , що вона є підтипом будь-якого іншого типу.

Припустимо, у вас є мова, де кожен тип містить однакове значення - назвемо це (). У такій мові тип одиниці, який має ()єдине значення, може бути підтипом кожного типу. Це не робить його нижчим типом у тому сенсі, що це означало ОП; в ОП було зрозуміло, що нижній тип не містить значень. Однак, оскільки це тип, який є підтипом кожного типу, він може грати майже таку ж роль, як і нижній тип.

Haskell робить щось трохи інакше. У Haskell вираз, який ніколи не створює значення, може мати типову схему forall a.a. Екземпляр цієї схеми типів буде уніфікований з будь-яким іншим типом, тому він ефективно діє як нижній тип, хоча (стандартний) Haskell не має поняття підтипу. Наприклад, errorфункція зі стандартної прелюдії має схему типу forall a. [Char] -> a. Тож можна писати

if E then A else error ""

і тип виразу буде таким самим, як і тип Aдля будь-якого виразу A.

Порожній список в Haskell має типову схему forall a. [a]. Якщо Aце вираз, тип якого - тип списку, то

if E then A else []

- це вираз того ж типу, що і A.


Яка різниця між типом forall a . [a]та типом [a]у Haskell? Чи не є змінні типу вже загально кількісно визначеними у виразах типу Haskell?
Джорджіо

@Giorgio У Haskell універсальне кількісне визначення неявне, якщо зрозуміло, що ви дивитесь на типову схему. Ви навіть не можете написати forallв стандартному Haskell 2010. Я написав кількісне визначення чітко, оскільки це не форум Haskell, і деякі люди можуть не бути знайомі з умовами Haskell. Так що різниці немає, крім того, що forall a . [a]це не є стандартним, тоді як [a]є.
Теодор Норвелл

19

Типи утворюють моноїд двома способами, разом роблячи семірінг . Саме так називають алгебраїчні типи даних . Для кінцевих типів цей семірінг безпосередньо стосується семірування натуральних чисел (включаючи нуль), що означає, що ви підраховуєте, скільки можливих значень має тип (виключаючи "значення, що не визначаються").

  • Нижній тип (я його назву Vacuous) має нульові значення .
  • Тип одиниці має одне значення. Я назву і тип, і його єдине значення ().
  • Композиція (яку більшість мов програмування підтримує досить безпосередньо, через записи / структури / класи з публічними полями) - це продуктова операція. Так , наприклад, (Bool, Bool)має чотири можливих значення, а саме (False,False), (False,True), (True,False)і (True,True).
    Тип одиниці - це ідентичний елемент операції композиції. Наприклад , ((), False)і ((), True)є тільки значення типу ((), Bool), так що цей тип ізоморфно Boolсобі.
  • Альтернативні типи дещо нехтують більшості мов (мови ОО начебто підтримують їх у спадок), але вони не менш корисні. Альтернатива між двома типами Aі в Bосновному має всі значення Aплюс всі значення B, отже, і суму суми . Так , наприклад, Either () Boolмає три значення, я буду називати їх Left (), Right Falseі Right True.
    Нижній тип - це ідентичний елемент суми: Either Vacuous Aмає лише значення форми Right a, оскільки Left ...не має сенсу ( Vacuousне має значень).

Що стосується цих моноїдів, це те, що коли ви вводите функції до своєї мови, категорія цих типів з функціями як морфізми є моноїдною категорією . Крім усього іншого, це дозволяє визначити прикладні функтори та монади , які виявляються відмінною абстракцією для загальних обчислень (можливо, із залученням побічних ефектів тощо) у суто функціональних умовах.

Тепер, насправді, ви можете зайти досить далеко, переймаючись лише однією стороною питання (моноїд композиції), тоді вам не потрібен явний тип явно. Наприклад, навіть Haskell довгий час не мав стандартного типу дна. Тепер є, називається Void.

Але якщо ви розглядаєте повну картину як бікартезіанську закриту категорію , то система типів насправді еквівалентна цілому лямбдальному числу , тому в основному ви маєте ідеальну абстракцію щодо всього можливого на мові, повній Тьюрінгу. Відмінно підходить для вбудованих мов, що стосуються домену, наприклад, існує проект прямого кодування електронних схем таким чином .

Звичайно, ви цілком можете сказати, що це загальна дурниця теоретиків . Вам взагалі не потрібно знати про теорію категорій, щоб бути хорошим програмістом, але коли ви це робите, це дає вам потужні та смішно загальні способи міркувати про код та доводити інваріанти.


mb21 нагадує мені зазначити, що це не слід плутати з нижчими значеннями . У ледачих мовах, як Haskell, кожен тип містить нижнє "значення", позначене . Це не конкретна річ, яку ви коли-небудь могли явно обійти, натомість це те, що «повертається», наприклад, коли функція навіяється вічно. Навіть Voidтип Haskell "містить" нижнє значення, таким чином, ім'я. У такому світлі нижній тип Хаскелла справді має одне значення, а його одиничний тип має два значення, але в обговоренні теорії категорій це, як правило, ігнорується.


"Нижній тип (я його назву Void)", який не слід плутати зі значенням bottom , яке є членом будь-якого типу в Haskell .
mb21

18

Можливо, він зациклюється назавжди, або, можливо, це викидає виняток.

Звучить, як корисний тип у таких ситуаціях, але вони рідкісні.

Крім того, незважаючи на те, що Nothing(назва Scala для нижнього типу) не може мати значень, List[Nothing]воно не має такого обмеження, що робить його корисним як тип порожнього списку. Більшість мов обходять це шляхом створення порожнього списку рядків іншого типу, ніж порожній список цілих чисел, який має сенс, але робить порожній список більш багатослівним для запису, що є великим недоліком у мові, орієнтованій на список.


12
«Порожній список Haskell є конструктором типу»: звичайно , відповідна річ про це тут більш , що це полиморфном, або перевантажений - тобто, порожніх списках різних типів різних значень, але []представляє все з них, і буде instanatiated до конкретний тип у міру необхідності.
Пітер ЛеФану Лумсдейн

Цікаво: Якщо ви намагаєтеся створити порожній масив в інтерпретаторі Haskell, ви отримаєте дуже певне значення з дуже невизначеним типом: [a]. Аналогічно :t Left 1врожайність Num a => Either a b. Власне оцінюючи вираз, змушує тип a, але не b:Either Integer b
Джон Дворак

5
Порожній список - це конструктор значень . Трохи заплутано, що задіяний конструктор типів має те саме ім'я, але сам порожній список - це значення не тип (ну, є і списки рівнів типів, але це зовсім інша тема). Частина, яка змушує роботу порожнього списку для будь-якого типу списку, має на увазі forallйого тип forall a. [a],. Є кілька приємних способів подумати forall, але це дійсно потребує певного часу, щоб зрозуміти.
Девід

@PeterLeFanuLumsdaine Саме це означає конструктор типу. Це просто означає, що це тип із виглядом, відмінним від *.
GregRos

2
У Haskell []- конструктор типу і []є виразом, що представляє порожній список. Але це не означає, що "порожній список Haskell - це конструктор типів". Контекст дає зрозуміти [], використовується він як тип або як вираз. Припустимо, ви заявляєте data Foo x = Foo | Bar x (Foo x); тепер ви можете використовувати Fooяк конструктор типів або як значення, але просто випадково вибирали те саме ім’я для обох.
Теодор Норвелл

3

Для статичного аналізу корисно документувати той факт, що певний шлях коду недоступний. Наприклад, якщо ви пишете таке в C #:

int F(int arg) {
 if (arg != 0)
  return arg + 1; //some computation
 else
  Assert(false); //this throws but the compiler does not know that
}
void Assert(bool cond) { if (!cond) throw ...; }

Компілятор поскаржиться, що Fнічого не повертає принаймні в один кодовий шлях. Якби Assertїх було позначено як неповернення, компілятору не потрібно було б попереджати.


2

У деяких мовах nullє нижній тип, оскільки підтип усіх типів добре визначає, для яких мов використовується нуль (незважаючи на легке протиріччя існування nullяк себе, так і функції, яка повертається сама, уникаючи загальних аргументів про те, чому botслід неживати).

Він також може бути використаний як загальний для функцій типів ( any -> bot), щоб обробляти відправлення, яке пішло не так.

І деякі мови дозволяють фактично вирішити botяк помилку, яку можна використовувати для надання власних помилок компілятора.


11
Ні, тип дна не є типом одиниці. Тип дна взагалі не має значення, тому функція, що повертає тип дна, не повинна повертатися (тобто кидати виняток або цикл на невизначений термін)
Basile Starynkevitch

@BasileStarynkevitch - я не кажу про тип одиниці. Одиничний тип відображається на voidзагальних мовах (хоча і з дещо різною семантикою для одного використання), ні null. Хоча ви також маєте рацію, що більшість мов не мають нульового типу нижнього типу.
Теластин

3
@TheodoreNorvell - ранні версії Tangent зробили це - хоча я це автор, тому, можливо, це обман. У мене немає збережених посилань для інших, і минуло час, коли я провів це дослідження.
Теластин

1
@Martijn Але ви можете використовувати null, наприклад, для порівняння вказівника на nullотримання булевого результату. Я думаю, що відповіді показують, що існують два різних види низу. (a) Мови (наприклад, Scala), де тип, який є підтипом кожного типу, являє собою обчислення, які не дають жодних результатів. По суті це порожній тип, хоча технічно часто заповнюється непотрібним донним значенням, що представляє собою невинищення. (b) Такі мови, як Тангент, у яких нижній тип є підмножиною кожного іншого типу, оскільки він містить корисне значення, яке також є у всіх інших типах - нульове.
Теодор Норвелл

4
Цікаво, що деякі мови мають значення з типом, який ти не можеш оголосити (загальний для нульового буквального), а інші мають тип, який ти можеш оголосити, але не має значень (традиційний нижній тип), і що вони заповнюють дещо порівнянні ролі .
Martijn

1

Так, це досить корисний тип; хоча його роль буде здебільшого внутрішня для типової системи, є певний випадок, коли нижній тип з'явиться відкрито.

Розглянемо статично типізовану мову, в якій умовні умови - це вирази (тому конструкція «if-then-else» подвоюється як потрійний оператор C і friends, і може бути подібний випадок із багатостороннього випадку). Мова функціонального програмування є такою, але це трапляється і в деяких імперативних мовах (ще з часу ALGOL 60). Тоді всі виразні гілки повинні в кінцевому рахунку виробляти тип усього умовного виразу. Можна просто вимагати, щоб їх типи були рівними (і я думаю, що це стосується потрійного оператора в С), але це занадто обмежувально, особливо, коли умовне також можна використовувати як умовне твердження (не повертаючи жодного корисного значення). Загалом хочеться, щоб кожен вираз гілки був (неявно) конвертованим до загального типу, який буде типом повного виразу (можливо, з більш-менш складними обмеженнями, щоб дозволити ефективно знайти цей загальний тип компілятором, пор. C ++, але я не буду вникати в ці деталі тут).

Є два типи ситуацій, коли загальний вид перетворення дозволить забезпечити необхідну гнучкість таких умовних виразів. Одне вже згадується, де тип результату є одиничним типомvoid; це, природно, супер-тип усіх інших типів, і дозволяє будь-який тип (тривіально) перетворюватися на нього дає змогу використовувати умовний вираз як умовний вислів. Інший стосується випадків, коли вираз повертає корисне значення, але одна або декілька гілок неможливо створити. Зазвичай вони піднімають виняток або передбачають стрибок, і вимагати від них (також) створити значення типу цілого виразу (з недосяжної точки) було б безглуздо. Саме таку ситуацію можна вишукано вирішити, надавши пропозиції про підвищення винятків, стрибки та виклики, які матимуть такий ефект, нижній тип, той тип, який можна (тривіально) перетворити на будь-який інший тип.

Я б запропонував написати такий нижній тип, *щоб запропонувати його конвертованість у довільний тип. Він може слугувати іншим корисним цілям внутрішньо, наприклад, намагаючись вивести тип результату для рекурсивної функції, яка не декларує жодної, inferencer типу може призначити тип *будь-якому рекурсивному виклику, щоб уникнути ситуації з куркою-яйцем; фактичний тип визначатиметься нерекурсивними гілками, а рекурсивні - перетворюються на загальний тип нерекурсивних. Якщо взагалі немає нерекурсивних гілок, тип залишатиметься *і правильно вказує, що функція не має можливості повернутися з рекурсії. Окрім цього та як результат типу функцій метання винятків, можна використовувати*як компонентний тип послідовностей довжиною 0, наприклад, порожній список; знову ж таки, якщо коли-небудь елемент буде обраний із виразу типу [*](обов’язково порожній список), то отриманий тип *буде правильно вказано, що це не може повернутися без помилки.


Тому ідея , що var foo = someCondition() ? functionReturningBar() : functionThatAlwaysThrows()може визначити тип , fooяк Bar, так як вираз ніколи не міг більше нічого дати?
supercat

1
Ви тільки що описали тип одиниці - принаймні в першій частині своєї відповіді. Функція, яка повертає тип одиниці, є такою ж, як та, яка оголошується як повернення voidв C. Друга частина вашої відповіді, де ви говорите про тип функції, яка ніколи не повертається, або список без елементів - це справді нижній тип! (Часто пишеться як, _|_а не *. Не впевнений, чому. Можливо, це схоже на (людське) дно :)
andrewf

2
Щоб уникнути сумнівів: "не повертає нічого корисного" відрізняється від "не повертається"; перший представлений типом одиниці; другий за типом "Низ".
andrewf

@andrewf: Так, я розумію відмінність. Моя відповідь трохи тривалий, але я хотів би сказати, що тип одиниці і нижній тип обидва грають (різні, але) порівнянні ролі, дозволяючи використовувати певні вирази більш гнучко (але все-таки безпечно).
Марк ван Левен

@supercat: Так, це ідея. В даний час в C ++ , який є незаконним, хоча це було б справедливо , якщо functionThatAlwaysThrows()були замінені явним throw, з - за особливої мови в стандарті. Наявність типу, який це робить, було б вдосконаленням.
Марк ван Левен

0

На деяких мовах ви можете коментувати функцію, щоб сказати як компілятору, так і розробникам, що виклик цієї функції не повернеться (і якщо функція написана таким чином, що вона може повернутися, компілятор не дозволить їй ). Це корисна річ, яку потрібно знати, але врешті-решт ви можете викликати таку функцію, як будь-яка інша. Компілятор може використовувати інформацію для оптимізації, для попередження про мертвий код тощо. Таким чином, немає дуже вагомих причин мати цей тип, але немає дуже вагомих причин уникати цього.

У багатьох мовах функція може повернути "void". Що саме це означає, залежить від мови. У C це означає, що функція нічого не повертає. У Swift це означає, що функція повертає об'єкт лише з одним можливим значенням, і оскільки існує лише одне можливе значення, це значення займає нульові біти і фактично не вимагає жодного коду. В будь-якому випадку це не те саме, що "дно".

"bottom" буде типом без можливих значень. Він ніколи не може існувати. Якщо функція повертає "знизу", вона фактично не може повернутися, оскільки немає значення типу "дно", яке вона могла б повернути.

Якщо мовний дизайнер відчуває, що це подобається, то немає підстав не мати такого типу. Реалізація не є складною (ви можете реалізувати її точно як функцію, що повертає недійсність і позначена як "не повертається"). Ви не можете змішувати покажчики до функцій, що повертаються знизу з покажчиками, до функцій, що повертають недійсність, оскільки вони не є одним типом).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.