Для чого корисна абсурдна функція в Data.Void?


97

absurdФункція Data.Voidмає такий підпис, де Voidє логічно нежилим типом експортованого цього пакетом:

-- | Since 'Void' values logically don't exist, this witnesses the logical
-- reasoning tool of \"ex falso quodlibet\".
absurd :: Void -> a

Я знаю достатньо логіки, щоб отримати зауваження документації про те, що це відповідає відповідно до пропозицій як типів дійсній формулі ⊥ → a.

Мене бентежить і цікавить: у яких практичних завданнях програмування ця функція корисна? Я думаю, що, можливо, це корисно в деяких випадках, оскільки безпечний тип вичерпного розгляду справ "не може статися", але я недостатньо знаю про практичне використання Керрі-Говарда, щоб визначити, чи ця ідея є правильний шлях взагалі.

РЕДАКТУВАТИ: Приклади бажано в Haskell, але якщо хтось хоче використовувати залежну мову, я не буду скаржитися ...


5
Швидкий пошук показує, що absurdфункція була використана в цій статті, що стосується Contмонади: haskellforall.com/2012/12/the-continuation-monad.html
Артем

6
Ви можете розглядати absurdяк один із напрямків ізоморфізму між Voidі forall a. a.
Даніель Вагнер

Відповіді:


61

Життя трохи важке, оскільки Хаскелл не є суворим. Загальним випадком використання є обробка неможливих шляхів. Наприклад

simple :: Either Void a -> a
simple (Left x) = absurd x
simple (Right y) = y

Це виявляється дещо корисним. Розглянемо простий тип дляPipes

data Pipe a b r
  = Pure r
  | Await (a -> Pipe a b r)
  | Yield !b (Pipe a b r)

це суворо ідентифікована та спрощена версія стандартного типу труб з Pipesбібліотеки Габріеля Гонсалеса . Тепер ми можемо кодувати трубу, яка ніколи не дає (тобто споживача), як

type Consumer a r = Pipe a Void r

це насправді ніколи не дає. Наслідком цього є те, що правильне правило згортання для a Consumerє

foldConsumer :: (r -> s) -> ((a -> s) -> s) -> Consumer a r -> s
foldConsumer onPure onAwait p 
 = case p of
     Pure x -> onPure x
     Await f -> onAwait $ \x -> foldConsumer onPure onAwait (f x)
     Yield x _ -> absurd x

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

Напевно, найбільш класичне використання Void- у CPS.

type Continuation a = a -> Void

тобто a Continuation- це функція, яка ніколи не повертається. Continuationє типовою версією "not." З цього ми отримуємо монаду CPS (що відповідає класичній логіці)

newtype CPS a = Continuation (Continuation a)

оскільки Хаскелл чистий, ми нічого не можемо отримати з цього типу.


1
Га, я насправді можу простежити за цим бітом CPS. Я, звичайно, раніше чув про подвійне заперечення / CPS про Каррі-Говарда, але не розумів цього; Я не збираюся стверджувати, що зараз його повністю отримую, але це, безумовно, допомагає!
Луїс Касільяс

"Життя трохи важке, оскільки Хаскелл не є суворим " - що ви маєте на увазі саме під цим?
Ерік Каплун,

4
@ErikAllik, суворою мовою, Voidє нежилим. У Haskell він містить _|_. Суворою мовою конструктор даних, який приймає аргумент типу, Voidніколи не може бути застосований, тому права частина збігу з шаблоном недосяжна. У Haskell вам потрібно використовувати a, !щоб забезпечити це, і GHC, ймовірно, не помітить, що шлях недоступний.
dfeuer

як щодо Агди? ліниво, але чи є _|_? і чи страждає воно від того самого обмеження тоді?
Ерік Каплун,

agda є, загалом кажучи, загальним, тому порядок оцінки недоступний. Немає закритого терміну agda порожнього типу, якщо ви не вимкнете перевірку припинення або щось подібне
Philip JF

58

Розглянемо це подання для лямбда-членів, параметризованих їх вільними змінними. (Див. Статті Bellegarde and Hook 1994, Bird and Paterson 1999, Altenkirch and Reus 1999.)

data Tm a  = Var a
           | Tm a :$ Tm a
           | Lam (Tm (Maybe a))

Ви, безумовно, можете зробити це Functor, захопивши поняття перейменування та Monadзахопивши поняття заміни.

instance Functor Tm where
  fmap rho (Var a)   = Var (rho a)
  fmap rho (f :$ s)  = fmap rho f :$ fmap rho s
  fmap rho (Lam t)   = Lam (fmap (fmap rho) t)

instance Monad Tm where
  return = Var
  Var a     >>= sig  = sig a
  (f :$ s)  >>= sig  = (f >>= sig) :$ (s >>= sig)
  Lam t     >>= sig  = Lam (t >>= maybe (Var Nothing) (fmap Just . sig))

А тепер розглянемо закриті терміни: це жителі Росії Tm Void. Ви повинні мати можливість вставляти закриті терміни в терміни з довільними вільними змінними. Як?

fmap absurd :: Tm Void -> Tm a

Суть, звичайно, полягає в тому, що ця функція буде перетинати термін, не роблячи абсолютно нічого. Але це штрих більш чесний, ніж unsafeCoerce. І ось чому vacuousбуло додано до Data.Void...

Або напишіть оцінювача. Ось значення з вільними змінними в b.

data Val b
  =  b :$$ [Val b]                              -- a stuck application
  |  forall a. LV (a -> Val b) (Tm (Maybe a))   -- we have an incomplete environment

Я щойно представляв лямбди як закриття. Оцінювач параметризується середовищем, що відображає вільні змінні у aзначення над b.

eval :: (a -> Val b) -> Tm a -> Val b
eval g (Var a)   = g a
eval g (f :$ s)  = eval g f $$ eval g s where
  (b :$$ vs)  $$ v  = b :$$ (vs ++ [v])         -- stuck application gets longer
  LV g t      $$ v  = eval (maybe v g) t        -- an applied lambda gets unstuck
eval g (Lam t)   = LV g t

Ви здогадалися. Оцінити закритий термін для будь-якої цілі

eval absurd :: Tm Void -> Val b

Більш загально, Voidвін рідко використовується самостійно, але є зручним, коли потрібно створити екземпляр параметра типу таким чином, що вказує на якусь неможливість (наприклад, тут, використовуючи вільну змінну в закритому терміні). Часто ці параметризрвані типи поставляються з функціями вищого порядку підйомних операцій за параметрами операцій по всьому типу (наприклад, тут, fmap, >>=, eval). Отже, ви переходите absurdяк операцію загального призначення Void.

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

either absurd id :: Either Void v -> v

бігти безпечно або

either absurd Right :: Either Void v -> Either e v

вбудовувати безпечні компоненти в небезпечний світ.

О, і останній ура, обробка "не може статися". Це відображається у загальній конструкції блискавки скрізь, де курсором бути не може.

class Differentiable f where
  type D f :: * -> *              -- an f with a hole
  plug :: (D f x, x) -> f x       -- plugging a child in the hole

newtype K a     x  = K a          -- no children, just a label
newtype I       x  = I x          -- one child
data (f :+: g)  x  = L (f x)      -- choice
                   | R (g x)
data (f :*: g)  x  = f x :&: g x  -- pairing

instance Differentiable (K a) where
  type D (K a) = K Void           -- no children, so no way to make a hole
  plug (K v, x) = absurd v        -- can't reinvent the label, so deny the hole!

Я вирішив не видаляти решту, хоча це не зовсім актуально.

instance Differentiable I where
  type D I = K ()
  plug (K (), x) = I x

instance (Differentiable f, Differentiable g) => Differentiable (f :+: g) where
  type D (f :+: g) = D f :+: D g
  plug (L df, x) = L (plug (df, x))
  plug (R dg, x) = R (plug (dg, x))

instance (Differentiable f, Differentiable g) => Differentiable (f :*: g) where
  type D (f :*: g) = (D f :*: g) :+: (f :*: D g)
  plug (L (df :&: g), x) = plug (df, x) :&: g
  plug (R (f :&: dg), x) = f :&: plug (dg, x)

Насправді, можливо, це актуально. Якщо ви відчуваєте авантюру, ця незакінчена стаття показує, як використовувати Voidстиснення подання термінів із вільними змінними

data Term f x = Var x | Con (f (Term f x))   -- the Free monad, yet again

у будь-якому синтаксисі, вільно сформованому з а Differentiableта Traversableфунктора f. Ми використовуємо Term f Voidдля представлення областей без вільних змінних та [D f (Term f Void)]для представлення труб, що проходять через області без вільних змінних, або до ізольованої вільної змінної, або до місця з'єднання на шляхах до двох або більше вільних змінних. Потрібно колись закінчити цю статтю.

Для типу без цінностей (або, принаймні, нічого, про що не варто говорити у чемній компанії), Voidнадзвичайно корисно. І absurdяк ви ним користуєтесь.


Чи forall f. vacuous f = unsafeCoerce fбуде дійсним правилом перезапису GHC?
Кактус

1
@Cactus, не дуже. Підроблені Functorекземпляри можуть бути GADT, які насправді не є чимось на зразок функторів.
dfeuer

Чи Functorне порушить це fmap id = idправило? Або це ви маєте на увазі під цим словом "підставний"?
Кактус

35

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

Це точно правильно.

Можна сказати, absurdце не корисніше, ніж const (error "Impossible"). Однак це тип обмежений, так що його єдиним входом може бути щось типу Void, тип даних, який навмисно залишається незаселеним. Це означає, що немає фактичного значення, на яке ви можете перейти absurd. Якщо ви коли-небудь потрапляєте в гілку коду, де програма перевірки типу думає, що у вас є доступ до чогось типу Void, то, ну, ви потрапили в абсурдну ситуацію. Отже, ви просто використовуєте, absurdщоб в основному позначити, що ця гілка коду ніколи не повинна бути досягнута.

"Ex falso quodlibet" буквально означає "з [а] помилкової [пропозиції] все, що слідує". Отже, коли ви виявляєте, що у вас є частина даних, якого типу Void, ви знаєте, що у вас є неправдиві докази. Отже, ви можете заповнити будь-яку діру, яку хочете (через absurd), оскільки з помилкової заяви все що завгодно випливає.

Я написав допис у блозі про ідеї Conduit, в якому є приклад використання absurd.

http://unknownparallel.wordpress.com/2012/07/30/pipes-to-conduits-part-6-leftovers/#running-a-pipeline


13

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

data RuleSet a            = Known !a | Unknown String
data GoRuleChoices        = Japanese | Chinese
type LinesOfActionChoices = Void
type GoRuleSet            = RuleSet GoRuleChoices
type LinesOfActionRuleSet = RuleSet LinesOfActionChoices

Тоді ви можете використовувати ось absurdтак, наприклад:

handleLOARules :: (String -> a) -> LinesOfActionsRuleSet -> a
handleLOARules f r = case r of
    Known   a -> absurd a
    Unknown s -> f s

13

Існують різні способи представлення порожнього типу даних . Один - це порожній алгебраїчний тип даних. Інший спосіб - зробити його псевдонімом для ∀α.α або

type Void' = forall a . a

в Haskell - ось як ми можемо кодувати його в Системі F (див. Главу 11 Докази та типи ). Ці два опису, звичайно , ізоморфні і ізоморфізм засвідчено \x -> x :: (forall a.a) -> Voidі absurd :: Void -> a.

У деяких випадках ми віддаємо перевагу явному варіанту, як правило, якщо порожній тип даних з'являється в аргументі функції або в більш складному типі даних, наприклад у Data.Conduit :

type Sink i m r = Pipe i i Void () m r

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

absurd виникає, коли ми перетворюємо між цими двома поданнями.


Наприклад, callcc :: ((a -> m b) -> m a) -> m aвикористовує (неявний) forall b. Це може бути як тип ((a -> m Void) -> m a) -> m a, оскільки виклик до континенції насправді не повертається, він передає управління в іншу точку. Якби ми хотіли працювати з продовженнями, ми могли б визначити

type Continuation r a = a -> Cont r Void

(Ми могли б використовувати, type Continuation' r a = forall b . a -> Cont r bале для цього потрібні типи 2-го рівня.) А потім vacuousMперетворює це Cont r Voidна Cont r b.

(Також зверніть увагу, що ви можете використовувати haskellers.com для пошуку використання (зворотних залежностей) певного пакету, наприклад, щоб побачити, хто і як використовує пакет void .)


TypeApplicationsможе бути використано , щоб бути більш чітко про деталі proof :: (forall a. a) -> Void: proof fls = fls @Void.
Iceland_jack

1

У таких залежних мовах, як Idris, це, мабуть, корисніше, ніж у Haskell. Як правило, у загальній функції, коли шаблон відповідає значенню, яке насправді неможливо вставити у функцію, тоді ви створюєте значення незаселеного типу та використовуєте absurdдля остаточного визначення справи.

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

shrink : (xs : Vect (S n) a) -> Elem x xs -> Vect n a
shrink (x :: ys) Here = ys
shrink (y :: []) (There p) = absurd p
shrink (y :: (x :: xs)) (There p) = y :: shrink (x :: xs) p

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

З точки зору Каррі-Говарда, де є пропозиції, тоді absurdце своєрідний КЕД як доказ протиріччя.

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