it-roy-ru.com

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

Функция 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.

Что меня озадачивает и что мне интересно: в каких практических задачах программирования эта функция полезна? Я думаю, что, возможно, в некоторых случаях он полезен как типобезопасный способ исчерпывающей обработки случаев «не может произойти», но я недостаточно знаю о практическом использовании Curry-Howard, чтобы определить, присутствует ли эта идея в правильный путь на всех.

Правка: Примеры желательно на Haskell, но если кто-то хочет использовать зависимый типизированный язык, я не собираюсь жаловаться ...

87
Luis Casillas

Жизнь немного сложна, поскольку Хаскелл не строг. Общий вариант использования - обработка невозможных путей. Например

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

это действительно никогда не дает. Следствием этого является то, что правильное правило сгиба для 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

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

newtype CPS a = Continuation (Continuation a)

поскольку Хаскелл чист, мы не можем ничего получить из этого типа.

53
Philip JF

Рассмотрим это представление для лямбда-членов, параметризованных их свободными переменными. (См. Статьи 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))

Теперь рассмотрим термины closed: это жители 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 и Traversablef. Мы используем Term f Void для представления областей без свободных переменных и [D f (Term f Void)] для представления tube, туннелирующих через области без свободных переменных либо к изолированной свободной переменной, либо к соединению в путях к двум или более свободным переменным. Должен закончить эту статью когда-нибудь.

Для типа без значений (или, по крайней мере, без упоминания в вежливой компании) Void является чрезвычайно полезным. И absurd, как вы его используете.

58
pigworker

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

Это точно верно.

Можно сказать, что absurd не более полезен, чем const (error "Impossible"). Однако он ограничен по типу, поэтому его единственным вводом может быть что-то типа Void, тип данных, который намеренно оставлен необитаемым. Это означает, что нет фактического значения, которое вы можете передать absurd. Если вы когда-нибудь окажетесь в ветке кода, где средство проверки типов считает, что у вас есть доступ к чему-то типа Void, тогда вы попадаете в ситуацию absurd. Таким образом, вы просто используете absurd, чтобы отметить, что эта ветвь кода никогда не должна быть достигнута.

«Ex falso quodlibet» буквально означает «из [ложного] [суждения], что следует». Поэтому, когда вы обнаружите, что у вас есть часть данных, тип которой Void, вы знаете, что у вас в руках ложные доказательства. Поэтому вы можете заполнить любую дыру по вашему желанию (через absurd), потому что из ложного предложения следует что угодно.

Я написал сообщение в блоге об идеях, лежащих в основе Conduit, в котором есть пример использования absurd

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

34
Dan Burton

Как правило, вы можете использовать его, чтобы избежать частичного совпадения с образцом. Например, получение аппроксимации объявлений типов данных из этот ответ :

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
12
Daniel Wagner

Есть разные способы представления пустого типа данных . Одним из них является пустой алгебраический тип данных. Другой способ - сделать его псевдонимом для ∀α.α или

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.)

10
Petr Pudlák

В языках с зависимой типизацией, таких как 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 является своего рода КЭД в доказательстве от противного.

0
user1747134